diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /layout/forms | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'layout/forms')
181 files changed, 17925 insertions, 0 deletions
diff --git a/layout/forms/HTMLSelectEventListener.cpp b/layout/forms/HTMLSelectEventListener.cpp new file mode 100644 index 0000000000..dffabe1bc7 --- /dev/null +++ b/layout/forms/HTMLSelectEventListener.cpp @@ -0,0 +1,851 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include "HTMLSelectEventListener.h" + +#include "nsListControlFrame.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/MouseEvent.h" +#include "mozilla/Casting.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/TextEvents.h" +#include "mozilla/PresShell.h" +#include "mozilla/dom/HTMLSelectElement.h" +#include "mozilla/dom/HTMLOptionElement.h" +#include "mozilla/StaticPrefs_ui.h" +#include "mozilla/ClearOnShutdown.h" + +using namespace mozilla; +using namespace mozilla::dom; + +static bool IsOptionInteractivelySelectable(HTMLSelectElement& aSelect, + HTMLOptionElement& aOption, + bool aIsCombobox) { + if (aSelect.IsOptionDisabled(&aOption)) { + return false; + } + if (!aIsCombobox) { + return aOption.GetPrimaryFrame(); + } + // In dropdown mode no options have frames, but we can check whether they + // are rendered / not in a display: none subtree. + if (!aOption.HasServoData() || Servo_Element_IsDisplayNone(&aOption)) { + return false; + } + // TODO(emilio): This is a bit silly and doesn't match the options that we + // show / don't show in the dropdown, but matches the frame construction we + // do for multiple selects. For backwards compat also don't allow selecting + // options in a display: contents subtree interactively. + // test_select_key_navigation_bug1498769.html tests for this and should + // probably be changed (and this loop removed) or alternatively + // SelectChild.jsm should be changed to match it. + for (Element* el = &aOption; el && el != &aSelect; + el = el->GetParentElement()) { + if (Servo_Element_IsDisplayContents(el)) { + return false; + } + } + return true; +} + +namespace mozilla { + +static StaticAutoPtr<nsString> sIncrementalString; +static TimeStamp gLastKeyTime; +static uintptr_t sLastKeyListener = 0; +static constexpr int32_t kNothingSelected = -1; + +static nsString& GetIncrementalString() { + MOZ_ASSERT(sLastKeyListener != 0); + if (!sIncrementalString) { + sIncrementalString = new nsString(); + ClearOnShutdown(&sIncrementalString); + } + return *sIncrementalString; +} + +class MOZ_RAII AutoIncrementalSearchHandler { + public: + explicit AutoIncrementalSearchHandler(HTMLSelectEventListener& aListener) { + if (sLastKeyListener != uintptr_t(&aListener)) { + sLastKeyListener = uintptr_t(&aListener); + GetIncrementalString().Truncate(); + // To make it easier to handle time comparisons in the other methods, + // initialize gLastKeyTime to a value in the past. + gLastKeyTime = TimeStamp::Now() - + TimeDuration::FromMilliseconds( + StaticPrefs::ui_menu_incremental_search_timeout() * 2); + } + } + ~AutoIncrementalSearchHandler() { + if (!mResettingCancelled) { + GetIncrementalString().Truncate(); + } + } + void CancelResetting() { mResettingCancelled = true; } + + private: + bool mResettingCancelled = false; +}; + +NS_IMPL_ISUPPORTS(HTMLSelectEventListener, nsIMutationObserver, + nsIDOMEventListener) + +HTMLSelectEventListener::~HTMLSelectEventListener() { + if (sLastKeyListener == uintptr_t(this)) { + sLastKeyListener = 0; + } +} + +nsListControlFrame* HTMLSelectEventListener::GetListControlFrame() const { + if (mIsCombobox) { + MOZ_ASSERT(!mElement->GetPrimaryFrame() || + !mElement->GetPrimaryFrame()->IsListControlFrame()); + return nullptr; + } + return do_QueryFrame(mElement->GetPrimaryFrame()); +} + +int32_t HTMLSelectEventListener::GetEndSelectionIndex() const { + if (auto* lf = GetListControlFrame()) { + return lf->GetEndSelectionIndex(); + } + // Combobox selects only have one selected index, so the end and start is the + // same. + return mElement->SelectedIndex(); +} + +bool HTMLSelectEventListener::IsOptionInteractivelySelectable( + uint32_t aIndex) const { + HTMLOptionElement* option = mElement->Item(aIndex); + return option && + ::IsOptionInteractivelySelectable(*mElement, *option, mIsCombobox); +} + +//--------------------------------------------------------------------- +// Ok, the entire idea of this routine is to move to the next item that +// is suppose to be selected. If the item is disabled then we search in +// the same direction looking for the next item to select. If we run off +// the end of the list then we start at the end of the list and search +// backwards until we get back to the original item or an enabled option +// +// aStartIndex - the index to start searching from +// aNewIndex - will get set to the new index if it finds one +// aNumOptions - the total number of options in the list +// aDoAdjustInc - the initial index increment / decrement +// aDoAdjustIncNext - the subsequent index increment/decrement used to search +// for the next enabled option +// +// the aDoAdjustInc could be a "1" for a single item or +// any number greater representing a page of items +// +void HTMLSelectEventListener::AdjustIndexForDisabledOpt( + int32_t aStartIndex, int32_t& aNewIndex, int32_t aNumOptions, + int32_t aDoAdjustInc, int32_t aDoAdjustIncNext) { + // Cannot select anything if there is nothing to select + if (aNumOptions == 0) { + aNewIndex = kNothingSelected; + return; + } + + // means we reached the end of the list and now we are searching backwards + bool doingReverse = false; + // lowest index in the search range + int32_t bottom = 0; + // highest index in the search range + int32_t top = aNumOptions; + + // Start off keyboard options at selectedIndex if nothing else is defaulted to + // + // XXX Perhaps this should happen for mouse too, to start off shift click + // automatically in multiple ... to do this, we'd need to override + // OnOptionSelected and set mStartSelectedIndex if nothing is selected. Not + // sure of the effects, though, so I'm not doing it just yet. + int32_t startIndex = aStartIndex; + if (startIndex < bottom) { + startIndex = mElement->SelectedIndex(); + } + int32_t newIndex = startIndex + aDoAdjustInc; + + // make sure we start off in the range + if (newIndex < bottom) { + newIndex = 0; + } else if (newIndex >= top) { + newIndex = aNumOptions - 1; + } + + while (true) { + // if the newIndex is selectable, we are golden, bail out + if (IsOptionInteractivelySelectable(newIndex)) { + break; + } + + // it WAS disabled, so sart looking ahead for the next enabled option + newIndex += aDoAdjustIncNext; + + // well, if we reach end reverse the search + if (newIndex < bottom) { + if (doingReverse) { + return; // if we are in reverse mode and reach the end bail out + } + // reset the newIndex to the end of the list we hit + // reverse the incrementer + // set the other end of the list to our original starting index + newIndex = bottom; + aDoAdjustIncNext = 1; + doingReverse = true; + top = startIndex; + } else if (newIndex >= top) { + if (doingReverse) { + return; // if we are in reverse mode and reach the end bail out + } + // reset the newIndex to the end of the list we hit + // reverse the incrementer + // set the other end of the list to our original starting index + newIndex = top - 1; + aDoAdjustIncNext = -1; + doingReverse = true; + bottom = startIndex; + } + } + + // Looks like we found one + aNewIndex = newIndex; +} + +NS_IMETHODIMP +HTMLSelectEventListener::HandleEvent(dom::Event* aEvent) { + nsAutoString eventType; + aEvent->GetType(eventType); + if (eventType.EqualsLiteral("keydown")) { + return KeyDown(aEvent); + } + if (eventType.EqualsLiteral("keypress")) { + return KeyPress(aEvent); + } + if (eventType.EqualsLiteral("mousedown")) { + if (aEvent->DefaultPrevented()) { + return NS_OK; + } + return MouseDown(aEvent); + } + if (eventType.EqualsLiteral("mouseup")) { + // Don't try to honor defaultPrevented here - it's not web compatible. + // (bug 1194733) + return MouseUp(aEvent); + } + if (eventType.EqualsLiteral("mousemove")) { + // I don't think we want to honor defaultPrevented on mousemove + // in general, and it would only prevent highlighting here. + return MouseMove(aEvent); + } + + MOZ_ASSERT_UNREACHABLE("Unexpected eventType"); + return NS_OK; +} + +void HTMLSelectEventListener::Attach() { + mElement->AddSystemEventListener(u"keydown"_ns, this, false, false); + mElement->AddSystemEventListener(u"keypress"_ns, this, false, false); + mElement->AddSystemEventListener(u"mousedown"_ns, this, false, false); + mElement->AddSystemEventListener(u"mouseup"_ns, this, false, false); + mElement->AddSystemEventListener(u"mousemove"_ns, this, false, false); + + if (mIsCombobox) { + mElement->AddMutationObserver(this); + } +} + +void HTMLSelectEventListener::Detach() { + mElement->RemoveSystemEventListener(u"keydown"_ns, this, false); + mElement->RemoveSystemEventListener(u"keypress"_ns, this, false); + mElement->RemoveSystemEventListener(u"mousedown"_ns, this, false); + mElement->RemoveSystemEventListener(u"mouseup"_ns, this, false); + mElement->RemoveSystemEventListener(u"mousemove"_ns, this, false); + + if (mIsCombobox) { + mElement->RemoveMutationObserver(this); + if (mElement->OpenInParentProcess()) { + nsContentUtils::AddScriptRunner(NS_NewRunnableFunction( + "HTMLSelectEventListener::Detach", [element = mElement] { + // Don't hide the dropdown if the element has another frame already, + // this prevents closing dropdowns on reframe, see bug 1440506. + // + // FIXME(emilio): The flush is needed to deal with reframes started + // from DOM node removal. But perhaps we can be a bit smarter here. + if (!element->IsCombobox() || + !element->GetPrimaryFrame(FlushType::Frames)) { + nsContentUtils::DispatchChromeEvent( + element->OwnerDoc(), ToSupports(element.get()), + u"mozhidedropdown"_ns, CanBubble::eYes, Cancelable::eNo); + } + })); + } + } +} + +const uint32_t kMaxDropdownRows = 20; // matches the setting for 4.x browsers + +int32_t HTMLSelectEventListener::ItemsPerPage() const { + uint32_t size = [&] { + if (mIsCombobox) { + return kMaxDropdownRows; + } + if (auto* lf = GetListControlFrame()) { + return lf->GetNumDisplayRows(); + } + return mElement->Size(); + }(); + if (size <= 1) { + return 1; + } + if (MOZ_UNLIKELY(size > INT32_MAX)) { + return INT32_MAX - 1; + } + return AssertedCast<int32_t>(size - 1u); +} + +void HTMLSelectEventListener::OptionValueMightHaveChanged( + nsIContent* aMutatingNode) { +#ifdef ACCESSIBILITY + if (nsAccessibilityService* acc = GetAccService()) { + acc->ComboboxOptionMaybeChanged(mElement->OwnerDoc()->GetPresShell(), + aMutatingNode); + } +#endif +} + +void HTMLSelectEventListener::AttributeChanged(dom::Element* aElement, + int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType, + const nsAttrValue* aOldValue) { + if (aElement->IsHTMLElement(nsGkAtoms::option) && + aNameSpaceID == kNameSpaceID_None && aAttribute == nsGkAtoms::label) { + // A11y has its own mutation listener for this so no need to do + // OptionValueMightHaveChanged(). + ComboboxMightHaveChanged(); + } +} + +void HTMLSelectEventListener::CharacterDataChanged( + nsIContent* aContent, const CharacterDataChangeInfo&) { + if (nsContentUtils::IsInSameAnonymousTree(mElement, aContent)) { + OptionValueMightHaveChanged(aContent); + ComboboxMightHaveChanged(); + } +} + +void HTMLSelectEventListener::ContentRemoved(nsIContent* aChild, + nsIContent* aPreviousSibling) { + if (nsContentUtils::IsInSameAnonymousTree(mElement, aChild)) { + OptionValueMightHaveChanged(aChild); + ComboboxMightHaveChanged(); + } +} + +void HTMLSelectEventListener::ContentAppended(nsIContent* aFirstNewContent) { + if (nsContentUtils::IsInSameAnonymousTree(mElement, aFirstNewContent)) { + OptionValueMightHaveChanged(aFirstNewContent); + ComboboxMightHaveChanged(); + } +} + +void HTMLSelectEventListener::ContentInserted(nsIContent* aChild) { + if (nsContentUtils::IsInSameAnonymousTree(mElement, aChild)) { + OptionValueMightHaveChanged(aChild); + ComboboxMightHaveChanged(); + } +} + +void HTMLSelectEventListener::ComboboxMightHaveChanged() { + if (nsIFrame* f = mElement->GetPrimaryFrame()) { + PresShell* ps = f->PresShell(); + // nsComoboxControlFrame::Reflow updates the selected text. AddOption / + // RemoveOption / etc takes care of keeping the displayed index up to date. + ps->FrameNeedsReflow(f, IntrinsicDirty::FrameAncestorsAndDescendants, + NS_FRAME_IS_DIRTY); +#ifdef ACCESSIBILITY + if (nsAccessibilityService* acc = GetAccService()) { + acc->ScheduleAccessibilitySubtreeUpdate(ps, mElement); + } +#endif + } +} + +void HTMLSelectEventListener::FireOnInputAndOnChange() { + RefPtr<HTMLSelectElement> element = mElement; + // Dispatch the input event. + DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(element); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "Failed to dispatch input event"); + + // Dispatch the change event. + nsContentUtils::DispatchTrustedEvent(element->OwnerDoc(), ToSupports(element), + u"change"_ns, CanBubble::eYes, + Cancelable::eNo); +} + +static void FireDropDownEvent(HTMLSelectElement* aElement, bool aShow, + bool aIsSourceTouchEvent) { + const auto eventName = [&] { + if (aShow) { + return aIsSourceTouchEvent ? u"mozshowdropdown-sourcetouch"_ns + : u"mozshowdropdown"_ns; + } + return u"mozhidedropdown"_ns; + }(); + nsContentUtils::DispatchChromeEvent(aElement->OwnerDoc(), + ToSupports(aElement), eventName, + CanBubble::eYes, Cancelable::eNo); +} + +nsresult HTMLSelectEventListener::MouseDown(dom::Event* aMouseEvent) { + NS_ASSERTION(aMouseEvent != nullptr, "aMouseEvent is null."); + + MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent(); + NS_ENSURE_TRUE(mouseEvent, NS_ERROR_FAILURE); + + if (mElement->State().HasState(ElementState::DISABLED)) { + return NS_OK; + } + + // only allow selection with the left button + // if a right button click is on the combobox itself + // or on the select when in listbox mode, then let the click through + const bool isLeftButton = mouseEvent->Button() == 0; + if (!isLeftButton) { + return NS_OK; + } + + if (mIsCombobox) { + uint16_t inputSource = mouseEvent->MozInputSource(); + if (mElement->OpenInParentProcess()) { + nsCOMPtr<nsIContent> target = do_QueryInterface(aMouseEvent->GetTarget()); + if (target && target->IsHTMLElement(nsGkAtoms::option)) { + return NS_OK; + } + } + + const bool isSourceTouchEvent = + inputSource == MouseEvent_Binding::MOZ_SOURCE_TOUCH; + FireDropDownEvent(mElement, !mElement->OpenInParentProcess(), + isSourceTouchEvent); + return NS_OK; + } + + if (nsListControlFrame* list = GetListControlFrame()) { + mButtonDown = true; + return list->HandleLeftButtonMouseDown(aMouseEvent); + } + return NS_OK; +} + +nsresult HTMLSelectEventListener::MouseUp(dom::Event* aMouseEvent) { + NS_ASSERTION(aMouseEvent != nullptr, "aMouseEvent is null."); + + MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent(); + NS_ENSURE_TRUE(mouseEvent, NS_ERROR_FAILURE); + + mButtonDown = false; + + if (mElement->State().HasState(ElementState::DISABLED)) { + return NS_OK; + } + + if (nsListControlFrame* lf = GetListControlFrame()) { + lf->CaptureMouseEvents(false); + } + + // only allow selection with the left button + // if a right button click is on the combobox itself + // or on the select when in listbox mode, then let the click through + const bool isLeftButton = mouseEvent->Button() == 0; + if (!isLeftButton) { + return NS_OK; + } + + if (nsListControlFrame* lf = GetListControlFrame()) { + return lf->HandleLeftButtonMouseUp(aMouseEvent); + } + + return NS_OK; +} + +nsresult HTMLSelectEventListener::MouseMove(dom::Event* aMouseEvent) { + NS_ASSERTION(aMouseEvent != nullptr, "aMouseEvent is null."); + + MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent(); + NS_ENSURE_TRUE(mouseEvent, NS_ERROR_FAILURE); + + if (!mButtonDown) { + return NS_OK; + } + + if (nsListControlFrame* lf = GetListControlFrame()) { + return lf->DragMove(aMouseEvent); + } + + return NS_OK; +} + +nsresult HTMLSelectEventListener::KeyPress(dom::Event* aKeyEvent) { + MOZ_ASSERT(aKeyEvent, "aKeyEvent is null."); + + if (mElement->State().HasState(ElementState::DISABLED)) { + return NS_OK; + } + + AutoIncrementalSearchHandler incrementalHandler(*this); + + const WidgetKeyboardEvent* keyEvent = + aKeyEvent->WidgetEventPtr()->AsKeyboardEvent(); + MOZ_ASSERT(keyEvent, + "DOM event must have WidgetKeyboardEvent for its internal event"); + + // Select option with this as the first character + // XXX Not I18N compliant + + // Don't do incremental search if the key event has already consumed. + if (keyEvent->DefaultPrevented()) { + return NS_OK; + } + + if (keyEvent->IsAlt()) { + return NS_OK; + } + + // With some keyboard layout, space key causes non-ASCII space. + // So, the check in keydown event handler isn't enough, we need to check it + // again with keypress event. + if (keyEvent->mCharCode != ' ') { + mControlSelectMode = false; + } + + bool isControlOrMeta = keyEvent->IsControl() || keyEvent->IsMeta(); + if (isControlOrMeta && keyEvent->mCharCode != ' ') { + return NS_OK; + } + + // NOTE: If mKeyCode of keypress event is not 0, mCharCode is always 0. + // Therefore, all non-printable keys are not handled after this block. + if (!keyEvent->mCharCode) { + // Backspace key will delete the last char in the string. Otherwise, + // non-printable keypress should reset incremental search. + if (keyEvent->mKeyCode == NS_VK_BACK) { + incrementalHandler.CancelResetting(); + if (!GetIncrementalString().IsEmpty()) { + GetIncrementalString().Truncate(GetIncrementalString().Length() - 1); + } + aKeyEvent->PreventDefault(); + } else { + // XXX When a select element has focus, even if the key causes nothing, + // it might be better to call preventDefault() here because nobody + // should expect one of other elements including chrome handles the + // key event. + } + return NS_OK; + } + + incrementalHandler.CancelResetting(); + + // We ate the key if we got this far. + aKeyEvent->PreventDefault(); + + // XXX Why don't we check/modify timestamp first? + + // Incremental Search: if time elapsed is below + // ui.menu.incremental_search.timeout, append this keystroke to the search + // string we will use to find options and start searching at the current + // keystroke. Otherwise, Truncate the string if it's been a long time + // since our last keypress. + if ((keyEvent->mTimeStamp - gLastKeyTime).ToMilliseconds() > + StaticPrefs::ui_menu_incremental_search_timeout()) { + // If this is ' ' and we are at the beginning of the string, treat it as + // "select this option" (bug 191543) + if (keyEvent->mCharCode == ' ') { + // Actually process the new index and let the selection code + // do the scrolling for us + PostHandleKeyEvent(GetEndSelectionIndex(), keyEvent->mCharCode, + keyEvent->IsShift(), isControlOrMeta); + + return NS_OK; + } + + GetIncrementalString().Truncate(); + } + + gLastKeyTime = keyEvent->mTimeStamp; + + // Append this keystroke to the search string. + char16_t uniChar = ToLowerCase(static_cast<char16_t>(keyEvent->mCharCode)); + GetIncrementalString().Append(uniChar); + + // See bug 188199, if all letters in incremental string are same, just try to + // match the first one + nsAutoString incrementalString(GetIncrementalString()); + uint32_t charIndex = 1, stringLength = incrementalString.Length(); + while (charIndex < stringLength && + incrementalString[charIndex] == incrementalString[charIndex - 1]) { + charIndex++; + } + if (charIndex == stringLength) { + incrementalString.Truncate(1); + stringLength = 1; + } + + // Determine where we're going to start reading the string + // If we have multiple characters to look for, we start looking *at* the + // current option. If we have only one character to look for, we start + // looking *after* the current option. + // Exception: if there is no option selected to start at, we always start + // *at* 0. + int32_t startIndex = mElement->SelectedIndex(); + if (startIndex == kNothingSelected) { + startIndex = 0; + } else if (stringLength == 1) { + startIndex++; + } + + // now make sure there are options or we are wasting our time + RefPtr<dom::HTMLOptionsCollection> options = mElement->Options(); + uint32_t numOptions = options->Length(); + + for (uint32_t i = 0; i < numOptions; ++i) { + uint32_t index = (i + startIndex) % numOptions; + RefPtr<dom::HTMLOptionElement> optionElement = options->ItemAsOption(index); + if (!optionElement || !::IsOptionInteractivelySelectable( + *mElement, *optionElement, mIsCombobox)) { + continue; + } + + nsAutoString text; + optionElement->GetRenderedLabel(text); + if (!StringBeginsWith( + nsContentUtils::TrimWhitespace< + nsContentUtils::IsHTMLWhitespaceOrNBSP>(text, false), + incrementalString, nsCaseInsensitiveStringComparator)) { + continue; + } + + if (mIsCombobox) { + if (optionElement->Selected()) { + return NS_OK; + } + optionElement->SetSelected(true); + FireOnInputAndOnChange(); + return NS_OK; + } + + if (nsListControlFrame* lf = GetListControlFrame()) { + bool wasChanged = + lf->PerformSelection(index, keyEvent->IsShift(), isControlOrMeta); + if (!wasChanged) { + return NS_OK; + } + FireOnInputAndOnChange(); + } + break; + } + + return NS_OK; +} + +nsresult HTMLSelectEventListener::KeyDown(dom::Event* aKeyEvent) { + MOZ_ASSERT(aKeyEvent, "aKeyEvent is null."); + + if (mElement->State().HasState(ElementState::DISABLED)) { + return NS_OK; + } + + AutoIncrementalSearchHandler incrementalHandler(*this); + + if (aKeyEvent->DefaultPrevented()) { + return NS_OK; + } + + const WidgetKeyboardEvent* keyEvent = + aKeyEvent->WidgetEventPtr()->AsKeyboardEvent(); + MOZ_ASSERT(keyEvent, + "DOM event must have WidgetKeyboardEvent for its internal event"); + + bool dropDownMenuOnUpDown; + bool dropDownMenuOnSpace; +#ifdef XP_MACOSX + dropDownMenuOnUpDown = mIsCombobox && !mElement->OpenInParentProcess(); + dropDownMenuOnSpace = mIsCombobox && !keyEvent->IsAlt() && + !keyEvent->IsControl() && !keyEvent->IsMeta(); +#else + dropDownMenuOnUpDown = mIsCombobox && keyEvent->IsAlt(); + dropDownMenuOnSpace = mIsCombobox && !mElement->OpenInParentProcess(); +#endif + bool withinIncrementalSearchTime = + (keyEvent->mTimeStamp - gLastKeyTime).ToMilliseconds() <= + StaticPrefs::ui_menu_incremental_search_timeout(); + if ((dropDownMenuOnUpDown && + (keyEvent->mKeyCode == NS_VK_UP || keyEvent->mKeyCode == NS_VK_DOWN)) || + (dropDownMenuOnSpace && keyEvent->mKeyCode == NS_VK_SPACE && + !withinIncrementalSearchTime)) { + FireDropDownEvent(mElement, !mElement->OpenInParentProcess(), false); + aKeyEvent->PreventDefault(); + return NS_OK; + } + if (keyEvent->IsAlt()) { + return NS_OK; + } + + // We should not change the selection if the popup is "opened in the parent + // process" (even when we're in single-process mode). + const bool shouldSelect = !mIsCombobox || !mElement->OpenInParentProcess(); + + // now make sure there are options or we are wasting our time + RefPtr<dom::HTMLOptionsCollection> options = mElement->Options(); + uint32_t numOptions = options->Length(); + + // this is the new index to set + int32_t newIndex = kNothingSelected; + + bool isControlOrMeta = keyEvent->IsControl() || keyEvent->IsMeta(); + // Don't try to handle multiple-select pgUp/pgDown in single-select lists. + if (isControlOrMeta && !mElement->Multiple() && + (keyEvent->mKeyCode == NS_VK_PAGE_UP || + keyEvent->mKeyCode == NS_VK_PAGE_DOWN)) { + return NS_OK; + } + if (isControlOrMeta && + (keyEvent->mKeyCode == NS_VK_UP || keyEvent->mKeyCode == NS_VK_LEFT || + keyEvent->mKeyCode == NS_VK_DOWN || keyEvent->mKeyCode == NS_VK_RIGHT || + keyEvent->mKeyCode == NS_VK_HOME || keyEvent->mKeyCode == NS_VK_END)) { + // Don't go into multiple-select mode unless this list can handle it. + isControlOrMeta = mControlSelectMode = mElement->Multiple(); + } else if (keyEvent->mKeyCode != NS_VK_SPACE) { + mControlSelectMode = false; + } + + switch (keyEvent->mKeyCode) { + case NS_VK_UP: + case NS_VK_LEFT: + if (shouldSelect) { + AdjustIndexForDisabledOpt(GetEndSelectionIndex(), newIndex, + int32_t(numOptions), -1, -1); + } + break; + case NS_VK_DOWN: + case NS_VK_RIGHT: + if (shouldSelect) { + AdjustIndexForDisabledOpt(GetEndSelectionIndex(), newIndex, + int32_t(numOptions), 1, 1); + } + break; + case NS_VK_RETURN: + // If this is single select listbox, Enter key doesn't cause anything. + if (!mElement->Multiple()) { + return NS_OK; + } + + newIndex = GetEndSelectionIndex(); + break; + case NS_VK_PAGE_UP: { + if (shouldSelect) { + AdjustIndexForDisabledOpt(GetEndSelectionIndex(), newIndex, + int32_t(numOptions), -ItemsPerPage(), -1); + } + break; + } + case NS_VK_PAGE_DOWN: { + if (shouldSelect) { + AdjustIndexForDisabledOpt(GetEndSelectionIndex(), newIndex, + int32_t(numOptions), ItemsPerPage(), 1); + } + break; + } + case NS_VK_HOME: + if (shouldSelect) { + AdjustIndexForDisabledOpt(0, newIndex, int32_t(numOptions), 0, 1); + } + break; + case NS_VK_END: + if (shouldSelect) { + AdjustIndexForDisabledOpt(int32_t(numOptions) - 1, newIndex, + int32_t(numOptions), 0, -1); + } + break; + default: // printable key will be handled by keypress event. + incrementalHandler.CancelResetting(); + return NS_OK; + } + + aKeyEvent->PreventDefault(); + + // Actually process the new index and let the selection code + // do the scrolling for us + PostHandleKeyEvent(newIndex, 0, keyEvent->IsShift(), isControlOrMeta); + return NS_OK; +} + +HTMLOptionElement* HTMLSelectEventListener::GetCurrentOption() const { + // The mEndSelectionIndex is what is currently being selected. Use + // the selected index if this is kNothingSelected. + int32_t endIndex = GetEndSelectionIndex(); + int32_t focusedIndex = + endIndex == kNothingSelected ? mElement->SelectedIndex() : endIndex; + if (focusedIndex != kNothingSelected) { + return mElement->Item(AssertedCast<uint32_t>(focusedIndex)); + } + + // There is no selected option. Return the first non-disabled option, if any. + return GetNonDisabledOptionFrom(0); +} + +HTMLOptionElement* HTMLSelectEventListener::GetNonDisabledOptionFrom( + int32_t aFromIndex, int32_t* aFoundIndex) const { + const uint32_t length = mElement->Length(); + for (uint32_t i = std::max(aFromIndex, 0); i < length; ++i) { + if (IsOptionInteractivelySelectable(i)) { + if (aFoundIndex) { + *aFoundIndex = i; + } + return mElement->Item(i); + } + } + return nullptr; +} + +void HTMLSelectEventListener::PostHandleKeyEvent(int32_t aNewIndex, + uint32_t aCharCode, + bool aIsShift, + bool aIsControlOrMeta) { + if (aNewIndex == kNothingSelected) { + int32_t endIndex = GetEndSelectionIndex(); + int32_t focusedIndex = + endIndex == kNothingSelected ? mElement->SelectedIndex() : endIndex; + if (focusedIndex != kNothingSelected) { + return; + } + // No options are selected. In this case the focus ring is on the first + // non-disabled option (if any), so we should behave as if that's the option + // the user acted on. + if (!GetNonDisabledOptionFrom(0, &aNewIndex)) { + return; + } + } + + if (mIsCombobox) { + RefPtr<HTMLOptionElement> newOption = mElement->Item(aNewIndex); + MOZ_ASSERT(newOption); + if (newOption->Selected()) { + return; + } + newOption->SetSelected(true); + FireOnInputAndOnChange(); + return; + } + if (nsListControlFrame* lf = GetListControlFrame()) { + lf->UpdateSelectionAfterKeyEvent(aNewIndex, aCharCode, aIsShift, + aIsControlOrMeta, mControlSelectMode); + } +} + +} // namespace mozilla diff --git a/layout/forms/HTMLSelectEventListener.h b/layout/forms/HTMLSelectEventListener.h new file mode 100644 index 0000000000..452610a217 --- /dev/null +++ b/layout/forms/HTMLSelectEventListener.h @@ -0,0 +1,103 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_HTMLSelectEventListener_h +#define mozilla_HTMLSelectEventListener_h + +#include "nsIDOMEventListener.h" +#include "nsStubMutationObserver.h" + +class nsIFrame; +class nsListControlFrame; + +namespace mozilla { + +namespace dom { +class HTMLSelectElement; +class HTMLOptionElement; +class Event; +} // namespace dom + +/** + * HTMLSelectEventListener + * This class is responsible for propagating events to the select element while + * it has a frame. + * Frames are not refcounted so they can't be used as event listeners. + */ + +class HTMLSelectEventListener final : public nsStubMutationObserver, + public nsIDOMEventListener { + public: + enum class SelectType : uint8_t { Listbox, Combobox }; + HTMLSelectEventListener(dom::HTMLSelectElement& aElement, + SelectType aSelectType) + : mElement(&aElement), mIsCombobox(aSelectType == SelectType::Combobox) { + Attach(); + } + + NS_DECL_ISUPPORTS + + // For comboboxes, we need to keep the list up to date when options change. + NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED + NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED + + // nsIDOMEventListener + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD HandleEvent(dom::Event*) override; + + void Attach(); + void Detach(); + + dom::HTMLOptionElement* GetCurrentOption() const; + + MOZ_CAN_RUN_SCRIPT void FireOnInputAndOnChange(); + + private: + // This is always guaranteed to be > 0, but callers want signed integers so we + // do the cast for them. + int32_t ItemsPerPage() const; + + nsListControlFrame* GetListControlFrame() const; + + MOZ_CAN_RUN_SCRIPT nsresult KeyDown(dom::Event*); + MOZ_CAN_RUN_SCRIPT nsresult KeyPress(dom::Event*); + MOZ_CAN_RUN_SCRIPT nsresult MouseDown(dom::Event*); + MOZ_CAN_RUN_SCRIPT nsresult MouseUp(dom::Event*); + MOZ_CAN_RUN_SCRIPT nsresult MouseMove(dom::Event*); + + void AdjustIndexForDisabledOpt(int32_t aStartIndex, int32_t& aNewIndex, + int32_t aNumOptions, int32_t aDoAdjustInc, + int32_t aDoAdjustIncNext); + bool IsOptionInteractivelySelectable(uint32_t aIndex) const; + int32_t GetEndSelectionIndex() const; + + MOZ_CAN_RUN_SCRIPT + void PostHandleKeyEvent(int32_t aNewIndex, uint32_t aCharCode, bool aIsShift, + bool aIsControlOrMeta); + + /** + * Return the first non-disabled option starting at aFromIndex (inclusive). + * @param aFoundIndex if non-null, set to the index of the returned option + */ + dom::HTMLOptionElement* GetNonDisabledOptionFrom( + int32_t aFromIndex, int32_t* aFoundIndex = nullptr) const; + + void ComboboxMightHaveChanged(); + void OptionValueMightHaveChanged(nsIContent* aMutatingNode); + + ~HTMLSelectEventListener(); + + RefPtr<dom::HTMLSelectElement> mElement; + const bool mIsCombobox; + bool mButtonDown = false; + bool mControlSelectMode = false; +}; + +} // namespace mozilla + +#endif // mozilla_HTMLSelectEventListener_h diff --git a/layout/forms/ListMutationObserver.cpp b/layout/forms/ListMutationObserver.cpp new file mode 100644 index 0000000000..13f9e367a0 --- /dev/null +++ b/layout/forms/ListMutationObserver.cpp @@ -0,0 +1,92 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include "ListMutationObserver.h" + +#include "mozilla/dom/HTMLInputElement.h" +#include "nsIFrame.h" + +namespace mozilla { +NS_IMPL_ISUPPORTS(ListMutationObserver, nsIMutationObserver) + +ListMutationObserver::~ListMutationObserver() = default; + +void ListMutationObserver::Attach(bool aRepaint) { + nsAutoString id; + if (InputElement().GetAttr(nsGkAtoms::list_, id)) { + Unlink(); + RefPtr<nsAtom> idAtom = NS_AtomizeMainThread(id); + ResetWithID(InputElement(), idAtom); + AddObserverIfNeeded(); + } + if (aRepaint) { + mOwningElementFrame->InvalidateFrame(); + } +} + +void ListMutationObserver::AddObserverIfNeeded() { + if (auto* list = get()) { + if (list->IsHTMLElement(nsGkAtoms::datalist)) { + list->AddMutationObserver(this); + } + } +} + +void ListMutationObserver::RemoveObserverIfNeeded(dom::Element* aList) { + if (aList && aList->IsHTMLElement(nsGkAtoms::datalist)) { + aList->RemoveMutationObserver(this); + } +} + +void ListMutationObserver::Detach() { + RemoveObserverIfNeeded(); + Unlink(); +} + +dom::HTMLInputElement& ListMutationObserver::InputElement() const { + MOZ_ASSERT(mOwningElementFrame->GetContent()->IsHTMLElement(nsGkAtoms::input), + "bad cast"); + return *static_cast<dom::HTMLInputElement*>( + mOwningElementFrame->GetContent()); +} + +void ListMutationObserver::AttributeChanged(dom::Element* aElement, + int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType, + const nsAttrValue* aOldValue) { + if (aAttribute == nsGkAtoms::value && aNameSpaceID == kNameSpaceID_None && + aElement->IsHTMLElement(nsGkAtoms::option)) { + mOwningElementFrame->InvalidateFrame(); + } +} + +void ListMutationObserver::CharacterDataChanged( + nsIContent* aContent, const CharacterDataChangeInfo& aInfo) { + mOwningElementFrame->InvalidateFrame(); +} + +void ListMutationObserver::ContentAppended(nsIContent* aFirstNewContent) { + mOwningElementFrame->InvalidateFrame(); +} + +void ListMutationObserver::ContentInserted(nsIContent* aChild) { + mOwningElementFrame->InvalidateFrame(); +} + +void ListMutationObserver::ContentRemoved(nsIContent* aChild, + nsIContent* aPreviousSibling) { + mOwningElementFrame->InvalidateFrame(); +} + +void ListMutationObserver::ElementChanged(dom::Element* aFrom, + dom::Element* aTo) { + IDTracker::ElementChanged(aFrom, aTo); + RemoveObserverIfNeeded(aFrom); + AddObserverIfNeeded(); + mOwningElementFrame->InvalidateFrame(); +} + +} // namespace mozilla diff --git a/layout/forms/ListMutationObserver.h b/layout/forms/ListMutationObserver.h new file mode 100644 index 0000000000..8c81077fb6 --- /dev/null +++ b/layout/forms/ListMutationObserver.h @@ -0,0 +1,67 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_ListMutationObserver_h +#define mozilla_ListMutationObserver_h + +#include "IDTracker.h" +#include "nsStubMutationObserver.h" + +class nsIFrame; + +namespace mozilla { + +namespace dom { +class HTMLInputElement; +} // namespace dom + +/** + * ListMutationObserver + * This class invalidates paint for the input element's frame when the content + * of its @list changes, or when the @list ID identifies a different element. It + * does *not* invalidate paint when the @list attribute itself changes. + */ + +class ListMutationObserver final : public nsStubMutationObserver, + public dom::IDTracker { + public: + explicit ListMutationObserver(nsIFrame& aOwningElementFrame, + bool aRepaint = false) + : mOwningElementFrame(&aOwningElementFrame) { + // We can skip invalidating paint if the frame is still being initialized. + Attach(aRepaint); + } + + NS_DECL_ISUPPORTS + + // We need to invalidate paint when the list or its options change. + NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED + NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED + + /** + * Triggered when the same @list ID identifies a different element than + * before. + */ + void ElementChanged(dom::Element* aFrom, dom::Element* aTo) override; + + void Attach(bool aRepaint = true); + void Detach(); + void AddObserverIfNeeded(); + void RemoveObserverIfNeeded(dom::Element* aList); + void RemoveObserverIfNeeded() { RemoveObserverIfNeeded(get()); } + dom::HTMLInputElement& InputElement() const; + + private: + ~ListMutationObserver(); + + nsIFrame* mOwningElementFrame; +}; +} // namespace mozilla + +#endif // mozilla_ListMutationObserver_h diff --git a/layout/forms/crashtests/1102791.html b/layout/forms/crashtests/1102791.html new file mode 100644 index 0000000000..0fd64d7553 --- /dev/null +++ b/layout/forms/crashtests/1102791.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html class="reftest-paged"><head> + <meta charset="utf-8"> + <title>Testcase for bug 1102791</title> + <style type="text/css"> + +html,body { + color:black; background-color:white; font-size:16px; padding:0; margin:0; +} + +button { + position: absolute; + -moz-appearance: none; + background: transparent; + padding: 0; + border-style:none; +} +button::before { + position: absolute; + content: "::before"; + width: 10px; + height: 200em; + border: 1px solid black; +} + + </style> +</head> +<body> + +<button></button> + +</body> +</html> diff --git a/layout/forms/crashtests/1140216.html b/layout/forms/crashtests/1140216.html new file mode 100644 index 0000000000..72612b8b3c --- /dev/null +++ b/layout/forms/crashtests/1140216.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> +<head> +<style> + +input { -moz-appearance: textfield; } + +</style> +<script> + +function boom() +{ + window.getComputedStyle(x, "::-moz-number-spin-down").getPropertyValue("color"); +} + +</script> +<body onload="boom();"> +<input type="number" id="x"> +</body> +</html> diff --git a/layout/forms/crashtests/1182414.html b/layout/forms/crashtests/1182414.html new file mode 100644 index 0000000000..3aaa0bbfcc --- /dev/null +++ b/layout/forms/crashtests/1182414.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html class="reftest-paged"> +<head> +<meta charset="UTF-8"> +<style type="text/css"> +#menu { position: fixed; left: 0px; top: 0px; } +</style> +</head> +<body> + <svg id="canvas" width="2427" height="2295.5" version="1.1" xmlns="http://www.w3.org/2000/svg"></svg> + +<div id="menu"> + <input id="chooseSize" type="range"> +</div> +</body> +</html> + diff --git a/layout/forms/crashtests/1212688.html b/layout/forms/crashtests/1212688.html new file mode 100644 index 0000000000..68262d907f --- /dev/null +++ b/layout/forms/crashtests/1212688.html @@ -0,0 +1,27 @@ +<style type="text/css">
+ optgroup {overflow-x: hidden;}
+</style>
+<select>
+ <optgroup label="optgroup">
+ <option>1</option>
+ <option>2</option>
+ <option>3</option>
+ <option>4</option>
+ <option>5</option>
+ <option>6</option>
+ <option>7</option>
+ <option>8</option>
+ <option>9</option>
+ <option>10</option>
+ <option>11</option>
+ <option>12</option>
+ <option>13</option>
+ <option>14</option>
+ <option>15</option>
+ <option>16</option>
+ <option>17</option>
+ <option>18</option>
+ <option>19</option>
+ <option>20</option>
+</optgroup>
+</select>
diff --git a/layout/forms/crashtests/1228670.xhtml b/layout/forms/crashtests/1228670.xhtml new file mode 100644 index 0000000000..cc8d309c0e --- /dev/null +++ b/layout/forms/crashtests/1228670.xhtml @@ -0,0 +1,7 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> + <body> + <select> + <optgroup style="display: list-item; list-style: inside;"></optgroup> + </select> + </body> +</html> diff --git a/layout/forms/crashtests/1279354.html b/layout/forms/crashtests/1279354.html new file mode 100644 index 0000000000..5013392526 --- /dev/null +++ b/layout/forms/crashtests/1279354.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html class="reftest-paged"><head> + <meta charset="utf-8"> + <title>Testcase for bug 1279354</title> +</head> +<body> + +<div style="position:fixed"><progress></progress></div> +1 +<br style="page-break-after: always"> +2 +<br style="page-break-after: always"> +3 +<br style="page-break-after: always"> + +</body> +</html> diff --git a/layout/forms/crashtests/1388230-1.html b/layout/forms/crashtests/1388230-1.html new file mode 100644 index 0000000000..6662a6ef01 --- /dev/null +++ b/layout/forms/crashtests/1388230-1.html @@ -0,0 +1,3 @@ +<cite contenteditable='true'> + <input type='color'/> +</cite> diff --git a/layout/forms/crashtests/1388230-2.html b/layout/forms/crashtests/1388230-2.html new file mode 100644 index 0000000000..630031d750 --- /dev/null +++ b/layout/forms/crashtests/1388230-2.html @@ -0,0 +1 @@ +<input contenteditable='true' type='color'> diff --git a/layout/forms/crashtests/1405830.html b/layout/forms/crashtests/1405830.html new file mode 100644 index 0000000000..c16eeca86a --- /dev/null +++ b/layout/forms/crashtests/1405830.html @@ -0,0 +1,19 @@ +<html> +<head> +<style> +#a { display: -webkit-box } +* { column-width: 0em } +</style> +<script> +function go() { + document.body.appendChild(a); +} +</script> +<body onload=go()> +<select id="a" multiple=""></select> +0 +Q +- +w +g +k diff --git a/layout/forms/crashtests/1418477.html b/layout/forms/crashtests/1418477.html new file mode 100644 index 0000000000..0be4732d58 --- /dev/null +++ b/layout/forms/crashtests/1418477.html @@ -0,0 +1,4 @@ +<head> +<meta charset="UTF-8"> +<meta http-equiv='content-language' content='onnect-src http://e/%2e%2f%2f%0B%1Bs%EF\u0073%09'> +<input type='number' value='9'> diff --git a/layout/forms/crashtests/1432853.html b/layout/forms/crashtests/1432853.html new file mode 100644 index 0000000000..bc0735c47f --- /dev/null +++ b/layout/forms/crashtests/1432853.html @@ -0,0 +1,8 @@ +<style> +.c1 { height: 10ch; position: fixed } +#a { column-width: 0em } +.c2 { -webkit-transform-style: preserve-3d } +</style> +<details id="a" open=""> +<h4 class="c2"> +<select class="c1"> diff --git a/layout/forms/crashtests/1460787-1.html b/layout/forms/crashtests/1460787-1.html new file mode 100644 index 0000000000..5b330555e2 --- /dev/null +++ b/layout/forms/crashtests/1460787-1.html @@ -0,0 +1,7 @@ +<script> +document.addEventListener("DOMContentLoaded", function(){ + document.getElementById('a').style.cssText="padding-inline-end:599352cm"; +}); +</script> +<fieldset> +<kbd id='a'> diff --git a/layout/forms/crashtests/1464165-1.html b/layout/forms/crashtests/1464165-1.html new file mode 100644 index 0000000000..2de4360f3c --- /dev/null +++ b/layout/forms/crashtests/1464165-1.html @@ -0,0 +1,14 @@ +<html> + <head> + <script> + function start () { + try { o1 = document.createElementNS('http://www.w3.org/1999/xhtml', 'select') } catch(e) { } + try { o2 = document.createElementNS('http://www.w3.org/1999/xhtml', 'style') } catch(e) { } + try { document.documentElement.appendChild(o1) } catch(e) { } + try { document.documentElement.appendChild(o2) } catch(e) { } + try { o2.sheet.insertRule('* { border-left: green; -moz-padding-start: 5% !important; padding: 171798691; -moz-border-end: solid;', 0) } catch(e) { } + } + document.addEventListener('DOMContentLoaded', start) + </script> + </head> +</html> diff --git a/layout/forms/crashtests/1471157.html b/layout/forms/crashtests/1471157.html new file mode 100644 index 0000000000..5cde3d0b84 --- /dev/null +++ b/layout/forms/crashtests/1471157.html @@ -0,0 +1,11 @@ +<!doctype html> +<input type="file"> +<script> + onload = function() { + var input = document.querySelector("input"); + console.log(input.offsetWidth); // Force layout flush and hence layout box + // creation. + input.dispatchEvent(new DragEvent("drop")); + } +</script> + diff --git a/layout/forms/crashtests/1488219.html b/layout/forms/crashtests/1488219.html new file mode 100644 index 0000000000..d10d3ef014 --- /dev/null +++ b/layout/forms/crashtests/1488219.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html class="reftest-wait"> +<script> +function doTest(){ + document.getElementById('c').appendChild(document.getElementById('b').cloneNode(true)); + window.frames[0].document.body.appendChild(document.getElementById('a')); + document.documentElement.removeAttribute('class'); +} +document.addEventListener("MozReftestInvalidate", doTest); +</script> + +<iframe hidden></iframe> +<style> +:last-of-type { + column-count:71; + float:left; +} +</style> +<bdo id='a'>A</bdo> +<address> +<blockquote> +</address> +<footer id='b'> +</footer> +<input id='c' type='time'> +</html> diff --git a/layout/forms/crashtests/1600207.html b/layout/forms/crashtests/1600207.html new file mode 100644 index 0000000000..a54a0d0c7d --- /dev/null +++ b/layout/forms/crashtests/1600207.html @@ -0,0 +1,9 @@ +<style> +:not(isindex) { + columns: 0px; + margin-top: 1%; +} +</style> +<fieldset> +<output> +<br></br> diff --git a/layout/forms/crashtests/1600367.html b/layout/forms/crashtests/1600367.html new file mode 100644 index 0000000000..7a481f814e --- /dev/null +++ b/layout/forms/crashtests/1600367.html @@ -0,0 +1,8 @@ +<style> +FIELDSET { + border: 35482ex groove ! important; + all: unset; +} +</style> +<i style="font-size-adjust: 113;" readonly> +<fieldset> diff --git a/layout/forms/crashtests/1617753.html b/layout/forms/crashtests/1617753.html new file mode 100644 index 0000000000..213c483451 --- /dev/null +++ b/layout/forms/crashtests/1617753.html @@ -0,0 +1,21 @@ +<style> + LEGEND, FORM { + white-space: pre-wrap; + } + + LEGEND { + max-height: 0em; + } + + FORM { + column-width: 1px; + } + + FIELDSET { + column-width: 1em; + } +</style> +<form id="htmlvar00005" class="class3"> + <fieldset> + <legend> + <!-- COMMENT --> diff --git a/layout/forms/crashtests/166750-1.html b/layout/forms/crashtests/166750-1.html new file mode 100644 index 0000000000..d873be1b29 --- /dev/null +++ b/layout/forms/crashtests/166750-1.html @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head><title>Killer</title></head>
+<body>
+ <form style="overflow: auto;">
+ <select style="position: fixed;">
+ <option>First</option>
+ <option>Second</option>
+ <option>Third</option>
+ </select>
+ </form>
+</body>
+</html>
\ No newline at end of file diff --git a/layout/forms/crashtests/1679471.html b/layout/forms/crashtests/1679471.html new file mode 100644 index 0000000000..8d2bbc8eb0 --- /dev/null +++ b/layout/forms/crashtests/1679471.html @@ -0,0 +1,8 @@ +<style> +* { + rotate: 0.8339rad; + inset: 84pt -15742cm 8% ! important; + position: fixed; +} +</style> +<button> diff --git a/layout/forms/crashtests/1690166-1.html b/layout/forms/crashtests/1690166-1.html new file mode 100644 index 0000000000..85e9b56702 --- /dev/null +++ b/layout/forms/crashtests/1690166-1.html @@ -0,0 +1,19 @@ +<style> +body { + columns: 0px; +} +* { + column-width: 95em; + break-before: page; +} +</style> +<script> +window.onload = () => { + document.vlinkColor = "x" +} +</script> +<body> +<canvas></canvas> +<form style="-webkit-perspective: 16px"> +<fieldset style="position: fixed"> +<legend>x</legend> diff --git a/layout/forms/crashtests/1690166-2.html b/layout/forms/crashtests/1690166-2.html new file mode 100644 index 0000000000..c39d53873c --- /dev/null +++ b/layout/forms/crashtests/1690166-2.html @@ -0,0 +1,19 @@ +<style> +body { + columns: 0px; +} +* { + column-width: 95em; + break-before: page; +} +</style> +<script> +window.onload = () => { + document.vlinkColor = "x" +} +</script> +<body> +<canvas></canvas> +<form style="-webkit-perspective: 16px"> +<fieldset style="float: left"> +<legend>x</legend> diff --git a/layout/forms/crashtests/200347-1.html b/layout/forms/crashtests/200347-1.html new file mode 100644 index 0000000000..b41ec8365a --- /dev/null +++ b/layout/forms/crashtests/200347-1.html @@ -0,0 +1,8 @@ +<html>
+ <body>
+ <fieldset style="width: 300px; position: fixed">
+ <legend>Crash test</legend>
+ <div style="float: right; background-color: orange; border: 1px solid black">Hello, my name is Inigo Montoya.</div> hello world content is the best content around, I love hello world content to death, especially when it wraps; that just gives me the chills. Anything less than hello world content is uncivilized.
+ </fieldset>
+ </body>
+</html>
diff --git a/layout/forms/crashtests/203041-1.html b/layout/forms/crashtests/203041-1.html new file mode 100644 index 0000000000..32d95b40e6 --- /dev/null +++ b/layout/forms/crashtests/203041-1.html @@ -0,0 +1,24 @@ +<html>
+
+<head></head>
+
+<body>
+<form method="post" action="#" enctype="multipart/form-data" name="content">
+
+ <div id="sshot" style="position:absolute; left:0; top:70; width:600;
+visibility:visible">
+ <input type="file" name="sshot-1" size="20">
+ </div>
+
+ <div id="comment" style="position:absolute; left:0; top:70; width:600;
+visibility:hidden"></div>
+
+ <script language="JavaScript">
+ <!--
+ document.documentElement.offsetHeight;
+ document.getElementById('sshot').style.display = 'none';
+ document.getElementById('comment').style.display = 'none';
+ // -->
+ </script>
+</form>
+</body></html>
\ No newline at end of file diff --git a/layout/forms/crashtests/213390-1.html b/layout/forms/crashtests/213390-1.html new file mode 100644 index 0000000000..0ff94ec542 --- /dev/null +++ b/layout/forms/crashtests/213390-1.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3c.org/TR/1999/REC-html401-19991224/loose.dtd">
+
+<HTML>
+ <BODY>
+
+ <EMBED>
+
+ <TABLE>
+ <TR>
+ <TD STYLE="FONT: 10px Arial;">
+ <A STYLE="FONT: 11px Arial;">1</A>
+ </TD>
+ </TR>
+ </TABLE>
+
+ <DIV STYLE="POSITION: absolute;">
+ <FORM>
+ <SELECT STYLE="FONT: 11px Arial;">
+ </FORM>
+ </DIV>
+
+ </BODY>
+</HTML>
diff --git a/layout/forms/crashtests/258101-1.html b/layout/forms/crashtests/258101-1.html new file mode 100644 index 0000000000..245917cbf7 --- /dev/null +++ b/layout/forms/crashtests/258101-1.html @@ -0,0 +1,18 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+<head>
+ <meta content="text/html; charset=ISO-8859-1"
+ http-equiv="content-type">
+ <title>Crash Testcase</title>
+ <script type="text/javascript">
+function crash()
+{
+ var inp = document.getElementById("theinp");
+ inp.type = "file";
+}
+ </script>
+</head>
+<body onload="crash();">
+<input id="theinp" type="text">
+</body>
+</html>
diff --git a/layout/forms/crashtests/266225-1.html b/layout/forms/crashtests/266225-1.html new file mode 100644 index 0000000000..c714125dd5 --- /dev/null +++ b/layout/forms/crashtests/266225-1.html @@ -0,0 +1,7 @@ +<HTML>
+<HEAD>
+</HEAD>
+<BODY>
+<FIELDSET STYLE="float:right; text-indent:999px;">Test</FIELDSET>
+</BODY>
+</HTML>
diff --git a/layout/forms/crashtests/310426-1.xhtml b/layout/forms/crashtests/310426-1.xhtml new file mode 100644 index 0000000000..683dba679f --- /dev/null +++ b/layout/forms/crashtests/310426-1.xhtml @@ -0,0 +1,9 @@ +<html xmlns="http://www.w3.org/1999/xhtml">
+
+<body>
+
+<select><span style="position: absolute;" /></select>
+
+</body>
+
+</html>
diff --git a/layout/forms/crashtests/310520-1.xhtml b/layout/forms/crashtests/310520-1.xhtml new file mode 100644 index 0000000000..6e111832a4 --- /dev/null +++ b/layout/forms/crashtests/310520-1.xhtml @@ -0,0 +1,19 @@ +<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+
+<script><![CDATA[
+
+function init()
+{
+ var select = document.getElementsByTagName("select")[0];
+ select.remove();
+}
+
+window.addEventListener("load", init);
+
+]]></script>
+
+
+</head>
+<body><select><input/></select></body>
+</html>
diff --git a/layout/forms/crashtests/315752-1.xhtml b/layout/forms/crashtests/315752-1.xhtml new file mode 100644 index 0000000000..66d3d793d7 --- /dev/null +++ b/layout/forms/crashtests/315752-1.xhtml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" xmlns:xml="http://www.w3.org/XML/1998/namespace">
+<head>
+<title>Settings - Unclassified NewsBoard</title>
+
+
+</head>
+<body>
+
+<select name="Language">
+ <option value="" style="clear: right;">
+ Automatic </option>
+ <option value="de" style="clear: right;">
+ <span alt="" style="float: right; margin-top: 1px;"> Deutsch</span> <small>(de)</small> </option>
+ <option value="en" selected="selected" style="clear: right;">
+ <span alt="" style="float: right; margin-top: 1px;"> English</span> <small>(en)</small> </option>
+</select>
+
+</body>
+</html>
\ No newline at end of file diff --git a/layout/forms/crashtests/317502-1.xhtml b/layout/forms/crashtests/317502-1.xhtml new file mode 100644 index 0000000000..7ef5f0b4c1 --- /dev/null +++ b/layout/forms/crashtests/317502-1.xhtml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"> +<head> + <title>Testcase, bug 317502</title> +</head> +<body onload="document.getElementsByTagName('input')[0].style.width='200px'"> + +<input type="file" style="position: fixed" /> + +</body> +</html> diff --git a/layout/forms/crashtests/321894.html b/layout/forms/crashtests/321894.html new file mode 100644 index 0000000000..0a82645ba4 --- /dev/null +++ b/layout/forms/crashtests/321894.html @@ -0,0 +1,17 @@ +<html><head>
+<title>
+Testcase bug 321894 - ASSERTION: RemovedAsPrimaryFrame called after PreDestroy: 'PR_FALSE'
+</title>
+</head>
+<body>
+This shouldn't assert in a debug build
+<span>
+ <script>var x=document.body.offsetHeight;</script>
+ <div>
+ <span style="float:left;">
+ <input type="text">
+ </span>
+ </div>
+</span>
+</body>
+</html>
diff --git a/layout/forms/crashtests/343510-1.html b/layout/forms/crashtests/343510-1.html new file mode 100644 index 0000000000..00cb94f211 --- /dev/null +++ b/layout/forms/crashtests/343510-1.html @@ -0,0 +1,4 @@ +<select style="position: absolute;"> +<meta> +<option style="position: absolute;"> +<body style="display: block;"> diff --git a/layout/forms/crashtests/363696-1.xhtml b/layout/forms/crashtests/363696-1.xhtml new file mode 100644 index 0000000000..19f938deba --- /dev/null +++ b/layout/forms/crashtests/363696-1.xhtml @@ -0,0 +1,10 @@ +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<body> + +<xul:hbox> + <input type="file" /> +</xul:hbox> + +</body> +</html>
\ No newline at end of file diff --git a/layout/forms/crashtests/363696-2.html b/layout/forms/crashtests/363696-2.html new file mode 100644 index 0000000000..9d93edd185 --- /dev/null +++ b/layout/forms/crashtests/363696-2.html @@ -0,0 +1,2 @@ +<div style="display: -moz-box;"> +<input type="file"> diff --git a/layout/forms/crashtests/363696-3.html b/layout/forms/crashtests/363696-3.html new file mode 100644 index 0000000000..e1d38076ed --- /dev/null +++ b/layout/forms/crashtests/363696-3.html @@ -0,0 +1,5 @@ +<html><head></head><body>
+
+<span style="display: -moz-box;"><input type="file"><textarea></textarea></span>
+
+</body></html>
\ No newline at end of file diff --git a/layout/forms/crashtests/366205-1.html b/layout/forms/crashtests/366205-1.html new file mode 100644 index 0000000000..2a3786afe0 --- /dev/null +++ b/layout/forms/crashtests/366205-1.html @@ -0,0 +1,11 @@ +<html> +<head> +</head> +<body onload='var yar=document.getElementById("yar"); yar.parentNode.removeChild(yar); document.getElementById("foo").removeAttribute("multiple");'> + +<div id="yar">Clicking the button below should not trigger an assertion</div> + +<select id="foo" multiple><option>A</option></select> + +</body> +</html>
\ No newline at end of file diff --git a/layout/forms/crashtests/366537-1.xhtml b/layout/forms/crashtests/366537-1.xhtml new file mode 100644 index 0000000000..b799cd2ca2 --- /dev/null +++ b/layout/forms/crashtests/366537-1.xhtml @@ -0,0 +1,32 @@ +<html xmlns="http://www.w3.org/1999/xhtml" class="reftest-wait"> + +<head> +<script> + +function boom() +{ + var fieldset = document.getElementById("fieldset"); + var h3 = document.getElementById("h3"); + var legend = document.getElementById("legend"); + + fieldset.appendChild(legend); + fieldset.appendChild(h3); + + document.documentElement.removeAttribute("class"); +} + +</script> +</head> + + +<body onload="setTimeout(boom, 30);"> + +<legend id="legend">legend</legend> + +<h3 id="h3">H3</h3> + +<select><option>option<fieldset id="fieldset"></fieldset></option></select> + +</body> + +</html> diff --git a/layout/forms/crashtests/367587-1.html b/layout/forms/crashtests/367587-1.html new file mode 100644 index 0000000000..9bffe569ed --- /dev/null +++ b/layout/forms/crashtests/367587-1.html @@ -0,0 +1,37 @@ +<html class="reftest-wait"> +<head> + +<style> + +#opt1 { display: table-footer-group; } +#opt1 { visibility: collapse; } +#opt1 { overflow: hidden scroll; } + +#opt2 { display: table-cell; } +#opt2 { clear: left; } + +select { width: 1px; } + +</style> + +<script> + +function boom() +{ + document.getElementById("opt2").style.clear = "none"; + document.documentElement.removeAttribute("class"); +} + +</script> + +</head> + +<body onload="setTimeout(boom, 30);"> + +<select multiple> +<option id="opt1">A</option> +<option id="opt2">B</option> +</select> + +</body> +</html> diff --git a/layout/forms/crashtests/370703-1.html b/layout/forms/crashtests/370703-1.html new file mode 100644 index 0000000000..b447b180b5 --- /dev/null +++ b/layout/forms/crashtests/370703-1.html @@ -0,0 +1,30 @@ +<html class="reftest-wait">
+<head>
+<script>
+
+function boom()
+{
+ document.getElementById("M").style.width = "6em";
+ document.documentElement.removeAttribute("class");
+}
+
+</script>
+
+<body onload="setTimeout(boom, 30);">
+
+<div style="position:absolute; top: 50px; left: 50px; border: 2px solid orange;">
+
+ <select>
+ <option id="M">M</option>
+ </select>
+
+ <br>
+
+ <select style="width: 200%">
+ <option style="visibility: collapse; overflow: auto; display: table-footer-group;">X</option>
+ </select>
+
+</div>
+
+</body>
+</html>
diff --git a/layout/forms/crashtests/370940-1.html b/layout/forms/crashtests/370940-1.html new file mode 100644 index 0000000000..ccaeb9643b --- /dev/null +++ b/layout/forms/crashtests/370940-1.html @@ -0,0 +1,28 @@ +<html class="reftest-wait"> +<head> +<script> + +var HTML_NS = "http://www.w3.org/1999/xhtml"; + +function boom1() +{ + var newSpan = document.createElementNS(HTML_NS, "span"); + document.body.appendChild(newSpan); + + setTimeout(boom2, 30); +} + +function boom2() +{ + var newDiv = document.createElementNS(HTML_NS, "div"); + document.getElementById("s").appendChild(newDiv); + document.documentElement.removeAttribute("class"); +} + +</script> +</head> +<body onload="setTimeout(boom1, 30);"> + <p>العربي</p> + <span id="s"></span><input><select></select><isindex><span></span> +</body> +</html> diff --git a/layout/forms/crashtests/370967.html b/layout/forms/crashtests/370967.html new file mode 100644 index 0000000000..be7854b41c --- /dev/null +++ b/layout/forms/crashtests/370967.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+
+<html>
+<head>
+<title>Untitled</title>
+</head>
+<body>
+Focus the input of the isindex, then do a reload, Mozilla should not crash
+<isindex autofocus>
+</label>
+
+</body>
+</html>
diff --git a/layout/forms/crashtests/378369.html b/layout/forms/crashtests/378369.html new file mode 100644 index 0000000000..90327735ef --- /dev/null +++ b/layout/forms/crashtests/378369.html @@ -0,0 +1,19 @@ +<html>
+<head>
+<title>Testcase bug - Crash [@ nsEventListenerManager::FixContextMenuEvent] when firing contextmenu event in display: none iframe</title>
+</head>
+<body>
+This page should not crash Mozilla
+<iframe style="display: none;"></iframe>
+
+<script>
+function docontextmenu(i){
+var doc = window.frames[0].document;
+ var ev = doc.createEvent ('MouseEvents');
+ ev.initMouseEvent('contextmenu', true,true, window, 3,5, 5, 400, 400, 0, 0, 0,0,0,null);
+ doc.dispatchEvent(ev);
+}
+setTimeout(docontextmenu,0);
+</script>
+</body>
+</html>
diff --git a/layout/forms/crashtests/380116-1.xhtml b/layout/forms/crashtests/380116-1.xhtml new file mode 100644 index 0000000000..cb37ff079a --- /dev/null +++ b/layout/forms/crashtests/380116-1.xhtml @@ -0,0 +1,11 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<body onload="document.getElementById('t').style.display = 'block';"> + +<table border="1" id="t"> + <tr> + <td><select><option>foopy</option></select></td> + </tr> +</table> + +</body> +</html> diff --git a/layout/forms/crashtests/382610-1.html b/layout/forms/crashtests/382610-1.html new file mode 100644 index 0000000000..9fe9c5b5c1 --- /dev/null +++ b/layout/forms/crashtests/382610-1.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> +<body> + +<div style="position: absolute"> + <input type="file" style="position: absolute; -moz-appearance: menulist;"> +</div> + +</body> +</html> + diff --git a/layout/forms/crashtests/383887-1.html b/layout/forms/crashtests/383887-1.html new file mode 100644 index 0000000000..65d4751f7d --- /dev/null +++ b/layout/forms/crashtests/383887-1.html @@ -0,0 +1,20 @@ +<html> +<body> + +<table> + <tr> + <td> + <select> + <option style="padding: 200%;"> + </select> + </td> + </tr> + <tr> + <td> + <div style="padding: 200%"><span style="float: left; border: 1px solid red;"></span></div> + </td> + </tr> +</table> + +</body> +</html> diff --git a/layout/forms/crashtests/386554-1.html b/layout/forms/crashtests/386554-1.html new file mode 100644 index 0000000000..585f1e5b54 --- /dev/null +++ b/layout/forms/crashtests/386554-1.html @@ -0,0 +1,14 @@ +<html> +<head></head> + +<body> + +<div style="position: absolute"><input id="input" type="file" style="top: 100%"></div> + +<script> +document.body.offsetHeight; +document.getElementById("input").style.position = "absolute"; +</script> + +</body> +</html> diff --git a/layout/forms/crashtests/388374-1.xhtml b/layout/forms/crashtests/388374-1.xhtml new file mode 100644 index 0000000000..2a871f68b4 --- /dev/null +++ b/layout/forms/crashtests/388374-1.xhtml @@ -0,0 +1,22 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<script> +function boom() +{ + var newText = document.createTextNode('x'); + document.getElementById("a").parentNode.appendChild(newText); +} +</script> +</head> + +<body onload="boom()"> + +<table border="1"> + <tr> + <td>1</td> + <td id="a" style="padding-left: 10%"><select style="padding-left: 20%; bottom: 10%;"><option style="padding: 0 0 10% 20%;">pppp</option></select></td> + </tr> +</table> + +</body> +</html> diff --git a/layout/forms/crashtests/388374-2.html b/layout/forms/crashtests/388374-2.html new file mode 100644 index 0000000000..81918c37cd --- /dev/null +++ b/layout/forms/crashtests/388374-2.html @@ -0,0 +1,25 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<script> +function boom() +{ + var newDiv = document.createElementNS("http://www.w3.org/1999/xhtml", "div"); + newDiv.style.width = "100px"; + newDiv.style.height = "8px"; + newDiv.style.background = "lightgreen"; + document.getElementById("tr").appendChild(newDiv); +} +</script> +</head> + +<body onload="boom()"> + +<table border="1"> + <tr id="tr"> + <td></td> + <td style="padding-left: 10%"><select style="padding-left: 20%; bottom: 10%;"><option style="padding: 0 0 10% 20%; width: 25px;"></option></select></td> + </tr> +</table> + +</body> +</html> diff --git a/layout/forms/crashtests/393656-1.xhtml b/layout/forms/crashtests/393656-1.xhtml new file mode 100644 index 0000000000..22a9326c5f --- /dev/null +++ b/layout/forms/crashtests/393656-1.xhtml @@ -0,0 +1,13 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +</head> + +<body> + <select style="float: right; clear: right;"> + <option><a style="float: right;"></a></option> + <option style="margin: 100%;"></option> + <option><div style="clear: both"></div></option> + </select> +</body> + +</html> diff --git a/layout/forms/crashtests/393656-2.xhtml b/layout/forms/crashtests/393656-2.xhtml new file mode 100644 index 0000000000..7d9adaf20d --- /dev/null +++ b/layout/forms/crashtests/393656-2.xhtml @@ -0,0 +1,22 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +</head> + +<body> + <div style="overflow: hidden;"> + <div style="float: right;"><span>XXX</span></div> + <select style="float: right; clear: right;"> + <option><a style="float: right;"></a></option> + <option style="margin: 100%;"></option> + <option><div style="clear: both"></div></option> + </select> + <dl style="float: left;"></dl> + <div> + <div style="float: left;"></div> + <div style="display: block; clear: both"></div> + <dl style="float: left;"></dl> + </div> + </div> +</body> + +</html> diff --git a/layout/forms/crashtests/399262.html b/layout/forms/crashtests/399262.html new file mode 100644 index 0000000000..77bc0a16db --- /dev/null +++ b/layout/forms/crashtests/399262.html @@ -0,0 +1,50 @@ +<style> +select::first-letter, option::first-letter { color:green; } +.block select, .block option{ display:block; } +.scroll select, .scroll option{ display:block; overflow:hidden; } +</style> + +<select></select> +<select>F</select> +<select>Ff</select> +<select><option>F</select> +<select><option>Ff</select> + +<div class="block"> +<select></select> +<select>F</select> +<select>Ff</select> +<select><option>F</select> +<select><option>Ff</select> +</div> + +<div class="scroll"> +<select></select> +<select>F</select> +<select>Ff</select> +<select><option>F</select> +<select><option>Ff</select> +</div> + +<select size=2></select> +<select size=2>F</select> +<select size=2>Ff</select> +<select size=2><option>F</select> +<select size=2><option>Ff</select> + +<div class="block"> +<select size=2></select> +<select size=2>F</select> +<select size=2>Ff</select> +<select size=2><option>F</select> +<select size=2><option>Ff</select> +</div> + +<div class="scroll"> +<select size=2></select> +<select size=2>F</select> +<select size=2>Ff</select> +<select size=2><option>F</select> +<select size=2><option>Ff</select> +</div> + diff --git a/layout/forms/crashtests/402852-1.html b/layout/forms/crashtests/402852-1.html new file mode 100644 index 0000000000..e5ba9adb13 --- /dev/null +++ b/layout/forms/crashtests/402852-1.html @@ -0,0 +1,2 @@ +<body style="text-indent: 99999999999999999999px;"> +<fieldset> diff --git a/layout/forms/crashtests/403148-1.html b/layout/forms/crashtests/403148-1.html new file mode 100644 index 0000000000..cfb38fdb13 --- /dev/null +++ b/layout/forms/crashtests/403148-1.html @@ -0,0 +1,22 @@ +<html> +<head> +<script> + +function boom() +{ + document.getElementById("ce").contentEditable = true; + document.documentElement.offsetHeight; + document.getElementById("finput").setAttribute("type", ""); +} + +</script> +</head> + +<body onload="boom();"> + +<input id="finput" type="file" disabled> + +<span id="ce"></span> + +</body> +</html> diff --git a/layout/forms/crashtests/404118-1.html b/layout/forms/crashtests/404118-1.html new file mode 100644 index 0000000000..40a2ac72b0 --- /dev/null +++ b/layout/forms/crashtests/404118-1.html @@ -0,0 +1,5 @@ +<html> +<body> +<input type="file" style="position: fixed; height: 0; width: 10px; white-space: normal;"> +</body> +</html> diff --git a/layout/forms/crashtests/404123-1.html b/layout/forms/crashtests/404123-1.html new file mode 100644 index 0000000000..73fbaa363c --- /dev/null +++ b/layout/forms/crashtests/404123-1.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> +<head> +</head> +<body> + +<fieldset> +<legend style="padding: 0pt 100%;"></legend> +</fieldset> + +</body> +</html> diff --git a/layout/forms/crashtests/407066.html b/layout/forms/crashtests/407066.html new file mode 100644 index 0000000000..98c4d0f4ae --- /dev/null +++ b/layout/forms/crashtests/407066.html @@ -0,0 +1 @@ +<div style="display: inline-block; column-count: 2;">text<input type="reset" style="float: right;"> diff --git a/layout/forms/crashtests/451316.html b/layout/forms/crashtests/451316.html new file mode 100644 index 0000000000..51fda22bc0 --- /dev/null +++ b/layout/forms/crashtests/451316.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<html> +<body> +<div style="column-width: 1px;"><select style="height: 12em; display: block;"></select></div> +<div style="column-width: 1px;"><select multiple style="height: 12em; display: block;"></select></div> +</body> +</html> diff --git a/layout/forms/crashtests/455451-1.html b/layout/forms/crashtests/455451-1.html new file mode 100644 index 0000000000..fd7d7b6b37 --- /dev/null +++ b/layout/forms/crashtests/455451-1.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<head> +<script type="text/javascript"> + +function boom() +{ + window.addEventListener("DOMCharacterDataModified", function(){}); + document.body.appendChild(document.createElement("isindex")); +} + +</script> +</head> + +<body onload="boom();"></body> + +</html> diff --git a/layout/forms/crashtests/457537-1.html b/layout/forms/crashtests/457537-1.html new file mode 100644 index 0000000000..167d3b7076 --- /dev/null +++ b/layout/forms/crashtests/457537-1.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<head> +<script type="text/javascript"> + +function boom() +{ + window.addEventListener("DOMAttrModified", function(){}); + document.body.appendChild(document.createElement("input")); +} + +</script> +</head> + +<body onload="boom();"></body> + +</html> diff --git a/layout/forms/crashtests/457537-2.html b/layout/forms/crashtests/457537-2.html new file mode 100644 index 0000000000..626b035b48 --- /dev/null +++ b/layout/forms/crashtests/457537-2.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<head> +<script type="text/javascript"> + +function boom() +{ + window.addEventListener("DOMAttrModified", function(){}); + document.body.appendChild(document.createElement("isindex")); +} + +</script> +</head> + +<body onload="boom();"></body> + +</html> diff --git a/layout/forms/crashtests/498698-1.html b/layout/forms/crashtests/498698-1.html new file mode 100644 index 0000000000..fa8e2e17a8 --- /dev/null +++ b/layout/forms/crashtests/498698-1.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> +<body> +<select style="height: 3401091640591pc"></select> +</body> +</html> diff --git a/layout/forms/crashtests/513113-1.html b/layout/forms/crashtests/513113-1.html new file mode 100644 index 0000000000..dc04ef6b6e --- /dev/null +++ b/layout/forms/crashtests/513113-1.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html><body onload="document.body.style.display = 'table';" style="column-count: 1; white-space: pre-line; -moz-appearance: resizer; height: 23698324514pc;"> + + + +<input></body></html> diff --git a/layout/forms/crashtests/538062-1.xhtml b/layout/forms/crashtests/538062-1.xhtml new file mode 100644 index 0000000000..431f7ab847 --- /dev/null +++ b/layout/forms/crashtests/538062-1.xhtml @@ -0,0 +1,20 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<script type="text/javascript"> +<![CDATA[ + +function boom() +{ + var option = document.getElementsByTagName("option")[0]; + var b = option.firstChild; + option.appendChild(document.createTextNode("\u064A")); + document.documentElement.offsetHeight; + option.removeChild(b); +} + +]]> +</script> +</head> + +<body onload="boom();"><select><option>B </option></select></body> +</html> diff --git a/layout/forms/crashtests/570624-1.html b/layout/forms/crashtests/570624-1.html new file mode 100644 index 0000000000..47a2777282 --- /dev/null +++ b/layout/forms/crashtests/570624-1.html @@ -0,0 +1,15 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<script> +function boom() +{ + var xt = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", 'textbox'); + document.body.appendChild(xt); + xt.setAttribute('disabled', "true"); + xt.setAttribute('value', "foo"); +} +</script> +</head> +<body onload="boom();"> +</body> +</html> diff --git a/layout/forms/crashtests/578604-1.html b/layout/forms/crashtests/578604-1.html new file mode 100644 index 0000000000..a7e3a870dd --- /dev/null +++ b/layout/forms/crashtests/578604-1.html @@ -0,0 +1,17 @@ +<html class="reftest-paged">
+<head>
+<style>
+ h2 { page-break-before: always; }
+ #reviewer { position: fixed;}
+</style>
+</head>
+
+<body>
+
+<h2>Crash Test Case</h2>
+<div>
+<input id='reviewer'>
+</div>
+
+</body>
+</html>
diff --git a/layout/forms/crashtests/590302-1.xhtml b/layout/forms/crashtests/590302-1.xhtml new file mode 100644 index 0000000000..85d49cb76c --- /dev/null +++ b/layout/forms/crashtests/590302-1.xhtml @@ -0,0 +1,4 @@ +<html class="reftest-paged" xmlns="http://www.w3.org/1999/xhtml" style=" float: left; white-space: pre; "> +<div style="page-break-before: always;"></div> +<textarea style="position: fixed;"></textarea> +</html> diff --git a/layout/forms/crashtests/626014.xhtml b/layout/forms/crashtests/626014.xhtml new file mode 100644 index 0000000000..05a2361d3d --- /dev/null +++ b/layout/forms/crashtests/626014.xhtml @@ -0,0 +1,20 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<script> +<![CDATA[ + +function boom() +{ + document.getElementById("i").selectionEnd; + document.getElementById("a").appendChild(document.getElementById("b")); +} + +]]> +</script> +</head> +<body onload="boom();"> + +<mrow xmlns="http://www.w3.org/1998/Math/MathML" id="a"><mrow id="b"/><input xmlns="http://www.w3.org/1999/xhtml" id="i" /></mrow> + +</body> +</html> diff --git a/layout/forms/crashtests/639733.xhtml b/layout/forms/crashtests/639733.xhtml new file mode 100644 index 0000000000..a6d5b01a8d --- /dev/null +++ b/layout/forms/crashtests/639733.xhtml @@ -0,0 +1,26 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<script> +<![CDATA[ + +function remove(n) +{ + n.remove(); +} + +function boom() +{ + + document.documentElement.offsetHeight; + remove(document.getElementById("s")); + remove(document.getElementById("e")); + document.documentElement.offsetHeight; +} + +]]> +</script> +</head> +<body onload="boom();"> +<embed id="e" src="data:text/html,A"></embed><span id="s"><div></div></span><isindex/> +</body> +</html> diff --git a/layout/forms/crashtests/669767.html b/layout/forms/crashtests/669767.html new file mode 100644 index 0000000000..f41a9125c2 --- /dev/null +++ b/layout/forms/crashtests/669767.html @@ -0,0 +1,14 @@ +<html>
+<head>
+<title>Untitled</title>
+
+
+</head>
+<body>
+<iframe src="data:text/html;charset=utf-8,%3Chtml%3E%3Chead%3E%3C/head%3E%3Cbody%3E%0A%3Ctextarea%3E%3C/textarea%3E%0A%0A%0A%3Cstyle%3E%0A@font-face%20%7B%0A%20%20%20%20%20%20font-family%3A%20%22cutabovetherest%22%3B%0A%20%20%20%20%20%20src%3A%20url%28%22http%3A//www.webpagepublicity.com/free-fonts/a/A%2520Cut%2520Above%2520The%2520Rest.ttf%22%29%3B%0A%7D%20%20%20%20%0A%0A%3C/style%3E%0A%0A%3Coptgroup%20contenteditable%3D%22true%22%20style%3D%22display%3A%20inline%3B%22%3E%3C/optgroup%3E%0A%0A%3C/body%3E%3C/html%3E"></iframe>
+<script>
+
+
+</script>
+</body>
+</html>
diff --git a/layout/forms/crashtests/682684-binding.xml b/layout/forms/crashtests/682684-binding.xml new file mode 100644 index 0000000000..910eec33c8 --- /dev/null +++ b/layout/forms/crashtests/682684-binding.xml @@ -0,0 +1,4 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<style>html::before { content:"b"; }</style> +<input contenteditable="true" spellcheck="true"/> +</html>
\ No newline at end of file diff --git a/layout/forms/crashtests/682684.xhtml b/layout/forms/crashtests/682684.xhtml new file mode 100644 index 0000000000..2b7b2c83fa --- /dev/null +++ b/layout/forms/crashtests/682684.xhtml @@ -0,0 +1,3 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<span style="mask: url(682684-binding.xml#a);"/> +</html> diff --git a/layout/forms/crashtests/865602.html b/layout/forms/crashtests/865602.html new file mode 100644 index 0000000000..f560e7a385 --- /dev/null +++ b/layout/forms/crashtests/865602.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html><body> + +<div style="column-count: 2;"><fieldset style="transform-style: preserve-3d;"><fieldset style="white-space: pre-line; position: fixed;"><div style="position: fixed;"> + + +</div></fieldset></fieldset></div> + +</body></html> diff --git a/layout/forms/crashtests/893331.html b/layout/forms/crashtests/893331.html new file mode 100644 index 0000000000..86a01f363e --- /dev/null +++ b/layout/forms/crashtests/893331.html @@ -0,0 +1,9 @@ +<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+</head>
+<body>
+<input type="range" min="10000.00000000003">
+</body>
+</html>
diff --git a/layout/forms/crashtests/893332-1.html b/layout/forms/crashtests/893332-1.html new file mode 100644 index 0000000000..1cd17c0b1c --- /dev/null +++ b/layout/forms/crashtests/893332-1.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8"> + </head> + <body> + <input min="-10" max="10" step="0.1" type="range" value="-5"/> + <input max="50" min="10" step="2" type="range" value="7"/> + </body> +</html> diff --git a/layout/forms/crashtests/944198.html b/layout/forms/crashtests/944198.html new file mode 100644 index 0000000000..0da7bc54dd --- /dev/null +++ b/layout/forms/crashtests/944198.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8"> +</head> +<body onload="c.removeAttribute('type'); c.removeAttribute('value');"> +<input id="c" type="color"> +</body> +</html> diff --git a/layout/forms/crashtests/949891.xhtml b/layout/forms/crashtests/949891.xhtml new file mode 100644 index 0000000000..8fbfe1e422 --- /dev/null +++ b/layout/forms/crashtests/949891.xhtml @@ -0,0 +1,5 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<body> +<input type="number"> </input> +</body> +</html> diff --git a/layout/forms/crashtests/959311.html b/layout/forms/crashtests/959311.html new file mode 100644 index 0000000000..1b4817221b --- /dev/null +++ b/layout/forms/crashtests/959311.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html class="reftest-paged"> +<head> +<meta charset="utf-8"> +<style type="text/css"> +@page { size:5in 3in; margin:0in; } +div { height: 2.5in; } +select { height: 0.5in; display:block; padding:20px; page-break-inside:initial; } +</style> +</head> +<body> + <div></div> + <select> + <option>Text</option> + </select> + </body> +</html> diff --git a/layout/forms/crashtests/960277-2.html b/layout/forms/crashtests/960277-2.html new file mode 100644 index 0000000000..ea771ae798 --- /dev/null +++ b/layout/forms/crashtests/960277-2.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<style> +#d { overflow:scroll; width:200px; height:200px; background:yellow; } +#d2 { right:0; width:100px; height:100px; background:purple; } +</style> +<fieldset id="d"> + <div id="d2"></div> +</fieldset> +<script> +d.getBoundingClientRect(); +d.style.transform = "translateX(100px)"; +d.getBoundingClientRect(); +d2.style.position = "absolute"; +</script> diff --git a/layout/forms/crashtests/997709-1.html b/layout/forms/crashtests/997709-1.html new file mode 100644 index 0000000000..baf5d0b14b --- /dev/null +++ b/layout/forms/crashtests/997709-1.html @@ -0,0 +1,5 @@ +<!DOCTYPE HTML> +<html class="reftest-paged"><body><div style="position: fixed;"> +<div style="page-break-after: always"></div> +<select style="display:flex; position: fixed;"><option>A</select> +</div></body></html> diff --git a/layout/forms/crashtests/crashtests.list b/layout/forms/crashtests/crashtests.list new file mode 100644 index 0000000000..5a8d7529b2 --- /dev/null +++ b/layout/forms/crashtests/crashtests.list @@ -0,0 +1,79 @@ +load 166750-1.html +load 200347-1.html +load 203041-1.html +load 213390-1.html +load 258101-1.html +load 266225-1.html +load 310426-1.xhtml +load 310520-1.xhtml +load 315752-1.xhtml +load 317502-1.xhtml +load 321894.html +load 343510-1.html +load chrome://reftest/content/crashtests/layout/forms/crashtests/363696-1.xhtml +load 363696-2.html +load 363696-3.html +load 366205-1.html +load 366537-1.xhtml +load 367587-1.html +load 370703-1.html +load 370940-1.html +load 370967.html +load 378369.html +load 380116-1.xhtml +load 382610-1.html +load 383887-1.html +load 386554-1.html +load 388374-1.xhtml +load 388374-2.html +load 393656-1.xhtml +load 393656-2.xhtml +load 399262.html +load 402852-1.html +load 403148-1.html +load 404118-1.html +load 404123-1.html +load 407066.html +load 451316.html +load 455451-1.html +load 457537-1.html +load 457537-2.html +load 498698-1.html +load 513113-1.html +load 538062-1.xhtml +load 570624-1.html +asserts(1) load 578604-1.html # bug 584564 +asserts(4-7) load 590302-1.xhtml # bug 584564 +load 626014.xhtml +load 639733.xhtml +load 669767.html +load 682684.xhtml +load 865602.html +load 893331.html +load 893332-1.html +load 944198.html +load 949891.xhtml +load 959311.html +load 960277-2.html +load 997709-1.html +load 1102791.html +load 1140216.html +load 1182414.html +load 1212688.html +load 1228670.xhtml +load 1279354.html +load 1388230-1.html +load 1388230-2.html +load 1405830.html +load 1418477.html +load 1432853.html +asserts(1-4) load 1460787-1.html +load 1464165-1.html +load 1471157.html +load 1488219.html +load 1600207.html +load 1600367.html +load 1617753.html +load 1679471.html +load 1690166-1.html +load 1690166-2.html diff --git a/layout/forms/moz.build b/layout/forms/moz.build new file mode 100644 index 0000000000..7502fb5a9a --- /dev/null +++ b/layout/forms/moz.build @@ -0,0 +1,54 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Layout: Form Controls") + +MOCHITEST_MANIFESTS += ["test/mochitest.ini"] +MOCHITEST_CHROME_MANIFESTS += ["test/chrome.ini"] + +EXPORTS += [ + "nsIFormControlFrame.h", + "nsISelectControlFrame.h", + "nsITextControlFrame.h", +] + +UNIFIED_SOURCES += [ + "HTMLSelectEventListener.cpp", + "ListMutationObserver.cpp", + "nsButtonFrameRenderer.cpp", + "nsCheckboxRadioFrame.cpp", + "nsColorControlFrame.cpp", + "nsComboboxControlFrame.cpp", + "nsDateTimeControlFrame.cpp", + "nsFieldSetFrame.cpp", + "nsFileControlFrame.cpp", + "nsGfxButtonControlFrame.cpp", + "nsHTMLButtonControlFrame.cpp", + "nsImageControlFrame.cpp", + "nsListControlFrame.cpp", + "nsMeterFrame.cpp", + "nsNumberControlFrame.cpp", + "nsProgressFrame.cpp", + "nsRangeFrame.cpp", + "nsSearchControlFrame.cpp", + "nsSelectsAreaFrame.cpp", + "nsTextControlFrame.cpp", +] + +FINAL_LIBRARY = "xul" + +include("/ipc/chromium/chromium-config.mozbuild") + +LOCAL_INCLUDES += [ + "../base", + "../generic", + "../painting", + "../style", + "../xul", + "/dom/base", + "/dom/html", +] diff --git a/layout/forms/nsButtonFrameRenderer.cpp b/layout/forms/nsButtonFrameRenderer.cpp new file mode 100644 index 0000000000..74c2d3f025 --- /dev/null +++ b/layout/forms/nsButtonFrameRenderer.cpp @@ -0,0 +1,476 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include "nsButtonFrameRenderer.h" +#include "nsCSSRendering.h" +#include "nsPresContext.h" +#include "nsPresContextInlines.h" +#include "nsGkAtoms.h" +#include "nsCSSPseudoElements.h" +#include "nsNameSpaceManager.h" +#include "mozilla/ServoStyleSet.h" +#include "mozilla/Unused.h" +#include "nsDisplayList.h" +#include "nsITheme.h" +#include "nsIFrame.h" +#include "mozilla/dom/Element.h" + +#include "gfxUtils.h" +#include "mozilla/layers/RenderRootStateManager.h" + +using namespace mozilla; +using namespace mozilla::image; +using namespace mozilla::layers; + +namespace mozilla { +class nsDisplayButtonBoxShadowOuter; +class nsDisplayButtonBorder; +class nsDisplayButtonForeground; +} // namespace mozilla + +nsButtonFrameRenderer::nsButtonFrameRenderer() : mFrame(nullptr) { + MOZ_COUNT_CTOR(nsButtonFrameRenderer); +} + +nsButtonFrameRenderer::~nsButtonFrameRenderer() { + MOZ_COUNT_DTOR(nsButtonFrameRenderer); +} + +void nsButtonFrameRenderer::SetFrame(nsIFrame* aFrame, + nsPresContext* aPresContext) { + mFrame = aFrame; + ReResolveStyles(aPresContext); +} + +nsIFrame* nsButtonFrameRenderer::GetFrame() { return mFrame; } + +void nsButtonFrameRenderer::SetDisabled(bool aDisabled, bool aNotify) { + dom::Element* element = mFrame->GetContent()->AsElement(); + if (aDisabled) + element->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled, u""_ns, aNotify); + else + element->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, aNotify); +} + +bool nsButtonFrameRenderer::isDisabled() { + return mFrame->GetContent()->AsElement()->IsDisabled(); +} + +nsresult nsButtonFrameRenderer::DisplayButton(nsDisplayListBuilder* aBuilder, + nsDisplayList* aBackground, + nsDisplayList* aForeground) { + if (!mFrame->StyleEffects()->mBoxShadow.IsEmpty()) { + aBackground->AppendNewToTop<nsDisplayButtonBoxShadowOuter>(aBuilder, + GetFrame()); + } + + nsRect buttonRect = + mFrame->GetRectRelativeToSelf() + aBuilder->ToReferenceFrame(mFrame); + + const AppendedBackgroundType result = + nsDisplayBackgroundImage::AppendBackgroundItemsToTop( + aBuilder, mFrame, buttonRect, aBackground); + if (result == AppendedBackgroundType::None) { + aBuilder->BuildCompositorHitTestInfoIfNeeded(GetFrame(), aBackground); + } + + aBackground->AppendNewToTop<nsDisplayButtonBorder>(aBuilder, GetFrame(), + this); + + // Only display focus rings if we actually have them. Since at most one + // button would normally display a focus ring, most buttons won't have them. + const auto* disp = mFrame->StyleDisplay(); + nsPresContext* pc = mFrame->PresContext(); + if (mInnerFocusStyle && mInnerFocusStyle->StyleBorder()->HasBorder() && + mFrame->IsThemed(disp) && + pc->Theme()->ThemeWantsButtonInnerFocusRing( + mFrame, disp->EffectiveAppearance())) { + aForeground->AppendNewToTop<nsDisplayButtonForeground>(aBuilder, GetFrame(), + this); + } + return NS_OK; +} + +void nsButtonFrameRenderer::GetButtonInnerFocusRect(const nsRect& aRect, + nsRect& aResult) { + aResult = aRect; + aResult.Deflate(mFrame->GetUsedBorderAndPadding()); + + if (mInnerFocusStyle) { + nsMargin innerFocusPadding(0, 0, 0, 0); + mInnerFocusStyle->StylePadding()->GetPadding(innerFocusPadding); + + nsMargin framePadding = mFrame->GetUsedPadding(); + + innerFocusPadding.top = std::min(innerFocusPadding.top, framePadding.top); + innerFocusPadding.right = + std::min(innerFocusPadding.right, framePadding.right); + innerFocusPadding.bottom = + std::min(innerFocusPadding.bottom, framePadding.bottom); + innerFocusPadding.left = + std::min(innerFocusPadding.left, framePadding.left); + + aResult.Inflate(innerFocusPadding); + } +} + +ImgDrawResult nsButtonFrameRenderer::PaintInnerFocusBorder( + nsDisplayListBuilder* aBuilder, nsPresContext* aPresContext, + gfxContext& aRenderingContext, const nsRect& aDirtyRect, + const nsRect& aRect) { + // we draw the -moz-focus-inner border just inside the button's + // normal border and padding, to match Windows themes. + + nsRect rect; + + PaintBorderFlags flags = aBuilder->ShouldSyncDecodeImages() + ? PaintBorderFlags::SyncDecodeImages + : PaintBorderFlags(); + + ImgDrawResult result = ImgDrawResult::SUCCESS; + + if (mInnerFocusStyle) { + GetButtonInnerFocusRect(aRect, rect); + + result &= + nsCSSRendering::PaintBorder(aPresContext, aRenderingContext, mFrame, + aDirtyRect, rect, mInnerFocusStyle, flags); + } + + return result; +} + +Maybe<nsCSSBorderRenderer> +nsButtonFrameRenderer::CreateInnerFocusBorderRenderer( + nsDisplayListBuilder* aBuilder, nsPresContext* aPresContext, + gfxContext* aRenderingContext, const nsRect& aDirtyRect, + const nsRect& aRect, bool* aBorderIsEmpty) { + if (mInnerFocusStyle) { + nsRect rect; + GetButtonInnerFocusRect(aRect, rect); + + gfx::DrawTarget* dt = + aRenderingContext ? aRenderingContext->GetDrawTarget() : nullptr; + return nsCSSRendering::CreateBorderRenderer( + aPresContext, dt, mFrame, aDirtyRect, rect, mInnerFocusStyle, + aBorderIsEmpty); + } + + return Nothing(); +} + +ImgDrawResult nsButtonFrameRenderer::PaintBorder(nsDisplayListBuilder* aBuilder, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect, + const nsRect& aRect) { + // get the button rect this is inside the focus and outline rects + nsRect buttonRect = aRect; + ComputedStyle* context = mFrame->Style(); + + PaintBorderFlags borderFlags = aBuilder->ShouldSyncDecodeImages() + ? PaintBorderFlags::SyncDecodeImages + : PaintBorderFlags(); + + nsCSSRendering::PaintBoxShadowInner(aPresContext, aRenderingContext, mFrame, + buttonRect); + + ImgDrawResult result = + nsCSSRendering::PaintBorder(aPresContext, aRenderingContext, mFrame, + aDirtyRect, buttonRect, context, borderFlags); + + return result; +} + +/** + * Call this when styles change + */ +void nsButtonFrameRenderer::ReResolveStyles(nsPresContext* aPresContext) { + // get all the styles + ServoStyleSet* styleSet = aPresContext->StyleSet(); + + // get styles assigned to -moz-focus-inner (ie dotted border on Windows) + mInnerFocusStyle = styleSet->ProbePseudoElementStyle( + *mFrame->GetContent()->AsElement(), PseudoStyleType::mozFocusInner, + mFrame->Style()); +} + +ComputedStyle* nsButtonFrameRenderer::GetComputedStyle(int32_t aIndex) const { + switch (aIndex) { + case NS_BUTTON_RENDERER_FOCUS_INNER_CONTEXT_INDEX: + return mInnerFocusStyle; + default: + return nullptr; + } +} + +void nsButtonFrameRenderer::SetComputedStyle(int32_t aIndex, + ComputedStyle* aComputedStyle) { + switch (aIndex) { + case NS_BUTTON_RENDERER_FOCUS_INNER_CONTEXT_INDEX: + mInnerFocusStyle = aComputedStyle; + break; + } +} + +namespace mozilla { + +class nsDisplayButtonBoxShadowOuter : public nsPaintedDisplayItem { + public: + nsDisplayButtonBoxShadowOuter(nsDisplayListBuilder* aBuilder, + nsIFrame* aFrame) + : nsPaintedDisplayItem(aBuilder, aFrame) { + MOZ_COUNT_CTOR(nsDisplayButtonBoxShadowOuter); + } + MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayButtonBoxShadowOuter) + + virtual bool CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) override; + + bool CanBuildWebRenderDisplayItems(); + + virtual void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override; + virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder, + bool* aSnap) const override; + NS_DISPLAY_DECL_NAME("ButtonBoxShadowOuter", TYPE_BUTTON_BOX_SHADOW_OUTER) +}; + +nsRect nsDisplayButtonBoxShadowOuter::GetBounds(nsDisplayListBuilder* aBuilder, + bool* aSnap) const { + *aSnap = false; + return mFrame->InkOverflowRectRelativeToSelf() + ToReferenceFrame(); +} + +void nsDisplayButtonBoxShadowOuter::Paint(nsDisplayListBuilder* aBuilder, + gfxContext* aCtx) { + nsRect frameRect = nsRect(ToReferenceFrame(), mFrame->GetSize()); + + nsCSSRendering::PaintBoxShadowOuter(mFrame->PresContext(), *aCtx, mFrame, + frameRect, GetPaintRect(aBuilder, aCtx)); +} + +bool nsDisplayButtonBoxShadowOuter::CanBuildWebRenderDisplayItems() { + // FIXME(emilio): Is this right? That doesn't make much sense. + if (mFrame->StyleEffects()->mBoxShadow.IsEmpty()) { + return false; + } + + bool hasBorderRadius; + bool nativeTheme = + nsCSSRendering::HasBoxShadowNativeTheme(mFrame, hasBorderRadius); + + // We don't support native themed things yet like box shadows around + // input buttons. + return !nativeTheme; +} + +bool nsDisplayButtonBoxShadowOuter::CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) { + if (!CanBuildWebRenderDisplayItems()) { + return false; + } + int32_t appUnitsPerDevPixel = mFrame->PresContext()->AppUnitsPerDevPixel(); + nsRect shadowRect = nsRect(ToReferenceFrame(), mFrame->GetSize()); + LayoutDeviceRect deviceBox = + LayoutDeviceRect::FromAppUnits(shadowRect, appUnitsPerDevPixel); + wr::LayoutRect deviceBoxRect = wr::ToLayoutRect(deviceBox); + + bool dummy; + LayoutDeviceRect clipRect = LayoutDeviceRect::FromAppUnits( + GetBounds(aDisplayListBuilder, &dummy), appUnitsPerDevPixel); + wr::LayoutRect deviceClipRect = wr::ToLayoutRect(clipRect); + + bool hasBorderRadius; + Unused << nsCSSRendering::HasBoxShadowNativeTheme(mFrame, hasBorderRadius); + + LayoutDeviceSize zeroSize; + wr::BorderRadius borderRadius = + wr::ToBorderRadius(zeroSize, zeroSize, zeroSize, zeroSize); + if (hasBorderRadius) { + gfx::RectCornerRadii borderRadii; + hasBorderRadius = nsCSSRendering::GetBorderRadii(shadowRect, shadowRect, + mFrame, borderRadii); + if (hasBorderRadius) { + borderRadius = wr::ToBorderRadius(borderRadii); + } + } + + const Span<const StyleBoxShadow> shadows = + mFrame->StyleEffects()->mBoxShadow.AsSpan(); + MOZ_ASSERT(!shadows.IsEmpty()); + + for (const StyleBoxShadow& shadow : Reversed(shadows)) { + if (shadow.inset) { + continue; + } + float blurRadius = + float(shadow.base.blur.ToAppUnits()) / float(appUnitsPerDevPixel); + gfx::DeviceColor shadowColor = + ToDeviceColor(nsCSSRendering::GetShadowColor(shadow.base, mFrame, 1.0)); + + LayoutDevicePoint shadowOffset = LayoutDevicePoint::FromAppUnits( + nsPoint(shadow.base.horizontal.ToAppUnits(), + shadow.base.vertical.ToAppUnits()), + appUnitsPerDevPixel); + + float spreadRadius = + float(shadow.spread.ToAppUnits()) / float(appUnitsPerDevPixel); + + aBuilder.PushBoxShadow(deviceBoxRect, deviceClipRect, !BackfaceIsHidden(), + deviceBoxRect, wr::ToLayoutVector2D(shadowOffset), + wr::ToColorF(shadowColor), blurRadius, spreadRadius, + borderRadius, wr::BoxShadowClipMode::Outset); + } + return true; +} + +class nsDisplayButtonBorder final : public nsPaintedDisplayItem { + public: + nsDisplayButtonBorder(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame, + nsButtonFrameRenderer* aRenderer) + : nsPaintedDisplayItem(aBuilder, aFrame), mBFR(aRenderer) { + MOZ_COUNT_CTOR(nsDisplayButtonBorder); + } + MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayButtonBorder) + + virtual bool MustPaintOnContentSide() const override { return true; } + + virtual void HitTest(nsDisplayListBuilder* aBuilder, const nsRect& aRect, + HitTestState* aState, + nsTArray<nsIFrame*>* aOutFrames) override { + aOutFrames->AppendElement(mFrame); + } + virtual void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override; + virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder, + bool* aSnap) const override; + virtual bool CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) override; + NS_DISPLAY_DECL_NAME("ButtonBorderBackground", TYPE_BUTTON_BORDER_BACKGROUND) + private: + nsButtonFrameRenderer* mBFR; +}; + +bool nsDisplayButtonBorder::CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) { + // This is really a combination of paint box shadow inner + + // paint border. + aBuilder.StartGroup(this); + const nsRect buttonRect = nsRect(ToReferenceFrame(), mFrame->GetSize()); + bool snap; + nsRect visible = GetBounds(aDisplayListBuilder, &snap); + nsDisplayBoxShadowInner::CreateInsetBoxShadowWebRenderCommands( + aBuilder, aSc, visible, mFrame, buttonRect); + + bool borderIsEmpty = false; + Maybe<nsCSSBorderRenderer> br = nsCSSRendering::CreateBorderRenderer( + mFrame->PresContext(), nullptr, mFrame, nsRect(), + nsRect(ToReferenceFrame(), mFrame->GetSize()), mFrame->Style(), + &borderIsEmpty, mFrame->GetSkipSides()); + if (!br) { + if (borderIsEmpty) { + aBuilder.FinishGroup(); + } else { + aBuilder.CancelGroup(true); + } + + return borderIsEmpty; + } + + br->CreateWebRenderCommands(this, aBuilder, aResources, aSc); + aBuilder.FinishGroup(); + return true; +} + +void nsDisplayButtonBorder::Paint(nsDisplayListBuilder* aBuilder, + gfxContext* aCtx) { + NS_ASSERTION(mFrame, "No frame?"); + nsPresContext* pc = mFrame->PresContext(); + nsRect r = nsRect(ToReferenceFrame(), mFrame->GetSize()); + + // draw the border and background inside the focus and outline borders + Unused << mBFR->PaintBorder(aBuilder, pc, *aCtx, GetPaintRect(aBuilder, aCtx), + r); +} + +nsRect nsDisplayButtonBorder::GetBounds(nsDisplayListBuilder* aBuilder, + bool* aSnap) const { + *aSnap = false; + return aBuilder->IsForEventDelivery() + ? nsRect(ToReferenceFrame(), mFrame->GetSize()) + : mFrame->InkOverflowRectRelativeToSelf() + ToReferenceFrame(); +} + +class nsDisplayButtonForeground final : public nsPaintedDisplayItem { + public: + nsDisplayButtonForeground(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame, + nsButtonFrameRenderer* aRenderer) + : nsPaintedDisplayItem(aBuilder, aFrame), mBFR(aRenderer) { + MOZ_COUNT_CTOR(nsDisplayButtonForeground); + } + MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayButtonForeground) + + void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override; + bool CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) override; + NS_DISPLAY_DECL_NAME("ButtonForeground", TYPE_BUTTON_FOREGROUND) + private: + nsButtonFrameRenderer* mBFR; +}; + +void nsDisplayButtonForeground::Paint(nsDisplayListBuilder* aBuilder, + gfxContext* aCtx) { + nsRect r = nsRect(ToReferenceFrame(), mFrame->GetSize()); + + // Draw the -moz-focus-inner border + Unused << mBFR->PaintInnerFocusBorder(aBuilder, mFrame->PresContext(), *aCtx, + GetPaintRect(aBuilder, aCtx), r); +} + +bool nsDisplayButtonForeground::CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) { + Maybe<nsCSSBorderRenderer> br; + bool borderIsEmpty = false; + bool dummy; + nsRect r = nsRect(ToReferenceFrame(), mFrame->GetSize()); + br = mBFR->CreateInnerFocusBorderRenderer( + aDisplayListBuilder, mFrame->PresContext(), nullptr, + GetBounds(aDisplayListBuilder, &dummy), r, &borderIsEmpty); + + if (!br) { + return borderIsEmpty; + } + + aBuilder.StartGroup(this); + br->CreateWebRenderCommands(this, aBuilder, aResources, aSc); + aBuilder.FinishGroup(); + + return true; +} + +} // namespace mozilla diff --git a/layout/forms/nsButtonFrameRenderer.h b/layout/forms/nsButtonFrameRenderer.h new file mode 100644 index 0000000000..ea5a9cdea4 --- /dev/null +++ b/layout/forms/nsButtonFrameRenderer.h @@ -0,0 +1,83 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsButtonFrameRenderer_h___ +#define nsButtonFrameRenderer_h___ + +#include "nsMargin.h" +#include "nsCSSRenderingBorders.h" + +class gfxContext; +class nsIFrame; +class nsPresContext; +struct nsRect; + +namespace mozilla { +class nsDisplayList; +class nsDisplayListBuilder; +} // namespace mozilla + +#define NS_BUTTON_RENDERER_FOCUS_INNER_CONTEXT_INDEX 0 +#define NS_BUTTON_RENDERER_LAST_CONTEXT_INDEX \ + NS_BUTTON_RENDERER_FOCUS_INNER_CONTEXT_INDEX + +class nsButtonFrameRenderer { + using nsDisplayList = mozilla::nsDisplayList; + using nsDisplayListBuilder = mozilla::nsDisplayListBuilder; + + typedef mozilla::image::ImgDrawResult ImgDrawResult; + typedef mozilla::ComputedStyle ComputedStyle; + + public: + nsButtonFrameRenderer(); + ~nsButtonFrameRenderer(); + + /** + * Create display list items for the button + */ + nsresult DisplayButton(nsDisplayListBuilder* aBuilder, + nsDisplayList* aBackground, + nsDisplayList* aForeground); + + ImgDrawResult PaintInnerFocusBorder(nsDisplayListBuilder* aBuilder, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect, + const nsRect& aRect); + + mozilla::Maybe<nsCSSBorderRenderer> CreateInnerFocusBorderRenderer( + nsDisplayListBuilder* aBuilder, nsPresContext* aPresContext, + gfxContext* aRenderingContext, const nsRect& aDirtyRect, + const nsRect& aRect, bool* aBorderIsEmpty); + + ImgDrawResult PaintBorder(nsDisplayListBuilder* aBuilder, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect, const nsRect& aRect); + + void SetFrame(nsIFrame* aFrame, nsPresContext* aPresContext); + + void SetDisabled(bool aDisabled, bool notify); + + bool isActive(); + bool isDisabled(); + + void GetButtonInnerFocusRect(const nsRect& aRect, nsRect& aResult); + + ComputedStyle* GetComputedStyle(int32_t aIndex) const; + void SetComputedStyle(int32_t aIndex, ComputedStyle* aComputedStyle); + void ReResolveStyles(nsPresContext* aPresContext); + + nsIFrame* GetFrame(); + + private: + // cached style for optional inner focus outline (used on Windows). + RefPtr<ComputedStyle> mInnerFocusStyle; + + nsIFrame* mFrame; +}; + +#endif diff --git a/layout/forms/nsCheckboxRadioFrame.cpp b/layout/forms/nsCheckboxRadioFrame.cpp new file mode 100644 index 0000000000..902b900fdd --- /dev/null +++ b/layout/forms/nsCheckboxRadioFrame.cpp @@ -0,0 +1,174 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsCheckboxRadioFrame.h" + +#include "nsGkAtoms.h" +#include "nsLayoutUtils.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/PresShell.h" +#include "nsIContent.h" +#include "nsStyleConsts.h" + +using namespace mozilla; +using mozilla::dom::HTMLInputElement; + +// #define FCF_NOISY + +nsCheckboxRadioFrame* NS_NewCheckboxRadioFrame(PresShell* aPresShell, + ComputedStyle* aStyle) { + return new (aPresShell) + nsCheckboxRadioFrame(aStyle, aPresShell->GetPresContext()); +} + +nsCheckboxRadioFrame::nsCheckboxRadioFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsAtomicContainerFrame(aStyle, aPresContext, kClassID) {} + +nsCheckboxRadioFrame::~nsCheckboxRadioFrame() = default; + +NS_IMPL_FRAMEARENA_HELPERS(nsCheckboxRadioFrame) + +NS_QUERYFRAME_HEAD(nsCheckboxRadioFrame) + NS_QUERYFRAME_ENTRY(nsIFormControlFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsAtomicContainerFrame) + +nscoord nsCheckboxRadioFrame::DefaultSize() { + if (StyleDisplay()->HasAppearance()) { + return PresContext()->Theme()->GetCheckboxRadioPrefSize(); + } + return CSSPixel::ToAppUnits(9); +} + +/* virtual */ +void nsCheckboxRadioFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + DO_GLOBAL_REFLOW_COUNT_DSP("nsCheckboxRadioFrame"); + DisplayBorderBackgroundOutline(aBuilder, aLists); +} + +/* virtual */ +nscoord nsCheckboxRadioFrame::GetMinISize(gfxContext* aRenderingContext) { + nscoord result; + DISPLAY_MIN_INLINE_SIZE(this, result); + result = StyleDisplay()->HasAppearance() ? DefaultSize() : 0; + return result; +} + +/* virtual */ +nscoord nsCheckboxRadioFrame::GetPrefISize(gfxContext* aRenderingContext) { + nscoord result; + DISPLAY_PREF_INLINE_SIZE(this, result); + result = StyleDisplay()->HasAppearance() ? DefaultSize() : 0; + return result; +} + +/* virtual */ +LogicalSize nsCheckboxRadioFrame::ComputeAutoSize( + gfxContext* aRC, WritingMode aWM, const LogicalSize& aCBSize, + nscoord aAvailableISize, const LogicalSize& aMargin, + const LogicalSize& aBorderPadding, const StyleSizeOverrides& aSizeOverrides, + ComputeSizeFlags aFlags) { + LogicalSize size(aWM, 0, 0); + if (!StyleDisplay()->HasAppearance()) { + return size; + } + + // Note: this call always set the BSize to NS_UNCONSTRAINEDSIZE. + size = nsAtomicContainerFrame::ComputeAutoSize( + aRC, aWM, aCBSize, aAvailableISize, aMargin, aBorderPadding, + aSizeOverrides, aFlags); + size.BSize(aWM) = DefaultSize(); + return size; +} + +Maybe<nscoord> nsCheckboxRadioFrame::GetNaturalBaselineBOffset( + WritingMode aWM, BaselineSharingGroup aBaselineGroup, + BaselineExportContext) const { + NS_ASSERTION(!IsSubtreeDirty(), "frame must not be dirty"); + + if (aBaselineGroup == BaselineSharingGroup::Last) { + return Nothing{}; + } + + if (StyleDisplay()->IsBlockOutsideStyle()) { + return Nothing{}; + } + + // For appearance:none we use a standard CSS baseline, i.e. synthesized from + // our margin-box. + if (!StyleDisplay()->HasAppearance()) { + return Nothing{}; + } + + if (aWM.IsCentralBaseline()) { + return Some(GetLogicalUsedBorderAndPadding(aWM).BStart(aWM) + + ContentSize(aWM).BSize(aWM) / 2); + } + // This is for compatibility with Chrome, Safari and Edge (Dec 2016). + // Treat radio buttons and checkboxes as having an intrinsic baseline + // at the block-end of the control (use the block-end content edge rather + // than the margin edge). + // For "inverted" lines (typically in writing-mode:vertical-lr), use the + // block-start end instead. + return Some(aWM.IsLineInverted() + ? GetLogicalUsedBorderAndPadding(aWM).BStart(aWM) + : BSize(aWM) - GetLogicalUsedBorderAndPadding(aWM).BEnd(aWM)); +} + +void nsCheckboxRadioFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MarkInReflow(); + DO_GLOBAL_REFLOW_COUNT("nsCheckboxRadioFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + NS_FRAME_TRACE( + NS_FRAME_TRACE_CALLS, + ("enter nsCheckboxRadioFrame::Reflow: aMaxSize=%d,%d", + aReflowInput.AvailableWidth(), aReflowInput.AvailableHeight())); + + const auto wm = aReflowInput.GetWritingMode(); + aDesiredSize.SetSize(wm, aReflowInput.ComputedSizeWithBorderPadding(wm)); + + if (nsLayoutUtils::FontSizeInflationEnabled(aPresContext)) { + float inflation = nsLayoutUtils::FontSizeInflationFor(this); + aDesiredSize.Width() *= inflation; + aDesiredSize.Height() *= inflation; + } + + NS_FRAME_TRACE(NS_FRAME_TRACE_CALLS, + ("exit nsCheckboxRadioFrame::Reflow: size=%d,%d", + aDesiredSize.Width(), aDesiredSize.Height())); + + aDesiredSize.SetOverflowAreasToDesiredBounds(); + FinishAndStoreOverflow(&aDesiredSize); +} + +void nsCheckboxRadioFrame::SetFocus(bool aOn, bool aRepaint) {} + +nsresult nsCheckboxRadioFrame::HandleEvent(nsPresContext* aPresContext, + WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) { + // Check for disabled content so that selection works properly (?). + if (IsContentDisabled()) { + return nsIFrame::HandleEvent(aPresContext, aEvent, aEventStatus); + } + return NS_OK; +} + +void nsCheckboxRadioFrame::GetCurrentCheckState(bool* aState) { + HTMLInputElement* inputElement = HTMLInputElement::FromNode(mContent); + if (inputElement) { + *aState = inputElement->Checked(); + } +} + +nsresult nsCheckboxRadioFrame::SetFormProperty(nsAtom* aName, + const nsAString& aValue) { + return NS_OK; +} diff --git a/layout/forms/nsCheckboxRadioFrame.h b/layout/forms/nsCheckboxRadioFrame.h new file mode 100644 index 0000000000..4fc165e29d --- /dev/null +++ b/layout/forms/nsCheckboxRadioFrame.h @@ -0,0 +1,97 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsCheckboxRadioFrame_h___ +#define nsCheckboxRadioFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsIFormControlFrame.h" +#include "nsAtomicContainerFrame.h" +#include "nsDisplayList.h" + +/** + * nsCheckboxRadioFrame is used for radio buttons and checkboxes. + * It also has a static method (GetUsableScreenRect) that is used by + * other form controls. + */ +class nsCheckboxRadioFrame final : public nsAtomicContainerFrame, + public nsIFormControlFrame { + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsCheckboxRadioFrame) + + explicit nsCheckboxRadioFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext); + + // nsIFrame replacements + virtual bool IsFrameOfType(uint32_t aFlags) const override { + return nsAtomicContainerFrame::IsFrameOfType( + aFlags & ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock)); + } + + virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + /** + * Both GetMinISize and GetPrefISize will return whatever GetIntrinsicISize + * returns. + */ + virtual nscoord GetMinISize(gfxContext* aRenderingContext) override; + virtual nscoord GetPrefISize(gfxContext* aRenderingContext) override; + + /** + * Our auto size is just intrinsic width and intrinsic height. + */ + mozilla::LogicalSize ComputeAutoSize( + gfxContext* aRenderingContext, mozilla::WritingMode aWM, + const mozilla::LogicalSize& aCBSize, nscoord aAvailableISize, + const mozilla::LogicalSize& aMargin, + const mozilla::LogicalSize& aBorderPadding, + const mozilla::StyleSizeOverrides& aSizeOverrides, + mozilla::ComputeSizeFlags aFlags) override; + + /** + * Respond to a gui event + * @see nsIFrame::HandleEvent + */ + virtual nsresult HandleEvent(nsPresContext* aPresContext, + mozilla::WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) override; + + Maybe<nscoord> GetNaturalBaselineBOffset( + mozilla::WritingMode aWM, BaselineSharingGroup aBaselineGroup, + BaselineExportContext) const override; + + /** + * Respond to the request to resize and/or reflow + * @see nsIFrame::Reflow + */ + virtual void Reflow(nsPresContext* aCX, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + // new behavior + + virtual void SetFocus(bool aOn = true, bool aRepaint = false) override; + + // nsIFormControlFrame + virtual nsresult SetFormProperty(nsAtom* aName, + const nsAString& aValue) override; + + protected: + virtual ~nsCheckboxRadioFrame(); + + nscoord DefaultSize(); + + /** + * Get the state of the checked attribute. + * @param aState set to true if the checked attribute is set, + * false if the checked attribute has been removed + */ + void GetCurrentCheckState(bool* aState); +}; + +#endif diff --git a/layout/forms/nsColorControlFrame.cpp b/layout/forms/nsColorControlFrame.cpp new file mode 100644 index 0000000000..f921016d06 --- /dev/null +++ b/layout/forms/nsColorControlFrame.cpp @@ -0,0 +1,128 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsColorControlFrame.h" + +#include "nsContentCreatorFunctions.h" +#include "nsContentUtils.h" +#include "nsCSSPseudoElements.h" +#include "nsGkAtoms.h" +#include "nsIFormControl.h" +#include "mozilla/PresShell.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/dom/Document.h" + +using namespace mozilla; +using mozilla::dom::CallerType; +using mozilla::dom::Document; +using mozilla::dom::Element; +using mozilla::dom::HTMLInputElement; + +nsColorControlFrame::nsColorControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsHTMLButtonControlFrame(aStyle, aPresContext, kClassID) {} + +nsIFrame* NS_NewColorControlFrame(PresShell* aPresShell, + ComputedStyle* aStyle) { + return new (aPresShell) + nsColorControlFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsColorControlFrame) + +NS_QUERYFRAME_HEAD(nsColorControlFrame) + NS_QUERYFRAME_ENTRY(nsColorControlFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) +NS_QUERYFRAME_TAIL_INHERITING(nsHTMLButtonControlFrame) + +void nsColorControlFrame::Destroy(DestroyContext& aContext) { + aContext.AddAnonymousContent(mColorContent.forget()); + nsHTMLButtonControlFrame::Destroy(aContext); +} + +#ifdef DEBUG_FRAME_DUMP +nsresult nsColorControlFrame::GetFrameName(nsAString& aResult) const { + return MakeFrameName(u"ColorControl"_ns, aResult); +} +#endif + +// Create the color area for the button. +// The frame will be generated by the frame constructor. +nsresult nsColorControlFrame::CreateAnonymousContent( + nsTArray<ContentInfo>& aElements) { + RefPtr<Document> doc = mContent->GetComposedDoc(); + mColorContent = doc->CreateHTMLElement(nsGkAtoms::div); + mColorContent->SetPseudoElementType(PseudoStyleType::mozColorSwatch); + + // Mark the element to be native anonymous before setting any attributes. + mColorContent->SetIsNativeAnonymousRoot(); + + nsresult rv = UpdateColor(); + NS_ENSURE_SUCCESS(rv, rv); + + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + aElements.AppendElement(mColorContent); + + return NS_OK; +} + +void nsColorControlFrame::AppendAnonymousContentTo( + nsTArray<nsIContent*>& aElements, uint32_t aFilter) { + if (mColorContent) { + aElements.AppendElement(mColorContent); + } +} + +nsresult nsColorControlFrame::UpdateColor() { + // Get the color from the "value" property of our content; it will return the + // default color (through the sanitization algorithm) if the value is empty. + nsAutoString color; + HTMLInputElement* elt = HTMLInputElement::FromNode(mContent); + elt->GetValue(color, CallerType::System); + + if (color.IsEmpty()) { + // OK, there is one case the color string might be empty -- if our content + // is still being created, i.e. if it has mDoneCreating==false. In that + // case, we simply do nothing, because we'll be called again with a complete + // content node before we ever reflow or paint. Specifically: we can expect + // that HTMLInputElement::DoneCreatingElement() will set mDoneCreating to + // true (which enables sanitization) and then it'll call SetValueInternal(), + // which produces a nonempty color (via sanitization), and then it'll call + // this function here, and we'll get the nonempty default color. + MOZ_ASSERT(HasAnyStateBits(NS_FRAME_FIRST_REFLOW), + "Content node's GetValue() should return a valid color string " + "by the time we've been reflowed (the default color, in case " + "no valid color is set)"); + return NS_OK; + } + + // Set the background-color CSS property of the swatch element to this color. + return mColorContent->SetAttr(kNameSpaceID_None, nsGkAtoms::style, + u"background-color:"_ns + color, + /* aNotify */ true); +} + +nsresult nsColorControlFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType) { + NS_ASSERTION(mColorContent, "The color div must exist"); + + // If the value attribute is set, update the color box, but only if we're + // still a color control, which might not be the case if the type attribute + // was removed/changed. + nsCOMPtr<nsIFormControl> fctrl = do_QueryInterface(GetContent()); + if (fctrl->ControlType() == FormControlType::InputColor && + aNameSpaceID == kNameSpaceID_None && nsGkAtoms::value == aAttribute) { + UpdateColor(); + } + return nsHTMLButtonControlFrame::AttributeChanged(aNameSpaceID, aAttribute, + aModType); +} + +nsContainerFrame* nsColorControlFrame::GetContentInsertionFrame() { + return this; +} diff --git a/layout/forms/nsColorControlFrame.h b/layout/forms/nsColorControlFrame.h new file mode 100644 index 0000000000..a85816324f --- /dev/null +++ b/layout/forms/nsColorControlFrame.h @@ -0,0 +1,60 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsColorControlFrame_h___ +#define nsColorControlFrame_h___ + +#include "nsCOMPtr.h" +#include "nsHTMLButtonControlFrame.h" +#include "nsIAnonymousContentCreator.h" + +namespace mozilla { +enum class PseudoStyleType : uint8_t; +class PresShell; +} // namespace mozilla + +// Class which implements the input type=color + +class nsColorControlFrame final : public nsHTMLButtonControlFrame, + public nsIAnonymousContentCreator { + typedef mozilla::PseudoStyleType PseudoStyleType; + typedef mozilla::dom::Element Element; + + public: + friend nsIFrame* NS_NewColorControlFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + void Destroy(DestroyContext&) override; + + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsColorControlFrame) + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override; +#endif + + // nsIAnonymousContentCreator + virtual nsresult CreateAnonymousContent( + nsTArray<ContentInfo>& aElements) override; + virtual void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) override; + + // nsIFrame + virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + virtual nsContainerFrame* GetContentInsertionFrame() override; + + // Refresh the color swatch, using associated input's value + nsresult UpdateColor(); + + private: + explicit nsColorControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext); + + nsCOMPtr<Element> mColorContent; +}; + +#endif // nsColorControlFrame_h___ diff --git a/layout/forms/nsComboboxControlFrame.cpp b/layout/forms/nsComboboxControlFrame.cpp new file mode 100644 index 0000000000..ea27a20d3d --- /dev/null +++ b/layout/forms/nsComboboxControlFrame.cpp @@ -0,0 +1,983 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsComboboxControlFrame.h" + +#include "gfxContext.h" +#include "gfxUtils.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/PathHelpers.h" +#include "nsCOMPtr.h" +#include "nsDeviceContext.h" +#include "nsFocusManager.h" +#include "nsCheckboxRadioFrame.h" +#include "nsGkAtoms.h" +#include "nsCSSAnonBoxes.h" +#include "nsHTMLParts.h" +#include "nsIFormControl.h" +#include "nsILayoutHistoryState.h" +#include "nsNameSpaceManager.h" +#include "nsListControlFrame.h" +#include "nsPIDOMWindow.h" +#include "mozilla/PresState.h" +#include "nsView.h" +#include "nsViewManager.h" +#include "nsIContentInlines.h" +#include "nsIDOMEventListener.h" +#include "nsISelectControlFrame.h" +#include "nsContentUtils.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/HTMLSelectElement.h" +#include "mozilla/dom/Document.h" +#include "nsIScrollableFrame.h" +#include "mozilla/ServoStyleSet.h" +#include "nsNodeInfoManager.h" +#include "nsContentCreatorFunctions.h" +#include "nsLayoutUtils.h" +#include "nsDisplayList.h" +#include "nsITheme.h" +#include "nsStyleConsts.h" +#include "nsTextFrameUtils.h" +#include "nsTextRunTransformations.h" +#include "HTMLSelectEventListener.h" +#include "mozilla/Likely.h" +#include <algorithm> +#include "nsTextNode.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/PresShell.h" +#include "mozilla/PresShellInlines.h" +#include "mozilla/Unused.h" +#include "gfx2DGlue.h" +#include "mozilla/widget/nsAutoRollup.h" + +using namespace mozilla; +using namespace mozilla::gfx; + +NS_IMETHODIMP +nsComboboxControlFrame::RedisplayTextEvent::Run() { + if (mControlFrame) mControlFrame->HandleRedisplayTextEvent(); + return NS_OK; +} + +// Drop down list event management. +// The combo box uses the following strategy for managing the drop-down list. +// If the combo box or its arrow button is clicked on the drop-down list is +// displayed If mouse exits the combo box with the drop-down list displayed the +// drop-down list is asked to capture events The drop-down list will capture all +// events including mouse down and up and will always return with +// ListWasSelected method call regardless of whether an item in the list was +// actually selected. +// The ListWasSelected code will turn off mouse-capture for the drop-down list. +// The drop-down list does not explicitly set capture when it is in the +// drop-down mode. + +nsComboboxControlFrame* NS_NewComboboxControlFrame(PresShell* aPresShell, + ComputedStyle* aStyle, + nsFrameState aStateFlags) { + nsComboboxControlFrame* it = new (aPresShell) + nsComboboxControlFrame(aStyle, aPresShell->GetPresContext()); + + if (it) { + // set the state flags (if any are provided) + it->AddStateBits(aStateFlags); + } + + return it; +} + +NS_IMPL_FRAMEARENA_HELPERS(nsComboboxControlFrame) + +//----------------------------------------------------------- +// Reflow Debugging Macros +// These let us "see" how many reflow counts are happening +//----------------------------------------------------------- +#ifdef DO_REFLOW_COUNTER + +# define MAX_REFLOW_CNT 1024 +static int32_t gTotalReqs = 0; +; +static int32_t gTotalReflows = 0; +; +static int32_t gReflowControlCntRQ[MAX_REFLOW_CNT]; +static int32_t gReflowControlCnt[MAX_REFLOW_CNT]; +static int32_t gReflowInx = -1; + +# define REFLOW_COUNTER() \ + if (mReflowId > -1) gReflowControlCnt[mReflowId]++; + +# define REFLOW_COUNTER_REQUEST() \ + if (mReflowId > -1) gReflowControlCntRQ[mReflowId]++; + +# define REFLOW_COUNTER_DUMP(__desc) \ + if (mReflowId > -1) { \ + gTotalReqs += gReflowControlCntRQ[mReflowId]; \ + gTotalReflows += gReflowControlCnt[mReflowId]; \ + printf("** Id:%5d %s RF: %d RQ: %d %d/%d %5.2f\n", mReflowId, \ + (__desc), gReflowControlCnt[mReflowId], \ + gReflowControlCntRQ[mReflowId], gTotalReflows, gTotalReqs, \ + float(gTotalReflows) / float(gTotalReqs) * 100.0f); \ + } + +# define REFLOW_COUNTER_INIT() \ + if (gReflowInx < MAX_REFLOW_CNT) { \ + gReflowInx++; \ + mReflowId = gReflowInx; \ + gReflowControlCnt[mReflowId] = 0; \ + gReflowControlCntRQ[mReflowId] = 0; \ + } else { \ + mReflowId = -1; \ + } + +// reflow messages +# define REFLOW_DEBUG_MSG(_msg1) printf((_msg1)) +# define REFLOW_DEBUG_MSG2(_msg1, _msg2) printf((_msg1), (_msg2)) +# define REFLOW_DEBUG_MSG3(_msg1, _msg2, _msg3) \ + printf((_msg1), (_msg2), (_msg3)) +# define REFLOW_DEBUG_MSG4(_msg1, _msg2, _msg3, _msg4) \ + printf((_msg1), (_msg2), (_msg3), (_msg4)) + +#else //------------- + +# define REFLOW_COUNTER_REQUEST() +# define REFLOW_COUNTER() +# define REFLOW_COUNTER_DUMP(__desc) +# define REFLOW_COUNTER_INIT() + +# define REFLOW_DEBUG_MSG(_msg) +# define REFLOW_DEBUG_MSG2(_msg1, _msg2) +# define REFLOW_DEBUG_MSG3(_msg1, _msg2, _msg3) +# define REFLOW_DEBUG_MSG4(_msg1, _msg2, _msg3, _msg4) + +#endif + +//------------------------------------------ +// This is for being VERY noisy +//------------------------------------------ +#ifdef DO_VERY_NOISY +# define REFLOW_NOISY_MSG(_msg1) printf((_msg1)) +# define REFLOW_NOISY_MSG2(_msg1, _msg2) printf((_msg1), (_msg2)) +# define REFLOW_NOISY_MSG3(_msg1, _msg2, _msg3) \ + printf((_msg1), (_msg2), (_msg3)) +# define REFLOW_NOISY_MSG4(_msg1, _msg2, _msg3, _msg4) \ + printf((_msg1), (_msg2), (_msg3), (_msg4)) +#else +# define REFLOW_NOISY_MSG(_msg) +# define REFLOW_NOISY_MSG2(_msg1, _msg2) +# define REFLOW_NOISY_MSG3(_msg1, _msg2, _msg3) +# define REFLOW_NOISY_MSG4(_msg1, _msg2, _msg3, _msg4) +#endif + +//------------------------------------------ +// Displays value in pixels or twips +//------------------------------------------ +#ifdef DO_PIXELS +# define PX(__v) __v / 15 +#else +# define PX(__v) __v +#endif + +//------------------------------------------------------ +//-- Done with macros +//------------------------------------------------------ + +nsComboboxControlFrame::nsComboboxControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsBlockFrame(aStyle, aPresContext, kClassID), + mDisplayFrame(nullptr), + mButtonFrame(nullptr), + mDisplayISize(0), + mMaxDisplayISize(0), + mRecentSelectedIndex(NS_SKIP_NOTIFY_INDEX), + mDisplayedIndex(-1), + mInRedisplayText(false), + mIsOpenInParentProcess(false){REFLOW_COUNTER_INIT()} + + //-------------------------------------------------------------- + nsComboboxControlFrame::~nsComboboxControlFrame() { + REFLOW_COUNTER_DUMP("nsCCF"); +} + +//-------------------------------------------------------------- + +NS_QUERYFRAME_HEAD(nsComboboxControlFrame) + NS_QUERYFRAME_ENTRY(nsComboboxControlFrame) + NS_QUERYFRAME_ENTRY(nsIFormControlFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) + NS_QUERYFRAME_ENTRY(nsISelectControlFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsBlockFrame) + +#ifdef ACCESSIBILITY +a11y::AccType nsComboboxControlFrame::AccessibleType() { + return a11y::eHTMLComboboxType; +} +#endif + +void nsComboboxControlFrame::SetFocus(bool aOn, bool aRepaint) { + // This is needed on a temporary basis. It causes the focus + // rect to be drawn. This is much faster than ReResolvingStyle + // Bug 32920 + InvalidateFrame(); +} + +nsPoint nsComboboxControlFrame::GetCSSTransformTranslation() { + nsIFrame* frame = this; + bool is3DTransform = false; + Matrix transform; + while (frame) { + nsIFrame* parent; + Matrix4x4Flagged ctm = frame->GetTransformMatrix( + ViewportType::Layout, RelativeTo{nullptr}, &parent); + Matrix matrix; + if (ctm.Is2D(&matrix)) { + transform = transform * matrix; + } else { + is3DTransform = true; + break; + } + frame = parent; + } + nsPoint translation; + if (!is3DTransform && !transform.HasNonTranslation()) { + nsPresContext* pc = PresContext(); + // To get the translation introduced only by transforms we subtract the + // regular non-transform translation. + nsRootPresContext* rootPC = pc->GetRootPresContext(); + if (rootPC) { + int32_t apd = pc->AppUnitsPerDevPixel(); + translation.x = NSFloatPixelsToAppUnits(transform._31, apd); + translation.y = NSFloatPixelsToAppUnits(transform._32, apd); + translation -= GetOffsetToCrossDoc(rootPC->PresShell()->GetRootFrame()); + } + } + return translation; +} + +//---------------------------------------------------------- +// +//---------------------------------------------------------- +#ifdef DO_REFLOW_DEBUG +static int myCounter = 0; + +static void printSize(char* aDesc, nscoord aSize) { + printf(" %s: ", aDesc); + if (aSize == NS_UNCONSTRAINEDSIZE) { + printf("UC"); + } else { + printf("%d", PX(aSize)); + } +} +#endif + +//------------------------------------------------------------------- +//-- Main Reflow for the Combobox +//------------------------------------------------------------------- + +bool nsComboboxControlFrame::HasDropDownButton() const { + const nsStyleDisplay* disp = StyleDisplay(); + // FIXME(emilio): Blink also shows this for menulist-button and such... Seems + // more similar to our mac / linux implementation. + return disp->EffectiveAppearance() == StyleAppearance::Menulist && + (!IsThemed(disp) || + PresContext()->Theme()->ThemeNeedsComboboxDropmarker()); +} + +nscoord nsComboboxControlFrame::DropDownButtonISize() { + if (!HasDropDownButton()) { + return 0; + } + + nsPresContext* pc = PresContext(); + LayoutDeviceIntSize dropdownButtonSize = pc->Theme()->GetMinimumWidgetSize( + pc, this, StyleAppearance::MozMenulistArrowButton); + return pc->DevPixelsToAppUnits(dropdownButtonSize.width); +} + +int32_t nsComboboxControlFrame::CharCountOfLargestOptionForInflation() const { + uint32_t maxLength = 0; + nsAutoString label; + for (auto i : IntegerRange(Select().Options()->Length())) { + GetOptionText(i, label); + maxLength = std::max( + maxLength, + nsTextFrameUtils::ComputeApproximateLengthWithWhitespaceCompression( + label, StyleText())); + } + if (MOZ_UNLIKELY(maxLength > uint32_t(INT32_MAX))) { + return INT32_MAX; + } + return int32_t(maxLength); +} + +nscoord nsComboboxControlFrame::GetLongestOptionISize( + gfxContext* aRenderingContext) const { + // Compute the width of each option's (potentially text-transformed) text, + // and use the widest one as part of our intrinsic size. + nscoord maxOptionSize = 0; + nsAutoString label; + nsAutoString transformedLabel; + RefPtr<nsFontMetrics> fm = + nsLayoutUtils::GetInflatedFontMetricsForFrame(this); + const nsStyleText* textStyle = StyleText(); + auto textTransform = textStyle->mTextTransform.IsNone() + ? Nothing() + : Some(textStyle->mTextTransform); + nsAtom* language = StyleFont()->mLanguage; + AutoTArray<bool, 50> charsToMergeArray; + AutoTArray<bool, 50> deletedCharsArray; + for (auto i : IntegerRange(Select().Options()->Length())) { + GetOptionText(i, label); + const nsAutoString* stringToUse = &label; + if (textTransform || + textStyle->mWebkitTextSecurity != StyleTextSecurity::None) { + transformedLabel.Truncate(); + charsToMergeArray.SetLengthAndRetainStorage(0); + deletedCharsArray.SetLengthAndRetainStorage(0); + nsCaseTransformTextRunFactory::TransformString( + label, transformedLabel, textTransform, + textStyle->TextSecurityMaskChar(), + /* aCaseTransformsOnly = */ false, language, charsToMergeArray, + deletedCharsArray); + stringToUse = &transformedLabel; + } + maxOptionSize = std::max(maxOptionSize, + nsLayoutUtils::AppUnitWidthOfStringBidi( + *stringToUse, this, *fm, *aRenderingContext)); + } + if (maxOptionSize) { + // HACK: Add one app unit to workaround silly Netgear router styling, see + // bug 1769580. In practice since this comes from font metrics is unlikely + // to be perceivable. + maxOptionSize += 1; + } + return maxOptionSize; +} + +nscoord nsComboboxControlFrame::GetIntrinsicISize(gfxContext* aRenderingContext, + IntrinsicISizeType aType) { + Maybe<nscoord> containISize = ContainIntrinsicISize(NS_UNCONSTRAINEDSIZE); + if (containISize && *containISize != NS_UNCONSTRAINEDSIZE) { + return *containISize; + } + + nscoord displayISize = mDisplayFrame->IntrinsicISizeOffsets().padding; + if (!containISize && !StyleContent()->mContent.IsNone()) { + displayISize += GetLongestOptionISize(aRenderingContext); + } + + // Add room for the dropmarker button (if there is one). + displayISize += DropDownButtonISize(); + return displayISize; +} + +nscoord nsComboboxControlFrame::GetMinISize(gfxContext* aRenderingContext) { + nscoord minISize; + DISPLAY_MIN_INLINE_SIZE(this, minISize); + minISize = GetIntrinsicISize(aRenderingContext, IntrinsicISizeType::MinISize); + return minISize; +} + +nscoord nsComboboxControlFrame::GetPrefISize(gfxContext* aRenderingContext) { + nscoord prefISize; + DISPLAY_PREF_INLINE_SIZE(this, prefISize); + prefISize = + GetIntrinsicISize(aRenderingContext, IntrinsicISizeType::PrefISize); + return prefISize; +} + +dom::HTMLSelectElement& nsComboboxControlFrame::Select() const { + return *static_cast<dom::HTMLSelectElement*>(GetContent()); +} + +void nsComboboxControlFrame::GetOptionText(uint32_t aIndex, + nsAString& aText) const { + aText.Truncate(); + if (Element* el = Select().Options()->GetElementAt(aIndex)) { + static_cast<dom::HTMLOptionElement*>(el)->GetRenderedLabel(aText); + } +} + +void nsComboboxControlFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MarkInReflow(); + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + // Constraints we try to satisfy: + + // 1) Default inline size of button is the vertical scrollbar size + // 2) If the inline size of button is bigger than our inline size, set + // inline size of button to 0. + // 3) Default block size of button is block size of display area + // 4) Inline size of display area is whatever is left over from our + // inline size after allocating inline size for the button. + + if (!mDisplayFrame) { + NS_ERROR("Why did the frame constructor allow this to happen? Fix it!!"); + return; + } + + // Make sure the displayed text is the same as the selected option, + // bug 297389. + mDisplayedIndex = Select().SelectedIndex(); + + // In dropped down mode the "selected index" is the hovered menu item, + // we want the last selected item which is |mDisplayedIndex| in this case. + RedisplayText(); + + WritingMode wm = aReflowInput.GetWritingMode(); + + // Check if the theme specifies a minimum size for the dropdown button + // first. + const nscoord buttonISize = DropDownButtonISize(); + const auto borderPadding = aReflowInput.ComputedLogicalBorderPadding(wm); + const auto padding = aReflowInput.ComputedLogicalPadding(wm); + const auto border = borderPadding - padding; + + mDisplayISize = aReflowInput.ComputedISize() - buttonISize; + mMaxDisplayISize = mDisplayISize + padding.IEnd(wm); + + nsBlockFrame::Reflow(aPresContext, aDesiredSize, aReflowInput, aStatus); + + // The button should occupy the same space as a scrollbar, and its position + // starts from the border edge. + if (mButtonFrame) { + LogicalRect buttonRect(wm); + buttonRect.IStart(wm) = borderPadding.IStart(wm) + mMaxDisplayISize; + buttonRect.BStart(wm) = border.BStart(wm); + + buttonRect.ISize(wm) = buttonISize; + buttonRect.BSize(wm) = mDisplayFrame->BSize(wm) + padding.BStartEnd(wm); + + const nsSize containerSize = aDesiredSize.PhysicalSize(); + mButtonFrame->SetRect(buttonRect, containerSize); + } + + if (!aStatus.IsInlineBreakBefore() && !aStatus.IsFullyComplete()) { + // This frame didn't fit inside a fragmentation container. Splitting + // a nsComboboxControlFrame makes no sense, so we override the status here. + aStatus.Reset(); + } +} + +void nsComboboxControlFrame::Init(nsIContent* aContent, + nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + nsBlockFrame::Init(aContent, aParent, aPrevInFlow); + + mEventListener = new HTMLSelectEventListener( + Select(), HTMLSelectEventListener::SelectType::Combobox); +} + +#ifdef DEBUG_FRAME_DUMP +nsresult nsComboboxControlFrame::GetFrameName(nsAString& aResult) const { + return MakeFrameName(u"ComboboxControl"_ns, aResult); +} +#endif + +/////////////////////////////////////////////////////////////// + +nsresult nsComboboxControlFrame::RedisplaySelectedText() { + nsAutoScriptBlocker scriptBlocker; + mDisplayedIndex = Select().SelectedIndex(); + return RedisplayText(); +} + +nsresult nsComboboxControlFrame::RedisplayText() { + nsString previewValue; + nsString previousText(mDisplayedOptionTextOrPreview); + + Select().GetPreviewValue(previewValue); + // Get the text to display + if (!previewValue.IsEmpty()) { + mDisplayedOptionTextOrPreview = previewValue; + } else if (mDisplayedIndex != -1 && !StyleContent()->mContent.IsNone()) { + GetOptionText(mDisplayedIndex, mDisplayedOptionTextOrPreview); + } else { + mDisplayedOptionTextOrPreview.Truncate(); + } + + REFLOW_DEBUG_MSG2( + "RedisplayText \"%s\"\n", + NS_LossyConvertUTF16toASCII(mDisplayedOptionTextOrPreview).get()); + + // Send reflow command because the new text maybe larger + nsresult rv = NS_OK; + if (mDisplayContent && !previousText.Equals(mDisplayedOptionTextOrPreview)) { + // Don't call ActuallyDisplayText(true) directly here since that + // could cause recursive frame construction. See bug 283117 and the comment + // in HandleRedisplayTextEvent() below. + + // Revoke outstanding events to avoid out-of-order events which could mean + // displaying the wrong text. + mRedisplayTextEvent.Revoke(); + + NS_ASSERTION(!nsContentUtils::IsSafeToRunScript(), + "If we happen to run our redisplay event now, we might kill " + "ourselves!"); + + mRedisplayTextEvent = new RedisplayTextEvent(this); + nsContentUtils::AddScriptRunner(mRedisplayTextEvent.get()); + } + return rv; +} + +void nsComboboxControlFrame::HandleRedisplayTextEvent() { + // First, make sure that the content model is up to date and we've + // constructed the frames for all our content in the right places. + // Otherwise they'll end up under the wrong insertion frame when we + // ActuallyDisplayText, since that flushes out the content sink by + // calling SetText on a DOM node with aNotify set to true. See bug + // 289730. + AutoWeakFrame weakThis(this); + PresContext()->Document()->FlushPendingNotifications( + FlushType::ContentAndNotify); + if (!weakThis.IsAlive()) return; + + // Redirect frame insertions during this method (see + // GetContentInsertionFrame()) so that any reframing that the frame + // constructor forces upon us is inserted into the correct parent + // (mDisplayFrame). See bug 282607. + MOZ_ASSERT(!mInRedisplayText, "Nested RedisplayText"); + mInRedisplayText = true; + mRedisplayTextEvent.Forget(); + + ActuallyDisplayText(true); + if (!weakThis.IsAlive()) { + return; + } + + // XXXbz This should perhaps be IntrinsicDirty::None. Check. + PresShell()->FrameNeedsReflow(mDisplayFrame, + IntrinsicDirty::FrameAncestorsAndDescendants, + NS_FRAME_IS_DIRTY); + + mInRedisplayText = false; +} + +void nsComboboxControlFrame::ActuallyDisplayText(bool aNotify) { + RefPtr<nsTextNode> displayContent = mDisplayContent; + if (mDisplayedOptionTextOrPreview.IsEmpty()) { + // Have to use a space character of some sort for line-block-size + // calculations to be right. Also, the space character must be zero-width + // in order for the the inline-size calculations to be consistent between + // size-contained comboboxes vs. empty comboboxes. + // + // XXXdholbert Does this space need to be "non-breaking"? I'm not sure + // if it matters, but we previously had a comment here (added in 2002) + // saying "Have to use a non-breaking space for line-height calculations + // to be right". So I'll stick with a non-breaking space for now... + static const char16_t space = 0xFEFF; + displayContent->SetText(&space, 1, aNotify); + } else { + displayContent->SetText(mDisplayedOptionTextOrPreview, aNotify); + } +} + +int32_t nsComboboxControlFrame::GetIndexOfDisplayArea() { + return mDisplayedIndex; +} + +bool nsComboboxControlFrame::IsDroppedDown() const { + return Select().OpenInParentProcess(); +} + +//---------------------------------------------------------------------- +// nsISelectControlFrame +//---------------------------------------------------------------------- +NS_IMETHODIMP +nsComboboxControlFrame::DoneAddingChildren(bool aIsDone) { return NS_OK; } + +NS_IMETHODIMP +nsComboboxControlFrame::AddOption(int32_t aIndex) { + if (aIndex <= mDisplayedIndex) { + ++mDisplayedIndex; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsComboboxControlFrame::RemoveOption(int32_t aIndex) { + if (Select().Options()->Length()) { + if (aIndex < mDisplayedIndex) { + --mDisplayedIndex; + } else if (aIndex == mDisplayedIndex) { + mDisplayedIndex = 0; // IE6 compat + RedisplayText(); + } + } else { + // If we removed the last option, we need to blank things out + mDisplayedIndex = -1; + RedisplayText(); + } + return NS_OK; +} + +NS_IMETHODIMP_(void) +nsComboboxControlFrame::OnSetSelectedIndex(int32_t aOldIndex, + int32_t aNewIndex) { + nsAutoScriptBlocker scriptBlocker; + mDisplayedIndex = aNewIndex; + RedisplayText(); +} + +// End nsISelectControlFrame +//---------------------------------------------------------------------- + +nsresult nsComboboxControlFrame::HandleEvent(nsPresContext* aPresContext, + WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) { + NS_ENSURE_ARG_POINTER(aEventStatus); + + if (nsEventStatus_eConsumeNoDefault == *aEventStatus) { + return NS_OK; + } + + if (mContent->AsElement()->State().HasState(dom::ElementState::DISABLED)) { + return NS_OK; + } + + // If we have style that affects how we are selected, feed event down to + // nsIFrame::HandleEvent so that selection takes place when appropriate. + if (IsContentDisabled()) { + return nsBlockFrame::HandleEvent(aPresContext, aEvent, aEventStatus); + } + return NS_OK; +} + +nsContainerFrame* nsComboboxControlFrame::GetContentInsertionFrame() { + return mInRedisplayText ? mDisplayFrame : nullptr; +} + +void nsComboboxControlFrame::AppendDirectlyOwnedAnonBoxes( + nsTArray<OwnedAnonBox>& aResult) { + aResult.AppendElement(OwnedAnonBox(mDisplayFrame)); +} + +nsresult nsComboboxControlFrame::CreateAnonymousContent( + nsTArray<ContentInfo>& aElements) { + // The frames used to display the combo box and the button used to popup the + // dropdown list are created through anonymous content. The dropdown list is + // not created through anonymous content because its frame is initialized + // specifically for the drop-down case and it is placed a special list + // referenced through NS_COMBO_FRAME_POPUP_LIST_INDEX to keep separate from + // the layout of the display and button. + // + // Note: The value attribute of the display content is set when an item is + // selected in the dropdown list. If the content specified below does not + // honor the value attribute than nothing will be displayed. + + // For now the content that is created corresponds to two input buttons. It + // would be better to create the tag as something other than input, but then + // there isn't any way to create a button frame since it isn't possible to set + // the display type in CSS2 to create a button frame. + + // create content used for display + // nsAtom* tag = NS_Atomize("mozcombodisplay"); + + // Add a child text content node for the label + + nsNodeInfoManager* nimgr = mContent->NodeInfo()->NodeInfoManager(); + + mDisplayContent = new (nimgr) nsTextNode(nimgr); + + // set the value of the text node + mDisplayedIndex = Select().SelectedIndex(); + if (mDisplayedIndex != -1) { + GetOptionText(mDisplayedIndex, mDisplayedOptionTextOrPreview); + } + ActuallyDisplayText(false); + + aElements.AppendElement(mDisplayContent); + if (HasDropDownButton()) { + mButtonContent = mContent->OwnerDoc()->CreateHTMLElement(nsGkAtoms::button); + if (!mButtonContent) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // make someone to listen to the button. + mButtonContent->SetAttr(kNameSpaceID_None, nsGkAtoms::type, u"button"_ns, + false); + // Set tabindex="-1" so that the button is not tabbable + mButtonContent->SetAttr(kNameSpaceID_None, nsGkAtoms::tabindex, u"-1"_ns, + false); + aElements.AppendElement(mButtonContent); + } + + return NS_OK; +} + +void nsComboboxControlFrame::AppendAnonymousContentTo( + nsTArray<nsIContent*>& aElements, uint32_t aFilter) { + if (mDisplayContent) { + aElements.AppendElement(mDisplayContent); + } + + if (mButtonContent) { + aElements.AppendElement(mButtonContent); + } +} + +nsIContent* nsComboboxControlFrame::GetDisplayNode() const { + return mDisplayContent; +} + +// XXXbz this is a for-now hack. Now that display:inline-block works, +// need to revisit this. +class nsComboboxDisplayFrame final : public nsBlockFrame { + public: + NS_DECL_FRAMEARENA_HELPERS(nsComboboxDisplayFrame) + + nsComboboxDisplayFrame(ComputedStyle* aStyle, + nsComboboxControlFrame* aComboBox) + : nsBlockFrame(aStyle, aComboBox->PresContext(), kClassID), + mComboBox(aComboBox) {} + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const final { + return MakeFrameName(u"ComboboxDisplay"_ns, aResult); + } +#endif + + bool IsFrameOfType(uint32_t aFlags) const final { + return nsBlockFrame::IsFrameOfType(aFlags & + ~(nsIFrame::eReplacedContainsBlock)); + } + + void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, nsReflowStatus& aStatus) final; + + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) final; + + protected: + nsComboboxControlFrame* mComboBox; +}; + +NS_IMPL_FRAMEARENA_HELPERS(nsComboboxDisplayFrame) + +void nsComboboxDisplayFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + MOZ_ASSERT(aReflowInput.mParentReflowInput && + aReflowInput.mParentReflowInput->mFrame == mComboBox, + "Combobox's frame tree is wrong!"); + + ReflowInput state(aReflowInput); + if (state.ComputedBSize() == NS_UNCONSTRAINEDSIZE) { + state.SetLineHeight(state.mParentReflowInput->GetLineHeight()); + } + const WritingMode wm = aReflowInput.GetWritingMode(); + const LogicalMargin bp = state.ComputedLogicalBorderPadding(wm); + MOZ_ASSERT(bp.BStartEnd(wm) == 0, + "We shouldn't have border and padding in the block axis in UA!"); + nscoord inlineBp = bp.IStartEnd(wm); + nscoord computedISize = mComboBox->mDisplayISize - inlineBp; + + // Other UAs ignore padding in some (but not all) platforms for (themed only) + // comboboxes. Instead of doing that, we prevent that padding if present from + // clipping the display text, by enforcing the display text minimum size in + // that situation. + const bool shouldHonorMinISize = + mComboBox->StyleDisplay()->EffectiveAppearance() == + StyleAppearance::Menulist; + if (shouldHonorMinISize) { + computedISize = std::max(state.ComputedMinISize(), computedISize); + // Don't let this size go over mMaxDisplayISize, since that'd be + // observable via clientWidth / scrollWidth. + computedISize = + std::min(computedISize, mComboBox->mMaxDisplayISize - inlineBp); + } + + state.SetComputedISize(std::max(0, computedISize)); + nsBlockFrame::Reflow(aPresContext, aDesiredSize, state, aStatus); + aStatus.Reset(); // this type of frame can't be split +} + +void nsComboboxDisplayFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + nsDisplayListCollection set(aBuilder); + nsBlockFrame::BuildDisplayList(aBuilder, set); + + // remove background items if parent frame is themed + if (mComboBox->IsThemed()) { + set.BorderBackground()->DeleteAll(aBuilder); + } + + set.MoveTo(aLists); +} + +nsIFrame* nsComboboxControlFrame::CreateFrameForDisplayNode() { + MOZ_ASSERT(mDisplayContent); + + // Get PresShell + mozilla::PresShell* ps = PresShell(); + ServoStyleSet* styleSet = ps->StyleSet(); + + // create the ComputedStyle for the anonymous block frame and text frame + RefPtr<ComputedStyle> computedStyle = + styleSet->ResolveInheritingAnonymousBoxStyle( + PseudoStyleType::mozDisplayComboboxControlFrame, mComputedStyle); + + RefPtr<ComputedStyle> textComputedStyle = + styleSet->ResolveStyleForText(mDisplayContent, mComputedStyle); + + // Start by creating our anonymous block frame + mDisplayFrame = new (ps) nsComboboxDisplayFrame(computedStyle, this); + mDisplayFrame->Init(mContent, this, nullptr); + + // Create a text frame and put it inside the block frame + nsIFrame* textFrame = NS_NewTextFrame(ps, textComputedStyle); + + // initialize the text frame + textFrame->Init(mDisplayContent, mDisplayFrame, nullptr); + mDisplayContent->SetPrimaryFrame(textFrame); + + mDisplayFrame->SetInitialChildList(FrameChildListID::Principal, + nsFrameList(textFrame, textFrame)); + return mDisplayFrame; +} + +void nsComboboxControlFrame::Destroy(DestroyContext& aContext) { + // Revoke any pending RedisplayTextEvent + mRedisplayTextEvent.Revoke(); + + mEventListener->Detach(); + + // Cleanup frames in popup child list + aContext.AddAnonymousContent(mDisplayContent.forget()); + aContext.AddAnonymousContent(mButtonContent.forget()); + nsBlockFrame::Destroy(aContext); +} + +const nsFrameList& nsComboboxControlFrame::GetChildList( + ChildListID aListID) const { + return nsBlockFrame::GetChildList(aListID); +} + +void nsComboboxControlFrame::GetChildLists(nsTArray<ChildList>* aLists) const { + nsBlockFrame::GetChildLists(aLists); +} + +void nsComboboxControlFrame::SetInitialChildList(ChildListID aListID, + nsFrameList&& aChildList) { + for (nsIFrame* f : aChildList) { + MOZ_ASSERT(f->GetParent() == this, "Unexpected parent"); + nsCOMPtr<nsIFormControl> formControl = do_QueryInterface(f->GetContent()); + if (formControl && + formControl->ControlType() == FormControlType::ButtonButton) { + mButtonFrame = f; + break; + } + } + nsBlockFrame::SetInitialChildList(aListID, std::move(aChildList)); +} + +namespace mozilla { + +class nsDisplayComboboxFocus : public nsPaintedDisplayItem { + public: + nsDisplayComboboxFocus(nsDisplayListBuilder* aBuilder, + nsComboboxControlFrame* aFrame) + : nsPaintedDisplayItem(aBuilder, aFrame) { + MOZ_COUNT_CTOR(nsDisplayComboboxFocus); + } + MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayComboboxFocus) + + void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override; + NS_DISPLAY_DECL_NAME("ComboboxFocus", TYPE_COMBOBOX_FOCUS) +}; + +void nsDisplayComboboxFocus::Paint(nsDisplayListBuilder* aBuilder, + gfxContext* aCtx) { + static_cast<nsComboboxControlFrame*>(mFrame)->PaintFocus( + *aCtx->GetDrawTarget(), ToReferenceFrame()); +} + +} // namespace mozilla + +void nsComboboxControlFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + if (aBuilder->IsForEventDelivery()) { + // Don't allow children to receive events. + // REVIEW: following old GetFrameForPoint + DisplayBorderBackgroundOutline(aBuilder, aLists); + } else { + // REVIEW: Our in-flow child frames are inline-level so they will paint in + // our content list, so we don't need to mess with layers. + nsBlockFrame::BuildDisplayList(aBuilder, aLists); + } + + // draw a focus indicator only when focus rings should be drawn + if (mContent->AsElement()->State().HasState(dom::ElementState::FOCUSRING)) { + nsPresContext* pc = PresContext(); + const nsStyleDisplay* disp = StyleDisplay(); + if (IsThemed(disp) && + pc->Theme()->ThemeWantsButtonInnerFocusRing( + this, disp->EffectiveAppearance()) && + mDisplayFrame && IsVisibleForPainting()) { + aLists.Content()->AppendNewToTop<nsDisplayComboboxFocus>(aBuilder, this); + } + } + + DisplaySelectionOverlay(aBuilder, aLists.Content()); +} + +void nsComboboxControlFrame::PaintFocus(DrawTarget& aDrawTarget, nsPoint aPt) { + /* Do we need to do anything? */ + dom::ElementState state = mContent->AsElement()->State(); + if (state.HasState(dom::ElementState::DISABLED) || + !state.HasState(dom::ElementState::FOCUS)) { + return; + } + + int32_t appUnitsPerDevPixel = PresContext()->AppUnitsPerDevPixel(); + + nsRect clipRect = mDisplayFrame->GetRect() + aPt; + aDrawTarget.PushClipRect( + NSRectToSnappedRect(clipRect, appUnitsPerDevPixel, aDrawTarget)); + + StrokeOptions strokeOptions; + nsLayoutUtils::InitDashPattern(strokeOptions, StyleBorderStyle::Dotted); + ColorPattern color(ToDeviceColor(StyleText()->mColor)); + nscoord onePixel = nsPresContext::CSSPixelsToAppUnits(1); + clipRect.width -= onePixel; + clipRect.height -= onePixel; + Rect r = ToRect(nsLayoutUtils::RectToGfxRect(clipRect, appUnitsPerDevPixel)); + StrokeSnappedEdgesOfRect(r, aDrawTarget, color, strokeOptions); + + aDrawTarget.PopClip(); +} + +//--------------------------------------------------------- +// gets the content (an option) by index and then set it as +// being selected or not selected +//--------------------------------------------------------- +NS_IMETHODIMP +nsComboboxControlFrame::OnOptionSelected(int32_t aIndex, bool aSelected) { + if (aSelected) { + nsAutoScriptBlocker blocker; + mDisplayedIndex = aIndex; + RedisplayText(); + } else { + AutoWeakFrame weakFrame(this); + RedisplaySelectedText(); + if (weakFrame.IsAlive()) { + FireValueChangeEvent(); // Fire after old option is unselected + } + } + return NS_OK; +} + +void nsComboboxControlFrame::FireValueChangeEvent() { + // Fire ValueChange event to indicate data value of combo box has changed + nsContentUtils::AddScriptRunner(new AsyncEventDispatcher( + mContent, u"ValueChange"_ns, CanBubble::eYes, ChromeOnlyDispatch::eNo)); +} diff --git a/layout/forms/nsComboboxControlFrame.h b/layout/forms/nsComboboxControlFrame.h new file mode 100644 index 0000000000..a34652bebe --- /dev/null +++ b/layout/forms/nsComboboxControlFrame.h @@ -0,0 +1,250 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsComboboxControlFrame_h___ +#define nsComboboxControlFrame_h___ + +#ifdef DEBUG_evaughan +// #define DEBUG_rods +#endif + +#ifdef DEBUG_rods +// #define DO_REFLOW_DEBUG +// #define DO_REFLOW_COUNTER +// #define DO_UNCONSTRAINED_CHECK +// #define DO_PIXELS +// #define DO_NEW_REFLOW +#endif + +// Mark used to indicate when onchange has been fired for current combobox item +#define NS_SKIP_NOTIFY_INDEX -2 + +#include "mozilla/Attributes.h" +#include "nsBlockFrame.h" +#include "nsIFormControlFrame.h" +#include "nsIAnonymousContentCreator.h" +#include "nsISelectControlFrame.h" +#include "nsIRollupListener.h" +#include "nsThreadUtils.h" + +class nsComboboxDisplayFrame; +class nsTextNode; + +namespace mozilla { +class PresShell; +class HTMLSelectEventListener; +namespace dom { +class HTMLSelectElement; +} + +namespace gfx { +class DrawTarget; +} // namespace gfx +} // namespace mozilla + +class nsComboboxControlFrame final : public nsBlockFrame, + public nsIFormControlFrame, + public nsIAnonymousContentCreator, + public nsISelectControlFrame { + using DrawTarget = mozilla::gfx::DrawTarget; + using Element = mozilla::dom::Element; + + public: + friend nsComboboxControlFrame* NS_NewComboboxControlFrame( + mozilla::PresShell* aPresShell, ComputedStyle* aStyle, + nsFrameState aFlags); + friend class nsComboboxDisplayFrame; + + explicit nsComboboxControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext); + ~nsComboboxControlFrame(); + + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsComboboxControlFrame) + + // nsIAnonymousContentCreator + nsresult CreateAnonymousContent(nsTArray<ContentInfo>& aElements) final; + void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) final; + + nsIContent* GetDisplayNode() const; + nsIFrame* CreateFrameForDisplayNode(); + +#ifdef ACCESSIBILITY + mozilla::a11y::AccType AccessibleType() final; +#endif + + nscoord GetMinISize(gfxContext* aRenderingContext) final; + + nscoord GetPrefISize(gfxContext* aRenderingContext) final; + + void Reflow(nsPresContext* aCX, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, nsReflowStatus& aStatus) final; + + MOZ_CAN_RUN_SCRIPT_BOUNDARY + nsresult HandleEvent(nsPresContext* aPresContext, + mozilla::WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) final; + + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) final; + + void PaintFocus(DrawTarget& aDrawTarget, nsPoint aPt); + + bool IsFrameOfType(uint32_t aFlags) const final { + return nsBlockFrame::IsFrameOfType( + aFlags & ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock)); + } + + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) final; + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const final; +#endif + void Destroy(DestroyContext&) final; + + void SetInitialChildList(ChildListID aListID, nsFrameList&& aChildList) final; + const nsFrameList& GetChildList(ChildListID aListID) const final; + void GetChildLists(nsTArray<ChildList>* aLists) const final; + + nsContainerFrame* GetContentInsertionFrame() final; + + // Return the dropdown and display frame. + void AppendDirectlyOwnedAnonBoxes(nsTArray<OwnedAnonBox>& aResult) final; + + // nsIFormControlFrame + nsresult SetFormProperty(nsAtom* aName, const nsAString& aValue) final { + return NS_OK; + } + + /** + * Inform the control that it got (or lost) focus. + * If it lost focus, the dropdown menu will be rolled up if needed, + * and FireOnChange() will be called. + * @param aOn true if got focus, false if lost focus. + * @param aRepaint if true then force repaint (NOTE: we always force repaint + * currently) + * @note This method might destroy |this|. + */ + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void SetFocus(bool aOn, bool aRepaint) final; + + /** + * Return the available space before and after this frame for + * placing the drop-down list, and the current 2D translation. + * Note that either or both can be less than or equal to zero, + * if both are then the drop-down should be closed. + */ + void GetAvailableDropdownSpace(mozilla::WritingMode aWM, nscoord* aBefore, + nscoord* aAfter, + mozilla::LogicalPoint* aTranslation); + int32_t GetIndexOfDisplayArea(); + /** + * @note This method might destroy |this|. + */ + nsresult RedisplaySelectedText(); + int32_t UpdateRecentIndex(int32_t aIndex); + + bool IsDroppedDown() const; + + // nsISelectControlFrame + NS_IMETHOD AddOption(int32_t index) final; + NS_IMETHOD RemoveOption(int32_t index) final; + NS_IMETHOD DoneAddingChildren(bool aIsDone) final; + NS_IMETHOD OnOptionSelected(int32_t aIndex, bool aSelected) final; + NS_IMETHOD_(void) + OnSetSelectedIndex(int32_t aOldIndex, int32_t aNewIndex) final; + + int32_t CharCountOfLargestOptionForInflation() const; + + protected: + friend class RedisplayTextEvent; + friend class nsAsyncResize; + friend class nsResizeDropdownAtFinalPosition; + + // Return true if we should render a dropdown button. + bool HasDropDownButton() const; + nscoord DropDownButtonISize(); + + enum DropDownPositionState { + // can't show the dropdown at its current position + eDropDownPositionSuppressed, + // a resize reflow is pending, don't show it yet + eDropDownPositionPendingResize, + // the dropdown has its final size and position and can be displayed here + eDropDownPositionFinal + }; + DropDownPositionState AbsolutelyPositionDropDown(); + + nscoord GetLongestOptionISize(gfxContext*) const; + + // Helper for GetMinISize/GetPrefISize + nscoord GetIntrinsicISize(gfxContext* aRenderingContext, + mozilla::IntrinsicISizeType aType); + + class RedisplayTextEvent : public mozilla::Runnable { + public: + NS_DECL_NSIRUNNABLE + explicit RedisplayTextEvent(nsComboboxControlFrame* c) + : mozilla::Runnable("nsComboboxControlFrame::RedisplayTextEvent"), + mControlFrame(c) {} + void Revoke() { mControlFrame = nullptr; } + + private: + nsComboboxControlFrame* mControlFrame; + }; + + void CheckFireOnChange(); + void FireValueChangeEvent(); + nsresult RedisplayText(); + void HandleRedisplayTextEvent(); + void ActuallyDisplayText(bool aNotify); + + // If our total transform to the root frame of the root document is only a 2d + // translation then return that translation, otherwise returns (0,0). + nsPoint GetCSSTransformTranslation(); + + mozilla::dom::HTMLSelectElement& Select() const; + void GetOptionText(uint32_t aIndex, nsAString& aText) const; + + RefPtr<nsTextNode> mDisplayContent; // Anonymous content used to display the + // current selection + RefPtr<Element> mButtonContent; // Anonymous content for the button + nsContainerFrame* mDisplayFrame; // frame to display selection + nsIFrame* mButtonFrame; // button frame + + // The inline size of our display area. Used by that frame's reflow + // to size to the full inline size except the drop-marker. + nscoord mDisplayISize; + // The maximum inline size of our display area, which is the + // nsComoboxControlFrame's border-box. + // + // Going over this would be observable via DOM APIs like client / scrollWidth. + nscoord mMaxDisplayISize; + + nsRevocableEventPtr<RedisplayTextEvent> mRedisplayTextEvent; + + int32_t mRecentSelectedIndex; + int32_t mDisplayedIndex; + nsString mDisplayedOptionTextOrPreview; + + RefPtr<mozilla::HTMLSelectEventListener> mEventListener; + + // See comment in HandleRedisplayTextEvent(). + bool mInRedisplayText; + bool mIsOpenInParentProcess; + + // static class data member for Bug 32920 + // only one control can be focused at a time + static nsComboboxControlFrame* sFocused; + +#ifdef DO_REFLOW_COUNTER + int32_t mReflowId; +#endif +}; + +#endif diff --git a/layout/forms/nsDateTimeControlFrame.cpp b/layout/forms/nsDateTimeControlFrame.cpp new file mode 100644 index 0000000000..d80f4ea2ce --- /dev/null +++ b/layout/forms/nsDateTimeControlFrame.cpp @@ -0,0 +1,201 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This frame type is used for input type=date, time, month, week, and + * datetime-local. + */ + +#include "nsDateTimeControlFrame.h" + +#include "mozilla/PresShell.h" +#include "nsLayoutUtils.h" +#include "nsTextControlFrame.h" + +using namespace mozilla; +using namespace mozilla::dom; + +nsIFrame* NS_NewDateTimeControlFrame(PresShell* aPresShell, + ComputedStyle* aStyle) { + return new (aPresShell) + nsDateTimeControlFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsDateTimeControlFrame) + +NS_QUERYFRAME_HEAD(nsDateTimeControlFrame) + NS_QUERYFRAME_ENTRY(nsDateTimeControlFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) + +nsDateTimeControlFrame::nsDateTimeControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsContainerFrame(aStyle, aPresContext, kClassID) {} + +nscoord nsDateTimeControlFrame::GetMinISize(gfxContext* aRenderingContext) { + nscoord result; + DISPLAY_MIN_INLINE_SIZE(this, result); + + nsIFrame* kid = mFrames.FirstChild(); + if (kid) { // display:none? + result = nsLayoutUtils::IntrinsicForContainer(aRenderingContext, kid, + IntrinsicISizeType::MinISize); + } else { + result = 0; + } + + return result; +} + +nscoord nsDateTimeControlFrame::GetPrefISize(gfxContext* aRenderingContext) { + nscoord result; + DISPLAY_PREF_INLINE_SIZE(this, result); + + nsIFrame* kid = mFrames.FirstChild(); + if (kid) { // display:none? + result = nsLayoutUtils::IntrinsicForContainer( + aRenderingContext, kid, IntrinsicISizeType::PrefISize); + } else { + result = 0; + } + + return result; +} + +Maybe<nscoord> nsDateTimeControlFrame::GetNaturalBaselineBOffset( + WritingMode aWM, BaselineSharingGroup aBaselineGroup, + BaselineExportContext) const { + return nsTextControlFrame::GetSingleLineTextControlBaseline( + this, mFirstBaseline, aWM, aBaselineGroup); +} + +void nsDateTimeControlFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MarkInReflow(); + + DO_GLOBAL_REFLOW_COUNT("nsDateTimeControlFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + NS_FRAME_TRACE( + NS_FRAME_TRACE_CALLS, + ("enter nsDateTimeControlFrame::Reflow: availSize=%d,%d", + aReflowInput.AvailableWidth(), aReflowInput.AvailableHeight())); + + NS_ASSERTION(mFrames.GetLength() <= 1, + "There should be no more than 1 frames"); + + const WritingMode myWM = aReflowInput.GetWritingMode(); + + { + auto baseline = nsTextControlFrame::ComputeBaseline( + this, aReflowInput, /* aForSingleLineControl = */ true); + mFirstBaseline = baseline.valueOr(NS_INTRINSIC_ISIZE_UNKNOWN); + if (baseline) { + aDesiredSize.SetBlockStartAscent(*baseline); + } + } + + // The ISize of our content box, which is the available ISize + // for our anonymous content: + const nscoord contentBoxISize = aReflowInput.ComputedISize(); + nscoord contentBoxBSize = aReflowInput.ComputedBSize(); + + // Figure out our border-box sizes as well (by adding borderPadding to + // content-box sizes): + const auto borderPadding = aReflowInput.ComputedLogicalBorderPadding(myWM); + const nscoord borderBoxISize = + contentBoxISize + borderPadding.IStartEnd(myWM); + + nscoord borderBoxBSize; + if (contentBoxBSize != NS_UNCONSTRAINEDSIZE) { + borderBoxBSize = contentBoxBSize + borderPadding.BStartEnd(myWM); + } // else, we'll figure out borderBoxBSize after we resolve contentBoxBSize. + + nsIFrame* inputAreaFrame = mFrames.FirstChild(); + if (!inputAreaFrame) { // display:none? + if (contentBoxBSize == NS_UNCONSTRAINEDSIZE) { + contentBoxBSize = 0; + borderBoxBSize = borderPadding.BStartEnd(myWM); + } + } else { + ReflowOutput childDesiredSize(aReflowInput); + + WritingMode wm = inputAreaFrame->GetWritingMode(); + LogicalSize availSize = aReflowInput.ComputedSize(wm); + availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE; + + ReflowInput childReflowInput(aPresContext, aReflowInput, inputAreaFrame, + availSize); + + // Convert input area margin into my own writing-mode (in case it differs): + LogicalMargin childMargin = childReflowInput.ComputedLogicalMargin(myWM); + + // offsets of input area frame within this frame: + LogicalPoint childOffset( + myWM, borderPadding.IStart(myWM) + childMargin.IStart(myWM), + borderPadding.BStart(myWM) + childMargin.BStart(myWM)); + + nsReflowStatus childStatus; + // We initially reflow the child with a dummy containerSize; positioning + // will be fixed later. + const nsSize dummyContainerSize; + ReflowChild(inputAreaFrame, aPresContext, childDesiredSize, + childReflowInput, myWM, childOffset, dummyContainerSize, + ReflowChildFlags::Default, childStatus); + MOZ_ASSERT(childStatus.IsFullyComplete(), + "We gave our child unconstrained available block-size, " + "so it should be complete"); + + nscoord childMarginBoxBSize = + childDesiredSize.BSize(myWM) + childMargin.BStartEnd(myWM); + + if (contentBoxBSize == NS_UNCONSTRAINEDSIZE) { + // We are intrinsically sized -- we should shrinkwrap the input area's + // block-size, or our line-height: + contentBoxBSize = + std::max(aReflowInput.GetLineHeight(), childMarginBoxBSize); + + // Make sure we obey min/max-bsize in the case when we're doing intrinsic + // sizing (we get it for free when we have a non-intrinsic + // aReflowInput.ComputedBSize()). Note that we do this before + // adjusting for borderpadding, since ComputedMaxBSize and + // ComputedMinBSize are content heights. + contentBoxBSize = + NS_CSS_MINMAX(contentBoxBSize, aReflowInput.ComputedMinBSize(), + aReflowInput.ComputedMaxBSize()); + + borderBoxBSize = contentBoxBSize + borderPadding.BStartEnd(myWM); + } + + // Center child in block axis + nscoord extraSpace = contentBoxBSize - childMarginBoxBSize; + childOffset.B(myWM) += std::max(0, extraSpace / 2); + + // Needed in FinishReflowChild, for logical-to-physical conversion: + nsSize borderBoxSize = + LogicalSize(myWM, borderBoxISize, borderBoxBSize).GetPhysicalSize(myWM); + + // Place the child + FinishReflowChild(inputAreaFrame, aPresContext, childDesiredSize, + &childReflowInput, myWM, childOffset, borderBoxSize, + ReflowChildFlags::Default); + } + + LogicalSize logicalDesiredSize(myWM, borderBoxISize, borderBoxBSize); + aDesiredSize.SetSize(myWM, logicalDesiredSize); + aDesiredSize.SetOverflowAreasToDesiredBounds(); + + if (inputAreaFrame) { + ConsiderChildOverflow(aDesiredSize.mOverflowAreas, inputAreaFrame); + } + + FinishAndStoreOverflow(&aDesiredSize); + + NS_FRAME_TRACE(NS_FRAME_TRACE_CALLS, + ("exit nsDateTimeControlFrame::Reflow: size=%d,%d", + aDesiredSize.Width(), aDesiredSize.Height())); +} diff --git a/layout/forms/nsDateTimeControlFrame.h b/layout/forms/nsDateTimeControlFrame.h new file mode 100644 index 0000000000..755cdd0933 --- /dev/null +++ b/layout/forms/nsDateTimeControlFrame.h @@ -0,0 +1,71 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This frame type is used for input type=date, time, month, week, and + * datetime-local. + * + * NOTE: some of the above-mentioned input types are still to-be-implemented. + * See nsCSSFrameConstructor::FindInputData, as well as bug 1286182 (date), + * bug 1306215 (month), bug 1306216 (week) and bug 1306217 (datetime-local). + */ + +#ifndef nsDateTimeControlFrame_h__ +#define nsDateTimeControlFrame_h__ + +#include "mozilla/Attributes.h" +#include "nsContainerFrame.h" +#include "nsIAnonymousContentCreator.h" +#include "nsCOMPtr.h" + +namespace mozilla { +class PresShell; +namespace dom { +struct DateTimeValue; +} // namespace dom +} // namespace mozilla + +class nsDateTimeControlFrame final : public nsContainerFrame { + typedef mozilla::dom::DateTimeValue DateTimeValue; + + explicit nsDateTimeControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext); + + public: + friend nsIFrame* NS_NewDateTimeControlFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsDateTimeControlFrame) + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"DateTimeControl"_ns, aResult); + } +#endif + + bool IsFrameOfType(uint32_t aFlags) const override { + return nsContainerFrame::IsFrameOfType( + aFlags & ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock)); + } + + // Reflow + nscoord GetMinISize(gfxContext* aRenderingContext) override; + + nscoord GetPrefISize(gfxContext* aRenderingContext) override; + + void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + Maybe<nscoord> GetNaturalBaselineBOffset( + mozilla::WritingMode aWM, BaselineSharingGroup aBaselineGroup, + BaselineExportContext) const override; + + nscoord mFirstBaseline = NS_INTRINSIC_ISIZE_UNKNOWN; +}; + +#endif // nsDateTimeControlFrame_h__ diff --git a/layout/forms/nsFieldSetFrame.cpp b/layout/forms/nsFieldSetFrame.cpp new file mode 100644 index 0000000000..539a1ff151 --- /dev/null +++ b/layout/forms/nsFieldSetFrame.cpp @@ -0,0 +1,938 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsFieldSetFrame.h" +#include "mozilla/dom/HTMLLegendElement.h" + +#include <algorithm> +#include "gfxContext.h" +#include "mozilla/Baseline.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/Likely.h" +#include "mozilla/PresShell.h" +#include "mozilla/Maybe.h" +#include "mozilla/webrender/WebRenderAPI.h" +#include "nsBlockFrame.h" +#include "nsCSSAnonBoxes.h" +#include "nsCSSFrameConstructor.h" +#include "nsCSSRendering.h" +#include "nsDisplayList.h" +#include "nsGkAtoms.h" +#include "nsIFrameInlines.h" +#include "nsIScrollableFrame.h" +#include "nsLayoutUtils.h" +#include "nsStyleConsts.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::layout; +using image::ImgDrawResult; + +nsContainerFrame* NS_NewFieldSetFrame(PresShell* aPresShell, + ComputedStyle* aStyle) { + return new (aPresShell) nsFieldSetFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsFieldSetFrame) +NS_QUERYFRAME_HEAD(nsFieldSetFrame) + NS_QUERYFRAME_ENTRY(nsFieldSetFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) + +nsFieldSetFrame::nsFieldSetFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsContainerFrame(aStyle, aPresContext, kClassID), + mLegendRect(GetWritingMode()) { + mLegendSpace = 0; +} + +nsRect nsFieldSetFrame::VisualBorderRectRelativeToSelf() const { + WritingMode wm = GetWritingMode(); + LogicalRect r(wm, LogicalPoint(wm, 0, 0), GetLogicalSize(wm)); + nsSize containerSize = r.Size(wm).GetPhysicalSize(wm); + nsIFrame* legend = GetLegend(); + if (legend && !GetPrevInFlow()) { + nscoord legendSize = legend->GetLogicalSize(wm).BSize(wm); + auto legendMargin = legend->GetLogicalUsedMargin(wm); + nscoord legendStartMargin = legendMargin.BStart(wm); + nscoord legendEndMargin = legendMargin.BEnd(wm); + nscoord border = GetUsedBorder().Side(wm.PhysicalSide(eLogicalSideBStart)); + // Calculate the offset from the border area block-axis start edge needed to + // center-align our border with the legend's border-box (in the block-axis). + nscoord off = (legendStartMargin + legendSize / 2) - border / 2; + // We don't want to display our border above our border area. + if (off > nscoord(0)) { + nscoord marginBoxSize = legendStartMargin + legendSize + legendEndMargin; + if (marginBoxSize > border) { + // We don't want to display our border below the legend's margin-box, + // so we align it to the block-axis end if that happens. + nscoord overflow = off + border - marginBoxSize; + if (overflow > nscoord(0)) { + off -= overflow; + } + r.BStart(wm) += off; + r.BSize(wm) -= off; + } + } + } + return r.GetPhysicalRect(wm, containerSize); +} + +nsContainerFrame* nsFieldSetFrame::GetInner() const { + for (nsIFrame* child : mFrames) { + if (child->Style()->GetPseudoType() == PseudoStyleType::fieldsetContent) { + return static_cast<nsContainerFrame*>(child); + } + } + return nullptr; +} + +nsIFrame* nsFieldSetFrame::GetLegend() const { + for (nsIFrame* child : mFrames) { + if (child->Style()->GetPseudoType() != PseudoStyleType::fieldsetContent) { + return child; + } + } + return nullptr; +} + +namespace mozilla { + +class nsDisplayFieldSetBorder final : public nsPaintedDisplayItem { + public: + nsDisplayFieldSetBorder(nsDisplayListBuilder* aBuilder, + nsFieldSetFrame* aFrame) + : nsPaintedDisplayItem(aBuilder, aFrame) { + MOZ_COUNT_CTOR(nsDisplayFieldSetBorder); + } + MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayFieldSetBorder) + + void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override; + bool CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) override; + virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder, + bool* aSnap) const override; + NS_DISPLAY_DECL_NAME("FieldSetBorder", TYPE_FIELDSET_BORDER_BACKGROUND) +}; + +void nsDisplayFieldSetBorder::Paint(nsDisplayListBuilder* aBuilder, + gfxContext* aCtx) { + Unused << static_cast<nsFieldSetFrame*>(mFrame)->PaintBorder( + aBuilder, *aCtx, ToReferenceFrame(), GetPaintRect(aBuilder, aCtx)); +} + +nsRect nsDisplayFieldSetBorder::GetBounds(nsDisplayListBuilder* aBuilder, + bool* aSnap) const { + // Just go ahead and claim our frame's overflow rect as the bounds, because we + // may have border-image-outset or other features that cause borders to extend + // outside the border rect. We could try to duplicate all the complexity + // nsDisplayBorder has here, but keeping things in sync would be a pain, and + // this code is not typically performance-sensitive. + *aSnap = false; + return Frame()->InkOverflowRectRelativeToSelf() + ToReferenceFrame(); +} + +bool nsDisplayFieldSetBorder::CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) { + auto frame = static_cast<nsFieldSetFrame*>(mFrame); + auto offset = ToReferenceFrame(); + Maybe<wr::SpaceAndClipChainHelper> clipOut; + + nsRect rect = frame->VisualBorderRectRelativeToSelf() + offset; + nsDisplayBoxShadowInner::CreateInsetBoxShadowWebRenderCommands( + aBuilder, aSc, rect, mFrame, rect); + + if (nsIFrame* legend = frame->GetLegend()) { + nsRect legendRect = legend->GetNormalRect() + offset; + + // Make sure we clip all of the border in case the legend is smaller. + nscoord borderTopWidth = frame->GetUsedBorder().top; + if (legendRect.height < borderTopWidth) { + legendRect.height = borderTopWidth; + legendRect.y = offset.y; + } + + if (!legendRect.IsEmpty()) { + // We need to clip out the part of the border where the legend would go + auto appUnitsPerDevPixel = frame->PresContext()->AppUnitsPerDevPixel(); + auto layoutRect = wr::ToLayoutRect(LayoutDeviceRect::FromAppUnits( + frame->InkOverflowRectRelativeToSelf() + offset, + appUnitsPerDevPixel)); + + wr::ComplexClipRegion region; + region.rect = wr::ToLayoutRect( + LayoutDeviceRect::FromAppUnits(legendRect, appUnitsPerDevPixel)); + region.mode = wr::ClipMode::ClipOut; + region.radii = wr::EmptyBorderRadius(); + + auto rect_clip = aBuilder.DefineRectClip(Nothing(), layoutRect); + auto complex_clip = aBuilder.DefineRoundedRectClip(Nothing(), region); + auto clipChain = + aBuilder.DefineClipChain({rect_clip, complex_clip}, true); + clipOut.emplace(aBuilder, clipChain); + } + } else { + rect = nsRect(offset, frame->GetRect().Size()); + } + + ImgDrawResult drawResult = nsCSSRendering::CreateWebRenderCommandsForBorder( + this, mFrame, rect, aBuilder, aResources, aSc, aManager, + aDisplayListBuilder); + if (drawResult == ImgDrawResult::NOT_SUPPORTED) { + return false; + } + return true; +}; + +} // namespace mozilla + +void nsFieldSetFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + // Paint our background and border in a special way. + // REVIEW: We don't really need to check frame emptiness here; if it's empty, + // the background/border display item won't do anything, and if it isn't + // empty, we need to paint the outline + if (!HasAnyStateBits(NS_FRAME_IS_OVERFLOW_CONTAINER) && + IsVisibleForPainting()) { + DisplayOutsetBoxShadowUnconditional(aBuilder, aLists.BorderBackground()); + + const nsRect rect = + VisualBorderRectRelativeToSelf() + aBuilder->ToReferenceFrame(this); + + nsDisplayBackgroundImage::AppendBackgroundItemsToTop( + aBuilder, this, rect, aLists.BorderBackground(), + /* aAllowWillPaintBorderOptimization = */ false); + + aLists.BorderBackground()->AppendNewToTop<nsDisplayFieldSetBorder>(aBuilder, + this); + + DisplayOutlineUnconditional(aBuilder, aLists); + + DO_GLOBAL_REFLOW_COUNT_DSP("nsFieldSetFrame"); + } + + if (GetPrevInFlow()) { + DisplayOverflowContainers(aBuilder, aLists); + } + + nsDisplayListCollection contentDisplayItems(aBuilder); + if (nsIFrame* inner = GetInner()) { + // Collect the inner frame's display items into their own collection. + // We need to be calling BuildDisplayList on it before the legend in + // case it contains out-of-flow frames whose placeholders are in the + // legend. However, we want the inner frame's display items to be + // after the legend's display items in z-order, so we need to save them + // and append them later. + BuildDisplayListForChild(aBuilder, inner, contentDisplayItems); + } + if (nsIFrame* legend = GetLegend()) { + // The legend's background goes on our BlockBorderBackgrounds list because + // it's a block child. + nsDisplayListSet set(aLists, aLists.BlockBorderBackgrounds()); + BuildDisplayListForChild(aBuilder, legend, set); + } + // Put the inner frame's display items on the master list. Note that this + // moves its border/background display items to our BorderBackground() list, + // which isn't really correct, but it's OK because the inner frame is + // anonymous and can't have its own border and background. + contentDisplayItems.MoveTo(aLists); +} + +ImgDrawResult nsFieldSetFrame::PaintBorder(nsDisplayListBuilder* aBuilder, + gfxContext& aRenderingContext, + nsPoint aPt, + const nsRect& aDirtyRect) { + // If the border is smaller than the legend, move the border down + // to be centered on the legend. We call VisualBorderRectRelativeToSelf() to + // compute the border positioning. + // FIXME: This means border-radius clamping is incorrect; we should + // override nsIFrame::GetBorderRadii. + nsRect rect = VisualBorderRectRelativeToSelf() + aPt; + nsPresContext* presContext = PresContext(); + + const auto skipSides = GetSkipSides(); + PaintBorderFlags borderFlags = aBuilder->ShouldSyncDecodeImages() + ? PaintBorderFlags::SyncDecodeImages + : PaintBorderFlags(); + + ImgDrawResult result = ImgDrawResult::SUCCESS; + + nsCSSRendering::PaintBoxShadowInner(presContext, aRenderingContext, this, + rect); + + if (nsIFrame* legend = GetLegend()) { + // We want to avoid drawing our border under the legend, so clip out the + // legend while drawing our border. We don't want to use mLegendRect here, + // because we do want to draw our border under the legend's inline-start and + // -end margins. And we use GetNormalRect(), not GetRect(), because we do + // not want relative positioning applied to the legend to change how our + // border looks. + nsRect legendRect = legend->GetNormalRect() + aPt; + + // Make sure we clip all of the border in case the legend is smaller. + nscoord borderTopWidth = GetUsedBorder().top; + if (legendRect.height < borderTopWidth) { + legendRect.height = borderTopWidth; + legendRect.y = aPt.y; + } + + DrawTarget* drawTarget = aRenderingContext.GetDrawTarget(); + // We set up a clip path which has our rect clockwise and the legend rect + // counterclockwise, with FILL_WINDING as the fill rule. That will allow us + // to paint within our rect but outside the legend rect. For "our rect" we + // use our ink overflow rect (relative to ourselves, so it's not affected + // by transforms), because we can have borders sticking outside our border + // box (e.g. due to border-image-outset). + RefPtr<PathBuilder> pathBuilder = + drawTarget->CreatePathBuilder(FillRule::FILL_WINDING); + int32_t appUnitsPerDevPixel = presContext->AppUnitsPerDevPixel(); + AppendRectToPath(pathBuilder, + NSRectToSnappedRect(InkOverflowRectRelativeToSelf() + aPt, + appUnitsPerDevPixel, *drawTarget), + true); + AppendRectToPath( + pathBuilder, + NSRectToSnappedRect(legendRect, appUnitsPerDevPixel, *drawTarget), + false); + RefPtr<Path> clipPath = pathBuilder->Finish(); + + aRenderingContext.Save(); + aRenderingContext.Clip(clipPath); + result &= nsCSSRendering::PaintBorder(presContext, aRenderingContext, this, + aDirtyRect, rect, mComputedStyle, + borderFlags, skipSides); + aRenderingContext.Restore(); + } else { + result &= nsCSSRendering::PaintBorder( + presContext, aRenderingContext, this, aDirtyRect, + nsRect(aPt, mRect.Size()), mComputedStyle, borderFlags, skipSides); + } + + return result; +} + +nscoord nsFieldSetFrame::GetIntrinsicISize(gfxContext* aRenderingContext, + IntrinsicISizeType aType) { + // Both inner and legend are children, and if the fieldset is + // size-contained they should not contribute to the intrinsic size. + if (Maybe<nscoord> containISize = ContainIntrinsicISize()) { + return *containISize; + } + + nscoord legendWidth = 0; + if (nsIFrame* legend = GetLegend()) { + legendWidth = + nsLayoutUtils::IntrinsicForContainer(aRenderingContext, legend, aType); + } + + nscoord contentWidth = 0; + if (nsIFrame* inner = GetInner()) { + // Ignore padding on the inner, since the padding will be applied to the + // outer instead, and the padding computed for the inner is wrong + // for percentage padding. + contentWidth = nsLayoutUtils::IntrinsicForContainer( + aRenderingContext, inner, aType, nsLayoutUtils::IGNORE_PADDING); + } + + return std::max(legendWidth, contentWidth); +} + +nscoord nsFieldSetFrame::GetMinISize(gfxContext* aRenderingContext) { + nscoord result = 0; + DISPLAY_MIN_INLINE_SIZE(this, result); + + result = GetIntrinsicISize(aRenderingContext, IntrinsicISizeType::MinISize); + return result; +} + +nscoord nsFieldSetFrame::GetPrefISize(gfxContext* aRenderingContext) { + nscoord result = 0; + DISPLAY_PREF_INLINE_SIZE(this, result); + + result = GetIntrinsicISize(aRenderingContext, IntrinsicISizeType::PrefISize); + return result; +} + +/* virtual */ +void nsFieldSetFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + using LegendAlignValue = mozilla::dom::HTMLLegendElement::LegendAlignValue; + + MarkInReflow(); + DO_GLOBAL_REFLOW_COUNT("nsFieldSetFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + NS_WARNING_ASSERTION(aReflowInput.ComputedISize() != NS_UNCONSTRAINEDSIZE, + "Should have a precomputed inline-size!"); + + OverflowAreas ocBounds; + nsReflowStatus ocStatus; + auto* prevInFlow = static_cast<nsFieldSetFrame*>(GetPrevInFlow()); + if (prevInFlow) { + ReflowOverflowContainerChildren(aPresContext, aReflowInput, ocBounds, + ReflowChildFlags::Default, ocStatus); + + AutoFrameListPtr prevOverflowFrames(PresContext(), + prevInFlow->StealOverflowFrames()); + if (prevOverflowFrames) { + nsContainerFrame::ReparentFrameViewList(*prevOverflowFrames, prevInFlow, + this); + mFrames.InsertFrames(this, nullptr, std::move(*prevOverflowFrames)); + } + } + + bool reflowInner; + bool reflowLegend; + nsIFrame* legend = GetLegend(); + nsContainerFrame* inner = GetInner(); + if (!legend || !inner) { + if (DrainSelfOverflowList()) { + legend = GetLegend(); + inner = GetInner(); + } + } + if (aReflowInput.ShouldReflowAllKids() || GetNextInFlow() || + aReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE) { + reflowInner = inner != nullptr; + reflowLegend = legend != nullptr; + } else { + reflowInner = inner && inner->IsSubtreeDirty(); + reflowLegend = legend && legend->IsSubtreeDirty(); + } + + // @note |this| frame applies borders but not any padding. Our anonymous + // inner frame applies the padding (but not borders). + const auto wm = GetWritingMode(); + auto skipSides = PreReflowBlockLevelLogicalSkipSides(); + LogicalMargin border = + aReflowInput.ComputedLogicalBorder(wm).ApplySkipSides(skipSides); + LogicalSize availSize(wm, aReflowInput.ComputedSize().ISize(wm), + aReflowInput.AvailableBSize()); + + // Figure out how big the legend is if there is one. + LogicalMargin legendMargin(wm); + Maybe<ReflowInput> legendReflowInput; + if (legend) { + const auto legendWM = legend->GetWritingMode(); + LogicalSize legendAvailSize = availSize.ConvertTo(legendWM, wm); + ComputeSizeFlags sizeFlags; + if (legend->StylePosition()->ISize(wm).IsAuto()) { + sizeFlags = ComputeSizeFlag::ShrinkWrap; + } + ReflowInput::InitFlags initFlags; // intentionally empty + StyleSizeOverrides sizeOverrides; // intentionally empty + legendReflowInput.emplace(aPresContext, aReflowInput, legend, + legendAvailSize, Nothing(), initFlags, + sizeOverrides, sizeFlags); + } + const bool avoidBreakInside = ShouldAvoidBreakInside(aReflowInput); + if (reflowLegend) { + ReflowOutput legendDesiredSize(aReflowInput); + + // We'll move the legend to its proper place later, so the position + // and containerSize passed here are unimportant. + const nsSize dummyContainerSize; + ReflowChild(legend, aPresContext, legendDesiredSize, *legendReflowInput, wm, + LogicalPoint(wm), dummyContainerSize, + ReflowChildFlags::NoMoveFrame, aStatus); + + if (aReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE && + !(HasAnyStateBits(NS_FRAME_OUT_OF_FLOW) && + aReflowInput.mStyleDisplay->IsAbsolutelyPositionedStyle()) && + !prevInFlow && !aReflowInput.mFlags.mIsTopOfPage) { + // Propagate break-before from the legend to the fieldset. + if (legend->StyleDisplay()->BreakBefore() || + aStatus.IsInlineBreakBefore()) { + aStatus.SetInlineLineBreakBeforeAndReset(); + return; + } + // Honor break-inside:avoid by breaking before instead. + if (MOZ_UNLIKELY(avoidBreakInside) && !aStatus.IsFullyComplete()) { + aStatus.SetInlineLineBreakBeforeAndReset(); + return; + } + } + + // Calculate the legend's margin-box rectangle. + legendMargin = legend->GetLogicalUsedMargin(wm); + mLegendRect = LogicalRect( + wm, 0, 0, legendDesiredSize.ISize(wm) + legendMargin.IStartEnd(wm), + legendDesiredSize.BSize(wm) + legendMargin.BStartEnd(wm)); + // We subtract mLegendSpace from inner's content-box block-size below. + nscoord oldSpace = mLegendSpace; + mLegendSpace = 0; + nscoord borderBStart = border.BStart(wm); + if (!prevInFlow) { + if (mLegendRect.BSize(wm) > borderBStart) { + mLegendSpace = mLegendRect.BSize(wm) - borderBStart; + } else { + // Calculate the border-box position that would center the legend's + // border-box within the fieldset border: + nscoord off = (borderBStart - legendDesiredSize.BSize(wm)) / 2; + off -= legendMargin.BStart(wm); // convert to a margin-box position + if (off > nscoord(0)) { + // Align the legend to the end if center-aligning it would overflow. + nscoord overflow = off + mLegendRect.BSize(wm) - borderBStart; + if (overflow > nscoord(0)) { + off -= overflow; + } + mLegendRect.BStart(wm) += off; + } + } + } else { + mLegendSpace = mLegendRect.BSize(wm); + } + + // If mLegendSpace changes then we need to reflow |inner| as well. + if (mLegendSpace != oldSpace && inner) { + reflowInner = true; + } + + FinishReflowChild(legend, aPresContext, legendDesiredSize, + legendReflowInput.ptr(), wm, LogicalPoint(wm), + dummyContainerSize, ReflowChildFlags::NoMoveFrame); + EnsureChildContinuation(legend, aStatus); + if (aReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE && + !legend->GetWritingMode().IsOrthogonalTo(wm) && + legend->StyleDisplay()->BreakAfter() && + (!legendReflowInput->mFlags.mIsTopOfPage || + mLegendRect.BSize(wm) > 0) && + aStatus.IsComplete()) { + // Pretend that we ran out of space to push children of |inner|. + // XXX(mats) perhaps pushing the inner frame would be more correct, + // but we don't support that yet. + availSize.BSize(wm) = nscoord(0); + aStatus.Reset(); + aStatus.SetIncomplete(); + } + } else if (!legend) { + mLegendRect.SetEmpty(); + mLegendSpace = 0; + } else { + // mLegendSpace and mLegendRect haven't changed, but we need + // the used margin when placing the legend. + legendMargin = legend->GetLogicalUsedMargin(wm); + } + + // This containerSize is incomplete as yet: it does not include the size + // of the |inner| frame itself. + nsSize containerSize = + (LogicalSize(wm, 0, mLegendSpace) + border.Size(wm)).GetPhysicalSize(wm); + if (reflowInner) { + LogicalSize innerAvailSize = availSize; + innerAvailSize.ISize(wm) = + aReflowInput.ComputedSizeWithPadding(wm).ISize(wm); + nscoord remainingComputedBSize = aReflowInput.ComputedBSize(); + if (prevInFlow && remainingComputedBSize != NS_UNCONSTRAINEDSIZE) { + // Subtract the consumed BSize associated with the legend. + for (nsIFrame* prev = prevInFlow; prev; prev = prev->GetPrevInFlow()) { + auto* prevFieldSet = static_cast<nsFieldSetFrame*>(prev); + remainingComputedBSize -= prevFieldSet->mLegendSpace; + } + remainingComputedBSize = std::max(0, remainingComputedBSize); + } + if (innerAvailSize.BSize(wm) != NS_UNCONSTRAINEDSIZE) { + innerAvailSize.BSize(wm) -= + std::max(mLegendRect.BSize(wm), border.BStart(wm)); + if (StyleBorder()->mBoxDecorationBreak == + StyleBoxDecorationBreak::Clone && + (aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE || + remainingComputedBSize + + aReflowInput.ComputedLogicalBorderPadding(wm).BStartEnd( + wm) >= + availSize.BSize(wm))) { + innerAvailSize.BSize(wm) -= border.BEnd(wm); + } + innerAvailSize.BSize(wm) = std::max(0, innerAvailSize.BSize(wm)); + } + ReflowInput kidReflowInput(aPresContext, aReflowInput, inner, + innerAvailSize, Nothing(), + ReflowInput::InitFlag::CallerWillInit); + // Override computed padding, in case it's percentage padding + kidReflowInput.Init( + aPresContext, Nothing(), Nothing(), + Some(aReflowInput.ComputedLogicalPadding(inner->GetWritingMode()))); + + // Propagate the aspect-ratio flag to |inner| (i.e. the container frame + // wrapped by nsFieldSetFrame), so we can let |inner|'s reflow code handle + // automatic content-based minimum. + // Note: Init() resets this flag, so we have to copy it again here. + if (aReflowInput.mFlags.mIsBSizeSetByAspectRatio) { + kidReflowInput.mFlags.mIsBSizeSetByAspectRatio = true; + } + + if (kidReflowInput.mFlags.mIsTopOfPage) { + // Prevent break-before from |inner| if we have a legend. + kidReflowInput.mFlags.mIsTopOfPage = !legend; + } + // Our child is "height:100%" but we actually want its height to be reduced + // by the amount of content-height the legend is eating up, unless our + // height is unconstrained (in which case the child's will be too). + if (aReflowInput.ComputedBSize() != NS_UNCONSTRAINEDSIZE) { + kidReflowInput.SetComputedBSize( + std::max(0, remainingComputedBSize - mLegendSpace)); + } + + if (aReflowInput.ComputedMinBSize() > 0) { + kidReflowInput.SetComputedMinBSize( + std::max(0, aReflowInput.ComputedMinBSize() - mLegendSpace)); + } + + if (aReflowInput.ComputedMaxBSize() != NS_UNCONSTRAINEDSIZE) { + kidReflowInput.SetComputedMaxBSize( + std::max(0, aReflowInput.ComputedMaxBSize() - mLegendSpace)); + } + + ReflowOutput kidDesiredSize(kidReflowInput); + NS_ASSERTION( + kidReflowInput.ComputedPhysicalMargin() == nsMargin(0, 0, 0, 0), + "Margins on anonymous fieldset child not supported!"); + LogicalPoint pt(wm, border.IStart(wm), border.BStart(wm) + mLegendSpace); + + // We don't know the correct containerSize until we have reflowed |inner|, + // so we use a dummy value for now; FinishReflowChild will fix the position + // if necessary. + const nsSize dummyContainerSize; + nsReflowStatus status; + // If our legend needs a continuation then *this* frame will have + // a continuation as well so we should keep our inner frame continuations + // too (even if 'inner' ends up being COMPLETE here). This ensures that + // our continuation will have a reasonable inline-size. + ReflowChildFlags flags = aStatus.IsFullyComplete() + ? ReflowChildFlags::Default + : ReflowChildFlags::NoDeleteNextInFlowChild; + ReflowChild(inner, aPresContext, kidDesiredSize, kidReflowInput, wm, pt, + dummyContainerSize, flags, status); + + // Honor break-inside:avoid when possible by returning a BreakBefore status. + if (MOZ_UNLIKELY(avoidBreakInside) && !prevInFlow && + !aReflowInput.mFlags.mIsTopOfPage && + availSize.BSize(wm) != NS_UNCONSTRAINEDSIZE) { + if (status.IsInlineBreakBefore() || !status.IsFullyComplete()) { + aStatus.SetInlineLineBreakBeforeAndReset(); + return; + } + } + + // Update containerSize to account for size of the inner frame, so that + // FinishReflowChild can position it correctly. + containerSize += kidDesiredSize.PhysicalSize(); + FinishReflowChild(inner, aPresContext, kidDesiredSize, &kidReflowInput, wm, + pt, containerSize, ReflowChildFlags::Default); + EnsureChildContinuation(inner, status); + aStatus.MergeCompletionStatusFrom(status); + NS_FRAME_TRACE_REFLOW_OUT("FieldSet::Reflow", aStatus); + } else if (inner) { + // |inner| didn't need to be reflowed but we do need to include its size + // in containerSize. + containerSize += inner->GetSize(); + } else { + // No |inner| means it was already complete in an earlier continuation. + MOZ_ASSERT(prevInFlow, "first-in-flow should always have an inner frame"); + for (nsIFrame* prev = prevInFlow; prev; prev = prev->GetPrevInFlow()) { + auto* prevFieldSet = static_cast<nsFieldSetFrame*>(prev); + if (auto* prevInner = prevFieldSet->GetInner()) { + containerSize += prevInner->GetSize(); + break; + } + } + } + + LogicalRect contentRect(wm); + if (inner) { + // We don't support margins on inner, so our content rect is just the + // inner's border-box. (We don't really care about container size at this + // point, as we'll figure out the actual positioning later.) + contentRect = inner->GetLogicalRect(wm, containerSize); + } else if (prevInFlow) { + auto size = prevInFlow->GetPaddingRectRelativeToSelf().Size(); + contentRect.ISize(wm) = wm.IsVertical() ? size.height : size.width; + } + + if (legend) { + // The legend is positioned inline-wards within the inner's content rect + // (so that padding on the fieldset affects the legend position). + LogicalRect innerContentRect = contentRect; + innerContentRect.Deflate(wm, aReflowInput.ComputedLogicalPadding(wm)); + // If the inner content rect is larger than the legend, we can align the + // legend. + if (innerContentRect.ISize(wm) > mLegendRect.ISize(wm)) { + // NOTE legend @align values are: left/right/center + // GetLogicalAlign converts left/right to start/end for the given WM. + // @see HTMLLegendElement::ParseAttribute/LogicalAlign + auto* legendElement = + dom::HTMLLegendElement::FromNode(legend->GetContent()); + switch (legendElement->LogicalAlign(wm)) { + case LegendAlignValue::InlineEnd: + mLegendRect.IStart(wm) = + innerContentRect.IEnd(wm) - mLegendRect.ISize(wm); + break; + case LegendAlignValue::Center: + // Note: rounding removed; there doesn't seem to be any need + mLegendRect.IStart(wm) = + innerContentRect.IStart(wm) + + (innerContentRect.ISize(wm) - mLegendRect.ISize(wm)) / 2; + break; + case LegendAlignValue::InlineStart: + mLegendRect.IStart(wm) = innerContentRect.IStart(wm); + break; + default: + MOZ_ASSERT_UNREACHABLE("unexpected GetLogicalAlign value"); + } + } else { + // otherwise just start-align it. + mLegendRect.IStart(wm) = innerContentRect.IStart(wm); + } + + // place the legend + LogicalRect actualLegendRect = mLegendRect; + actualLegendRect.Deflate(wm, legendMargin); + LogicalPoint actualLegendPos(actualLegendRect.Origin(wm)); + + // Note that legend's writing mode may be different from the fieldset's, + // so we need to convert offsets before applying them to it (bug 1134534). + LogicalMargin offsets = legendReflowInput->ComputedLogicalOffsets(wm); + ReflowInput::ApplyRelativePositioning(legend, wm, offsets, &actualLegendPos, + containerSize); + + legend->SetPosition(wm, actualLegendPos, containerSize); + nsContainerFrame::PositionFrameView(legend); + nsContainerFrame::PositionChildViews(legend); + } + + // Skip our block-end border if we're INCOMPLETE. + if (!aStatus.IsComplete() && + StyleBorder()->mBoxDecorationBreak != StyleBoxDecorationBreak::Clone) { + border.BEnd(wm) = nscoord(0); + } + + // Return our size and our result. + LogicalSize finalSize( + wm, contentRect.ISize(wm) + border.IStartEnd(wm), + mLegendSpace + border.BStartEnd(wm) + (inner ? inner->BSize(wm) : 0)); + if (Maybe<nscoord> containBSize = + aReflowInput.mFrame->ContainIntrinsicBSize()) { + // If we're size-contained in block axis, then we must set finalSize + // according to contain-intrinsic-block-size, disregarding legend and inner. + // Note: normally the fieldset's own padding (which we still must honor) + // would be accounted for as part of inner's size (see kidReflowInput.Init() + // call above). So: since we're disregarding sizing information from + // 'inner', we need to account for that padding ourselves here. + nscoord contentBoxBSize = + aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE + ? aReflowInput.ApplyMinMaxBSize(*containBSize) + : aReflowInput.ComputedBSize(); + finalSize.BSize(wm) = + contentBoxBSize + + aReflowInput.ComputedLogicalBorderPadding(wm).BStartEnd(wm); + } + + if (aStatus.IsComplete() && + aReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE && + finalSize.BSize(wm) > aReflowInput.AvailableBSize() && + border.BEnd(wm) > 0 && aReflowInput.AvailableBSize() > border.BEnd(wm)) { + // Our end border doesn't fit but it should fit in the next column/page. + if (MOZ_UNLIKELY(avoidBreakInside)) { + aStatus.SetInlineLineBreakBeforeAndReset(); + return; + } else { + if (StyleBorder()->mBoxDecorationBreak == + StyleBoxDecorationBreak::Slice) { + finalSize.BSize(wm) -= border.BEnd(wm); + } + aStatus.SetIncomplete(); + } + } + + if (!aStatus.IsComplete()) { + MOZ_ASSERT(aReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE, + "must be Complete in an unconstrained available block-size"); + // Stretch our BSize to fill the fragmentainer. + finalSize.BSize(wm) = + std::max(finalSize.BSize(wm), aReflowInput.AvailableBSize()); + } + aDesiredSize.SetSize(wm, finalSize); + aDesiredSize.SetOverflowAreasToDesiredBounds(); + + if (legend) { + ConsiderChildOverflow(aDesiredSize.mOverflowAreas, legend); + } + if (inner) { + ConsiderChildOverflow(aDesiredSize.mOverflowAreas, inner); + } + + // Merge overflow container bounds and status. + aDesiredSize.mOverflowAreas.UnionWith(ocBounds); + aStatus.MergeCompletionStatusFrom(ocStatus); + + FinishReflowWithAbsoluteFrames(aPresContext, aDesiredSize, aReflowInput, + aStatus); + InvalidateFrame(); +} + +void nsFieldSetFrame::SetInitialChildList(ChildListID aListID, + nsFrameList&& aChildList) { + nsContainerFrame::SetInitialChildList(aListID, std::move(aChildList)); + if (nsBlockFrame* legend = do_QueryFrame(GetLegend())) { + // A rendered legend always establish a new formatting context. + // https://html.spec.whatwg.org/multipage/rendering.html#rendered-legend + legend->AddStateBits(NS_BLOCK_FORMATTING_CONTEXT_STATE_BITS); + } + MOZ_ASSERT( + aListID != FrameChildListID::Principal || GetInner() || GetLegend(), + "Setting principal child list should populate our inner frame " + "or our rendered legend"); +} + +void nsFieldSetFrame::AppendFrames(ChildListID aListID, + nsFrameList&& aFrameList) { + MOZ_ASSERT(aListID == FrameChildListID::NoReflowPrincipal && + HasAnyStateBits(NS_FRAME_FIRST_REFLOW), + "AppendFrames should only be used from " + "nsCSSFrameConstructor::ConstructFieldSetFrame"); + nsContainerFrame::AppendFrames(aListID, std::move(aFrameList)); + MOZ_ASSERT(GetInner(), "at this point we should have an inner frame"); +} + +void nsFieldSetFrame::InsertFrames(ChildListID aListID, nsIFrame* aPrevFrame, + const nsLineList::iterator* aPrevFrameLine, + nsFrameList&& aFrameList) { + MOZ_ASSERT( + aListID == FrameChildListID::Principal && !aPrevFrame && !GetLegend(), + "InsertFrames should only be used to prepend a rendered legend " + "from nsCSSFrameConstructor::ConstructFramesFromItemList"); + nsContainerFrame::InsertFrames(aListID, aPrevFrame, aPrevFrameLine, + std::move(aFrameList)); + MOZ_ASSERT(GetLegend()); + if (nsBlockFrame* legend = do_QueryFrame(GetLegend())) { + // A rendered legend always establish a new formatting context. + // https://html.spec.whatwg.org/multipage/rendering.html#rendered-legend + legend->AddStateBits(NS_BLOCK_FORMATTING_CONTEXT_STATE_BITS); + } +} + +#ifdef DEBUG +void nsFieldSetFrame::RemoveFrame(DestroyContext&, ChildListID, nsIFrame*) { + MOZ_CRASH("nsFieldSetFrame::RemoveFrame not supported"); +} +#endif + +#ifdef ACCESSIBILITY +a11y::AccType nsFieldSetFrame::AccessibleType() { + return a11y::eHTMLGroupboxType; +} +#endif + +BaselineSharingGroup nsFieldSetFrame::GetDefaultBaselineSharingGroup() const { + switch (StyleDisplay()->DisplayInside()) { + case mozilla::StyleDisplayInside::Grid: + case mozilla::StyleDisplayInside::Flex: + return BaselineSharingGroup::First; + default: + return BaselineSharingGroup::Last; + } +} + +nscoord nsFieldSetFrame::SynthesizeFallbackBaseline( + WritingMode aWM, BaselineSharingGroup aBaselineGroup) const { + return Baseline::SynthesizeBOffsetFromMarginBox(this, aWM, aBaselineGroup); +} + +Maybe<nscoord> nsFieldSetFrame::GetNaturalBaselineBOffset( + WritingMode aWM, BaselineSharingGroup aBaselineGroup, + BaselineExportContext aExportContext) const { + if (StyleDisplay()->IsContainLayout()) { + // If we are layout-contained, our child 'inner' should not + // affect how we calculate our baseline. + return Nothing{}; + } + nsIFrame* inner = GetInner(); + if (MOZ_UNLIKELY(!inner)) { + return Nothing{}; + } + MOZ_ASSERT(!inner->GetWritingMode().IsOrthogonalTo(aWM)); + const auto result = + inner->GetNaturalBaselineBOffset(aWM, aBaselineGroup, aExportContext); + if (!result) { + return Nothing{}; + } + nscoord innerBStart = inner->BStart(aWM, GetSize()); + if (aBaselineGroup == BaselineSharingGroup::First) { + return Some(*result + innerBStart); + } + return Some(*result + BSize(aWM) - (innerBStart + inner->BSize(aWM))); +} + +nsIScrollableFrame* nsFieldSetFrame::GetScrollTargetFrame() const { + return do_QueryFrame(GetInner()); +} + +void nsFieldSetFrame::AppendDirectlyOwnedAnonBoxes( + nsTArray<OwnedAnonBox>& aResult) { + if (nsIFrame* kid = GetInner()) { + aResult.AppendElement(OwnedAnonBox(kid)); + } +} + +void nsFieldSetFrame::EnsureChildContinuation(nsIFrame* aChild, + const nsReflowStatus& aStatus) { + MOZ_ASSERT(aChild == GetLegend() || aChild == GetInner(), + "unexpected child frame"); + nsIFrame* nif = aChild->GetNextInFlow(); + if (aStatus.IsFullyComplete()) { + if (nif) { + // NOTE: we want to avoid our DEBUG version of RemoveFrame above. + DestroyContext context(PresShell()); + nsContainerFrame::RemoveFrame(context, + FrameChildListID::NoReflowPrincipal, nif); + MOZ_ASSERT(!aChild->GetNextInFlow()); + } + } else { + nsFrameList nifs; + if (!nif) { + auto* fc = PresShell()->FrameConstructor(); + nif = fc->CreateContinuingFrame(aChild, this); + if (aStatus.IsOverflowIncomplete()) { + nif->AddStateBits(NS_FRAME_IS_OVERFLOW_CONTAINER); + } + nifs = nsFrameList(nif, nif); + } else { + // Steal all nifs and push them again in case they are currently on + // the wrong list. + for (nsIFrame* n = nif; n; n = n->GetNextInFlow()) { + n->GetParent()->StealFrame(n); + nifs.AppendFrame(this, n); + if (aStatus.IsOverflowIncomplete()) { + n->AddStateBits(NS_FRAME_IS_OVERFLOW_CONTAINER); + } else { + n->RemoveStateBits(NS_FRAME_IS_OVERFLOW_CONTAINER); + } + } + } + if (aStatus.IsOverflowIncomplete()) { + if (nsFrameList* eoc = GetExcessOverflowContainers()) { + eoc->AppendFrames(nullptr, std::move(nifs)); + } else { + SetExcessOverflowContainers(std::move(nifs)); + } + } else { + if (nsFrameList* oc = GetOverflowFrames()) { + oc->AppendFrames(nullptr, std::move(nifs)); + } else { + SetOverflowFrames(std::move(nifs)); + } + } + } +} diff --git a/layout/forms/nsFieldSetFrame.h b/layout/forms/nsFieldSetFrame.h new file mode 100644 index 0000000000..c3b9f67e0a --- /dev/null +++ b/layout/forms/nsFieldSetFrame.h @@ -0,0 +1,118 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsFieldSetFrame_h___ +#define nsFieldSetFrame_h___ + +#include "mozilla/Attributes.h" +#include "ImgDrawResult.h" +#include "nsContainerFrame.h" +#include "nsIScrollableFrame.h" + +class nsFieldSetFrame final : public nsContainerFrame { + typedef mozilla::image::ImgDrawResult ImgDrawResult; + + public: + NS_DECL_FRAMEARENA_HELPERS(nsFieldSetFrame) + NS_DECL_QUERYFRAME + + explicit nsFieldSetFrame(ComputedStyle* aStyle, nsPresContext* aPresContext); + + nscoord GetIntrinsicISize(gfxContext* aRenderingContext, + mozilla::IntrinsicISizeType); + nscoord GetMinISize(gfxContext* aRenderingContext) override; + nscoord GetPrefISize(gfxContext* aRenderingContext) override; + + /** + * The area to paint box-shadows around. It's the border rect except + * when there's a <legend> we offset the y-position to the center of it. + */ + nsRect VisualBorderRectRelativeToSelf() const override; + + void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + nscoord SynthesizeFallbackBaseline( + mozilla::WritingMode aWM, + BaselineSharingGroup aBaselineGroup) const override; + BaselineSharingGroup GetDefaultBaselineSharingGroup() const override; + Maybe<nscoord> GetNaturalBaselineBOffset( + mozilla::WritingMode aWM, BaselineSharingGroup aBaselineGroup, + BaselineExportContext aExportContext) const override; + + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + ImgDrawResult PaintBorder(nsDisplayListBuilder* aBuilder, + gfxContext& aRenderingContext, nsPoint aPt, + const nsRect& aDirtyRect); + + void SetInitialChildList(ChildListID aListID, + nsFrameList&& aChildList) override; + void AppendFrames(ChildListID aListID, nsFrameList&& aFrameList) override; + void InsertFrames(ChildListID aListID, nsIFrame* aPrevFrame, + const nsLineList::iterator* aPrevFrameLine, + nsFrameList&& aFrameList) override; +#ifdef DEBUG + void RemoveFrame(DestroyContext&, ChildListID aListID, + nsIFrame* aOldFrame) override; +#endif + + bool IsFrameOfType(uint32_t aFlags) const override { + return nsContainerFrame::IsFrameOfType( + aFlags & ~nsIFrame::eCanContainOverflowContainers); + } + nsIScrollableFrame* GetScrollTargetFrame() const override; + + // Return the block wrapper around our kids. + void AppendDirectlyOwnedAnonBoxes(nsTArray<OwnedAnonBox>& aResult) override; + +#ifdef ACCESSIBILITY + virtual mozilla::a11y::AccType AccessibleType() override; +#endif + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"FieldSet"_ns, aResult); + } +#endif + + /** + * Return the anonymous frame that contains all descendants except the legend + * frame. This can be a block/grid/flex/scroll frame. It always has + * the pseudo type nsCSSAnonBoxes::fieldsetContent. If it's a scroll frame, + * the scrolled frame can be a block/grid/flex frame. + * This may return null, for example for a fieldset that is a true overflow + * container. + */ + nsContainerFrame* GetInner() const; + + /** + * Return the frame that represents the rendered legend if any. + * https://html.spec.whatwg.org/multipage/rendering.html#rendered-legend + */ + nsIFrame* GetLegend() const; + + /** @see mLegendSpace below */ + nscoord LegendSpace() const { return mLegendSpace; } + + protected: + /** + * Convenience method to create a continuation for aChild after we've + * reflowed it and got the reflow status aStatus. + */ + void EnsureChildContinuation(nsIFrame* aChild, const nsReflowStatus& aStatus); + + mozilla::LogicalRect mLegendRect; + + // This is how much to subtract from our inner frame's content-box block-size + // to account for a protruding legend. It's zero if there's no legend or + // the legend fits entirely inside our start border. + nscoord mLegendSpace; +}; + +#endif // nsFieldSetFrame_h___ diff --git a/layout/forms/nsFileControlFrame.cpp b/layout/forms/nsFileControlFrame.cpp new file mode 100644 index 0000000000..e700066a65 --- /dev/null +++ b/layout/forms/nsFileControlFrame.cpp @@ -0,0 +1,426 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsFileControlFrame.h" + +#include "nsGkAtoms.h" +#include "nsCOMPtr.h" +#include "mozilla/dom/BlobImpl.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/NodeInfo.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/DOMStringList.h" +#include "mozilla/dom/DataTransfer.h" +#include "mozilla/dom/Directory.h" +#include "mozilla/dom/DragEvent.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/FileList.h" +#include "mozilla/dom/HTMLButtonElement.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/dom/MutationEventBinding.h" +#include "mozilla/Preferences.h" +#include "mozilla/PresShell.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/TextEditor.h" +#include "MiddleCroppingBlockFrame.h" +#include "nsIFrame.h" +#include "nsNodeInfoManager.h" +#include "nsContentCreatorFunctions.h" +#include "nsContentUtils.h" +#include "nsIFile.h" +#include "nsLayoutUtils.h" +#include "nsTextNode.h" +#include "nsTextFrame.h" +#include "gfxContext.h" + +using namespace mozilla; +using namespace mozilla::dom; + +nsIFrame* NS_NewFileControlFrame(PresShell* aPresShell, ComputedStyle* aStyle) { + return new (aPresShell) + nsFileControlFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsFileControlFrame) + +nsFileControlFrame::nsFileControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsBlockFrame(aStyle, aPresContext, kClassID) { + AddStateBits(NS_BLOCK_FLOAT_MGR); +} + +void nsFileControlFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + nsBlockFrame::Init(aContent, aParent, aPrevInFlow); + + mMouseListener = new DnDListener(this); +} + +void nsFileControlFrame::Destroy(DestroyContext& aContext) { + NS_ENSURE_TRUE_VOID(mContent); + + // Remove the events. + if (mContent) { + mContent->RemoveSystemEventListener(u"drop"_ns, mMouseListener, false); + mContent->RemoveSystemEventListener(u"dragover"_ns, mMouseListener, false); + } + + aContext.AddAnonymousContent(mTextContent.forget()); + aContext.AddAnonymousContent(mBrowseFilesOrDirs.forget()); + + mMouseListener->ForgetFrame(); + nsBlockFrame::Destroy(aContext); +} + +static already_AddRefed<Element> MakeAnonButton( + Document* aDoc, const char* labelKey, HTMLInputElement* aInputElement) { + RefPtr<Element> button = aDoc->CreateHTMLElement(nsGkAtoms::button); + // NOTE: SetIsNativeAnonymousRoot() has to be called before setting any + // attribute. + button->SetIsNativeAnonymousRoot(); + button->SetPseudoElementType(PseudoStyleType::fileSelectorButton); + + // Set the file picking button text depending on the current locale. + nsAutoString buttonTxt; + nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES, + labelKey, aDoc, buttonTxt); + + auto* nim = aDoc->NodeInfoManager(); + // Set the browse button text. It's a bit of a pain to do because we want to + // make sure we are not notifying. + RefPtr textContent = new (nim) nsTextNode(nim); + textContent->SetText(buttonTxt, false); + + IgnoredErrorResult error; + button->AppendChildTo(textContent, false, error); + if (error.Failed()) { + return nullptr; + } + + auto* buttonElement = HTMLButtonElement::FromNode(button); + // We allow tabbing over the input itself, not the button. + buttonElement->SetTabIndex(-1, IgnoreErrors()); + return button.forget(); +} + +nsresult nsFileControlFrame::CreateAnonymousContent( + nsTArray<ContentInfo>& aElements) { + nsCOMPtr<Document> doc = mContent->GetComposedDoc(); + RefPtr fileContent = HTMLInputElement::FromNode(mContent); + + mBrowseFilesOrDirs = MakeAnonButton(doc, "Browse", fileContent); + if (!mBrowseFilesOrDirs) { + return NS_ERROR_OUT_OF_MEMORY; + } + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier, or change the return type to void. + aElements.AppendElement(mBrowseFilesOrDirs); + + // Create and setup the text showing the selected files. + mTextContent = doc->CreateHTMLElement(nsGkAtoms::label); + // NOTE: SetIsNativeAnonymousRoot() has to be called before setting any + // attribute. + mTextContent->SetIsNativeAnonymousRoot(); + RefPtr<nsTextNode> text = + new (doc->NodeInfoManager()) nsTextNode(doc->NodeInfoManager()); + mTextContent->AppendChildTo(text, false, IgnoreErrors()); + + aElements.AppendElement(mTextContent); + + // We should be able to interact with the element by doing drag and drop. + mContent->AddSystemEventListener(u"drop"_ns, mMouseListener, false); + mContent->AddSystemEventListener(u"dragover"_ns, mMouseListener, false); + + SyncDisabledState(); + + return NS_OK; +} + +void nsFileControlFrame::AppendAnonymousContentTo( + nsTArray<nsIContent*>& aElements, uint32_t aFilter) { + if (mBrowseFilesOrDirs) { + aElements.AppendElement(mBrowseFilesOrDirs); + } + + if (mTextContent) { + aElements.AppendElement(mTextContent); + } +} + +NS_QUERYFRAME_HEAD(nsFileControlFrame) + NS_QUERYFRAME_ENTRY(nsFileControlFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) + NS_QUERYFRAME_ENTRY(nsIFormControlFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsBlockFrame) + +void nsFileControlFrame::SetFocus(bool aOn, bool aRepaint) {} + +static void AppendBlobImplAsDirectory(nsTArray<OwningFileOrDirectory>& aArray, + BlobImpl* aBlobImpl, + nsIContent* aContent) { + MOZ_ASSERT(aBlobImpl); + MOZ_ASSERT(aBlobImpl->IsDirectory()); + + nsAutoString fullpath; + ErrorResult err; + aBlobImpl->GetMozFullPath(fullpath, SystemCallerGuarantee(), err); + if (err.Failed()) { + err.SuppressException(); + return; + } + + nsCOMPtr<nsIFile> file; + nsresult rv = NS_NewLocalFile(fullpath, true, getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + nsPIDOMWindowInner* inner = aContent->OwnerDoc()->GetInnerWindow(); + if (!inner || !inner->IsCurrentInnerWindow()) { + return; + } + + RefPtr<Directory> directory = Directory::Create(inner->AsGlobal(), file); + MOZ_ASSERT(directory); + + OwningFileOrDirectory* element = aArray.AppendElement(); + element->SetAsDirectory() = directory; +} + +/** + * This is called when we receive a drop or a dragover. + */ +NS_IMETHODIMP +nsFileControlFrame::DnDListener::HandleEvent(Event* aEvent) { + NS_ASSERTION(mFrame, "We should have been unregistered"); + + if (aEvent->DefaultPrevented()) { + return NS_OK; + } + + DragEvent* dragEvent = aEvent->AsDragEvent(); + if (!dragEvent) { + return NS_OK; + } + + RefPtr<DataTransfer> dataTransfer = dragEvent->GetDataTransfer(); + if (!IsValidDropData(dataTransfer)) { + return NS_OK; + } + + RefPtr<HTMLInputElement> inputElement = + HTMLInputElement::FromNode(mFrame->GetContent()); + bool supportsMultiple = + inputElement->HasAttr(kNameSpaceID_None, nsGkAtoms::multiple); + if (!CanDropTheseFiles(dataTransfer, supportsMultiple)) { + dataTransfer->SetDropEffect(u"none"_ns); + aEvent->StopPropagation(); + return NS_OK; + } + + nsAutoString eventType; + aEvent->GetType(eventType); + if (eventType.EqualsLiteral("dragover")) { + // Prevent default if we can accept this drag data + aEvent->PreventDefault(); + return NS_OK; + } + + if (eventType.EqualsLiteral("drop")) { + aEvent->StopPropagation(); + aEvent->PreventDefault(); + + RefPtr<FileList> fileList = + dataTransfer->GetFiles(*nsContentUtils::GetSystemPrincipal()); + + RefPtr<BlobImpl> webkitDir; + nsresult rv = + GetBlobImplForWebkitDirectory(fileList, getter_AddRefs(webkitDir)); + NS_ENSURE_SUCCESS(rv, NS_OK); + + nsTArray<OwningFileOrDirectory> array; + if (webkitDir) { + AppendBlobImplAsDirectory(array, webkitDir, inputElement); + inputElement->MozSetDndFilesAndDirectories(array); + } else { + bool blinkFileSystemEnabled = + StaticPrefs::dom_webkitBlink_filesystem_enabled(); + if (blinkFileSystemEnabled) { + FileList* files = static_cast<FileList*>(fileList.get()); + if (files) { + for (uint32_t i = 0; i < files->Length(); ++i) { + File* file = files->Item(i); + if (file) { + if (file->Impl() && file->Impl()->IsDirectory()) { + AppendBlobImplAsDirectory(array, file->Impl(), inputElement); + } else { + OwningFileOrDirectory* element = array.AppendElement(); + element->SetAsFile() = file; + } + } + } + } + } + + // Entries API. + if (blinkFileSystemEnabled) { + // This is rather ugly. Pass the directories as Files using SetFiles, + // but then if blink filesystem API is enabled, it wants + // FileOrDirectory array. + inputElement->SetFiles(fileList, true); + inputElement->UpdateEntries(array); + } + // Normal DnD + else { + inputElement->SetFiles(fileList, true); + } + + RefPtr<TextEditor> textEditor; + DebugOnly<nsresult> rvIgnored = + nsContentUtils::DispatchInputEvent(inputElement); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "Failed to dispatch input event"); + nsContentUtils::DispatchTrustedEvent( + inputElement->OwnerDoc(), static_cast<nsINode*>(inputElement), + u"change"_ns, CanBubble::eYes, Cancelable::eNo); + } + } + + return NS_OK; +} + +nsresult nsFileControlFrame::DnDListener::GetBlobImplForWebkitDirectory( + FileList* aFileList, BlobImpl** aBlobImpl) { + *aBlobImpl = nullptr; + + HTMLInputElement* inputElement = + HTMLInputElement::FromNode(mFrame->GetContent()); + bool webkitDirPicker = + StaticPrefs::dom_webkitBlink_dirPicker_enabled() && + inputElement->HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory); + if (!webkitDirPicker) { + return NS_OK; + } + + if (!aFileList) { + return NS_ERROR_FAILURE; + } + + // webkitdirectory doesn't care about the length of the file list but + // only about the first item on it. + uint32_t len = aFileList->Length(); + if (len) { + File* file = aFileList->Item(0); + if (file) { + BlobImpl* impl = file->Impl(); + if (impl && impl->IsDirectory()) { + RefPtr<BlobImpl> retVal = impl; + retVal.swap(*aBlobImpl); + return NS_OK; + } + } + } + + return NS_ERROR_FAILURE; +} + +bool nsFileControlFrame::DnDListener::IsValidDropData( + DataTransfer* aDataTransfer) { + if (!aDataTransfer) { + return false; + } + + // We only support dropping files onto a file upload control + return aDataTransfer->HasFile(); +} + +bool nsFileControlFrame::DnDListener::CanDropTheseFiles( + DataTransfer* aDataTransfer, bool aSupportsMultiple) { + RefPtr<FileList> fileList = + aDataTransfer->GetFiles(*nsContentUtils::GetSystemPrincipal()); + + RefPtr<BlobImpl> webkitDir; + nsresult rv = + GetBlobImplForWebkitDirectory(fileList, getter_AddRefs(webkitDir)); + // Just check if either there isn't webkitdirectory attribute, or + // fileList has a directory which can be dropped to the element. + // No need to use webkitDir for anything here. + NS_ENSURE_SUCCESS(rv, false); + + uint32_t listLength = 0; + if (fileList) { + listLength = fileList->Length(); + } + return listLength <= 1 || aSupportsMultiple; +} + +void nsFileControlFrame::SyncDisabledState() { + if (mContent->AsElement()->State().HasState(ElementState::DISABLED)) { + mBrowseFilesOrDirs->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled, u""_ns, + true); + } else { + mBrowseFilesOrDirs->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, true); + } +} + +void nsFileControlFrame::ElementStateChanged(ElementState aStates) { + if (aStates.HasState(ElementState::DISABLED)) { + nsContentUtils::AddScriptRunner(new SyncDisabledStateEvent(this)); + } +} + +#ifdef DEBUG_FRAME_DUMP +nsresult nsFileControlFrame::GetFrameName(nsAString& aResult) const { + return MakeFrameName(u"FileControl"_ns, aResult); +} +#endif + +nsresult nsFileControlFrame::SetFormProperty(nsAtom* aName, + const nsAString& aValue) { + if (nsGkAtoms::value == aName) { + if (MiddleCroppingBlockFrame* f = + do_QueryFrame(mTextContent->GetPrimaryFrame())) { + f->UpdateDisplayedValueToUncroppedValue(true); + } + } + return NS_OK; +} + +#ifdef ACCESSIBILITY +a11y::AccType nsFileControlFrame::AccessibleType() { + return a11y::eHTMLFileInputType; +} +#endif + +NS_IMPL_ISUPPORTS(nsFileControlFrame::MouseListener, nsIDOMEventListener) + +class FileControlLabelFrame final : public MiddleCroppingBlockFrame { + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(FileControlLabelFrame) + + FileControlLabelFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) + : MiddleCroppingBlockFrame(aStyle, aPresContext, kClassID) {} + + HTMLInputElement& FileInput() const { + return *HTMLInputElement::FromNode(mContent->GetParent()); + } + + void GetUncroppedValue(nsAString& aValue) override { + return FileInput().GetDisplayFileName(aValue); + } +}; + +NS_QUERYFRAME_HEAD(FileControlLabelFrame) + NS_QUERYFRAME_ENTRY(FileControlLabelFrame) +NS_QUERYFRAME_TAIL_INHERITING(MiddleCroppingBlockFrame) +NS_IMPL_FRAMEARENA_HELPERS(FileControlLabelFrame) + +nsIFrame* NS_NewFileControlLabelFrame(PresShell* aPresShell, + ComputedStyle* aStyle) { + return new (aPresShell) + FileControlLabelFrame(aStyle, aPresShell->GetPresContext()); +} diff --git a/layout/forms/nsFileControlFrame.h b/layout/forms/nsFileControlFrame.h new file mode 100644 index 0000000000..085cab98a3 --- /dev/null +++ b/layout/forms/nsFileControlFrame.h @@ -0,0 +1,142 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsFileControlFrame_h___ +#define nsFileControlFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsBlockFrame.h" +#include "nsIFormControlFrame.h" +#include "nsIDOMEventListener.h" +#include "nsIAnonymousContentCreator.h" +#include "nsCOMPtr.h" + +namespace mozilla::dom { +class FileList; +class BlobImpl; +class DataTransfer; +} // namespace mozilla::dom + +class nsFileControlFrame final : public nsBlockFrame, + public nsIFormControlFrame, + public nsIAnonymousContentCreator { + using Element = mozilla::dom::Element; + + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsFileControlFrame) + + explicit nsFileControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext); + + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + + // nsIFormControlFrame + nsresult SetFormProperty(nsAtom* aName, const nsAString& aValue) override; + void SetFocus(bool aOn, bool aRepaint) override; + + void Destroy(DestroyContext&) override; + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override; +#endif + + void ElementStateChanged(mozilla::dom::ElementState aStates) override; + + // nsIAnonymousContentCreator + nsresult CreateAnonymousContent(nsTArray<ContentInfo>&) override; + void AppendAnonymousContentTo(nsTArray<nsIContent*>&, + uint32_t aFilter) override; + +#ifdef ACCESSIBILITY + mozilla::a11y::AccType AccessibleType() override; +#endif + + protected: + class MouseListener; + friend class MouseListener; + class MouseListener : public nsIDOMEventListener { + public: + NS_DECL_ISUPPORTS + + explicit MouseListener(nsFileControlFrame* aFrame) : mFrame(aFrame) {} + + void ForgetFrame() { mFrame = nullptr; } + + protected: + virtual ~MouseListener() = default; + + nsFileControlFrame* mFrame; + }; + + class SyncDisabledStateEvent; + friend class SyncDisabledStateEvent; + class SyncDisabledStateEvent : public mozilla::Runnable { + public: + explicit SyncDisabledStateEvent(nsFileControlFrame* aFrame) + : mozilla::Runnable("nsFileControlFrame::SyncDisabledStateEvent"), + mFrame(aFrame) {} + + NS_IMETHOD Run() override { + nsFileControlFrame* frame = + static_cast<nsFileControlFrame*>(mFrame.GetFrame()); + NS_ENSURE_STATE(frame); + + frame->SyncDisabledState(); + return NS_OK; + } + + private: + WeakFrame mFrame; + }; + + class DnDListener : public MouseListener { + public: + explicit DnDListener(nsFileControlFrame* aFrame) : MouseListener(aFrame) {} + + // nsIDOMEventListener + MOZ_CAN_RUN_SCRIPT_BOUNDARY + NS_IMETHOD HandleEvent(mozilla::dom::Event* aEvent) override; + + nsresult GetBlobImplForWebkitDirectory(mozilla::dom::FileList* aFileList, + mozilla::dom::BlobImpl** aBlobImpl); + + bool IsValidDropData(mozilla::dom::DataTransfer* aDataTransfer); + bool CanDropTheseFiles(mozilla::dom::DataTransfer* aDataTransfer, + bool aSupportsMultiple); + }; + + bool IsFrameOfType(uint32_t aFlags) const override { + return nsBlockFrame::IsFrameOfType( + aFlags & ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock)); + } + + /** + * The text box input. + * @see nsFileControlFrame::CreateAnonymousContent + */ + RefPtr<Element> mTextContent; + /** + * The button to open a file or directory picker. + * @see nsFileControlFrame::CreateAnonymousContent + */ + RefPtr<Element> mBrowseFilesOrDirs; + + /** + * Drag and drop mouse listener. + * This makes sure we don't get used after destruction. + */ + RefPtr<DnDListener> mMouseListener; + + protected: + /** + * Sync the disabled state of the content with anonymous children. + */ + void SyncDisabledState(); +}; + +#endif // nsFileControlFrame_h___ diff --git a/layout/forms/nsGfxButtonControlFrame.cpp b/layout/forms/nsGfxButtonControlFrame.cpp new file mode 100644 index 0000000000..37aa996c27 --- /dev/null +++ b/layout/forms/nsGfxButtonControlFrame.cpp @@ -0,0 +1,178 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsGfxButtonControlFrame.h" +#include "nsIFormControl.h" +#include "nsGkAtoms.h" +#include "mozilla/PresShell.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "nsContentUtils.h" +#include "nsTextNode.h" + +using namespace mozilla; + +nsGfxButtonControlFrame::nsGfxButtonControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsHTMLButtonControlFrame(aStyle, aPresContext, kClassID) {} + +nsContainerFrame* NS_NewGfxButtonControlFrame(PresShell* aPresShell, + ComputedStyle* aStyle) { + return new (aPresShell) + nsGfxButtonControlFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsGfxButtonControlFrame) + +void nsGfxButtonControlFrame::Destroy(DestroyContext& aContext) { + aContext.AddAnonymousContent(mTextContent.forget()); + nsHTMLButtonControlFrame::Destroy(aContext); +} + +#ifdef DEBUG_FRAME_DUMP +nsresult nsGfxButtonControlFrame::GetFrameName(nsAString& aResult) const { + return MakeFrameName(u"ButtonControl"_ns, aResult); +} +#endif + +// Create the text content used as label for the button. +// The frame will be generated by the frame constructor. +nsresult nsGfxButtonControlFrame::CreateAnonymousContent( + nsTArray<ContentInfo>& aElements) { + nsAutoString label; + nsresult rv = GetLabel(label); + NS_ENSURE_SUCCESS(rv, rv); + + // Add a child text content node for the label + mTextContent = new (mContent->NodeInfo()->NodeInfoManager()) + nsTextNode(mContent->NodeInfo()->NodeInfoManager()); + + // set the value of the text node and add it to the child list + mTextContent->SetText(label, false); + aElements.AppendElement(mTextContent); + + return NS_OK; +} + +void nsGfxButtonControlFrame::AppendAnonymousContentTo( + nsTArray<nsIContent*>& aElements, uint32_t aFilter) { + if (mTextContent) { + aElements.AppendElement(mTextContent); + } +} + +NS_QUERYFRAME_HEAD(nsGfxButtonControlFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) +NS_QUERYFRAME_TAIL_INHERITING(nsHTMLButtonControlFrame) + +// Initially we hardcoded the default strings here. +// Next, we used html.css to store the default label for various types +// of buttons. (nsGfxButtonControlFrame::DoNavQuirksReflow rev 1.20) +// However, since html.css is not internationalized, we now grab the default +// label from a string bundle as is done for all other UI strings. +// See bug 16999 for further details. +nsresult nsGfxButtonControlFrame::GetDefaultLabel(nsAString& aString) const { + nsCOMPtr<nsIFormControl> form = do_QueryInterface(mContent); + NS_ENSURE_TRUE(form, NS_ERROR_UNEXPECTED); + + auto type = form->ControlType(); + const char* prop; + if (type == FormControlType::InputReset) { + prop = "Reset"; + } else if (type == FormControlType::InputSubmit) { + prop = "Submit"; + } else { + aString.Truncate(); + return NS_OK; + } + + return nsContentUtils::GetMaybeLocalizedString( + nsContentUtils::eFORMS_PROPERTIES, prop, mContent->OwnerDoc(), aString); +} + +nsresult nsGfxButtonControlFrame::GetLabel(nsString& aLabel) { + // Get the text from the "value" property on our content if there is + // one; otherwise set it to a default value (localized). + auto* elt = dom::HTMLInputElement::FromNode(mContent); + if (elt && elt->HasAttr(nsGkAtoms::value)) { + elt->GetValue(aLabel, dom::CallerType::System); + } else { + // Generate localized label. + // We can't make any assumption as to what the default would be + // because the value is localized for non-english platforms, thus + // it might not be the string "Reset", "Submit Query", or "Browse..." + nsresult rv; + rv = GetDefaultLabel(aLabel); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Compress whitespace out of label if needed. + if (!StyleText()->WhiteSpaceIsSignificant()) { + aLabel.CompressWhitespace(); + } else if (aLabel.Length() > 2 && aLabel.First() == ' ' && + aLabel.CharAt(aLabel.Length() - 1) == ' ') { + // This is a bit of a hack. The reason this is here is as follows: we now + // have default padding on our buttons to make them non-ugly. + // Unfortunately, IE-windows does not have such padding, so people will + // stick values like " ok " (with the spaces) in the buttons in an attempt + // to make them look decent. Unfortunately, if they do this the button + // looks way too big in Mozilla. Worse yet, if they do this _and_ set a + // fixed width for the button we run into trouble because our focus-rect + // border/padding and outer border take up 10px of the horizontal button + // space or so; the result is that the text is misaligned, even with the + // recentering we do in nsHTMLButtonControlFrame::Reflow. So to solve + // this, even if the whitespace is significant, single leading and trailing + // _spaces_ (and not other whitespace) are removed. The proper solution, + // of course, is to not have the focus rect painting taking up 6px of + // horizontal space. We should do that instead (changing the renderer) and + // remove this. + aLabel.Cut(0, 1); + aLabel.Truncate(aLabel.Length() - 1); + } + + return NS_OK; +} + +nsresult nsGfxButtonControlFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType) { + nsresult rv = NS_OK; + + // If the value attribute is set, update the text of the label + if (nsGkAtoms::value == aAttribute) { + if (mTextContent && mContent) { + nsAutoString label; + rv = GetLabel(label); + NS_ENSURE_SUCCESS(rv, rv); + + mTextContent->SetText(label, true); + } else { + rv = NS_ERROR_UNEXPECTED; + } + + // defer to HTMLButtonControlFrame + } else { + rv = nsHTMLButtonControlFrame::AttributeChanged(aNameSpaceID, aAttribute, + aModType); + } + return rv; +} + +nsContainerFrame* nsGfxButtonControlFrame::GetContentInsertionFrame() { + return this; +} + +nsresult nsGfxButtonControlFrame::HandleEvent(nsPresContext* aPresContext, + WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) { + // Override the HandleEvent to prevent the nsIFrame::HandleEvent + // from being called. The nsIFrame::HandleEvent causes the button label + // to be selected (Drawn with an XOR rectangle over the label) + + if (IsContentDisabled()) { + return nsIFrame::HandleEvent(aPresContext, aEvent, aEventStatus); + } + return NS_OK; +} diff --git a/layout/forms/nsGfxButtonControlFrame.h b/layout/forms/nsGfxButtonControlFrame.h new file mode 100644 index 0000000000..b9e1b40984 --- /dev/null +++ b/layout/forms/nsGfxButtonControlFrame.h @@ -0,0 +1,64 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsGfxButtonControlFrame_h___ +#define nsGfxButtonControlFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsHTMLButtonControlFrame.h" +#include "nsCOMPtr.h" +#include "nsIAnonymousContentCreator.h" + +class nsTextNode; + +// Class which implements the input[type=button, reset, submit] and +// browse button for input[type=file]. +// The label for button is specified through generated content +// in the ua.css file. + +class nsGfxButtonControlFrame final : public nsHTMLButtonControlFrame, + public nsIAnonymousContentCreator { + public: + NS_DECL_FRAMEARENA_HELPERS(nsGfxButtonControlFrame) + + explicit nsGfxButtonControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext); + + void Destroy(DestroyContext&) override; + + virtual nsresult HandleEvent(nsPresContext* aPresContext, + mozilla::WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) override; + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override; +#endif + + NS_DECL_QUERYFRAME + + // nsIAnonymousContentCreator + virtual nsresult CreateAnonymousContent( + nsTArray<ContentInfo>& aElements) override; + virtual void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) override; + + virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + + virtual nsContainerFrame* GetContentInsertionFrame() override; + + protected: + nsresult GetDefaultLabel(nsAString& aLabel) const; + + nsresult GetLabel(nsString& aLabel); + + virtual bool IsInput() override { return true; } + + private: + RefPtr<nsTextNode> mTextContent; +}; + +#endif diff --git a/layout/forms/nsHTMLButtonControlFrame.cpp b/layout/forms/nsHTMLButtonControlFrame.cpp new file mode 100644 index 0000000000..52ec18ed0e --- /dev/null +++ b/layout/forms/nsHTMLButtonControlFrame.cpp @@ -0,0 +1,398 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsHTMLButtonControlFrame.h" + +#include "mozilla/Baseline.h" +#include "mozilla/PresShell.h" +#include "nsContainerFrame.h" +#include "nsIFormControlFrame.h" +#include "nsIFrameInlines.h" +#include "nsPresContext.h" +#include "nsGkAtoms.h" +#include "nsButtonFrameRenderer.h" +#include "nsCSSAnonBoxes.h" +#include "nsNameSpaceManager.h" +#include "nsDisplayList.h" +#include <algorithm> + +using namespace mozilla; + +nsContainerFrame* NS_NewHTMLButtonControlFrame(PresShell* aPresShell, + ComputedStyle* aStyle) { + return new (aPresShell) + nsHTMLButtonControlFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsHTMLButtonControlFrame) + +nsHTMLButtonControlFrame::nsHTMLButtonControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext, + nsIFrame::ClassID aID) + : nsContainerFrame(aStyle, aPresContext, aID) {} + +nsHTMLButtonControlFrame::~nsHTMLButtonControlFrame() = default; + +void nsHTMLButtonControlFrame::Init(nsIContent* aContent, + nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + nsContainerFrame::Init(aContent, aParent, aPrevInFlow); + mRenderer.SetFrame(this, PresContext()); +} + +NS_QUERYFRAME_HEAD(nsHTMLButtonControlFrame) + NS_QUERYFRAME_ENTRY(nsIFormControlFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) + +#ifdef ACCESSIBILITY +a11y::AccType nsHTMLButtonControlFrame::AccessibleType() { + return a11y::eHTMLButtonType; +} +#endif + +void nsHTMLButtonControlFrame::SetFocus(bool aOn, bool aRepaint) {} + +nsresult nsHTMLButtonControlFrame::HandleEvent(nsPresContext* aPresContext, + WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) { + // if disabled do nothing + if (mRenderer.isDisabled()) { + return NS_OK; + } + + // mouse clicks are handled by content + // we don't want our children to get any events. So just pass it to frame. + return nsIFrame::HandleEvent(aPresContext, aEvent, aEventStatus); +} + +bool nsHTMLButtonControlFrame::ShouldClipPaintingToBorderBox() { + return IsInput() || StyleDisplay()->mOverflowX != StyleOverflow::Visible; +} + +void nsHTMLButtonControlFrame::BuildDisplayList( + nsDisplayListBuilder* aBuilder, const nsDisplayListSet& aLists) { + nsDisplayList onTop(aBuilder); + if (IsVisibleForPainting()) { + // Clip the button itself to its border area for event hit testing. + Maybe<DisplayListClipState::AutoSaveRestore> eventClipState; + if (aBuilder->IsForEventDelivery()) { + eventClipState.emplace(aBuilder); + nsRect rect(aBuilder->ToReferenceFrame(this), GetSize()); + nscoord radii[8]; + bool hasRadii = GetBorderRadii(radii); + eventClipState->ClipContainingBlockDescendants( + rect, hasRadii ? radii : nullptr); + } + + mRenderer.DisplayButton(aBuilder, aLists.BorderBackground(), &onTop); + } + + nsDisplayListCollection set(aBuilder); + + { + DisplayListClipState::AutoSaveRestore clipState(aBuilder); + + if (ShouldClipPaintingToBorderBox()) { + nsMargin border = StyleBorder()->GetComputedBorder(); + nsRect rect(aBuilder->ToReferenceFrame(this), GetSize()); + rect.Deflate(border); + nscoord radii[8]; + bool hasRadii = GetPaddingBoxBorderRadii(radii); + clipState.ClipContainingBlockDescendants(rect, + hasRadii ? radii : nullptr); + } + + BuildDisplayListForChild(aBuilder, mFrames.FirstChild(), set, + DisplayChildFlag::ForcePseudoStackingContext); + } + + // Put the foreground outline and focus rects on top of the children + set.Content()->AppendToTop(&onTop); + set.MoveTo(aLists); + + DisplayOutline(aBuilder, aLists); + + // to draw border when selected in editor + DisplaySelectionOverlay(aBuilder, aLists.Content()); +} + +nscoord nsHTMLButtonControlFrame::GetMinISize(gfxContext* aRenderingContext) { + nscoord result; + DISPLAY_MIN_INLINE_SIZE(this, result); + if (Maybe<nscoord> containISize = ContainIntrinsicISize()) { + result = *containISize; + } else { + nsIFrame* kid = mFrames.FirstChild(); + result = nsLayoutUtils::IntrinsicForContainer(aRenderingContext, kid, + IntrinsicISizeType::MinISize); + } + return result; +} + +nscoord nsHTMLButtonControlFrame::GetPrefISize(gfxContext* aRenderingContext) { + nscoord result; + DISPLAY_PREF_INLINE_SIZE(this, result); + if (Maybe<nscoord> containISize = ContainIntrinsicISize()) { + result = *containISize; + } else { + nsIFrame* kid = mFrames.FirstChild(); + result = nsLayoutUtils::IntrinsicForContainer( + aRenderingContext, kid, IntrinsicISizeType::PrefISize); + } + return result; +} + +void nsHTMLButtonControlFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MarkInReflow(); + DO_GLOBAL_REFLOW_COUNT("nsHTMLButtonControlFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + + // Reflow the child + nsIFrame* firstKid = mFrames.FirstChild(); + + MOZ_ASSERT(firstKid, "Button should have a child frame for its contents"); + MOZ_ASSERT(!firstKid->GetNextSibling(), + "Button should have exactly one child frame"); + MOZ_ASSERT( + firstKid->Style()->GetPseudoType() == PseudoStyleType::buttonContent, + "Button's child frame has unexpected pseudo type!"); + + // XXXbz Eventually we may want to check-and-bail if + // !aReflowInput.ShouldReflowAllKids() && + // !firstKid->IsSubtreeDirty(). + // We'd need to cache our ascent for that, of course. + + // Reflow the contents of the button. + // (This populates our aDesiredSize, too.) + ReflowButtonContents(aPresContext, aDesiredSize, aReflowInput, firstKid); + + if (!ShouldClipPaintingToBorderBox()) { + ConsiderChildOverflow(aDesiredSize.mOverflowAreas, firstKid); + } + // else, we ignore child overflow -- anything that overflows beyond our + // own border-box will get clipped when painting. + + FinishReflowWithAbsoluteFrames(aPresContext, aDesiredSize, aReflowInput, + aStatus); + + // We're always complete and we don't support overflow containers + // so we shouldn't have a next-in-flow ever. + aStatus.Reset(); + MOZ_ASSERT(!GetNextInFlow()); +} + +void nsHTMLButtonControlFrame::ReflowButtonContents( + nsPresContext* aPresContext, ReflowOutput& aButtonDesiredSize, + const ReflowInput& aButtonReflowInput, nsIFrame* aFirstKid) { + WritingMode wm = GetWritingMode(); + LogicalSize availSize = aButtonReflowInput.ComputedSize(wm); + availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE; + + // shorthand for a value we need to use in a bunch of places + const LogicalMargin& clbp = + aButtonReflowInput.ComputedLogicalBorderPadding(wm); + + LogicalPoint childPos(wm); + childPos.I(wm) = clbp.IStart(wm); + availSize.ISize(wm) = std::max(availSize.ISize(wm), 0); + + ReflowInput contentsReflowInput(aPresContext, aButtonReflowInput, aFirstKid, + availSize); + + nsReflowStatus contentsReflowStatus; + ReflowOutput contentsDesiredSize(aButtonReflowInput); + childPos.B(wm) = 0; // This will be set properly later, after reflowing the + // child to determine its size. + + if (aFirstKid->IsFlexOrGridContainer()) { + // XXX: Should we use ResetResizeFlags::Yes? + contentsReflowInput.SetComputedBSize(aButtonReflowInput.ComputedBSize(), + ReflowInput::ResetResizeFlags::No); + contentsReflowInput.SetComputedMinBSize( + aButtonReflowInput.ComputedMinBSize()); + contentsReflowInput.SetComputedMaxBSize( + aButtonReflowInput.ComputedMaxBSize()); + } + + // We just pass a dummy containerSize here, as the child will be + // repositioned later by FinishReflowChild. + nsSize dummyContainerSize; + ReflowChild(aFirstKid, aPresContext, contentsDesiredSize, contentsReflowInput, + wm, childPos, dummyContainerSize, ReflowChildFlags::Default, + contentsReflowStatus); + MOZ_ASSERT(contentsReflowStatus.IsComplete(), + "We gave button-contents frame unconstrained available height, " + "so it should be complete"); + + // Compute the button's content-box size: + LogicalSize buttonContentBox(wm); + if (aButtonReflowInput.ComputedBSize() != NS_UNCONSTRAINEDSIZE) { + // Button has a fixed block-size -- that's its content-box bSize. + buttonContentBox.BSize(wm) = aButtonReflowInput.ComputedBSize(); + } else { + // Button is intrinsically sized -- it should shrinkwrap the + // button-contents' bSize. But if it has size containment in block axis, + // ignore the contents and use contain-intrinsic-block-size. + nscoord bSize = aButtonReflowInput.mFrame->ContainIntrinsicBSize().valueOr( + contentsDesiredSize.BSize(wm)); + + // Make sure we obey min/max-bSize in the case when we're doing intrinsic + // sizing (we get it for free when we have a non-intrinsic + // aButtonReflowInput.ComputedBSize()). Note that we do this before + // adjusting for borderpadding, since mComputedMaxBSize and + // mComputedMinBSize are content bSizes. + buttonContentBox.BSize(wm) = + NS_CSS_MINMAX(bSize, aButtonReflowInput.ComputedMinBSize(), + aButtonReflowInput.ComputedMaxBSize()); + } + if (aButtonReflowInput.ComputedISize() != NS_UNCONSTRAINEDSIZE) { + buttonContentBox.ISize(wm) = aButtonReflowInput.ComputedISize(); + } else { + nscoord iSize = aButtonReflowInput.mFrame->ContainIntrinsicISize().valueOr( + contentsDesiredSize.ISize(wm)); + buttonContentBox.ISize(wm) = + NS_CSS_MINMAX(iSize, aButtonReflowInput.ComputedMinISize(), + aButtonReflowInput.ComputedMaxISize()); + } + + // Center child in the block-direction in the button + // (technically, inside of the button's focus-padding area) + nscoord extraSpace = + buttonContentBox.BSize(wm) - contentsDesiredSize.BSize(wm); + + childPos.B(wm) = std::max(0, extraSpace / 2); + + // Adjust childPos.B() to be in terms of the button's frame-rect: + childPos.B(wm) += clbp.BStart(wm); + + nsSize containerSize = (buttonContentBox + clbp.Size(wm)).GetPhysicalSize(wm); + + // Place the child + FinishReflowChild(aFirstKid, aPresContext, contentsDesiredSize, + &contentsReflowInput, wm, childPos, containerSize, + ReflowChildFlags::Default); + + // Make sure we have a useful 'ascent' value for the child + if (contentsDesiredSize.BlockStartAscent() == + ReflowOutput::ASK_FOR_BASELINE) { + WritingMode wm = aButtonReflowInput.GetWritingMode(); + contentsDesiredSize.SetBlockStartAscent(aFirstKid->GetLogicalBaseline(wm)); + } + + // OK, we're done with the child frame. + // Use what we learned to populate the button frame's reflow metrics. + // * Button's height & width are content-box size + border-box contribution: + aButtonDesiredSize.SetSize( + wm, + LogicalSize(wm, aButtonReflowInput.ComputedISize() + clbp.IStartEnd(wm), + buttonContentBox.BSize(wm) + clbp.BStartEnd(wm))); + + // * Button's ascent is its child's ascent, plus the child's block-offset + // within our frame... unless it's orthogonal, in which case we'll use the + // contents inline-size as an approximation for now. + // XXX is there a better strategy? should we include border-padding? + if (!aButtonReflowInput.mStyleDisplay->IsContainLayout()) { + if (aButtonDesiredSize.GetWritingMode().IsOrthogonalTo(wm)) { + aButtonDesiredSize.SetBlockStartAscent( + wm.IsAlphabeticalBaseline() ? contentsDesiredSize.ISize(wm) + : contentsDesiredSize.ISize(wm) / 2); + } else { + aButtonDesiredSize.SetBlockStartAscent( + contentsDesiredSize.BlockStartAscent() + childPos.B(wm)); + } + } // else: we're layout-contained, and so we have no baseline. + + aButtonDesiredSize.SetOverflowAreasToDesiredBounds(); +} + +Maybe<nscoord> nsHTMLButtonControlFrame::GetNaturalBaselineBOffset( + WritingMode aWM, BaselineSharingGroup aBaselineGroup, + BaselineExportContext aExportContext) const { + if (StyleDisplay()->IsContainLayout()) { + return Nothing{}; + } + + nsIFrame* inner = mFrames.FirstChild(); + if (MOZ_UNLIKELY(inner->GetWritingMode().IsOrthogonalTo(aWM))) { + return Nothing{}; + } + auto result = + inner->GetNaturalBaselineBOffset(aWM, aBaselineGroup, aExportContext) + .valueOrFrom([inner, aWM, aBaselineGroup]() { + return Baseline::SynthesizeBOffsetFromBorderBox(inner, aWM, + aBaselineGroup); + }); + + nscoord innerBStart = inner->BStart(aWM, GetSize()); + if (aBaselineGroup == BaselineSharingGroup::First) { + return Some(result + innerBStart); + } + return Some(result + BSize(aWM) - (innerBStart + inner->BSize(aWM))); +} + +BaselineSharingGroup nsHTMLButtonControlFrame::GetDefaultBaselineSharingGroup() + const { + nsIFrame* firstKid = mFrames.FirstChild(); + + MOZ_ASSERT(firstKid, "Button should have a child frame for its contents"); + MOZ_ASSERT(!firstKid->GetNextSibling(), + "Button should have exactly one child frame"); + return firstKid->GetDefaultBaselineSharingGroup(); +} + +nscoord nsHTMLButtonControlFrame::SynthesizeFallbackBaseline( + mozilla::WritingMode aWM, BaselineSharingGroup aBaselineGroup) const { + return Baseline::SynthesizeBOffsetFromMarginBox(this, aWM, aBaselineGroup); +} + +nsresult nsHTMLButtonControlFrame::SetFormProperty(nsAtom* aName, + const nsAString& aValue) { + if (nsGkAtoms::value == aName) { + return mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::value, + aValue, true); + } + return NS_OK; +} + +ComputedStyle* nsHTMLButtonControlFrame::GetAdditionalComputedStyle( + int32_t aIndex) const { + return mRenderer.GetComputedStyle(aIndex); +} + +void nsHTMLButtonControlFrame::SetAdditionalComputedStyle( + int32_t aIndex, ComputedStyle* aComputedStyle) { + mRenderer.SetComputedStyle(aIndex, aComputedStyle); +} + +void nsHTMLButtonControlFrame::AppendDirectlyOwnedAnonBoxes( + nsTArray<OwnedAnonBox>& aResult) { + MOZ_ASSERT(mFrames.FirstChild(), "Must have our button-content anon box"); + MOZ_ASSERT(!mFrames.FirstChild()->GetNextSibling(), + "Must only have our button-content anon box"); + aResult.AppendElement(OwnedAnonBox(mFrames.FirstChild())); +} + +#ifdef DEBUG +void nsHTMLButtonControlFrame::AppendFrames(ChildListID aListID, + nsFrameList&& aFrameList) { + MOZ_CRASH("unsupported operation"); +} + +void nsHTMLButtonControlFrame::InsertFrames( + ChildListID aListID, nsIFrame* aPrevFrame, + const nsLineList::iterator* aPrevFrameLine, nsFrameList&& aFrameList) { + MOZ_CRASH("unsupported operation"); +} + +void nsHTMLButtonControlFrame::RemoveFrame(DestroyContext&, ChildListID, + nsIFrame*) { + MOZ_CRASH("unsupported operation"); +} +#endif diff --git a/layout/forms/nsHTMLButtonControlFrame.h b/layout/forms/nsHTMLButtonControlFrame.h new file mode 100644 index 0000000000..4294a3e960 --- /dev/null +++ b/layout/forms/nsHTMLButtonControlFrame.h @@ -0,0 +1,117 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsHTMLButtonControlFrame_h___ +#define nsHTMLButtonControlFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsContainerFrame.h" +#include "nsIFormControlFrame.h" +#include "nsButtonFrameRenderer.h" + +class gfxContext; +class nsPresContext; + +class nsHTMLButtonControlFrame : public nsContainerFrame, + public nsIFormControlFrame { + public: + explicit nsHTMLButtonControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsHTMLButtonControlFrame(aStyle, aPresContext, kClassID) {} + + ~nsHTMLButtonControlFrame(); + + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsHTMLButtonControlFrame) + + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + nscoord GetMinISize(gfxContext* aRenderingContext) override; + + nscoord GetPrefISize(gfxContext* aRenderingContext) override; + + void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + Maybe<nscoord> GetNaturalBaselineBOffset( + mozilla::WritingMode aWM, BaselineSharingGroup aBaselineGroup, + BaselineExportContext aExportContext) const override; + + nsresult HandleEvent(nsPresContext* aPresContext, + mozilla::WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) override; + + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + + ComputedStyle* GetAdditionalComputedStyle(int32_t aIndex) const override; + void SetAdditionalComputedStyle(int32_t aIndex, + ComputedStyle* aComputedStyle) override; + +#ifdef DEBUG + void AppendFrames(ChildListID aListID, nsFrameList&& aFrameList) override; + void InsertFrames(ChildListID aListID, nsIFrame* aPrevFrame, + const nsLineList::iterator* aPrevFrameLine, + nsFrameList&& aFrameList) override; + void RemoveFrame(DestroyContext&, ChildListID, nsIFrame*) override; +#endif + +#ifdef ACCESSIBILITY + mozilla::a11y::AccType AccessibleType() override; +#endif + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"HTMLButtonControl"_ns, aResult); + } +#endif + + // nsIFormControlFrame + void SetFocus(bool aOn, bool aRepaint) override; + nsresult SetFormProperty(nsAtom* aName, const nsAString& aValue) override; + + // Inserted child content gets its frames parented by our child block + nsContainerFrame* GetContentInsertionFrame() override { + return PrincipalChildList().FirstChild()->GetContentInsertionFrame(); + } + + bool IsFrameOfType(uint32_t aFlags) const override { + return nsContainerFrame::IsFrameOfType( + aFlags & ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock)); + } + + // Return the ::-moz-button-content anonymous box. + void AppendDirectlyOwnedAnonBoxes(nsTArray<OwnedAnonBox>& aResult) override; + + protected: + nsHTMLButtonControlFrame(ComputedStyle* aStyle, nsPresContext* aPresContext, + nsIFrame::ClassID aID); + + virtual bool IsInput() { return false; } + + // Indicates whether we should clip our children's painting to our + // border-box (either because of "overflow" or because of legacy reasons + // about how <input>-flavored buttons work). + bool ShouldClipPaintingToBorderBox(); + + // Reflows the button's sole child frame, and computes the desired size + // of the button itself from the results. + void ReflowButtonContents(nsPresContext* aPresContext, + ReflowOutput& aButtonDesiredSize, + const ReflowInput& aButtonReflowInput, + nsIFrame* aFirstKid); + + BaselineSharingGroup GetDefaultBaselineSharingGroup() const override; + nscoord SynthesizeFallbackBaseline( + mozilla::WritingMode aWM, + BaselineSharingGroup aBaselineGroup) const override; + + nsButtonFrameRenderer mRenderer; +}; + +#endif diff --git a/layout/forms/nsIFormControlFrame.h b/layout/forms/nsIFormControlFrame.h new file mode 100644 index 0000000000..45b7c63aa4 --- /dev/null +++ b/layout/forms/nsIFormControlFrame.h @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsIFormControlFrame_h___ +#define nsIFormControlFrame_h___ + +#include "nsQueryFrame.h" +#include "nsStringFwd.h" + +class nsAtom; + +/** + * nsIFormControlFrame is the common interface for frames of form controls. It + * provides a uniform way of creating widgets, resizing, and painting. + * @see nsLeafFrame and its base classes for more info + */ +class nsIFormControlFrame : public nsQueryFrame { + public: + NS_DECL_QUERYFRAME_TARGET(nsIFormControlFrame) + + /** + * + * @param aOn + * @param aRepaint + */ + virtual void SetFocus(bool aOn = true, bool aRepaint = false) = 0; + + /** + * Set a property on the form control frame. + * + * @param aName name of the property to set + * @param aValue value of the property + * @returns NS_OK if the property name is valid, otherwise an error code + */ + virtual nsresult SetFormProperty(nsAtom* aName, const nsAString& aValue) = 0; +}; + +#endif diff --git a/layout/forms/nsISelectControlFrame.h b/layout/forms/nsISelectControlFrame.h new file mode 100644 index 0000000000..b849f04d52 --- /dev/null +++ b/layout/forms/nsISelectControlFrame.h @@ -0,0 +1,50 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsISelectControlFrame_h___ +#define nsISelectControlFrame_h___ + +#include "nsQueryFrame.h" + +/** + * nsISelectControlFrame is the interface for combo boxes and listboxes + */ +class nsISelectControlFrame : public nsQueryFrame { + public: + NS_DECL_QUERYFRAME_TARGET(nsISelectControlFrame) + + /** + * Adds an option to the list at index + */ + + NS_IMETHOD AddOption(int32_t index) = 0; + + /** + * Removes the option at index. The caller must have a live script + * blocker while calling this method. + */ + NS_IMETHOD RemoveOption(int32_t index) = 0; + + /** + * Sets whether the parser is done adding children + * @param aIsDone whether the parser is done adding children + */ + NS_IMETHOD DoneAddingChildren(bool aIsDone) = 0; + + /** + * Notify the frame when an option is selected + */ + NS_IMETHOD OnOptionSelected(int32_t aIndex, bool aSelected) = 0; + + /** + * Notify the frame when selectedIndex was changed. This might + * destroy the frame. + */ + NS_IMETHOD_(void) + OnSetSelectedIndex(int32_t aOldIndex, int32_t aNewIndex) = 0; +}; + +#endif diff --git a/layout/forms/nsITextControlFrame.h b/layout/forms/nsITextControlFrame.h new file mode 100644 index 0000000000..a1c1df1e2e --- /dev/null +++ b/layout/forms/nsITextControlFrame.h @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsITextControlFrame_h___ +#define nsITextControlFrame_h___ + +#include "nsIFormControlFrame.h" +#include "mozilla/AlreadyAddRefed.h" + +class nsISelectionController; +class nsFrameSelection; + +namespace mozilla { +class TextEditor; +} // namespace mozilla + +// FIXME(emilio): This has only one implementation, seems it could be removed... +class nsITextControlFrame : public nsIFormControlFrame { + public: + NS_DECL_QUERYFRAME_TARGET(nsITextControlFrame) + + enum class SelectionDirection : uint8_t { None, Forward, Backward }; + + virtual already_AddRefed<mozilla::TextEditor> GetTextEditor() = 0; + + MOZ_CAN_RUN_SCRIPT NS_IMETHOD + SetSelectionRange(uint32_t aSelectionStart, uint32_t aSelectionEnd, + SelectionDirection = SelectionDirection::None) = 0; + + NS_IMETHOD GetOwnedSelectionController(nsISelectionController** aSelCon) = 0; + virtual nsFrameSelection* GetOwnedFrameSelection() = 0; + + /** + * Ensure editor is initialized with the proper flags and the default value. + * @throws NS_ERROR_NOT_INITIALIZED if mEditor has not been created + * @throws various and sundry other things + */ + virtual nsresult EnsureEditorInitialized() = 0; +}; + +#endif diff --git a/layout/forms/nsImageControlFrame.cpp b/layout/forms/nsImageControlFrame.cpp new file mode 100644 index 0000000000..898d890e04 --- /dev/null +++ b/layout/forms/nsImageControlFrame.cpp @@ -0,0 +1,151 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsImageFrame.h" + +#include "mozilla/MouseEvents.h" +#include "mozilla/PresShell.h" +#include "nsIFormControlFrame.h" +#include "nsPresContext.h" +#include "nsGkAtoms.h" +#include "nsStyleConsts.h" +#include "nsLayoutUtils.h" +#include "nsIContent.h" + +using namespace mozilla; + +class nsImageControlFrame final : public nsImageFrame, + public nsIFormControlFrame { + public: + explicit nsImageControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext); + ~nsImageControlFrame() final; + + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) final; + + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsImageControlFrame) + + void Reflow(nsPresContext*, ReflowOutput&, const ReflowInput&, + nsReflowStatus&) final; + + nsresult HandleEvent(nsPresContext*, WidgetGUIEvent*, nsEventStatus*) final; + +#ifdef ACCESSIBILITY + mozilla::a11y::AccType AccessibleType() final; +#endif + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const final { + return MakeFrameName(u"ImageControl"_ns, aResult); + } +#endif + + Maybe<Cursor> GetCursor(const nsPoint&) final; + + // nsIFormContromFrame + void SetFocus(bool aOn, bool aRepaint) final; + nsresult SetFormProperty(nsAtom* aName, const nsAString& aValue) final; +}; + +nsImageControlFrame::nsImageControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsImageFrame(aStyle, aPresContext, kClassID) {} + +nsImageControlFrame::~nsImageControlFrame() = default; + +nsIFrame* NS_NewImageControlFrame(PresShell* aPresShell, + ComputedStyle* aStyle) { + return new (aPresShell) + nsImageControlFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsImageControlFrame) + +void nsImageControlFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + nsImageFrame::Init(aContent, aParent, aPrevInFlow); + + if (aPrevInFlow) { + return; + } + + mContent->SetProperty(nsGkAtoms::imageClickedPoint, new CSSIntPoint(0, 0), + nsINode::DeleteProperty<CSSIntPoint>); +} + +NS_QUERYFRAME_HEAD(nsImageControlFrame) + NS_QUERYFRAME_ENTRY(nsIFormControlFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsImageFrame) + +#ifdef ACCESSIBILITY +a11y::AccType nsImageControlFrame::AccessibleType() { + if (mContent->IsAnyOfHTMLElements(nsGkAtoms::button, nsGkAtoms::input)) { + return a11y::eHTMLButtonType; + } + + return a11y::eNoType; +} +#endif + +void nsImageControlFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + DO_GLOBAL_REFLOW_COUNT("nsImageControlFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + return nsImageFrame::Reflow(aPresContext, aDesiredSize, aReflowInput, + aStatus); +} + +nsresult nsImageControlFrame::HandleEvent(nsPresContext* aPresContext, + WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) { + NS_ENSURE_ARG_POINTER(aEventStatus); + + // Don't do anything if the event has already been handled by someone + if (nsEventStatus_eConsumeNoDefault == *aEventStatus) { + return NS_OK; + } + + if (IsContentDisabled()) { + return nsIFrame::HandleEvent(aPresContext, aEvent, aEventStatus); + } + + *aEventStatus = nsEventStatus_eIgnore; + + if (aEvent->mMessage == eMouseUp && + aEvent->AsMouseEvent()->mButton == MouseButton::ePrimary) { + // Store click point for HTMLInputElement::SubmitNamesValues + // Do this on MouseUp because the specs don't say and that's what IE does + auto* lastClickedPoint = static_cast<CSSIntPoint*>( + mContent->GetProperty(nsGkAtoms::imageClickedPoint)); + if (lastClickedPoint) { + // normally lastClickedPoint is not null, as it's allocated in Init() + nsPoint pt = nsLayoutUtils::GetEventCoordinatesRelativeTo( + aEvent, RelativeTo{this}); + *lastClickedPoint = TranslateEventCoords(pt); + } + } + return nsImageFrame::HandleEvent(aPresContext, aEvent, aEventStatus); +} + +void nsImageControlFrame::SetFocus(bool aOn, bool aRepaint) {} + +Maybe<nsIFrame::Cursor> nsImageControlFrame::GetCursor(const nsPoint&) { + StyleCursorKind kind = StyleUI()->Cursor().keyword; + if (kind == StyleCursorKind::Auto) { + kind = StyleCursorKind::Pointer; + } + return Some(Cursor{kind, AllowCustomCursorImage::Yes}); +} + +nsresult nsImageControlFrame::SetFormProperty(nsAtom* aName, + const nsAString& aValue) { + return NS_OK; +} diff --git a/layout/forms/nsListControlFrame.cpp b/layout/forms/nsListControlFrame.cpp new file mode 100644 index 0000000000..3dc4acd3b3 --- /dev/null +++ b/layout/forms/nsListControlFrame.cpp @@ -0,0 +1,1211 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nscore.h" +#include "nsCOMPtr.h" +#include "nsUnicharUtils.h" +#include "nsListControlFrame.h" +#include "HTMLSelectEventListener.h" +#include "nsGkAtoms.h" +#include "nsComboboxControlFrame.h" +#include "nsFontMetrics.h" +#include "nsIScrollableFrame.h" +#include "nsCSSRendering.h" +#include "nsLayoutUtils.h" +#include "nsDisplayList.h" +#include "nsContentUtils.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/HTMLOptGroupElement.h" +#include "mozilla/dom/HTMLOptionsCollection.h" +#include "mozilla/dom/HTMLSelectElement.h" +#include "mozilla/dom/MouseEvent.h" +#include "mozilla/dom/MouseEventBinding.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/Preferences.h" +#include "mozilla/PresShell.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/StaticPrefs_ui.h" +#include "mozilla/TextEvents.h" +#include <algorithm> + +using namespace mozilla; +using namespace mozilla::dom; + +// Static members +nsListControlFrame* nsListControlFrame::mFocused = nullptr; + +//--------------------------------------------------------- +nsListControlFrame* NS_NewListControlFrame(PresShell* aPresShell, + ComputedStyle* aStyle) { + nsListControlFrame* it = + new (aPresShell) nsListControlFrame(aStyle, aPresShell->GetPresContext()); + + it->AddStateBits(NS_FRAME_INDEPENDENT_SELECTION); + + return it; +} + +NS_IMPL_FRAMEARENA_HELPERS(nsListControlFrame) + +nsListControlFrame::nsListControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsHTMLScrollFrame(aStyle, aPresContext, kClassID, false), + mMightNeedSecondPass(false), + mHasPendingInterruptAtStartOfReflow(false), + mForceSelection(false) { + mChangesSinceDragStart = false; + + mIsAllContentHere = false; + mIsAllFramesHere = false; + mHasBeenInitialized = false; + mNeedToReset = true; + mPostChildrenLoadedReset = false; +} + +nsListControlFrame::~nsListControlFrame() = default; + +Maybe<nscoord> nsListControlFrame::GetNaturalBaselineBOffset( + WritingMode aWM, BaselineSharingGroup aBaselineGroup, + BaselineExportContext) const { + // Unlike scroll frames which we inherit from, we don't export a baseline. + return Nothing{}; +} +// for Bug 47302 (remove this comment later) +void nsListControlFrame::Destroy(DestroyContext& aContext) { + // get the receiver interface from the browser button's content node + NS_ENSURE_TRUE_VOID(mContent); + + // Clear the frame pointer on our event listener, just in case the + // event listener can outlive the frame. + + mEventListener->Detach(); + nsHTMLScrollFrame::Destroy(aContext); +} + +void nsListControlFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + // We allow visibility:hidden <select>s to contain visible options. + + // Don't allow painting of list controls when painting is suppressed. + // XXX why do we need this here? we should never reach this. Maybe + // because these can have widgets? Hmm + if (aBuilder->IsBackgroundOnly()) return; + + DO_GLOBAL_REFLOW_COUNT_DSP("nsListControlFrame"); + + nsHTMLScrollFrame::BuildDisplayList(aBuilder, aLists); +} + +HTMLOptionElement* nsListControlFrame::GetCurrentOption() const { + return mEventListener->GetCurrentOption(); +} + +/** + * This is called by the SelectsAreaFrame, which is the same + * as the frame returned by GetOptionsContainer. It's the frame which is + * scrolled by us. + * @param aPt the offset of this frame, relative to the rendering reference + * frame + */ +void nsListControlFrame::PaintFocus(DrawTarget* aDrawTarget, nsPoint aPt) { + if (mFocused != this) return; + + nsIFrame* containerFrame = GetOptionsContainer(); + if (!containerFrame) return; + + nsIFrame* childframe = nullptr; + nsCOMPtr<nsIContent> focusedContent = GetCurrentOption(); + if (focusedContent) { + childframe = focusedContent->GetPrimaryFrame(); + } + + nsRect fRect; + if (childframe) { + // get the child rect + fRect = childframe->GetRect(); + // get it into our coordinates + fRect.MoveBy(childframe->GetParent()->GetOffsetTo(this)); + } else { + float inflation = nsLayoutUtils::FontSizeInflationFor(this); + fRect.x = fRect.y = 0; + if (GetWritingMode().IsVertical()) { + fRect.width = GetScrollPortRect().width; + fRect.height = CalcFallbackRowBSize(inflation); + } else { + fRect.width = CalcFallbackRowBSize(inflation); + fRect.height = GetScrollPortRect().height; + } + fRect.MoveBy(containerFrame->GetOffsetTo(this)); + } + fRect += aPt; + + const auto* domOpt = HTMLOptionElement::FromNodeOrNull(focusedContent); + const bool isSelected = domOpt && domOpt->Selected(); + + // Set up back stop colors and then ask L&F service for the real colors + nscolor color = + LookAndFeel::Color(isSelected ? LookAndFeel::ColorID::Selecteditemtext + : LookAndFeel::ColorID::Selecteditem, + this); + + nsCSSRendering::PaintFocus(PresContext(), aDrawTarget, fRect, color); +} + +void nsListControlFrame::InvalidateFocus() { + if (mFocused != this) return; + + nsIFrame* containerFrame = GetOptionsContainer(); + if (containerFrame) { + containerFrame->InvalidateFrame(); + } +} + +NS_QUERYFRAME_HEAD(nsListControlFrame) + NS_QUERYFRAME_ENTRY(nsIFormControlFrame) + NS_QUERYFRAME_ENTRY(nsISelectControlFrame) + NS_QUERYFRAME_ENTRY(nsListControlFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsHTMLScrollFrame) + +#ifdef ACCESSIBILITY +a11y::AccType nsListControlFrame::AccessibleType() { + return a11y::eHTMLSelectListType; +} +#endif + +// Return true if we found at least one <option> or non-empty <optgroup> label +// that has a frame. aResult will be the maximum BSize of those. +static bool GetMaxRowBSize(nsIFrame* aContainer, WritingMode aWM, + nscoord* aResult) { + bool found = false; + for (nsIFrame* child : aContainer->PrincipalChildList()) { + if (child->GetContent()->IsHTMLElement(nsGkAtoms::optgroup)) { + // An optgroup; drill through any scroll frame and recurse. |inner| might + // be null here though if |inner| is an anonymous leaf frame of some sort. + auto inner = child->GetContentInsertionFrame(); + if (inner && GetMaxRowBSize(inner, aWM, aResult)) { + found = true; + } + } else { + // an option or optgroup label + bool isOptGroupLabel = + child->Style()->IsPseudoElement() && + aContainer->GetContent()->IsHTMLElement(nsGkAtoms::optgroup); + nscoord childBSize = child->BSize(aWM); + // XXX bug 1499176: skip empty <optgroup> labels (zero bsize) for now + if (!isOptGroupLabel || childBSize > nscoord(0)) { + found = true; + *aResult = std::max(childBSize, *aResult); + } + } + } + return found; +} + +//----------------------------------------------------------------- +// Main Reflow for ListBox/Dropdown +//----------------------------------------------------------------- + +nscoord nsListControlFrame::CalcBSizeOfARow() { + // Calculate the block size in our writing mode of a single row in the + // listbox or dropdown list by using the tallest thing in the subtree, + // since there may be option groups in addition to option elements, + // either of which may be visible or invisible, may use different + // fonts, etc. + nscoord rowBSize(0); + if (GetContainSizeAxes().mBContained || + !GetMaxRowBSize(GetOptionsContainer(), GetWritingMode(), &rowBSize)) { + // We don't have any <option>s or <optgroup> labels with a frame. + // (Or we're size-contained in block axis, which has the same outcome for + // our sizing.) + float inflation = nsLayoutUtils::FontSizeInflationFor(this); + rowBSize = CalcFallbackRowBSize(inflation); + } + return rowBSize; +} + +nscoord nsListControlFrame::GetPrefISize(gfxContext* aRenderingContext) { + nscoord result; + DISPLAY_PREF_INLINE_SIZE(this, result); + + // Always add scrollbar inline sizes to the pref-inline-size of the + // scrolled content. Combobox frames depend on this happening in the + // dropdown, and standalone listboxes are overflow:scroll so they need + // it too. + WritingMode wm = GetWritingMode(); + Maybe<nscoord> containISize = ContainIntrinsicISize(); + result = containISize ? *containISize + : GetScrolledFrame()->GetPrefISize(aRenderingContext); + LogicalMargin scrollbarSize(wm, GetDesiredScrollbarSizes()); + result = NSCoordSaturatingAdd(result, scrollbarSize.IStartEnd(wm)); + return result; +} + +nscoord nsListControlFrame::GetMinISize(gfxContext* aRenderingContext) { + nscoord result; + DISPLAY_MIN_INLINE_SIZE(this, result); + + // Always add scrollbar inline sizes to the min-inline-size of the + // scrolled content. Combobox frames depend on this happening in the + // dropdown, and standalone listboxes are overflow:scroll so they need + // it too. + WritingMode wm = GetWritingMode(); + Maybe<nscoord> containISize = ContainIntrinsicISize(); + result = containISize ? *containISize + : GetScrolledFrame()->GetMinISize(aRenderingContext); + LogicalMargin scrollbarSize(wm, GetDesiredScrollbarSizes()); + result += scrollbarSize.IStartEnd(wm); + + return result; +} + +void nsListControlFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + NS_WARNING_ASSERTION(aReflowInput.ComputedISize() != NS_UNCONSTRAINEDSIZE, + "Must have a computed inline size"); + + SchedulePaint(); + + mHasPendingInterruptAtStartOfReflow = aPresContext->HasPendingInterrupt(); + + // If all the content and frames are here + // then initialize it before reflow + if (mIsAllContentHere && !mHasBeenInitialized) { + if (!mIsAllFramesHere) { + CheckIfAllFramesHere(); + } + if (mIsAllFramesHere && !mHasBeenInitialized) { + mHasBeenInitialized = true; + } + } + + MarkInReflow(); + // Due to the fact that our intrinsic block size depends on the block + // sizes of our kids, we end up having to do two-pass reflow, in + // general -- the first pass to find the intrinsic block size and a + // second pass to reflow the scrollframe at that block size (which + // will size the scrollbars correctly, etc). + // + // Naturally, we want to avoid doing the second reflow as much as + // possible. We can skip it in the following cases (in all of which the first + // reflow is already happening at the right block size): + bool autoBSize = (aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE); + Maybe<nscoord> containBSize = ContainIntrinsicBSize(NS_UNCONSTRAINEDSIZE); + bool usingContainBSize = + autoBSize && containBSize && *containBSize != NS_UNCONSTRAINEDSIZE; + + mMightNeedSecondPass = [&] { + if (!autoBSize) { + // We're reflowing with a constrained computed block size -- just use that + // block size. + return false; + } + if (!IsSubtreeDirty() && !aReflowInput.ShouldReflowAllKids()) { + // We're not dirty and have no dirty kids and shouldn't be reflowing all + // kids. In this case, our cached max block size of a child is not going + // to change. + return false; + } + if (usingContainBSize) { + // We're size-contained in the block axis. In this case the size of a row + // doesn't depend on our children (it's the "fallback" size). + return false; + } + // We might need to do a second pass. If we do our first reflow using our + // cached max block size of a child, then compute the new max block size, + // and it's the same as the old one, we might still skip it (see the + // IsScrollbarUpdateSuppressed() check). + return true; + }(); + + ReflowInput state(aReflowInput); + int32_t length = GetNumberOfRows(); + + nscoord oldBSizeOfARow = BSizeOfARow(); + + if (!HasAnyStateBits(NS_FRAME_FIRST_REFLOW) && autoBSize) { + // When not doing an initial reflow, and when the block size is + // auto, start off with our computed block size set to what we'd + // expect our block size to be. + nscoord computedBSize = CalcIntrinsicBSize(oldBSizeOfARow, length); + computedBSize = state.ApplyMinMaxBSize(computedBSize); + state.SetComputedBSize(computedBSize); + } + + if (usingContainBSize) { + state.SetComputedBSize(*containBSize); + } + + nsHTMLScrollFrame::Reflow(aPresContext, aDesiredSize, state, aStatus); + + if (!mMightNeedSecondPass) { + NS_ASSERTION(!autoBSize || BSizeOfARow() == oldBSizeOfARow, + "How did our BSize of a row change if nothing was dirty?"); + NS_ASSERTION(!autoBSize || !HasAnyStateBits(NS_FRAME_FIRST_REFLOW) || + usingContainBSize, + "How do we not need a second pass during initial reflow at " + "auto BSize?"); + NS_ASSERTION(!IsScrollbarUpdateSuppressed(), + "Shouldn't be suppressing if we don't need a second pass!"); + if (!autoBSize || usingContainBSize) { + // Update our mNumDisplayRows based on our new row block size now + // that we know it. Note that if autoBSize and we landed in this + // code then we already set mNumDisplayRows in CalcIntrinsicBSize. + // Also note that we can't use BSizeOfARow() here because that + // just uses a cached value that we didn't compute. + nscoord rowBSize = CalcBSizeOfARow(); + if (rowBSize == 0) { + // Just pick something + mNumDisplayRows = 1; + } else { + mNumDisplayRows = std::max(1, state.ComputedBSize() / rowBSize); + } + } + + return; + } + + mMightNeedSecondPass = false; + + // Now see whether we need a second pass. If we do, our + // nsSelectsAreaFrame will have suppressed the scrollbar update. + if (!IsScrollbarUpdateSuppressed()) { + // All done. No need to do more reflow. + return; + } + + SetSuppressScrollbarUpdate(false); + + // Gotta reflow again. + // XXXbz We're just changing the block size here; do we need to dirty + // ourselves or anything like that? We might need to, per the letter + // of the reflow protocol, but things seem to work fine without it... + // Is that just an implementation detail of nsHTMLScrollFrame that + // we're depending on? + nsHTMLScrollFrame::DidReflow(aPresContext, &state); + + // Now compute the block size we want to have + nscoord computedBSize = CalcIntrinsicBSize(BSizeOfARow(), length); + computedBSize = state.ApplyMinMaxBSize(computedBSize); + state.SetComputedBSize(computedBSize); + + // XXXbz to make the ascent really correct, we should add our + // mComputedPadding.top to it (and subtract it from descent). Need that + // because nsGfxScrollFrame just adds in the border.... + aStatus.Reset(); + nsHTMLScrollFrame::Reflow(aPresContext, aDesiredSize, state, aStatus); +} + +bool nsListControlFrame::ShouldPropagateComputedBSizeToScrolledContent() const { + return true; +} + +//--------------------------------------------------------- +nsContainerFrame* nsListControlFrame::GetContentInsertionFrame() { + return GetOptionsContainer()->GetContentInsertionFrame(); +} + +//--------------------------------------------------------- +bool nsListControlFrame::ExtendedSelection(int32_t aStartIndex, + int32_t aEndIndex, bool aClearAll) { + return SetOptionsSelectedFromFrame(aStartIndex, aEndIndex, true, aClearAll); +} + +//--------------------------------------------------------- +bool nsListControlFrame::SingleSelection(int32_t aClickedIndex, + bool aDoToggle) { +#ifdef ACCESSIBILITY + nsCOMPtr<nsIContent> prevOption = mEventListener->GetCurrentOption(); +#endif + bool wasChanged = false; + // Get Current selection + if (aDoToggle) { + wasChanged = ToggleOptionSelectedFromFrame(aClickedIndex); + } else { + wasChanged = + SetOptionsSelectedFromFrame(aClickedIndex, aClickedIndex, true, true); + } + AutoWeakFrame weakFrame(this); + ScrollToIndex(aClickedIndex); + if (!weakFrame.IsAlive()) { + return wasChanged; + } + + mStartSelectionIndex = aClickedIndex; + mEndSelectionIndex = aClickedIndex; + InvalidateFocus(); + +#ifdef ACCESSIBILITY + FireMenuItemActiveEvent(prevOption); +#endif + + return wasChanged; +} + +void nsListControlFrame::InitSelectionRange(int32_t aClickedIndex) { + // + // If nothing is selected, set the start selection depending on where + // the user clicked and what the initial selection is: + // - if the user clicked *before* selectedIndex, set the start index to + // the end of the first contiguous selection. + // - if the user clicked *after* the end of the first contiguous + // selection, set the start index to selectedIndex. + // - if the user clicked *within* the first contiguous selection, set the + // start index to selectedIndex. + // The last two rules, of course, boil down to the same thing: if the user + // clicked >= selectedIndex, return selectedIndex. + // + // This makes it so that shift click works properly when you first click + // in a multiple select. + // + int32_t selectedIndex = GetSelectedIndex(); + if (selectedIndex >= 0) { + // Get the end of the contiguous selection + RefPtr<dom::HTMLOptionsCollection> options = GetOptions(); + NS_ASSERTION(options, "Collection of options is null!"); + uint32_t numOptions = options->Length(); + // Push i to one past the last selected index in the group. + uint32_t i; + for (i = selectedIndex + 1; i < numOptions; i++) { + if (!options->ItemAsOption(i)->Selected()) { + break; + } + } + + if (aClickedIndex < selectedIndex) { + // User clicked before selection, so start selection at end of + // contiguous selection + mStartSelectionIndex = i - 1; + mEndSelectionIndex = selectedIndex; + } else { + // User clicked after selection, so start selection at start of + // contiguous selection + mStartSelectionIndex = selectedIndex; + mEndSelectionIndex = i - 1; + } + } +} + +static uint32_t CountOptionsAndOptgroups(nsIFrame* aFrame) { + uint32_t count = 0; + for (nsIFrame* child : aFrame->PrincipalChildList()) { + nsIContent* content = child->GetContent(); + if (content) { + if (content->IsHTMLElement(nsGkAtoms::option)) { + ++count; + } else { + RefPtr<HTMLOptGroupElement> optgroup = + HTMLOptGroupElement::FromNode(content); + if (optgroup) { + nsAutoString label; + optgroup->GetLabel(label); + if (label.Length() > 0) { + ++count; + } + count += CountOptionsAndOptgroups(child); + } + } + } + } + return count; +} + +uint32_t nsListControlFrame::GetNumberOfRows() { + return ::CountOptionsAndOptgroups(GetContentInsertionFrame()); +} + +//--------------------------------------------------------- +bool nsListControlFrame::PerformSelection(int32_t aClickedIndex, bool aIsShift, + bool aIsControl) { + bool wasChanged = false; + + if (aClickedIndex == kNothingSelected && !mForceSelection) { + // Ignore kNothingSelected unless the selection is forced + } else if (GetMultiple()) { + if (aIsShift) { + // Make sure shift+click actually does something expected when + // the user has never clicked on the select + if (mStartSelectionIndex == kNothingSelected) { + InitSelectionRange(aClickedIndex); + } + + // Get the range from beginning (low) to end (high) + // Shift *always* works, even if the current option is disabled + int32_t startIndex; + int32_t endIndex; + if (mStartSelectionIndex == kNothingSelected) { + startIndex = aClickedIndex; + endIndex = aClickedIndex; + } else if (mStartSelectionIndex <= aClickedIndex) { + startIndex = mStartSelectionIndex; + endIndex = aClickedIndex; + } else { + startIndex = aClickedIndex; + endIndex = mStartSelectionIndex; + } + + // Clear only if control was not pressed + wasChanged = ExtendedSelection(startIndex, endIndex, !aIsControl); + AutoWeakFrame weakFrame(this); + ScrollToIndex(aClickedIndex); + if (!weakFrame.IsAlive()) { + return wasChanged; + } + + if (mStartSelectionIndex == kNothingSelected) { + mStartSelectionIndex = aClickedIndex; + } +#ifdef ACCESSIBILITY + nsCOMPtr<nsIContent> prevOption = GetCurrentOption(); +#endif + mEndSelectionIndex = aClickedIndex; + InvalidateFocus(); + +#ifdef ACCESSIBILITY + FireMenuItemActiveEvent(prevOption); +#endif + } else if (aIsControl) { + wasChanged = SingleSelection(aClickedIndex, true); // might destroy us + } else { + wasChanged = SingleSelection(aClickedIndex, false); // might destroy us + } + } else { + wasChanged = SingleSelection(aClickedIndex, false); // might destroy us + } + + return wasChanged; +} + +//--------------------------------------------------------- +bool nsListControlFrame::HandleListSelection(dom::Event* aEvent, + int32_t aClickedIndex) { + MouseEvent* mouseEvent = aEvent->AsMouseEvent(); + bool isControl; +#ifdef XP_MACOSX + isControl = mouseEvent->MetaKey(); +#else + isControl = mouseEvent->CtrlKey(); +#endif + bool isShift = mouseEvent->ShiftKey(); + return PerformSelection(aClickedIndex, isShift, + isControl); // might destroy us +} + +//--------------------------------------------------------- +void nsListControlFrame::CaptureMouseEvents(bool aGrabMouseEvents) { + if (aGrabMouseEvents) { + PresShell::SetCapturingContent(mContent, CaptureFlags::IgnoreAllowedState); + } else { + nsIContent* capturingContent = PresShell::GetCapturingContent(); + if (capturingContent == mContent) { + // only clear the capturing content if *we* are the ones doing the + // capturing (or if the dropdown is hidden, in which case NO-ONE should + // be capturing anything - it could be a scrollbar inside this listbox + // which is actually grabbing + // This shouldn't be necessary. We should simply ensure that events + // targeting scrollbars are never visible to DOM consumers. + PresShell::ReleaseCapturingContent(); + } + } +} + +//--------------------------------------------------------- +nsresult nsListControlFrame::HandleEvent(nsPresContext* aPresContext, + WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) { + NS_ENSURE_ARG_POINTER(aEventStatus); + + /*const char * desc[] = {"eMouseMove", + "NS_MOUSE_LEFT_BUTTON_UP", + "NS_MOUSE_LEFT_BUTTON_DOWN", + "<NA>","<NA>","<NA>","<NA>","<NA>","<NA>","<NA>", + "NS_MOUSE_MIDDLE_BUTTON_UP", + "NS_MOUSE_MIDDLE_BUTTON_DOWN", + "<NA>","<NA>","<NA>","<NA>","<NA>","<NA>","<NA>","<NA>", + "NS_MOUSE_RIGHT_BUTTON_UP", + "NS_MOUSE_RIGHT_BUTTON_DOWN", + "eMouseOver", + "eMouseOut", + "NS_MOUSE_LEFT_DOUBLECLICK", + "NS_MOUSE_MIDDLE_DOUBLECLICK", + "NS_MOUSE_RIGHT_DOUBLECLICK", + "NS_MOUSE_LEFT_CLICK", + "NS_MOUSE_MIDDLE_CLICK", + "NS_MOUSE_RIGHT_CLICK"}; + int inx = aEvent->mMessage - eMouseEventFirst; + if (inx >= 0 && inx <= (NS_MOUSE_RIGHT_CLICK - eMouseEventFirst)) { + printf("Mouse in ListFrame %s [%d]\n", desc[inx], aEvent->mMessage); + } else { + printf("Mouse in ListFrame <UNKNOWN> [%d]\n", aEvent->mMessage); + }*/ + + if (nsEventStatus_eConsumeNoDefault == *aEventStatus) return NS_OK; + + // disabled state affects how we're selected, but we don't want to go through + // nsHTMLScrollFrame if we're disabled. + if (IsContentDisabled()) { + return nsIFrame::HandleEvent(aPresContext, aEvent, aEventStatus); + } + + return nsHTMLScrollFrame::HandleEvent(aPresContext, aEvent, aEventStatus); +} + +//--------------------------------------------------------- +void nsListControlFrame::SetInitialChildList(ChildListID aListID, + nsFrameList&& aChildList) { + if (aListID == FrameChildListID::Principal) { + // First check to see if all the content has been added + mIsAllContentHere = mContent->IsDoneAddingChildren(); + if (!mIsAllContentHere) { + mIsAllFramesHere = false; + mHasBeenInitialized = false; + } + } + nsHTMLScrollFrame::SetInitialChildList(aListID, std::move(aChildList)); +} + +HTMLSelectElement& nsListControlFrame::Select() const { + return *static_cast<HTMLSelectElement*>(GetContent()); +} + +//--------------------------------------------------------- +void nsListControlFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + nsHTMLScrollFrame::Init(aContent, aParent, aPrevInFlow); + + // we shouldn't have to unregister this listener because when + // our frame goes away all these content node go away as well + // because our frame is the only one who references them. + // we need to hook up our listeners before the editor is initialized + mEventListener = new HTMLSelectEventListener( + Select(), HTMLSelectEventListener::SelectType::Listbox); + + mStartSelectionIndex = kNothingSelected; + mEndSelectionIndex = kNothingSelected; +} + +dom::HTMLOptionsCollection* nsListControlFrame::GetOptions() const { + return Select().Options(); +} + +dom::HTMLOptionElement* nsListControlFrame::GetOption(uint32_t aIndex) const { + return Select().Item(aIndex); +} + +NS_IMETHODIMP +nsListControlFrame::OnOptionSelected(int32_t aIndex, bool aSelected) { + if (aSelected) { + ScrollToIndex(aIndex); + } + return NS_OK; +} + +void nsListControlFrame::OnContentReset() { ResetList(true); } + +void nsListControlFrame::ResetList(bool aAllowScrolling) { + // if all the frames aren't here + // don't bother reseting + if (!mIsAllFramesHere) { + return; + } + + if (aAllowScrolling) { + mPostChildrenLoadedReset = true; + + // Scroll to the selected index + int32_t indexToSelect = kNothingSelected; + + HTMLSelectElement* selectElement = HTMLSelectElement::FromNode(mContent); + if (selectElement) { + indexToSelect = selectElement->SelectedIndex(); + AutoWeakFrame weakFrame(this); + ScrollToIndex(indexToSelect); + if (!weakFrame.IsAlive()) { + return; + } + } + } + + mStartSelectionIndex = kNothingSelected; + mEndSelectionIndex = kNothingSelected; + InvalidateFocus(); + // Combobox will redisplay itself with the OnOptionSelected event +} + +void nsListControlFrame::SetFocus(bool aOn, bool aRepaint) { + InvalidateFocus(); + + if (aOn) { + mFocused = this; + } else { + mFocused = nullptr; + } + + InvalidateFocus(); +} + +void nsListControlFrame::GetOptionText(uint32_t aIndex, nsAString& aStr) { + aStr.Truncate(); + if (dom::HTMLOptionElement* optionElement = GetOption(aIndex)) { + optionElement->GetRenderedLabel(aStr); + } +} + +int32_t nsListControlFrame::GetSelectedIndex() { + dom::HTMLSelectElement* select = + dom::HTMLSelectElement::FromNodeOrNull(mContent); + return select->SelectedIndex(); +} + +uint32_t nsListControlFrame::GetNumberOfOptions() { + dom::HTMLOptionsCollection* options = GetOptions(); + if (!options) { + return 0; + } + + return options->Length(); +} + +//---------------------------------------------------------------------- +// nsISelectControlFrame +//---------------------------------------------------------------------- +bool nsListControlFrame::CheckIfAllFramesHere() { + // XXX Need to find a fail proof way to determine that + // all the frames are there + mIsAllFramesHere = true; + + // now make sure we have a frame each piece of content + + return mIsAllFramesHere; +} + +NS_IMETHODIMP +nsListControlFrame::DoneAddingChildren(bool aIsDone) { + mIsAllContentHere = aIsDone; + if (mIsAllContentHere) { + // Here we check to see if all the frames have been created + // for all the content. + // If so, then we can initialize; + if (!mIsAllFramesHere) { + // if all the frames are now present we can initialize + if (CheckIfAllFramesHere()) { + mHasBeenInitialized = true; + ResetList(true); + } + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsListControlFrame::AddOption(int32_t aIndex) { +#ifdef DO_REFLOW_DEBUG + printf("---- Id: %d nsLCF %p Added Option %d\n", mReflowId, this, aIndex); +#endif + + if (!mIsAllContentHere) { + mIsAllContentHere = mContent->IsDoneAddingChildren(); + if (!mIsAllContentHere) { + mIsAllFramesHere = false; + mHasBeenInitialized = false; + } else { + mIsAllFramesHere = + (aIndex == static_cast<int32_t>(GetNumberOfOptions() - 1)); + } + } + + // Make sure we scroll to the selected option as needed + mNeedToReset = true; + + if (!mHasBeenInitialized) { + return NS_OK; + } + + mPostChildrenLoadedReset = mIsAllContentHere; + return NS_OK; +} + +static int32_t DecrementAndClamp(int32_t aSelectionIndex, int32_t aLength) { + return aLength == 0 ? nsListControlFrame::kNothingSelected + : std::max(0, aSelectionIndex - 1); +} + +NS_IMETHODIMP +nsListControlFrame::RemoveOption(int32_t aIndex) { + MOZ_ASSERT(aIndex >= 0, "negative <option> index"); + + // Need to reset if we're a dropdown + if (mStartSelectionIndex != kNothingSelected) { + NS_ASSERTION(mEndSelectionIndex != kNothingSelected, ""); + int32_t numOptions = GetNumberOfOptions(); + // NOTE: numOptions is the new number of options whereas aIndex is the + // unadjusted index of the removed option (hence the <= below). + NS_ASSERTION(aIndex <= numOptions, "out-of-bounds <option> index"); + + int32_t forward = mEndSelectionIndex - mStartSelectionIndex; + int32_t* low = forward >= 0 ? &mStartSelectionIndex : &mEndSelectionIndex; + int32_t* high = forward >= 0 ? &mEndSelectionIndex : &mStartSelectionIndex; + if (aIndex < *low) *low = ::DecrementAndClamp(*low, numOptions); + if (aIndex <= *high) *high = ::DecrementAndClamp(*high, numOptions); + if (forward == 0) *low = *high; + } else + NS_ASSERTION(mEndSelectionIndex == kNothingSelected, ""); + + InvalidateFocus(); + return NS_OK; +} + +//--------------------------------------------------------- +// Set the option selected in the DOM. This method is named +// as it is because it indicates that the frame is the source +// of this event rather than the receiver. +bool nsListControlFrame::SetOptionsSelectedFromFrame(int32_t aStartIndex, + int32_t aEndIndex, + bool aValue, + bool aClearAll) { + using OptionFlag = HTMLSelectElement::OptionFlag; + RefPtr<HTMLSelectElement> selectElement = + HTMLSelectElement::FromNode(mContent); + + HTMLSelectElement::OptionFlags mask = OptionFlag::Notify; + if (mForceSelection) { + mask += OptionFlag::SetDisabled; + } + if (aValue) { + mask += OptionFlag::IsSelected; + } + if (aClearAll) { + mask += OptionFlag::ClearAll; + } + + return selectElement->SetOptionsSelectedByIndex(aStartIndex, aEndIndex, mask); +} + +bool nsListControlFrame::ToggleOptionSelectedFromFrame(int32_t aIndex) { + RefPtr<HTMLOptionElement> option = GetOption(static_cast<uint32_t>(aIndex)); + NS_ENSURE_TRUE(option, false); + + RefPtr<HTMLSelectElement> selectElement = + HTMLSelectElement::FromNode(mContent); + + HTMLSelectElement::OptionFlags mask = HTMLSelectElement::OptionFlag::Notify; + if (!option->Selected()) { + mask += HTMLSelectElement::OptionFlag::IsSelected; + } + + return selectElement->SetOptionsSelectedByIndex(aIndex, aIndex, mask); +} + +// Dispatch event and such +bool nsListControlFrame::UpdateSelection() { + if (mIsAllFramesHere) { + // if it's a combobox, display the new text. Note that after + // FireOnInputAndOnChange we might be dead, as that can run script. + AutoWeakFrame weakFrame(this); + if (mIsAllContentHere) { + RefPtr listener = mEventListener; + listener->FireOnInputAndOnChange(); + } + return weakFrame.IsAlive(); + } + return true; +} + +NS_IMETHODIMP_(void) +nsListControlFrame::OnSetSelectedIndex(int32_t aOldIndex, int32_t aNewIndex) { +#ifdef ACCESSIBILITY + nsCOMPtr<nsIContent> prevOption = GetCurrentOption(); +#endif + + AutoWeakFrame weakFrame(this); + ScrollToIndex(aNewIndex); + if (!weakFrame.IsAlive()) { + return; + } + mStartSelectionIndex = aNewIndex; + mEndSelectionIndex = aNewIndex; + InvalidateFocus(); + +#ifdef ACCESSIBILITY + if (aOldIndex != aNewIndex) { + FireMenuItemActiveEvent(prevOption); + } +#endif +} + +//---------------------------------------------------------------------- +// End nsISelectControlFrame +//---------------------------------------------------------------------- + +class AsyncReset final : public Runnable { + public: + AsyncReset(nsListControlFrame* aFrame, bool aScroll) + : Runnable("AsyncReset"), mFrame(aFrame), mScroll(aScroll) {} + + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override { + if (mFrame.IsAlive()) { + static_cast<nsListControlFrame*>(mFrame.GetFrame())->ResetList(mScroll); + } + return NS_OK; + } + + private: + WeakFrame mFrame; + bool mScroll; +}; + +nsresult nsListControlFrame::SetFormProperty(nsAtom* aName, + const nsAString& aValue) { + if (nsGkAtoms::selected == aName) { + return NS_ERROR_INVALID_ARG; // Selected is readonly according to spec. + } else if (nsGkAtoms::selectedindex == aName) { + // You shouldn't be calling me for this!!! + return NS_ERROR_INVALID_ARG; + } + + // We should be told about selectedIndex by the DOM element through + // OnOptionSelected + + return NS_OK; +} + +void nsListControlFrame::DidReflow(nsPresContext* aPresContext, + const ReflowInput* aReflowInput) { + bool wasInterrupted = !mHasPendingInterruptAtStartOfReflow && + aPresContext->HasPendingInterrupt(); + + nsHTMLScrollFrame::DidReflow(aPresContext, aReflowInput); + + if (mNeedToReset && !wasInterrupted) { + mNeedToReset = false; + // Suppress scrolling to the selected element if we restored scroll + // history state AND the list contents have not changed since we loaded + // all the children AND nothing else forced us to scroll by calling + // ResetList(true). The latter two conditions are folded into + // mPostChildrenLoadedReset. + // + // The idea is that we want scroll history restoration to trump ResetList + // scrolling to the selected element, when the ResetList was probably only + // caused by content loading normally. + const bool scroll = !DidHistoryRestore() || mPostChildrenLoadedReset; + nsContentUtils::AddScriptRunner(new AsyncReset(this, scroll)); + } + + mHasPendingInterruptAtStartOfReflow = false; +} + +#ifdef DEBUG_FRAME_DUMP +nsresult nsListControlFrame::GetFrameName(nsAString& aResult) const { + return MakeFrameName(u"ListControl"_ns, aResult); +} +#endif + +nscoord nsListControlFrame::GetBSizeOfARow() { return BSizeOfARow(); } + +bool nsListControlFrame::IsOptionInteractivelySelectable(int32_t aIndex) const { + auto& select = Select(); + if (HTMLOptionElement* item = select.Item(aIndex)) { + return IsOptionInteractivelySelectable(&select, item); + } + return false; +} + +bool nsListControlFrame::IsOptionInteractivelySelectable( + HTMLSelectElement* aSelect, HTMLOptionElement* aOption) { + return !aSelect->IsOptionDisabled(aOption) && aOption->GetPrimaryFrame(); +} + +nscoord nsListControlFrame::CalcFallbackRowBSize(float aFontSizeInflation) { + RefPtr<nsFontMetrics> fontMet = + nsLayoutUtils::GetFontMetricsForFrame(this, aFontSizeInflation); + return fontMet->MaxHeight(); +} + +nscoord nsListControlFrame::CalcIntrinsicBSize(nscoord aBSizeOfARow, + int32_t aNumberOfOptions) { + mNumDisplayRows = Select().Size(); + if (mNumDisplayRows < 1) { + mNumDisplayRows = 4; + } + return mNumDisplayRows * aBSizeOfARow; +} + +#ifdef ACCESSIBILITY +void nsListControlFrame::FireMenuItemActiveEvent(nsIContent* aPreviousOption) { + if (mFocused != this) { + return; + } + + nsIContent* optionContent = GetCurrentOption(); + if (aPreviousOption == optionContent) { + // No change + return; + } + + if (aPreviousOption) { + FireDOMEvent(u"DOMMenuItemInactive"_ns, aPreviousOption); + } + + if (optionContent) { + FireDOMEvent(u"DOMMenuItemActive"_ns, optionContent); + } +} +#endif + +nsresult nsListControlFrame::GetIndexFromDOMEvent(dom::Event* aMouseEvent, + int32_t& aCurIndex) { + if (PresShell::GetCapturingContent() != mContent) { + // If we're not capturing, then ignore movement in the border + nsPoint pt = + nsLayoutUtils::GetDOMEventCoordinatesRelativeTo(aMouseEvent, this); + nsRect borderInnerEdge = GetScrollPortRect(); + if (!borderInnerEdge.Contains(pt)) { + return NS_ERROR_FAILURE; + } + } + + RefPtr<dom::HTMLOptionElement> option; + for (nsCOMPtr<nsIContent> content = + PresContext()->EventStateManager()->GetEventTargetContent(nullptr); + content && !option; content = content->GetParent()) { + option = dom::HTMLOptionElement::FromNode(content); + } + + if (option) { + aCurIndex = option->Index(); + MOZ_ASSERT(aCurIndex >= 0); + return NS_OK; + } + + return NS_ERROR_FAILURE; +} + +nsresult nsListControlFrame::HandleLeftButtonMouseDown( + dom::Event* aMouseEvent) { + int32_t selectedIndex; + if (NS_SUCCEEDED(GetIndexFromDOMEvent(aMouseEvent, selectedIndex))) { + // Handle Like List + CaptureMouseEvents(true); + AutoWeakFrame weakFrame(this); + bool change = + HandleListSelection(aMouseEvent, selectedIndex); // might destroy us + if (!weakFrame.IsAlive()) { + return NS_OK; + } + mChangesSinceDragStart = change; + } + return NS_OK; +} + +nsresult nsListControlFrame::HandleLeftButtonMouseUp(dom::Event* aMouseEvent) { + if (!StyleVisibility()->IsVisible()) { + return NS_OK; + } + // Notify + if (mChangesSinceDragStart) { + // reset this so that future MouseUps without a prior MouseDown + // won't fire onchange + mChangesSinceDragStart = false; + RefPtr listener = mEventListener; + listener->FireOnInputAndOnChange(); + // Note that `this` may be dead now, as the above call runs script. + } + return NS_OK; +} + +nsresult nsListControlFrame::DragMove(dom::Event* aMouseEvent) { + NS_ASSERTION(aMouseEvent, "aMouseEvent is null."); + + int32_t selectedIndex; + if (NS_SUCCEEDED(GetIndexFromDOMEvent(aMouseEvent, selectedIndex))) { + // Don't waste cycles if we already dragged over this item + if (selectedIndex == mEndSelectionIndex) { + return NS_OK; + } + MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent(); + NS_ASSERTION(mouseEvent, "aMouseEvent is not a MouseEvent!"); + bool isControl; +#ifdef XP_MACOSX + isControl = mouseEvent->MetaKey(); +#else + isControl = mouseEvent->CtrlKey(); +#endif + AutoWeakFrame weakFrame(this); + // Turn SHIFT on when you are dragging, unless control is on. + bool wasChanged = PerformSelection(selectedIndex, !isControl, isControl); + if (!weakFrame.IsAlive()) { + return NS_OK; + } + mChangesSinceDragStart = mChangesSinceDragStart || wasChanged; + } + return NS_OK; +} + +//---------------------------------------------------------------------- +// Scroll helpers. +//---------------------------------------------------------------------- +void nsListControlFrame::ScrollToIndex(int32_t aIndex) { + if (aIndex < 0) { + // XXX shouldn't we just do nothing if we're asked to scroll to + // kNothingSelected? + ScrollTo(nsPoint(0, 0), ScrollMode::Instant); + } else { + RefPtr<dom::HTMLOptionElement> option = + GetOption(AssertedCast<uint32_t>(aIndex)); + if (option) { + ScrollToFrame(*option); + } + } +} + +void nsListControlFrame::ScrollToFrame(dom::HTMLOptionElement& aOptElement) { + // otherwise we find the content's frame and scroll to it + if (nsIFrame* childFrame = aOptElement.GetPrimaryFrame()) { + RefPtr<mozilla::PresShell> presShell = PresShell(); + presShell->ScrollFrameIntoView(childFrame, Nothing(), ScrollAxis(), + ScrollAxis(), + ScrollFlags::ScrollOverflowHidden | + ScrollFlags::ScrollFirstAncestorOnly); + } +} + +void nsListControlFrame::UpdateSelectionAfterKeyEvent( + int32_t aNewIndex, uint32_t aCharCode, bool aIsShift, bool aIsControlOrMeta, + bool aIsControlSelectMode) { + // If you hold control, but not shift, no key will actually do anything + // except space. + AutoWeakFrame weakFrame(this); + bool wasChanged = false; + if (aIsControlOrMeta && !aIsShift && aCharCode != ' ') { +#ifdef ACCESSIBILITY + nsCOMPtr<nsIContent> prevOption = GetCurrentOption(); +#endif + mStartSelectionIndex = aNewIndex; + mEndSelectionIndex = aNewIndex; + InvalidateFocus(); + ScrollToIndex(aNewIndex); + if (!weakFrame.IsAlive()) { + return; + } + +#ifdef ACCESSIBILITY + FireMenuItemActiveEvent(prevOption); +#endif + } else if (aIsControlSelectMode && aCharCode == ' ') { + wasChanged = SingleSelection(aNewIndex, true); + } else { + wasChanged = PerformSelection(aNewIndex, aIsShift, aIsControlOrMeta); + } + if (wasChanged && weakFrame.IsAlive()) { + // dispatch event, update combobox, etc. + UpdateSelection(); + } +} diff --git a/layout/forms/nsListControlFrame.h b/layout/forms/nsListControlFrame.h new file mode 100644 index 0000000000..41d44e03d1 --- /dev/null +++ b/layout/forms/nsListControlFrame.h @@ -0,0 +1,356 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef nsListControlFrame_h___ +#define nsListControlFrame_h___ + +#ifdef DEBUG_evaughan +// #define DEBUG_rods +#endif + +#ifdef DEBUG_rods +// #define DO_REFLOW_DEBUG +// #define DO_REFLOW_COUNTER +// #define DO_UNCONSTRAINED_CHECK +// #define DO_PIXELS +#endif + +#include "mozilla/Attributes.h" +#include "mozilla/StaticPtr.h" +#include "nsGfxScrollFrame.h" +#include "nsIFormControlFrame.h" +#include "nsISelectControlFrame.h" +#include "nsSelectsAreaFrame.h" + +class nsComboboxControlFrame; +class nsPresContext; + +namespace mozilla { +class PresShell; +class HTMLSelectEventListener; + +namespace dom { +class Event; +class HTMLOptionElement; +class HTMLSelectElement; +class HTMLOptionsCollection; +} // namespace dom +} // namespace mozilla + +/** + * Frame-based listbox. + */ + +class nsListControlFrame final : public nsHTMLScrollFrame, + public nsIFormControlFrame, + public nsISelectControlFrame { + public: + typedef mozilla::dom::HTMLOptionElement HTMLOptionElement; + + friend nsListControlFrame* NS_NewListControlFrame( + mozilla::PresShell* aPresShell, ComputedStyle* aStyle); + + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsListControlFrame) + + Maybe<nscoord> GetNaturalBaselineBOffset( + mozilla::WritingMode aWM, BaselineSharingGroup aBaselineGroup, + BaselineExportContext) const override; + + // nsIFrame + nsresult HandleEvent(nsPresContext* aPresContext, + mozilla::WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) final; + + void SetInitialChildList(ChildListID aListID, nsFrameList&& aChildList) final; + + nscoord GetPrefISize(gfxContext* aRenderingContext) final; + nscoord GetMinISize(gfxContext* aRenderingContext) final; + + void Reflow(nsPresContext* aCX, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, nsReflowStatus& aStatus) final; + + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) final; + + void DidReflow(nsPresContext* aPresContext, + const ReflowInput* aReflowInput) final; + void Destroy(DestroyContext&) override; + + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) final; + + nsContainerFrame* GetContentInsertionFrame() final; + + int32_t GetEndSelectionIndex() const { return mEndSelectionIndex; } + + mozilla::dom::HTMLOptionElement* GetCurrentOption() const; + + bool IsFrameOfType(uint32_t aFlags) const final { + return nsHTMLScrollFrame::IsFrameOfType( + aFlags & ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock)); + } + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const final; +#endif + + // nsIFormControlFrame + nsresult SetFormProperty(nsAtom* aName, const nsAString& aValue) final; + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void SetFocus(bool aOn = true, bool aRepaint = false) final; + + bool ShouldPropagateComputedBSizeToScrolledContent() const final; + + // for accessibility purposes +#ifdef ACCESSIBILITY + mozilla::a11y::AccType AccessibleType() final; +#endif + + int32_t GetSelectedIndex(); + + /** + * Gets the text of the currently selected item. + * If the there are zero items then an empty string is returned + * If there is nothing selected, then the 0th item's text is returned. + */ + void GetOptionText(uint32_t aIndex, nsAString& aStr); + + void CaptureMouseEvents(bool aGrabMouseEvents); + nscoord GetBSizeOfARow(); + uint32_t GetNumberOfOptions(); + + MOZ_CAN_RUN_SCRIPT_BOUNDARY void OnContentReset(); + + // nsISelectControlFrame + NS_IMETHOD AddOption(int32_t index) final; + NS_IMETHOD RemoveOption(int32_t index) final; + MOZ_CAN_RUN_SCRIPT_BOUNDARY + NS_IMETHOD DoneAddingChildren(bool aIsDone) final; + + /** + * Gets the content (an option) by index and then set it as + * being selected or not selected. + */ + MOZ_CAN_RUN_SCRIPT_BOUNDARY + NS_IMETHOD OnOptionSelected(int32_t aIndex, bool aSelected) final; + MOZ_CAN_RUN_SCRIPT_BOUNDARY + NS_IMETHOD_(void) + OnSetSelectedIndex(int32_t aOldIndex, int32_t aNewIndex) final; + + /** + * Mouse event listeners. + * @note These methods might destroy the frame, pres shell and other objects. + */ + MOZ_CAN_RUN_SCRIPT + nsresult HandleLeftButtonMouseDown(mozilla::dom::Event* aMouseEvent); + MOZ_CAN_RUN_SCRIPT + nsresult HandleLeftButtonMouseUp(mozilla::dom::Event* aMouseEvent); + MOZ_CAN_RUN_SCRIPT + nsresult DragMove(mozilla::dom::Event* aMouseEvent); + MOZ_CAN_RUN_SCRIPT + + MOZ_CAN_RUN_SCRIPT + bool PerformSelection(int32_t aClickedIndex, bool aIsShift, bool aIsControl); + MOZ_CAN_RUN_SCRIPT + void UpdateSelectionAfterKeyEvent(int32_t aNewIndex, uint32_t aCharCode, + bool aIsShift, bool aIsControlOrMeta, + bool aIsControlSelectMode); + + /** + * Returns the options collection for mContent, if any. + */ + mozilla::dom::HTMLOptionsCollection* GetOptions() const; + /** + * Returns the HTMLOptionElement for a given index in mContent's collection. + */ + HTMLOptionElement* GetOption(uint32_t aIndex) const; + + // Helper + bool IsFocused() { return this == mFocused; } + + /** + * Function to paint the focus rect when our nsSelectsAreaFrame is painting. + * @param aPt the offset of this frame, relative to the rendering reference + * frame + */ + void PaintFocus(mozilla::gfx::DrawTarget* aDrawTarget, nsPoint aPt); + + /** + * If this frame IsFocused(), invalidates an area that includes anything + * that PaintFocus will or could have painted --- basically the whole + * GetOptionsContainer, plus some extra stuff if there are no options. This + * must be called every time mEndSelectionIndex changes. + */ + void InvalidateFocus(); + + /** + * Function to calculate the block size of a row, for use with the + * "size" attribute. + * Can't be const because GetNumberOfOptions() isn't const. + */ + nscoord CalcBSizeOfARow(); + + /** + * Function to ask whether we're currently in what might be the + * first pass of a two-pass reflow. + */ + bool MightNeedSecondPass() const { return mMightNeedSecondPass; } + + void SetSuppressScrollbarUpdate(bool aSuppress) { + nsHTMLScrollFrame::SetSuppressScrollbarUpdate(aSuppress); + } + + /** + * Return the number of displayed rows in the list. + */ + uint32_t GetNumDisplayRows() const { return mNumDisplayRows; } + +#ifdef ACCESSIBILITY + /** + * Post a custom DOM event for the change, so that accessibility can + * fire a native focus event for accessibility + * (Some 3rd party products need to track our focus) + */ + void FireMenuItemActiveEvent( + nsIContent* aPreviousOption); // Inform assistive tech what got focused +#endif + + protected: + /** + * Updates the selected text in a combobox and then calls FireOnChange(). + * @note This method might destroy the frame, pres shell and other objects. + * Returns false if calling it destroyed |this|. + */ + MOZ_CAN_RUN_SCRIPT + bool UpdateSelection(); + + /** + * Returns whether mContent supports multiple selection. + */ + bool GetMultiple() const { + return mContent->AsElement()->HasAttr(kNameSpaceID_None, + nsGkAtoms::multiple); + } + + mozilla::dom::HTMLSelectElement& Select() const; + + /** + * @return true if the <option> at aIndex is selectable by the user. + */ + bool IsOptionInteractivelySelectable(int32_t aIndex) const; + /** + * @return true if aOption in aSelect is selectable by the user. + */ + static bool IsOptionInteractivelySelectable( + mozilla::dom::HTMLSelectElement* aSelect, + mozilla::dom::HTMLOptionElement* aOption); + + MOZ_CAN_RUN_SCRIPT void ScrollToFrame(HTMLOptionElement& aOptElement); + + MOZ_CAN_RUN_SCRIPT void ScrollToIndex(int32_t anIndex); + + public: + /** + * Resets the select back to it's original default values; + * those values as determined by the original HTML + */ + MOZ_CAN_RUN_SCRIPT void ResetList(bool aAllowScrolling); + + protected: + explicit nsListControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext); + virtual ~nsListControlFrame(); + + /** + * Sets the mSelectedIndex and mOldSelectedIndex from figuring out what + * item was selected using content + * @param aPoint the event point, in listcontrolframe coordinates + * @return NS_OK if it successfully found the selection + */ + nsresult GetIndexFromDOMEvent(mozilla::dom::Event* aMouseEvent, + int32_t& aCurIndex); + + bool CheckIfAllFramesHere(); + + // guess at a row block size based on our own style. + nscoord CalcFallbackRowBSize(float aFontSizeInflation); + + // CalcIntrinsicBSize computes our intrinsic block size (taking the + // "size" attribute into account). This should only be called in + // non-dropdown mode. + nscoord CalcIntrinsicBSize(nscoord aBSizeOfARow, int32_t aNumberOfOptions); + + // Dropped down stuff + void SetComboboxItem(int32_t aIndex); + + // Selection + bool SetOptionsSelectedFromFrame(int32_t aStartIndex, int32_t aEndIndex, + bool aValue, bool aClearAll); + bool ToggleOptionSelectedFromFrame(int32_t aIndex); + + MOZ_CAN_RUN_SCRIPT + bool SingleSelection(int32_t aClickedIndex, bool aDoToggle); + bool ExtendedSelection(int32_t aStartIndex, int32_t aEndIndex, + bool aClearAll); + MOZ_CAN_RUN_SCRIPT + bool HandleListSelection(mozilla::dom::Event* aDOMEvent, + int32_t selectedIndex); + void InitSelectionRange(int32_t aClickedIndex); + + public: + nsSelectsAreaFrame* GetOptionsContainer() const { + return static_cast<nsSelectsAreaFrame*>(GetScrolledFrame()); + } + + static constexpr int32_t kNothingSelected = -1; + + protected: + nscoord BSizeOfARow() { return GetOptionsContainer()->BSizeOfARow(); } + + /** + * @return how many displayable options/optgroups this frame has. + */ + uint32_t GetNumberOfRows(); + + // Data Members + int32_t mStartSelectionIndex; + int32_t mEndSelectionIndex; + + uint32_t mNumDisplayRows; + bool mChangesSinceDragStart : 1; + + // Has the user selected a visible item since we showed the dropdown? + bool mItemSelectionStarted : 1; + + bool mIsAllContentHere : 1; + bool mIsAllFramesHere : 1; + bool mHasBeenInitialized : 1; + bool mNeedToReset : 1; + bool mPostChildrenLoadedReset : 1; + + // True if we're in the middle of a reflow and might need a second + // pass. This only happens for auto heights. + bool mMightNeedSecondPass : 1; + + /** + * Set to aPresContext->HasPendingInterrupt() at the start of Reflow. + * Set to false at the end of DidReflow. + */ + bool mHasPendingInterruptAtStartOfReflow : 1; + + // True if the selection can be set to nothing or disabled options. + bool mForceSelection : 1; + + RefPtr<mozilla::HTMLSelectEventListener> mEventListener; + + static nsListControlFrame* mFocused; + +#ifdef DO_REFLOW_COUNTER + int32_t mReflowId; +#endif +}; + +#endif /* nsListControlFrame_h___ */ diff --git a/layout/forms/nsMeterFrame.cpp b/layout/forms/nsMeterFrame.cpp new file mode 100644 index 0000000000..76e569226c --- /dev/null +++ b/layout/forms/nsMeterFrame.cpp @@ -0,0 +1,222 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMeterFrame.h" + +#include "mozilla/PresShell.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLMeterElement.h" +#include "nsIContent.h" +#include "nsLayoutUtils.h" +#include "nsPresContext.h" +#include "nsGkAtoms.h" +#include "nsNameSpaceManager.h" +#include "nsNodeInfoManager.h" +#include "nsContentCreatorFunctions.h" +#include "nsFontMetrics.h" +#include "nsCSSPseudoElements.h" +#include "nsStyleConsts.h" +#include <algorithm> + +using namespace mozilla; +using mozilla::dom::Document; +using mozilla::dom::Element; +using mozilla::dom::HTMLMeterElement; + +nsIFrame* NS_NewMeterFrame(PresShell* aPresShell, ComputedStyle* aStyle) { + return new (aPresShell) nsMeterFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsMeterFrame) + +nsMeterFrame::nsMeterFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) + : nsContainerFrame(aStyle, aPresContext, kClassID), mBarDiv(nullptr) {} + +nsMeterFrame::~nsMeterFrame() = default; + +void nsMeterFrame::Destroy(DestroyContext& aContext) { + NS_ASSERTION(!GetPrevContinuation(), + "nsMeterFrame should not have continuations; if it does we " + "need to call RegUnregAccessKey only for the first."); + aContext.AddAnonymousContent(mBarDiv.forget()); + nsContainerFrame::Destroy(aContext); +} + +nsresult nsMeterFrame::CreateAnonymousContent( + nsTArray<ContentInfo>& aElements) { + // Get the NodeInfoManager and tag necessary to create the meter bar div. + nsCOMPtr<Document> doc = mContent->GetComposedDoc(); + + // Create the div. + mBarDiv = doc->CreateHTMLElement(nsGkAtoms::div); + + // Associate ::-moz-meter-bar pseudo-element to the anonymous child. + mBarDiv->SetPseudoElementType(PseudoStyleType::mozMeterBar); + + aElements.AppendElement(mBarDiv); + + return NS_OK; +} + +void nsMeterFrame::AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) { + if (mBarDiv) { + aElements.AppendElement(mBarDiv); + } +} + +NS_QUERYFRAME_HEAD(nsMeterFrame) + NS_QUERYFRAME_ENTRY(nsMeterFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) +NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) + +void nsMeterFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MarkInReflow(); + DO_GLOBAL_REFLOW_COUNT("nsMeterFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + + NS_ASSERTION(mBarDiv, "Meter bar div must exist!"); + NS_ASSERTION(!GetPrevContinuation(), + "nsMeterFrame should not have continuations; if it does we " + "need to call RegUnregAccessKey only for the first."); + + nsIFrame* barFrame = mBarDiv->GetPrimaryFrame(); + NS_ASSERTION(barFrame, "The meter frame should have a child with a frame!"); + + ReflowBarFrame(barFrame, aPresContext, aReflowInput, aStatus); + + const auto wm = aReflowInput.GetWritingMode(); + aDesiredSize.SetSize(wm, aReflowInput.ComputedSizeWithBorderPadding(wm)); + + aDesiredSize.SetOverflowAreasToDesiredBounds(); + ConsiderChildOverflow(aDesiredSize.mOverflowAreas, barFrame); + FinishAndStoreOverflow(&aDesiredSize); + + aStatus.Reset(); // This type of frame can't be split. +} + +void nsMeterFrame::ReflowBarFrame(nsIFrame* aBarFrame, + nsPresContext* aPresContext, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + bool vertical = ResolvedOrientationIsVertical(); + WritingMode wm = aBarFrame->GetWritingMode(); + LogicalSize availSize = aReflowInput.ComputedSize(wm); + availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE; + ReflowInput reflowInput(aPresContext, aReflowInput, aBarFrame, availSize); + nscoord size = + vertical ? aReflowInput.ComputedHeight() : aReflowInput.ComputedWidth(); + nscoord xoffset = aReflowInput.ComputedPhysicalBorderPadding().left; + nscoord yoffset = aReflowInput.ComputedPhysicalBorderPadding().top; + + auto* meterElement = static_cast<HTMLMeterElement*>(GetContent()); + size = NSToCoordRound(size * meterElement->Position()); + + if (!vertical && wm.IsPhysicalRTL()) { + xoffset += aReflowInput.ComputedWidth() - size; + } + + // The bar position is *always* constrained. + if (vertical) { + // We want the bar to begin at the bottom. + yoffset += aReflowInput.ComputedHeight() - size; + + size -= reflowInput.ComputedPhysicalMargin().TopBottom() + + reflowInput.ComputedPhysicalBorderPadding().TopBottom(); + size = std::max(size, 0); + reflowInput.SetComputedHeight(size); + } else { + size -= reflowInput.ComputedPhysicalMargin().LeftRight() + + reflowInput.ComputedPhysicalBorderPadding().LeftRight(); + size = std::max(size, 0); + reflowInput.SetComputedWidth(size); + } + + xoffset += reflowInput.ComputedPhysicalMargin().left; + yoffset += reflowInput.ComputedPhysicalMargin().top; + + ReflowOutput barDesiredSize(reflowInput); + ReflowChild(aBarFrame, aPresContext, barDesiredSize, reflowInput, xoffset, + yoffset, ReflowChildFlags::Default, aStatus); + FinishReflowChild(aBarFrame, aPresContext, barDesiredSize, &reflowInput, + xoffset, yoffset, ReflowChildFlags::Default); +} + +nsresult nsMeterFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, int32_t aModType) { + NS_ASSERTION(mBarDiv, "Meter bar div must exist!"); + + if (aNameSpaceID == kNameSpaceID_None && + (aAttribute == nsGkAtoms::value || aAttribute == nsGkAtoms::max || + aAttribute == nsGkAtoms::min)) { + nsIFrame* barFrame = mBarDiv->GetPrimaryFrame(); + NS_ASSERTION(barFrame, "The meter frame should have a child with a frame!"); + PresShell()->FrameNeedsReflow(barFrame, IntrinsicDirty::None, + NS_FRAME_IS_DIRTY); + InvalidateFrame(); + } + + return nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType); +} + +LogicalSize nsMeterFrame::ComputeAutoSize( + gfxContext* aRenderingContext, WritingMode aWM, const LogicalSize& aCBSize, + nscoord aAvailableISize, const LogicalSize& aMargin, + const LogicalSize& aBorderPadding, const StyleSizeOverrides& aSizeOverrides, + ComputeSizeFlags aFlags) { + RefPtr<nsFontMetrics> fontMet = + nsLayoutUtils::GetFontMetricsForFrame(this, 1.0f); + + const WritingMode wm = GetWritingMode(); + LogicalSize autoSize(wm); + autoSize.BSize(wm) = autoSize.ISize(wm) = + fontMet->Font().size.ToAppUnits(); // 1em + + if (ResolvedOrientationIsVertical() == wm.IsVertical()) { + autoSize.ISize(wm) *= 5; // 5em + } else { + autoSize.BSize(wm) *= 5; // 5em + } + + return autoSize.ConvertTo(aWM, wm); +} + +nscoord nsMeterFrame::GetMinISize(gfxContext* aRenderingContext) { + RefPtr<nsFontMetrics> fontMet = + nsLayoutUtils::GetFontMetricsForFrame(this, 1.0f); + + nscoord minISize = fontMet->Font().size.ToAppUnits(); // 1em + + if (ResolvedOrientationIsVertical() == GetWritingMode().IsVertical()) { + // The orientation is inline + minISize *= 5; // 5em + } + + return minISize; +} + +nscoord nsMeterFrame::GetPrefISize(gfxContext* aRenderingContext) { + return GetMinISize(aRenderingContext); +} + +bool nsMeterFrame::ShouldUseNativeStyle() const { + nsIFrame* barFrame = mBarDiv->GetPrimaryFrame(); + + // Use the native style if these conditions are satisfied: + // - both frames use the native appearance; + // - neither frame has author specified rules setting the border or the + // background. + return StyleDisplay()->EffectiveAppearance() == StyleAppearance::Meter && + !Style()->HasAuthorSpecifiedBorderOrBackground() && barFrame && + barFrame->StyleDisplay()->EffectiveAppearance() == + StyleAppearance::Meterchunk && + !barFrame->Style()->HasAuthorSpecifiedBorderOrBackground(); +} diff --git a/layout/forms/nsMeterFrame.h b/layout/forms/nsMeterFrame.h new file mode 100644 index 0000000000..fb6200a4cd --- /dev/null +++ b/layout/forms/nsMeterFrame.h @@ -0,0 +1,82 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMeterFrame_h___ +#define nsMeterFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsContainerFrame.h" +#include "nsIAnonymousContentCreator.h" +#include "nsCOMPtr.h" +#include "nsCSSPseudoElements.h" + +class nsMeterFrame final : public nsContainerFrame, + public nsIAnonymousContentCreator + +{ + typedef mozilla::dom::Element Element; + + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsMeterFrame) + + explicit nsMeterFrame(ComputedStyle* aStyle, nsPresContext* aPresContext); + virtual ~nsMeterFrame(); + + void Destroy(DestroyContext&) override; + + virtual void Reflow(nsPresContext* aCX, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"Meter"_ns, aResult); + } +#endif + + // nsIAnonymousContentCreator + virtual nsresult CreateAnonymousContent( + nsTArray<ContentInfo>& aElements) override; + virtual void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) override; + + virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + + virtual mozilla::LogicalSize ComputeAutoSize( + gfxContext* aRenderingContext, mozilla::WritingMode aWM, + const mozilla::LogicalSize& aCBSize, nscoord aAvailableISize, + const mozilla::LogicalSize& aMargin, + const mozilla::LogicalSize& aBorderPadding, + const mozilla::StyleSizeOverrides& aSizeOverrides, + mozilla::ComputeSizeFlags aFlags) override; + + virtual nscoord GetMinISize(gfxContext* aRenderingContext) override; + virtual nscoord GetPrefISize(gfxContext* aRenderingContext) override; + + virtual bool IsFrameOfType(uint32_t aFlags) const override { + return nsContainerFrame::IsFrameOfType( + aFlags & ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock)); + } + + /** + * Returns whether the frame and its child should use the native style. + */ + bool ShouldUseNativeStyle() const; + + protected: + // Helper function which reflow the anonymous div frame. + void ReflowBarFrame(nsIFrame* aBarFrame, nsPresContext* aPresContext, + const ReflowInput& aReflowInput, nsReflowStatus& aStatus); + /** + * The div used to show the meter bar. + * @see nsMeterFrame::CreateAnonymousContent + */ + nsCOMPtr<Element> mBarDiv; +}; + +#endif diff --git a/layout/forms/nsNumberControlFrame.cpp b/layout/forms/nsNumberControlFrame.cpp new file mode 100644 index 0000000000..e8ed87c8f3 --- /dev/null +++ b/layout/forms/nsNumberControlFrame.cpp @@ -0,0 +1,175 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsNumberControlFrame.h" + +#include "mozilla/BasicEvents.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/PresShell.h" +#include "HTMLInputElement.h" +#include "nsGkAtoms.h" +#include "nsNameSpaceManager.h" +#include "nsStyleConsts.h" +#include "nsContentUtils.h" +#include "nsContentCreatorFunctions.h" +#include "nsCSSPseudoElements.h" +#include "nsLayoutUtils.h" + +#ifdef ACCESSIBILITY +# include "mozilla/a11y/AccTypes.h" +#endif + +using namespace mozilla; +using namespace mozilla::dom; + +nsIFrame* NS_NewNumberControlFrame(PresShell* aPresShell, + ComputedStyle* aStyle) { + return new (aPresShell) + nsNumberControlFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsNumberControlFrame) + +NS_QUERYFRAME_HEAD(nsNumberControlFrame) + NS_QUERYFRAME_ENTRY(nsNumberControlFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsTextControlFrame) + +nsNumberControlFrame::nsNumberControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsTextControlFrame(aStyle, aPresContext, kClassID) {} + +void nsNumberControlFrame::Destroy(DestroyContext& aContext) { + aContext.AddAnonymousContent(mSpinBox.forget()); + nsTextControlFrame::Destroy(aContext); +} + +nsresult nsNumberControlFrame::CreateAnonymousContent( + nsTArray<ContentInfo>& aElements) { + // We create an anonymous tree for our input element that is structured as + // follows: + // + // input + // div - placeholder + // div - preview div + // div - editor root + // div - spin box wrapping up/down arrow buttons + // div - spin up (up arrow button) + // div - spin down (down arrow button) + // + // If you change this, be careful to change the order of stuff returned in + // AppendAnonymousContentTo. + + nsTextControlFrame::CreateAnonymousContent(aElements); + + // The author has elected to hide the spinner by setting this + // -moz-appearance. We will reframe if it changes. + if (StyleDisplay()->EffectiveAppearance() != StyleAppearance::Textfield) { + // Create the ::-moz-number-spin-box pseudo-element: + mSpinBox = MakeAnonElement(PseudoStyleType::mozNumberSpinBox); + + // Create the ::-moz-number-spin-up pseudo-element: + mSpinUp = MakeAnonElement(PseudoStyleType::mozNumberSpinUp, mSpinBox); + + // Create the ::-moz-number-spin-down pseudo-element: + mSpinDown = MakeAnonElement(PseudoStyleType::mozNumberSpinDown, mSpinBox); + + aElements.AppendElement(mSpinBox); + } + + return NS_OK; +} + +/* static */ +nsNumberControlFrame* nsNumberControlFrame::GetNumberControlFrameForSpinButton( + nsIFrame* aFrame) { + // If aFrame is a spin button for an <input type=number> then we expect the + // frame of the NAC root parent to be that input's frame. We have to check for + // this via the content tree because we don't know whether extra frames will + // be wrapped around any of the elements between aFrame and the + // nsNumberControlFrame that we're looking for (e.g. flex wrappers). + nsIContent* content = aFrame->GetContent(); + auto* nacHost = content->GetClosestNativeAnonymousSubtreeRootParentOrHost(); + if (!nacHost) { + return nullptr; + } + auto* input = HTMLInputElement::FromNode(nacHost); + if (!input || input->ControlType() != FormControlType::InputNumber) { + return nullptr; + } + return do_QueryFrame(input->GetPrimaryFrame()); +} + +int32_t nsNumberControlFrame::GetSpinButtonForPointerEvent( + WidgetGUIEvent* aEvent) const { + MOZ_ASSERT(aEvent->mClass == eMouseEventClass, "Unexpected event type"); + + if (!mSpinBox) { + // we don't have a spinner + return eSpinButtonNone; + } + if (aEvent->mOriginalTarget == mSpinUp) { + return eSpinButtonUp; + } + if (aEvent->mOriginalTarget == mSpinDown) { + return eSpinButtonDown; + } + if (aEvent->mOriginalTarget == mSpinBox) { + // In the case that the up/down buttons are hidden (display:none) we use + // just the spin box element, spinning up if the pointer is over the top + // half of the element, or down if it's over the bottom half. This is + // important to handle since this is the state things are in for the + // default UA style sheet. See the comment in forms.css for why. + LayoutDeviceIntPoint absPoint = aEvent->mRefPoint; + nsPoint point = nsLayoutUtils::GetEventCoordinatesRelativeTo( + aEvent, absPoint, RelativeTo{mSpinBox->GetPrimaryFrame()}); + if (point != nsPoint(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE)) { + if (point.y < mSpinBox->GetPrimaryFrame()->GetSize().height / 2) { + return eSpinButtonUp; + } + return eSpinButtonDown; + } + } + return eSpinButtonNone; +} + +void nsNumberControlFrame::SpinnerStateChanged() const { + if (mSpinUp) { + nsIFrame* spinUpFrame = mSpinUp->GetPrimaryFrame(); + if (spinUpFrame && spinUpFrame->IsThemed()) { + spinUpFrame->InvalidateFrame(); + } + } + if (mSpinDown) { + nsIFrame* spinDownFrame = mSpinDown->GetPrimaryFrame(); + if (spinDownFrame && spinDownFrame->IsThemed()) { + spinDownFrame->InvalidateFrame(); + } + } +} + +bool nsNumberControlFrame::SpinnerUpButtonIsDepressed() const { + return HTMLInputElement::FromNode(mContent) + ->NumberSpinnerUpButtonIsDepressed(); +} + +bool nsNumberControlFrame::SpinnerDownButtonIsDepressed() const { + return HTMLInputElement::FromNode(mContent) + ->NumberSpinnerDownButtonIsDepressed(); +} + +void nsNumberControlFrame::AppendAnonymousContentTo( + nsTArray<nsIContent*>& aElements, uint32_t aFilter) { + nsTextControlFrame::AppendAnonymousContentTo(aElements, aFilter); + if (mSpinBox) { + aElements.AppendElement(mSpinBox); + } +} + +#ifdef ACCESSIBILITY +a11y::AccType nsNumberControlFrame::AccessibleType() { + return a11y::eHTMLSpinnerType; +} +#endif diff --git a/layout/forms/nsNumberControlFrame.h b/layout/forms/nsNumberControlFrame.h new file mode 100644 index 0000000000..d7201ea1e1 --- /dev/null +++ b/layout/forms/nsNumberControlFrame.h @@ -0,0 +1,105 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsNumberControlFrame_h__ +#define nsNumberControlFrame_h__ + +#include "mozilla/Attributes.h" +#include "nsContainerFrame.h" +#include "nsTextControlFrame.h" +#include "nsIFormControlFrame.h" +#include "nsIAnonymousContentCreator.h" +#include "nsCOMPtr.h" + +class nsITextControlFrame; +class nsPresContext; + +namespace mozilla { +enum class PseudoStyleType : uint8_t; +class PresShell; +class WidgetEvent; +class WidgetGUIEvent; +namespace dom { +class HTMLInputElement; +} // namespace dom +} // namespace mozilla + +/** + * This frame type is used for <input type=number>. + * + * TODO(emilio): Maybe merge with nsTextControlFrame? + */ +class nsNumberControlFrame final : public nsTextControlFrame { + friend nsIFrame* NS_NewNumberControlFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + typedef mozilla::PseudoStyleType PseudoStyleType; + typedef mozilla::dom::Element Element; + typedef mozilla::dom::HTMLInputElement HTMLInputElement; + typedef mozilla::WidgetEvent WidgetEvent; + typedef mozilla::WidgetGUIEvent WidgetGUIEvent; + + explicit nsNumberControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext); + + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsNumberControlFrame) + + void Destroy(DestroyContext&) override; + +#ifdef ACCESSIBILITY + mozilla::a11y::AccType AccessibleType() override; +#endif + + // nsIAnonymousContentCreator + nsresult CreateAnonymousContent(nsTArray<ContentInfo>& aElements) override; + void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) override; + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"NumberControl"_ns, aResult); + } +#endif + + /** + * If the frame is the frame for an nsNumberControlFrame's up or down spin + * button, returns the nsNumberControlFrame. Else returns nullptr. + */ + static nsNumberControlFrame* GetNumberControlFrameForSpinButton( + nsIFrame* aFrame); + + enum SpinButtonEnum { eSpinButtonNone, eSpinButtonUp, eSpinButtonDown }; + + /** + * Returns one of the SpinButtonEnum values to depending on whether the + * pointer event is over the spin-up button, the spin-down button, or + * neither. + */ + int32_t GetSpinButtonForPointerEvent(WidgetGUIEvent* aEvent) const; + + void SpinnerStateChanged() const; + + bool SpinnerUpButtonIsDepressed() const; + bool SpinnerDownButtonIsDepressed() const; + + /** + * Our element had HTMLInputElement::Select() called on it. + */ + void HandleSelectCall(); + + bool ShouldUseNativeStyleForSpinner() const; + + private: + // See nsNumberControlFrame::CreateAnonymousContent for a description of + // these. + nsCOMPtr<Element> mSpinBox; + nsCOMPtr<Element> mSpinUp; + nsCOMPtr<Element> mSpinDown; +}; + +#endif // nsNumberControlFrame_h__ diff --git a/layout/forms/nsProgressFrame.cpp b/layout/forms/nsProgressFrame.cpp new file mode 100644 index 0000000000..e91a7be62c --- /dev/null +++ b/layout/forms/nsProgressFrame.cpp @@ -0,0 +1,248 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsProgressFrame.h" + +#include "mozilla/PresShell.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLProgressElement.h" +#include "nsIContent.h" +#include "nsLayoutUtils.h" +#include "nsPresContext.h" +#include "nsGkAtoms.h" +#include "nsNameSpaceManager.h" +#include "nsNodeInfoManager.h" +#include "nsContentCreatorFunctions.h" +#include "nsFontMetrics.h" +#include "nsCSSPseudoElements.h" +#include "nsStyleConsts.h" +#include <algorithm> + +using namespace mozilla; +using namespace mozilla::dom; + +nsIFrame* NS_NewProgressFrame(PresShell* aPresShell, ComputedStyle* aStyle) { + return new (aPresShell) nsProgressFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsProgressFrame) + +nsProgressFrame::nsProgressFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsContainerFrame(aStyle, aPresContext, kClassID), mBarDiv(nullptr) {} + +nsProgressFrame::~nsProgressFrame() = default; + +void nsProgressFrame::Destroy(DestroyContext& aContext) { + NS_ASSERTION(!GetPrevContinuation(), + "nsProgressFrame should not have continuations; if it does we " + "need to call RegUnregAccessKey only for the first."); + aContext.AddAnonymousContent(mBarDiv.forget()); + nsContainerFrame::Destroy(aContext); +} + +nsresult nsProgressFrame::CreateAnonymousContent( + nsTArray<ContentInfo>& aElements) { + // Create the progress bar div. + nsCOMPtr<Document> doc = mContent->GetComposedDoc(); + mBarDiv = doc->CreateHTMLElement(nsGkAtoms::div); + + // Associate ::-moz-progress-bar pseudo-element to the anonymous child. + mBarDiv->SetPseudoElementType(PseudoStyleType::mozProgressBar); + + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier, or change the return type to void. + aElements.AppendElement(mBarDiv); + + return NS_OK; +} + +void nsProgressFrame::AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) { + if (mBarDiv) { + aElements.AppendElement(mBarDiv); + } +} + +NS_QUERYFRAME_HEAD(nsProgressFrame) + NS_QUERYFRAME_ENTRY(nsProgressFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) +NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) + +void nsProgressFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + BuildDisplayListForInline(aBuilder, aLists); +} + +void nsProgressFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MarkInReflow(); + DO_GLOBAL_REFLOW_COUNT("nsProgressFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + + NS_ASSERTION(mBarDiv, "Progress bar div must exist!"); + NS_ASSERTION( + PrincipalChildList().GetLength() == 1 && + PrincipalChildList().FirstChild() == mBarDiv->GetPrimaryFrame(), + "unexpected child frames"); + NS_ASSERTION(!GetPrevContinuation(), + "nsProgressFrame should not have continuations; if it does we " + "need to call RegUnregAccessKey only for the first."); + + const auto wm = aReflowInput.GetWritingMode(); + aDesiredSize.SetSize(wm, aReflowInput.ComputedSizeWithBorderPadding(wm)); + aDesiredSize.SetOverflowAreasToDesiredBounds(); + + for (auto childFrame : PrincipalChildList()) { + ReflowChildFrame(childFrame, aPresContext, aReflowInput, aStatus); + ConsiderChildOverflow(aDesiredSize.mOverflowAreas, childFrame); + } + + FinishAndStoreOverflow(&aDesiredSize); + + aStatus.Reset(); // This type of frame can't be split. +} + +void nsProgressFrame::ReflowChildFrame(nsIFrame* aChild, + nsPresContext* aPresContext, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + bool vertical = ResolvedOrientationIsVertical(); + WritingMode wm = aChild->GetWritingMode(); + LogicalSize availSize = aReflowInput.ComputedSize(wm); + availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE; + ReflowInput reflowInput(aPresContext, aReflowInput, aChild, availSize); + nscoord size = + vertical ? aReflowInput.ComputedHeight() : aReflowInput.ComputedWidth(); + nscoord xoffset = aReflowInput.ComputedPhysicalBorderPadding().left; + nscoord yoffset = aReflowInput.ComputedPhysicalBorderPadding().top; + + double position = static_cast<HTMLProgressElement*>(GetContent())->Position(); + + // Force the bar's size to match the current progress. + // When indeterminate, the progress' size will be 100%. + if (position >= 0.0) { + size *= position; + } + + if (!vertical && wm.IsPhysicalRTL()) { + xoffset += aReflowInput.ComputedWidth() - size; + } + + // The bar size is fixed in these cases: + // - the progress position is determined: the bar size is fixed according + // to it's value. + // - the progress position is indeterminate and the bar appearance should be + // shown as native: the bar size is forced to 100%. + // Otherwise (when the progress is indeterminate and the bar appearance isn't + // native), the bar size isn't fixed and can be set by the author. + if (position != -1 || ShouldUseNativeStyle()) { + if (vertical) { + // We want the bar to begin at the bottom. + yoffset += aReflowInput.ComputedHeight() - size; + + size -= reflowInput.ComputedPhysicalMargin().TopBottom() + + reflowInput.ComputedPhysicalBorderPadding().TopBottom(); + size = std::max(size, 0); + reflowInput.SetComputedHeight(size); + } else { + size -= reflowInput.ComputedPhysicalMargin().LeftRight() + + reflowInput.ComputedPhysicalBorderPadding().LeftRight(); + size = std::max(size, 0); + reflowInput.SetComputedWidth(size); + } + } else if (vertical) { + // For vertical progress bars, we need to position the bar specificly when + // the width isn't constrained (position == -1 and !ShouldUseNativeStyle()) + // because aReflowInput.ComputedHeight() - size == 0. + yoffset += aReflowInput.ComputedHeight() - reflowInput.ComputedHeight(); + } + + xoffset += reflowInput.ComputedPhysicalMargin().left; + yoffset += reflowInput.ComputedPhysicalMargin().top; + + ReflowOutput barDesiredSize(aReflowInput); + ReflowChild(aChild, aPresContext, barDesiredSize, reflowInput, xoffset, + yoffset, ReflowChildFlags::Default, aStatus); + FinishReflowChild(aChild, aPresContext, barDesiredSize, &reflowInput, xoffset, + yoffset, ReflowChildFlags::Default); +} + +nsresult nsProgressFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType) { + NS_ASSERTION(mBarDiv, "Progress bar div must exist!"); + + if (aNameSpaceID == kNameSpaceID_None && + (aAttribute == nsGkAtoms::value || aAttribute == nsGkAtoms::max)) { + auto presShell = PresShell(); + for (auto childFrame : PrincipalChildList()) { + presShell->FrameNeedsReflow(childFrame, IntrinsicDirty::None, + NS_FRAME_IS_DIRTY); + } + InvalidateFrame(); + } + + return nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType); +} + +LogicalSize nsProgressFrame::ComputeAutoSize( + gfxContext* aRenderingContext, WritingMode aWM, const LogicalSize& aCBSize, + nscoord aAvailableISize, const LogicalSize& aMargin, + const LogicalSize& aBorderPadding, const StyleSizeOverrides& aSizeOverrides, + ComputeSizeFlags aFlags) { + const WritingMode wm = GetWritingMode(); + LogicalSize autoSize(wm); + autoSize.BSize(wm) = autoSize.ISize(wm) = + StyleFont() + ->mFont.size.ScaledBy(nsLayoutUtils::FontSizeInflationFor(this)) + .ToAppUnits(); // 1em + + if (ResolvedOrientationIsVertical() == wm.IsVertical()) { + autoSize.ISize(wm) *= 10; // 10em + } else { + autoSize.BSize(wm) *= 10; // 10em + } + + return autoSize.ConvertTo(aWM, wm); +} + +nscoord nsProgressFrame::GetMinISize(gfxContext* aRenderingContext) { + RefPtr<nsFontMetrics> fontMet = + nsLayoutUtils::GetFontMetricsForFrame(this, 1.0f); + + nscoord minISize = fontMet->Font().size.ToAppUnits(); // 1em + + if (ResolvedOrientationIsVertical() == GetWritingMode().IsVertical()) { + // The orientation is inline + minISize *= 10; // 10em + } + + return minISize; +} + +nscoord nsProgressFrame::GetPrefISize(gfxContext* aRenderingContext) { + return GetMinISize(aRenderingContext); +} + +bool nsProgressFrame::ShouldUseNativeStyle() const { + nsIFrame* barFrame = PrincipalChildList().FirstChild(); + + // Use the native style if these conditions are satisfied: + // - both frames use the native appearance; + // - neither frame has author specified rules setting the border or the + // background. + return StyleDisplay()->EffectiveAppearance() == + StyleAppearance::ProgressBar && + !Style()->HasAuthorSpecifiedBorderOrBackground() && barFrame && + barFrame->StyleDisplay()->EffectiveAppearance() == + StyleAppearance::Progresschunk && + !barFrame->Style()->HasAuthorSpecifiedBorderOrBackground(); +} diff --git a/layout/forms/nsProgressFrame.h b/layout/forms/nsProgressFrame.h new file mode 100644 index 0000000000..5f4e604313 --- /dev/null +++ b/layout/forms/nsProgressFrame.h @@ -0,0 +1,89 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsProgressFrame_h___ +#define nsProgressFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsContainerFrame.h" +#include "nsIAnonymousContentCreator.h" +#include "nsCOMPtr.h" + +namespace mozilla { +enum class PseudoStyleType : uint8_t; +} // namespace mozilla + +class nsProgressFrame final : public nsContainerFrame, + public nsIAnonymousContentCreator { + typedef mozilla::PseudoStyleType PseudoStyleType; + typedef mozilla::dom::Element Element; + + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsProgressFrame) + + explicit nsProgressFrame(ComputedStyle* aStyle, nsPresContext* aPresContext); + virtual ~nsProgressFrame(); + + void Destroy(DestroyContext&) override; + + virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + virtual void Reflow(nsPresContext* aCX, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"Progress"_ns, aResult); + } +#endif + + // nsIAnonymousContentCreator + virtual nsresult CreateAnonymousContent( + nsTArray<ContentInfo>& aElements) override; + virtual void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) override; + + virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + + virtual mozilla::LogicalSize ComputeAutoSize( + gfxContext* aRenderingContext, mozilla::WritingMode aWM, + const mozilla::LogicalSize& aCBSize, nscoord aAvailableISize, + const mozilla::LogicalSize& aMargin, + const mozilla::LogicalSize& aBorderPadding, + const mozilla::StyleSizeOverrides& aSizeOverrides, + mozilla::ComputeSizeFlags aFlags) override; + + virtual nscoord GetMinISize(gfxContext* aRenderingContext) override; + virtual nscoord GetPrefISize(gfxContext* aRenderingContext) override; + + virtual bool IsFrameOfType(uint32_t aFlags) const override { + return nsContainerFrame::IsFrameOfType( + aFlags & ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock)); + } + + /** + * Returns whether the frame and its child should use the native style. + */ + bool ShouldUseNativeStyle() const; + + protected: + // Helper function to reflow a child frame. + void ReflowChildFrame(nsIFrame* aChild, nsPresContext* aPresContext, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus); + + /** + * The div used to show the progress bar. + * @see nsProgressFrame::CreateAnonymousContent + */ + nsCOMPtr<Element> mBarDiv; +}; + +#endif diff --git a/layout/forms/nsRangeFrame.cpp b/layout/forms/nsRangeFrame.cpp new file mode 100644 index 0000000000..331f8f6a43 --- /dev/null +++ b/layout/forms/nsRangeFrame.cpp @@ -0,0 +1,769 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsRangeFrame.h" + +#include "ListMutationObserver.h" +#include "mozilla/Assertions.h" +#include "mozilla/PresShell.h" +#include "mozilla/TouchEvents.h" + +#include "gfxContext.h" +#include "nsContentCreatorFunctions.h" +#include "nsCSSPseudoElements.h" +#include "nsCSSRendering.h" +#include "nsDisplayList.h" +#include "nsIContent.h" +#include "nsLayoutUtils.h" +#include "mozilla/dom/Document.h" +#include "nsNameSpaceManager.h" +#include "nsGkAtoms.h" +#include "mozilla/dom/HTMLDataListElement.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/dom/HTMLOptionElement.h" +#include "mozilla/dom/MutationEventBinding.h" +#include "nsPresContext.h" +#include "nsPresContextInlines.h" +#include "nsNodeInfoManager.h" +#include "mozilla/dom/Element.h" +#include "mozilla/ServoStyleSet.h" +#include "nsStyleConsts.h" +#include "nsTArray.h" + +#ifdef ACCESSIBILITY +# include "nsAccessibilityService.h" +#endif + +// Our intrinsic size is 12em in the main-axis and 1.3em in the cross-axis. +#define MAIN_AXIS_EM_SIZE 12 +#define CROSS_AXIS_EM_SIZE 1.3f + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::image; + +nsIFrame* NS_NewRangeFrame(PresShell* aPresShell, ComputedStyle* aStyle) { + return new (aPresShell) nsRangeFrame(aStyle, aPresShell->GetPresContext()); +} + +nsRangeFrame::nsRangeFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) + : nsContainerFrame(aStyle, aPresContext, kClassID) {} + +void nsRangeFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + nsContainerFrame::Init(aContent, aParent, aPrevInFlow); + if (InputElement().HasAttr(nsGkAtoms::list_)) { + mListMutationObserver = new ListMutationObserver(*this); + } +} + +nsRangeFrame::~nsRangeFrame() = default; + +NS_IMPL_FRAMEARENA_HELPERS(nsRangeFrame) + +NS_QUERYFRAME_HEAD(nsRangeFrame) + NS_QUERYFRAME_ENTRY(nsRangeFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) +NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) + +void nsRangeFrame::Destroy(DestroyContext& aContext) { + NS_ASSERTION(!GetPrevContinuation() && !GetNextContinuation(), + "nsRangeFrame should not have continuations; if it does we " + "need to call RegUnregAccessKey only for the first."); + + if (mListMutationObserver) { + mListMutationObserver->Detach(); + } + aContext.AddAnonymousContent(mTrackDiv.forget()); + aContext.AddAnonymousContent(mProgressDiv.forget()); + aContext.AddAnonymousContent(mThumbDiv.forget()); + nsContainerFrame::Destroy(aContext); +} + +nsresult nsRangeFrame::MakeAnonymousDiv(Element** aResult, + PseudoStyleType aPseudoType, + nsTArray<ContentInfo>& aElements) { + nsCOMPtr<Document> doc = mContent->GetComposedDoc(); + RefPtr<Element> resultElement = doc->CreateHTMLElement(nsGkAtoms::div); + + // Associate the pseudo-element with the anonymous child. + resultElement->SetPseudoElementType(aPseudoType); + + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier, or change the return type to void. + aElements.AppendElement(resultElement); + + resultElement.forget(aResult); + return NS_OK; +} + +nsresult nsRangeFrame::CreateAnonymousContent( + nsTArray<ContentInfo>& aElements) { + nsresult rv; + + // Create the ::-moz-range-track pseudo-element (a div): + rv = MakeAnonymousDiv(getter_AddRefs(mTrackDiv), + PseudoStyleType::mozRangeTrack, aElements); + NS_ENSURE_SUCCESS(rv, rv); + + // Create the ::-moz-range-progress pseudo-element (a div): + rv = MakeAnonymousDiv(getter_AddRefs(mProgressDiv), + PseudoStyleType::mozRangeProgress, aElements); + NS_ENSURE_SUCCESS(rv, rv); + + // Create the ::-moz-range-thumb pseudo-element (a div): + rv = MakeAnonymousDiv(getter_AddRefs(mThumbDiv), + PseudoStyleType::mozRangeThumb, aElements); + return rv; +} + +void nsRangeFrame::AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) { + if (mTrackDiv) { + aElements.AppendElement(mTrackDiv); + } + + if (mProgressDiv) { + aElements.AppendElement(mProgressDiv); + } + + if (mThumbDiv) { + aElements.AppendElement(mThumbDiv); + } +} + +void nsRangeFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + const nsStyleDisplay* disp = StyleDisplay(); + if (IsThemed(disp)) { + DisplayBorderBackgroundOutline(aBuilder, aLists); + // Only create items for the thumb. Specifically, we do not want + // the track to paint, since *our* background is used to paint + // the track, and we don't want the unthemed track painting over + // the top of the themed track. + // This logic is copied from + // nsContainerFrame::BuildDisplayListForNonBlockChildren as + // called by BuildDisplayListForInline. + nsIFrame* thumb = mThumbDiv->GetPrimaryFrame(); + if (thumb) { + nsDisplayListSet set(aLists, aLists.Content()); + BuildDisplayListForChild(aBuilder, thumb, set, DisplayChildFlag::Inline); + } + } else { + BuildDisplayListForInline(aBuilder, aLists); + } +} + +void nsRangeFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MarkInReflow(); + DO_GLOBAL_REFLOW_COUNT("nsRangeFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + + NS_ASSERTION(mTrackDiv, "::-moz-range-track div must exist!"); + NS_ASSERTION(mProgressDiv, "::-moz-range-progress div must exist!"); + NS_ASSERTION(mThumbDiv, "::-moz-range-thumb div must exist!"); + NS_ASSERTION(!GetPrevContinuation() && !GetNextContinuation(), + "nsRangeFrame should not have continuations; if it does we " + "need to call RegUnregAccessKey only for the first."); + + WritingMode wm = aReflowInput.GetWritingMode(); + nscoord computedBSize = aReflowInput.ComputedBSize(); + if (computedBSize == NS_UNCONSTRAINEDSIZE) { + computedBSize = 0; + } + const auto borderPadding = aReflowInput.ComputedLogicalBorderPadding(wm); + LogicalSize finalSize( + wm, aReflowInput.ComputedISize() + borderPadding.IStartEnd(wm), + computedBSize + borderPadding.BStartEnd(wm)); + aDesiredSize.SetSize(wm, finalSize); + + ReflowAnonymousContent(aPresContext, aDesiredSize, aReflowInput); + + aDesiredSize.SetOverflowAreasToDesiredBounds(); + + nsIFrame* trackFrame = mTrackDiv->GetPrimaryFrame(); + if (trackFrame) { + ConsiderChildOverflow(aDesiredSize.mOverflowAreas, trackFrame); + } + + nsIFrame* rangeProgressFrame = mProgressDiv->GetPrimaryFrame(); + if (rangeProgressFrame) { + ConsiderChildOverflow(aDesiredSize.mOverflowAreas, rangeProgressFrame); + } + + nsIFrame* thumbFrame = mThumbDiv->GetPrimaryFrame(); + if (thumbFrame) { + ConsiderChildOverflow(aDesiredSize.mOverflowAreas, thumbFrame); + } + + FinishAndStoreOverflow(&aDesiredSize); + + MOZ_ASSERT(aStatus.IsEmpty(), "This type of frame can't be split."); +} + +void nsRangeFrame::ReflowAnonymousContent(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput) { + // The width/height of our content box, which is the available width/height + // for our anonymous content: + nscoord rangeFrameContentBoxWidth = aReflowInput.ComputedWidth(); + nscoord rangeFrameContentBoxHeight = aReflowInput.ComputedHeight(); + if (rangeFrameContentBoxHeight == NS_UNCONSTRAINEDSIZE) { + rangeFrameContentBoxHeight = 0; + } + + nsIFrame* trackFrame = mTrackDiv->GetPrimaryFrame(); + + if (trackFrame) { // display:none? + + // Position the track: + // The idea here is that we allow content authors to style the width, + // height, border and padding of the track, but we ignore margin and + // positioning properties and do the positioning ourself to keep the center + // of the track's border box on the center of the nsRangeFrame's content + // box. + + WritingMode wm = trackFrame->GetWritingMode(); + LogicalSize availSize = aReflowInput.ComputedSize(wm); + availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE; + ReflowInput trackReflowInput(aPresContext, aReflowInput, trackFrame, + availSize); + + // Find the x/y position of the track frame such that it will be positioned + // as described above. These coordinates are with respect to the + // nsRangeFrame's border-box. + nscoord trackX = rangeFrameContentBoxWidth / 2; + nscoord trackY = rangeFrameContentBoxHeight / 2; + + // Account for the track's border and padding (we ignore its margin): + trackX -= trackReflowInput.ComputedPhysicalBorderPadding().left + + trackReflowInput.ComputedWidth() / 2; + trackY -= trackReflowInput.ComputedPhysicalBorderPadding().top + + trackReflowInput.ComputedHeight() / 2; + + // Make relative to our border box instead of our content box: + trackX += aReflowInput.ComputedPhysicalBorderPadding().left; + trackY += aReflowInput.ComputedPhysicalBorderPadding().top; + + nsReflowStatus frameStatus; + ReflowOutput trackDesiredSize(aReflowInput); + ReflowChild(trackFrame, aPresContext, trackDesiredSize, trackReflowInput, + trackX, trackY, ReflowChildFlags::Default, frameStatus); + MOZ_ASSERT( + frameStatus.IsFullyComplete(), + "We gave our child unconstrained height, so it should be complete"); + FinishReflowChild(trackFrame, aPresContext, trackDesiredSize, + &trackReflowInput, trackX, trackY, + ReflowChildFlags::Default); + } + + nsIFrame* thumbFrame = mThumbDiv->GetPrimaryFrame(); + + if (thumbFrame) { // display:none? + WritingMode wm = thumbFrame->GetWritingMode(); + LogicalSize availSize = aReflowInput.ComputedSize(wm); + availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE; + ReflowInput thumbReflowInput(aPresContext, aReflowInput, thumbFrame, + availSize); + + // Where we position the thumb depends on its size, so we first reflow + // the thumb at {0,0} to obtain its size, then position it afterwards. + + nsReflowStatus frameStatus; + ReflowOutput thumbDesiredSize(aReflowInput); + ReflowChild(thumbFrame, aPresContext, thumbDesiredSize, thumbReflowInput, 0, + 0, ReflowChildFlags::Default, frameStatus); + MOZ_ASSERT( + frameStatus.IsFullyComplete(), + "We gave our child unconstrained height, so it should be complete"); + FinishReflowChild(thumbFrame, aPresContext, thumbDesiredSize, + &thumbReflowInput, 0, 0, ReflowChildFlags::Default); + DoUpdateThumbPosition(thumbFrame, + nsSize(aDesiredSize.Width(), aDesiredSize.Height())); + } + + nsIFrame* rangeProgressFrame = mProgressDiv->GetPrimaryFrame(); + + if (rangeProgressFrame) { // display:none? + WritingMode wm = rangeProgressFrame->GetWritingMode(); + LogicalSize availSize = aReflowInput.ComputedSize(wm); + availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE; + ReflowInput progressReflowInput(aPresContext, aReflowInput, + rangeProgressFrame, availSize); + + // We first reflow the range-progress frame at {0,0} to obtain its + // unadjusted dimensions, then we adjust it to so that the appropriate edge + // ends at the thumb. + + nsReflowStatus frameStatus; + ReflowOutput progressDesiredSize(aReflowInput); + ReflowChild(rangeProgressFrame, aPresContext, progressDesiredSize, + progressReflowInput, 0, 0, ReflowChildFlags::Default, + frameStatus); + MOZ_ASSERT( + frameStatus.IsFullyComplete(), + "We gave our child unconstrained height, so it should be complete"); + FinishReflowChild(rangeProgressFrame, aPresContext, progressDesiredSize, + &progressReflowInput, 0, 0, ReflowChildFlags::Default); + DoUpdateRangeProgressFrame( + rangeProgressFrame, + nsSize(aDesiredSize.Width(), aDesiredSize.Height())); + } +} + +#ifdef ACCESSIBILITY +a11y::AccType nsRangeFrame::AccessibleType() { return a11y::eHTMLRangeType; } +#endif + +double nsRangeFrame::GetValueAsFractionOfRange() { + return GetDoubleAsFractionOfRange(InputElement().GetValueAsDecimal()); +} + +double nsRangeFrame::GetDoubleAsFractionOfRange(const Decimal& aValue) { + auto& input = InputElement(); + + Decimal minimum = input.GetMinimum(); + Decimal maximum = input.GetMaximum(); + + MOZ_ASSERT(aValue.isFinite() && minimum.isFinite() && maximum.isFinite(), + "type=range should have a default maximum/minimum"); + + if (maximum <= minimum) { + // Avoid rounding triggering the assert by checking against an epsilon. + MOZ_ASSERT((aValue - minimum).abs().toDouble() < + std::numeric_limits<float>::epsilon(), + "Unsanitized value"); + return 0.0; + } + + MOZ_ASSERT(aValue >= minimum && aValue <= maximum, "Unsanitized value"); + + return ((aValue - minimum) / (maximum - minimum)).toDouble(); +} + +Decimal nsRangeFrame::GetValueAtEventPoint(WidgetGUIEvent* aEvent) { + MOZ_ASSERT( + aEvent->mClass == eMouseEventClass || aEvent->mClass == eTouchEventClass, + "Unexpected event type - aEvent->mRefPoint may be meaningless"); + + MOZ_ASSERT(mContent->IsHTMLElement(nsGkAtoms::input), "bad cast"); + dom::HTMLInputElement* input = + static_cast<dom::HTMLInputElement*>(GetContent()); + + MOZ_ASSERT(input->ControlType() == FormControlType::InputRange); + + Decimal minimum = input->GetMinimum(); + Decimal maximum = input->GetMaximum(); + MOZ_ASSERT(minimum.isFinite() && maximum.isFinite(), + "type=range should have a default maximum/minimum"); + if (maximum <= minimum) { + return minimum; + } + Decimal range = maximum - minimum; + + LayoutDeviceIntPoint absPoint; + if (aEvent->mClass == eTouchEventClass) { + MOZ_ASSERT(aEvent->AsTouchEvent()->mTouches.Length() == 1, + "Unexpected number of mTouches"); + absPoint = aEvent->AsTouchEvent()->mTouches[0]->mRefPoint; + } else { + absPoint = aEvent->mRefPoint; + } + nsPoint point = nsLayoutUtils::GetEventCoordinatesRelativeTo( + aEvent, absPoint, RelativeTo{this}); + + if (point == nsPoint(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE)) { + // We don't want to change the current value for this error state. + return static_cast<dom::HTMLInputElement*>(GetContent()) + ->GetValueAsDecimal(); + } + + nsRect rangeContentRect = GetContentRectRelativeToSelf(); + nsSize thumbSize; + + if (IsThemed()) { + // We need to get the size of the thumb from the theme. + nsPresContext* pc = PresContext(); + LayoutDeviceIntSize size = pc->Theme()->GetMinimumWidgetSize( + pc, this, StyleAppearance::RangeThumb); + thumbSize = + LayoutDeviceIntSize::ToAppUnits(size, pc->AppUnitsPerDevPixel()); + // For GTK, GetMinimumWidgetSize returns zero for the thumb dimension + // perpendicular to the orientation of the slider. That's okay since we + // only care about the dimension in the direction of the slider when using + // |thumbSize| below, but it means this assertion need to check + // IsHorizontal(). + MOZ_ASSERT((IsHorizontal() && thumbSize.width > 0) || + (!IsHorizontal() && thumbSize.height > 0), + "The thumb is expected to take up some slider space"); + } else { + nsIFrame* thumbFrame = mThumbDiv->GetPrimaryFrame(); + if (thumbFrame) { // diplay:none? + thumbSize = thumbFrame->GetSize(); + } + } + + Decimal fraction; + if (IsHorizontal()) { + nscoord traversableDistance = rangeContentRect.width - thumbSize.width; + if (traversableDistance <= 0) { + return minimum; + } + nscoord posAtStart = rangeContentRect.x + thumbSize.width / 2; + nscoord posAtEnd = posAtStart + traversableDistance; + nscoord posOfPoint = mozilla::clamped(point.x, posAtStart, posAtEnd); + fraction = Decimal(posOfPoint - posAtStart) / Decimal(traversableDistance); + if (IsRightToLeft()) { + fraction = Decimal(1) - fraction; + } + } else { + nscoord traversableDistance = rangeContentRect.height - thumbSize.height; + if (traversableDistance <= 0) { + return minimum; + } + nscoord posAtStart = rangeContentRect.y + thumbSize.height / 2; + nscoord posAtEnd = posAtStart + traversableDistance; + nscoord posOfPoint = mozilla::clamped(point.y, posAtStart, posAtEnd); + // For a vertical range, the top (posAtStart) is the highest value, so we + // subtract the fraction from 1.0 to get that polarity correct. + fraction = Decimal(1) - + Decimal(posOfPoint - posAtStart) / Decimal(traversableDistance); + } + + MOZ_ASSERT(fraction >= Decimal(0) && fraction <= Decimal(1)); + return minimum + fraction * range; +} + +void nsRangeFrame::UpdateForValueChange() { + if (IsSubtreeDirty()) { + return; // we're going to be updated when we reflow + } + nsIFrame* rangeProgressFrame = mProgressDiv->GetPrimaryFrame(); + nsIFrame* thumbFrame = mThumbDiv->GetPrimaryFrame(); + if (!rangeProgressFrame && !thumbFrame) { + return; // diplay:none? + } + if (rangeProgressFrame) { + DoUpdateRangeProgressFrame(rangeProgressFrame, GetSize()); + } + if (thumbFrame) { + DoUpdateThumbPosition(thumbFrame, GetSize()); + } + if (IsThemed()) { + // We don't know the exact dimensions or location of the thumb when native + // theming is applied, so we just repaint the entire range. + InvalidateFrame(); + } + +#ifdef ACCESSIBILITY + if (nsAccessibilityService* accService = GetAccService()) { + accService->RangeValueChanged(PresShell(), mContent); + } +#endif + + SchedulePaint(); +} + +nsTArray<Decimal> nsRangeFrame::TickMarks() { + nsTArray<Decimal> tickMarks; + auto& input = InputElement(); + auto* list = input.GetList(); + if (!list) { + return tickMarks; + } + auto min = input.GetMinimum(); + auto max = input.GetMaximum(); + auto* options = list->Options(); + nsAutoString label; + for (uint32_t i = 0; i < options->Length(); ++i) { + auto* item = options->Item(i); + auto* option = HTMLOptionElement::FromNode(item); + MOZ_ASSERT(option); + if (option->Disabled()) { + continue; + } + nsAutoString str; + option->GetValue(str); + auto tickMark = HTMLInputElement::StringToDecimal(str); + if (tickMark.isNaN() || tickMark < min || tickMark > max || + input.ValueIsStepMismatch(tickMark)) { + continue; + } + tickMarks.AppendElement(tickMark); + } + tickMarks.Sort(); + return tickMarks; +} + +Decimal nsRangeFrame::NearestTickMark(const Decimal& aValue) { + auto tickMarks = TickMarks(); + if (tickMarks.IsEmpty() || aValue.isNaN()) { + return Decimal::nan(); + } + size_t index; + if (BinarySearch(tickMarks, 0, tickMarks.Length(), aValue, &index)) { + return tickMarks[index]; + } + if (index == tickMarks.Length()) { + return tickMarks.LastElement(); + } + if (index == 0) { + return tickMarks[0]; + } + const auto& smallerTickMark = tickMarks[index - 1]; + const auto& largerTickMark = tickMarks[index]; + MOZ_ASSERT(smallerTickMark < aValue); + MOZ_ASSERT(largerTickMark > aValue); + return (aValue - smallerTickMark).abs() < (aValue - largerTickMark).abs() + ? smallerTickMark + : largerTickMark; +} + +mozilla::dom::HTMLInputElement& nsRangeFrame::InputElement() const { + MOZ_ASSERT(mContent->IsHTMLElement(nsGkAtoms::input), "bad cast"); + auto& input = *static_cast<dom::HTMLInputElement*>(GetContent()); + MOZ_ASSERT(input.ControlType() == FormControlType::InputRange); + return input; +} + +void nsRangeFrame::DoUpdateThumbPosition(nsIFrame* aThumbFrame, + const nsSize& aRangeSize) { + MOZ_ASSERT(aThumbFrame); + + // The idea here is that we want to position the thumb so that the center + // of the thumb is on an imaginary line drawn from the middle of one edge + // of the range frame's content box to the middle of the opposite edge of + // its content box (the opposite edges being the left/right edge if the + // range is horizontal, or else the top/bottom edges if the range is + // vertical). How far along this line the center of the thumb is placed + // depends on the value of the range. + + nsMargin borderAndPadding = GetUsedBorderAndPadding(); + nsPoint newPosition(borderAndPadding.left, borderAndPadding.top); + + nsSize rangeContentBoxSize(aRangeSize); + rangeContentBoxSize.width -= borderAndPadding.LeftRight(); + rangeContentBoxSize.height -= borderAndPadding.TopBottom(); + + nsSize thumbSize = aThumbFrame->GetSize(); + double fraction = GetValueAsFractionOfRange(); + MOZ_ASSERT(fraction >= 0.0 && fraction <= 1.0); + + if (IsHorizontal()) { + if (thumbSize.width < rangeContentBoxSize.width) { + nscoord traversableDistance = rangeContentBoxSize.width - thumbSize.width; + if (IsRightToLeft()) { + newPosition.x += NSToCoordRound((1.0 - fraction) * traversableDistance); + } else { + newPosition.x += NSToCoordRound(fraction * traversableDistance); + } + newPosition.y += (rangeContentBoxSize.height - thumbSize.height) / 2; + } + } else { + if (thumbSize.height < rangeContentBoxSize.height) { + nscoord traversableDistance = + rangeContentBoxSize.height - thumbSize.height; + newPosition.x += (rangeContentBoxSize.width - thumbSize.width) / 2; + newPosition.y += NSToCoordRound((1.0 - fraction) * traversableDistance); + } + } + aThumbFrame->SetPosition(newPosition); +} + +void nsRangeFrame::DoUpdateRangeProgressFrame(nsIFrame* aRangeProgressFrame, + const nsSize& aRangeSize) { + MOZ_ASSERT(aRangeProgressFrame); + + // The idea here is that we want to position the ::-moz-range-progress + // pseudo-element so that the center line running along its length is on the + // corresponding center line of the nsRangeFrame's content box. In the other + // dimension, we align the "start" edge of the ::-moz-range-progress + // pseudo-element's border-box with the corresponding edge of the + // nsRangeFrame's content box, and we size the progress element's border-box + // to have a length of GetValueAsFractionOfRange() times the nsRangeFrame's + // content-box size. + + nsMargin borderAndPadding = GetUsedBorderAndPadding(); + nsSize progSize = aRangeProgressFrame->GetSize(); + nsRect progRect(borderAndPadding.left, borderAndPadding.top, progSize.width, + progSize.height); + + nsSize rangeContentBoxSize(aRangeSize); + rangeContentBoxSize.width -= borderAndPadding.LeftRight(); + rangeContentBoxSize.height -= borderAndPadding.TopBottom(); + + double fraction = GetValueAsFractionOfRange(); + MOZ_ASSERT(fraction >= 0.0 && fraction <= 1.0); + + if (IsHorizontal()) { + nscoord progLength = NSToCoordRound(fraction * rangeContentBoxSize.width); + if (IsRightToLeft()) { + progRect.x += rangeContentBoxSize.width - progLength; + } + progRect.y += (rangeContentBoxSize.height - progSize.height) / 2; + progRect.width = progLength; + } else { + nscoord progLength = NSToCoordRound(fraction * rangeContentBoxSize.height); + progRect.x += (rangeContentBoxSize.width - progSize.width) / 2; + progRect.y += rangeContentBoxSize.height - progLength; + progRect.height = progLength; + } + aRangeProgressFrame->SetRect(progRect); +} + +nsresult nsRangeFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, int32_t aModType) { + NS_ASSERTION(mTrackDiv, "The track div must exist!"); + NS_ASSERTION(mThumbDiv, "The thumb div must exist!"); + + if (aNameSpaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::value || aAttribute == nsGkAtoms::min || + aAttribute == nsGkAtoms::max || aAttribute == nsGkAtoms::step) { + // We want to update the position of the thumb, except in one special + // case: If the value attribute is being set, it is possible that we are + // in the middle of a type change away from type=range, under the + // SetAttr(..., nsGkAtoms::value, ...) call in HTMLInputElement:: + // HandleTypeChange. In that case the HTMLInputElement's type will + // already have changed, and if we call UpdateForValueChange() + // we'll fail the asserts under that call that check the type of our + // HTMLInputElement. Given that we're changing away from being a range + // and this frame will shortly be destroyed, there's no point in calling + // UpdateForValueChange() anyway. + MOZ_ASSERT(mContent->IsHTMLElement(nsGkAtoms::input), "bad cast"); + bool typeIsRange = + static_cast<dom::HTMLInputElement*>(GetContent())->ControlType() == + FormControlType::InputRange; + // If script changed the <input>'s type before setting these attributes + // then we don't need to do anything since we are going to be reframed. + if (typeIsRange) { + UpdateForValueChange(); + } + } else if (aAttribute == nsGkAtoms::orient) { + PresShell()->FrameNeedsReflow(this, IntrinsicDirty::None, + NS_FRAME_IS_DIRTY); + } else if (aAttribute == nsGkAtoms::list_) { + const bool isRemoval = aModType == MutationEvent_Binding::REMOVAL; + if (mListMutationObserver) { + mListMutationObserver->Detach(); + if (isRemoval) { + mListMutationObserver = nullptr; + } else { + mListMutationObserver->Attach(); + } + } else if (!isRemoval) { + mListMutationObserver = new ListMutationObserver(*this, true); + } + } + } + + return nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType); +} + +nscoord nsRangeFrame::AutoCrossSize(Length aEm) { + nscoord minCrossSize(0); + if (IsThemed()) { + nsPresContext* pc = PresContext(); + LayoutDeviceIntSize size = pc->Theme()->GetMinimumWidgetSize( + pc, this, StyleAppearance::RangeThumb); + minCrossSize = + pc->DevPixelsToAppUnits(IsHorizontal() ? size.height : size.width); + } + return std::max(minCrossSize, aEm.ScaledBy(CROSS_AXIS_EM_SIZE).ToAppUnits()); +} + +static mozilla::Length OneEm(nsRangeFrame* aFrame) { + return aFrame->StyleFont()->mFont.size.ScaledBy( + nsLayoutUtils::FontSizeInflationFor(aFrame)); +} + +LogicalSize nsRangeFrame::ComputeAutoSize( + gfxContext* aRenderingContext, WritingMode aWM, const LogicalSize& aCBSize, + nscoord aAvailableISize, const LogicalSize& aMargin, + const LogicalSize& aBorderPadding, const StyleSizeOverrides& aSizeOverrides, + ComputeSizeFlags aFlags) { + bool isInlineOriented = IsInlineOriented(); + auto em = OneEm(this); + + const WritingMode wm = GetWritingMode(); + LogicalSize autoSize(wm); + if (isInlineOriented) { + autoSize.ISize(wm) = em.ScaledBy(MAIN_AXIS_EM_SIZE).ToAppUnits(); + autoSize.BSize(wm) = AutoCrossSize(em); + } else { + autoSize.ISize(wm) = AutoCrossSize(em); + autoSize.BSize(wm) = em.ScaledBy(MAIN_AXIS_EM_SIZE).ToAppUnits(); + } + + return autoSize.ConvertTo(aWM, wm); +} + +nscoord nsRangeFrame::GetMinISize(gfxContext* aRenderingContext) { + auto pos = StylePosition(); + auto wm = GetWritingMode(); + if (pos->ISize(wm).HasPercent()) { + // https://drafts.csswg.org/css-sizing-3/#percentage-sizing + // https://drafts.csswg.org/css-sizing-3/#min-content-zero + return nsLayoutUtils::ResolveToLength<true>( + pos->ISize(wm).AsLengthPercentage(), nscoord(0)); + } + return GetPrefISize(aRenderingContext); +} + +nscoord nsRangeFrame::GetPrefISize(gfxContext* aRenderingContext) { + auto em = OneEm(this); + if (IsInlineOriented()) { + return em.ScaledBy(MAIN_AXIS_EM_SIZE).ToAppUnits(); + } + return AutoCrossSize(em); +} + +bool nsRangeFrame::IsHorizontal() const { + dom::HTMLInputElement* element = + static_cast<dom::HTMLInputElement*>(GetContent()); + return element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::orient, + nsGkAtoms::horizontal, eCaseMatters) || + (!element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::orient, + nsGkAtoms::vertical, eCaseMatters) && + GetWritingMode().IsVertical() == + element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::orient, + nsGkAtoms::block, eCaseMatters)); +} + +double nsRangeFrame::GetMin() const { + return static_cast<dom::HTMLInputElement*>(GetContent()) + ->GetMinimum() + .toDouble(); +} + +double nsRangeFrame::GetMax() const { + return static_cast<dom::HTMLInputElement*>(GetContent()) + ->GetMaximum() + .toDouble(); +} + +double nsRangeFrame::GetValue() const { + return static_cast<dom::HTMLInputElement*>(GetContent()) + ->GetValueAsDecimal() + .toDouble(); +} + +bool nsRangeFrame::ShouldUseNativeStyle() const { + nsIFrame* trackFrame = mTrackDiv->GetPrimaryFrame(); + nsIFrame* progressFrame = mProgressDiv->GetPrimaryFrame(); + nsIFrame* thumbFrame = mThumbDiv->GetPrimaryFrame(); + + return StyleDisplay()->EffectiveAppearance() == StyleAppearance::Range && + trackFrame && + !trackFrame->Style()->HasAuthorSpecifiedBorderOrBackground() && + progressFrame && + !progressFrame->Style()->HasAuthorSpecifiedBorderOrBackground() && + thumbFrame && + !thumbFrame->Style()->HasAuthorSpecifiedBorderOrBackground(); +} diff --git a/layout/forms/nsRangeFrame.h b/layout/forms/nsRangeFrame.h new file mode 100644 index 0000000000..96d2666b4d --- /dev/null +++ b/layout/forms/nsRangeFrame.h @@ -0,0 +1,209 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsRangeFrame_h___ +#define nsRangeFrame_h___ + +#include "mozilla/Attributes.h" +#include "mozilla/Decimal.h" +#include "mozilla/EventForwards.h" +#include "nsContainerFrame.h" +#include "nsIAnonymousContentCreator.h" +#include "nsIDOMEventListener.h" +#include "nsCOMPtr.h" +#include "nsTArray.h" + +class nsDisplayRangeFocusRing; + +namespace mozilla { +class ListMutationObserver; +class PresShell; +namespace dom { +class Event; +class HTMLInputElement; +} // namespace dom +} // namespace mozilla + +class nsRangeFrame final : public nsContainerFrame, + public nsIAnonymousContentCreator { + friend nsIFrame* NS_NewRangeFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + + friend class nsDisplayRangeFocusRing; + + explicit nsRangeFrame(ComputedStyle* aStyle, nsPresContext* aPresContext); + virtual ~nsRangeFrame(); + + typedef mozilla::PseudoStyleType PseudoStyleType; + typedef mozilla::dom::Element Element; + + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsRangeFrame) + + // nsIFrame overrides + void Destroy(DestroyContext&) override; + + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + virtual void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"Range"_ns, aResult); + } +#endif + +#ifdef ACCESSIBILITY + virtual mozilla::a11y::AccType AccessibleType() override; +#endif + + // nsIAnonymousContentCreator + virtual nsresult CreateAnonymousContent( + nsTArray<ContentInfo>& aElements) override; + virtual void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) override; + + virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + + mozilla::LogicalSize ComputeAutoSize( + gfxContext* aRenderingContext, mozilla::WritingMode aWM, + const mozilla::LogicalSize& aCBSize, nscoord aAvailableISize, + const mozilla::LogicalSize& aMargin, + const mozilla::LogicalSize& aBorderPadding, + const mozilla::StyleSizeOverrides& aSizeOverrides, + mozilla::ComputeSizeFlags aFlags) override; + + virtual nscoord GetMinISize(gfxContext* aRenderingContext) override; + virtual nscoord GetPrefISize(gfxContext* aRenderingContext) override; + + virtual bool IsFrameOfType(uint32_t aFlags) const override { + return nsContainerFrame::IsFrameOfType( + aFlags & ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock)); + } + + /** + * Returns true if the slider's thumb moves horizontally, or else false if it + * moves vertically. + */ + bool IsHorizontal() const; + + /** + * Returns true if the slider is oriented along the inline axis. + */ + bool IsInlineOriented() const { + return IsHorizontal() != GetWritingMode().IsVertical(); + } + + /** + * Returns true if the slider's thumb moves right-to-left for increasing + * values; only relevant when IsHorizontal() is true. + */ + bool IsRightToLeft() const { + MOZ_ASSERT(IsHorizontal()); + return GetWritingMode().IsPhysicalRTL(); + } + + double GetMin() const; + double GetMax() const; + double GetValue() const; + + /** + * Returns the input element's value as a fraction of the difference between + * the input's minimum and its maximum (i.e. returns 0.0 when the value is + * the same as the minimum, and returns 1.0 when the value is the same as the + * maximum). + */ + double GetValueAsFractionOfRange(); + + /** + * Returns the given value as a fraction of the difference between the input's + * minimum and its maximum (i.e. returns 0.0 when the value is the same as the + * input's minimum, and returns 1.0 when the value is the same as the input's + * maximum). + */ + double GetDoubleAsFractionOfRange(const mozilla::Decimal& value); + + /** + * Returns whether the frame and its child should use the native style. + */ + bool ShouldUseNativeStyle() const; + + mozilla::Decimal GetValueAtEventPoint(mozilla::WidgetGUIEvent* aEvent); + + /** + * Helper that's used when the value of the range changes to reposition the + * thumb, resize the range-progress element, and schedule a repaint. (This + * does not reflow, since the position and size of the thumb and + * range-progress element do not affect the position or size of any other + * frames.) + */ + void UpdateForValueChange(); + + nsTArray<mozilla::Decimal> TickMarks(); + + /** + * Returns the given value's offset from the range's nearest list tick mark + * or NaN if there are no tick marks. + */ + mozilla::Decimal NearestTickMark(const mozilla::Decimal& aValue); + + protected: + mozilla::dom::HTMLInputElement& InputElement() const; + + private: + // Return our preferred size in the cross-axis (the axis perpendicular + // to the direction of movement of the thumb). + nscoord AutoCrossSize(mozilla::Length aEm); + + nsresult MakeAnonymousDiv(Element** aResult, PseudoStyleType aPseudoType, + nsTArray<ContentInfo>& aElements); + + // Helper function which reflows the anonymous div frames. + void ReflowAnonymousContent(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput); + + void DoUpdateThumbPosition(nsIFrame* aThumbFrame, const nsSize& aRangeSize); + + void DoUpdateRangeProgressFrame(nsIFrame* aProgressFrame, + const nsSize& aRangeSize); + + /** + * The div used to show the ::-moz-range-track pseudo-element. + * @see nsRangeFrame::CreateAnonymousContent + */ + nsCOMPtr<Element> mTrackDiv; + + /** + * The div used to show the ::-moz-range-progress pseudo-element, which is + * used to (optionally) style the specific chunk of track leading up to the + * thumb's current position. + * @see nsRangeFrame::CreateAnonymousContent + */ + nsCOMPtr<Element> mProgressDiv; + + /** + * The div used to show the ::-moz-range-thumb pseudo-element. + * @see nsRangeFrame::CreateAnonymousContent + */ + nsCOMPtr<Element> mThumbDiv; + + /** + * This mutation observer is used to invalidate paint when the @list changes, + * when a @list exists. + */ + RefPtr<mozilla::ListMutationObserver> mListMutationObserver; +}; + +#endif diff --git a/layout/forms/nsSearchControlFrame.cpp b/layout/forms/nsSearchControlFrame.cpp new file mode 100644 index 0000000000..e9249cca77 --- /dev/null +++ b/layout/forms/nsSearchControlFrame.cpp @@ -0,0 +1,82 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsSearchControlFrame.h" + +#include "HTMLInputElement.h" +#include "mozilla/PresShell.h" +#include "nsGkAtoms.h" +#include "nsNameSpaceManager.h" +#include "nsStyleConsts.h" +#include "nsContentUtils.h" +#include "nsContentCreatorFunctions.h" +#include "nsCSSPseudoElements.h" +#include "nsICSSDeclaration.h" + +#ifdef ACCESSIBILITY +# include "mozilla/a11y/AccTypes.h" +#endif + +using namespace mozilla; +using namespace mozilla::dom; + +nsIFrame* NS_NewSearchControlFrame(PresShell* aPresShell, + ComputedStyle* aStyle) { + return new (aPresShell) + nsSearchControlFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsSearchControlFrame) + +NS_QUERYFRAME_HEAD(nsSearchControlFrame) + NS_QUERYFRAME_ENTRY(nsSearchControlFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsTextControlFrame) + +nsSearchControlFrame::nsSearchControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsTextControlFrame(aStyle, aPresContext, kClassID) {} + +void nsSearchControlFrame::Destroy(DestroyContext& aContext) { + aContext.AddAnonymousContent(mClearButton.forget()); + nsTextControlFrame::Destroy(aContext); +} + +nsresult nsSearchControlFrame::CreateAnonymousContent( + nsTArray<ContentInfo>& aElements) { + // We create an anonymous tree for our input element that is structured as + // follows: + // + // input + // div - placeholder + // div - preview div + // div - editor root + // button - clear button + // + // If you change this, be careful to change the order of stuff in + // AppendAnonymousContentTo. + + nsTextControlFrame::CreateAnonymousContent(aElements); + + // FIXME: We could use nsTextControlFrame making the show password buttton + // code a bit more generic, or rename this frame and use it for password + // inputs. + // + // Create the ::-moz-search-clear-button pseudo-element: + mClearButton = MakeAnonElement(PseudoStyleType::mozSearchClearButton, nullptr, + nsGkAtoms::button); + + aElements.AppendElement(mClearButton); + + return NS_OK; +} + +void nsSearchControlFrame::AppendAnonymousContentTo( + nsTArray<nsIContent*>& aElements, uint32_t aFilter) { + nsTextControlFrame::AppendAnonymousContentTo(aElements, aFilter); + if (mClearButton) { + aElements.AppendElement(mClearButton); + } +} diff --git a/layout/forms/nsSearchControlFrame.h b/layout/forms/nsSearchControlFrame.h new file mode 100644 index 0000000000..0499fa84ea --- /dev/null +++ b/layout/forms/nsSearchControlFrame.h @@ -0,0 +1,68 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsSearchControlFrame_h__ +#define nsSearchControlFrame_h__ + +#include "mozilla/Attributes.h" +#include "nsTextControlFrame.h" +#include "nsIAnonymousContentCreator.h" +#include "mozilla/RefPtr.h" + +class nsITextControlFrame; +class nsPresContext; + +namespace mozilla { +enum class PseudoStyleType : uint8_t; +class PresShell; +namespace dom { +class Element; +} // namespace dom +} // namespace mozilla + +/** + * This frame type is used for <input type=search>. + */ +class nsSearchControlFrame final : public nsTextControlFrame { + friend nsIFrame* NS_NewSearchControlFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + using PseudoStyleType = mozilla::PseudoStyleType; + using Element = mozilla::dom::Element; + + explicit nsSearchControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext); + + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsSearchControlFrame) + + void Destroy(DestroyContext&) override; + + // nsIAnonymousContentCreator + nsresult CreateAnonymousContent(nsTArray<ContentInfo>& aElements) override; + void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) override; + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SearchControl"_ns, aResult); + } +#endif + + Element* GetAnonClearButton() const { return mClearButton; } + + /** + * Update visbility of the clear button depending on the value + */ + void UpdateClearButtonState(); + + private: + // See nsSearchControlFrame::CreateAnonymousContent of a description of these + RefPtr<Element> mClearButton; +}; + +#endif // nsSearchControlFrame_h__ diff --git a/layout/forms/nsSelectsAreaFrame.cpp b/layout/forms/nsSelectsAreaFrame.cpp new file mode 100644 index 0000000000..64fcdbd029 --- /dev/null +++ b/layout/forms/nsSelectsAreaFrame.cpp @@ -0,0 +1,191 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include "nsSelectsAreaFrame.h" + +#include "mozilla/PresShell.h" +#include "nsIContent.h" +#include "nsListControlFrame.h" +#include "nsDisplayList.h" +#include "WritingModes.h" + +using namespace mozilla; + +nsContainerFrame* NS_NewSelectsAreaFrame(PresShell* aShell, + ComputedStyle* aStyle, + nsFrameState aFlags) { + nsSelectsAreaFrame* it = + new (aShell) nsSelectsAreaFrame(aStyle, aShell->GetPresContext()); + + // We need NS_BLOCK_FLOAT_MGR to ensure that the options inside the select + // aren't expanded by right floats outside the select. + it->AddStateBits(aFlags | NS_BLOCK_FLOAT_MGR); + + return it; +} + +NS_IMPL_FRAMEARENA_HELPERS(nsSelectsAreaFrame) + +static nsListControlFrame* GetEnclosingListFrame(nsIFrame* aSelectsAreaFrame) { + nsIFrame* frame = aSelectsAreaFrame->GetParent(); + while (frame) { + if (frame->IsListControlFrame()) + return static_cast<nsListControlFrame*>(frame); + frame = frame->GetParent(); + } + return nullptr; +} + +void nsSelectsAreaFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + + nsListControlFrame* list = GetEnclosingListFrame(this); + NS_ASSERTION(list, + "Must have an nsListControlFrame! Frame constructor is " + "broken"); + + nsBlockFrame::Reflow(aPresContext, aDesiredSize, aReflowInput, aStatus); + + // Check whether we need to suppress scrollbar updates. We want to do + // that if we're in a possible first pass and our block size of a row + // has changed. + if (list->MightNeedSecondPass()) { + nscoord newBSizeOfARow = list->CalcBSizeOfARow(); + // We'll need a second pass if our block size of a row changed. For + // comboboxes, we'll also need it if our block size changed. If + // we're going to do a second pass, suppress scrollbar updates for + // this pass. + if (newBSizeOfARow != mBSizeOfARow) { + mBSizeOfARow = newBSizeOfARow; + list->SetSuppressScrollbarUpdate(true); + } + } +} + +namespace mozilla { +/** + * This wrapper class lets us redirect mouse hits from the child frame of + * an option element to the element's own frame. + * REVIEW: This is what nsSelectsAreaFrame::GetFrameForPoint used to do + */ +class nsDisplayOptionEventGrabber : public nsDisplayWrapList { + public: + nsDisplayOptionEventGrabber(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame, + nsDisplayItem* aItem) + : nsDisplayWrapList(aBuilder, aFrame, aItem) {} + nsDisplayOptionEventGrabber(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame, + nsDisplayList* aList) + : nsDisplayWrapList(aBuilder, aFrame, aList) {} + virtual void HitTest(nsDisplayListBuilder* aBuilder, const nsRect& aRect, + HitTestState* aState, + nsTArray<nsIFrame*>* aOutFrames) override; + virtual bool ShouldFlattenAway(nsDisplayListBuilder* aBuilder) override { + return false; + } + void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override { + GetChildren()->Paint(aBuilder, aCtx, + mFrame->PresContext()->AppUnitsPerDevPixel()); + } + NS_DISPLAY_DECL_NAME("OptionEventGrabber", TYPE_OPTION_EVENT_GRABBER) +}; + +void nsDisplayOptionEventGrabber::HitTest(nsDisplayListBuilder* aBuilder, + const nsRect& aRect, + HitTestState* aState, + nsTArray<nsIFrame*>* aOutFrames) { + nsTArray<nsIFrame*> outFrames; + mList.HitTest(aBuilder, aRect, aState, &outFrames); + + for (uint32_t i = 0; i < outFrames.Length(); i++) { + nsIFrame* selectedFrame = outFrames.ElementAt(i); + while (selectedFrame && + !(selectedFrame->GetContent() && + selectedFrame->GetContent()->IsHTMLElement(nsGkAtoms::option))) { + selectedFrame = selectedFrame->GetParent(); + } + if (selectedFrame) { + aOutFrames->AppendElement(selectedFrame); + } else { + // keep the original result, which could be this frame + aOutFrames->AppendElement(outFrames.ElementAt(i)); + } + } +} + +class nsOptionEventGrabberWrapper : public nsDisplayItemWrapper { + public: + nsOptionEventGrabberWrapper() = default; + virtual nsDisplayItem* WrapList(nsDisplayListBuilder* aBuilder, + nsIFrame* aFrame, + nsDisplayList* aList) override { + return MakeDisplayItem<nsDisplayOptionEventGrabber>(aBuilder, aFrame, + aList); + } + virtual nsDisplayItem* WrapItem(nsDisplayListBuilder* aBuilder, + nsDisplayItem* aItem) override { + return MakeDisplayItem<nsDisplayOptionEventGrabber>(aBuilder, + aItem->Frame(), aItem); + } +}; + +class nsDisplayListFocus : public nsPaintedDisplayItem { + public: + nsDisplayListFocus(nsDisplayListBuilder* aBuilder, nsSelectsAreaFrame* aFrame) + : nsPaintedDisplayItem(aBuilder, aFrame) { + MOZ_COUNT_CTOR(nsDisplayListFocus); + } + MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayListFocus) + + virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder, + bool* aSnap) const override { + *aSnap = false; + // override bounds because the list item focus ring may extend outside + // the nsSelectsAreaFrame + nsListControlFrame* listFrame = GetEnclosingListFrame(Frame()); + return listFrame->InkOverflowRectRelativeToSelf() + + listFrame->GetOffsetToCrossDoc(Frame()) + ToReferenceFrame(); + } + virtual void Paint(nsDisplayListBuilder* aBuilder, + gfxContext* aCtx) override { + nsListControlFrame* listFrame = GetEnclosingListFrame(Frame()); + // listFrame must be non-null or we wouldn't get called. + listFrame->PaintFocus( + aCtx->GetDrawTarget(), + listFrame->GetOffsetToCrossDoc(Frame()) + ToReferenceFrame()); + } + NS_DISPLAY_DECL_NAME("ListFocus", TYPE_LIST_FOCUS) +}; + +} // namespace mozilla + +void nsSelectsAreaFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + if (!aBuilder->IsForEventDelivery()) { + BuildDisplayListInternal(aBuilder, aLists); + return; + } + + nsDisplayListCollection set(aBuilder); + BuildDisplayListInternal(aBuilder, set); + + nsOptionEventGrabberWrapper wrapper; + wrapper.WrapLists(aBuilder, this, set, aLists); +} + +void nsSelectsAreaFrame::BuildDisplayListInternal( + nsDisplayListBuilder* aBuilder, const nsDisplayListSet& aLists) { + nsBlockFrame::BuildDisplayList(aBuilder, aLists); + + nsListControlFrame* listFrame = GetEnclosingListFrame(this); + if (listFrame && listFrame->IsFocused()) { + // we can't just associate the display item with the list frame, + // because then the list's scrollframe won't clip it (the scrollframe + // only clips contained descendants). + aLists.Outlines()->AppendNewToTop<nsDisplayListFocus>(aBuilder, this); + } +} diff --git a/layout/forms/nsSelectsAreaFrame.h b/layout/forms/nsSelectsAreaFrame.h new file mode 100644 index 0000000000..e0e487bad7 --- /dev/null +++ b/layout/forms/nsSelectsAreaFrame.h @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef nsSelectsAreaFrame_h___ +#define nsSelectsAreaFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsBlockFrame.h" + +namespace mozilla { +class PresShell; +} // namespace mozilla + +class nsSelectsAreaFrame final : public nsBlockFrame { + public: + NS_DECL_FRAMEARENA_HELPERS(nsSelectsAreaFrame) + + friend nsContainerFrame* NS_NewSelectsAreaFrame(mozilla::PresShell* aShell, + ComputedStyle* aStyle, + nsFrameState aFlags); + + virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + void BuildDisplayListInternal(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists); + + virtual void Reflow(nsPresContext* aCX, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + nscoord BSizeOfARow() const { return mBSizeOfARow; } + + protected: + explicit nsSelectsAreaFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsBlockFrame(aStyle, aPresContext, kClassID), + // initialize to wacky value so first call of + // nsSelectsAreaFrame::Reflow will always invalidate + mBSizeOfARow(nscoord_MIN) {} + + // We cache the block size of a single row so that changes to the + // "size" attribute, padding, etc. can all be handled with only one + // reflow. We'll have to reflow twice if someone changes our font + // size or something like that, so that the block size of our options + // will change. + nscoord mBSizeOfARow; +}; + +#endif /* nsSelectsAreaFrame_h___ */ diff --git a/layout/forms/nsTextControlFrame.cpp b/layout/forms/nsTextControlFrame.cpp new file mode 100644 index 0000000000..8513481fbf --- /dev/null +++ b/layout/forms/nsTextControlFrame.cpp @@ -0,0 +1,1392 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/DebugOnly.h" + +#include "gfxContext.h" +#include "nsCOMPtr.h" +#include "nsFontMetrics.h" +#include "nsTextControlFrame.h" +#include "nsIEditor.h" +#include "nsCaret.h" +#include "nsCSSPseudoElements.h" +#include "nsDisplayList.h" +#include "nsGenericHTMLElement.h" +#include "nsTextFragment.h" +#include "nsNameSpaceManager.h" + +#include "nsIContent.h" +#include "nsIScrollableFrame.h" +#include "nsPresContext.h" +#include "nsGkAtoms.h" +#include "nsLayoutUtils.h" + +#include <algorithm> +#include "nsRange.h" //for selection setting helper func +#include "nsINode.h" +#include "nsPIDOMWindow.h" //needed for notify selection changed to update the menus ect. +#include "nsQueryObject.h" +#include "nsILayoutHistoryState.h" + +#include "nsFocusManager.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/PresShell.h" +#include "mozilla/PresState.h" +#include "mozilla/TextEditor.h" +#include "nsAttrValueInlines.h" +#include "mozilla/dom/Selection.h" +#include "nsContentUtils.h" +#include "nsTextNode.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/dom/HTMLTextAreaElement.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/Text.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/StaticPrefs_layout.h" +#include "nsFrameSelection.h" + +#define DEFAULT_COLUMN_WIDTH 20 + +using namespace mozilla; +using namespace mozilla::dom; + +nsIFrame* NS_NewTextControlFrame(PresShell* aPresShell, ComputedStyle* aStyle) { + return new (aPresShell) + nsTextControlFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsTextControlFrame) + +NS_QUERYFRAME_HEAD(nsTextControlFrame) + NS_QUERYFRAME_ENTRY(nsTextControlFrame) + NS_QUERYFRAME_ENTRY(nsIFormControlFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) + NS_QUERYFRAME_ENTRY(nsITextControlFrame) + NS_QUERYFRAME_ENTRY(nsIStatefulFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) + +#ifdef ACCESSIBILITY +a11y::AccType nsTextControlFrame::AccessibleType() { + return a11y::eHTMLTextFieldType; +} +#endif + +#ifdef DEBUG +class EditorInitializerEntryTracker { + public: + explicit EditorInitializerEntryTracker(nsTextControlFrame& frame) + : mFrame(frame), mFirstEntry(false) { + if (!mFrame.mInEditorInitialization) { + mFrame.mInEditorInitialization = true; + mFirstEntry = true; + } + } + ~EditorInitializerEntryTracker() { + if (mFirstEntry) { + mFrame.mInEditorInitialization = false; + } + } + bool EnteredMoreThanOnce() const { return !mFirstEntry; } + + private: + nsTextControlFrame& mFrame; + bool mFirstEntry; +}; +#endif + +class nsTextControlFrame::nsAnonDivObserver final + : public nsStubMutationObserver { + public: + explicit nsAnonDivObserver(nsTextControlFrame& aFrame) : mFrame(aFrame) {} + NS_DECL_ISUPPORTS + NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED + + private: + ~nsAnonDivObserver() = default; + nsTextControlFrame& mFrame; +}; + +nsTextControlFrame::nsTextControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext, + nsIFrame::ClassID aClassID) + : nsContainerFrame(aStyle, aPresContext, aClassID) {} + +nsTextControlFrame::~nsTextControlFrame() = default; + +nsIScrollableFrame* nsTextControlFrame::GetScrollTargetFrame() const { + if (!mRootNode) { + return nullptr; + } + return do_QueryFrame(mRootNode->GetPrimaryFrame()); +} + +void nsTextControlFrame::Destroy(DestroyContext& aContext) { + RemoveProperty(TextControlInitializer()); + + // Unbind the text editor state object from the frame. The editor will live + // on, but things like controllers will be released. + RefPtr textControlElement = TextControlElement::FromNode(GetContent()); + MOZ_ASSERT(textControlElement); + if (mMutationObserver) { + textControlElement->UnbindFromFrame(this); + mRootNode->RemoveMutationObserver(mMutationObserver); + mMutationObserver = nullptr; + } + + // If there is a drag session, user may be dragging selection in removing + // text node in the text control. If so, we should set source node to the + // text control because another text node may be recreated soon if the text + // control is just reframed. + if (nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession()) { + if (dragSession->IsDraggingTextInTextControl() && mRootNode && + mRootNode->GetFirstChild()) { + nsCOMPtr<nsINode> sourceNode; + if (NS_SUCCEEDED( + dragSession->GetSourceNode(getter_AddRefs(sourceNode))) && + mRootNode->Contains(sourceNode)) { + MOZ_ASSERT(sourceNode->IsText()); + dragSession->UpdateSource(textControlElement, nullptr); + } + } + } + // Otherwise, EventStateManager may track gesture to start drag with native + // anonymous nodes in the text control element. + else if (textControlElement->GetPresContext(Element::eForComposedDoc)) { + textControlElement->GetPresContext(Element::eForComposedDoc) + ->EventStateManager() + ->TextControlRootWillBeRemoved(*textControlElement); + } + + // If we're a subclass like nsNumberControlFrame, then it owns the root of the + // anonymous subtree where mRootNode is. + aContext.AddAnonymousContent(mRootNode.forget()); + aContext.AddAnonymousContent(mPlaceholderDiv.forget()); + aContext.AddAnonymousContent(mPreviewDiv.forget()); + aContext.AddAnonymousContent(mRevealButton.forget()); + + nsContainerFrame::Destroy(aContext); +} + +LogicalSize nsTextControlFrame::CalcIntrinsicSize( + gfxContext* aRenderingContext, WritingMode aWM, + float aFontSizeInflation) const { + LogicalSize intrinsicSize(aWM); + RefPtr<nsFontMetrics> fontMet = + nsLayoutUtils::GetFontMetricsForFrame(this, aFontSizeInflation); + const nscoord lineHeight = + ReflowInput::CalcLineHeight(*Style(), PresContext(), GetContent(), + NS_UNCONSTRAINEDSIZE, aFontSizeInflation); + // Use the larger of the font's "average" char width or the width of the + // zero glyph (if present) as the basis for resolving the size attribute. + const nscoord charWidth = + std::max(fontMet->ZeroOrAveCharWidth(), fontMet->AveCharWidth()); + const nscoord charMaxAdvance = fontMet->MaxAdvance(); + + // Initialize based on the width in characters. + const int32_t cols = GetCols(); + intrinsicSize.ISize(aWM) = cols * charWidth; + + // If we do not have what appears to be a fixed-width font, add a "slop" + // amount based on the max advance of the font (clamped to twice charWidth, + // because some fonts have a few extremely-wide outliers that would result + // in excessive width here; e.g. the triple-emdash ligature in SFNS Text), + // minus 4px. This helps avoid input fields becoming unusably narrow with + // small size values. + if (charMaxAdvance - charWidth > AppUnitsPerCSSPixel()) { + nscoord internalPadding = + std::max(0, std::min(charMaxAdvance, charWidth * 2) - + nsPresContext::CSSPixelsToAppUnits(4)); + internalPadding = RoundToMultiple(internalPadding, AppUnitsPerCSSPixel()); + intrinsicSize.ISize(aWM) += internalPadding; + } else { + // This is to account for the anonymous <br> having a 1 twip width + // in Full Standards mode, see BRFrame::Reflow and bug 228752. + if (PresContext()->CompatibilityMode() == eCompatibility_FullStandards) { + intrinsicSize.ISize(aWM) += 1; + } + } + + // Increment width with cols * letter-spacing. + { + const StyleLength& letterSpacing = StyleText()->mLetterSpacing; + if (!letterSpacing.IsZero()) { + intrinsicSize.ISize(aWM) += cols * letterSpacing.ToAppUnits(); + } + } + + // Set the height equal to total number of rows (times the height of each + // line, of course) + intrinsicSize.BSize(aWM) = lineHeight * GetRows(); + + // Add in the size of the scrollbars for textarea + if (IsTextArea()) { + nsIScrollableFrame* scrollableFrame = GetScrollTargetFrame(); + NS_ASSERTION(scrollableFrame, "Child must be scrollable"); + if (scrollableFrame) { + LogicalMargin scrollbarSizes(aWM, + scrollableFrame->GetDesiredScrollbarSizes()); + intrinsicSize.ISize(aWM) += scrollbarSizes.IStartEnd(aWM); + intrinsicSize.BSize(aWM) += scrollbarSizes.BStartEnd(aWM); + } + } + return intrinsicSize; +} + +nsresult nsTextControlFrame::EnsureEditorInitialized() { + // This method initializes our editor, if needed. + + // This code used to be called from CreateAnonymousContent(), but + // when the editor set the initial string, it would trigger a + // PresShell listener which called FlushPendingNotifications() + // during frame construction. This was causing other form controls + // to display wrong values. Additionally, calling this every time + // a text frame control is instantiated means that we're effectively + // instantiating the editor for all text fields, even if they + // never get used. So, now this method is being called lazily only + // when we actually need an editor. + + if (mEditorHasBeenInitialized) return NS_OK; + + Document* doc = mContent->GetComposedDoc(); + NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE); + + AutoWeakFrame weakFrame(this); + + // Flush out content on our document. Have to do this, because script + // blockers don't prevent the sink flushing out content and notifying in the + // process, which can destroy frames. + doc->FlushPendingNotifications(FlushType::ContentAndNotify); + NS_ENSURE_TRUE(weakFrame.IsAlive(), NS_ERROR_FAILURE); + + // Make sure that editor init doesn't do things that would kill us off + // (especially off the script blockers it'll create for its DOM mutations). + { + RefPtr<TextControlElement> textControlElement = + TextControlElement::FromNode(GetContent()); + MOZ_ASSERT(textControlElement); + + // Hide selection changes during the initialization, as webpages should not + // be aware of these initializations + AutoHideSelectionChanges hideSelectionChanges( + textControlElement->GetConstFrameSelection()); + + nsAutoScriptBlocker scriptBlocker; + + // Time to mess with our security context... See comments in GetValue() + // for why this is needed. + mozilla::dom::AutoNoJSAPI nojsapi; + + // Make sure that we try to focus the content even if the method fails + class EnsureSetFocus { + public: + explicit EnsureSetFocus(nsTextControlFrame* aFrame) : mFrame(aFrame) {} + ~EnsureSetFocus() { + if (nsContentUtils::IsFocusedContent(mFrame->GetContent())) + mFrame->SetFocus(true, false); + } + + private: + nsTextControlFrame* mFrame; + }; + EnsureSetFocus makeSureSetFocusHappens(this); + +#ifdef DEBUG + // Make sure we are not being called again until we're finished. + // If reentrancy happens, just pretend that we don't have an editor. + const EditorInitializerEntryTracker tracker(*this); + NS_ASSERTION(!tracker.EnteredMoreThanOnce(), + "EnsureEditorInitialized has been called while a previous " + "call was in progress"); +#endif + + // Create an editor for the frame, if one doesn't already exist + nsresult rv = textControlElement->CreateEditor(); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_STATE(weakFrame.IsAlive()); + + // Set mEditorHasBeenInitialized so that subsequent calls will use the + // editor. + mEditorHasBeenInitialized = true; + + if (weakFrame.IsAlive()) { + uint32_t position = 0; + + // Set the selection to the end of the text field (bug 1287655), + // but only if the contents has changed (bug 1337392). + if (textControlElement->ValueChanged()) { + nsAutoString val; + textControlElement->GetTextEditorValue(val, true); + position = val.Length(); + } + + SetSelectionEndPoints(position, position); + } + } + NS_ENSURE_STATE(weakFrame.IsAlive()); + return NS_OK; +} + +already_AddRefed<Element> nsTextControlFrame::MakeAnonElement( + PseudoStyleType aPseudoType, Element* aParent, nsAtom* aTag) const { + MOZ_ASSERT(aPseudoType != PseudoStyleType::NotPseudo); + Document* doc = PresContext()->Document(); + RefPtr<Element> element = doc->CreateHTMLElement(aTag); + element->SetPseudoElementType(aPseudoType); + if (aPseudoType == PseudoStyleType::mozTextControlEditingRoot) { + // Make our root node editable + element->SetFlags(NODE_IS_EDITABLE); + } + + if (aPseudoType == PseudoStyleType::mozNumberSpinDown || + aPseudoType == PseudoStyleType::mozNumberSpinUp) { + element->SetAttr(kNameSpaceID_None, nsGkAtoms::aria_hidden, u"true"_ns, + false); + } + + if (aParent) { + aParent->AppendChildTo(element, false, IgnoreErrors()); + } + + return element.forget(); +} + +already_AddRefed<Element> nsTextControlFrame::MakeAnonDivWithTextNode( + PseudoStyleType aPseudoType) const { + RefPtr<Element> div = MakeAnonElement(aPseudoType); + + // Create the text node for the anonymous <div> element. + nsNodeInfoManager* nim = div->OwnerDoc()->NodeInfoManager(); + RefPtr<nsTextNode> textNode = new (nim) nsTextNode(nim); + // If the anonymous div element is not for the placeholder, we should + // mark the text node as "maybe modified frequently" for avoiding ASCII + // range checks at every input. + if (aPseudoType != PseudoStyleType::placeholder) { + textNode->MarkAsMaybeModifiedFrequently(); + // Additionally, this is a password field, the text node needs to be + // marked as "maybe masked" unless it's in placeholder. + if (IsPasswordTextControl()) { + textNode->MarkAsMaybeMasked(); + } + } + div->AppendChildTo(textNode, false, IgnoreErrors()); + return div.forget(); +} + +nsresult nsTextControlFrame::CreateAnonymousContent( + nsTArray<ContentInfo>& aElements) { + MOZ_ASSERT(!nsContentUtils::IsSafeToRunScript()); + MOZ_ASSERT(mContent, "We should have a content!"); + + AddStateBits(NS_FRAME_INDEPENDENT_SELECTION); + + RefPtr<TextControlElement> textControlElement = + TextControlElement::FromNode(GetContent()); + MOZ_ASSERT(textControlElement); + mRootNode = MakeAnonElement(PseudoStyleType::mozTextControlEditingRoot); + if (NS_WARN_IF(!mRootNode)) { + return NS_ERROR_FAILURE; + } + + mMutationObserver = new nsAnonDivObserver(*this); + mRootNode->AddMutationObserver(mMutationObserver); + + // Bind the frame to its text control. + // + // This can realistically fail in paginated mode, where we may replicate + // fixed-positioned elements and the replicated frame will not get the chance + // to get an editor. + nsresult rv = textControlElement->BindToFrame(this); + if (NS_WARN_IF(NS_FAILED(rv))) { + mRootNode->RemoveMutationObserver(mMutationObserver); + mMutationObserver = nullptr; + mRootNode = nullptr; + return rv; + } + + CreatePlaceholderIfNeeded(); + if (mPlaceholderDiv) { + aElements.AppendElement(mPlaceholderDiv); + } + CreatePreviewIfNeeded(); + if (mPreviewDiv) { + aElements.AppendElement(mPreviewDiv); + } + + // NOTE(emilio): We want the root node always after the placeholder so that + // background on the placeholder doesn't obscure the caret. + aElements.AppendElement(mRootNode); + + if (StaticPrefs::layout_forms_reveal_password_button_enabled() && + IsPasswordTextControl()) { + mRevealButton = + MakeAnonElement(PseudoStyleType::mozReveal, nullptr, nsGkAtoms::button); + mRevealButton->SetAttr(kNameSpaceID_None, nsGkAtoms::aria_hidden, + u"true"_ns, false); + mRevealButton->SetAttr(kNameSpaceID_None, nsGkAtoms::tabindex, u"-1"_ns, + false); + aElements.AppendElement(mRevealButton); + } + + rv = UpdateValueDisplay(false); + NS_ENSURE_SUCCESS(rv, rv); + + InitializeEagerlyIfNeeded(); + return NS_OK; +} + +bool nsTextControlFrame::ShouldInitializeEagerly() const { + // textareas are eagerly initialized. + if (!IsSingleLineTextControl()) { + return true; + } + + // Also, input elements which have a cached selection should get eager + // editor initialization. + TextControlElement* textControlElement = + TextControlElement::FromNode(GetContent()); + MOZ_ASSERT(textControlElement); + if (textControlElement->HasCachedSelection()) { + return true; + } + + // So do input text controls with spellcheck=true + if (auto* htmlElement = nsGenericHTMLElement::FromNode(mContent)) { + if (htmlElement->Spellcheck()) { + return true; + } + } + + // If text in the editor is being dragged, we need the editor to create + // new source node for the drag session (TextEditor creates the text node + // in the anonymous <div> element. + if (nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession()) { + if (dragSession->IsDraggingTextInTextControl()) { + nsCOMPtr<nsINode> sourceNode; + if (NS_SUCCEEDED( + dragSession->GetSourceNode(getter_AddRefs(sourceNode))) && + sourceNode == textControlElement) { + return true; + } + } + } + + return false; +} + +void nsTextControlFrame::InitializeEagerlyIfNeeded() { + MOZ_ASSERT(!nsContentUtils::IsSafeToRunScript(), + "Someone forgot a script blocker?"); + if (!ShouldInitializeEagerly()) { + return; + } + + EditorInitializer* initializer = new EditorInitializer(this); + SetProperty(TextControlInitializer(), initializer); + nsContentUtils::AddScriptRunner(initializer); +} + +void nsTextControlFrame::CreatePlaceholderIfNeeded() { + MOZ_ASSERT(!mPlaceholderDiv); + + // Do we need a placeholder node? + nsAutoString placeholder; + if (!mContent->AsElement()->GetAttr(nsGkAtoms::placeholder, placeholder)) { + return; + } + + mPlaceholderDiv = MakeAnonDivWithTextNode(PseudoStyleType::placeholder); + UpdatePlaceholderText(placeholder, false); +} + +void nsTextControlFrame::PlaceholderChanged(const nsAttrValue* aOld, + const nsAttrValue* aNew) { + if (!aOld || !aNew) { + return; // This should be handled by GetAttributeChangeHint. + } + + // If we've changed the attribute but we still haven't reframed, there's + // nothing to do either. + if (!mPlaceholderDiv) { + return; + } + + nsAutoString placeholder; + aNew->ToString(placeholder); + UpdatePlaceholderText(placeholder, true); +} + +void nsTextControlFrame::UpdatePlaceholderText(nsString& aPlaceholder, + bool aNotify) { + MOZ_DIAGNOSTIC_ASSERT(mPlaceholderDiv); + MOZ_DIAGNOSTIC_ASSERT(mPlaceholderDiv->GetFirstChild()); + + if (IsTextArea()) { // <textarea>s preserve newlines... + nsContentUtils::PlatformToDOMLineBreaks(aPlaceholder); + } else { // ...<input>s don't + nsContentUtils::RemoveNewlines(aPlaceholder); + } + + mPlaceholderDiv->GetFirstChild()->AsText()->SetText(aPlaceholder, aNotify); +} + +void nsTextControlFrame::CreatePreviewIfNeeded() { + RefPtr<TextControlElement> textControlElement = + TextControlElement::FromNode(GetContent()); + MOZ_ASSERT(textControlElement); + if (!textControlElement->IsPreviewEnabled()) { + return; + } + + mPreviewDiv = MakeAnonDivWithTextNode(PseudoStyleType::mozTextControlPreview); +} + +void nsTextControlFrame::AppendAnonymousContentTo( + nsTArray<nsIContent*>& aElements, uint32_t aFilter) { + if (mPlaceholderDiv && !(aFilter & nsIContent::eSkipPlaceholderContent)) { + aElements.AppendElement(mPlaceholderDiv); + } + + if (mPreviewDiv) { + aElements.AppendElement(mPreviewDiv); + } + + if (mRevealButton) { + aElements.AppendElement(mRevealButton); + } + + aElements.AppendElement(mRootNode); +} + +nscoord nsTextControlFrame::GetPrefISize(gfxContext* aRenderingContext) { + nscoord result = 0; + DISPLAY_PREF_INLINE_SIZE(this, result); + float inflation = nsLayoutUtils::FontSizeInflationFor(this); + WritingMode wm = GetWritingMode(); + result = CalcIntrinsicSize(aRenderingContext, wm, inflation).ISize(wm); + return result; +} + +nscoord nsTextControlFrame::GetMinISize(gfxContext* aRenderingContext) { + // Our min inline size is just our preferred width if we have auto inline size + nscoord result; + DISPLAY_MIN_INLINE_SIZE(this, result); + result = GetPrefISize(aRenderingContext); + return result; +} + +LogicalSize nsTextControlFrame::ComputeAutoSize( + gfxContext* aRenderingContext, WritingMode aWM, const LogicalSize& aCBSize, + nscoord aAvailableISize, const LogicalSize& aMargin, + const LogicalSize& aBorderPadding, const StyleSizeOverrides& aSizeOverrides, + ComputeSizeFlags aFlags) { + float inflation = nsLayoutUtils::FontSizeInflationFor(this); + LogicalSize autoSize = CalcIntrinsicSize(aRenderingContext, aWM, inflation); + + // Note: nsContainerFrame::ComputeAutoSize only computes the inline-size (and + // only for 'auto'), the block-size it returns is always NS_UNCONSTRAINEDSIZE. + const auto& styleISize = aSizeOverrides.mStyleISize + ? *aSizeOverrides.mStyleISize + : StylePosition()->ISize(aWM); + if (styleISize.IsAuto()) { + if (aFlags.contains(ComputeSizeFlag::IClampMarginBoxMinSize)) { + // CalcIntrinsicSize isn't aware of grid-item margin-box clamping, so we + // fall back to nsContainerFrame's ComputeAutoSize to handle that. + // XXX maybe a font-inflation issue here? (per the assertion below). + autoSize.ISize(aWM) = + nsContainerFrame::ComputeAutoSize( + aRenderingContext, aWM, aCBSize, aAvailableISize, aMargin, + aBorderPadding, aSizeOverrides, aFlags) + .ISize(aWM); + } +#ifdef DEBUG + else { + LogicalSize ancestorAutoSize = nsContainerFrame::ComputeAutoSize( + aRenderingContext, aWM, aCBSize, aAvailableISize, aMargin, + aBorderPadding, aSizeOverrides, aFlags); + MOZ_ASSERT(inflation != 1.0f || + ancestorAutoSize.ISize(aWM) == autoSize.ISize(aWM), + "Incorrect size computed by ComputeAutoSize?"); + } +#endif + } + return autoSize; +} + +Maybe<nscoord> nsTextControlFrame::ComputeBaseline( + const nsIFrame* aFrame, const ReflowInput& aReflowInput, + bool aForSingleLineControl) { + // If we're layout-contained, we have no baseline. + if (aReflowInput.mStyleDisplay->IsContainLayout()) { + return Nothing(); + } + WritingMode wm = aReflowInput.GetWritingMode(); + + nscoord lineHeight = aReflowInput.ComputedBSize(); + if (!aForSingleLineControl || lineHeight == NS_UNCONSTRAINEDSIZE) { + lineHeight = NS_CSS_MINMAX(aReflowInput.GetLineHeight(), + aReflowInput.ComputedMinBSize(), + aReflowInput.ComputedMaxBSize()); + } + RefPtr<nsFontMetrics> fontMet = + nsLayoutUtils::GetInflatedFontMetricsForFrame(aFrame); + return Some(nsLayoutUtils::GetCenteredFontBaseline(fontMet, lineHeight, + wm.IsLineInverted()) + + aReflowInput.ComputedLogicalBorderPadding(wm).BStart(wm)); +} + +static bool IsButtonBox(const nsIFrame* aFrame) { + auto pseudoType = aFrame->Style()->GetPseudoType(); + return pseudoType == PseudoStyleType::mozNumberSpinBox || + pseudoType == PseudoStyleType::mozSearchClearButton || + pseudoType == PseudoStyleType::mozReveal; +} + +void nsTextControlFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MarkInReflow(); + DO_GLOBAL_REFLOW_COUNT("nsTextControlFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + + // set values of reflow's out parameters + WritingMode wm = aReflowInput.GetWritingMode(); + aDesiredSize.SetSize(wm, aReflowInput.ComputedSizeWithBorderPadding(wm)); + + { + // Calculate the baseline and store it in mFirstBaseline. + auto baseline = + ComputeBaseline(this, aReflowInput, IsSingleLineTextControl()); + mFirstBaseline = baseline.valueOr(NS_INTRINSIC_ISIZE_UNKNOWN); + if (baseline) { + aDesiredSize.SetBlockStartAscent(*baseline); + } + } + + // overflow handling + aDesiredSize.SetOverflowAreasToDesiredBounds(); + + nsIFrame* buttonBox = [&]() -> nsIFrame* { + nsIFrame* last = mFrames.LastChild(); + if (!last || !IsButtonBox(last)) { + return nullptr; + } + return last; + }(); + + // Reflow the button box first, so that we can use its size for the other + // frames. + nscoord buttonBoxISize = 0; + if (buttonBox) { + ReflowTextControlChild(buttonBox, aPresContext, aReflowInput, aStatus, + aDesiredSize, buttonBoxISize); + } + + // perform reflow on all kids + nsIFrame* kid = mFrames.FirstChild(); + while (kid) { + if (kid != buttonBox) { + MOZ_ASSERT(!IsButtonBox(kid), + "Should only have one button box, and should be last"); + ReflowTextControlChild(kid, aPresContext, aReflowInput, aStatus, + aDesiredSize, buttonBoxISize); + } + kid = kid->GetNextSibling(); + } + + // take into account css properties that affect overflow handling + FinishAndStoreOverflow(&aDesiredSize); + + aStatus.Reset(); // This type of frame can't be split. +} + +void nsTextControlFrame::ReflowTextControlChild( + nsIFrame* aKid, nsPresContext* aPresContext, + const ReflowInput& aReflowInput, nsReflowStatus& aStatus, + ReflowOutput& aParentDesiredSize, nscoord& aButtonBoxISize) { + const WritingMode outerWM = aReflowInput.GetWritingMode(); + // compute available size and frame offsets for child + const WritingMode wm = aKid->GetWritingMode(); + LogicalSize availSize = aReflowInput.ComputedSizeWithPadding(wm); + availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE; + + bool isButtonBox = IsButtonBox(aKid); + + ReflowInput kidReflowInput(aPresContext, aReflowInput, aKid, availSize, + Nothing(), ReflowInput::InitFlag::CallerWillInit); + + // Override padding with our computed padding in case we got it from theming + // or percentage, if we're not the button box. + auto overridePadding = + isButtonBox ? Nothing() : Some(aReflowInput.ComputedLogicalPadding(wm)); + if (!isButtonBox && aButtonBoxISize) { + // Button box respects inline-end-padding, so we don't need to. + overridePadding->IEnd(outerWM) = 0; + } + + // We want to let our button box fill the frame in the block axis, up to the + // edge of the control's border. So, we use the control's padding-box as the + // containing block size for our button box. + auto overrideCBSize = + isButtonBox ? Some(aReflowInput.ComputedSizeWithPadding(wm)) : Nothing(); + kidReflowInput.Init(aPresContext, overrideCBSize, Nothing(), overridePadding); + + LogicalPoint position(wm); + if (!isButtonBox) { + MOZ_ASSERT(wm == outerWM, + "Shouldn't have to care about orthogonal " + "writing-modes and such inside the control, " + "except for the number spin-box which forces " + "horizontal-tb"); + + const auto& border = aReflowInput.ComputedLogicalBorder(wm); + + // Offset the frame by the size of the parent's border. Note that we don't + // have to account for the parent's padding here, because this child + // actually "inherits" that padding and manages it on behalf of the parent. + position.B(wm) = border.BStart(wm); + position.I(wm) = border.IStart(wm); + + // Set computed width and computed height for the child (the button box is + // the only exception, which has an auto size). + kidReflowInput.SetComputedISize( + std::max(0, aReflowInput.ComputedISize() - aButtonBoxISize)); + kidReflowInput.SetComputedBSize(aReflowInput.ComputedBSize()); + } + + // reflow the child + ReflowOutput desiredSize(aReflowInput); + const nsSize containerSize = + aReflowInput.ComputedSizeWithBorderPadding(outerWM).GetPhysicalSize( + outerWM); + ReflowChild(aKid, aPresContext, desiredSize, kidReflowInput, wm, position, + containerSize, ReflowChildFlags::Default, aStatus); + + if (isButtonBox) { + const auto& bp = aReflowInput.ComputedLogicalBorderPadding(outerWM); + auto size = desiredSize.Size(outerWM); + // Center button in the block axis of our content box. We do this + // computation in terms of outerWM for simplicity. + LogicalRect buttonRect(outerWM); + buttonRect.BSize(outerWM) = size.BSize(outerWM); + buttonRect.ISize(outerWM) = size.ISize(outerWM); + buttonRect.BStart(outerWM) = + bp.BStart(outerWM) + + (aReflowInput.ComputedBSize() - size.BSize(outerWM)) / 2; + // Align to the inline-end of the content box. + buttonRect.IStart(outerWM) = + bp.IStart(outerWM) + aReflowInput.ComputedISize() - size.ISize(outerWM); + buttonRect = buttonRect.ConvertTo(wm, outerWM, containerSize); + position = buttonRect.Origin(wm); + aButtonBoxISize = size.ISize(outerWM); + } + + // place the child + FinishReflowChild(aKid, aPresContext, desiredSize, &kidReflowInput, wm, + position, containerSize, ReflowChildFlags::Default); + + // consider the overflow + aParentDesiredSize.mOverflowAreas.UnionWith(desiredSize.mOverflowAreas); +} + +// IMPLEMENTING NS_IFORMCONTROLFRAME +void nsTextControlFrame::SetFocus(bool aOn, bool aRepaint) { + TextControlElement* textControlElement = + TextControlElement::FromNode(GetContent()); + MOZ_ASSERT(textControlElement); + + // If 'dom.placeholeder.show_on_focus' preference is 'false', focusing or + // blurring the frame can have an impact on the placeholder visibility. + if (!aOn) { + return; + } + + nsISelectionController* selCon = textControlElement->GetSelectionController(); + if (!selCon) { + return; + } + + RefPtr<Selection> ourSel = + selCon->GetSelection(nsISelectionController::SELECTION_NORMAL); + if (!ourSel) { + return; + } + + mozilla::PresShell* presShell = PresContext()->GetPresShell(); + RefPtr<nsCaret> caret = presShell->GetCaret(); + if (!caret) { + return; + } + + // Tell the caret to use our selection + caret->SetSelection(ourSel); + + // mutual-exclusion: the selection is either controlled by the + // document or by the text input/area. Clear any selection in the + // document since the focus is now on our independent selection. + + RefPtr<Selection> docSel = + presShell->GetSelection(nsISelectionController::SELECTION_NORMAL); + if (!docSel) { + return; + } + + if (!docSel->IsCollapsed()) { + docSel->RemoveAllRanges(IgnoreErrors()); + } + + // If the focus moved to a text control during text selection by pointer + // device, stop extending the selection. + if (RefPtr<nsFrameSelection> frameSelection = presShell->FrameSelection()) { + frameSelection->SetDragState(false); + } +} + +nsresult nsTextControlFrame::SetFormProperty(nsAtom* aName, + const nsAString& aValue) { + if (!mIsProcessing) { // some kind of lock. + mIsProcessing = true; + if (nsGkAtoms::select == aName) { + // Select all the text. + // + // XXX: This is lame, we can't call editor's SelectAll method + // because that triggers AutoCopies in unix builds. + // Instead, we have to call our own homegrown version + // of select all which merely builds a range that selects + // all of the content and adds that to the selection. + + AutoWeakFrame weakThis = this; + SelectAllOrCollapseToEndOfText(true); // NOTE: can destroy the world + if (!weakThis.IsAlive()) { + return NS_OK; + } + } + mIsProcessing = false; + } + return NS_OK; +} + +already_AddRefed<TextEditor> nsTextControlFrame::GetTextEditor() { + if (NS_WARN_IF(NS_FAILED(EnsureEditorInitialized()))) { + return nullptr; + } + + RefPtr<TextControlElement> textControlElement = + TextControlElement::FromNode(GetContent()); + MOZ_ASSERT(textControlElement); + RefPtr<TextEditor> textEditor = textControlElement->GetTextEditor(); + return textEditor.forget(); +} + +nsresult nsTextControlFrame::SetSelectionInternal( + nsINode* aStartNode, uint32_t aStartOffset, nsINode* aEndNode, + uint32_t aEndOffset, SelectionDirection aDirection) { + // Get the selection, clear it and add the new range to it! + TextControlElement* textControlElement = + TextControlElement::FromNode(GetContent()); + MOZ_ASSERT(textControlElement); + nsISelectionController* selCon = textControlElement->GetSelectionController(); + NS_ENSURE_TRUE(selCon, NS_ERROR_FAILURE); + + RefPtr<Selection> selection = + selCon->GetSelection(nsISelectionController::SELECTION_NORMAL); + NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE); + + nsDirection direction; + if (aDirection == SelectionDirection::None) { + // Preserve the direction + direction = selection->GetDirection(); + } else { + direction = + (aDirection == SelectionDirection::Backward) ? eDirPrevious : eDirNext; + } + + MOZ_TRY(selection->SetStartAndEndInLimiter(*aStartNode, aStartOffset, + *aEndNode, aEndOffset, direction, + nsISelectionListener::JS_REASON)); + return NS_OK; +} + +void nsTextControlFrame::ScrollSelectionIntoViewAsync( + ScrollAncestors aScrollAncestors) { + auto* textControlElement = TextControlElement::FromNode(GetContent()); + MOZ_ASSERT(textControlElement); + nsISelectionController* selCon = textControlElement->GetSelectionController(); + if (!selCon) { + return; + } + + int16_t flags = aScrollAncestors == ScrollAncestors::Yes + ? 0 + : nsISelectionController::SCROLL_FIRST_ANCESTOR_ONLY; + + // Scroll the selection into view (see bug 231389). + selCon->ScrollSelectionIntoView( + nsISelectionController::SELECTION_NORMAL, + nsISelectionController::SELECTION_FOCUS_REGION, flags); +} + +nsresult nsTextControlFrame::SelectAllOrCollapseToEndOfText(bool aSelect) { + nsresult rv = EnsureEditorInitialized(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + RefPtr<nsINode> rootNode = mRootNode; + NS_ENSURE_TRUE(rootNode, NS_ERROR_FAILURE); + + RefPtr<Text> text = Text::FromNodeOrNull(rootNode->GetFirstChild()); + MOZ_ASSERT(text); + + uint32_t length = text->Length(); + + rv = SetSelectionInternal(text, aSelect ? 0 : length, text, length); + NS_ENSURE_SUCCESS(rv, rv); + + ScrollSelectionIntoViewAsync(); + return NS_OK; +} + +nsresult nsTextControlFrame::SetSelectionEndPoints( + uint32_t aSelStart, uint32_t aSelEnd, + nsITextControlFrame::SelectionDirection aDirection) { + NS_ASSERTION(aSelStart <= aSelEnd, "Invalid selection offsets!"); + + if (aSelStart > aSelEnd) return NS_ERROR_FAILURE; + + nsCOMPtr<nsINode> startNode, endNode; + uint32_t startOffset, endOffset; + + // Calculate the selection start point. + + nsresult rv = + OffsetToDOMPoint(aSelStart, getter_AddRefs(startNode), &startOffset); + + NS_ENSURE_SUCCESS(rv, rv); + + if (aSelStart == aSelEnd) { + // Collapsed selection, so start and end are the same! + endNode = startNode; + endOffset = startOffset; + } else { + // Selection isn't collapsed so we have to calculate + // the end point too. + + rv = OffsetToDOMPoint(aSelEnd, getter_AddRefs(endNode), &endOffset); + + NS_ENSURE_SUCCESS(rv, rv); + } + + return SetSelectionInternal(startNode, startOffset, endNode, endOffset, + aDirection); +} + +NS_IMETHODIMP +nsTextControlFrame::SetSelectionRange( + uint32_t aSelStart, uint32_t aSelEnd, + nsITextControlFrame::SelectionDirection aDirection) { + nsresult rv = EnsureEditorInitialized(); + NS_ENSURE_SUCCESS(rv, rv); + + if (aSelStart > aSelEnd) { + // Simulate what we'd see SetSelectionStart() was called, followed + // by a SetSelectionEnd(). + + aSelStart = aSelEnd; + } + + return SetSelectionEndPoints(aSelStart, aSelEnd, aDirection); +} + +nsresult nsTextControlFrame::OffsetToDOMPoint(uint32_t aOffset, + nsINode** aResult, + uint32_t* aPosition) { + NS_ENSURE_ARG_POINTER(aResult && aPosition); + + *aResult = nullptr; + *aPosition = 0; + + nsresult rv = EnsureEditorInitialized(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + RefPtr<Element> rootNode = mRootNode; + NS_ENSURE_TRUE(rootNode, NS_ERROR_FAILURE); + + nsCOMPtr<nsINodeList> nodeList = rootNode->ChildNodes(); + uint32_t length = nodeList->Length(); + + NS_ASSERTION(length <= 2, + "We should have one text node and one mozBR at most"); + + nsCOMPtr<nsINode> firstNode = nodeList->Item(0); + Text* textNode = firstNode ? firstNode->GetAsText() : nullptr; + + if (length == 0) { + rootNode.forget(aResult); + *aPosition = 0; + } else if (textNode) { + uint32_t textLength = textNode->Length(); + firstNode.forget(aResult); + *aPosition = std::min(aOffset, textLength); + } else { + rootNode.forget(aResult); + *aPosition = 0; + } + + return NS_OK; +} + +/////END INTERFACE IMPLEMENTATIONS + +////NSIFRAME +nsresult nsTextControlFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType) { + auto* textControlElement = TextControlElement::FromNode(GetContent()); + MOZ_ASSERT(textControlElement); + nsISelectionController* selCon = textControlElement->GetSelectionController(); + const bool needEditor = + nsGkAtoms::maxlength == aAttribute || nsGkAtoms::readonly == aAttribute || + nsGkAtoms::disabled == aAttribute || nsGkAtoms::spellcheck == aAttribute; + RefPtr<TextEditor> textEditor = needEditor ? GetTextEditor() : nullptr; + if ((needEditor && !textEditor) || !selCon) { + return nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, + aModType); + } + + if (nsGkAtoms::maxlength == aAttribute) { + if (textEditor) { + textEditor->SetMaxTextLength(textControlElement->UsedMaxLength()); + } + return NS_OK; + } + + if (nsGkAtoms::readonly == aAttribute || nsGkAtoms::disabled == aAttribute) { + if (AttributeExists(aAttribute)) { + if (nsContentUtils::IsFocusedContent(mContent)) { + selCon->SetCaretEnabled(false); + } + textEditor->AddFlags(nsIEditor::eEditorReadonlyMask); + } else { + if (!AttributeExists(aAttribute == nsGkAtoms::readonly + ? nsGkAtoms::disabled + : nsGkAtoms::readonly)) { + if (nsContentUtils::IsFocusedContent(mContent)) { + selCon->SetCaretEnabled(true); + } + textEditor->RemoveFlags(nsIEditor::eEditorReadonlyMask); + } + } + return NS_OK; + } + + if (!mEditorHasBeenInitialized && nsGkAtoms::value == aAttribute) { + UpdateValueDisplay(true); + return NS_OK; + } + + // Allow the base class to handle common attributes supported by all form + // elements... + return nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType); +} + +void nsTextControlFrame::GetText(nsString& aText) { + if (HTMLInputElement* inputElement = HTMLInputElement::FromNode(mContent)) { + if (IsSingleLineTextControl()) { + // There will be no line breaks so we can ignore the wrap property. + inputElement->GetTextEditorValue(aText, true); + return; + } + aText.Truncate(); + return; + } + + MOZ_ASSERT(!IsSingleLineTextControl()); + if (HTMLTextAreaElement* textAreaElement = + HTMLTextAreaElement::FromNode(mContent)) { + textAreaElement->GetValue(aText); + return; + } + + MOZ_ASSERT(aText.IsEmpty()); + aText.Truncate(); +} + +bool nsTextControlFrame::TextEquals(const nsAString& aText) const { + if (HTMLInputElement* inputElement = HTMLInputElement::FromNode(mContent)) { + if (IsSingleLineTextControl()) { + // There will be no line breaks so we can ignore the wrap property. + return inputElement->TextEditorValueEquals(aText); + } + return aText.IsEmpty(); + } + + MOZ_ASSERT(!IsSingleLineTextControl()); + if (HTMLTextAreaElement* textAreaElement = + HTMLTextAreaElement::FromNode(mContent)) { + return textAreaElement->ValueEquals(aText); + } + + return aText.IsEmpty(); +} + +/// END NSIFRAME OVERLOADS + +// NOTE(emilio): This is needed because the root->primary frame map is not set +// up by the time this is called. +static nsIFrame* FindRootNodeFrame(const nsFrameList& aChildList, + const nsIContent* aRoot) { + for (nsIFrame* f : aChildList) { + if (f->GetContent() == aRoot) { + return f; + } + if (nsIFrame* root = FindRootNodeFrame(f->PrincipalChildList(), aRoot)) { + return root; + } + } + return nullptr; +} + +void nsTextControlFrame::SetInitialChildList(ChildListID aListID, + nsFrameList&& aChildList) { + nsContainerFrame::SetInitialChildList(aListID, std::move(aChildList)); + if (aListID != FrameChildListID::Principal) { + return; + } + + // Mark the scroll frame as being a reflow root. This will allow incremental + // reflows to be initiated at the scroll frame, rather than descending from + // the root frame of the frame hierarchy. + if (nsIFrame* frame = FindRootNodeFrame(PrincipalChildList(), mRootNode)) { + frame->AddStateBits(NS_FRAME_REFLOW_ROOT); + + auto* textControlElement = TextControlElement::FromNode(GetContent()); + MOZ_ASSERT(textControlElement); + textControlElement->InitializeKeyboardEventListeners(); + + bool hasProperty; + nsPoint contentScrollPos = TakeProperty(ContentScrollPos(), &hasProperty); + if (hasProperty) { + // If we have a scroll pos stored to be passed to our anonymous + // div, do it here! + nsIStatefulFrame* statefulFrame = do_QueryFrame(frame); + NS_ASSERTION(statefulFrame, + "unexpected type of frame for the anonymous div"); + UniquePtr<PresState> fakePresState = NewPresState(); + fakePresState->scrollState() = contentScrollPos; + statefulFrame->RestoreState(fakePresState.get()); + } + } else { + MOZ_ASSERT(!mRootNode || PrincipalChildList().IsEmpty()); + } +} + +nsresult nsTextControlFrame::UpdateValueDisplay(bool aNotify, + bool aBeforeEditorInit, + const nsAString* aValue) { + if (!IsSingleLineTextControl()) { // textareas don't use this + return NS_OK; + } + + MOZ_ASSERT(mRootNode, "Must have a div content\n"); + MOZ_ASSERT(!mEditorHasBeenInitialized, + "Do not call this after editor has been initialized"); + + nsIContent* childContent = mRootNode->GetFirstChild(); + Text* textContent; + if (!childContent) { + // Set up a textnode with our value + RefPtr<nsTextNode> textNode = new (mContent->NodeInfo()->NodeInfoManager()) + nsTextNode(mContent->NodeInfo()->NodeInfoManager()); + textNode->MarkAsMaybeModifiedFrequently(); + if (IsPasswordTextControl()) { + textNode->MarkAsMaybeMasked(); + } + mRootNode->AppendChildTo(textNode, aNotify, IgnoreErrors()); + textContent = textNode; + } else { + textContent = childContent->GetAsText(); + } + + NS_ENSURE_TRUE(textContent, NS_ERROR_UNEXPECTED); + + TextControlElement* textControlElement = + TextControlElement::FromNode(GetContent()); + MOZ_ASSERT(textControlElement); + + // Get the current value of the textfield from the content. + nsAutoString value; + if (aValue) { + value = *aValue; + } else { + textControlElement->GetTextEditorValue(value, true); + } + + return textContent->SetText(value, aNotify); +} + +NS_IMETHODIMP +nsTextControlFrame::GetOwnedSelectionController( + nsISelectionController** aSelCon) { + NS_ENSURE_ARG_POINTER(aSelCon); + + TextControlElement* textControlElement = + TextControlElement::FromNode(GetContent()); + MOZ_ASSERT(textControlElement); + + *aSelCon = textControlElement->GetSelectionController(); + NS_IF_ADDREF(*aSelCon); + + return NS_OK; +} + +nsFrameSelection* nsTextControlFrame::GetOwnedFrameSelection() { + auto* textControlElement = TextControlElement::FromNode(GetContent()); + MOZ_ASSERT(textControlElement); + return textControlElement->GetConstFrameSelection(); +} + +UniquePtr<PresState> nsTextControlFrame::SaveState() { + if (nsIStatefulFrame* scrollStateFrame = + do_QueryFrame(GetScrollTargetFrame())) { + return scrollStateFrame->SaveState(); + } + + return nullptr; +} + +NS_IMETHODIMP +nsTextControlFrame::RestoreState(PresState* aState) { + NS_ENSURE_ARG_POINTER(aState); + + // Query the nsIStatefulFrame from the HTMLScrollFrame + if (nsIStatefulFrame* scrollStateFrame = + do_QueryFrame(GetScrollTargetFrame())) { + return scrollStateFrame->RestoreState(aState); + } + + // Most likely, we don't have our anonymous content constructed yet, which + // would cause us to end up here. In this case, we'll just store the scroll + // pos ourselves, and forward it to the scroll frame later when it's created. + SetProperty(ContentScrollPos(), aState->scrollState()); + return NS_OK; +} + +nsresult nsTextControlFrame::PeekOffset(PeekOffsetStruct* aPos) { + return NS_ERROR_FAILURE; +} + +void nsTextControlFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + DO_GLOBAL_REFLOW_COUNT_DSP("nsTextControlFrame"); + + DisplayBorderBackgroundOutline(aBuilder, aLists); + + // Redirect all lists to the Content list so that nothing can escape, ie + // opacity creating stacking contexts that then get sorted with stacking + // contexts external to us. + nsDisplayList* content = aLists.Content(); + nsDisplayListSet set(content, content, content, content, content, content); + + for (auto* kid : mFrames) { + BuildDisplayListForChild(aBuilder, kid, set); + } +} + +NS_IMETHODIMP +nsTextControlFrame::EditorInitializer::Run() { + if (!mFrame) { + return NS_OK; + } + + // Need to block script to avoid bug 669767. + nsAutoScriptBlocker scriptBlocker; + + RefPtr<mozilla::PresShell> presShell = mFrame->PresShell(); + bool observes = presShell->ObservesNativeAnonMutationsForPrint(); + presShell->ObserveNativeAnonMutationsForPrint(true); + // This can cause the frame to be destroyed (and call Revoke()). + mFrame->EnsureEditorInitialized(); + presShell->ObserveNativeAnonMutationsForPrint(observes); + + // The frame can *still* be destroyed even though we have a scriptblocker, + // bug 682684. + if (!mFrame) { + return NS_ERROR_FAILURE; + } + + // If there is a drag session which is for dragging text in a text control + // and its source node is the text control element, we're being reframed. + // In this case we should restore the source node of the drag session to + // new text node because it's required for dispatching `dragend` event. + if (nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession()) { + if (dragSession->IsDraggingTextInTextControl()) { + nsCOMPtr<nsINode> sourceNode; + if (NS_SUCCEEDED( + dragSession->GetSourceNode(getter_AddRefs(sourceNode))) && + mFrame->GetContent() == sourceNode) { + if (TextControlElement* textControlElement = + TextControlElement::FromNode(mFrame->GetContent())) { + if (TextEditor* textEditor = + textControlElement->GetTextEditorWithoutCreation()) { + if (Element* anonymousDivElement = textEditor->GetRoot()) { + if (anonymousDivElement && anonymousDivElement->GetFirstChild()) { + MOZ_ASSERT(anonymousDivElement->GetFirstChild()->IsText()); + dragSession->UpdateSource(anonymousDivElement->GetFirstChild(), + textEditor->GetSelection()); + } + } + } + } + } + } + } + // Otherwise, EventStateManager may be tracking gesture to start a drag. + else if (TextControlElement* textControlElement = + TextControlElement::FromNode(mFrame->GetContent())) { + if (nsPresContext* presContext = + textControlElement->GetPresContext(Element::eForComposedDoc)) { + if (TextEditor* textEditor = + textControlElement->GetTextEditorWithoutCreation()) { + if (Element* anonymousDivElement = textEditor->GetRoot()) { + presContext->EventStateManager()->TextControlRootAdded( + *anonymousDivElement, *textControlElement); + } + } + } + } + + mFrame->FinishedInitializer(); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(nsTextControlFrame::nsAnonDivObserver, nsIMutationObserver) + +void nsTextControlFrame::nsAnonDivObserver::CharacterDataChanged( + nsIContent* aContent, const CharacterDataChangeInfo&) { + mFrame.ClearCachedValue(); +} + +void nsTextControlFrame::nsAnonDivObserver::ContentAppended( + nsIContent* aFirstNewContent) { + mFrame.ClearCachedValue(); +} + +void nsTextControlFrame::nsAnonDivObserver::ContentInserted( + nsIContent* aChild) { + mFrame.ClearCachedValue(); +} + +void nsTextControlFrame::nsAnonDivObserver::ContentRemoved( + nsIContent* aChild, nsIContent* aPreviousSibling) { + mFrame.ClearCachedValue(); +} diff --git a/layout/forms/nsTextControlFrame.h b/layout/forms/nsTextControlFrame.h new file mode 100644 index 0000000000..f880454563 --- /dev/null +++ b/layout/forms/nsTextControlFrame.h @@ -0,0 +1,366 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsTextControlFrame_h___ +#define nsTextControlFrame_h___ + +#include "mozilla/Attributes.h" +#include "mozilla/TextControlElement.h" +#include "nsContainerFrame.h" +#include "nsIAnonymousContentCreator.h" +#include "nsIContent.h" +#include "nsITextControlFrame.h" +#include "nsIStatefulFrame.h" + +class nsISelectionController; +class EditorInitializerEntryTracker; +namespace mozilla { +class AutoTextControlHandlingState; +class TextEditor; +class TextControlState; +enum class PseudoStyleType : uint8_t; +namespace dom { +class Element; +} // namespace dom +} // namespace mozilla + +class nsTextControlFrame : public nsContainerFrame, + public nsIAnonymousContentCreator, + public nsITextControlFrame, + public nsIStatefulFrame { + using Element = mozilla::dom::Element; + + public: + NS_DECL_FRAMEARENA_HELPERS(nsTextControlFrame) + + NS_DECLARE_FRAME_PROPERTY_SMALL_VALUE(ContentScrollPos, nsPoint) + + protected: + nsTextControlFrame(ComputedStyle*, nsPresContext*, nsIFrame::ClassID); + + public: + explicit nsTextControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsTextControlFrame(aStyle, aPresContext, kClassID) {} + + virtual ~nsTextControlFrame(); + + /** + * Destroy() causes preparing to destroy editor and that may cause running + * selection listeners of spellchecker selection and document state listeners. + * Not sure whether the former does something or not, but nobody should run + * content script. The latter is currently only FinderHighlighter to clean up + * its fields at destruction. Thus, the latter won't run content script too. + * Therefore, this won't run unsafe script. + */ + MOZ_CAN_RUN_SCRIPT_BOUNDARY void Destroy(DestroyContext&) override; + + nsIScrollableFrame* GetScrollTargetFrame() const override; + + nscoord GetMinISize(gfxContext* aRenderingContext) override; + nscoord GetPrefISize(gfxContext* aRenderingContext) override; + + mozilla::LogicalSize ComputeAutoSize( + gfxContext* aRenderingContext, mozilla::WritingMode aWM, + const mozilla::LogicalSize& aCBSize, nscoord aAvailableISize, + const mozilla::LogicalSize& aMargin, + const mozilla::LogicalSize& aBorderPadding, + const mozilla::StyleSizeOverrides& aSizeOverrides, + mozilla::ComputeSizeFlags aFlags) override; + + void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + Maybe<nscoord> GetNaturalBaselineBOffset( + mozilla::WritingMode aWM, BaselineSharingGroup aBaselineGroup, + BaselineExportContext) const override { + if (!IsSingleLineTextControl()) { + return Nothing{}; + } + NS_ASSERTION(!IsSubtreeDirty(), "frame must not be dirty"); + return GetSingleLineTextControlBaseline(this, mFirstBaseline, aWM, + aBaselineGroup); + } + + static Maybe<nscoord> GetSingleLineTextControlBaseline( + const nsIFrame* aFrame, nscoord aFirstBaseline, mozilla::WritingMode aWM, + BaselineSharingGroup aBaselineGroup) { + if (aFrame->StyleDisplay()->IsContainLayout()) { + return Nothing{}; + } + NS_ASSERTION(aFirstBaseline != NS_INTRINSIC_ISIZE_UNKNOWN, + "please call Reflow before asking for the baseline"); + return mozilla::Some(aBaselineGroup == BaselineSharingGroup::First + ? aFirstBaseline + : aFrame->BSize(aWM) - aFirstBaseline); + } + +#ifdef ACCESSIBILITY + mozilla::a11y::AccType AccessibleType() override; +#endif + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + aResult.AssignLiteral("nsTextControlFrame"); + return NS_OK; + } +#endif + + bool IsFrameOfType(uint32_t aFlags) const override { + return nsContainerFrame::IsFrameOfType( + aFlags & ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock)); + } + + // nsIAnonymousContentCreator + nsresult CreateAnonymousContent(nsTArray<ContentInfo>& aElements) override; + void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) override; + + void SetInitialChildList(ChildListID, nsFrameList&&) override; + + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + //==== BEGIN NSIFORMCONTROLFRAME + MOZ_CAN_RUN_SCRIPT_BOUNDARY void SetFocus(bool aOn, bool aRepaint) override; + MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult + SetFormProperty(nsAtom* aName, const nsAString& aValue) override; + + //==== END NSIFORMCONTROLFRAME + + //==== NSITEXTCONTROLFRAME + + MOZ_CAN_RUN_SCRIPT_BOUNDARY already_AddRefed<mozilla::TextEditor> + GetTextEditor() override; + MOZ_CAN_RUN_SCRIPT NS_IMETHOD + SetSelectionRange(uint32_t aSelectionStart, uint32_t aSelectionEnd, + SelectionDirection = SelectionDirection::None) override; + NS_IMETHOD GetOwnedSelectionController( + nsISelectionController** aSelCon) override; + nsFrameSelection* GetOwnedFrameSelection() override; + + void PlaceholderChanged(const nsAttrValue* aOld, const nsAttrValue* aNew); + + /** + * Ensure mEditor is initialized with the proper flags and the default value. + * @throws NS_ERROR_NOT_INITIALIZED if mEditor has not been created + * @throws various and sundry other things + */ + MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult EnsureEditorInitialized() override; + + //==== END NSITEXTCONTROLFRAME + + //==== NSISTATEFULFRAME + + mozilla::UniquePtr<mozilla::PresState> SaveState() override; + NS_IMETHOD RestoreState(mozilla::PresState* aState) override; + + //=== END NSISTATEFULFRAME + + //==== OVERLOAD of nsIFrame + + /** handler for attribute changes to mContent */ + MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult AttributeChanged( + int32_t aNameSpaceID, nsAtom* aAttribute, int32_t aModType) override; + + void GetText(nsString& aText); + + /** + * TextEquals() is designed for internal use so that aValue shouldn't + * include \r character. It should be handled before calling this with + * nsContentUtils::PlatformToDOMLineBreaks(). + */ + bool TextEquals(const nsAString& aText) const; + + nsresult PeekOffset(mozilla::PeekOffsetStruct* aPos) override; + + NS_DECL_QUERYFRAME + + // Whether we should scroll only the current selection into view in the inner + // scroller, or also ancestors as needed. + enum class ScrollAncestors { No, Yes }; + void ScrollSelectionIntoViewAsync(ScrollAncestors = ScrollAncestors::No); + + protected: + /** + * Launch the reflow on the child frames - see nsTextControlFrame::Reflow() + */ + void ReflowTextControlChild(nsIFrame* aFrame, nsPresContext* aPresContext, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus, + ReflowOutput& aParentDesiredSize, + nscoord& aButtonBoxISize); + + public: + static Maybe<nscoord> ComputeBaseline(const nsIFrame*, const ReflowInput&, + bool aForSingleLineControl); + + Element* GetRootNode() const { return mRootNode; } + + Element* GetPreviewNode() const { return mPreviewDiv; } + + Element* GetPlaceholderNode() const { return mPlaceholderDiv; } + + Element* GetRevealButton() const { return mRevealButton; } + + // called by the focus listener + nsresult MaybeBeginSecureKeyboardInput(); + void MaybeEndSecureKeyboardInput(); + +#define DEFINE_TEXTCTRL_CONST_FORWARDER(type, name) \ + type name() const { \ + mozilla::TextControlElement* textControlElement = \ + mozilla::TextControlElement::FromNode(GetContent()); \ + return textControlElement->name(); \ + } + + DEFINE_TEXTCTRL_CONST_FORWARDER(bool, IsSingleLineTextControl) + DEFINE_TEXTCTRL_CONST_FORWARDER(bool, IsTextArea) + DEFINE_TEXTCTRL_CONST_FORWARDER(bool, IsPasswordTextControl) + DEFINE_TEXTCTRL_CONST_FORWARDER(int32_t, GetCols) + DEFINE_TEXTCTRL_CONST_FORWARDER(int32_t, GetRows) + +#undef DEFINE_TEXTCTRL_CONST_FORWARDER + + protected: + class EditorInitializer; + friend class EditorInitializer; + friend class mozilla::AutoTextControlHandlingState; // needs access to + // CacheValue + friend class mozilla::TextControlState; // needs access to UpdateValueDisplay + + // Temp reference to scriptrunner + NS_DECLARE_FRAME_PROPERTY_WITH_DTOR(TextControlInitializer, EditorInitializer, + nsTextControlFrame::RevokeInitializer) + + static void RevokeInitializer(EditorInitializer* aInitializer) { + aInitializer->Revoke(); + }; + + class EditorInitializer : public mozilla::Runnable { + public: + explicit EditorInitializer(nsTextControlFrame* aFrame) + : mozilla::Runnable("nsTextControlFrame::EditorInitializer"), + mFrame(aFrame) {} + + NS_IMETHOD Run() override; + + // avoids use of AutoWeakFrame + void Revoke() { mFrame = nullptr; } + + private: + nsTextControlFrame* mFrame; + }; + + nsresult OffsetToDOMPoint(uint32_t aOffset, nsINode** aResult, + uint32_t* aPosition); + + /** + * Update the textnode under our anonymous div to show the new + * value. This should only be called when we have no editor yet. + * @throws NS_ERROR_UNEXPECTED if the div has no text content + */ + nsresult UpdateValueDisplay(bool aNotify, bool aBeforeEditorInit = false, + const nsAString* aValue = nullptr); + + /** + * Find out whether an attribute exists on the content or not. + * @param aAtt the attribute to determine the existence of + * @returns false if it does not exist + */ + bool AttributeExists(nsAtom* aAtt) const { + return mContent && mContent->AsElement()->HasAttr(kNameSpaceID_None, aAtt); + } + + /** + * We call this when we are being destroyed or removed from the PFM. + * @param aPresContext the current pres context + */ + void PreDestroy(); + + // Compute our intrinsic size. This does not include any borders, paddings, + // etc. Just the size of our actual area for the text (and the scrollbars, + // for <textarea>). + mozilla::LogicalSize CalcIntrinsicSize(gfxContext* aRenderingContext, + mozilla::WritingMode aWM, + float aFontSizeInflation) const; + + private: + // helper methods + MOZ_CAN_RUN_SCRIPT nsresult SetSelectionInternal( + nsINode* aStartNode, uint32_t aStartOffset, nsINode* aEndNode, + uint32_t aEndOffset, SelectionDirection = SelectionDirection::None); + MOZ_CAN_RUN_SCRIPT nsresult SelectAllOrCollapseToEndOfText(bool aSelect); + MOZ_CAN_RUN_SCRIPT nsresult + SetSelectionEndPoints(uint32_t aSelStart, uint32_t aSelEnd, + SelectionDirection = SelectionDirection::None); + + void FinishedInitializer() { RemoveProperty(TextControlInitializer()); } + + const nsAString& CachedValue() const { return mCachedValue; } + + void ClearCachedValue() { mCachedValue.SetIsVoid(true); } + + void CacheValue(const nsAString& aValue) { mCachedValue.Assign(aValue); } + + [[nodiscard]] bool CacheValue(const nsAString& aValue, + const mozilla::fallible_t& aFallible) { + if (!mCachedValue.Assign(aValue, aFallible)) { + ClearCachedValue(); + return false; + } + return true; + } + + protected: + class nsAnonDivObserver; + + nsresult CreateRootNode(); + void CreatePlaceholderIfNeeded(); + void UpdatePlaceholderText(nsString&, bool aNotify); + void CreatePreviewIfNeeded(); + already_AddRefed<Element> MakeAnonElement( + mozilla::PseudoStyleType, Element* aParent = nullptr, + nsAtom* aTag = nsGkAtoms::div) const; + already_AddRefed<Element> MakeAnonDivWithTextNode( + mozilla::PseudoStyleType) const; + + bool ShouldInitializeEagerly() const; + void InitializeEagerlyIfNeeded(); + + RefPtr<Element> mRootNode; + RefPtr<Element> mPlaceholderDiv; + RefPtr<Element> mPreviewDiv; + // The Reveal Password button. Only used for type=password, nullptr + // otherwise. + RefPtr<Element> mRevealButton; + RefPtr<nsAnonDivObserver> mMutationObserver; + // Cache of the |.value| of <input> or <textarea> element without hard-wrap. + // If its IsVoid() returns true, it doesn't cache |.value|. + // Otherwise, it's cached when setting specific value or getting value from + // TextEditor. Additionally, when contents in the anonymous <div> element + // is modified, this is cleared. + // + // FIXME(bug 1402545): Consider using an nsAutoString here. + nsString mCachedValue{VoidString()}; + + // Our first baseline, or NS_INTRINSIC_ISIZE_UNKNOWN if we have a pending + // Reflow (or if we're contain:layout, which means we have no baseline). + nscoord mFirstBaseline = NS_INTRINSIC_ISIZE_UNKNOWN; + + // these packed bools could instead use the high order bits on mState, saving + // 4 bytes + bool mEditorHasBeenInitialized = false; + bool mIsProcessing = false; + +#ifdef DEBUG + bool mInEditorInitialization = false; + friend class EditorInitializerEntryTracker; +#endif +}; + +#endif diff --git a/layout/forms/test/bug287446_subframe.html b/layout/forms/test/bug287446_subframe.html new file mode 100644 index 0000000000..51fd701e3b --- /dev/null +++ b/layout/forms/test/bug287446_subframe.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<html> + <head> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script> + function doIs(arg1, arg2, arg3) { + window.parent.postMessage("t " + encodeURIComponent(arg1) + " " + + encodeURIComponent(arg2) + " " + + encodeURIComponent(arg3), "*"); + } + + function $(arg) { return document.getElementById(arg); } + + window.addEventListener("message", + function(evt) { + var t = $("target"); + if (evt.data == "start") { + doIs(t.value, "Test", "Shouldn't have lost our initial value"); + t.focus(); + sendString("Foo"); + doIs(t.value, "FooTest", "Typing should work"); + window.parent.postMessage("c", "*"); + } else { + doIs(evt.data, "continue", "Unexpected message"); + doIs(t.value, "FooTest", "Shouldn't have lost our typed value"); + sendString("Bar"); + doIs(t.value, "FooBarTest", "Typing should still work"); + window.parent.postMessage("f", "*"); + } + }, + "false"); + + </script> + </head> + <body> + <input id="target" value="Test"> + </body> +</html> diff --git a/layout/forms/test/bug477700_subframe.html b/layout/forms/test/bug477700_subframe.html new file mode 100644 index 0000000000..c0398fb2ad --- /dev/null +++ b/layout/forms/test/bug477700_subframe.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<html> + <head> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script> + function doIs(arg1, arg2, arg3) { + window.parent.postMessage("t " + encodeURIComponent(arg1) + " " + + encodeURIComponent(arg2) + " " + + encodeURIComponent(arg3), "*"); + } + + function $(arg) { return document.getElementById(arg); } + + window.addEventListener("message", + function(evt) { + doIs(evt.data, "start", "Unexpected message"); + $("target").focus(); + sendString("Test"); + var t = $("target"); + doIs(t.value, "Test", "Typing should work"); + (function() { + SpecialPowers.wrap(t).editor.undo(); + })() + doIs(t.value, "", "Undo should work"); + (function() { + SpecialPowers.wrap(t).editor.redo(); + })() + doIs(t.value, "Test", "Redo should work"); + window.parent.postMessage("f", "*"); + }, + "false"); + + </script> + </head> + <body> + <input id="target"> + </body> +</html> + diff --git a/layout/forms/test/bug536567_iframe.html b/layout/forms/test/bug536567_iframe.html new file mode 100644 index 0000000000..b2b2ca60ac --- /dev/null +++ b/layout/forms/test/bug536567_iframe.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML>
+<html>
+ <head>
+ </head>
+ <body>
+ <iframe id="content"></iframe>
+ </body>
+</html>
+
diff --git a/layout/forms/test/bug536567_subframe.html b/layout/forms/test/bug536567_subframe.html new file mode 100644 index 0000000000..c1271becf0 --- /dev/null +++ b/layout/forms/test/bug536567_subframe.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> +<body> +<input id="target" type="file" /> +<script type="text/javascript"> + +window.onload = function() { + var fileInput = document.getElementById("target"); + fileInput.click(); +}; + +</script> +</body> +</html> diff --git a/layout/forms/test/bug564115_window.html b/layout/forms/test/bug564115_window.html new file mode 100644 index 0000000000..b55cc0400d --- /dev/null +++ b/layout/forms/test/bug564115_window.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Window for Bug 564115</title> +</head> +<body> +<input type="text"> +<div style="height: 10000px;"></div> +</body> +</html> diff --git a/layout/forms/test/chrome.ini b/layout/forms/test/chrome.ini new file mode 100644 index 0000000000..cab43b716c --- /dev/null +++ b/layout/forms/test/chrome.ini @@ -0,0 +1,7 @@ +[DEFAULT] +skip-if = os == 'android' +support-files = + bug536567_iframe.html + bug536567_subframe.html + +[test_bug536567_perwindowpb.html] diff --git a/layout/forms/test/mochitest.ini b/layout/forms/test/mochitest.ini new file mode 100644 index 0000000000..ed03daa32e --- /dev/null +++ b/layout/forms/test/mochitest.ini @@ -0,0 +1,68 @@ +[DEFAULT] +support-files = + bug287446_subframe.html + bug477700_subframe.html + bug564115_window.html + +[test_bug231389.html] +[test_bug287446.html] +skip-if = + http3 +[test_bug345267.html] +[test_bug346043.html] +[test_bug348236.html] +skip-if = true # mac(select form control popup behavior is different) +[test_bug353539.html] +[test_bug365410.html] +[test_bug378670.html] +[test_bug402198.html] +[test_bug411236.html] +[test_bug446663.html] +[test_bug476308.html] +[test_bug477531.html] +[test_bug477700.html] +skip-if = + http3 +[test_bug534785.html] +[test_bug542914.html] +[test_bug549170.html] +[test_bug562447.html] +[test_bug563642.html] +[test_bug564115.html] +skip-if = toolkit == 'android' #TIMED_OUT +[test_bug571352.html] +skip-if = toolkit == 'android' #TIMED_OUT +[test_bug572406.html] +[test_bug572649.html] +skip-if = toolkit == 'android' # Bug 1635771 +[test_bug595310.html] +[test_bug620936.html] +[test_bug644542.html] +[test_bug672810.html] +[test_bug704049.html] +[test_bug717878_input_scroll.html] +[test_bug869314.html] +[test_bug903715.html] +skip-if = true +[test_bug935876.html] +[test_bug957562.html] +[test_bug960277.html] +[test_select_key_navigation_bug961363.html] +[test_bug1111995.html] +[test_bug1301290.html] +skip-if = toolkit == 'android' +[test_bug1305282.html] +[test_listcontrol_search.html] +[test_select_prevent_default.html] +[test_select_reframe.html] +[test_select_vertical.html] +skip-if = true # Bug 1170129, # <select> elements don't use an in-page popup on Android +[test_textarea_resize.html] +skip-if = toolkit == 'android' +[test_bug1327129.html] +[test_bug1529036.html] +[test_readonly.html] +[test_select_key_navigation_bug1498769.html] +[test_select_collapsed_page_keys.html] +skip-if = os == 'mac' # select control keyboard behavior is different +[test_unstyled_control_height.html] diff --git a/layout/forms/test/test_bug1111995.html b/layout/forms/test/test_bug1111995.html new file mode 100644 index 0000000000..c164dc23f7 --- /dev/null +++ b/layout/forms/test/test_bug1111995.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1111995 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1111995</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1111995 **/ + + function doTest() { + SimpleTest.waitForExplicitFinish(); + + var clicks = 0; + var elms = document.querySelectorAll('.click'); + for (var i = 0; i < elms.length; ++i) { + var e = elms[i]; + e.addEventListener('click', function(event) { + ++clicks; + }); + } + + for (var i = 0; i < elms.length; ++i) { + var e = elms[i]; + synthesizeMouse(e, 3, 3, {}); + } + is(clicks, 0, "click events outside border with radius"); + + clicks = 0; + synthesizeMouse($("t3"), 17, 17, {}); + synthesizeMouse($("t4"), 17, 17, {}); + is(clicks, 2, "click events on border with radius"); + + SimpleTest.finish(); + } + </script> +</head> +<body onload="SimpleTest.waitForFocus(doTest, window)"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1111995">Mozilla Bug 1111995</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<input class="click" id="t1" type=button style="width:100px; height:100px; padding:20px; border-radius:50%" value="Round button"> +<button class="click" id="t2" style="width:100px; height:100px; padding:20px; border-radius:50%">Round button</button> +<input class="click" id="t3" type=button style="width:100px; height:100px; border-width:20px; border-radius:50%" value="Round button"> +<button class="click" id="t4" style="width:100px; height:100px; border-width:20px; border-radius:50%">Round button</button> +<input class="click" id="t5" type=button style="width:100px; height:100px; border-radius:50%;overflow:hidden" value="Round button"> +<button class="click" id="t6" style="width:100px; height:100px; border-radius:50%;overflow:hidden">Round button</button> + +</body> +</html> diff --git a/layout/forms/test/test_bug1301290.html b/layout/forms/test/test_bug1301290.html new file mode 100644 index 0000000000..6b3fc0df14 --- /dev/null +++ b/layout/forms/test/test_bug1301290.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<html> + <head> + <title>Test for Bug 1301290</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style type="text/css"> + .blue, .green { + border: none; + box-sizing: border-box; + display: block; + width: 200px; + height: 100px; + overflow: scroll; + resize: both; + } + + .blue { + background: blue; + } + + .green { + background: green; + margin-top: -100px; + } + </style> + </head> + <body> + <div class="blue"></div> + <textarea class="green" id="textarea"></textarea> + <script type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + addLoadEvent(() => SimpleTest.executeSoon(function() { + var textarea = $("textarea"); + var rect = textarea.getBoundingClientRect(); + + synthesizeMouse(textarea, rect.width - 9, rect.height - 9, { type: "mousedown" }); + synthesizeMouse(textarea, rect.width + 40, rect.height + 40, { type: "mousemove" }); + synthesizeMouse(textarea, rect.width + 40, rect.height + 40, { type: "mouseup" }); + + var newrect = textarea.getBoundingClientRect(); + ok(newrect.width > rect.width, "width did not increase"); + ok(newrect.height > rect.height, "height did not increase"); + SimpleTest.finish(); + })); + </script> + </body> +</html> diff --git a/layout/forms/test/test_bug1305282.html b/layout/forms/test/test_bug1305282.html new file mode 100644 index 0000000000..bd631f3444 --- /dev/null +++ b/layout/forms/test/test_bug1305282.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=615697 +--> +<head> + <title>Test for Bug 1305282</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1305282">Mozilla Bug 1305282</a> +<p id="display"></p> +<div id="content"> + <select> + <option>f o o</option> + <option>b a r</option> + <option>b o o</option> + </select> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 1305282 **/ + +var select = document.getElementsByTagName('select')[0]; + +select.addEventListener("change", function(aEvent) { + is(select.selectedIndex, 1, "'b a r' option is selected"); + SimpleTest.finish(); +}, {once: true}); + +select.addEventListener("focus", function() { + SimpleTest.executeSoon(function () { + synthesizeKey("KEY_ArrowDown"); + SimpleTest.executeSoon(function () { + sendString("b"); + SimpleTest.executeSoon(function () { + sendString(" "); + SimpleTest.executeSoon(function () { + synthesizeKey("KEY_Enter"); + }); + }); + }); + }); +}, {once: true}); + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + select.focus(); +}); + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug1327129.html b/layout/forms/test/test_bug1327129.html new file mode 100644 index 0000000000..0e52f140b3 --- /dev/null +++ b/layout/forms/test/test_bug1327129.html @@ -0,0 +1,385 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=935876 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1327129</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1327129">Mozilla Bug 1327129</a> +<p id="display"></p> +<div> +<select size="3" firstNonDisabledIndex="0"> + <option>1</option> + <option>2</option> + <option>3</option> + <option>4</option> + <option>5</option> +</select> +<select size="3" firstNonDisabledIndex="1"> + <option disabled>1</option> + <option>2</option> + <option>3</option> + <option>4</option> + <option>5</option> +</select> +<select size="3" firstNonDisabledIndex="0"> + <optgroup><option>1</option></optgroup> + <option>2</option> + <option>3</option> + <option>4</option> + <option>5</option> +</select> +<select size="3" firstNonDisabledIndex="1"> + <option disabled>1</option> + <optgroup><option>2</option></optgroup> + <option>3</option> + <option>4</option> + <option>5</option> +</select> +<select size="3" firstNonDisabledIndex="2"> + <option disabled>1</option> + <optgroup><option disabled>2</option></optgroup> + <option>3</option> + <option>4</option> + <option>5</option> +</select> +<select size="3" firstNonDisabledIndex="-1"> + <option disabled>1</option> + <optgroup><option disabled>2</option></optgroup> + <option disabled>3</option> + <option disabled>4</option> + <option disabled>5</option> +</select> +<select size="3" multiple firstNonDisabledIndex="0"> + <option>1</option> + <option>2</option> + <option>3</option> + <option>4</option> + <option>5</option> +</select> +<select size="3" multiple firstNonDisabledIndex="0"> + <option>1</option> + <option>2</option> + <option>3</option> + <option>4</option> + <option>5</option> +</select> +<select size="3" multiple firstNonDisabledIndex="0"> + <optgroup><option>1</option></optgroup> + <option>2</option> + <option>3</option> + <option>4</option> + <option>5</option> +</select> +<select size="3" multiple firstNonDisabledIndex="1"> + <option disabled>1</option> + <option>2</option> + <option>3</option> + <option>4</option> + <option>5</option> +</select> +<select size="3" multiple firstNonDisabledIndex="3"> + <option disabled>1</option> + <optgroup><option disabled>2</option></optgroup> + <option disabled>3</option> + <option>4</option> + <option>5</option> +</select> +<select size="3" multiple firstNonDisabledIndex="-1"> + <option disabled>1</option> + <optgroup><option disabled>2</option></optgroup> + <option disabled>3</option> + <option disabled>4</option> + <option disabled>5</option> +</select> +<select size="1" firstNonDisabledIndex="0"> + <option>1</option> + <option>2</option> + <option>3</option> + <option>4</option> + <option>5</option> +</select> +<select size="1" firstNonDisabledIndex="0"> + <optgroup><option>1</option></optgroup> + <option>2</option> + <option>3</option> + <option>4</option> + <option>5</option> +</select> +<select size="1" firstNonDisabledIndex="1"> + <optgroup disabled><option>1</option></optgroup> + <option>2</option> + <option>3</option> + <option>4</option> + <option>5</option> +</select> +<select size="1" firstNonDisabledIndex="1"> + <option disabled>1</option> + <option>2</option> + <option>3</option> + <option>4</option> + <option>5</option> +</select> +<select size="1" firstNonDisabledIndex="1"> + <option disabled>1</option> + <optgroup><option>2</option></optgroup> + <option>3</option> + <option>4</option> + <option>5</option> +</select> +<select size="1" firstNonDisabledIndex="2"> + <option disabled>1</option> + <optgroup><option disabled>2</option></optgroup> + <option>3</option> + <option>4</option> + <option>5</option> +</select> +<select size="1" firstNonDisabledIndex="3"> + <option disabled>1</option> + <optgroup><option disabled>2</option></optgroup> + <option disabled>3</option> + <option>4</option> + <option>5</option> +</select> +<select size="1" firstNonDisabledIndex="-1"> + <option disabled>1</option> + <optgroup><option disabled>2</option></optgroup> + <option disabled>3</option> + <option disabled>4</option> + <option disabled>5</option> +</select> +</div> +<pre id="test"> +</pre> + +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +const kIsMac = navigator.platform.indexOf("Mac") == 0; + +function runTests() +{ + const all = Array.from(document.querySelectorAll('select')); + let i = 0; + all.forEach((elem) => { + elem.selectedIndex = -1; + ++i; + if (!elem.id) + elem.id = "element " + i; + }); + + // + // Test DOWN key on a <select> with no selected options. + // + const listboxes = Array.from(document.querySelectorAll('select[size="3"]')); + listboxes.forEach((elem) => { + elem.focus(); + synthesizeKey("KEY_ArrowDown"); + is(""+elem.selectedIndex, + elem.getAttribute('firstNonDisabledIndex'), + elem.id + ": DOWN selected first non-disabled option"); + }); + + const comboboxes = Array.from(document.querySelectorAll('select[size="1"]')); + // Mac shows the drop-down menu for DOWN, so skip this test there. + if (!kIsMac) { + comboboxes.forEach((elem) => { + elem.focus(); + synthesizeKey("KEY_ArrowDown"); + is(""+elem.selectedIndex, + elem.getAttribute('firstNonDisabledIndex'), + elem.id + ": DOWN selected first non-disabled option"); + }); + } + + all.forEach((elem) => { + elem.selectedIndex = -1; + elem.blur(); + }); + + // + // Test SHIFT+DOWN on a <select> with no selected options. + // + listboxes.forEach((elem) => { + elem.focus(); + synthesizeKey("KEY_ArrowDown", {shiftKey:true}); + is(""+elem.selectedIndex, + elem.getAttribute('firstNonDisabledIndex'), + elem.id + ": SHIFT+DOWN selected first non-disabled option"); + }); + + // Mac shows the drop-down menu for SHIFT+DOWN, so skip this test there. + if (!kIsMac) { + comboboxes.forEach((elem) => { + elem.focus(); + synthesizeKey("KEY_ArrowDown", {shiftKey:true}); + is(""+elem.selectedIndex, + elem.getAttribute('firstNonDisabledIndex'), + elem.id + ": SHIFT+DOWN selected first non-disabled option"); + }); + } + + all.forEach((elem) => { + elem.selectedIndex = -1; + elem.blur(); + }); + + // + // Test CTRL+DOWN on a <select> with no selected options. + // + listboxes.forEach((elem) => { + elem.focus(); + synthesizeKey("KEY_ArrowDown", {ctrlKey:true}); + if (!elem.multiple) + is(""+elem.selectedIndex, + elem.getAttribute('firstNonDisabledIndex'), + elem.id + ": CTRL+DOWN selected first non-disabled option"); + else + is(elem.selectedIndex, -1, elem.id + ": CTRL+DOWN did NOT select first any option"); + }); + + // Mac shows the drop-down menu for CTRL+DOWN, so skip this test there. + if (!kIsMac) { + comboboxes.forEach((elem) => { + elem.focus(); + synthesizeKey("KEY_ArrowDown", {ctrlKey:true}); + is(""+elem.selectedIndex, + elem.getAttribute('firstNonDisabledIndex'), + elem.id + ": CTRL+DOWN selected first non-disabled option"); + }); + } + + all.forEach((elem) => { + elem.selectedIndex = -1; + elem.blur(); + }); + + // + // Test SPACE on a <select> with no selected options. + // + listboxes.forEach((elem) => { + elem.focus(); + sendString(" "); + is(""+elem.selectedIndex, + elem.getAttribute('firstNonDisabledIndex'), + elem.id + ": SPACE selected first non-disabled option"); + }); + + // All platforms shows the drop-down menu for SPACE so skip testing that + // on the comboboxes. + + all.forEach((elem) => { + elem.selectedIndex = -1; + elem.blur(); + }); + + // + // Test CTRL+SPACE on a <select> with no selected options. + // + listboxes.forEach((elem) => { + elem.focus(); + synthesizeKey(" ", {ctrlKey:true}); + is(""+elem.selectedIndex, + elem.getAttribute('firstNonDisabledIndex'), + elem.id + ": CTRL+SPACE selected first non-disabled option"); + }); + + // non-Mac shows the drop-down menu for CTRL+SPACE, so skip this test there. + if (kIsMac) { + comboboxes.forEach((elem) => { + elem.focus(); + synthesizeKey(" ", {ctrlKey:true}); + is(""+elem.selectedIndex, + elem.getAttribute('firstNonDisabledIndex'), + elem.id + ": CTRL+SPACE selected first non-disabled option"); + }); + } + + all.forEach((elem) => { + elem.selectedIndex = -1; + elem.blur(); + }); + + // + // Test SHIFT+SPACE on a <select> with no selected options. + // + listboxes.forEach((elem) => { + elem.focus(); + synthesizeKey(" ", {shiftKey:true}); + is(""+elem.selectedIndex, + elem.getAttribute('firstNonDisabledIndex'), + elem.id + ": SHIFT+SPACE selected first non-disabled option"); + }); + + // All platforms shows the drop-down menu for SHIFT+SPACE so skip testing that + // on the comboboxes. + + all.forEach((elem) => { + elem.selectedIndex = -1; + elem.blur(); + }); + + // + // Test CTRL+SHIFT+DOWN on a <select> with no selected options. + // + listboxes.forEach((elem) => { + elem.focus(); + synthesizeKey("KEY_ArrowDown", {ctrlKey:true, shiftKey:true}); + is(""+elem.selectedIndex, + elem.getAttribute('firstNonDisabledIndex'), + elem.id + ": CTRL+SHIFT+DOWN selected first non-disabled option"); + }); + + // Mac shows the drop-down menu for CTRL+SHIFT+DOWN, so skip this test there. + if (!kIsMac) { + comboboxes.forEach((elem) => { + elem.focus(); + synthesizeKey("KEY_ArrowDown", {ctrlKey:true, shiftKey:true}); + is(""+elem.selectedIndex, + elem.getAttribute('firstNonDisabledIndex'), + elem.id + ": CTRL+SHIFT+DOWN selected first non-disabled option"); + }); + } + + all.forEach((elem) => { + elem.selectedIndex = -1; + elem.blur(); + }); + + // + // Test CTRL+SHIFT+SPACE on a <select> with no selected options. + // + listboxes.forEach((elem) => { + elem.focus(); + synthesizeKey(" ", {ctrlKey:true, shiftKey:true}); + is(""+elem.selectedIndex, + elem.getAttribute('firstNonDisabledIndex'), + elem.id + ": CTRL+SHIFT+SPACE selected first non-disabled option"); + }); + + // non-Mac shows the drop-down menu for CTRL+SHIFT+SPACE, so skip this test there. + if (kIsMac) { + comboboxes.forEach((elem) => { + elem.focus(); + synthesizeKey(" ", {ctrlKey:true, shiftKey:true}); + is(""+elem.selectedIndex, + elem.getAttribute('firstNonDisabledIndex'), + elem.id + ": CTRL+SHIFT+SPACE selected first non-disabled option"); + }); + } + + SimpleTest.finish(); +} + + +SimpleTest.waitForFocus(runTests); +</script> +</body> +</html> diff --git a/layout/forms/test/test_bug1529036.html b/layout/forms/test/test_bug1529036.html new file mode 100644 index 0000000000..0d1c4fa207 --- /dev/null +++ b/layout/forms/test/test_bug1529036.html @@ -0,0 +1,73 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1529036 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1529036</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> +html,body { + color:black; background-color:white; font:16px/1 monospace; padding:0; margin:0; +} + </style> + <script type="application/javascript"> + + /** Test for Bug 1529036 **/ + + function doTest() { + SimpleTest.waitForExplicitFinish(); + + var clicks = 0; + var elms = document.querySelectorAll('.click'); + for (var i = 0; i < elms.length; ++i) { + var e = elms[i]; + e.addEventListener('click', function(event) { + ++clicks; + }); + } + + var elms = document.querySelectorAll('.click.hit'); + for (var i = 0; i < elms.length; ++i) { + var e = elms[i]; + let r = e.getBoundingClientRect(); + synthesizeMouse(e, 50, 50, {}); + } + is(clicks, elms.length, "click events on overflow"); + + clicks = 0; + elms = document.querySelectorAll('.click.nohit'); + for (var i = 0; i < elms.length; ++i) { + var e = elms[i]; + let r = e.getBoundingClientRect(); + synthesizeMouse(e, 50, 50, {}); + } + is(clicks, 0, "click events on clipped overflow"); + + SimpleTest.finish(); + } + </script> +</head> +<body onload="SimpleTest.waitForFocus(doTest, window)"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1529036">Mozilla Bug 1529036</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<div style="border: 1px solid"> +<button class="click hit" id="t1" style="width:10px; height:10px; padding:20px; border-radius:50%"><div>button<br>button<br>button<br>button<br>button<br></div></button> +<button class="click hit" id="t2" style="width:10px; height:10px; padding:20px; border:1px solid"><div>button<br>button<br>button<br>button<br>button<br></div></button> +<button class="click hit" id="t3" style="width:10px; height:10px; border-width:20px; border-radius:50%"><div>button<br>button<br>button<br>button<br>button<br></div></button> +<button class="click hit" id="t4" style="width:10px; height:10px; border:20px solid"><div>button<br>button<br>button<br>button<br>button<br></div></button> +<button class="click nohit" id="t5" style="width:10px; height:10px; padding:20px; overflow:hidden; border-radius:50%"><div>button<br>button<br>button<br>button<br>button<br></div></button> +<button class="click nohit" id="t6" style="width:10px; height:10px; padding:20px; overflow:hidden; border:1px solid"><div>button<br>button<br>button<br>button<br>button<br></div></button> +</div> + +</body> +</html> diff --git a/layout/forms/test/test_bug231389.html b/layout/forms/test/test_bug231389.html new file mode 100644 index 0000000000..9239839247 --- /dev/null +++ b/layout/forms/test/test_bug231389.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=231389 +--> +<head> + <title>Test for Bug 231389</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=231389">Mozilla Bug 231389</a> +<p id="display"> + <textarea id="area" rows="5"> + Here + is + some + very + long + text + that + we're + using + for + testing + purposes + </textarea> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 231389 **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var area = document.getElementById("area"); + var val = area.value; + var pos = val.indexOf("purposes"); + + is(area.scrollTop, 0, "The textarea should not be scrolled initially"); + area.selectionStart = pos; + area.selectionEnd = pos; + requestAnimationFrame(function() { + isnot(area.scrollTop, 0, "The textarea's insertion point should be scrolled into view"); + + SimpleTest.finish(); + }); +}); + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug287446.html b/layout/forms/test/test_bug287446.html new file mode 100644 index 0000000000..bb9e3bec39 --- /dev/null +++ b/layout/forms/test/test_bug287446.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=287446 +--> +<head> + <title>Test for Bug 287446</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=287446">Mozilla Bug 287446</a> +<p id="display"> + <iframe id="i" + src="http://example.com/tests/layout/forms/test/bug287446_subframe.html"></iframe> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 287446 **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + isnot(window.location.host, "example.com", "test is not testing cross-site"); + var accessed = false; + try { + $("i").contentDocument.documentElement; + accessed = true; + } catch(e) {} + is(accessed, false, "Shouldn't be able to access cross-site"); + + $("i").style.display = "none"; + document.body.offsetWidth; + is(document.defaultView.getComputedStyle($("i")).display, "none", + "toggling display failed"); + $("i").style.display = ""; + document.body.offsetWidth; + is(document.defaultView.getComputedStyle($("i")).display, "inline", + "toggling display back failed"); + + $("i").contentWindow.postMessage("start", "*"); +}); + +function continueTest() { + $("i").style.display = "none"; + document.body.offsetWidth; + is(document.defaultView.getComputedStyle($("i")).display, "none", + "toggling display second time failed"); + $("i").style.display = ""; + document.body.offsetWidth; + is(document.defaultView.getComputedStyle($("i")).display, "inline", + "toggling display back second time failed"); + +$("i").contentWindow.postMessage("continue", "*"); +} + +window.addEventListener("message", + function(evt) { + var arr = evt.data.split(/ /).map(decodeURIComponent); + if (arr[0] == 't') { + is(arr[1], arr[2], arr[3]); + } else if (arr[0] == 'c') { + continueTest(); + } else if (arr[0] == 'f') { + SimpleTest.finish(); + } + }); + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug345267.html b/layout/forms/test/test_bug345267.html new file mode 100644 index 0000000000..ba9a3bd555 --- /dev/null +++ b/layout/forms/test/test_bug345267.html @@ -0,0 +1,97 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=345267 +--> +<head> + <title>Test for Bug 345267</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=345267">Mozilla Bug 345267</a> +<p id="display"> + <input id="d1" maxlength="3" value="abcde"> + <input id="d2" maxlength="3"> + <input id="d3" maxlength="3"> + <input id="d4" value="abcdefghijk"> + <input id="target" value="abcdefghijklm" maxlength="3"> +</p> +<div id="content" style="display: none"> + <input id="u1" maxlength="3" value="abcdef"> + <input id="u2" maxlength="3"> + <input id="u3" maxlength="3"> + <input id="u4" value="abcdefghijkl"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 345267 **/ +SimpleTest.waitForExplicitFinish(); +runTest(); + +function runTest() { + is($("d1").value, "abcde", + "Displayed initial value should not be truncated by maxlength"); + is($("u1").value, "abcdef", + "Undisplayed initial value should not be truncated by maxlength"); + + $("d2").value = "abcdefg"; + is($("d2").value, "abcdefg", + "Displayed set value should not be truncated by maxlength"); + + $("u2").value = "abcdefgh"; + is($("u2").value, "abcdefgh", + "Undisplayed set value should not be truncated by maxlength"); + + $("d3").defaultValue = "abcdefghi"; + is($("d3").value, "abcdefghi", + "Displayed set defaultValue should not be truncated by maxlength"); + + $("u3").defaultValue = "abcdefghij"; + is($("u3").value, "abcdefghij", + "Undisplayed set defaultValue should not be truncated by maxlength"); + + $("d4").maxLength = "3"; + is($("d4").value, "abcdefghijk", + "Displayed: setting maxLength should not truncate existing value"); + + $("u4").maxLength = "3"; + is($("u4").value, "abcdefghijkl", + "Undisplayed: setting maxLength should not truncate existing value"); + + // Now start the editing tests + is($("target").value, "abcdefghijklm", "Test starting state incorrect"); + $("target").focus(); + $("target").selectionStart = $("target").selectionEnd = 13; + sendKey("back_space"); + is($("target").value, "abcdefghijkl", "Should only delete one char"); + sendKey("back_space"); + is($("target").value, "abcdefghijk", "Should only delete one char again"); + (function () { + SpecialPowers.wrap($("target")).controllers.getControllerForCommand('cmd_undo') + .doCommand('cmd_undo'); + })(); + is($("target").value, "abcdefghijklm", + "Should be able to undo deletion in the face of maxlength"); + sendString("nopq"); + is($("target").value, "abcdefghijklm", + "Typing should have no effect when already past maxlength"); + + $("target").value = ""; + sendString("abcde"); + is($("target").value, "abc", "Typing should be limited by maxlength"); + + $("target").value = ""; + sendString("ad"); + sendKey("left"); + sendString("bc"); + is($("target").value, "abd", "Typing should be limited by maxlength again"); + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> + diff --git a/layout/forms/test/test_bug346043.html b/layout/forms/test/test_bug346043.html new file mode 100644 index 0000000000..e5db9bb8cf --- /dev/null +++ b/layout/forms/test/test_bug346043.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=346043 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 346043</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript"> + /** Test for Bug 346043 **/ + function test(select, index, useEscape) { + select.selectedIndex = index; + is(select.selectedIndex, index, "Selected index is broken"); + + // Open the select dropdown. + // + // Using Alt+Down (instead of a click) seems necessary to make subsequent + // mouse events work with the dropdown itself, instead of the window below + // it. + select.focus(); + synthesizeKey("KEY_ArrowDown", {altKey: true}); + + var options = select.getElementsByTagName("option"); + synthesizeMouseAtCenter(options[1], + {type: "mousemove", clickCount: 0}); + + // Close the select dropdown. + if (useEscape) + synthesizeKey("KEY_Escape"); // Tests a different code path. + select.blur(); + is(select.selectedIndex, index, "Selected index shouldn't change"); + } + + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(function() { + test(document.getElementById("test-unselected"), -1, true); + test(document.getElementById("test-unselected"), -1, false); + test(document.getElementById("test-disabled"), 0, true); + test(document.getElementById("test-disabled"), 0, false); + SimpleTest.finish(); + }); + </script> +</head> +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=346043">Mozilla Bug 346043</a> + <p id="display"></p> + <div id="content"> + <select id="test-unselected"> + <option>A</option> + <option>B</option> + <option>C</option> + </select> + + <select id="test-disabled"> + <option disabled>A</option> + <option>B</option> + <option>C</option> + </select> + </div> + <pre id="test"></pre> +</body> +</html> diff --git a/layout/forms/test/test_bug348236.html b/layout/forms/test/test_bug348236.html new file mode 100644 index 0000000000..f00f89efa7 --- /dev/null +++ b/layout/forms/test/test_bug348236.html @@ -0,0 +1,123 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=348236 +--> +<head> + + <title>Test for Bug 348236</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <style type="text/css"> + #eSelect { + position: fixed; top:0; left: 350px; font-size: 24px; width: 100px + } + #eSelect option { + margin: 0; padding: 0; height: 24px + } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=348236">Mozilla Bug 348236</a> +<p id="display"></p> +<div id="content"> + + <select id="eSelect" size="1" onchange="++this.onchangeCount"> + <option selected>1</option> + <option>2</option> + <option id="option3">3</option> + </select> +</div> +<pre id="test"> +<script type="text/javascript"> + + /** Test for Bug 348236 **/ + +SimpleTest.waitForExplicitFinish() +addLoadEvent(function test() { + + var + WinUtils = SpecialPowers.getDOMWindowUtils(window), + sec = netscape.security, + eSelect = $("eSelect"), + timeout = 0 // Choose a larger value like 500 ms if you want to see what's happening. + + function keypressOnSelect(key) { + eSelect.focus(); + synthesizeKey(key.key, {altKey: key.altKey}); + } + + function testKey(key, keyString, functionToContinue) { + var selectGotClick + function clickListener() { selectGotClick = true } + eSelect.selectedIndex = 0 + eSelect.onchangeCount = 0 + + // Drop the SELECT down. + keypressOnSelect(key) + // This timeout and the following are necessary to let the sent events take effect. + setTimeout(cont1, timeout) + function cont1() { + // Move the mouse over option 3. + let option3 = document.getElementById("option3"); + let rect = option3.getBoundingClientRect(); + WinUtils.sendMouseEvent("mousemove", rect.left + 4, rect.top + 4, 0, 0, 0, true) + setTimeout(cont2, timeout) + } + function cont2() { + // Close the select. + keypressOnSelect(key) + setTimeout(cont3, timeout) + } + function cont3() { + is(eSelect.value, "3", "Select's value should be 3 after hovering over option 3 and pressing " + keyString + ".") + is(eSelect.onchangeCount, 1, "Onchange should have fired once.") + + // Simulate click on area to the left of the select. + eSelect.addEventListener("click", clickListener, true) + selectGotClick = false + WinUtils.sendMouseEvent("mousedown", 320, 0, 0, 0, 0, true) + WinUtils.sendMouseEvent("mouseup", 320, 0, 0, 0, 0, true) + setTimeout(cont4, timeout) + } + function cont4() { + eSelect.removeEventListener("click", clickListener, true) + ok(!selectGotClick, "SELECT must not capture mouse events after closing it with " + keyString + ".") + functionToContinue() + } + } + + + // Quick sanity checks. + is(eSelect.value, "1", "SELECT value should be 1 after load.") + is(eSelect.selectedIndex, 0, "SELECT selectedIndex should be 0 after load.") + + // Check if sending key events works. + keypressOnSelect({key: "KEY_ArrowDown"}); + is(eSelect.value, "2", "SELECT value should be 2 after pressing Down.") + + // Test ALT-Down. + testKey({key: "KEY_ArrowDown", altKey: true}, "ALT-Down", nextKey1) + function nextKey1() { + // Test ALT-Up. + testKey({key: "KEY_ArrowUp", altKey: true}, "ALT-Up", nextKey2) + } + function nextKey2() { + // Test the F4 key on Windows. + if (/Win/i.test(navigator.platform)) + testKey({key: "KEY_F4"}, "F4", finished) + else + finished() + } + function finished() { + // Reset value to get the expected value if we reload the page. + eSelect.selectedIndex = 0 + SimpleTest.finish() + } +}) + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug353539.html b/layout/forms/test/test_bug353539.html new file mode 100644 index 0000000000..593250d207 --- /dev/null +++ b/layout/forms/test/test_bug353539.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=353539 +--> +<head> + <title>Test for Bug 353539</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=353539">Mozilla Bug 353539</a> +<p id="display"> + <textarea id="area" rows="5"> + Here + is + some + very + long + text + that + we're + using + for + testing + purposes + </textarea> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 353539 **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var area = document.getElementById("area"); + + is(area.scrollTop, 0, "The textarea should not be scrolled initially"); + area.focus(); + setTimeout(function() { + is(area.scrollTop, 0, "The textarea's insertion point should not be scrolled into view"); + + SimpleTest.finish(); + }, 0); +}); + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug365410.html b/layout/forms/test/test_bug365410.html new file mode 100644 index 0000000000..94ba8abfa0 --- /dev/null +++ b/layout/forms/test/test_bug365410.html @@ -0,0 +1,132 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=365410 +--> +<title>Test for Bug 365410</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<style> + select { box-sizing: content-box } +</style> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=365410">Mozilla Bug 365410</a> +<p id="display"> +<select id="test0" multiple="multiple"> + <option id="option">Item 1</option> + <option>Item 2</option> + <option>Item 3</option> + <option>Item 4</option> + <option>Item 5</option> + <option>Item 6</option> + <option>Item 7</option> + <option>Item 8</option> + <option>Item 9</option> + <option>Item 10</option> + <option>Item 11</option> + <option>Item 12</option> + <option>Item 13</option> + <option>Item 14</option> + <option>Item 15</option> +</select> +<select id="test1" multiple="multiple" size="1"> + <option>Item 1</option> + <option>Item 2</option> + <option>Item 3</option> + <option>Item 4</option> + <option>Item 5</option> + <option>Item 6</option> + <option>Item 7</option> + <option>Item 8</option> + <option>Item 9</option> + <option>Item 10</option> + <option>Item 11</option> + <option>Item 12</option> + <option>Item 13</option> + <option>Item 14</option> + <option>Item 15</option> +</select> +<select id="test2" multiple="multiple" size="1" style="height:0.9em"> + <option>Item 1</option> + <option>Item 2</option> + <option>Item 3</option> + <option>Item 4</option> + <option>Item 5</option> + <option>Item 6</option> + <option>Item 7</option> + <option>Item 8</option> + <option>Item 9</option> + <option>Item 10</option> + <option>Item 11</option> + <option>Item 12</option> + <option>Item 13</option> + <option>Item 14</option> + <option>Item 15</option> +</select> +<select id="test3" multiple="multiple" size="1"></select> +<select id="test4" multiple="multiple" size="1" style="height:0.9em"></select> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 365410 **/ + +function pageUpDownTest(id,index) { + var elm = document.getElementById(id); + elm.focus(); + elm.selectedIndex = 0; + sendKey("page_down"); + sendKey("page_down"); + sendKey("page_up"); + sendKey("page_down"); + is(elm.selectedIndex, index, "pageUpDownTest: selectedIndex for " + id + " is " + index); +} + +function upDownTest(id,index) { + var elm = document.getElementById(id); + elm.focus(); + elm.selectedIndex = 0; + sendKey("down"); + sendKey("down"); + sendKey("up"); + sendKey("down"); + is(elm.selectedIndex, index, "upDownTest: selectedIndex for " + id + " is " + index); +} + +function setHeight(id, h) { + var elm = document.getElementById(id); + elm.style.height = h + 'px'; +} + +function runTest() { + var h = document.getElementById("option").clientHeight; + var list5itemsHeight = h * 5.5; + setHeight("test0", list5itemsHeight); + setHeight("test1", list5itemsHeight); + setHeight("test3", list5itemsHeight); + + pageUpDownTest("test0",8); + pageUpDownTest("test1",8); + pageUpDownTest("test2",2); + pageUpDownTest("test3",-1); + pageUpDownTest("test4",-1); + upDownTest("test0",2); + upDownTest("test1",2); + upDownTest("test2",2); + upDownTest("test3",-1); + upDownTest("test4",-1); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTest); + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug378670.html b/layout/forms/test/test_bug378670.html new file mode 100644 index 0000000000..f039cf35d6 --- /dev/null +++ b/layout/forms/test/test_bug378670.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=378670 +--> +<head> + <title>Test for Bug 378670</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=378670">Mozilla Bug 378670</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +Clicking on the select should not crash Mozilla +<select id="select"> +<option>1</option> +<option>2</option> +</select> + +<pre id="test"> +<script> +document.body.addEventListener('popupshowing', function(e) {e.target.remove() }, true); +</script> +<script type="application/javascript"> + +/** Test for Bug 378670 **/ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); + +function clickit() { + var select = document.getElementById('select'); + + sendMouseEvent({type:'mousedown'}, select); + sendMouseEvent({type:'mouseup'}, select); + sendMouseEvent({type:'click'}, select); + + setTimeout(finish, 200); +} + +window.addEventListener('load', clickit); + +function finish() +{ + ok(true, "This is a mochikit version of a crash test. To complete is to pass."); + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug402198.html b/layout/forms/test/test_bug402198.html new file mode 100644 index 0000000000..f319a11976 --- /dev/null +++ b/layout/forms/test/test_bug402198.html @@ -0,0 +1,77 @@ +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=402198 +--> +<head> + <title>Test for Bug 402198</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=402198">Mozilla Bug 402198</a> +<p id="display"> + <select></select> + <select><optgroup></optgroup></select> + <legend style="overflow: scroll;"> + <select></select> + </legend> + <span></span> + <span style="display: -moz-box;"> + <select></select> + </span> + <legend style=" "> + <label style="overflow: scroll; display: -moz-box;"> + <select></select> + </label> + </legend> + <legend> + <label style=" display: table;"> + <select id="a"> + <option>High Grade</option> + <option>Medium Grade</option> + </select> + </label> + </legend> + + <input> + <select multiple="multiple"></select> + <select style="overflow: scroll; display: -moz-box;"> + <optgroup></optgroup> + <optgroup style="display: table-cell;"></optgroup> + </select> +</p> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +function doe3() { + document.documentElement.style.display = 'none'; + document.body.offsetHeight; + document.documentElement.style.display = ''; + document.body.offsetHeight; + + document.getElementById('a').focus(); + document.body.style.display = 'none'; + + synthesizeKey('KEY_Tab', {shiftKey: true}); + + is(0, 0, "this is a crash/assertion test, so we're ok if we survived this far"); + setTimeout(function() {document.body.style.display = ''; SimpleTest.finish();}, 0); +} + +function do_test() { + setTimeout(doe3,300); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); +addLoadEvent(do_test); +</script> +</pre> + +<style> +* {quotes: "quote" "quote" !important;} +</style> + +</body> +</html> diff --git a/layout/forms/test/test_bug411236.html b/layout/forms/test/test_bug411236.html new file mode 100644 index 0000000000..b2e2bee380 --- /dev/null +++ b/layout/forms/test/test_bug411236.html @@ -0,0 +1,71 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=411236 +--> +<head> + <title>Test for Bug 411236</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=411236">Mozilla Bug 411236</a> +<p id="display"></p> +<div id="content"> + <input type="file" onfocus="window.oTarget = event.originalTarget;" + onclick="window.fileInputGotClick = true; return false;" + id="fileinput"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 411236 **/ + +window.oTarget = null; +window.fileInputGotClick = false; + +function test() { + // Try to find the '<input>' using tabbing. + var i = 0; + while (!window.oTarget && i < 100) { + ++i; + synthesizeKey("KEY_Tab"); + } + + if (i >= 100) { + ok(false, "Couldn't find an input element!"); + SimpleTest.finish(); + return; + } + + ok(window.oTarget instanceof HTMLInputElement, "Should have focused the input element!"); + var e = document.createEvent("mouseevents"); + e.initMouseEvent("click", true, true, window, 0, 1, 1, 1, 1, + false, false, false, false, 0, null); + SpecialPowers.wrap(window.oTarget).dispatchEvent(e); + ok(window.fileInputGotClick, + "File input should have got a click event, but not open the file dialog."); + SimpleTest.finish(); +} + +function beginTest() { + // accessibility.tabfocus must be set to value 7 before running test also + // on a mac. + SpecialPowers.pushPrefEnv({"set": [["accessibility.tabfocus", 7]]}, do_test); +} + +function do_test() { + window.focus(); + document.getElementById('fileinput').focus(); + setTimeout(test, 100); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); +addLoadEvent(beginTest); + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug446663.html b/layout/forms/test/test_bug446663.html new file mode 100644 index 0000000000..bdab0b20ef --- /dev/null +++ b/layout/forms/test/test_bug446663.html @@ -0,0 +1,80 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=446663 +--> +<head> + <title>Test for Bug 446663</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=446663">Mozilla Bug 446663</a> +<p id="display"> +<style>#bug446663_a:focus{overflow:hidden}</style> +<input id="bug446663_a"><input id="bug446663_b"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 446663 **/ + +function test_edit_cmds(id) { + + var elm = document.getElementById(id); + elm.focus(); + elm.select(); + SpecialPowers.wrap(elm).controllers.getControllerForCommand('cmd_cut') + .doCommand('cmd_cut'); + is(elm.value, '', id + " cut"); + + SpecialPowers.wrap(elm).controllers.getControllerForCommand('cmd_undo') + .doCommand('cmd_undo'); + is(elm.value, '123', id + " undo"); +} + +var inputHappened = false; +function inputListener() { + inputHappened = true; + $(id).removeEventListener("input", inputListener); +} + +var id = 'bug446663_a' +var elm = document.getElementById(id); +elm.focus(); +var x = document.body.offsetHeight; +$(id).addEventListener("input", inputListener); +sendChar('1'); +is(inputHappened, true, "How come no input?"); +sendChar('3'); +sendKey('LEFT') +sendChar('2'); +elm.blur(); +x = document.body.offsetHeight; +is(elm.value, '123', id + " edit"); +test_edit_cmds(id) + +id = 'bug446663_b' +elm = document.getElementById(id); +elm.focus(); +sendChar('1'); +elm.style.display = 'none' +var x = document.body.offsetHeight; +elm.style.display = 'inline' +x = document.body.offsetHeight; +sendChar('3'); +sendKey('LEFT') +sendChar('2'); +elm.blur(); +x = document.body.offsetHeight; +is(elm.value, '123', id + " edit"); +test_edit_cmds(id) + +</script> +</pre> +</body> +</html> + diff --git a/layout/forms/test/test_bug476308.html b/layout/forms/test/test_bug476308.html new file mode 100644 index 0000000000..41858d9eb8 --- /dev/null +++ b/layout/forms/test/test_bug476308.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=476308 +--> +<head> + <title>Test for Bug 345267</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<button style="-moz-appearance: none; width: 100px; height: 60px; background-color: red; border: 2px solid green; box-shadow: 30px 0px 3.5px black; position: absolute; top: 300px; left: 20px;" + id="button1">1</button> + +<br /> +<div style="width: 100px; height: 100px; background-color: green; border: 3px dotted blue; box-shadow: -30px -20px 0px black; position: absolute; top: 500px; left: 70px;" + id="div1">2</div> + +<script type="text/javascript"> + var elem = document.elementFromPoint(130, 310); + isnot(elem, document.getElementById("button1"), "button1's box-shadow is receiving events when it shouldn't"); + + elem = document.elementFromPoint(50, 500); + isnot(elem, document.getElementById("div1"), "div1's box-shadow is receiving events when it shouldn't"); +</script> + +</body> +</html> + diff --git a/layout/forms/test/test_bug477531.html b/layout/forms/test/test_bug477531.html new file mode 100644 index 0000000000..e0c7979af4 --- /dev/null +++ b/layout/forms/test/test_bug477531.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=477531 +--> +<head> + <title>Test for Bug 477531</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + + <style type="text/css"> + #s { + margin-left: 10px; + } + + #s:indeterminate { + margin-left: 30px; + } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=477531">Mozilla Bug 477531</a> +<p id="display"></p> +<div id="content"> + +<input type="checkbox" id="s" /> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 477531 **/ +is(document.defaultView.getComputedStyle($("s")).getPropertyValue("margin-left"), + "10px", + "Non-indeterminate checkbox should have a margin of 10px"); + +$("s").indeterminate = true; + +is(document.defaultView.getComputedStyle($("s")).getPropertyValue("margin-left"), + "30px", + "Indeterminate checkbox should have a margin of 30px"); + +$("s").setAttribute("type", "radio"); + +is(document.defaultView.getComputedStyle($("s")).getPropertyValue("margin-left"), + "30px", + "Setting an indeterminate element to type radio should give it indeterminate styles"); + +$("s").setAttribute("type", "checkbox"); + +is(document.defaultView.getComputedStyle($("s")).getPropertyValue("margin-left"), + "30px", + "Setting an indeterminate element to type checkbox should give it indeterminate styles"); + +$("s").indeterminate = false; + +is(document.defaultView.getComputedStyle($("s")).getPropertyValue("margin-left"), + "10px", + "Newly non-indeterminate checkbox should have a margin of 10px"); + +</script> +</pre> +</body> +</html> + diff --git a/layout/forms/test/test_bug477700.html b/layout/forms/test/test_bug477700.html new file mode 100644 index 0000000000..1ab8d88321 --- /dev/null +++ b/layout/forms/test/test_bug477700.html @@ -0,0 +1,59 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=477700 +--> +<head> + <title>Test for Bug 477700</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=477700">Mozilla Bug 477700</a> +<p id="display"> + <iframe id="i" + src="http://example.com/tests/layout/forms/test/bug477700_subframe.html"></iframe> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 477700 **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + isnot(window.location.host, "example.com", "test is not testing cross-site"); + var accessed = false; + try { + $("i").contentDocument.documentElement; + accessed = true; + } catch(e) {} + is(accessed, false, "Shouldn't be able to access cross-site"); + + $("i").style.display = "none"; + document.body.offsetWidth; + is(document.defaultView.getComputedStyle($("i")).display, "none", + "toggling display failed"); + $("i").style.display = ""; + document.body.offsetWidth; + is(document.defaultView.getComputedStyle($("i")).display, "inline", + "toggling display back failed"); + + $("i").contentWindow.postMessage("start", "*"); +}); + +window.addEventListener("message", + function(evt) { + var arr = evt.data.split(/ /).map(decodeURIComponent); + if (arr[0] == 't') { + is(arr[1], arr[2], arr[3]); + } else if (arr[0] == 'f') { + SimpleTest.finish(); + } + }); + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug534785.html b/layout/forms/test/test_bug534785.html new file mode 100644 index 0000000000..7e43838927 --- /dev/null +++ b/layout/forms/test/test_bug534785.html @@ -0,0 +1,88 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=534785 +--> +<head> + <title>Test for Bug 534785</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=534785">Mozilla Bug 534785</a> +<p id="display"></p> +<input type="text" value="test"> +<div id="reframe"> +<textarea></textarea> +<textarea>test</textarea> +<input type="text"> +<input type="text" value="test"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 534785 **/ + +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(function() { + var i = document.querySelector("input"); + i.addEventListener("focus", function() { + is(i.value, "test", "Sanity check"); + + is(document.activeElement, i, "Should be focused before frame reconstruction"); + sendString("1"); + is(i.value, "1test", "Can accept keyboard events before frame reconstruction"); + + // force frame reconstruction + i.style.display = "none"; + document.offsetHeight; + i.style.display = ""; + document.offsetHeight; + + is(document.activeElement, i, "Should be focused after frame reconstruction"); + sendString("2"); + is(i.value, "12test", "Can accept keyboard events after frame reconstruction"); + + // Make sure reframing happens gracefully + var reframeDiv = document.getElementById("reframe"); + var textAreaWithoutValue = reframeDiv.querySelectorAll("textarea")[0]; + var textAreaWithValue = reframeDiv.querySelectorAll("textarea")[1]; + var inputWithoutValue = reframeDiv.querySelectorAll("input")[0]; + var inputWithValue = reframeDiv.querySelectorAll("input")[1]; + reframeDiv.style.display = "none"; + document.body.offsetWidth; + reframeDiv.style.display = ""; + document.body.offsetWidth; + [textAreaWithoutValue, inputWithoutValue].forEach(function (elem) { + is(elem.value, "", "Value should persist correctly"); + }); + [textAreaWithValue, inputWithValue].forEach(function (elem) { + is(elem.value, "test", "Value should persist correctly"); + }); + [inputWithoutValue, inputWithValue].forEach(elem => elem.type = "submit"); + document.body.offsetWidth; + is(inputWithoutValue.value, "", "Value should persist correctly"); + is(inputWithValue.value, "test", "Value should persist correctly"); + [inputWithoutValue, inputWithValue].forEach(elem => elem.type = "text"); + document.body.offsetWidth; + is(inputWithoutValue.value, "", "Value should persist correctly"); + is(inputWithValue.value, "test", "Value should persist correctly"); + [inputWithoutValue, inputWithValue].forEach(elem => elem.focus()); // initialze the editor + reframeDiv.style.display = "none"; + document.body.offsetWidth; + reframeDiv.style.display = ""; + document.body.offsetWidth; + is(inputWithoutValue.value, "", "Value should persist correctly with editor"); + is(inputWithValue.value, "test", "Value should persist correctly with editor"); + + SimpleTest.finish(); + }); + i.focus(); +}); + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug536567_perwindowpb.html b/layout/forms/test/test_bug536567_perwindowpb.html new file mode 100644 index 0000000000..224b2c74a4 --- /dev/null +++ b/layout/forms/test/test_bug536567_perwindowpb.html @@ -0,0 +1,215 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=536567 +--> +<head> + <title>Test for Bug 536567</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=536567">Mozilla Bug 536567</a> +<p id="display"></p> +<pre id="test"> +<script type="application/javascript"> +/** Test for Bug 536567 **/ + +const Cm = Components.manager; + +var MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); + +var tmpDir = Services.dirsvc.get("TmpD", Ci.nsIFile); +var homeDir = Services.dirsvc.get("Desk", Ci.nsIFile); + +function newDir() { + var dir = tmpDir.clone(); + dir.append("testdir" + Math.floor(Math.random() * 10000)); + dir.QueryInterface(Ci.nsIFile); + dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o700); + return dir; +} + +var dirs = []; +for(let i = 0; i < 6; i++) { + dirs.push(newDir()); +} +dirs.push(homeDir); +var domains = ['http://mochi.test:8888', 'http://example.org:80', 'http://example.com:80']; +/* + * These tests take 3 args each: + * - which domain to load + * - the filePicker displayDirectory we expect to be set + * - the file to pick (in most cases this will show up in the next test, + * as indicated by the comments) + */ +var tests = [ + "clear history", + [0, 6, 0], // 0 -> 3 + [1, 6, 1], // 1 -> 4 + [2, 6, 2], // 2 -> 5 + [0, 0, 3], // 3 -> 6 + [1, 1, 1], // 4 -> 8 + [2, 2, 2], // 5 -> 9 + [0, 3, 1], // 6 -> 7 + [0, 1, 0], // 7 -> x + [1, 1, 1], // 8 -> x + [2, 2, 2], // 9 -> x + "clear history", + [0, 6, 0], // 11 -> 15 + [1, 6, 1], // 12 -> 16 + [2, 6, 2], // 13 -> 17 + "pb on", + [0, 0, 3], // 15 -> 18 + [1, 1, 4], // 16 -> 19 + [2, 2, 5], // 17 -> 20 + [0, 3, 3], // 18 -> x + [1, 4, 4], // 19 -> x + [2, 5, 5], // 20 -> x + "pb off", + [0, 0, 5], // 22 -> 26 + [1, 1, 4], // 23 -> 27 + [2, 2, 3], // 24 -> 28 + "pb on", + [0, 3, 5], // 26 -> x + [1, 4, 4], // 27 -> x + [2, 5, 3], // 28 -> x + "clear history", + // Not checking after clear history because browser.download.lastDir content + // pref is not being clear properly in private windows. + //[0, 6, 0], // 30 -> x + //[1, 6, 1], // 31 -> x + //[2, 6, 2], // 32 -> x + "pb off" +]; + +var testIndex = 0; +var content; +var normalWindow; +var privateWindow; +var normalWindowIframe; +var privateWindowIframe; + +function runTest() { + var test = tests[testIndex]; + if (test == undefined) { + endTest(); + } else if (test == "pb on") { + content = privateWindowIframe; + testIndex++; + runTest(); + } else if (test == "pb off") { + content = normalWindowIframe; + testIndex++; + runTest(); + } else if (test == "clear history") { + Services.obs.notifyObservers(null, "browser:purge-session-history"); + testIndex++; + runTest(); + } else { + var file = dirs[test[2]].clone(); + file.append("file.file"); + MockFilePicker.setFiles([file]); + content.setAttribute('src', domains[test[0]] + '/chrome/layout/forms/test/bug536567_subframe.html'); + } +} + +function endTest() { + for(let i = 0; i < dirs.length - 1; i++) { + dirs[i].remove(true); + } + + normalWindow.close(); + privateWindow.close(); + MockFilePicker.cleanup(); + SimpleTest.finish(); +} + +var mainWindow = window.browsingContext.topChromeWindow; +var contentPage = "http://mochi.test:8888/chrome/layout/forms/test/bug536567_iframe.html"; + +function whenDelayedStartupFinished(aWindow, aCallback) { + Services.obs.addObserver(function observer(aSubject, aTopic) { + if (aWindow == aSubject) { + Services.obs.removeObserver(observer, aTopic); + setTimeout(aCallback, 0); + } + }, "browser-delayed-startup-finished"); +} + +function testOnWindow(aIsPrivate, aCallback) { + var win = mainWindow.OpenBrowserWindow({private: aIsPrivate}); + whenDelayedStartupFinished(win, function() { + win.addEventListener("DOMContentLoaded", function onInnerLoad() { + if (win.content.location.href != contentPage) { + win.gBrowser.loadURI(Services.io.newURI(contentPage), { + triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({}), + }); + return; + } + win.removeEventListener("DOMContentLoaded", onInnerLoad, true); + win.gBrowser.selectedBrowser.focus(); + SimpleTest.info("DOMContentLoaded's window: " + win.location + " vs. " + window.location); + win.setTimeout(function() { aCallback(win); }, 0); + }, true); + SimpleTest.info("load's window: " + win.location + " vs. " + window.location); + win.setTimeout(function() { + win.gBrowser.loadURI(Services.io.newURI(contentPage), { + triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({}), + }); + }, 0); + }); +} + +MockFilePicker.showCallback = function(filepicker) { + var test = tests[testIndex]; + var returned = -1; + for (let i = 0; i < dirs.length; i++) { + var dir = MockFilePicker.displayDirectory + ? MockFilePicker.displayDirectory + : Services.dirsvc.get(MockFilePicker.displaySpecialDirectory, Ci.nsIFile); + if (dirs[i].path == dir.path) { + returned = i; + break; + } + } + if (test[1] == -1) { + ok(false, "We should never get an unknown directory back"); + } else { + is(returned, test[1], 'test ' + testIndex); + } + + filepicker.window.setTimeout(function() { + testIndex++; + runTest(); + }, 0); +}; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + testOnWindow(false, function(aWin) { + var selectedBrowser = aWin.gBrowser.selectedBrowser; + + normalWindow = aWin; + normalWindowIframe = + selectedBrowser.contentDocument.getElementById("content"); + + testOnWindow(true, function(aPrivateWin) { + selectedBrowser = aPrivateWin.gBrowser.selectedBrowser; + + privateWindow = aPrivateWin; + privateWindowIframe = + selectedBrowser.contentDocument.getElementById("content"); + + content = normalWindowIframe; + selectedBrowser.contentWindow.setTimeout(runTest, 0); + }); + }); +}; + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug542914.html b/layout/forms/test/test_bug542914.html new file mode 100644 index 0000000000..ff7a68acea --- /dev/null +++ b/layout/forms/test/test_bug542914.html @@ -0,0 +1,115 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=542914 +--> +<head> + <title>Test for Bug 542914</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=542914">Mozilla Bug 542914</a> +<p id="display"> + <input type="text" id="a" value="test"> + <input type="text" id="b"> + <input type="text" id="c"> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 542914 **/ +SimpleTest.waitForExplicitFinish(); +function runTests(callback, type) { + var a = $("a"); + + // Test that the initial value of the control is available to script + // without initilization of the editor + is(a.value, "test", "The value is available before initialization"); + // Initialize the editor + a.focus(); + // Test that the value does not change after initialization + is(a.value, "test", "The value does not change after initializtion"); + + var b = $("b"); + + // Test that the initial value is empty before initialization. + is(b.value, "", "The value is empty before initialization"); + // Make sure that the value can be changed before initialization + b.value ="some value"; + is(b.value, "some value", "The value can be changed before initialization"); + // Initialize the editor + b.focus(); + // Make sure that the value does not change after initialization + is(b.value, "some value", "The value does not change after initialization"); + // Make sure that the value does not change if the element is hidden + b.style.display = "none"; + document.body.offsetHeight; + is(b.value, "some value", "The value does not change while hidden"); + b.style.display = ""; + document.body.offsetHeight; + b.focus(); + is(b.value, "some value", "The value does not change after being shown"); + + var c = $("c"); + + // Make sure that the control accepts input events without explicit initialization + is(c.value, "", "Control is empty initially"); + c.focus(); + sendChar("a"); + is(c.value, "a", "Control accepts input without explicit initialization"); + // Make sure that the control retains its caret position + c.focus(); + c.blur(); + c.focus(); + sendChar("b"); + is(c.value, "ab", "Control retains caret position after being re-focused"); + + var d = document.createElement("input"); + d.setAttribute("type", type); + $("display").appendChild(d); + document.body.offsetHeight; + + // Make sure dynamically injected inputs work as expected + is(d.value, "", "Dynamic control's initial value should be empty"); + d.value = "new"; + d.focus(); + is(d.value, "new", "Dynamic control's value can be set before initialization"); + sendChar("x"); + is(d.value, "newx", "Dynamic control accepts keyboard input without explicit initialization"); + $("display").removeChild(d); + is(d.value, "newx", "Dynamic control retains value after being removed from the document"); + + callback(); +} + +var gPreviousType = "text"; +function setTypes(aType) { + var content = document.getElementById("display"); + content.innerHTML = content.innerHTML.replace(gPreviousType, aType); + gPreviousType = aType; +} + +addLoadEvent(function() { + ok(true, "Running tests on <input type=text>"); + runTests(function() { + ok(true, "Running tests on <input type=password>"); + setTypes("password"); + runTests(function() { + ok(true, "Running tests on <input type=tel>"); + setTypes("tel"); + runTests(function() { + SimpleTest.finish(); + }, "tel"); + }, "password"); + }, "text"); +}); + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug549170.html b/layout/forms/test/test_bug549170.html new file mode 100644 index 0000000000..a8a3f6d508 --- /dev/null +++ b/layout/forms/test/test_bug549170.html @@ -0,0 +1,77 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=549170 +--> +<head> + <title>Test for Bug 549170</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="window.setTimeout(runTests, 0);"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=549170">Mozilla Bug 549170</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<input id='i' + onmouseup="mouseHandler(event);" + onmousedown="mouseHandler(event);"> +<textarea id='t' + onmouseup="mouseHandler(event);" + onmousedown="mouseHandler(event);"></textarea><br> +<input id='ip' placeholder='foo' + onmouseup="mouseHandler(event);" + onmousedown="mouseHandler(event);"> +<textarea id='tp' placeholder='foo' + onmouseup="mouseHandler(event);" + onmousedown="mouseHandler(event);"></textarea> +<pre id="test"> + +<script type="application/javascript"> + +/** Test for Bug 549170 **/ + +var gNumberOfMouseEventsCaught = 0; + +SimpleTest.waitForExplicitFinish(); + +function mouseHandler(aEvent) +{ + gNumberOfMouseEventsCaught++; + is(SpecialPowers.wrap(aEvent).originalTarget.nodeName, "DIV", "An inner div should be the target of the event"); + ok(SpecialPowers.wrap(aEvent).originalTarget.implementedPseudoElement == "::-moz-text-control-editing-root", "the target div should be the editor div"); +} + +function checkMouseEvents(element) +{ + gNumberOfMouseEventsCaught = 0; + + let x = element.offsetWidth / 2; + let y = element.offsetHeight / 2; + + synthesizeMouse(element, x, y, {type: "mousedown", button: 0}); + synthesizeMouse(element, x, y, {type: "mouseup", button: 0}); + synthesizeMouse(element, x, y, {type: "mousedown", button: 1}); + // NOTE: this event is going to copy the buffer on linux, this should not be a problem + synthesizeMouse(element, x, y, {type: "mouseup", button: 1}); + synthesizeMouse(element, x, y, {type: "mousedown", button: 2}); + synthesizeMouse(element, x, y, {type: "mouseup", button: 2}); + + is(gNumberOfMouseEventsCaught, 6, "Some mouse events have not been caught"); +} + +function runTests() +{ + checkMouseEvents(document.getElementById('i')); + checkMouseEvents(document.getElementById('t')); + checkMouseEvents(document.getElementById('ip')); + checkMouseEvents(document.getElementById('tp')); + + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug562447.html b/layout/forms/test/test_bug562447.html new file mode 100644 index 0000000000..86042bb485 --- /dev/null +++ b/layout/forms/test/test_bug562447.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=562447 +--> +<head> + <title>Test for Bug 562447</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + +</head> +<body> +<p><a target="_blank" href="https://bugzilla.mozilla.org/show_bug?id=562447">Mozilla Bug 562447</a> + +<input id="WhyDoYouFocusMe" + style="position: absolute; left: -50px; top: 10000px;"> + +<pre id="test"> +<script> +addLoadEvent(function() { + // Scroll down a bit + window.scrollTo(0, 5000); + + setTimeout(function() { + // Make sure that we're scrolled by 5000px + is(Math.round(window.pageYOffset), 5000, "Make sure we're scrolled correctly"); + + // Scroll back up, and mess with the input box along the way + var input = document.getElementById("WhyDoYouFocusMe"); + input.focus(); + input.blur(); + window.scrollTo(0, 0); + + setTimeout(function() { + is(window.pageYOffset, 0, "Make sure we're scrolled back up correctly"); + + // Scroll back up + window.scrollTo(0, 5000); + + setTimeout(function() { + is(Math.round(window.pageYOffset), 5000, "Sanity check"); + + window.scrollTo(0, 0); + input.focus(); + input.blur(); + + setTimeout(function() { + isnot(Math.round(window.pageYOffset), 0, "This time we shouldn't be scrolled up"); + + SimpleTest.finish(); + }, 0); + }, 0); + }, 0); + }, 0); +}); + +SimpleTest.waitForExplicitFinish(); +</script> +</pre> + +</body> +</html> diff --git a/layout/forms/test/test_bug563642.html b/layout/forms/test/test_bug563642.html new file mode 100644 index 0000000000..72bb4c6858 --- /dev/null +++ b/layout/forms/test/test_bug563642.html @@ -0,0 +1,82 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=563642 +--> +<head> + <title>Test for Bug 563642</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=563642">Mozilla Bug 563642</a> +<p id="display"> +<select id="test1" multiple="multiple" size="1"> + <option>Item 1</option> + <option>Item 2</option> + <option>Item 3</option> + <option>Item 4</option> + <option>Item 5</option> +</select> +<select id="test2" multiple="multiple" size="1"> + <option>Item 1</option> + <option disabled>Item 2</option> + <option>Item 3</option> + <option disabled>Item 4</option> + <option>Item 5</option> +</select> +<select id="test3" multiple="multiple"></select> +<select id="test4" multiple="multiple" size="1"></select> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 563642 **/ + +function pageUpDownTest(id,index) { + var elm = document.getElementById(id); + elm.focus(); + elm.selectedIndex = 0; + sendKey("page_down"); + sendKey("page_down"); + sendKey("page_down"); + sendKey("page_up"); + sendKey("page_down"); + is(elm.selectedIndex, index, "pageUpDownTest: selectedIndex for " + id + " is " + index); +} + +function upDownTest(id,index) { + var elm = document.getElementById(id); + elm.focus(); + elm.selectedIndex = 0; + sendKey("down"); + sendKey("down"); + sendKey("down"); + sendKey("up"); + sendKey("down"); + is(elm.selectedIndex, index, "upDownTest: selectedIndex for " + id + " is " + index); +} + +function runTest() { + pageUpDownTest("test1",3); + pageUpDownTest("test2",4); + pageUpDownTest("test3",-1); + pageUpDownTest("test4",-1); + upDownTest("test1",3); + upDownTest("test2",4); + upDownTest("test3",-1); + upDownTest("test4",-1); + + SimpleTest.finish(); +} +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTest); + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug564115.html b/layout/forms/test/test_bug564115.html new file mode 100644 index 0000000000..16fb423341 --- /dev/null +++ b/layout/forms/test/test_bug564115.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=564115 +--> +<head> + <title>Test for Bug 564115</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + +</head> +<body> +<p><a target="_blank" href="https://bugzilla.mozilla.org/show_bug?id=564115">Mozilla Bug 564115</a> + +<pre id="test"> +<script> + +const TEST_URL = "/tests/layout/forms/test/bug564115_window.html"; + +addLoadEvent(function() { + var win = open(TEST_URL, "", "width=600,height=600"); + SimpleTest.waitForFocus(function() { + var doc = win.document; + var input = doc.querySelector("input"); + + // Focus the input box, and wait for the focus to actually happen + input.focus(); + win.requestAnimationFrame(function() { + win.requestAnimationFrame(function() { + // Scroll down a bit + win.scrollTo(0, 5000); + + setTimeout(function() { + is(Math.round(win.pageYOffset), 5000, "Page should be scrolled correctly"); + + // Refocus the window + SimpleTest.waitForFocus(function() { + SimpleTest.waitForFocus(function() { + is(Math.round(win.pageYOffset), 5000, + "The page's scroll offset should not have been changed"); + + win.close(); + SimpleTest.finish(); + }, win); + }); + }, 0); + }); + }); + }, win); +}); + +SimpleTest.waitForExplicitFinish(); +</script> +</pre> + +</body> +</html> diff --git a/layout/forms/test/test_bug571352.html b/layout/forms/test/test_bug571352.html new file mode 100644 index 0000000000..73ad7454f3 --- /dev/null +++ b/layout/forms/test/test_bug571352.html @@ -0,0 +1,86 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=571352 +--> +<head> + <title>Test for Bug 571352</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=571352">Mozilla Bug 571352</a> +<p id="display"><select multiple style="width:300px/*to avoid any overlay scrollbar messing with our mouse clicks*/"><option>0<option>1<option>2<option>3<option>4<option>5</select></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 571352 **/ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function test() { + function focusList() { + $('display').firstChild.focus(); + } + function option(index) { + return $('display').firstChild.childNodes[index]; + } + function remove(index) { + var sel = $('display').firstChild; + sel.removeChild(sel.childNodes[index]); + document.body.clientHeight; + } + function up() { synthesizeKey("KEY_ArrowUp"); } + function shiftUp() { synthesizeKey("KEY_ArrowUp", {shiftKey:true}); } + function down() { synthesizeKey("KEY_ArrowDown"); } + function shiftDown() { synthesizeKey("KEY_ArrowDown", {shiftKey:true}); } + function mouseEvent(index,event) { + synthesizeMouse(option(index), 5, 5, event); + } + + const click = {}; + const shiftClick = {shiftKey:true}; + focusList(); + mouseEvent(0,click) + is(document.activeElement,$('display').firstChild,"<select> is focused"); + ok(option(0).selected,"first option is selected"); + mouseEvent(2,shiftClick) + remove(0); + ok(option(0).selected && option(1).selected,"first two options are selected"); + mouseEvent(2,shiftClick) + ok(option(0).selected && option(1).selected && option(2).selected,"first three options are selected"); + shiftUp(); + ok(option(0).selected && option(1).selected,"first two options are selected"); + remove(1); + ok(option(0).selected,"first option is selected"); + shiftDown(); + ok(option(0).selected && option(1).selected,"first two options are selected"); + down(); + ok(option(2).selected,"third option is selected"); + shiftDown(); + ok(option(2).selected && option(3).selected,"third & fourth option are selected"); + remove(2); + shiftUp(); + ok(option(1).selected && option(2).selected,"2nd & third option are selected"); + remove(0); + mouseEvent(0,shiftClick) + ok(option(0).selected && option(1).selected,"all remaining 2 options are selected"); + shiftDown(); + remove(1); + ok(!option(0).selected,"first option is unselected"); + remove(0); // select is now empty + ok($('display').firstChild.firstChild==null,"all options were removed"); + + SimpleTest.finish(); +}); + + + + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug572406.html b/layout/forms/test/test_bug572406.html new file mode 100644 index 0000000000..8b4d0b81ce --- /dev/null +++ b/layout/forms/test/test_bug572406.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=572406 +--> +<head> + <title>Test for Bug 572406</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload='runTests();'> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=572406">Mozilla Bug 572406</a> +<p id="display"></p> +<div id='content'> + <textarea id='i'>foo</textarea> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 572406 **/ + +SimpleTest.waitForExplicitFinish(); + +function runTests() +{ + var textarea = document.getElementById('i'); + + textarea.firstChild.nodeValue = "bar"; + is(textarea.value, textarea.firstChild.nodeValue, + "textarea value should be firstChild.nodeValue"); + is(textarea.defaultValue, textarea.firstChild.nodeValue, + "textarea defaultValue should be firstChild.nodeValue"); + + textarea.style.display = 'none'; + SimpleTest.executeSoon(function() { + textarea.firstChild.nodeValue = "tulip"; + is(textarea.value, textarea.firstChild.nodeValue, + "textarea value should be firstChild.nodeValue"); + is(textarea.defaultValue, textarea.firstChild.nodeValue, + "textarea defaultValue should be firstChild.nodeValue"); + SimpleTest.finish(); + }); +} + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug572649.html b/layout/forms/test/test_bug572649.html new file mode 100644 index 0000000000..10fa6e63f7 --- /dev/null +++ b/layout/forms/test/test_bug572649.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=572649 +--> +<head> + <title>Test for Bug 572649</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=572649">Mozilla Bug 572649</a> +<p id="display"> + <textarea id="area" rows="5"> + Here + is + some + very + long + text + that + we're + using + for + testing + purposes + </textarea> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 572649 **/ +SimpleTest.waitForExplicitFinish(); + +// We intermittently trigger two "Wrong parent ComputedStyle" assertions +// on B2G emulator builds (bug XXXXXXX). The two frames that get incorrect +// ComputedStyle parents are scroll bar parts in the <textarea>. +SimpleTest.expectAssertions(0, 2); + +addLoadEvent(function() { + var area = document.getElementById("area"); + + is(area.scrollTop, 0, "The textarea should not be scrolled initially"); + area.addEventListener("focus", function() { + setTimeout(function() { + is(area.scrollTop, 0, "The textarea's insertion point should not be scrolled into view"); + + SimpleTest.finish(); + }, 0); + }, {once: true}); + setTimeout(function() { + var rect = area.getBoundingClientRect(); + synthesizeMouse(area, rect.width - 5, 5, {}); + }, 0); +}); +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug595310.html b/layout/forms/test/test_bug595310.html new file mode 100644 index 0000000000..47db981167 --- /dev/null +++ b/layout/forms/test/test_bug595310.html @@ -0,0 +1,64 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=595310 +--> +<head> + <title>Test for Bug 595310</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=595310">Mozilla Bug 595310</a> +<p id="display"></p> +<div id="content"> + <input id='i' value="bar"> + <textarea id='t'>bar</textarea> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 595310 **/ + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + // We want to compare to value="bar" and no placeholder shown. + // That is what is currently showed. + var s1 = snapshotWindow(window, false); + + var content = document.getElementById('content'); + var i = document.getElementById('i'); + var t = SpecialPowers.wrap(document.getElementById('t')); + i.value = ""; i.placeholder = "foo"; + t.value = ""; t.placeholder = "foo"; + + // Flushing. + // Note: one call would have been enough actually but I didn't want to favour + // one element... ;) + i.getBoundingClientRect(); + t.getBoundingClientRect(); + + function synthesizeDropText(aElement, aText) + { + SpecialPowers.wrap(aElement).editor.insertText(aText); + } + + // We insert "bar" and we should only see "bar" now. + synthesizeDropText(i, "bar"); + synthesizeDropText(t, "bar"); + + var s2 = snapshotWindow(window, false); + + ok(compareSnapshots(s1, s2, true)[0], + "When setting the value, the placeholder should disappear."); + + SimpleTest.finish(); +}); + + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug620936.html b/layout/forms/test/test_bug620936.html new file mode 100644 index 0000000000..aa1beed01a --- /dev/null +++ b/layout/forms/test/test_bug620936.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=620936 +--> +<head> + <title>Test for Bug 620936</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=620936">Mozilla Bug 620936</a> +<p id="display"></p> +<div id="content"> + <input value="foo"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 620936 **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var i = document.querySelector("input"); + i.focus(); + i.setSelectionRange(100, 100); + is(i.selectionStart, 3, "The selection should be set to the end of the text"); + is(i.selectionEnd, 3, "The selection should be set to the end of the text"); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug644542.html b/layout/forms/test/test_bug644542.html new file mode 100644 index 0000000000..6c33cbe958 --- /dev/null +++ b/layout/forms/test/test_bug644542.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=644542 +--> +<head> + <title>Test for Bug 644542</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style type="text/css"> + select:after { + content: ' appended string'; + } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=644542">Mozilla Bug 644542</a> +<p id="display"> + <form method="post" action=""> + <select id="select"> + <option value="1">1</option> + <option value="2">2</option> + </select> + </form> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 644542 **/ + +var select = document.getElementById("select"); + +var clicks = 0; + +function click() { + synthesizeMouseAtCenter(select, { }); + ++clicks; + + // At least two clicks were required for bug 644542, sometimes more; + // delay is long enough that this doesn't look like a double + // click, and also allows time for popup to show and for painting. + setTimeout(clicks < 4 ? click : done, 500); +} + +function done() { + ok(true, "No crash on opening dropdown"); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); +// waitForFocus is most likely not the right thing to wait for, but +// without this the first click is ineffective (even with a reflow forced +// before synthesizeMouse). +SimpleTest.waitForFocus(click); +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug672810.html b/layout/forms/test/test_bug672810.html new file mode 100644 index 0000000000..991fe57389 --- /dev/null +++ b/layout/forms/test/test_bug672810.html @@ -0,0 +1,120 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=672810 +--> +<head> + <title>Test for Bug 672810</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=672810">Mozilla Bug 672810</a> +<p id="display"></p> +<div id="content"> + <select id="s1" multiple size="10"><option>x<option>x<option>x<option>x<option>x<option>x<option>x<option>x<option>x<option>x</select> + <select id="s2" size="10"><option>x<option>x<option>x<option>x<option>x<option>x<option>x<option>x<option>x<option>x</select> + <select id="s3" size="1"><option>x<option>x<option>x<option>x<option>x<option>x<option>x<option>x<option>x<option>x</select> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 672810 **/ + +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(function() { + var sel = document.getElementsByTagName('select'); + + sel[0].addEventListener('focus', function() { + s = sel[0]; + s.removeEventListener('focus', arguments.callee); + synthesizeKey('KEY_ArrowDown'); + is(s.selectedIndex,0, s.id + ": initial DOWN selects first option"); + synthesizeKey('KEY_ArrowDown', {ctrlKey: true}); + is(s.selectedIndex,0, s.id + ": first option is still selected"); + ok(!s[1].selected,s.id + ": CTRL+DOWN did not select 2nd option"); + synthesizeKey('KEY_ArrowDown', {ctrlKey: true}); + synthesizeKey('KEY_ArrowDown', {ctrlKey: true, shiftKey: true}); + is(s.selectedIndex,0, s.id + ": first option is still selected"); + ok(!s[1].selected,s.id + ": 2nd option is still unselected"); + ok(s[2].selected,s.id + ": 3rd option is selected"); + ok(s[3].selected,s.id + ": 4th option is selected"); + ok(!s[4].selected,s.id + ": 5th option is unselected"); + synthesizeKey('KEY_ArrowDown', {ctrlKey: true, shiftKey: true}); + is(s.selectedIndex,0, s.id + ": first option is still selected"); + ok(!s[1].selected,s.id + ": 2nd option is still unselected"); + ok(s[2].selected,s.id + ": 3rd option is still selected"); + ok(s[3].selected,s.id + ": 4th option is still selected"); + ok(s[4].selected,s.id + ": 5th option is selected"); + ok(!s[5].selected,s.id + ": 6th option is unselected"); + synthesizeKey('KEY_ArrowUp', {ctrlKey: true, shiftKey: true}); + is(s.selectedIndex,0, s.id + ": first option is still selected"); + ok(!s[1].selected,s.id + ": 2nd option is still unselected"); + ok(s[2].selected,s.id + ": 3rd option is still selected"); + ok(s[3].selected,s.id + ": 4th option is still selected"); + ok(s[4].selected,s.id + ": 5th option is still selected"); + ok(!s[5].selected,s.id + ": 6th option is still unselected"); + synthesizeKey(' ', {ctrlKey: true, shiftKey: true}); + is(s.selectedIndex,0, s.id + ": first option is still selected"); + ok(!s[1].selected,s.id + ": 2nd option is still unselected"); + ok(s[2].selected,s.id + ": 3rd option is still selected"); + ok(!s[3].selected,s.id + ": 4th option is unselected"); + ok(s[4].selected,s.id + ": 5th option is still selected"); + ok(!s[5].selected,s.id + ": 6th option is still unselected"); + synthesizeKey(' ', {ctrlKey: true, shiftKey: true}); + is(s.selectedIndex,0, s.id + ": first option is still selected"); + ok(!s[1].selected,s.id + ": 2nd option is still unselected"); + ok(s[2].selected,s.id + ": 3rd option is still selected"); + ok(s[3].selected,s.id + ": 4th option is selected"); + ok(s[4].selected,s.id + ": 5th option is still selected"); + ok(!s[5].selected,s.id + ": 6th option is still unselected"); + setTimeout(function(){sel[1].focus()},0); + }); + sel[1].addEventListener('focus', function() { + s = sel[1]; + s.removeEventListener('focus', arguments.callee); + synthesizeKey('KEY_ArrowDown'); + is(s.selectedIndex,0, s.id + ": initial DOWN selects first option"); + synthesizeKey('KEY_ArrowDown', {ctrlKey: true}); + is(s.selectedIndex,1, s.id + ": 2nd option is selected"); + ok(!s[0].selected,s.id + ": CTRL+DOWN deselected first option"); + synthesizeKey('KEY_ArrowDown', {ctrlKey: true}); + synthesizeKey('KEY_ArrowDown', {ctrlKey: true, shiftKey: true}); + is(s.selectedIndex,3, s.id + ": 4th option is selected"); + ok(!s[1].selected,s.id + ": CTRL+SHIFT+DOWN deselected 2nd option"); + synthesizeKey(' ', {ctrlKey: true, shiftKey: true}); + is(s.selectedIndex,3, s.id + ": 4th option is still selected"); + synthesizeKey(' ', {ctrlKey: true, shiftKey: true}); + is(s.selectedIndex,3, s.id + ": 4th option is still selected"); + setTimeout(function(){sel[2].focus()},0); + }); + sel[2].addEventListener('focus', function() { + if (!navigator.platform.includes("Mac")) { + s = sel[2]; + s.removeEventListener('focus', arguments.callee); + synthesizeKey('KEY_ArrowDown'); + is(s.selectedIndex,1, s.id + ": initial DOWN selects 2nd option"); + synthesizeKey('KEY_ArrowDown', {ctrlKey: true}); + is(s.selectedIndex,2, s.id + ": 3rd option is selected"); + ok(!s[1].selected,s.id + ": CTRL+DOWN deselected 2nd option"); + synthesizeKey('KEY_ArrowDown', {ctrlKey: true, shiftKey: true}); + is(s.selectedIndex,3, s.id + ": 4th option is selected"); + ok(!s[2].selected,s.id + ": CTRL+SHIFT+DOWN deselected 3rd option"); + synthesizeKey(' ', {ctrlKey: true, shiftKey: true}); + is(s.selectedIndex,3, s.id + ": 4th option is still selected"); + synthesizeKey(' ', {ctrlKey: true, shiftKey: true}); + is(s.selectedIndex,3, s.id + ": 4th option is still selected"); + } else { + todo(false, "Make this test work on OSX"); + } + setTimeout(function(){SimpleTest.finish()},0); + }); + sel[0].focus(); +}); + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug704049.html b/layout/forms/test/test_bug704049.html new file mode 100644 index 0000000000..1da2badea2 --- /dev/null +++ b/layout/forms/test/test_bug704049.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=704049 +--> +<head> + <title>Test for Bug 704049</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=704049">Mozilla Bug 704049</a> +<p id="display"></p> +<input type="radio" id="radio11" name="group1"> +<input type="radio" id="radio12" name="group1"> +<input type="radio" id="radio21" name="group2" checked> +<input type="radio" id="radio22" name="group2"> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 704049 **/ + +window.addEventListener("click", function (e) { e.preventDefault(); }); + +function doTest() +{ + var target = document.getElementById("radio11"); + synthesizeMouseAtCenter(target, {}); + is(target.checked, false, "radio11 is checked"); + target = document.getElementById("radio12"); + synthesizeMouseAtCenter(target, {}); + is(target.checked, false, "radio12 is checked"); + target = document.getElementById("radio21"); + synthesizeMouseAtCenter(target, {}); + is(target.checked, true, "radio21 is not checked"); + target = document.getElementById("radio22"); + synthesizeMouseAtCenter(target, {}); + is(target.checked, false, "radio22 is checked"); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(doTest); + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug717878_input_scroll.html b/layout/forms/test/test_bug717878_input_scroll.html new file mode 100644 index 0000000000..6bd27ddacc --- /dev/null +++ b/layout/forms/test/test_bug717878_input_scroll.html @@ -0,0 +1,107 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=717878 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 717878</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=717878">Mozilla Bug 717878</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<!-- size=10 and monospace font ensure there's no overflow in either direction --> +<input id="no-overflow" type="text" + size="10" + style=" + font-family: monospace; + font-size: 1em;" + value="Short"> +<!-- ditto, with appearance:none --> +<input id="no-overflow2" type="text" + size="10" + style=" + -webkit-appearance:none; + font-family: monospace; + font-size: 1em;" + value="Short"> +<!-- size=10, monospace font, and height=0.5em ensure overflow in both directions --> +<input id="overflow" type="text" + size="10" + style=" + font-family: monospace; + font-size: 3em; + height: 0.5em;" + value="This is a long string"> +<!-- ditto, with appearance:none --> +<input id="overflow2" type="text" + size="10" + style=" + -webkit-appearance:none; + font-family: monospace; + font-size: 3em; + height: 0.5em;" + value="This is a long string"> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 717878 **/ + +/** + * Test an element's scroll properties for correctness + * + * @param element Element to test + * @param prop Specify the property to test, + * i.e. "scrollLeft" or "scrollTop" + * @param propMax Specify the scrollMax property to test, + * i.e. "scrollLeftMax" or "scrollTopMax" + * @param is_overflow Specify whether the element is + * scrollable in the above direction + */ +function test_scroll(element, scroll, scrollMax, is_overflow) { + + is(element[scroll], 0, element.id + " initial " + scroll + " != 0"); + if (is_overflow) { + isnot(element[scrollMax], 0, element.id + " " + scrollMax + " == 0"); + } else { + is(element[scrollMax], 0, element.id + " " + scrollMax + " != 0"); + } + + element[scroll] = 10; + if (is_overflow) { + isnot(element[scroll], 0, element.id + " unable to scroll " + scroll); + } else { + is(element[scroll], 0, element.id + " able to scroll " + scroll); + } + + element[scroll] = element[scrollMax]; + is(element[scroll], element[scrollMax], element.id + " did not scroll to " + scrollMax); + + element[scroll] = element[scrollMax] + 10; + is(element[scroll], element[scrollMax], element.id + " scrolled past " + scrollMax); +} + +var no_overflow = document.getElementById("no-overflow"); +test_scroll(no_overflow, "scrollLeft", "scrollLeftMax", /* is_overflow */ false); +test_scroll(no_overflow, "scrollTop", "scrollTopMax", /* is_overflow */ false); + +var no_overflow2 = document.getElementById("no-overflow2"); +test_scroll(no_overflow2, "scrollLeft", "scrollLeftMax", /* is_overflow */ false); +test_scroll(no_overflow2, "scrollTop", "scrollTopMax", /* is_overflow */ false); + +var overflow = document.getElementById("overflow"); +test_scroll(overflow, "scrollLeft", "scrollLeftMax", /* is_overflow */ true); +test_scroll(overflow, "scrollTop", "scrollTopMax", /* is_overflow */ true); + +var overflow2 = document.getElementById("overflow2"); +test_scroll(overflow2, "scrollLeft", "scrollLeftMax", /* is_overflow */ true); +test_scroll(overflow2, "scrollTop", "scrollTopMax", /* is_overflow */ true); + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug869314.html b/layout/forms/test/test_bug869314.html new file mode 100644 index 0000000000..7c786fccfc --- /dev/null +++ b/layout/forms/test/test_bug869314.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=869314 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 869314</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <style type="text/css"> + .selectbox { + background-color: #00FF00; + } + </style> + +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=869314">Mozilla Bug 869314</a> +<p id="display"></p> +<div id="content"> + + <select id="selectbox1" name="non-native selectbox" class="selectbox"> + <option value="item">test item</option> + </select> + + <select id="selectbox2" name="native selectbox"> + <option value="item">test item</option> + </select> + + <script type="application/javascript"> + let Cc = SpecialPowers.Cc; + let Ci = SpecialPowers.Ci; + let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2); + let osName = sysInfo.getProperty("name"); + let isNNT = SpecialPowers.getBoolPref("widget.non-native-theme.enabled"); + if (osName == "Darwin" && !isNNT) { // Native styled macOS form controls. + // This test is for macOS with native styled form controls only. See bug for more info. + ok(document.getElementById("selectbox1").clientWidth > + document.getElementById("selectbox2").clientWidth, + "Non-native styled combobox does not have enough space for a " + + "dropmarker!"); + } else { + // We need to call at least one test function to make the test harness + // happy. + ok(true, "Test wasn't ignored but should have been."); + } + </script> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug903715.html b/layout/forms/test/test_bug903715.html new file mode 100644 index 0000000000..b887b2cd01 --- /dev/null +++ b/layout/forms/test/test_bug903715.html @@ -0,0 +1,81 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=903715 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 903715</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=903715">Mozilla Bug 903715</a> +<p id="display"></p> +<div id="content"> + <form id="form" action="/"> + <select id="select" name="select"> + <option>1</option> + <option>2</option> + <option>3</option> + <option>4</option> + <option>5</option> + <option>6</option> + <option>7</option> + <option>8</option> + <option>9</option> + </select> + <input id="input-text" name="text" value="some text"> + <input id="input-submit" type="submit"> + </form> +</div> +<pre id="test"> +</pre> +<script type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); +SimpleTest.waitForFocus(runTests, window); + +function runTests() +{ + var form = document.getElementById("form"); + form.addEventListener("keypress", function (aEvent) { + ok(false, "keypress event shouldn't be fired when the preceding keydown event caused closing the dropdown of the select element"); + }, true); + form.addEventListener("submit", function (aEvent) { + ok(false, "submit shouldn't be performed by the Enter key press on the select element"); + aEvent.preventDefault(); + }, true); + var select = document.getElementById("select"); + select.addEventListener("change", function (aEvent) { + var input = document.getElementById("input-text"); + input.focus(); + input.select(); + }); + + select.focus(); + + select.addEventListener("popupshowing", function (aEvent) { + setTimeout(function () { + synthesizeKey("KEY_ArrowDown"); + select.addEventListener("popuphiding", function (aEventInner) { + setTimeout(function () { + // Enter key should cause closing the dropdown of the select element + // and keypress event shouldn't be fired on the input element because + // which shouldn't cause sumbmitting the form contents. + ok(true, "Test passes if there is no error"); + SimpleTest.finish(); + }, 100); + }); + // Close dropdown. + synthesizeKey("KEY_Enter"); + }, 100); + }); + + // Open dropdown. + synthesizeKey("KEY_ArrowDown", { altKey: true }); +} +</script> +</body> +</html> diff --git a/layout/forms/test/test_bug935876.html b/layout/forms/test/test_bug935876.html new file mode 100644 index 0000000000..4488fdf962 --- /dev/null +++ b/layout/forms/test/test_bug935876.html @@ -0,0 +1,502 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=935876 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 935876</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=935876">Mozilla Bug 935876</a> +<p id="display"></p> +<div> +<select id="listbox" size="3"> + <option selected>1</option> + <option>2</option> + <option>3</option> + <option>4</option> + <option>5</option> + <option>6</option> + <option>7</option> +</select> +<select id="multipleListbox" size="3" multiple> + <option selected>1</option> + <option>2</option> + <option>3</option> + <option>4</option> + <option>5</option> + <option>6</option> + <option>7</option> +</select> +<select id="combobox"> + <option selected>1</option> + <option>2</option> + <option>3</option> + <option>4</option> + <option>5</option> + <option>6</option> + <option>7</option> +</select> +</div> +<pre id="test"> +</pre> +<script type="application/javascript"> +SimpleTest.waitForExplicitFinish(); + +const kIsWin = navigator.platform.indexOf("Win") == 0; +const kIsMac = navigator.platform.indexOf("Mac") == 0; +const kIsAndroid = navigator.appVersion.indexOf("Android") != 0; + +function runTests() +{ + var doPreventDefault = false; + function onKeydown(aEvent) + { + if (doPreventDefault) { + aEvent.preventDefault(); + } + } + + var keyPressEventFired = false; + function onKeypress(aEvent) + { + keyPressEventFired = true; + } + + var keyDownEventConsumedByJS = false; + var keyDownEventConsumed = false; + function onkeydownInSystemEventGroup(aEvent) + { + keyDownEventConsumedByJS = aEvent.defaultPrevented; + // If defaultPrevented is true via SpecialPowers, that means default was + // prevented, possibly in a non-content-visible way (e.g. by a system + // group handler). + keyDownEventConsumed = SpecialPowers.wrap(aEvent).defaultPrevented; + } + + function reset() + { + keyPressEventFired = false; + keyDownEventConsumedByJS = false; + keyDownEventConsumed = false; + } + + function check(aExpectingKeydownConsumed, aIsPrintableKey, aDescription) + { + if (doPreventDefault) { + ok(!keyPressEventFired, "keypress event shouldn't be fired for " + aDescription + + " if preventDefault() of keydown event was called"); + ok(keyDownEventConsumedByJS, "keydown event of " + aDescription + + " should be consumed in content level if preventDefault() of keydown event is called"); + ok(keyDownEventConsumed, "keydown event of " + aDescription + + " should be consumed in system level if preventDefault() of keydown event is called"); + } else if (aExpectingKeydownConsumed) { + ok(!keyPressEventFired, "keypress event shouldn't be fired for " + aDescription); + ok(!keyDownEventConsumedByJS, "keydown event of " + aDescription + " shouldn't be consumed in content level"); + ok(keyDownEventConsumed, "keydown event of " + aDescription + " should be consumed in system level"); + } else { + if (aIsPrintableKey) { + ok(keyPressEventFired, "keypress event should be fired for printable key, " + aDescription); + } else { + ok(!keyPressEventFired, "keypress event shouldn't be fired for non-printable key, " + aDescription); + } + ok(!keyDownEventConsumedByJS, "keydown event of " + aDescription + " shouldn't be consumed in content level"); + ok(!keyDownEventConsumed, "keydown event of " + aDescription + " should be consumed in system level"); + } + } + + var listbox = document.getElementById("listbox"); + listbox.addEventListener("keydown", onKeydown); + listbox.addEventListener("keypress", onKeypress); + SpecialPowers.addSystemEventListener(listbox, "keydown", onkeydownInSystemEventGroup, false); + + listbox.focus(); + + [ false, true ].forEach(function (consume) { + doPreventDefault = consume; + for (var i = 0; i < listbox.options.length + 1; i++) { + reset() + synthesizeKey("KEY_ArrowDown"); + check(true, false, "DownArrow key on listbox #" + i); + } + + for (var i = 0; i < listbox.options.length + 1; i++) { + reset() + synthesizeKey("KEY_ArrowUp"); + check(true, false, "'ArrowUp' key on listbox #" + i); + } + + for (var i = 0; i < listbox.options.length + 1; i++) { + reset() + synthesizeKey("KEY_ArrowRight"); + check(true, false, "'ArrowRight' key on listbox #" + i); + } + + for (var i = 0; i < listbox.options.length + 1; i++) { + reset() + synthesizeKey("KEY_ArrowLeft"); + check(true, false, "'ArrowLeft' key on listbox #" + i); + } + + for (var i = 0; i < 4; i++) { + reset() + synthesizeKey("KEY_PageDown"); + check(true, false, "'PageDown' key on listbox #" + i); + } + + for (var i = 0; i < 4; i++) { + reset() + synthesizeKey("KEY_PageUp"); + check(true, false, "'PageUp' key on listbox #" + i); + } + + for (var i = 0; i < 2; i++) { + reset() + synthesizeKey("KEY_End"); + check(true, false, "'End' key on listbox #" + i); + } + + for (var i = 0; i < 2; i++) { + reset() + synthesizeKey("KEY_Home"); + check(true, false, "'Home' key on listbox #" + i); + } + + reset() + synthesizeKey("KEY_Enter"); + check(false, true, "'Enter' key on listbox"); + + reset() + synthesizeKey("KEY_Escape"); + check(false, false, "'Escape' key on listbox"); + + reset() + synthesizeKey("KEY_F4"); + check(false, false, "F4 key on listbox"); + + reset() + sendString("a"); + check(false, true, "'A' key on listbox"); + }); + + listbox.removeEventListener("keydown", onKeydown); + listbox.removeEventListener("keypress", onKeypress); + SpecialPowers.removeSystemEventListener(listbox, "keydown", onkeydownInSystemEventGroup, false); + + + + var multipleListbox = document.getElementById("multipleListbox"); + multipleListbox.addEventListener("keydown", onKeydown); + multipleListbox.addEventListener("keypress", onKeypress); + SpecialPowers.addSystemEventListener(multipleListbox, "keydown", onkeydownInSystemEventGroup, false); + + multipleListbox.focus(); + + [ false, true ].forEach(function (consume) { + doPreventDefault = consume; + for (var i = 0; i < multipleListbox.options.length + 1; i++) { + reset() + synthesizeKey("KEY_ArrowDown"); + check(true, false, "'ArrowDown' key on multiple listbox #" + i); + } + + for (var i = 0; i < multipleListbox.options.length + 1; i++) { + reset() + synthesizeKey("KEY_ArrowUp"); + check(true, false, "'ArrowUp' key on multiple listbox #" + i); + } + + for (var i = 0; i < multipleListbox.options.length + 1; i++) { + reset() + synthesizeKey("KEY_ArrowRight"); + check(true, false, "'ArrowRight' key on multiple listbox #" + i); + } + + for (var i = 0; i < multipleListbox.options.length + 1; i++) { + reset() + synthesizeKey("KEY_ArrowLeft"); + check(true, false, "'ArrowLeft' key on multiple listbox #" + i); + } + + for (var i = 0; i < 4; i++) { + reset() + synthesizeKey("KEY_PageDown"); + check(true, false, "'PageDown' key on multiple listbox #" + i); + } + + for (var i = 0; i < 4; i++) { + reset() + synthesizeKey("KEY_PageUp"); + check(true, false, "'PageUp' key on multiple listbox #" + i); + } + + for (var i = 0; i < 2; i++) { + reset() + synthesizeKey("KEY_End"); + check(true, false, "'End' key on multiple listbox #" + i); + } + + for (var i = 0; i < 2; i++) { + reset() + synthesizeKey("KEY_Home"); + check(true, false, "'Home' key on multiple listbox #" + i); + } + + reset() + synthesizeKey("KEY_Enter"); + check(true, true, "'Enter' key on multiple listbox"); + + reset() + synthesizeKey("KEY_Escape"); + check(false, false, "'Escape' key on multiple listbox"); + + reset() + synthesizeKey("KEY_F4"); + check(false, false, "'F4' key on multiple listbox"); + + reset() + sendString("a"); + check(false, true, "'A' key on multiple listbox"); + }); + + multipleListbox.removeEventListener("keydown", onKeydown); + multipleListbox.removeEventListener("keypress", onKeypress); + SpecialPowers.removeSystemEventListener(multipleListbox, "keydown", onkeydownInSystemEventGroup, false); + + + + var combobox = document.getElementById("combobox"); + combobox.addEventListener("keydown", onKeydown); + combobox.addEventListener("keypress", onKeypress); + SpecialPowers.addSystemEventListener(combobox, "keydown", onkeydownInSystemEventGroup, false); + + combobox.focus(); + + [ false, true ].forEach(function (consume) { + doPreventDefault = consume; + if (!kIsMac) { + for (var i = 0; i < combobox.options.length + 1; i++) { + reset() + synthesizeKey("KEY_ArrowDown"); + check(true, false, "'ArrowDown' key on combobox #" + i); + } + + for (var i = 0; i < combobox.options.length + 1; i++) { + reset() + synthesizeKey("KEY_ArrowUp"); + check(true, false, "'ArrowUp' key on combobox #" + i); + } + } else { + todo(false, "Make this test work on OSX"); + } + + for (var i = 0; i < combobox.options.length + 1; i++) { + reset() + synthesizeKey("KEY_ArrowRight"); + check(true, false, "'ArrowRight' key on combobox #" + i); + } + + for (var i = 0; i < combobox.options.length + 1; i++) { + reset() + synthesizeKey("KEY_ArrowLeft"); + check(true, false, "'ArrowLeft' key on combobox #" + i); + } + + for (var i = 0; i < 4; i++) { + reset() + synthesizeKey("KEY_PageDown"); + check(true, false, "'PageDown' key on combobox #" + i); + } + + for (var i = 0; i < 4; i++) { + reset() + synthesizeKey("KEY_PageUp"); + check(true, false, "'PageUp' key on combobox #" + i); + } + + for (var i = 0; i < 2; i++) { + reset() + synthesizeKey("KEY_End"); + check(true, false, "'End' key on combobox #" + i); + } + + for (var i = 0; i < 2; i++) { + reset() + synthesizeKey("KEY_Home"); + check(true, false, "'Home' key on combobox #" + i); + } + + reset() + synthesizeKey("KEY_Enter"); + check(false, true, "'Enter' key on combobox"); + + reset() + synthesizeKey("KEY_Escape"); + check(false, false, "'Escape' key on combobox"); + + if (!kIsWin) { + reset() + synthesizeKey("KEY_F4"); + check(false, false, "'F4' key on combobox"); + } + + reset() + sendString("a"); + check(false, true, "'A' key on combobox"); + }); + + function finish() + { + combobox.removeEventListener("keydown", onKeydown); + combobox.removeEventListener("keypress", onKeypress); + SpecialPowers.removeSystemEventListener(combobox, "keydown", onkeydownInSystemEventGroup, false); + SimpleTest.finish(); + } + + // Mac uses native popup for dropdown. Let's skip the tests for popup + // since it's not handled in nsListControlFrame. + // Similarly, Android doesn't use popup for dropdown. + if (kIsMac || kIsAndroid) { + finish(); + return; + } + + function testDropDown(aCallback) + { + testOpenDropDown(function () { + reset() + synthesizeKey("KEY_ArrowDown", {altKey: true}); + }, function () { + check(true, false, "Alt + DownArrow key on combobox at opening dropdown"); + + for (var i = 0; i < combobox.options.length + 1; i++) { + reset() + synthesizeKey("KEY_ArrowDown"); + check(true, false, "'ArrowDown' key on combobox during dropdown open #" + i); + } + + for (var i = 0; i < combobox.options.length + 1; i++) { + reset() + synthesizeKey("KEY_ArrowUp"); + check(true, false, "'ArrowUp' key on combobox during dropdown open #" + i); + } + + for (var i = 0; i < combobox.options.length + 1; i++) { + reset() + synthesizeKey("KEY_ArrowRight"); + check(true, false, "'ArrowRight' key on combobox during dropdown open #" + i); + } + + for (var i = 0; i < combobox.options.length + 1; i++) { + reset() + synthesizeKey("KEY_ArrowLeft"); + check(true, false, "'ArrowLeft' key on combobox during dropdown open #" + i); + } + + for (var i = 0; i < 4; i++) { + reset() + synthesizeKey("KEY_PageDown"); + check(true, false, "'PageDown' key on combobox during dropdown open #" + i); + } + + for (var i = 0; i < 4; i++) { + reset() + synthesizeKey("KEY_PageUp"); + check(true, false, "'PageUp' key on combobox during dropdown open #" + i); + } + + for (var i = 0; i < 2; i++) { + reset() + synthesizeKey("KEY_End"); + check(true, false, "'End' key on combobox during dropdown open #" + i); + } + + for (var i = 0; i < 2; i++) { + reset() + synthesizeKey("KEY_Home"); + check(true, false, "'Home' key on combobox during dropdown open #" + i); + } + + testCloseDropDown(function () { + reset() + synthesizeKey("KEY_Enter"); + }, function () { + testOpenDropDown(function () { + check(true, true, "'Enter' key on combobox at closing dropdown"); + + synthesizeKey("KEY_ArrowUp", {altKey: true}); + }, function () { + check(true, false, "'Alt' + 'ArrowUp' key on combobox at opening dropdown"); + + testCloseDropDown(function () { + reset() + synthesizeKey("KEY_Escape"); + }, function () { + check(true, false, "'Escape' key on combobox at closing dropdown"); + + // F4 key opens/closes dropdown only on Windows. So, other platforms + // don't need to do anymore. + if (!kIsWin) { + aCallback(); + return; + } + + testOpenDropDown(function () { + reset() + synthesizeKey("KEY_F4"); + }, function () { + check(true, false, "'F4' key on combobox at opening dropdown on Windows"); + + testCloseDropDown(function () { + reset() + synthesizeKey("KEY_F4"); + }, function () { + check(true, false, "'F4' key on combobox at closing dropdown on Windows"); + + aCallback(); + return; + }); + }); + }); + }); + }); + }); + } + + doPreventDefault = false; + testDropDown(function () { + // Even if keydown event is consumed by JS, opening/closing dropdown + // should work for a11y and security (e.g., cannot close dropdown causes + // staying top-most window on the screen). If it's blocked by JS, this + // test would cause permanent timeout. + doPreventDefault = true; + testDropDown(finish); + }); +} + +function testOpenDropDown(aTest, aOnOpenDropDown) +{ + document.addEventListener("popupshowing", function (aEvent) { + document.removeEventListener(aEvent.type, arguments.callee); + setTimeout(aOnOpenDropDown, 0); + }); + aTest(); +} + +function testCloseDropDown(aTest, aOnCloseDropDown) +{ + document.addEventListener("popuphiding", function (aEvent) { + document.removeEventListener(aEvent.type, arguments.callee); + setTimeout(aOnCloseDropDown, 0) + }); + aTest(); +} + +SimpleTest.waitForFocus(runTests); +</script> +</body> +</html> diff --git a/layout/forms/test/test_bug957562.html b/layout/forms/test/test_bug957562.html new file mode 100644 index 0000000000..52821d8757 --- /dev/null +++ b/layout/forms/test/test_bug957562.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=957562 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 903715</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=957562">Mozilla Bug 957562</a> +<p id="display"></p> +<input id="n" onfocus="kill()" type="number" style="border:20px solid black"> +<pre id="test"> +</pre> +<script type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests, window); + +var killed = false; +function kill() { + if (killed) { + return; + } + killed = true; + n.style.display = 'none'; + r = n.getBoundingClientRect(); + setTimeout(function() { + ok(true, "Didn't crash"); + SimpleTest.finish(); + }, 0); +} + +function runTests() +{ + synthesizeMouse(n, 2, 2, {}); +} +</script> +</body> +</html> diff --git a/layout/forms/test/test_bug960277.html b/layout/forms/test/test_bug960277.html new file mode 100644 index 0000000000..28981b121a --- /dev/null +++ b/layout/forms/test/test_bug960277.html @@ -0,0 +1,29 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=960277 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 903715</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=960277">Mozilla Bug 960277</a> +<p id="display"></p> +<fieldset style="position:relative; height:100px; margin:50px; background:blue;"> +<legend style="background:purple"> + <div id="d" style="position:absolute; background:yellow; top:0; left:0; width:50px; height:50px;"></div> +</legend> +</fieldset> +<pre id="test"> +</pre> +<script type="application/javascript"> +var rect = d.getBoundingClientRect(); +is(document.elementFromPoint(rect.left + 10, rect.top + 10), d, + "Hit testing yellow div"); +</script> +</body> +</html> diff --git a/layout/forms/test/test_listcontrol_search.html b/layout/forms/test/test_listcontrol_search.html new file mode 100644 index 0000000000..d696edae66 --- /dev/null +++ b/layout/forms/test/test_listcontrol_search.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=849438 +--> +<head> + <meta charset="utf-8"> + <title>Test for <select> list control search</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** This test will focus a select element and press a key that matches the + first non-space character of an entry. **/ + + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(function() { + var select = document.getElementsByTagName('select')[0]; + select.focus(); + sendString('a'); + + is(select.options[0].selected, false, "the first option isn't selected"); + is(select.options[1].selected, true, "the second option is selected"); + + select.blur(); + + SimpleTest.finish(); + }); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=849438">Mozilla Bug 849438</a> +<p id="display"></p> +<div id="content"> + <select> + <option>Please select an entry</option> + <option> a</option> + <option> b</option> + </select> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_readonly.html b/layout/forms/test/test_readonly.html new file mode 100644 index 0000000000..3bfb768f9c --- /dev/null +++ b/layout/forms/test/test_readonly.html @@ -0,0 +1,58 @@ +<!doctype html> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="should-apply"> + <textarea></textarea> + <input type="text"> + <input type="password"> + <input type="search"> + <input type="tel"> + <input type="email"> + <input type="url"> + <input type="number"> + <input type="date"> + <input type="time"> + <input type="month"> + <input type="week"> + <input type="datetime-local"> +</div> +<div id="should-not-apply"> + <input type="hidden"> + <input type="button"> + <input type="image"> + <input type="reset"> + <input type="submit"> + <input type="radio"> + <input type="file"> + <input type="checkbox"> + <input type="range"> + <input type="color"> +</div> +<script> +for (const element of Array.from(document.querySelectorAll('#should-apply *'))) { + let elementDesc = element.tagName.toLowerCase(); + if (elementDesc === "input") + elementDesc += ` type="${element.type}"`; + test(function() { + assert_false(element.matches(':read-only'), "Shouldn't be initially read-only"); + assert_true(element.matches(':read-write'), "Thus should be read-write"); + element.setAttribute("readonly", "readonly"); + assert_true(element.matches(':read-only'), "Should become read-only"); + assert_false(element.matches(':read-write'), "Thus should stop being read-write"); + }, elementDesc); +} + +for (const element of Array.from(document.querySelectorAll('#should-not-apply *'))) { + let elementDesc = element.tagName.toLowerCase(); + if (elementDesc === "input") + elementDesc += ` type="${element.type}"`; + test(function() { + assert_true(element.matches(':read-only'), "Should match read-only"); + assert_false(element.matches(':read-write'), "Should not be read-write"); + element.setAttribute("readonly", "readonly"); + assert_true(element.matches(':read-only'), "Should keep matching read-only"); + assert_false(element.matches(':read-write'), "Should still not be read-write"); + }, elementDesc); +} +</script> diff --git a/layout/forms/test/test_select_collapsed_page_keys.html b/layout/forms/test/test_select_collapsed_page_keys.html new file mode 100644 index 0000000000..08c1615152 --- /dev/null +++ b/layout/forms/test/test_select_collapsed_page_keys.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<title>Test for page up/down in collapsed select (bug 1488828)</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script> + SimpleTest.waitForExplicitFinish(); + + function test() { + let select = document.getElementById("select"); + select.focus(); + is(select.selectedIndex, 0, "Option 0 initially selected"); + synthesizeKey("KEY_PageDown", {}); + ok(select.selectedIndex >= 2, "PageDown skips more than 1 option"); + ok(select.selectedIndex < 49, "PageDown does not move to the last option"); + synthesizeKey("KEY_PageUp", {}); + is(select.selectedIndex, 0, "PageUp skips more than 1 option"); + SimpleTest.finish(); + } +</script> +</head> +<body onload="test()"> +<div> + <select id="select" size="1"> + <option selected>0</option> + </select> + <script> + // Add more options so we have 50 in total. + let select = document.getElementById("select"); + for (let i = 1; i <= 49; ++i) { + let option = document.createElement("option"); + option.textContent = i; + select.appendChild(option); + } + </script> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_select_key_navigation_bug1498769.html b/layout/forms/test/test_select_key_navigation_bug1498769.html new file mode 100644 index 0000000000..3574761e66 --- /dev/null +++ b/layout/forms/test/test_select_key_navigation_bug1498769.html @@ -0,0 +1,123 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1498769 +--> +<head> +<meta charset="utf-8"> +<title>Test for Bug 1498769</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script type="application/javascript"> + /** Test for Bug 1498769 **/ + + SimpleTest.waitForExplicitFinish(); + + function test() { + const kIsMac = navigator.platform.indexOf("Mac") == 0; + SimpleTest.waitForFocus(function() { + [...document.querySelectorAll('select')].forEach(function(e) { + e.focus(); + const description = ` (size ${e.size})`; + is(e.selectedIndex, 1, "the 'selected' attribute is respected" + description); + if (kIsMac && e.size == "1") { + // On OSX, UP/DOWN opens the dropdown menu rather than changing + // the value so we skip the rest of this test there in this case. + return; + } + synthesizeKey("VK_DOWN", {}); + is(e.selectedIndex, 2, "VK_DOWN selected the first option below" + description); + synthesizeKey("VK_UP", {}); + is(e.selectedIndex, 0, "VK_UP skips the display:none/contents option" + description); + synthesizeKey("VK_DOWN", {}); + is(e.selectedIndex, 2, "VK_DOWN skips the display:none/contents option" + description); + }); + SimpleTest.finish(); + }); + } +</script> +</head> +<body onload="test()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1498769">Mozilla Bug 1498769</a> +<div> + <select size="4"> + <option>0</option> + <option selected style="display:none">1</option> + <option>2</option> + <option>3</option> + </select> + <select size="4"> + <option>0</option> + <option selected style="display:contents">1</option> + <option>2</option> + <option>3</option> + </select> + <select size="4"> + <option>0</option> + <optgroup label="group" style="display:none"> + <option selected>1</option> + </optgroup> + <option>2</option> + <option>3</option> + </select> + <select size="4"> + <option>0</option> + <optgroup label="group" style="display:contents"> + <option selected>1</option> + </optgroup> + <option>2</option> + <option>3</option> + </select> + <select size="4"> + <option>0</option> + <optgroup label="group" style="display:contents"> + <option selected style="display:none">1</option> + </optgroup> + <option>2</option> + <option>3</option> + </select> + +<!-- Same as above but with size="1" --> + + <select size="1"> + <option>0</option> + <option selected style="display:none">1</option> + <option>2</option> + <option>3</option> + </select> + <select size="1"> + <option>0</option> + <option selected style="display:contents">1</option> + <option>2</option> + <option>3</option> + </select> + <select size="1"> + <option>0</option> + <optgroup label="group" style="display:none"> + <option selected>1</option> + </optgroup> + <option>2</option> + <option>3</option> + </select> + <select size="1"> + <option>0</option> + <optgroup label="group" style="display:contents"> + <option selected>1</option> + </optgroup> + <option>2</option> + <option>3</option> + </select> + <select size="1"> + <option>0</option> + <optgroup label="group" style="display:contents"> + <option selected style="display:none">1</option> + </optgroup> + <option>2</option> + <option>3</option> + </select> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_select_key_navigation_bug961363.html b/layout/forms/test/test_select_key_navigation_bug961363.html new file mode 100644 index 0000000000..7815149778 --- /dev/null +++ b/layout/forms/test/test_select_key_navigation_bug961363.html @@ -0,0 +1,131 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=961363 +--> +<head> +<meta charset="utf-8"> +<title>Test for Bug 961363</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script type="application/javascript"> + /** Test for Bug 961363 **/ + + SimpleTest.waitForExplicitFinish(); + + function test() { + SimpleTest.waitForFocus(function() { + const single_list = [ + {key: "DOWN", change: true, state: [false, false, true, false]}, + {key: "UP", change: true, state: [false, true, false, false]}, + {key: "RIGHT", change: true, state: [false, false, true, false]}, + {key: "LEFT", change: true, state: [false, true, false, false]}, + {key: "END", change: true, state: [false, false, false, true ]}, + {key: "HOME", change: true, state: [true, false, false, false]}, + {key: "PAGE_DOWN", change: false, state: [true, false, false, false]}, + {key: "PAGE_UP", change: false, state: [true, false, false, false]} + ]; + + const single_dropdown = [ + {key: "DOWN", change: true, state: [false, false, true, false]}, + {key: "UP", change: true, state: [false, true, false, false]}, + {key: "RIGHT", change: true, state: [false, false, true, false]}, + {key: "LEFT", change: true, state: [false, true, false, false]}, + {key: "END", change: true, state: [false, false, false, true ]}, + {key: "HOME", change: true, state: [true, false, false, false]}, + {key: "PAGE_DOWN", change: false, state: [true, false, false, false]}, + {key: "PAGE_UP", change: false, state: [true, false, false, false]} + ]; + + const multiple = [ + {key: "DOWN", change: false, state: [false, true, true, false]}, + {key: "UP", change: false, state: [false, false, true, false]}, + {key: "RIGHT", change: false, state: [false, false, false, false]}, + {key: "LEFT", change: false, state: [false, true, false, false]}, + {key: "PAGE_DOWN", change: false, state: [false, true, false, true ]}, + {key: "PAGE_UP", change: false, state: [false, false, false, true ]}, + {key: "END", change: false, state: [false, false, false, false]}, + {key: "HOME", change: false, state: [true, false, false, false]} + ]; + + function select_test(id, tests) { + let element = document.getElementById(id); + element.focus(); + tests.forEach(data => { + let previousValue = element.value; + let key = data.k; + synthesizeKey("VK_" + data.key, {shiftKey: false, metaKey: false, + ctrlKey: true }); + (data.change ? isnot : is)( + element.value, previousValue, + `value should ${data.change ? "": "not "} have changed while testing CTRL+${data.key} (id: ${id})` + ); + + // Hit ctrl+space, but only for <select multiple> elements; doing so + // for single <select> elements will just trigger the dropdown to + // open. This is especially important because e10s-backed dropdowns + // behave differently: their .value isn't updated until the dropdown + // is closed (and the change confirmed), e.g. by pressing Enter. + let action; + if (element.multiple) { + synthesizeKey(" ", {shiftKey: false, metaKey: false, + ctrlKey: true}); + action = `CTRL+SPACE (after testing CTRL+${data.key})`; + } else { + action = `testing CTRL+${data.key}`; + } + + let selected = [...element.options].map(o => o.selected); + is(selected.toString(), data.state.toString(), + `selected options match after ${action} (id: ${id})`); + }); + } + + select_test("single-list", single_list); + if (!navigator.platform.includes("Mac")) { + select_test("single-dropdown", single_dropdown); + } else { + todo(false, "Make these tests work on OSX"); + } + + select_test("multiple", multiple); + SimpleTest.finish(); + }); + } +</script> +</head> +<body onload="test();"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=961363">Mozilla Bug 961363</a> +<div> + <ul> + <li> + <select id="single-list" size="3"> + <option>0</option> + <option selected>1</option> + <option>2</option> + <option>3</option> + </select> + </li> + <li> + <select id="single-dropdown" size="1"> + <option>0</option> + <option selected>1</option> + <option>2</option> + <option>3</option> + </select> + </li> + <li> + <select id="multiple" multiple size="3"> + <option>0</option> + <option selected>1</option> + <option>2</option> + <option>3</option> + </select> + </li> + </ul> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_select_prevent_default.html b/layout/forms/test/test_select_prevent_default.html new file mode 100644 index 0000000000..4ad9db0580 --- /dev/null +++ b/layout/forms/test/test_select_prevent_default.html @@ -0,0 +1,116 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=291082 +--> +<head> +<meta charset="utf-8"> +<title>Test for Bug 291082</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script type="application/javascript"> + /** Test for Bug 291082 **/ + + + SimpleTest.waitForExplicitFinish(); + + function preventDefault(event) { + event.preventDefault(); + } + + function runTest() { + document.getElementById("keydown").addEventListener("keydown", preventDefault); + document.getElementById("keypress").addEventListener("keypress", preventDefault); + + SimpleTest.waitForFocus(function() { + if (navigator.platform.indexOf("Mac") == 0) { + todo(false, "Make this test work on OSX"); + SimpleTest.finish(); + return; + } + var testData = [ "one", "two", "three", "four", "keydown", "keypress" ]; + + // The order of the keys in otherKeys is important for the test to function properly. + var otherKeys = [ "DOWN", "UP", "RIGHT", "LEFT", "PAGE_DOWN", "PAGE_UP", + "END", "HOME" ]; + + testData.forEach(function(id) { + var element = document.getElementById(id); + element.focus(); + var previousValue = element.value; + sendChar('2'); + is(element.value, previousValue, "value should not have changed (id: " + id + ")"); + previousValue = element.value; + otherKeys.forEach(function(key) { + sendKey(key); + // All these preventDefault on key down in various ways. + let shouldchange = id != "keydown" && id != "one" && id != "three"; + (shouldchange ? isnot : is)(element.value, previousValue, "value should " + (shouldchange ? "" : "not ") + "have changed while testing key " + key + " (id: " + id + ")"); + previousValue = element.value; + }); + }); + SimpleTest.finish(); + }); + } +</script> +</head> +<body onload="runTest();"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=291082">Mozilla Bug 291082</a> +<div> + <ul> + <li> + <select id="one" onkeydown="event.preventDefault();"> + <option>0</option> + <option>1</option> + <option>2</option> + </select> + select onkeydown="event.preventDefault();" + </li> + <li> + <select id="two" onkeypress="event.preventDefault();"> + <option>0</option> + <option>1</option> + <option>2</option> + </select> + select onkeypress="event.preventDefault();" + </li> + <li onkeydown="event.preventDefault();"> + <select id="three"> + <option>0</option> + <option>1</option> + <option>2</option> + </select> + li onkeydown="event.preventDefault();" + </li> + <li onkeypress="event.preventDefault();"> + <select id="four"> + <option>0</option> + <option>1</option> + <option>2</option> + </select> + li onkeypress="event.preventDefault();" + </li> + <li> + <select id="keydown"> + <option>0</option> + <option>1</option> + <option>2</option> + </select> + select.addEventListener("keydown", function(event) { event.preventDefault(); }); + </li> + <li> + <select id="keypress"> + <option>0</option> + <option>1</option> + <option>2</option> + <option>9</option> + </select> + select.addEventListener("keypress", function(event) { event.preventDefault(); }); + </li> + </ul> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_select_reframe.html b/layout/forms/test/test_select_reframe.html new file mode 100644 index 0000000000..666d8074b8 --- /dev/null +++ b/layout/forms/test/test_select_reframe.html @@ -0,0 +1,52 @@ +<!doctype html> +<meta charset="utf-8"> +<title>Test for page up/down in collapsed select (bug 1488828)</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<style> +.reframe { + display: flex; +} +</style> +<div id="container"> + <select> + <option>ABC</option> + <option>DEF</option> + </select> +</div> +<script> +(async function() { + SimpleTest.waitForExplicitFinish(); + + const utils = SpecialPowers.DOMWindowUtils; + const select = document.querySelector("select"); + await SimpleTest.promiseFocus(window); + + ok(!select.openInParentProcess, "Should not be open") + + select.focus(); + synthesizeKey("VK_SPACE"); + + ok(SpecialPowers.wrap(select).openInParentProcess, "Should open"); + + const container = document.getElementById("container"); + container.getBoundingClientRect(); // flush layout + + const frameCountBeforeReframe = utils.framesConstructed; + + container.classList.add("reframe"); + + container.getBoundingClientRect(); // flush layout + + ok(utils.framesConstructed > frameCountBeforeReframe, "Should have reframed"); + ok(SpecialPowers.wrap(select).openInParentProcess, "Should remain open"); + + select.remove(); + + container.getBoundingClientRect(); // flush layout + ok(!SpecialPowers.wrap(select).openInParentProcess, "Should close after removal"); + + SimpleTest.finish(); +}()); +</script> diff --git a/layout/forms/test/test_select_vertical.html b/layout/forms/test/test_select_vertical.html new file mode 100644 index 0000000000..51c6208bd3 --- /dev/null +++ b/layout/forms/test/test_select_vertical.html @@ -0,0 +1,75 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Test for select popup in vertical writing mode</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<script> +SimpleTest.waitForExplicitFinish(); + +function test() { + SimpleTest.waitForFocus(function() { + var vlr = document.getElementById("vlr"); + var selWidth = vlr.offsetWidth; + var optWidth = vlr.children[0].offsetWidth; + + // We should be able to choose options from the vertical-lr <select> + // at positions increasingly to the right of the element itself. + is(vlr.value, "1", "(vertical-lr) initial value should be 1"); + + synthesizeMouse(vlr, 5, 5, { type : "mousedown", button: 0 }); + synthesizeMouse(vlr, selWidth + 1.5 * optWidth, 5, { type : "mouseup", button: 0 }); + + is(vlr.value, "2", "(vertical-lr) new value should be 2"); + + synthesizeMouse(vlr, 5, 5, { type : "mousedown", button: 0 }); + synthesizeMouse(vlr, selWidth + 2.5 * optWidth, 5, { type : "mouseup", button: 0 }); + + is(vlr.value, "3", "(vertical-lr) new value should be 3"); + + synthesizeMouse(vlr, 5, 5, { type : "mousedown", button: 0 }); + synthesizeMouse(vlr, selWidth + 0.5 * optWidth, 5, { type : "mouseup", button: 0 }); + + is(vlr.value, "1", "(vertical-lr) value should be back to 1"); + + var vrl = document.getElementById("vrl"); + + // We should be able to choose options from the vertical-rl <select> + // at positions increasingly to the left of the element itself. + is(vrl.value, "1", "(vertical-rl) initial value should be 1"); + + synthesizeMouse(vrl, 5, 5, { type : "mousedown", button: 0 }); + synthesizeMouse(vrl, -1.5 * optWidth, 5, { type : "mouseup", button: 0 }); + + is(vrl.value, "2", "(vertical-rl) new value should be 2"); + + synthesizeMouse(vrl, 5, 5, { type : "mousedown", button: 0 }); + synthesizeMouse(vrl, -2.5 * optWidth, 5, { type : "mouseup", button: 0 }); + + is(vrl.value, "3", "(vertical-rl) new value should be 3"); + + synthesizeMouse(vrl, 5, 5, { type : "mousedown", button: 0 }); + synthesizeMouse(vrl, -0.5 * optWidth, 5, { type : "mouseup", button: 0 }); + + is(vrl.value, "1", "(vertical-rl) value should be back to 1"); + + SimpleTest.finish(); + }); +} +</script> + +<body onload="test();"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1113206">Mozilla Bug 1113206</a> +<div> + <select id="vlr" style="writing-mode: vertical-lr; margin: 80px;"> + <option value="1">one + <option value="2">two + <option value="3">three + <option value="4">four + </select> + <select id="vrl" style="writing-mode: vertical-rl; margin: 80px;"> + <option value="1">one + <option value="2">two + <option value="3">three + <option value="4">four + </select> +</div> diff --git a/layout/forms/test/test_textarea_resize.html b/layout/forms/test/test_textarea_resize.html new file mode 100644 index 0000000000..93889a0d17 --- /dev/null +++ b/layout/forms/test/test_textarea_resize.html @@ -0,0 +1,102 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Bug 477700</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<div id="content" style="display: none"> +</div> + +<textarea id="textarea" style="-moz-appearance: none; border: 2px solid black; padding: 3px; box-sizing: border-box; min-width: 15px; min-height: 15px;">Text</textarea> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for textbox resizing **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(() => SimpleTest.executeSoon(doTheTest)); + +// -1 means use the default value which is 'both', then test explicitly +// setting each possible value. +var currentResize = -1; +var currentBoxSizing = 0; +var currentPointer = 0; +var resizeTypes = [ "horizontal", "vertical", "none", "inherit", "both" ]; +var boxSizingTypes = [ "", "border-box" ]; +var pointerTypes = [ synthesizeMouse, synthesizeTouch] + +function doTheTest() { + runTest(pointerTypes[currentPointer]); +} + +function runTest(aPointerFunc) { + var boxSizingText = " with box sizing " + (currentBoxSizing ? boxSizingTypes[currentBoxSizing] : "content-box"); + + var textarea = $("textarea"); + var rect = textarea.getBoundingClientRect(); + var touch = aPointerFunc.name.match(/Touch/); + // -1 means use the default value of resize, i.e. "both" + var type = (currentResize == -1) ? "both" : resizeTypes[currentResize]; + // assume that the resizer is in the lower right corner + + aPointerFunc(textarea, rect.width - 10, rect.height - 10, { type: touch ? "touchstart" : "mousedown" }); + aPointerFunc(textarea, rect.width + 40, rect.height + 40, { type: touch ? "touchmove" : "mousemove" }); + + var newrect = textarea.getBoundingClientRect(); + var hchange = (type == "both" || type == "horizontal"); + var vchange = (type == "both" || type == "vertical"); + + is(Math.round(newrect.width), Math.round(rect.width + (hchange ? 50 : 0)), + type + " width has increased" + boxSizingText + " using " + aPointerFunc.name); + is(Math.round(newrect.height), Math.round(rect.height + (vchange ? 50 : 0)), + type + " height has increased" + boxSizingText + " using " + aPointerFunc.name); + + aPointerFunc(textarea, rect.width - 20, rect.height - 20, { type: touch ? "touchmove" : "mousemove" }); + + newrect = textarea.getBoundingClientRect(); + + is(Math.round(newrect.width), Math.round(rect.width - (hchange ? 10 : 0)), + type + " width has decreased" + boxSizingText + " using " + aPointerFunc.name); + is(Math.round(newrect.height), Math.round(rect.height - (vchange ? 10 : 0)), + type + " height has decreased" + boxSizingText + " using " + aPointerFunc.name); + + aPointerFunc(textarea, rect.width - 220, rect.height - 220, { type: touch ? "touchmove" : "mousemove" }); + + newrect = textarea.getBoundingClientRect(); + ok(hchange ? newrect.width >= 15 : Math.round(newrect.width) == Math.round(rect.width), + type + " width decreased below minimum" + boxSizingText + " using " + newrect.width); + ok(vchange ? newrect.height >= 15 : Math.round(newrect.height) == Math.round(rect.height), + type + " height decreased below minimum" + boxSizingText + " using " + aPointerFunc.name); + + aPointerFunc(textarea, rect.width - 8, rect.height - 8, { type: touch ? "touchend" : "mouseup" }); + + textarea.style.width = "auto"; + textarea.style.height = "auto"; + + if (currentBoxSizing++ <= boxSizingTypes.length) { + textarea.style.MozBoxSizing = boxSizingTypes[currentBoxSizing]; + SimpleTest.executeSoon(doTheTest); + } else { + currentBoxSizing = 0; + if (++currentResize < resizeTypes.length) { + textarea.style.resize = resizeTypes[currentResize]; + SimpleTest.executeSoon(doTheTest); + } else { + currentResize = -1; + textarea.style.resize = ""; + if (++currentPointer < pointerTypes.length) { + SimpleTest.executeSoon(doTheTest); + } else { + SimpleTest.finish(); + } + } + } +} + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_unstyled_control_height.html b/layout/forms/test/test_unstyled_control_height.html new file mode 100644 index 0000000000..ad5cd125d2 --- /dev/null +++ b/layout/forms/test/test_unstyled_control_height.html @@ -0,0 +1,72 @@ +<!doctype html> +<meta charset="utf-8"> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<style> + #container *, #container2 * { + white-space: nowrap; + appearance: none; + } + input { + /* Reduce the width so that container can fit all its children in 600px viewport width. */ + width: 100px; + } + /* date by default uses a monospace font, which might have different metrics */ + input, button, select { + font: Menu; + } + .small-font * { + font-size: 8px !important; /* important to override rule above */ + } + .no-padding * { + padding: 0; + } +</style> + +<!-- Each container should fit all its children in the same line to verify every + child has the same |top|. --> +<div id="container"> + <input> + <!-- Putting the <input> containing Burmese characters here is just to verify + our current behavior. They are slightly clipped. So if we fix it by + making the <input> taller, it's OK to remove it from this test. --> + <input value="漢字 jpg မြန်မာစာ"> + <input type=date> + <button>Foo</button> + <select><option>Foo</option></select> +</div> + +<br> +<div id="container2"> + <button>漢字 Foo မြန်မာစာ</button> + <select><option>漢字 Foo မြန်မာစာ</option></select> +</div> + +<script> +function testHeightMatches(id, desc) { + let commonHeight = null; + let commonTop = null; + for (let element of document.querySelectorAll(`#${id} > *`)) { + let rect = element.getBoundingClientRect(); + if (commonHeight === null) { + commonHeight = rect.height; + commonTop = rect.top; + } + is(rect.height, commonHeight, `Height of the controls should match for ${element.outerHTML}${desc}`); + is(rect.top, commonTop, `Top of the controls should match for ${element.outerHTML}${desc}`); + } +} + +for (id of ["container", "container2"]) { + const container = document.getElementById(id); + + testHeightMatches(id, ""); + + container.className = "no-padding"; + + testHeightMatches(id, " without padding"); + + container.className = "small-font"; + + testHeightMatches(id, " with an small font"); +} +</script> |