2 * Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
3 * Copyright (C) 1999 Lars Knoll (knoll@kde.org)
4 * (C) 1999 Antti Koivisto (koivisto@kde.org)
5 * (C) 2001 Dirk Mueller (mueller@kde.org)
6 * Copyright (C) 2004, 2005, 2006, 2007, 2009, 2010, 2011 Apple Inc. All rights reserved.
7 * (C) 2006 Alexey Proskuryakov (ap@nypop.com)
8 * Copyright (C) 2010 Google Inc. All rights reserved.
9 * Copyright (C) 2009 Torch Mobile Inc. All rights reserved. (http://www.torchmobile.com/)
11 * This library is free software; you can redistribute it and/or
12 * modify it under the terms of the GNU Library General Public
13 * License as published by the Free Software Foundation; either
14 * version 2 of the License, or (at your option) any later version.
16 * This library is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 * Library General Public License for more details.
21 * You should have received a copy of the GNU Library General Public License
22 * along with this library; see the file COPYING.LIB. If not, write to
23 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
24 * Boston, MA 02110-1301, USA.
29 #include "core/html/HTMLSelectElement.h"
31 #include "bindings/core/v8/ExceptionMessages.h"
32 #include "bindings/core/v8/ExceptionState.h"
33 #include "bindings/core/v8/ExceptionStatePlaceholder.h"
34 #include "bindings/core/v8/UnionTypesCore.h"
35 #include "core/HTMLNames.h"
36 #include "core/dom/AXObjectCache.h"
37 #include "core/dom/Attribute.h"
38 #include "core/dom/ElementTraversal.h"
39 #include "core/dom/NodeComputedStyle.h"
40 #include "core/dom/NodeListsNodeData.h"
41 #include "core/dom/NodeTraversal.h"
42 #include "core/events/GestureEvent.h"
43 #include "core/events/KeyboardEvent.h"
44 #include "core/events/MouseEvent.h"
45 #include "core/frame/FrameHost.h"
46 #include "core/frame/FrameView.h"
47 #include "core/frame/LocalFrame.h"
48 #include "core/html/FormData.h"
49 #include "core/html/HTMLFormElement.h"
50 #include "core/html/HTMLOptGroupElement.h"
51 #include "core/html/HTMLOptionElement.h"
52 #include "core/html/forms/FormController.h"
53 #include "core/input/EventHandler.h"
54 #include "core/inspector/ConsoleMessage.h"
55 #include "core/layout/HitTestRequest.h"
56 #include "core/layout/HitTestResult.h"
57 #include "core/layout/LayoutListBox.h"
58 #include "core/layout/LayoutMenuList.h"
59 #include "core/layout/LayoutText.h"
60 #include "core/layout/LayoutTheme.h"
61 #include "core/layout/LayoutView.h"
62 #include "core/page/AutoscrollController.h"
63 #include "core/page/ChromeClient.h"
64 #include "core/page/Page.h"
65 #include "core/page/SpatialNavigation.h"
66 #include "platform/PlatformMouseEvent.h"
67 #include "platform/PopupMenu.h"
68 #include "platform/text/PlatformLocale.h"
70 using namespace WTF::Unicode
;
74 using namespace HTMLNames
;
76 // Upper limit agreed upon with representatives of Opera and Mozilla.
77 static const unsigned maxSelectItems
= 10000;
79 HTMLSelectElement::HTMLSelectElement(Document
& document
, HTMLFormElement
* form
)
80 : HTMLFormControlElementWithState(selectTag
, document
, form
)
83 , m_lastOnChangeIndex(-1)
84 , m_activeSelectionAnchorIndex(-1)
85 , m_activeSelectionEndIndex(-1)
86 , m_isProcessingUserDrivenChange(false)
88 , m_activeSelectionState(false)
89 , m_shouldRecalcListItems(false)
90 , m_suggestedIndex(-1)
91 , m_isAutofilledByPreview(false)
92 , m_indexToSelectOnCancel(-1)
93 , m_popupIsVisible(false)
95 setHasCustomStyleCallbacks();
98 PassRefPtrWillBeRawPtr
<HTMLSelectElement
> HTMLSelectElement::create(Document
& document
)
100 RefPtrWillBeRawPtr
<HTMLSelectElement
> select
= adoptRefWillBeNoop(new HTMLSelectElement(document
, 0));
101 select
->ensureUserAgentShadowRoot();
102 return select
.release();
105 PassRefPtrWillBeRawPtr
<HTMLSelectElement
> HTMLSelectElement::create(Document
& document
, HTMLFormElement
* form
)
107 RefPtrWillBeRawPtr
<HTMLSelectElement
> select
= adoptRefWillBeNoop(new HTMLSelectElement(document
, form
));
108 select
->ensureUserAgentShadowRoot();
109 return select
.release();
112 HTMLSelectElement::~HTMLSelectElement()
116 const AtomicString
& HTMLSelectElement::formControlType() const
118 DEFINE_STATIC_LOCAL(const AtomicString
, selectMultiple
, ("select-multiple", AtomicString::ConstructFromLiteral
));
119 DEFINE_STATIC_LOCAL(const AtomicString
, selectOne
, ("select-one", AtomicString::ConstructFromLiteral
));
120 return m_multiple
? selectMultiple
: selectOne
;
123 void HTMLSelectElement::optionSelectedByUser(int optionIndex
, bool fireOnChangeNow
, bool allowMultipleSelection
)
125 // User interaction such as mousedown events can cause list box select elements to send change events.
126 // This produces that same behavior for changes triggered by other code running on behalf of the user.
127 if (!usesMenuList()) {
128 updateSelectedState(optionToListIndex(optionIndex
), allowMultipleSelection
, false);
129 setNeedsValidityCheck();
135 // Bail out if this index is already the selected one, to avoid running unnecessary JavaScript that can mess up
136 // autofill when there is no actual change (see https://bugs.webkit.org/show_bug.cgi?id=35256 and <rdar://7467917>).
137 // The selectOption function does not behave this way, possibly because other callers need a change event even
138 // in cases where the selected option is not change.
139 if (optionIndex
== selectedIndex())
142 selectOption(optionIndex
, DeselectOtherOptions
| (fireOnChangeNow
? DispatchInputAndChangeEvent
: 0) | UserDriven
);
145 bool HTMLSelectElement::hasPlaceholderLabelOption() const
147 // The select element has no placeholder label option if it has an attribute "multiple" specified or a display size of non-1.
149 // The condition "size() > 1" is not compliant with the HTML5 spec as of Dec 3, 2010. "size() != 1" is correct.
150 // Using "size() > 1" here because size() may be 0 in WebKit.
151 // See the discussion at https://bugs.webkit.org/show_bug.cgi?id=43887
153 // "0 size()" happens when an attribute "size" is absent or an invalid size attribute is specified.
154 // In this case, the display size should be assumed as the default.
155 // The default display size is 1 for non-multiple select elements, and 4 for multiple select elements.
157 // Finally, if size() == 0 and non-multiple, the display size can be assumed as 1.
158 if (multiple() || size() > 1)
161 int listIndex
= optionToListIndex(0);
162 ASSERT(listIndex
>= 0);
165 return !listIndex
&& toHTMLOptionElement(listItems()[listIndex
])->value().isEmpty();
168 String
HTMLSelectElement::validationMessage() const
173 return customValidationMessage();
175 return locale().queryString(WebLocalizedString::ValidationValueMissingForSelect
);
179 bool HTMLSelectElement::valueMissing() const
187 int firstSelectionIndex
= selectedIndex();
189 // If a non-placeholer label option is selected (firstSelectionIndex > 0), it's not value-missing.
190 return firstSelectionIndex
< 0 || (!firstSelectionIndex
&& hasPlaceholderLabelOption());
193 void HTMLSelectElement::listBoxSelectItem(int listIndex
, bool allowMultiplySelections
, bool shift
, bool fireOnChangeNow
)
196 optionSelectedByUser(listToOptionIndex(listIndex
), fireOnChangeNow
, false);
198 updateSelectedState(listIndex
, allowMultiplySelections
, shift
);
199 setNeedsValidityCheck();
205 bool HTMLSelectElement::usesMenuList() const
207 if (LayoutTheme::theme().delegatesMenuListRendering())
210 return !m_multiple
&& m_size
<= 1;
213 int HTMLSelectElement::activeSelectionEndListIndex() const
215 if (m_activeSelectionEndIndex
>= 0)
216 return m_activeSelectionEndIndex
;
217 return lastSelectedListIndex();
220 void HTMLSelectElement::add(const HTMLOptionElementOrHTMLOptGroupElement
& element
, const HTMLElementOrLong
& before
, ExceptionState
& exceptionState
)
222 RefPtrWillBeRawPtr
<HTMLElement
> elementToInsert
;
223 ASSERT(!element
.isNull());
224 if (element
.isHTMLOptionElement())
225 elementToInsert
= element
.getAsHTMLOptionElement();
227 elementToInsert
= element
.getAsHTMLOptGroupElement();
229 RefPtrWillBeRawPtr
<HTMLElement
> beforeElement
;
230 if (before
.isHTMLElement())
231 beforeElement
= before
.getAsHTMLElement();
232 else if (before
.isLong())
233 beforeElement
= options()->item(before
.getAsLong());
235 beforeElement
= nullptr;
237 insertBefore(elementToInsert
, beforeElement
.get(), exceptionState
);
238 setNeedsValidityCheck();
241 void HTMLSelectElement::remove(int optionIndex
)
243 int listIndex
= optionToListIndex(optionIndex
);
247 listItems()[listIndex
]->remove(IGNORE_EXCEPTION
);
250 String
HTMLSelectElement::value() const
252 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& items
= listItems();
253 for (unsigned i
= 0; i
< items
.size(); i
++) {
254 if (isHTMLOptionElement(items
[i
]) && toHTMLOptionElement(items
[i
])->selected())
255 return toHTMLOptionElement(items
[i
])->value();
260 void HTMLSelectElement::setValue(const String
&value
, bool sendEvents
)
262 // We clear the previously selected option(s) when needed, to guarantee calling setSelectedIndex() only once.
264 if (value
.isNull()) {
267 // Find the option with value() matching the given parameter and make it the current selection.
268 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& items
= listItems();
269 for (unsigned i
= 0; i
< items
.size(); i
++) {
270 if (isHTMLOptionElement(items
[i
])) {
271 if (toHTMLOptionElement(items
[i
])->value() == value
)
276 if (optionIndex
>= static_cast<int>(items
.size()))
280 int previousSelectedIndex
= selectedIndex();
281 setSuggestedIndex(-1);
282 if (m_isAutofilledByPreview
)
283 setAutofilled(false);
284 setSelectedIndex(optionIndex
);
286 if (sendEvents
&& previousSelectedIndex
!= selectedIndex()) {
288 dispatchInputAndChangeEventForMenuList(false);
294 String
HTMLSelectElement::suggestedValue() const
296 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& items
= listItems();
297 for (unsigned i
= 0; i
< items
.size(); ++i
) {
298 if (isHTMLOptionElement(items
[i
]) && m_suggestedIndex
>= 0) {
299 if (i
== static_cast<unsigned>(m_suggestedIndex
))
300 return toHTMLOptionElement(items
[i
])->value();
306 void HTMLSelectElement::setSuggestedValue(const String
& value
)
308 if (value
.isNull()) {
309 setSuggestedIndex(-1);
313 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& items
= listItems();
314 unsigned optionIndex
= 0;
315 for (unsigned i
= 0; i
< items
.size(); ++i
) {
316 if (isHTMLOptionElement(items
[i
])) {
317 if (toHTMLOptionElement(items
[i
])->value() == value
) {
318 setSuggestedIndex(optionIndex
);
319 m_isAutofilledByPreview
= true;
326 setSuggestedIndex(-1);
329 bool HTMLSelectElement::isPresentationAttribute(const QualifiedName
& name
) const
331 if (name
== alignAttr
) {
332 // Don't map 'align' attribute. This matches what Firefox, Opera and IE do.
333 // See http://bugs.webkit.org/show_bug.cgi?id=12072
337 return HTMLFormControlElementWithState::isPresentationAttribute(name
);
340 void HTMLSelectElement::parseAttribute(const QualifiedName
& name
, const AtomicString
& value
)
342 if (name
== sizeAttr
) {
343 unsigned oldSize
= m_size
;
344 // Set the attribute value to a number.
345 // This is important since the style rules for this attribute can determine the appearance property.
346 unsigned size
= value
.string().toUInt();
347 AtomicString attrSize
= AtomicString::number(size
);
348 if (attrSize
!= value
) {
349 // FIXME: This is horribly factored.
350 if (Attribute
* sizeAttribute
= ensureUniqueElementData().attributes().find(sizeAttr
))
351 sizeAttribute
->setValue(attrSize
);
353 size
= std::max(size
, 0u);
355 // Ensure that we've determined selectedness of the items at least once prior to changing the size.
357 updateListItemSelectedStates();
360 setNeedsValidityCheck();
361 if (m_size
!= oldSize
&& inActiveDocument()) {
362 lazyReattachIfAttached();
363 setRecalcListItems();
365 } else if (name
== multipleAttr
) {
366 parseMultipleAttribute(value
);
367 } else if (name
== accesskeyAttr
) {
368 // FIXME: ignore for the moment.
370 } else if (name
== disabledAttr
) {
371 HTMLFormControlElementWithState::parseAttribute(name
, value
);
372 if (popupIsVisible())
376 HTMLFormControlElementWithState::parseAttribute(name
, value
);
380 bool HTMLSelectElement::shouldShowFocusRingOnMouseFocus() const
385 bool HTMLSelectElement::canSelectAll() const
387 return !usesMenuList();
390 LayoutObject
* HTMLSelectElement::createLayoutObject(const ComputedStyle
&)
393 return new LayoutMenuList(this);
394 return new LayoutListBox(this);
397 PassRefPtrWillBeRawPtr
<HTMLCollection
> HTMLSelectElement::selectedOptions()
399 updateListItemSelectedStates();
400 return ensureCachedCollection
<HTMLCollection
>(SelectedOptions
);
403 PassRefPtrWillBeRawPtr
<HTMLOptionsCollection
> HTMLSelectElement::options()
405 return ensureCachedCollection
<HTMLOptionsCollection
>(SelectOptions
);
408 void HTMLSelectElement::updateListItemSelectedStates()
410 if (!m_shouldRecalcListItems
)
413 setNeedsValidityCheck();
416 void HTMLSelectElement::childrenChanged(const ChildrenChange
& change
)
418 setRecalcListItems();
419 setNeedsValidityCheck();
420 m_lastOnChangeSelection
.clear();
422 HTMLFormControlElementWithState::childrenChanged(change
);
425 void HTMLSelectElement::optionElementChildrenChanged()
427 setRecalcListItems();
428 setNeedsValidityCheck();
430 if (layoutObject()) {
431 if (AXObjectCache
* cache
= layoutObject()->document().existingAXObjectCache())
432 cache
->childrenChanged(this);
436 void HTMLSelectElement::accessKeyAction(bool sendMouseEvents
)
439 dispatchSimulatedClick(0, sendMouseEvents
? SendMouseUpDownEvents
: SendNoEvents
);
442 void HTMLSelectElement::setMultiple(bool multiple
)
444 bool oldMultiple
= this->multiple();
445 int oldSelectedIndex
= selectedIndex();
446 setAttribute(multipleAttr
, multiple
? emptyAtom
: nullAtom
);
448 // Restore selectedIndex after changing the multiple flag to preserve
449 // selection as single-line and multi-line has different defaults.
450 if (oldMultiple
!= this->multiple())
451 setSelectedIndex(oldSelectedIndex
);
454 void HTMLSelectElement::setSize(unsigned size
)
456 setUnsignedIntegralAttribute(sizeAttr
, size
);
459 Element
* HTMLSelectElement::namedItem(const AtomicString
& name
)
461 return options()->namedItem(name
);
464 HTMLOptionElement
* HTMLSelectElement::item(unsigned index
)
466 return options()->item(index
);
469 void HTMLSelectElement::setOption(unsigned index
, HTMLOptionElement
* option
, ExceptionState
& exceptionState
)
471 if (index
>= length() && index
>= maxSelectItems
) {
472 document().addConsoleMessage(ConsoleMessage::create(JSMessageSource
, WarningMessageLevel
,
473 String::format("Blocked to expand the option list and set an option at index=%u. The maximum list length is %u.", index
, maxSelectItems
)));
476 int diff
= index
- length();
477 HTMLOptionElementOrHTMLOptGroupElement element
;
478 element
.setHTMLOptionElement(option
);
479 HTMLElementOrLong before
;
480 // Out of array bounds? First insert empty dummies.
482 setLength(index
, exceptionState
);
483 // Replace an existing entry?
484 } else if (diff
< 0) {
485 before
.setHTMLElement(options()->item(index
+ 1));
488 // Finally add the new element.
489 if (!exceptionState
.hadException()) {
490 add(element
, before
, exceptionState
);
491 if (diff
>= 0 && option
->selected())
492 optionSelectionStateChanged(option
, true);
496 void HTMLSelectElement::setLength(unsigned newLen
, ExceptionState
& exceptionState
)
498 if (newLen
> length() && newLen
> maxSelectItems
) {
499 document().addConsoleMessage(ConsoleMessage::create(JSMessageSource
, WarningMessageLevel
,
500 String::format("Blocked to expand the option list to %u items. The maximum list length is %u.", newLen
, maxSelectItems
)));
503 int diff
= length() - newLen
;
505 if (diff
< 0) { // Add dummy elements.
507 appendChild(document().createElement(optionTag
, false), exceptionState
);
508 if (exceptionState
.hadException())
512 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& items
= listItems();
514 // Removing children fires mutation events, which might mutate the DOM further, so we first copy out a list
515 // of elements that we intend to remove then attempt to remove them one at a time.
516 WillBeHeapVector
<RefPtrWillBeMember
<Element
>> itemsToRemove
;
517 size_t optionIndex
= 0;
518 for (size_t i
= 0; i
< items
.size(); ++i
) {
519 Element
* item
= items
[i
];
520 if (isHTMLOptionElement(items
[i
]) && optionIndex
++ >= newLen
) {
521 ASSERT(item
->parentNode());
522 itemsToRemove
.append(item
);
526 for (size_t i
= 0; i
< itemsToRemove
.size(); ++i
) {
527 Element
* item
= itemsToRemove
[i
].get();
528 if (item
->parentNode())
529 item
->parentNode()->removeChild(item
, exceptionState
);
532 setNeedsValidityCheck();
535 bool HTMLSelectElement::isRequiredFormControl() const
540 // Returns the 1st valid item |skip| items from |listIndex| in direction |direction| if there is one.
541 // Otherwise, it returns the valid item closest to that boundary which is past |listIndex| if there is one.
542 // Otherwise, it returns |listIndex|.
543 // Valid means that it is enabled and an option element.
544 int HTMLSelectElement::nextValidIndex(int listIndex
, SkipDirection direction
, int skip
) const
546 ASSERT(direction
== -1 || direction
== 1);
547 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& listItems
= this->listItems();
548 int lastGoodIndex
= listIndex
;
549 int size
= listItems
.size();
550 for (listIndex
+= direction
; listIndex
>= 0 && listIndex
< size
; listIndex
+= direction
) {
552 HTMLElement
* element
= listItems
[listIndex
];
553 if (!isHTMLOptionElement(*element
))
555 if (toHTMLOptionElement(*element
).isDisplayNone())
557 if (element
->isDisabledFormControl())
559 if (!usesMenuList() && !element
->layoutObject())
561 lastGoodIndex
= listIndex
;
565 return lastGoodIndex
;
568 int HTMLSelectElement::nextSelectableListIndex(int startIndex
) const
570 return nextValidIndex(startIndex
, SkipForwards
, 1);
573 int HTMLSelectElement::previousSelectableListIndex(int startIndex
) const
575 if (startIndex
== -1)
576 startIndex
= listItems().size();
577 return nextValidIndex(startIndex
, SkipBackwards
, 1);
580 int HTMLSelectElement::firstSelectableListIndex() const
582 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& items
= listItems();
583 int index
= nextValidIndex(items
.size(), SkipBackwards
, INT_MAX
);
584 if (static_cast<size_t>(index
) == items
.size())
589 int HTMLSelectElement::lastSelectableListIndex() const
591 return nextValidIndex(-1, SkipForwards
, INT_MAX
);
594 // Returns the index of the next valid item one page away from |startIndex| in direction |direction|.
595 int HTMLSelectElement::nextSelectableListIndexPageAway(int startIndex
, SkipDirection direction
) const
597 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& items
= listItems();
598 // Can't use m_size because layoutObject forces a minimum size.
600 if (layoutObject()->isListBox())
601 pageSize
= toLayoutListBox(layoutObject())->size() - 1; // -1 so we still show context.
603 // One page away, but not outside valid bounds.
604 // If there is a valid option item one page away, the index is chosen.
605 // If there is no exact one page away valid option, returns startIndex or the most far index.
606 int edgeIndex
= (direction
== SkipForwards
) ? 0 : (items
.size() - 1);
607 int skipAmount
= pageSize
+ ((direction
== SkipForwards
) ? startIndex
: (edgeIndex
- startIndex
));
608 return nextValidIndex(edgeIndex
, direction
, skipAmount
);
611 void HTMLSelectElement::selectAll()
613 ASSERT(!usesMenuList());
614 if (!layoutObject() || !m_multiple
)
617 // Save the selection so it can be compared to the new selectAll selection
618 // when dispatching change events.
621 m_activeSelectionState
= true;
622 setActiveSelectionAnchorIndex(nextSelectableListIndex(-1));
623 setActiveSelectionEndIndex(previousSelectableListIndex(-1));
625 updateListBoxSelection(false, false);
627 setNeedsValidityCheck();
630 void HTMLSelectElement::saveLastSelection()
632 if (usesMenuList()) {
633 m_lastOnChangeIndex
= selectedIndex();
637 m_lastOnChangeSelection
.clear();
638 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& items
= listItems();
639 for (unsigned i
= 0; i
< items
.size(); ++i
) {
640 HTMLElement
* element
= items
[i
];
641 m_lastOnChangeSelection
.append(isHTMLOptionElement(*element
) && toHTMLOptionElement(element
)->selected());
645 void HTMLSelectElement::setActiveSelectionAnchorIndex(int index
)
647 m_activeSelectionAnchorIndex
= index
;
649 // Cache the selection state so we can restore the old selection as the new
650 // selection pivots around this anchor index.
651 m_cachedStateForActiveSelection
.clear();
653 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& items
= listItems();
654 for (unsigned i
= 0; i
< items
.size(); ++i
) {
655 HTMLElement
* element
= items
[i
];
656 m_cachedStateForActiveSelection
.append(isHTMLOptionElement(*element
) && toHTMLOptionElement(element
)->selected());
660 void HTMLSelectElement::setActiveSelectionEndIndex(int index
)
662 m_activeSelectionEndIndex
= index
;
665 void HTMLSelectElement::updateListBoxSelection(bool deselectOtherOptions
, bool scroll
)
667 ASSERT(layoutObject() && (layoutObject()->isListBox() || m_multiple
));
669 int start
= std::min(m_activeSelectionAnchorIndex
, m_activeSelectionEndIndex
);
670 int end
= std::max(m_activeSelectionAnchorIndex
, m_activeSelectionEndIndex
);
672 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& items
= listItems();
673 for (int i
= 0; i
< static_cast<int>(items
.size()); ++i
) {
674 HTMLElement
* element
= items
[i
];
675 if (!isHTMLOptionElement(*element
) || toHTMLOptionElement(element
)->isDisabledFormControl() || !toHTMLOptionElement(element
)->layoutObject())
678 if (i
>= start
&& i
<= end
)
679 toHTMLOptionElement(element
)->setSelectedState(m_activeSelectionState
);
680 else if (deselectOtherOptions
|| i
>= static_cast<int>(m_cachedStateForActiveSelection
.size()))
681 toHTMLOptionElement(element
)->setSelectedState(false);
683 toHTMLOptionElement(element
)->setSelectedState(m_cachedStateForActiveSelection
[i
]);
686 setNeedsValidityCheck();
689 notifyFormStateChanged();
692 void HTMLSelectElement::listBoxOnChange()
694 ASSERT(!usesMenuList() || m_multiple
);
696 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& items
= listItems();
698 // If the cached selection list is empty, or the size has changed, then fire
699 // dispatchFormControlChangeEvent, and return early.
700 // FIXME: Why? This looks unreasonable.
701 if (m_lastOnChangeSelection
.isEmpty() || m_lastOnChangeSelection
.size() != items
.size()) {
702 dispatchFormControlChangeEvent();
706 // Update m_lastOnChangeSelection and fire dispatchFormControlChangeEvent.
707 bool fireOnChange
= false;
708 for (unsigned i
= 0; i
< items
.size(); ++i
) {
709 HTMLElement
* element
= items
[i
];
710 bool selected
= isHTMLOptionElement(*element
) && toHTMLOptionElement(element
)->selected();
711 if (selected
!= m_lastOnChangeSelection
[i
])
713 m_lastOnChangeSelection
[i
] = selected
;
717 RefPtrWillBeRawPtr
<HTMLSelectElement
> protector(this);
718 dispatchInputEvent();
719 dispatchFormControlChangeEvent();
723 void HTMLSelectElement::dispatchInputAndChangeEventForMenuList(bool requiresUserGesture
)
725 ASSERT(usesMenuList());
727 int selected
= selectedIndex();
728 if (m_lastOnChangeIndex
!= selected
&& (!requiresUserGesture
|| m_isProcessingUserDrivenChange
)) {
729 m_lastOnChangeIndex
= selected
;
730 m_isProcessingUserDrivenChange
= false;
731 RefPtrWillBeRawPtr
<HTMLSelectElement
> protector(this);
732 dispatchInputEvent();
733 dispatchFormControlChangeEvent();
737 void HTMLSelectElement::scrollToSelection()
739 if (!isFinishedParsingChildren())
743 scrollToIndex(activeSelectionEndListIndex());
744 if (AXObjectCache
* cache
= document().existingAXObjectCache())
745 cache
->listboxActiveIndexChanged(this);
748 void HTMLSelectElement::setOptionsChangedOnLayoutObject()
750 if (LayoutObject
* layoutObject
= this->layoutObject()) {
752 toLayoutMenuList(layoutObject
)->setOptionsChanged(true);
756 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& HTMLSelectElement::listItems() const
758 if (m_shouldRecalcListItems
) {
762 WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>> items
= m_listItems
;
763 recalcListItems(false);
764 ASSERT(items
== m_listItems
);
771 void HTMLSelectElement::invalidateSelectedItems()
773 if (HTMLCollection
* collection
= cachedCollection
<HTMLCollection
>(SelectedOptions
))
774 collection
->invalidateCache();
777 void HTMLSelectElement::setRecalcListItems()
779 // FIXME: This function does a bunch of confusing things depending on if it
780 // is in the document or not.
782 m_shouldRecalcListItems
= true;
783 // Manual selection anchor is reset when manipulating the select programmatically.
784 m_activeSelectionAnchorIndex
= -1;
785 setOptionsChangedOnLayoutObject();
786 setNeedsStyleRecalc(SubtreeStyleChange
, StyleChangeReasonForTracing::create(StyleChangeReason::ControlValue
));
788 if (HTMLOptionsCollection
* collection
= cachedCollection
<HTMLOptionsCollection
>(SelectOptions
))
789 collection
->invalidateCache();
792 invalidateSelectedItems();
794 if (layoutObject()) {
795 if (AXObjectCache
* cache
= layoutObject()->document().existingAXObjectCache())
796 cache
->childrenChanged(this);
800 void HTMLSelectElement::recalcListItems(bool updateSelectedStates
) const
804 m_shouldRecalcListItems
= false;
806 HTMLOptionElement
* foundSelected
= 0;
807 HTMLOptionElement
* firstOption
= 0;
808 for (Element
* currentElement
= ElementTraversal::firstWithin(*this); currentElement
; ) {
809 if (!currentElement
->isHTMLElement()) {
810 currentElement
= ElementTraversal::nextSkippingChildren(*currentElement
, this);
813 HTMLElement
& current
= toHTMLElement(*currentElement
);
815 // We should ignore nested optgroup elements. The HTML parser flatten
816 // them. However we need to ignore nested optgroups built by DOM APIs.
817 // This behavior matches to IE and Firefox.
818 if (isHTMLOptGroupElement(current
)) {
819 if (current
.parentNode() != this) {
820 currentElement
= ElementTraversal::nextSkippingChildren(current
, this);
823 m_listItems
.append(¤t
);
824 if (Element
* nextElement
= ElementTraversal::firstWithin(current
)) {
825 currentElement
= nextElement
;
830 if (isHTMLOptionElement(current
)) {
831 m_listItems
.append(¤t
);
833 if (updateSelectedStates
&& !m_multiple
) {
834 HTMLOptionElement
& option
= toHTMLOptionElement(current
);
836 firstOption
= &option
;
837 if (option
.selected()) {
839 foundSelected
->setSelectedState(false);
840 foundSelected
= &option
;
841 } else if (m_size
<= 1 && !foundSelected
&& !option
.isDisabledFormControl()) {
842 foundSelected
= &option
;
843 foundSelected
->setSelectedState(true);
848 if (isHTMLHRElement(current
))
849 m_listItems
.append(¤t
);
851 // In conforming HTML code, only <optgroup> and <option> will be found
852 // within a <select>. We call NodeTraversal::nextSkippingChildren so that we only step
853 // into those tags that we choose to. For web-compat, we should cope
854 // with the case where odd tags like a <div> have been added but we
855 // handle this because such tags have already been removed from the
856 // <select>'s subtree at this point.
857 currentElement
= ElementTraversal::nextSkippingChildren(*currentElement
, this);
860 if (!foundSelected
&& m_size
<= 1 && firstOption
&& !firstOption
->selected())
861 firstOption
->setSelectedState(true);
864 int HTMLSelectElement::selectedIndex() const
868 // Return the number of the first option selected.
869 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& items
= listItems();
870 for (size_t i
= 0; i
< items
.size(); ++i
) {
871 HTMLElement
* element
= items
[i
];
872 if (isHTMLOptionElement(*element
)) {
873 if (toHTMLOptionElement(*element
).selected())
882 void HTMLSelectElement::setSelectedIndex(int index
)
884 selectOption(index
, DeselectOtherOptions
);
887 int HTMLSelectElement::suggestedIndex() const
889 return m_suggestedIndex
;
892 void HTMLSelectElement::setSuggestedIndex(int suggestedIndex
)
894 m_suggestedIndex
= suggestedIndex
;
896 if (LayoutObject
* layoutObject
= this->layoutObject()) {
897 layoutObject
->updateFromElement();
898 scrollToIndex(suggestedIndex
);
900 if (popupIsVisible())
901 m_popup
->updateFromElement();
904 void HTMLSelectElement::scrollToIndex(int listIndex
)
910 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& items
= listItems();
911 int listSize
= static_cast<int>(items
.size());
912 if (listIndex
>= listSize
)
914 document().updateLayoutIgnorePendingStylesheets();
915 if (!layoutObject() || !layoutObject()->isListBox())
917 LayoutRect bounds
= items
[listIndex
]->boundingBox();
918 toLayoutListBox(layoutObject())->scrollToRect(bounds
);
921 void HTMLSelectElement::optionSelectionStateChanged(HTMLOptionElement
* option
, bool optionIsSelected
)
923 ASSERT(option
->ownerSelectElement() == this);
924 if (optionIsSelected
)
925 selectOption(option
->index());
926 else if (!usesMenuList() || multiple())
929 selectOption(nextSelectableListIndex(-1));
932 void HTMLSelectElement::optionInserted(const HTMLOptionElement
& option
, bool optionIsSelected
)
934 ASSERT(option
.ownerSelectElement() == this);
935 if (optionIsSelected
)
936 selectOption(option
.index());
939 void HTMLSelectElement::optionRemoved(const HTMLOptionElement
& option
)
941 if (m_activeSelectionAnchorIndex
< 0 && m_activeSelectionEndIndex
< 0)
943 int listIndex
= optionToListIndex(option
.index());
944 if (listIndex
<= m_activeSelectionAnchorIndex
)
945 m_activeSelectionAnchorIndex
--;
946 if (listIndex
<= m_activeSelectionEndIndex
)
947 m_activeSelectionEndIndex
--;
948 if (listIndex
== selectedIndex())
949 setAutofilled(false);
952 // TODO(tkent): This function is not efficient. It contains multiple O(N)
954 void HTMLSelectElement::selectOption(int optionIndex
, SelectOptionFlags flags
)
956 bool shouldDeselect
= !m_multiple
|| (flags
& DeselectOtherOptions
);
958 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& items
= listItems();
959 // optionToListIndex is O(N).
960 int listIndex
= optionToListIndex(optionIndex
);
962 // selectedIndex() is O(N).
963 if (selectedIndex() != optionIndex
&& isAutofilled())
964 setAutofilled(false);
966 HTMLElement
* element
= 0;
967 if (listIndex
>= 0) {
968 element
= items
[listIndex
];
969 if (isHTMLOptionElement(*element
)) {
970 // setActiveSelectionAnchorIndex is O(N).
971 if (m_activeSelectionAnchorIndex
< 0 || shouldDeselect
)
972 setActiveSelectionAnchorIndex(listIndex
);
973 if (m_activeSelectionEndIndex
< 0 || shouldDeselect
)
974 setActiveSelectionEndIndex(listIndex
);
975 toHTMLOptionElement(*element
).setSelectedState(true);
979 // deselectItemsWithoutValidation() is O(N).
981 deselectItemsWithoutValidation(element
);
983 // For the menu list case, this is what makes the selected element appear.
984 if (LayoutObject
* layoutObject
= this->layoutObject())
985 layoutObject
->updateFromElement();
986 // PopupMenu::updateFromElement() posts an O(N) task.
987 if (popupIsVisible())
988 m_popup
->updateFromElement();
991 setNeedsValidityCheck();
993 if (usesMenuList()) {
994 m_isProcessingUserDrivenChange
= flags
& UserDriven
;
995 if (flags
& DispatchInputAndChangeEvent
)
996 dispatchInputAndChangeEventForMenuList();
997 if (LayoutObject
* layoutObject
= this->layoutObject()) {
998 if (usesMenuList()) {
999 // didSetSelectedIndex() is O(N) because of listToOptionIndex
1000 // and optionToListIndex.
1001 toLayoutMenuList(layoutObject
)->didSetSelectedIndex(listIndex
);
1006 notifyFormStateChanged();
1009 int HTMLSelectElement::optionToListIndex(int optionIndex
) const
1011 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& items
= listItems();
1012 int listSize
= static_cast<int>(items
.size());
1013 if (optionIndex
< 0 || optionIndex
>= listSize
)
1016 int optionIndex2
= -1;
1017 for (int listIndex
= 0; listIndex
< listSize
; ++listIndex
) {
1018 if (isHTMLOptionElement(*items
[listIndex
])) {
1020 if (optionIndex2
== optionIndex
)
1028 int HTMLSelectElement::listToOptionIndex(int listIndex
) const
1030 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& items
= listItems();
1031 if (listIndex
< 0 || listIndex
>= static_cast<int>(items
.size()) || !isHTMLOptionElement(*items
[listIndex
]))
1034 // Actual index of option not counting OPTGROUP entries that may be in list.
1035 int optionIndex
= 0;
1036 for (int i
= 0; i
< listIndex
; ++i
) {
1037 if (isHTMLOptionElement(*items
[i
]))
1044 void HTMLSelectElement::dispatchFocusEvent(Element
* oldFocusedElement
, WebFocusType type
, InputDeviceCapabilities
* sourceCapabilities
)
1046 // Save the selection so it can be compared to the new selection when
1047 // dispatching change events during blur event dispatch.
1049 saveLastSelection();
1050 HTMLFormControlElementWithState::dispatchFocusEvent(oldFocusedElement
, type
, sourceCapabilities
);
1053 void HTMLSelectElement::dispatchBlurEvent(Element
* newFocusedElement
, WebFocusType type
, InputDeviceCapabilities
* sourceCapabilities
)
1055 m_typeAhead
.resetSession();
1056 // We only need to fire change events here for menu lists, because we fire
1057 // change events for list boxes whenever the selection change is actually made.
1058 // This matches other browsers' behavior.
1060 dispatchInputAndChangeEventForMenuList();
1061 HTMLFormControlElementWithState::dispatchBlurEvent(newFocusedElement
, type
, sourceCapabilities
);
1064 void HTMLSelectElement::deselectItemsWithoutValidation(HTMLElement
* excludeElement
)
1066 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& items
= listItems();
1067 for (unsigned i
= 0; i
< items
.size(); ++i
) {
1068 HTMLElement
* element
= items
[i
];
1069 if (element
!= excludeElement
&& isHTMLOptionElement(*element
))
1070 toHTMLOptionElement(element
)->setSelectedState(false);
1074 FormControlState
HTMLSelectElement::saveFormControlState() const
1076 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& items
= listItems();
1077 size_t length
= items
.size();
1078 FormControlState state
;
1079 for (unsigned i
= 0; i
< length
; ++i
) {
1080 if (!isHTMLOptionElement(*items
[i
]))
1082 HTMLOptionElement
* option
= toHTMLOptionElement(items
[i
]);
1083 if (!option
->selected())
1085 state
.append(option
->value());
1086 state
.append(String::number(i
));
1093 size_t HTMLSelectElement::searchOptionsForValue(const String
& value
, size_t listIndexStart
, size_t listIndexEnd
) const
1095 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& items
= listItems();
1096 size_t loopEndIndex
= std::min(items
.size(), listIndexEnd
);
1097 for (size_t i
= listIndexStart
; i
< loopEndIndex
; ++i
) {
1098 if (!isHTMLOptionElement(items
[i
]))
1100 if (toHTMLOptionElement(items
[i
])->value() == value
)
1106 void HTMLSelectElement::restoreFormControlState(const FormControlState
& state
)
1110 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& items
= listItems();
1111 size_t itemsSize
= items
.size();
1115 for (size_t i
= 0; i
< itemsSize
; ++i
) {
1116 if (!isHTMLOptionElement(items
[i
]))
1118 toHTMLOptionElement(items
[i
])->setSelectedState(false);
1121 // The saved state should have at least one value and an index.
1122 ASSERT(state
.valueSize() >= 2);
1124 size_t index
= state
[1].toUInt();
1125 if (index
< itemsSize
&& isHTMLOptionElement(items
[index
]) && toHTMLOptionElement(items
[index
])->value() == state
[0]) {
1126 toHTMLOptionElement(items
[index
])->setSelectedState(true);
1128 size_t foundIndex
= searchOptionsForValue(state
[0], 0, itemsSize
);
1129 if (foundIndex
!= kNotFound
)
1130 toHTMLOptionElement(items
[foundIndex
])->setSelectedState(true);
1133 size_t startIndex
= 0;
1134 for (size_t i
= 0; i
< state
.valueSize(); i
+= 2) {
1135 const String
& value
= state
[i
];
1136 const size_t index
= state
[i
+ 1].toUInt();
1137 if (index
< itemsSize
&& isHTMLOptionElement(items
[index
]) && toHTMLOptionElement(items
[index
])->value() == value
) {
1138 toHTMLOptionElement(items
[index
])->setSelectedState(true);
1139 startIndex
= index
+ 1;
1141 size_t foundIndex
= searchOptionsForValue(value
, startIndex
, itemsSize
);
1142 if (foundIndex
== kNotFound
)
1143 foundIndex
= searchOptionsForValue(value
, 0, startIndex
);
1144 if (foundIndex
== kNotFound
)
1146 toHTMLOptionElement(items
[foundIndex
])->setSelectedState(true);
1147 startIndex
= foundIndex
+ 1;
1152 setOptionsChangedOnLayoutObject();
1153 setNeedsValidityCheck();
1156 void HTMLSelectElement::parseMultipleAttribute(const AtomicString
& value
)
1158 m_multiple
= !value
.isNull();
1159 setNeedsValidityCheck();
1161 lazyReattachIfAttached();
1164 void HTMLSelectElement::appendToFormData(FormData
& formData
)
1166 const AtomicString
& name
= this->name();
1170 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& items
= listItems();
1171 for (unsigned i
= 0; i
< items
.size(); ++i
) {
1172 HTMLElement
* element
= items
[i
];
1173 if (isHTMLOptionElement(*element
) && toHTMLOptionElement(*element
).selected() && !toHTMLOptionElement(*element
).isDisabledFormControl())
1174 formData
.append(name
, toHTMLOptionElement(*element
).value());
1178 void HTMLSelectElement::resetImpl()
1180 HTMLOptionElement
* firstOption
= 0;
1181 HTMLOptionElement
* selectedOption
= 0;
1183 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& items
= listItems();
1184 for (unsigned i
= 0; i
< items
.size(); ++i
) {
1185 HTMLElement
* element
= items
[i
];
1186 if (!isHTMLOptionElement(*element
))
1189 if (items
[i
]->fastHasAttribute(selectedAttr
)) {
1190 if (selectedOption
&& !m_multiple
)
1191 selectedOption
->setSelectedState(false);
1192 toHTMLOptionElement(element
)->setSelectedState(true);
1193 selectedOption
= toHTMLOptionElement(element
);
1195 toHTMLOptionElement(element
)->setSelectedState(false);
1199 firstOption
= toHTMLOptionElement(element
);
1202 if (!selectedOption
&& firstOption
&& !m_multiple
&& m_size
<= 1)
1203 firstOption
->setSelectedState(true);
1205 setOptionsChangedOnLayoutObject();
1206 setNeedsValidityCheck();
1209 void HTMLSelectElement::handlePopupOpenKeyboardEvent(Event
* event
)
1212 // Calling focus() may cause us to lose our layoutObject. Return true so
1213 // that our caller doesn't process the event further, but don't set
1214 // the event as handled.
1215 if (!layoutObject() || !layoutObject()->isMenuList() || isDisabledFormControl())
1217 // Save the selection so it can be compared to the new selection when
1218 // dispatching change events during selectOption, which gets called from
1219 // valueChanged, which gets called after the user makes a selection from the
1221 saveLastSelection();
1223 event
->setDefaultHandled();
1227 bool HTMLSelectElement::shouldOpenPopupForKeyDownEvent(KeyboardEvent
* keyEvent
)
1229 const String
& keyIdentifier
= keyEvent
->keyIdentifier();
1230 LayoutTheme
& layoutTheme
= LayoutTheme::theme();
1232 if (isSpatialNavigationEnabled(document().frame()))
1235 return ((layoutTheme
.popsMenuByArrowKeys() && (keyIdentifier
== "Down" || keyIdentifier
== "Up"))
1236 || (layoutTheme
.popsMenuByAltDownUpOrF4Key() && (keyIdentifier
== "Down" || keyIdentifier
== "Up") && keyEvent
->altKey())
1237 || (layoutTheme
.popsMenuByAltDownUpOrF4Key() && (!keyEvent
->altKey() && !keyEvent
->ctrlKey() && keyIdentifier
== "F4")));
1240 bool HTMLSelectElement::shouldOpenPopupForKeyPressEvent(KeyboardEvent
*event
)
1242 LayoutTheme
& layoutTheme
= LayoutTheme::theme();
1243 int keyCode
= event
->keyCode();
1245 return ((layoutTheme
.popsMenuBySpaceKey() && event
->keyCode() == ' ' && !m_typeAhead
.hasActiveSession(event
))
1246 || (layoutTheme
.popsMenuByReturnKey() && keyCode
== '\r'));
1249 void HTMLSelectElement::menuListDefaultEventHandler(Event
* event
)
1251 if (event
->type() == EventTypeNames::keydown
) {
1252 if (!layoutObject() || !event
->isKeyboardEvent())
1255 KeyboardEvent
* keyEvent
= toKeyboardEvent(event
);
1256 if (shouldOpenPopupForKeyDownEvent(keyEvent
)) {
1257 handlePopupOpenKeyboardEvent(event
);
1261 // When using spatial navigation, we want to be able to navigate away
1262 // from the select element when the user hits any of the arrow keys,
1263 // instead of changing the selection.
1264 if (isSpatialNavigationEnabled(document().frame())) {
1265 if (!m_activeSelectionState
)
1269 // The key handling below shouldn't be used for non spatial navigation mode Mac
1270 if (LayoutTheme::theme().popsMenuByArrowKeys() && !isSpatialNavigationEnabled(document().frame()))
1273 const String
& keyIdentifier
= keyEvent
->keyIdentifier();
1274 bool handled
= true;
1275 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& listItems
= this->listItems();
1276 int listIndex
= optionToListIndex(selectedIndex());
1278 if (keyIdentifier
== "Down" || keyIdentifier
== "Right")
1279 listIndex
= nextValidIndex(listIndex
, SkipForwards
, 1);
1280 else if (keyIdentifier
== "Up" || keyIdentifier
== "Left")
1281 listIndex
= nextValidIndex(listIndex
, SkipBackwards
, 1);
1282 else if (keyIdentifier
== "PageDown")
1283 listIndex
= nextValidIndex(listIndex
, SkipForwards
, 3);
1284 else if (keyIdentifier
== "PageUp")
1285 listIndex
= nextValidIndex(listIndex
, SkipBackwards
, 3);
1286 else if (keyIdentifier
== "Home")
1287 listIndex
= nextValidIndex(-1, SkipForwards
, 1);
1288 else if (keyIdentifier
== "End")
1289 listIndex
= nextValidIndex(listItems
.size(), SkipBackwards
, 1);
1293 if (handled
&& static_cast<size_t>(listIndex
) < listItems
.size())
1294 selectOption(listToOptionIndex(listIndex
), DeselectOtherOptions
| DispatchInputAndChangeEvent
| UserDriven
);
1297 event
->setDefaultHandled();
1300 if (event
->type() == EventTypeNames::keypress
) {
1301 if (!layoutObject() || !event
->isKeyboardEvent())
1304 int keyCode
= toKeyboardEvent(event
)->keyCode();
1305 if (keyCode
== ' ' && isSpatialNavigationEnabled(document().frame())) {
1306 // Use space to toggle arrow key handling for selection change or spatial navigation.
1307 m_activeSelectionState
= !m_activeSelectionState
;
1308 event
->setDefaultHandled();
1312 KeyboardEvent
* keyEvent
= toKeyboardEvent(event
);
1313 if (shouldOpenPopupForKeyPressEvent(keyEvent
)) {
1314 handlePopupOpenKeyboardEvent(event
);
1318 if (!LayoutTheme::theme().popsMenuByReturnKey() && keyCode
== '\r') {
1320 form()->submitImplicitly(event
, false);
1321 dispatchInputAndChangeEventForMenuList();
1322 event
->setDefaultHandled();
1326 if (event
->type() == EventTypeNames::mousedown
&& event
->isMouseEvent() && toMouseEvent(event
)->button() == LeftButton
) {
1327 InputDeviceCapabilities
* sourceCapabilities
= toMouseEvent(event
)->fromTouch() ? InputDeviceCapabilities::firesTouchEventsSourceCapabilities() : InputDeviceCapabilities::doesntFireTouchEventsSourceCapabilities();
1328 focus(true, WebFocusTypeNone
, sourceCapabilities
);
1329 if (layoutObject() && layoutObject()->isMenuList() && !isDisabledFormControl()) {
1330 if (popupIsVisible()) {
1333 // Save the selection so it can be compared to the new selection
1334 // when we call onChange during selectOption, which gets called
1335 // from valueChanged, which gets called after the user makes a
1336 // selection from the menu.
1337 saveLastSelection();
1338 // TODO(lanwei): Will check if we need to add InputDeviceCapabilities here
1339 // when select menu list gets focus, see https://crbug.com/476530.
1343 event
->setDefaultHandled();
1346 if (event
->type() == EventTypeNames::blur
) {
1347 if (popupIsVisible())
1352 void HTMLSelectElement::updateSelectedState(int listIndex
, bool multi
, bool shift
)
1354 ASSERT(listIndex
>= 0);
1356 HTMLElement
* clickedElement
= listItems()[listIndex
];
1357 ASSERT(clickedElement
);
1358 if (isHTMLOptGroupElement(clickedElement
))
1361 // Save the selection so it can be compared to the new selection when
1362 // dispatching change events during mouseup, or after autoscroll finishes.
1363 saveLastSelection();
1365 m_activeSelectionState
= true;
1367 bool shiftSelect
= m_multiple
&& shift
;
1368 bool multiSelect
= m_multiple
&& multi
&& !shift
;
1370 if (isHTMLOptionElement(*clickedElement
)) {
1371 // Keep track of whether an active selection (like during drag
1372 // selection), should select or deselect.
1373 if (toHTMLOptionElement(*clickedElement
).selected() && multiSelect
)
1374 m_activeSelectionState
= false;
1375 if (!m_activeSelectionState
)
1376 toHTMLOptionElement(*clickedElement
).setSelectedState(false);
1379 // If we're not in any special multiple selection mode, then deselect all
1380 // other items, excluding the clicked option. If no option was clicked, then
1381 // this will deselect all items in the list.
1382 if (!shiftSelect
&& !multiSelect
)
1383 deselectItemsWithoutValidation(clickedElement
);
1385 // If the anchor hasn't been set, and we're doing a single selection or a
1386 // shift selection, then initialize the anchor to the first selected index.
1387 if (m_activeSelectionAnchorIndex
< 0 && !multiSelect
)
1388 setActiveSelectionAnchorIndex(selectedIndex());
1390 // Set the selection state of the clicked option.
1391 if (isHTMLOptionElement(*clickedElement
) && !toHTMLOptionElement(*clickedElement
).isDisabledFormControl())
1392 toHTMLOptionElement(*clickedElement
).setSelectedState(true);
1394 // If there was no selectedIndex() for the previous initialization, or If
1395 // we're doing a single selection, or a multiple selection (using cmd or
1396 // ctrl), then initialize the anchor index to the listIndex that just got
1398 if (m_activeSelectionAnchorIndex
< 0 || !shiftSelect
)
1399 setActiveSelectionAnchorIndex(listIndex
);
1401 setActiveSelectionEndIndex(listIndex
);
1402 updateListBoxSelection(!multiSelect
);
1405 int HTMLSelectElement::listIndexForEventTargetOption(const Event
& event
)
1407 Node
* targetNode
= event
.target()->toNode();
1408 if (!targetNode
|| !isHTMLOptionElement(*targetNode
))
1410 return listIndexForOption(toHTMLOptionElement(*targetNode
));
1413 int HTMLSelectElement::listIndexForOption(const HTMLOptionElement
& option
)
1415 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& items
= this->listItems();
1416 size_t length
= items
.size();
1417 for (size_t i
= 0; i
< length
; ++i
) {
1418 if (items
[i
].get() == &option
)
1424 AutoscrollController
* HTMLSelectElement::autoscrollController() const
1426 if (Page
* page
= document().page())
1427 return &page
->autoscrollController();
1431 void HTMLSelectElement::handleMouseRelease()
1433 // We didn't start this click/drag on any options.
1434 if (m_lastOnChangeSelection
.isEmpty())
1439 void HTMLSelectElement::listBoxDefaultEventHandler(Event
* event
)
1441 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& listItems
= this->listItems();
1442 if (event
->type() == EventTypeNames::gesturetap
&& event
->isGestureEvent()) {
1444 // Calling focus() may cause us to lose our layoutObject or change the layoutObject type, in which case do not want to handle the event.
1445 if (!layoutObject() || !layoutObject()->isListBox())
1448 // Convert to coords relative to the list box if needed.
1449 GestureEvent
& gestureEvent
= toGestureEvent(*event
);
1450 int listIndex
= listIndexForEventTargetOption(gestureEvent
);
1451 if (listIndex
>= 0) {
1452 if (!isDisabledFormControl()) {
1453 updateSelectedState(listIndex
, true, gestureEvent
.shiftKey());
1456 event
->setDefaultHandled();
1458 } else if (event
->type() == EventTypeNames::mousedown
&& event
->isMouseEvent() && toMouseEvent(event
)->button() == LeftButton
) {
1460 // Calling focus() may cause us to lose our layoutObject, in which case do not want to handle the event.
1461 if (!layoutObject() || !layoutObject()->isListBox() || isDisabledFormControl())
1464 // Convert to coords relative to the list box if needed.
1465 MouseEvent
* mouseEvent
= toMouseEvent(event
);
1466 int listIndex
= listIndexForEventTargetOption(*mouseEvent
);
1467 if (listIndex
>= 0) {
1468 if (!isDisabledFormControl()) {
1470 updateSelectedState(listIndex
, mouseEvent
->metaKey(), mouseEvent
->shiftKey());
1472 updateSelectedState(listIndex
, mouseEvent
->ctrlKey(), mouseEvent
->shiftKey());
1475 if (LocalFrame
* frame
= document().frame())
1476 frame
->eventHandler().setMouseDownMayStartAutoscroll();
1478 event
->setDefaultHandled();
1480 } else if (event
->type() == EventTypeNames::mousemove
&& event
->isMouseEvent()) {
1481 MouseEvent
* mouseEvent
= toMouseEvent(event
);
1482 if (mouseEvent
->button() != LeftButton
|| !mouseEvent
->buttonDown())
1485 if (Page
* page
= document().page())
1486 page
->autoscrollController().startAutoscrollForSelection(layoutObject());
1488 int listIndex
= listIndexForEventTargetOption(*mouseEvent
);
1489 if (listIndex
>= 0) {
1490 if (!isDisabledFormControl()) {
1492 // Only extend selection if there is something selected.
1493 if (m_activeSelectionAnchorIndex
< 0)
1496 setActiveSelectionEndIndex(listIndex
);
1497 updateListBoxSelection(false);
1499 setActiveSelectionAnchorIndex(listIndex
);
1500 setActiveSelectionEndIndex(listIndex
);
1501 updateListBoxSelection(true);
1505 } else if (event
->type() == EventTypeNames::mouseup
&& event
->isMouseEvent() && toMouseEvent(event
)->button() == LeftButton
&& layoutObject()) {
1506 if (document().page() && document().page()->autoscrollController().autoscrollInProgress(toLayoutBox(layoutObject())))
1507 document().page()->autoscrollController().stopAutoscroll();
1509 handleMouseRelease();
1510 } else if (event
->type() == EventTypeNames::keydown
) {
1511 if (!event
->isKeyboardEvent())
1513 const String
& keyIdentifier
= toKeyboardEvent(event
)->keyIdentifier();
1515 bool handled
= false;
1517 if (m_activeSelectionEndIndex
< 0) {
1518 // Initialize the end index
1519 if (keyIdentifier
== "Down" || keyIdentifier
== "PageDown") {
1520 int startIndex
= lastSelectedListIndex();
1522 if (keyIdentifier
== "Down")
1523 endIndex
= nextSelectableListIndex(startIndex
);
1525 endIndex
= nextSelectableListIndexPageAway(startIndex
, SkipForwards
);
1526 } else if (keyIdentifier
== "Up" || keyIdentifier
== "PageUp") {
1527 int startIndex
= optionToListIndex(selectedIndex());
1529 if (keyIdentifier
== "Up")
1530 endIndex
= previousSelectableListIndex(startIndex
);
1532 endIndex
= nextSelectableListIndexPageAway(startIndex
, SkipBackwards
);
1535 // Set the end index based on the current end index.
1536 if (keyIdentifier
== "Down") {
1537 endIndex
= nextSelectableListIndex(m_activeSelectionEndIndex
);
1539 } else if (keyIdentifier
== "Up") {
1540 endIndex
= previousSelectableListIndex(m_activeSelectionEndIndex
);
1542 } else if (keyIdentifier
== "PageDown") {
1543 endIndex
= nextSelectableListIndexPageAway(m_activeSelectionEndIndex
, SkipForwards
);
1545 } else if (keyIdentifier
== "PageUp") {
1546 endIndex
= nextSelectableListIndexPageAway(m_activeSelectionEndIndex
, SkipBackwards
);
1550 if (keyIdentifier
== "Home") {
1551 endIndex
= firstSelectableListIndex();
1553 } else if (keyIdentifier
== "End") {
1554 endIndex
= lastSelectableListIndex();
1558 if (isSpatialNavigationEnabled(document().frame())) {
1559 // Check if the selection moves to the boundary.
1560 if (keyIdentifier
== "Left" || keyIdentifier
== "Right" || ((keyIdentifier
== "Down" || keyIdentifier
== "Up") && endIndex
== m_activeSelectionEndIndex
))
1564 if (endIndex
>= 0 && handled
) {
1565 // Save the selection so it can be compared to the new selection
1566 // when dispatching change events immediately after making the new
1568 saveLastSelection();
1570 ASSERT_UNUSED(listItems
, !listItems
.size() || static_cast<size_t>(endIndex
) < listItems
.size());
1571 setActiveSelectionEndIndex(endIndex
);
1573 bool selectNewItem
= !m_multiple
|| toKeyboardEvent(event
)->shiftKey() || !isSpatialNavigationEnabled(document().frame());
1575 m_activeSelectionState
= true;
1576 // If the anchor is unitialized, or if we're going to deselect all
1577 // other options, then set the anchor index equal to the end index.
1578 bool deselectOthers
= !m_multiple
|| (!toKeyboardEvent(event
)->shiftKey() && selectNewItem
);
1579 if (m_activeSelectionAnchorIndex
< 0 || deselectOthers
) {
1581 deselectItemsWithoutValidation();
1582 setActiveSelectionAnchorIndex(m_activeSelectionEndIndex
);
1585 scrollToIndex(endIndex
);
1586 if (selectNewItem
) {
1587 updateListBoxSelection(deselectOthers
);
1590 scrollToSelection();
1593 event
->setDefaultHandled();
1595 } else if (event
->type() == EventTypeNames::keypress
) {
1596 if (!event
->isKeyboardEvent())
1598 int keyCode
= toKeyboardEvent(event
)->keyCode();
1600 if (keyCode
== '\r') {
1602 form()->submitImplicitly(event
, false);
1603 event
->setDefaultHandled();
1604 } else if (m_multiple
&& keyCode
== ' ' && isSpatialNavigationEnabled(document().frame())) {
1605 // Use space to toggle selection change.
1606 m_activeSelectionState
= !m_activeSelectionState
;
1607 updateSelectedState(listToOptionIndex(m_activeSelectionEndIndex
), true /*multi*/, false /*shift*/);
1609 event
->setDefaultHandled();
1614 void HTMLSelectElement::defaultEventHandler(Event
* event
)
1616 if (!layoutObject())
1619 if (isDisabledFormControl()) {
1620 HTMLFormControlElementWithState::defaultEventHandler(event
);
1625 menuListDefaultEventHandler(event
);
1627 listBoxDefaultEventHandler(event
);
1628 if (event
->defaultHandled())
1631 if (event
->type() == EventTypeNames::keypress
&& event
->isKeyboardEvent()) {
1632 KeyboardEvent
* keyboardEvent
= toKeyboardEvent(event
);
1633 if (!keyboardEvent
->ctrlKey() && !keyboardEvent
->altKey() && !keyboardEvent
->metaKey() && isPrintableChar(keyboardEvent
->charCode())) {
1634 typeAheadFind(keyboardEvent
);
1635 event
->setDefaultHandled();
1639 HTMLFormControlElementWithState::defaultEventHandler(event
);
1642 int HTMLSelectElement::lastSelectedListIndex() const
1644 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& items
= listItems();
1645 for (size_t i
= items
.size(); i
;) {
1646 HTMLElement
* element
= items
[--i
];
1647 if (isHTMLOptionElement(*element
) && toHTMLOptionElement(element
)->selected())
1653 int HTMLSelectElement::indexOfSelectedOption() const
1655 return optionToListIndex(selectedIndex());
1658 int HTMLSelectElement::optionCount() const
1660 return listItems().size();
1663 String
HTMLSelectElement::optionAtIndex(int index
) const
1665 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& items
= listItems();
1667 HTMLElement
* element
= items
[index
];
1668 if (!isHTMLOptionElement(*element
) || toHTMLOptionElement(element
)->isDisabledFormControl())
1670 return toHTMLOptionElement(element
)->text();
1673 void HTMLSelectElement::typeAheadFind(KeyboardEvent
* event
)
1675 int index
= m_typeAhead
.handleEvent(event
, TypeAhead::MatchPrefix
| TypeAhead::CycleFirstChar
);
1678 selectOption(listToOptionIndex(index
), DeselectOtherOptions
| DispatchInputAndChangeEvent
| UserDriven
);
1679 if (!usesMenuList())
1683 Node::InsertionNotificationRequest
HTMLSelectElement::insertedInto(ContainerNode
* insertionPoint
)
1685 // When the element is created during document parsing, it won't have any
1686 // items yet - but for innerHTML and related methods, this method is called
1687 // after the whole subtree is constructed.
1689 HTMLFormControlElementWithState::insertedInto(insertionPoint
);
1690 return InsertionDone
;
1693 void HTMLSelectElement::accessKeySetSelectedIndex(int index
)
1695 // First bring into focus the list box.
1697 accessKeyAction(false);
1699 // If this index is already selected, unselect. otherwise update the selected index.
1700 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& items
= listItems();
1701 int listIndex
= optionToListIndex(index
);
1702 if (listIndex
>= 0) {
1703 HTMLElement
* element
= items
[listIndex
];
1704 if (isHTMLOptionElement(*element
)) {
1705 if (toHTMLOptionElement(*element
).selected())
1706 toHTMLOptionElement(*element
).setSelectedState(false);
1708 selectOption(index
, DispatchInputAndChangeEvent
| UserDriven
);
1713 dispatchInputAndChangeEventForMenuList();
1717 scrollToSelection();
1720 unsigned HTMLSelectElement::length() const
1722 unsigned options
= 0;
1724 const WillBeHeapVector
<RawPtrWillBeMember
<HTMLElement
>>& items
= listItems();
1725 for (unsigned i
= 0; i
< items
.size(); ++i
) {
1726 if (isHTMLOptionElement(*items
[i
]))
1733 void HTMLSelectElement::finishParsingChildren()
1735 HTMLFormControlElementWithState::finishParsingChildren();
1736 updateListItemSelectedStates();
1737 if (!usesMenuList())
1738 scrollToSelection();
1741 bool HTMLSelectElement::anonymousIndexedSetter(unsigned index
, PassRefPtrWillBeRawPtr
<HTMLOptionElement
> value
, ExceptionState
& exceptionState
)
1743 if (!value
) { // undefined or null
1747 setOption(index
, value
.get(), exceptionState
);
1751 bool HTMLSelectElement::isInteractiveContent() const
1756 bool HTMLSelectElement::supportsAutofocus() const
1761 void HTMLSelectElement::updateListOnLayoutObject()
1763 setOptionsChangedOnLayoutObject();
1766 DEFINE_TRACE(HTMLSelectElement
)
1769 visitor
->trace(m_listItems
);
1771 visitor
->trace(m_popup
);
1772 HTMLFormControlElementWithState::trace(visitor
);
1775 void HTMLSelectElement::willRecalcStyle(StyleRecalcChange change
)
1777 // recalcListItems will update the selected state of the <option> elements
1778 // in this <select> so we need to do it before we recalc their style so they
1779 // match the right selectors (ex. :checked).
1780 // TODO(esprehn): Find a way to avoid needing a willRecalcStyle callback.
1781 if (m_shouldRecalcListItems
)
1785 void HTMLSelectElement::didAddUserAgentShadowRoot(ShadowRoot
& root
)
1787 RefPtrWillBeRawPtr
<HTMLContentElement
> content
= HTMLContentElement::create(document());
1788 content
->setAttribute(selectAttr
, "option,optgroup,hr");
1789 root
.appendChild(content
);
1792 HTMLOptionElement
* HTMLSelectElement::spatialNavigationFocusedOption()
1794 if (!isSpatialNavigationEnabled(document().frame()))
1796 int focusedIndex
= activeSelectionEndListIndex();
1797 if (focusedIndex
< 0)
1798 focusedIndex
= firstSelectableListIndex();
1799 if (focusedIndex
< 0)
1801 HTMLElement
* focused
= listItems()[focusedIndex
];
1802 return isHTMLOptionElement(focused
) ? toHTMLOptionElement(focused
) : nullptr;
1805 String
HTMLSelectElement::itemText(const Element
& element
) const
1808 if (isHTMLOptGroupElement(element
))
1809 itemString
= toHTMLOptGroupElement(element
).groupLabelText();
1810 else if (isHTMLOptionElement(element
))
1811 itemString
= toHTMLOptionElement(element
).textIndentedToRespectGroupLabel();
1814 applyTextTransform(layoutObject()->style(), itemString
, ' ');
1818 bool HTMLSelectElement::itemIsDisplayNone(Element
& element
) const
1820 if (isHTMLOptionElement(element
))
1821 return toHTMLOptionElement(element
).isDisplayNone();
1822 if (const ComputedStyle
* style
= itemComputedStyle(element
))
1823 return style
->display() == NONE
;
1827 const ComputedStyle
* HTMLSelectElement::itemComputedStyle(Element
& element
) const
1829 return element
.computedStyle() ? element
.computedStyle() : element
.ensureComputedStyle();
1832 IntRect
HTMLSelectElement::elementRectRelativeToViewport() const
1834 if (!layoutObject())
1836 // We don't use absoluteBoundingBoxRect() because it can return an IntRect
1837 // larger the actual size by 1px.
1838 return document().view()->contentsToViewport(roundedIntRect(layoutObject()->absoluteBoundingBoxFloatRect()));
1841 LayoutUnit
HTMLSelectElement::clientPaddingLeft() const
1843 if (layoutObject() && layoutObject()->isMenuList())
1844 return toLayoutMenuList(layoutObject())->clientPaddingLeft();
1848 LayoutUnit
HTMLSelectElement::clientPaddingRight() const
1850 if (layoutObject() && layoutObject()->isMenuList())
1851 return toLayoutMenuList(layoutObject())->clientPaddingRight();
1855 void HTMLSelectElement::popupDidHide()
1857 m_popupIsVisible
= false;
1858 if (AXObjectCache
* cache
= document().existingAXObjectCache()) {
1859 if (layoutObject() && layoutObject()->isMenuList())
1860 cache
->didHideMenuListPopup(toLayoutMenuList(layoutObject()));
1864 void HTMLSelectElement::setIndexToSelectOnCancel(int listIndex
)
1866 m_indexToSelectOnCancel
= listIndex
;
1868 layoutObject()->updateFromElement();
1871 int HTMLSelectElement::optionIndexToBeShown() const
1873 if (m_indexToSelectOnCancel
>= 0)
1874 return listToOptionIndex(m_indexToSelectOnCancel
);
1875 if (suggestedIndex() >= 0)
1876 return suggestedIndex();
1877 return selectedIndex();
1880 void HTMLSelectElement::valueChanged(unsigned listIndex
)
1882 // Check to ensure a page navigation has not occurred while the popup was
1884 Document
& doc
= document();
1885 if (&doc
!= doc
.frame()->document())
1888 setIndexToSelectOnCancel(-1);
1889 optionSelectedByUser(listToOptionIndex(listIndex
), true);
1892 void HTMLSelectElement::popupDidCancel()
1894 if (m_indexToSelectOnCancel
>= 0)
1895 valueChanged(m_indexToSelectOnCancel
);
1898 void HTMLSelectElement::provisionalSelectionChanged(unsigned listIndex
)
1900 setIndexToSelectOnCancel(listIndex
);
1903 void HTMLSelectElement::showPopup()
1905 if (popupIsVisible())
1907 if (document().frameHost()->chromeClient().hasOpenedPopup())
1909 if (!layoutObject() || !layoutObject()->isMenuList())
1913 m_popup
= document().frameHost()->chromeClient().openPopupMenu(*document().frame(), *this);
1914 m_popupIsVisible
= true;
1916 LayoutMenuList
* menuList
= toLayoutMenuList(layoutObject());
1917 FloatQuad
quad(menuList
->localToAbsoluteQuad(FloatQuad(menuList
->borderBoundingBox())));
1918 IntSize size
= pixelSnappedIntRect(menuList
->frameRect()).size();
1919 m_popup
->show(quad
, size
, optionToListIndex(selectedIndex()));
1920 if (AXObjectCache
* cache
= document().existingAXObjectCache())
1921 cache
->didShowMenuListPopup(menuList
);
1924 void HTMLSelectElement::hidePopup()
1930 void HTMLSelectElement::didRecalcStyle(StyleRecalcChange change
)
1932 HTMLFormControlElementWithState::didRecalcStyle(change
);
1933 if (popupIsVisible())
1934 m_popup
->updateFromElement();
1937 void HTMLSelectElement::detach(const AttachContext
& context
)
1939 HTMLFormControlElementWithState::detach(context
);
1941 m_popup
->disconnectClient();
1942 m_popupIsVisible
= false;
1946 void HTMLSelectElement::resetTypeAheadSessionForTesting()
1948 m_typeAhead
.resetSession();
1951 } // namespace blink