1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "HTMLSelectEventListener.h"
8 #include "nsListControlFrame.h"
9 #include "mozilla/dom/Event.h"
10 #include "mozilla/dom/MouseEvent.h"
11 #include "mozilla/Casting.h"
12 #include "mozilla/MouseEvents.h"
13 #include "mozilla/TextEvents.h"
14 #include "mozilla/PresShell.h"
15 #include "mozilla/dom/HTMLSelectElement.h"
16 #include "mozilla/dom/HTMLOptionElement.h"
17 #include "mozilla/StaticPrefs_ui.h"
18 #include "mozilla/ClearOnShutdown.h"
20 using namespace mozilla
;
21 using namespace mozilla::dom
;
23 static bool IsOptionInteractivelySelectable(HTMLSelectElement
& aSelect
,
24 HTMLOptionElement
& aOption
,
26 if (aSelect
.IsOptionDisabled(&aOption
)) {
30 return aOption
.GetPrimaryFrame();
32 // In dropdown mode no options have frames, but we can check whether they
33 // are rendered / not in a display: none subtree.
34 if (!aOption
.HasServoData() || Servo_Element_IsDisplayNone(&aOption
)) {
37 // TODO(emilio): This is a bit silly and doesn't match the options that we
38 // show / don't show in the dropdown, but matches the frame construction we
39 // do for multiple selects. For backwards compat also don't allow selecting
40 // options in a display: contents subtree interactively.
41 // test_select_key_navigation_bug1498769.html tests for this and should
42 // probably be changed (and this loop removed) or alternatively
43 // SelectChild.jsm should be changed to match it.
44 for (Element
* el
= &aOption
; el
&& el
!= &aSelect
;
45 el
= el
->GetParentElement()) {
46 if (Servo_Element_IsDisplayContents(el
)) {
55 static StaticAutoPtr
<nsString
> sIncrementalString
;
56 static TimeStamp gLastKeyTime
;
57 static uintptr_t sLastKeyListener
= 0;
58 static constexpr int32_t kNothingSelected
= -1;
60 static nsString
& GetIncrementalString() {
61 MOZ_ASSERT(sLastKeyListener
!= 0);
62 if (!sIncrementalString
) {
63 sIncrementalString
= new nsString();
64 ClearOnShutdown(&sIncrementalString
);
66 return *sIncrementalString
;
69 class MOZ_RAII AutoIncrementalSearchHandler
{
71 explicit AutoIncrementalSearchHandler(HTMLSelectEventListener
& aListener
) {
72 if (sLastKeyListener
!= uintptr_t(&aListener
)) {
73 sLastKeyListener
= uintptr_t(&aListener
);
74 GetIncrementalString().Truncate();
75 // To make it easier to handle time comparisons in the other methods,
76 // initialize gLastKeyTime to a value in the past.
77 gLastKeyTime
= TimeStamp::Now() -
78 TimeDuration::FromMilliseconds(
79 StaticPrefs::ui_menu_incremental_search_timeout() * 2);
82 ~AutoIncrementalSearchHandler() {
83 if (!mResettingCancelled
) {
84 GetIncrementalString().Truncate();
87 void CancelResetting() { mResettingCancelled
= true; }
90 bool mResettingCancelled
= false;
93 NS_IMPL_ISUPPORTS(HTMLSelectEventListener
, nsIMutationObserver
,
96 HTMLSelectEventListener::~HTMLSelectEventListener() {
97 if (sLastKeyListener
== uintptr_t(this)) {
102 nsListControlFrame
* HTMLSelectEventListener::GetListControlFrame() const {
104 MOZ_ASSERT(!mElement
->GetPrimaryFrame() ||
105 !mElement
->GetPrimaryFrame()->IsListControlFrame());
108 return do_QueryFrame(mElement
->GetPrimaryFrame());
111 int32_t HTMLSelectEventListener::GetEndSelectionIndex() const {
112 if (auto* lf
= GetListControlFrame()) {
113 return lf
->GetEndSelectionIndex();
115 // Combobox selects only have one selected index, so the end and start is the
117 return mElement
->SelectedIndex();
120 bool HTMLSelectEventListener::IsOptionInteractivelySelectable(
121 uint32_t aIndex
) const {
122 HTMLOptionElement
* option
= mElement
->Item(aIndex
);
124 ::IsOptionInteractivelySelectable(*mElement
, *option
, mIsCombobox
);
127 //---------------------------------------------------------------------
128 // Ok, the entire idea of this routine is to move to the next item that
129 // is suppose to be selected. If the item is disabled then we search in
130 // the same direction looking for the next item to select. If we run off
131 // the end of the list then we start at the end of the list and search
132 // backwards until we get back to the original item or an enabled option
134 // aStartIndex - the index to start searching from
135 // aNewIndex - will get set to the new index if it finds one
136 // aNumOptions - the total number of options in the list
137 // aDoAdjustInc - the initial index increment / decrement
138 // aDoAdjustIncNext - the subsequent index increment/decrement used to search
139 // for the next enabled option
141 // the aDoAdjustInc could be a "1" for a single item or
142 // any number greater representing a page of items
144 void HTMLSelectEventListener::AdjustIndexForDisabledOpt(
145 int32_t aStartIndex
, int32_t& aNewIndex
, int32_t aNumOptions
,
146 int32_t aDoAdjustInc
, int32_t aDoAdjustIncNext
) {
147 // Cannot select anything if there is nothing to select
148 if (aNumOptions
== 0) {
149 aNewIndex
= kNothingSelected
;
153 // means we reached the end of the list and now we are searching backwards
154 bool doingReverse
= false;
155 // lowest index in the search range
157 // highest index in the search range
158 int32_t top
= aNumOptions
;
160 // Start off keyboard options at selectedIndex if nothing else is defaulted to
162 // XXX Perhaps this should happen for mouse too, to start off shift click
163 // automatically in multiple ... to do this, we'd need to override
164 // OnOptionSelected and set mStartSelectedIndex if nothing is selected. Not
165 // sure of the effects, though, so I'm not doing it just yet.
166 int32_t startIndex
= aStartIndex
;
167 if (startIndex
< bottom
) {
168 startIndex
= mElement
->SelectedIndex();
170 int32_t newIndex
= startIndex
+ aDoAdjustInc
;
172 // make sure we start off in the range
173 if (newIndex
< bottom
) {
175 } else if (newIndex
>= top
) {
176 newIndex
= aNumOptions
- 1;
180 // if the newIndex is selectable, we are golden, bail out
181 if (IsOptionInteractivelySelectable(newIndex
)) {
185 // it WAS disabled, so sart looking ahead for the next enabled option
186 newIndex
+= aDoAdjustIncNext
;
188 // well, if we reach end reverse the search
189 if (newIndex
< bottom
) {
191 return; // if we are in reverse mode and reach the end bail out
193 // reset the newIndex to the end of the list we hit
194 // reverse the incrementer
195 // set the other end of the list to our original starting index
197 aDoAdjustIncNext
= 1;
200 } else if (newIndex
>= top
) {
202 return; // if we are in reverse mode and reach the end bail out
204 // reset the newIndex to the end of the list we hit
205 // reverse the incrementer
206 // set the other end of the list to our original starting index
208 aDoAdjustIncNext
= -1;
214 // Looks like we found one
215 aNewIndex
= newIndex
;
219 HTMLSelectEventListener::HandleEvent(dom::Event
* aEvent
) {
220 nsAutoString eventType
;
221 aEvent
->GetType(eventType
);
222 if (eventType
.EqualsLiteral("keydown")) {
223 return KeyDown(aEvent
);
225 if (eventType
.EqualsLiteral("keypress")) {
226 return KeyPress(aEvent
);
228 if (eventType
.EqualsLiteral("mousedown")) {
229 if (aEvent
->DefaultPrevented()) {
232 return MouseDown(aEvent
);
234 if (eventType
.EqualsLiteral("mouseup")) {
235 // Don't try to honor defaultPrevented here - it's not web compatible.
237 return MouseUp(aEvent
);
239 if (eventType
.EqualsLiteral("mousemove")) {
240 // I don't think we want to honor defaultPrevented on mousemove
241 // in general, and it would only prevent highlighting here.
242 return MouseMove(aEvent
);
245 MOZ_ASSERT_UNREACHABLE("Unexpected eventType");
249 void HTMLSelectEventListener::Attach() {
250 mElement
->AddSystemEventListener(u
"keydown"_ns
, this, false, false);
251 mElement
->AddSystemEventListener(u
"keypress"_ns
, this, false, false);
252 mElement
->AddSystemEventListener(u
"mousedown"_ns
, this, false, false);
253 mElement
->AddSystemEventListener(u
"mouseup"_ns
, this, false, false);
254 mElement
->AddSystemEventListener(u
"mousemove"_ns
, this, false, false);
257 mElement
->AddMutationObserver(this);
261 void HTMLSelectEventListener::Detach() {
262 mElement
->RemoveSystemEventListener(u
"keydown"_ns
, this, false);
263 mElement
->RemoveSystemEventListener(u
"keypress"_ns
, this, false);
264 mElement
->RemoveSystemEventListener(u
"mousedown"_ns
, this, false);
265 mElement
->RemoveSystemEventListener(u
"mouseup"_ns
, this, false);
266 mElement
->RemoveSystemEventListener(u
"mousemove"_ns
, this, false);
269 mElement
->RemoveMutationObserver(this);
270 if (mElement
->OpenInParentProcess()) {
271 nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
272 "HTMLSelectEventListener::Detach", [element
= mElement
] {
273 // Don't hide the dropdown if the element has another frame already,
274 // this prevents closing dropdowns on reframe, see bug 1440506.
276 // FIXME(emilio): The flush is needed to deal with reframes started
277 // from DOM node removal. But perhaps we can be a bit smarter here.
278 if (!element
->IsCombobox() ||
279 !element
->GetPrimaryFrame(FlushType::Frames
)) {
280 nsContentUtils::DispatchChromeEvent(
281 element
->OwnerDoc(), element
, u
"mozhidedropdown"_ns
,
282 CanBubble::eYes
, Cancelable::eNo
);
289 const uint32_t kMaxDropdownRows
= 20; // matches the setting for 4.x browsers
291 int32_t HTMLSelectEventListener::ItemsPerPage() const {
292 uint32_t size
= [&] {
294 return kMaxDropdownRows
;
296 if (auto* lf
= GetListControlFrame()) {
297 return lf
->GetNumDisplayRows();
299 return mElement
->Size();
304 if (MOZ_UNLIKELY(size
> INT32_MAX
)) {
305 return INT32_MAX
- 1;
307 return AssertedCast
<int32_t>(size
- 1u);
310 void HTMLSelectEventListener::OptionValueMightHaveChanged(
311 nsIContent
* aMutatingNode
) {
313 if (nsAccessibilityService
* acc
= GetAccService()) {
314 acc
->ComboboxOptionMaybeChanged(mElement
->OwnerDoc()->GetPresShell(),
320 void HTMLSelectEventListener::AttributeChanged(dom::Element
* aElement
,
321 int32_t aNameSpaceID
,
324 const nsAttrValue
* aOldValue
) {
325 if (aElement
->IsHTMLElement(nsGkAtoms::option
) &&
326 aNameSpaceID
== kNameSpaceID_None
&& aAttribute
== nsGkAtoms::label
) {
327 // A11y has its own mutation listener for this so no need to do
328 // OptionValueMightHaveChanged().
329 ComboboxMightHaveChanged();
333 void HTMLSelectEventListener::CharacterDataChanged(
334 nsIContent
* aContent
, const CharacterDataChangeInfo
&) {
335 if (nsContentUtils::IsInSameAnonymousTree(mElement
, aContent
)) {
336 OptionValueMightHaveChanged(aContent
);
337 ComboboxMightHaveChanged();
341 void HTMLSelectEventListener::ContentRemoved(nsIContent
* aChild
,
342 nsIContent
* aPreviousSibling
) {
343 if (nsContentUtils::IsInSameAnonymousTree(mElement
, aChild
)) {
344 OptionValueMightHaveChanged(aChild
);
345 ComboboxMightHaveChanged();
349 void HTMLSelectEventListener::ContentAppended(nsIContent
* aFirstNewContent
) {
350 if (nsContentUtils::IsInSameAnonymousTree(mElement
, aFirstNewContent
)) {
351 OptionValueMightHaveChanged(aFirstNewContent
);
352 ComboboxMightHaveChanged();
356 void HTMLSelectEventListener::ContentInserted(nsIContent
* aChild
) {
357 if (nsContentUtils::IsInSameAnonymousTree(mElement
, aChild
)) {
358 OptionValueMightHaveChanged(aChild
);
359 ComboboxMightHaveChanged();
363 void HTMLSelectEventListener::ComboboxMightHaveChanged() {
364 if (nsIFrame
* f
= mElement
->GetPrimaryFrame()) {
365 PresShell
* ps
= f
->PresShell();
366 // nsComoboxControlFrame::Reflow updates the selected text. AddOption /
367 // RemoveOption / etc takes care of keeping the displayed index up to date.
368 ps
->FrameNeedsReflow(f
, IntrinsicDirty::FrameAncestorsAndDescendants
,
371 if (nsAccessibilityService
* acc
= GetAccService()) {
372 acc
->ScheduleAccessibilitySubtreeUpdate(ps
, mElement
);
378 void HTMLSelectEventListener::FireOnInputAndOnChange() {
379 RefPtr
<HTMLSelectElement
> element
= mElement
;
380 element
->UserFinishedInteracting(/* aChanged = */ true);
383 static void FireDropDownEvent(HTMLSelectElement
* aElement
, bool aShow
,
384 bool aIsSourceTouchEvent
) {
385 const auto eventName
= [&] {
387 return aIsSourceTouchEvent
? u
"mozshowdropdown-sourcetouch"_ns
388 : u
"mozshowdropdown"_ns
;
390 return u
"mozhidedropdown"_ns
;
392 nsContentUtils::DispatchChromeEvent(aElement
->OwnerDoc(), aElement
, eventName
,
393 CanBubble::eYes
, Cancelable::eNo
);
396 nsresult
HTMLSelectEventListener::MouseDown(dom::Event
* aMouseEvent
) {
397 NS_ASSERTION(aMouseEvent
!= nullptr, "aMouseEvent is null.");
399 MouseEvent
* mouseEvent
= aMouseEvent
->AsMouseEvent();
400 NS_ENSURE_TRUE(mouseEvent
, NS_ERROR_FAILURE
);
402 if (mElement
->State().HasState(ElementState::DISABLED
)) {
406 // only allow selection with the left button
407 // if a right button click is on the combobox itself
408 // or on the select when in listbox mode, then let the click through
409 const bool isLeftButton
= mouseEvent
->Button() == 0;
415 uint16_t inputSource
= mouseEvent
->InputSource();
416 if (mElement
->OpenInParentProcess()) {
417 nsCOMPtr
<nsIContent
> target
= do_QueryInterface(aMouseEvent
->GetTarget());
418 if (target
&& target
->IsHTMLElement(nsGkAtoms::option
)) {
423 const bool isSourceTouchEvent
=
424 inputSource
== MouseEvent_Binding::MOZ_SOURCE_TOUCH
;
425 FireDropDownEvent(mElement
, !mElement
->OpenInParentProcess(),
430 if (nsListControlFrame
* list
= GetListControlFrame()) {
432 return list
->HandleLeftButtonMouseDown(aMouseEvent
);
437 nsresult
HTMLSelectEventListener::MouseUp(dom::Event
* aMouseEvent
) {
438 NS_ASSERTION(aMouseEvent
!= nullptr, "aMouseEvent is null.");
440 MouseEvent
* mouseEvent
= aMouseEvent
->AsMouseEvent();
441 NS_ENSURE_TRUE(mouseEvent
, NS_ERROR_FAILURE
);
445 if (mElement
->State().HasState(ElementState::DISABLED
)) {
449 if (nsListControlFrame
* lf
= GetListControlFrame()) {
450 lf
->CaptureMouseEvents(false);
453 // only allow selection with the left button
454 // if a right button click is on the combobox itself
455 // or on the select when in listbox mode, then let the click through
456 const bool isLeftButton
= mouseEvent
->Button() == 0;
461 if (nsListControlFrame
* lf
= GetListControlFrame()) {
462 return lf
->HandleLeftButtonMouseUp(aMouseEvent
);
468 nsresult
HTMLSelectEventListener::MouseMove(dom::Event
* aMouseEvent
) {
469 NS_ASSERTION(aMouseEvent
!= nullptr, "aMouseEvent is null.");
471 MouseEvent
* mouseEvent
= aMouseEvent
->AsMouseEvent();
472 NS_ENSURE_TRUE(mouseEvent
, NS_ERROR_FAILURE
);
478 if (nsListControlFrame
* lf
= GetListControlFrame()) {
479 return lf
->DragMove(aMouseEvent
);
485 nsresult
HTMLSelectEventListener::KeyPress(dom::Event
* aKeyEvent
) {
486 MOZ_ASSERT(aKeyEvent
, "aKeyEvent is null.");
488 if (mElement
->State().HasState(ElementState::DISABLED
)) {
492 AutoIncrementalSearchHandler
incrementalHandler(*this);
494 const WidgetKeyboardEvent
* keyEvent
=
495 aKeyEvent
->WidgetEventPtr()->AsKeyboardEvent();
497 "DOM event must have WidgetKeyboardEvent for its internal event");
499 // Select option with this as the first character
500 // XXX Not I18N compliant
502 // Don't do incremental search if the key event has already consumed.
503 if (keyEvent
->DefaultPrevented()) {
507 if (keyEvent
->IsAlt()) {
511 // With some keyboard layout, space key causes non-ASCII space.
512 // So, the check in keydown event handler isn't enough, we need to check it
513 // again with keypress event.
514 if (keyEvent
->mCharCode
!= ' ') {
515 mControlSelectMode
= false;
518 const bool isControlOrMeta
=
519 keyEvent
->IsControl()
520 #if !defined(XP_WIN) && !defined(MOZ_WIDGET_GTK)
521 // Ignore Windows Logo key press in Win/Linux because it's not a usual
522 // modifier for applications. Here wants to check "Accel" like modifier.
523 || keyEvent
->IsMeta()
526 if (isControlOrMeta
&& keyEvent
->mCharCode
!= ' ') {
530 // NOTE: If mKeyCode of keypress event is not 0, mCharCode is always 0.
531 // Therefore, all non-printable keys are not handled after this block.
532 if (!keyEvent
->mCharCode
) {
533 // Backspace key will delete the last char in the string. Otherwise,
534 // non-printable keypress should reset incremental search.
535 if (keyEvent
->mKeyCode
== NS_VK_BACK
) {
536 incrementalHandler
.CancelResetting();
537 if (!GetIncrementalString().IsEmpty()) {
538 GetIncrementalString().Truncate(GetIncrementalString().Length() - 1);
540 aKeyEvent
->PreventDefault();
542 // XXX When a select element has focus, even if the key causes nothing,
543 // it might be better to call preventDefault() here because nobody
544 // should expect one of other elements including chrome handles the
550 incrementalHandler
.CancelResetting();
552 // We ate the key if we got this far.
553 aKeyEvent
->PreventDefault();
555 // XXX Why don't we check/modify timestamp first?
557 // Incremental Search: if time elapsed is below
558 // ui.menu.incremental_search.timeout, append this keystroke to the search
559 // string we will use to find options and start searching at the current
560 // keystroke. Otherwise, Truncate the string if it's been a long time
561 // since our last keypress.
562 if ((keyEvent
->mTimeStamp
- gLastKeyTime
).ToMilliseconds() >
563 StaticPrefs::ui_menu_incremental_search_timeout()) {
564 // If this is ' ' and we are at the beginning of the string, treat it as
565 // "select this option" (bug 191543)
566 if (keyEvent
->mCharCode
== ' ') {
567 // Actually process the new index and let the selection code
568 // do the scrolling for us
569 PostHandleKeyEvent(GetEndSelectionIndex(), keyEvent
->mCharCode
,
570 keyEvent
->IsShift(), isControlOrMeta
);
575 GetIncrementalString().Truncate();
578 gLastKeyTime
= keyEvent
->mTimeStamp
;
580 // Append this keystroke to the search string.
581 char16_t uniChar
= ToLowerCase(static_cast<char16_t
>(keyEvent
->mCharCode
));
582 GetIncrementalString().Append(uniChar
);
584 // See bug 188199, if all letters in incremental string are same, just try to
585 // match the first one
586 nsAutoString
incrementalString(GetIncrementalString());
587 uint32_t charIndex
= 1, stringLength
= incrementalString
.Length();
588 while (charIndex
< stringLength
&&
589 incrementalString
[charIndex
] == incrementalString
[charIndex
- 1]) {
592 if (charIndex
== stringLength
) {
593 incrementalString
.Truncate(1);
597 // Determine where we're going to start reading the string
598 // If we have multiple characters to look for, we start looking *at* the
599 // current option. If we have only one character to look for, we start
600 // looking *after* the current option.
601 // Exception: if there is no option selected to start at, we always start
603 int32_t startIndex
= mElement
->SelectedIndex();
604 if (startIndex
== kNothingSelected
) {
606 } else if (stringLength
== 1) {
610 // now make sure there are options or we are wasting our time
611 RefPtr
<dom::HTMLOptionsCollection
> options
= mElement
->Options();
612 uint32_t numOptions
= options
->Length();
614 for (uint32_t i
= 0; i
< numOptions
; ++i
) {
615 uint32_t index
= (i
+ startIndex
) % numOptions
;
616 RefPtr
<dom::HTMLOptionElement
> optionElement
= options
->ItemAsOption(index
);
617 if (!optionElement
|| !::IsOptionInteractivelySelectable(
618 *mElement
, *optionElement
, mIsCombobox
)) {
623 optionElement
->GetRenderedLabel(text
);
624 if (!StringBeginsWith(
625 nsContentUtils::TrimWhitespace
<
626 nsContentUtils::IsHTMLWhitespaceOrNBSP
>(text
, false),
627 incrementalString
, nsCaseInsensitiveStringComparator
)) {
632 if (optionElement
->Selected()) {
635 optionElement
->SetSelected(true);
636 FireOnInputAndOnChange();
640 if (nsListControlFrame
* lf
= GetListControlFrame()) {
642 lf
->PerformSelection(index
, keyEvent
->IsShift(), isControlOrMeta
);
646 FireOnInputAndOnChange();
654 nsresult
HTMLSelectEventListener::KeyDown(dom::Event
* aKeyEvent
) {
655 MOZ_ASSERT(aKeyEvent
, "aKeyEvent is null.");
657 if (mElement
->State().HasState(ElementState::DISABLED
)) {
661 AutoIncrementalSearchHandler
incrementalHandler(*this);
663 if (aKeyEvent
->DefaultPrevented()) {
667 const WidgetKeyboardEvent
* keyEvent
=
668 aKeyEvent
->WidgetEventPtr()->AsKeyboardEvent();
670 "DOM event must have WidgetKeyboardEvent for its internal event");
672 bool dropDownMenuOnUpDown
;
673 bool dropDownMenuOnSpace
;
675 dropDownMenuOnUpDown
= mIsCombobox
&& !mElement
->OpenInParentProcess();
676 dropDownMenuOnSpace
= mIsCombobox
&& !keyEvent
->IsAlt() &&
677 !keyEvent
->IsControl() && !keyEvent
->IsMeta();
679 dropDownMenuOnUpDown
= mIsCombobox
&& keyEvent
->IsAlt();
680 dropDownMenuOnSpace
= mIsCombobox
&& !mElement
->OpenInParentProcess();
682 bool withinIncrementalSearchTime
=
683 (keyEvent
->mTimeStamp
- gLastKeyTime
).ToMilliseconds() <=
684 StaticPrefs::ui_menu_incremental_search_timeout();
685 if ((dropDownMenuOnUpDown
&&
686 (keyEvent
->mKeyCode
== NS_VK_UP
|| keyEvent
->mKeyCode
== NS_VK_DOWN
)) ||
687 (dropDownMenuOnSpace
&& keyEvent
->mKeyCode
== NS_VK_SPACE
&&
688 !withinIncrementalSearchTime
)) {
689 FireDropDownEvent(mElement
, !mElement
->OpenInParentProcess(), false);
690 aKeyEvent
->PreventDefault();
693 if (keyEvent
->IsAlt()) {
697 // We should not change the selection if the popup is "opened in the parent
698 // process" (even when we're in single-process mode).
699 const bool shouldSelect
= !mIsCombobox
|| !mElement
->OpenInParentProcess();
701 // now make sure there are options or we are wasting our time
702 RefPtr
<dom::HTMLOptionsCollection
> options
= mElement
->Options();
703 uint32_t numOptions
= options
->Length();
705 // this is the new index to set
706 int32_t newIndex
= kNothingSelected
;
708 bool isControlOrMeta
=
709 keyEvent
->IsControl()
710 #if !defined(XP_WIN) && !defined(MOZ_WIDGET_GTK)
711 // Ignore Windows Logo key press in Win/Linux because it's not a usual
712 // modifier for applications. Here wants to check "Accel" like modifier.
713 || keyEvent
->IsMeta()
716 // Don't try to handle multiple-select pgUp/pgDown in single-select lists.
717 if (isControlOrMeta
&& !mElement
->Multiple() &&
718 (keyEvent
->mKeyCode
== NS_VK_PAGE_UP
||
719 keyEvent
->mKeyCode
== NS_VK_PAGE_DOWN
)) {
722 if (isControlOrMeta
&&
723 (keyEvent
->mKeyCode
== NS_VK_UP
|| keyEvent
->mKeyCode
== NS_VK_LEFT
||
724 keyEvent
->mKeyCode
== NS_VK_DOWN
|| keyEvent
->mKeyCode
== NS_VK_RIGHT
||
725 keyEvent
->mKeyCode
== NS_VK_HOME
|| keyEvent
->mKeyCode
== NS_VK_END
)) {
726 // Don't go into multiple-select mode unless this list can handle it.
727 isControlOrMeta
= mControlSelectMode
= mElement
->Multiple();
728 } else if (keyEvent
->mKeyCode
!= NS_VK_SPACE
) {
729 mControlSelectMode
= false;
732 switch (keyEvent
->mKeyCode
) {
736 AdjustIndexForDisabledOpt(GetEndSelectionIndex(), newIndex
,
737 int32_t(numOptions
), -1, -1);
743 AdjustIndexForDisabledOpt(GetEndSelectionIndex(), newIndex
,
744 int32_t(numOptions
), 1, 1);
748 // If this is single select listbox, Enter key doesn't cause anything.
749 if (!mElement
->Multiple()) {
753 newIndex
= GetEndSelectionIndex();
755 case NS_VK_PAGE_UP
: {
757 AdjustIndexForDisabledOpt(GetEndSelectionIndex(), newIndex
,
758 int32_t(numOptions
), -ItemsPerPage(), -1);
762 case NS_VK_PAGE_DOWN
: {
764 AdjustIndexForDisabledOpt(GetEndSelectionIndex(), newIndex
,
765 int32_t(numOptions
), ItemsPerPage(), 1);
771 AdjustIndexForDisabledOpt(0, newIndex
, int32_t(numOptions
), 0, 1);
776 AdjustIndexForDisabledOpt(int32_t(numOptions
) - 1, newIndex
,
777 int32_t(numOptions
), 0, -1);
780 default: // printable key will be handled by keypress event.
781 incrementalHandler
.CancelResetting();
785 aKeyEvent
->PreventDefault();
787 // Actually process the new index and let the selection code
788 // do the scrolling for us
789 PostHandleKeyEvent(newIndex
, 0, keyEvent
->IsShift(), isControlOrMeta
);
793 HTMLOptionElement
* HTMLSelectEventListener::GetCurrentOption() const {
794 // The mEndSelectionIndex is what is currently being selected. Use
795 // the selected index if this is kNothingSelected.
796 int32_t endIndex
= GetEndSelectionIndex();
797 int32_t focusedIndex
=
798 endIndex
== kNothingSelected
? mElement
->SelectedIndex() : endIndex
;
799 if (focusedIndex
!= kNothingSelected
) {
800 return mElement
->Item(AssertedCast
<uint32_t>(focusedIndex
));
803 // There is no selected option. Return the first non-disabled option, if any.
804 return GetNonDisabledOptionFrom(0);
807 HTMLOptionElement
* HTMLSelectEventListener::GetNonDisabledOptionFrom(
808 int32_t aFromIndex
, int32_t* aFoundIndex
) const {
809 const uint32_t length
= mElement
->Length();
810 for (uint32_t i
= std::max(aFromIndex
, 0); i
< length
; ++i
) {
811 if (IsOptionInteractivelySelectable(i
)) {
815 return mElement
->Item(i
);
821 void HTMLSelectEventListener::PostHandleKeyEvent(int32_t aNewIndex
,
824 bool aIsControlOrMeta
) {
825 if (aNewIndex
== kNothingSelected
) {
826 int32_t endIndex
= GetEndSelectionIndex();
827 int32_t focusedIndex
=
828 endIndex
== kNothingSelected
? mElement
->SelectedIndex() : endIndex
;
829 if (focusedIndex
!= kNothingSelected
) {
832 // No options are selected. In this case the focus ring is on the first
833 // non-disabled option (if any), so we should behave as if that's the option
834 // the user acted on.
835 if (!GetNonDisabledOptionFrom(0, &aNewIndex
)) {
841 RefPtr
<HTMLOptionElement
> newOption
= mElement
->Item(aNewIndex
);
842 MOZ_ASSERT(newOption
);
843 if (newOption
->Selected()) {
846 newOption
->SetSelected(true);
847 FireOnInputAndOnChange();
850 if (nsListControlFrame
* lf
= GetListControlFrame()) {
851 lf
->UpdateSelectionAfterKeyEvent(aNewIndex
, aCharCode
, aIsShift
,
852 aIsControlOrMeta
, mControlSelectMode
);
856 } // namespace mozilla