Move parseFontFaceDescriptor to CSSPropertyParser.cpp
[chromium-blink-merge.git] / third_party / WebKit / Source / core / html / HTMLSelectElement.cpp
blob0da37222e510ed9c8c6c1437cc59b2fd73e827e7
1 /*
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.
28 #include "config.h"
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;
72 namespace blink {
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)
81 , m_typeAhead(this)
82 , m_size(0)
83 , m_lastOnChangeIndex(-1)
84 , m_activeSelectionAnchorIndex(-1)
85 , m_activeSelectionEndIndex(-1)
86 , m_isProcessingUserDrivenChange(false)
87 , m_multiple(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();
130 if (fireOnChangeNow)
131 listBoxOnChange();
132 return;
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())
140 return;
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)
159 return false;
161 int listIndex = optionToListIndex(0);
162 ASSERT(listIndex >= 0);
163 if (listIndex < 0)
164 return false;
165 return !listIndex && toHTMLOptionElement(listItems()[listIndex])->value().isEmpty();
168 String HTMLSelectElement::validationMessage() const
170 if (!willValidate())
171 return String();
172 if (customError())
173 return customValidationMessage();
174 if (valueMissing())
175 return locale().queryString(WebLocalizedString::ValidationValueMissingForSelect);
176 return String();
179 bool HTMLSelectElement::valueMissing() const
181 if (!willValidate())
182 return false;
184 if (!isRequired())
185 return false;
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)
195 if (!multiple()) {
196 optionSelectedByUser(listToOptionIndex(listIndex), fireOnChangeNow, false);
197 } else {
198 updateSelectedState(listIndex, allowMultiplySelections, shift);
199 setNeedsValidityCheck();
200 if (fireOnChangeNow)
201 listBoxOnChange();
205 bool HTMLSelectElement::usesMenuList() const
207 if (LayoutTheme::theme().delegatesMenuListRendering())
208 return true;
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();
226 else
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());
234 else
235 beforeElement = nullptr;
237 insertBefore(elementToInsert, beforeElement.get(), exceptionState);
238 setNeedsValidityCheck();
241 void HTMLSelectElement::remove(int optionIndex)
243 int listIndex = optionToListIndex(optionIndex);
244 if (listIndex < 0)
245 return;
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();
257 return "";
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.
263 int optionIndex = 0;
264 if (value.isNull()) {
265 optionIndex = -1;
266 } else {
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)
272 break;
273 optionIndex++;
276 if (optionIndex >= static_cast<int>(items.size()))
277 optionIndex = -1;
280 int previousSelectedIndex = selectedIndex();
281 setSuggestedIndex(-1);
282 if (m_isAutofilledByPreview)
283 setAutofilled(false);
284 setSelectedIndex(optionIndex);
286 if (sendEvents && previousSelectedIndex != selectedIndex()) {
287 if (usesMenuList())
288 dispatchInputAndChangeEventForMenuList(false);
289 else
290 listBoxOnChange();
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();
303 return "";
306 void HTMLSelectElement::setSuggestedValue(const String& value)
308 if (value.isNull()) {
309 setSuggestedIndex(-1);
310 return;
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;
320 return;
322 optionIndex++;
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
334 return false;
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.
356 if (oldSize != size)
357 updateListItemSelectedStates();
359 m_size = size;
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())
373 hidePopup();
375 } else {
376 HTMLFormControlElementWithState::parseAttribute(name, value);
380 bool HTMLSelectElement::shouldShowFocusRingOnMouseFocus() const
382 return true;
385 bool HTMLSelectElement::canSelectAll() const
387 return !usesMenuList();
390 LayoutObject* HTMLSelectElement::createLayoutObject(const ComputedStyle&)
392 if (usesMenuList())
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)
411 return;
412 recalcListItems();
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)
438 focus();
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)));
474 return;
476 int diff = index - length();
477 HTMLOptionElementOrHTMLOptGroupElement element;
478 element.setHTMLOptionElement(option);
479 HTMLElementOrLong before;
480 // Out of array bounds? First insert empty dummies.
481 if (diff > 0) {
482 setLength(index, exceptionState);
483 // Replace an existing entry?
484 } else if (diff < 0) {
485 before.setHTMLElement(options()->item(index + 1));
486 remove(index);
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)));
501 return;
503 int diff = length() - newLen;
505 if (diff < 0) { // Add dummy elements.
506 do {
507 appendChild(document().createElement(optionTag, false), exceptionState);
508 if (exceptionState.hadException())
509 break;
510 } while (++diff);
511 } else {
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
537 return isRequired();
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) {
551 --skip;
552 HTMLElement* element = listItems[listIndex];
553 if (!isHTMLOptionElement(*element))
554 continue;
555 if (toHTMLOptionElement(*element).isDisplayNone())
556 continue;
557 if (element->isDisabledFormControl())
558 continue;
559 if (!usesMenuList() && !element->layoutObject())
560 continue;
561 lastGoodIndex = listIndex;
562 if (skip <= 0)
563 break;
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())
585 return -1;
586 return index;
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.
599 int pageSize = 0;
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)
615 return;
617 // Save the selection so it can be compared to the new selectAll selection
618 // when dispatching change events.
619 saveLastSelection();
621 m_activeSelectionState = true;
622 setActiveSelectionAnchorIndex(nextSelectableListIndex(-1));
623 setActiveSelectionEndIndex(previousSelectableListIndex(-1));
625 updateListBoxSelection(false, false);
626 listBoxOnChange();
627 setNeedsValidityCheck();
630 void HTMLSelectElement::saveLastSelection()
632 if (usesMenuList()) {
633 m_lastOnChangeIndex = selectedIndex();
634 return;
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())
676 continue;
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);
682 else
683 toHTMLOptionElement(element)->setSelectedState(m_cachedStateForActiveSelection[i]);
686 setNeedsValidityCheck();
687 if (scroll)
688 scrollToSelection();
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();
703 return;
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])
712 fireOnChange = true;
713 m_lastOnChangeSelection[i] = selected;
716 if (fireOnChange) {
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())
740 return;
741 if (usesMenuList())
742 return;
743 scrollToIndex(activeSelectionEndListIndex());
744 if (AXObjectCache* cache = document().existingAXObjectCache())
745 cache->listboxActiveIndexChanged(this);
748 void HTMLSelectElement::setOptionsChangedOnLayoutObject()
750 if (LayoutObject* layoutObject = this->layoutObject()) {
751 if (usesMenuList())
752 toLayoutMenuList(layoutObject)->setOptionsChanged(true);
756 const WillBeHeapVector<RawPtrWillBeMember<HTMLElement>>& HTMLSelectElement::listItems() const
758 if (m_shouldRecalcListItems) {
759 recalcListItems();
760 } else {
761 #if ENABLE(ASSERT)
762 WillBeHeapVector<RawPtrWillBeMember<HTMLElement>> items = m_listItems;
763 recalcListItems(false);
764 ASSERT(items == m_listItems);
765 #endif
768 return 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));
787 if (!inDocument()) {
788 if (HTMLOptionsCollection* collection = cachedCollection<HTMLOptionsCollection>(SelectOptions))
789 collection->invalidateCache();
791 if (!inDocument())
792 invalidateSelectedItems();
794 if (layoutObject()) {
795 if (AXObjectCache* cache = layoutObject()->document().existingAXObjectCache())
796 cache->childrenChanged(this);
800 void HTMLSelectElement::recalcListItems(bool updateSelectedStates) const
802 m_listItems.clear();
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);
811 continue;
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);
821 continue;
823 m_listItems.append(&current);
824 if (Element* nextElement = ElementTraversal::firstWithin(current)) {
825 currentElement = nextElement;
826 continue;
830 if (isHTMLOptionElement(current)) {
831 m_listItems.append(&current);
833 if (updateSelectedStates && !m_multiple) {
834 HTMLOptionElement& option = toHTMLOptionElement(current);
835 if (!firstOption)
836 firstOption = &option;
837 if (option.selected()) {
838 if (foundSelected)
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(&current);
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
866 unsigned index = 0;
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())
874 return index;
875 ++index;
879 return -1;
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)
906 if (listIndex < 0)
907 return;
908 if (usesMenuList())
909 return;
910 const WillBeHeapVector<RawPtrWillBeMember<HTMLElement>>& items = listItems();
911 int listSize = static_cast<int>(items.size());
912 if (listIndex >= listSize)
913 return;
914 document().updateLayoutIgnorePendingStylesheets();
915 if (!layoutObject() || !layoutObject()->isListBox())
916 return;
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())
927 selectOption(-1);
928 else
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)
942 return;
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)
953 // operations.
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).
980 if (shouldDeselect)
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();
990 scrollToSelection();
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)
1014 return -1;
1016 int optionIndex2 = -1;
1017 for (int listIndex = 0; listIndex < listSize; ++listIndex) {
1018 if (isHTMLOptionElement(*items[listIndex])) {
1019 ++optionIndex2;
1020 if (optionIndex2 == optionIndex)
1021 return listIndex;
1025 return -1;
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]))
1032 return -1;
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]))
1038 ++optionIndex;
1041 return optionIndex;
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.
1048 if (usesMenuList())
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.
1059 if (usesMenuList())
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]))
1081 continue;
1082 HTMLOptionElement* option = toHTMLOptionElement(items[i]);
1083 if (!option->selected())
1084 continue;
1085 state.append(option->value());
1086 state.append(String::number(i));
1087 if (!multiple())
1088 break;
1090 return state;
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]))
1099 continue;
1100 if (toHTMLOptionElement(items[i])->value() == value)
1101 return i;
1103 return kNotFound;
1106 void HTMLSelectElement::restoreFormControlState(const FormControlState& state)
1108 recalcListItems();
1110 const WillBeHeapVector<RawPtrWillBeMember<HTMLElement>>& items = listItems();
1111 size_t itemsSize = items.size();
1112 if (!itemsSize)
1113 return;
1115 for (size_t i = 0; i < itemsSize; ++i) {
1116 if (!isHTMLOptionElement(items[i]))
1117 continue;
1118 toHTMLOptionElement(items[i])->setSelectedState(false);
1121 // The saved state should have at least one value and an index.
1122 ASSERT(state.valueSize() >= 2);
1123 if (!multiple()) {
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);
1127 } else {
1128 size_t foundIndex = searchOptionsForValue(state[0], 0, itemsSize);
1129 if (foundIndex != kNotFound)
1130 toHTMLOptionElement(items[foundIndex])->setSelectedState(true);
1132 } else {
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;
1140 } else {
1141 size_t foundIndex = searchOptionsForValue(value, startIndex, itemsSize);
1142 if (foundIndex == kNotFound)
1143 foundIndex = searchOptionsForValue(value, 0, startIndex);
1144 if (foundIndex == kNotFound)
1145 continue;
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();
1167 if (name.isEmpty())
1168 return;
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))
1187 continue;
1189 if (items[i]->fastHasAttribute(selectedAttr)) {
1190 if (selectedOption && !m_multiple)
1191 selectedOption->setSelectedState(false);
1192 toHTMLOptionElement(element)->setSelectedState(true);
1193 selectedOption = toHTMLOptionElement(element);
1194 } else {
1195 toHTMLOptionElement(element)->setSelectedState(false);
1198 if (!firstOption)
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)
1211 focus();
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())
1216 return;
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
1220 // menu.
1221 saveLastSelection();
1222 showPopup();
1223 event->setDefaultHandled();
1224 return;
1227 bool HTMLSelectElement::shouldOpenPopupForKeyDownEvent(KeyboardEvent* keyEvent)
1229 const String& keyIdentifier = keyEvent->keyIdentifier();
1230 LayoutTheme& layoutTheme = LayoutTheme::theme();
1232 if (isSpatialNavigationEnabled(document().frame()))
1233 return false;
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())
1253 return;
1255 KeyboardEvent* keyEvent = toKeyboardEvent(event);
1256 if (shouldOpenPopupForKeyDownEvent(keyEvent)) {
1257 handlePopupOpenKeyboardEvent(event);
1258 return;
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)
1266 return;
1269 // The key handling below shouldn't be used for non spatial navigation mode Mac
1270 if (LayoutTheme::theme().popsMenuByArrowKeys() && !isSpatialNavigationEnabled(document().frame()))
1271 return;
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);
1290 else
1291 handled = false;
1293 if (handled && static_cast<size_t>(listIndex) < listItems.size())
1294 selectOption(listToOptionIndex(listIndex), DeselectOtherOptions | DispatchInputAndChangeEvent | UserDriven);
1296 if (handled)
1297 event->setDefaultHandled();
1300 if (event->type() == EventTypeNames::keypress) {
1301 if (!layoutObject() || !event->isKeyboardEvent())
1302 return;
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();
1309 return;
1312 KeyboardEvent* keyEvent = toKeyboardEvent(event);
1313 if (shouldOpenPopupForKeyPressEvent(keyEvent)) {
1314 handlePopupOpenKeyboardEvent(event);
1315 return;
1318 if (!LayoutTheme::theme().popsMenuByReturnKey() && keyCode == '\r') {
1319 if (form())
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()) {
1331 hidePopup();
1332 } else {
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.
1340 showPopup();
1343 event->setDefaultHandled();
1346 if (event->type() == EventTypeNames::blur) {
1347 if (popupIsVisible())
1348 hidePopup();
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))
1359 return;
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
1397 // clicked.
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))
1409 return -1;
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)
1419 return i;
1421 return -1;
1424 AutoscrollController* HTMLSelectElement::autoscrollController() const
1426 if (Page* page = document().page())
1427 return &page->autoscrollController();
1428 return 0;
1431 void HTMLSelectElement::handleMouseRelease()
1433 // We didn't start this click/drag on any options.
1434 if (m_lastOnChangeSelection.isEmpty())
1435 return;
1436 listBoxOnChange();
1439 void HTMLSelectElement::listBoxDefaultEventHandler(Event* event)
1441 const WillBeHeapVector<RawPtrWillBeMember<HTMLElement>>& listItems = this->listItems();
1442 if (event->type() == EventTypeNames::gesturetap && event->isGestureEvent()) {
1443 focus();
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())
1446 return;
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());
1454 listBoxOnChange();
1456 event->setDefaultHandled();
1458 } else if (event->type() == EventTypeNames::mousedown && event->isMouseEvent() && toMouseEvent(event)->button() == LeftButton) {
1459 focus();
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())
1462 return;
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()) {
1469 #if OS(MACOSX)
1470 updateSelectedState(listIndex, mouseEvent->metaKey(), mouseEvent->shiftKey());
1471 #else
1472 updateSelectedState(listIndex, mouseEvent->ctrlKey(), mouseEvent->shiftKey());
1473 #endif
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())
1483 return;
1485 if (Page* page = document().page())
1486 page->autoscrollController().startAutoscrollForSelection(layoutObject());
1488 int listIndex = listIndexForEventTargetOption(*mouseEvent);
1489 if (listIndex >= 0) {
1490 if (!isDisabledFormControl()) {
1491 if (m_multiple) {
1492 // Only extend selection if there is something selected.
1493 if (m_activeSelectionAnchorIndex < 0)
1494 return;
1496 setActiveSelectionEndIndex(listIndex);
1497 updateListBoxSelection(false);
1498 } else {
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();
1508 else
1509 handleMouseRelease();
1510 } else if (event->type() == EventTypeNames::keydown) {
1511 if (!event->isKeyboardEvent())
1512 return;
1513 const String& keyIdentifier = toKeyboardEvent(event)->keyIdentifier();
1515 bool handled = false;
1516 int endIndex = 0;
1517 if (m_activeSelectionEndIndex < 0) {
1518 // Initialize the end index
1519 if (keyIdentifier == "Down" || keyIdentifier == "PageDown") {
1520 int startIndex = lastSelectedListIndex();
1521 handled = true;
1522 if (keyIdentifier == "Down")
1523 endIndex = nextSelectableListIndex(startIndex);
1524 else
1525 endIndex = nextSelectableListIndexPageAway(startIndex, SkipForwards);
1526 } else if (keyIdentifier == "Up" || keyIdentifier == "PageUp") {
1527 int startIndex = optionToListIndex(selectedIndex());
1528 handled = true;
1529 if (keyIdentifier == "Up")
1530 endIndex = previousSelectableListIndex(startIndex);
1531 else
1532 endIndex = nextSelectableListIndexPageAway(startIndex, SkipBackwards);
1534 } else {
1535 // Set the end index based on the current end index.
1536 if (keyIdentifier == "Down") {
1537 endIndex = nextSelectableListIndex(m_activeSelectionEndIndex);
1538 handled = true;
1539 } else if (keyIdentifier == "Up") {
1540 endIndex = previousSelectableListIndex(m_activeSelectionEndIndex);
1541 handled = true;
1542 } else if (keyIdentifier == "PageDown") {
1543 endIndex = nextSelectableListIndexPageAway(m_activeSelectionEndIndex, SkipForwards);
1544 handled = true;
1545 } else if (keyIdentifier == "PageUp") {
1546 endIndex = nextSelectableListIndexPageAway(m_activeSelectionEndIndex, SkipBackwards);
1547 handled = true;
1550 if (keyIdentifier == "Home") {
1551 endIndex = firstSelectableListIndex();
1552 handled = true;
1553 } else if (keyIdentifier == "End") {
1554 endIndex = lastSelectableListIndex();
1555 handled = true;
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))
1561 return;
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
1567 // selection.
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());
1574 if (selectNewItem)
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) {
1580 if (deselectOthers)
1581 deselectItemsWithoutValidation();
1582 setActiveSelectionAnchorIndex(m_activeSelectionEndIndex);
1585 scrollToIndex(endIndex);
1586 if (selectNewItem) {
1587 updateListBoxSelection(deselectOthers);
1588 listBoxOnChange();
1589 } else {
1590 scrollToSelection();
1593 event->setDefaultHandled();
1595 } else if (event->type() == EventTypeNames::keypress) {
1596 if (!event->isKeyboardEvent())
1597 return;
1598 int keyCode = toKeyboardEvent(event)->keyCode();
1600 if (keyCode == '\r') {
1601 if (form())
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*/);
1608 listBoxOnChange();
1609 event->setDefaultHandled();
1614 void HTMLSelectElement::defaultEventHandler(Event* event)
1616 if (!layoutObject())
1617 return;
1619 if (isDisabledFormControl()) {
1620 HTMLFormControlElementWithState::defaultEventHandler(event);
1621 return;
1624 if (usesMenuList())
1625 menuListDefaultEventHandler(event);
1626 else
1627 listBoxDefaultEventHandler(event);
1628 if (event->defaultHandled())
1629 return;
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();
1636 return;
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())
1648 return i;
1650 return -1;
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())
1669 return String();
1670 return toHTMLOptionElement(element)->text();
1673 void HTMLSelectElement::typeAheadFind(KeyboardEvent* event)
1675 int index = m_typeAhead.handleEvent(event, TypeAhead::MatchPrefix | TypeAhead::CycleFirstChar);
1676 if (index < 0)
1677 return;
1678 selectOption(listToOptionIndex(index), DeselectOtherOptions | DispatchInputAndChangeEvent | UserDriven);
1679 if (!usesMenuList())
1680 listBoxOnChange();
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.
1688 recalcListItems();
1689 HTMLFormControlElementWithState::insertedInto(insertionPoint);
1690 return InsertionDone;
1693 void HTMLSelectElement::accessKeySetSelectedIndex(int index)
1695 // First bring into focus the list box.
1696 if (!focused())
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);
1707 else
1708 selectOption(index, DispatchInputAndChangeEvent | UserDriven);
1712 if (usesMenuList())
1713 dispatchInputAndChangeEventForMenuList();
1714 else
1715 listBoxOnChange();
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]))
1727 ++options;
1730 return options;
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
1744 remove(index);
1745 return true;
1747 setOption(index, value.get(), exceptionState);
1748 return true;
1751 bool HTMLSelectElement::isInteractiveContent() const
1753 return true;
1756 bool HTMLSelectElement::supportsAutofocus() const
1758 return true;
1761 void HTMLSelectElement::updateListOnLayoutObject()
1763 setOptionsChangedOnLayoutObject();
1766 DEFINE_TRACE(HTMLSelectElement)
1768 #if ENABLE(OILPAN)
1769 visitor->trace(m_listItems);
1770 #endif
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)
1782 recalcListItems();
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()))
1795 return nullptr;
1796 int focusedIndex = activeSelectionEndListIndex();
1797 if (focusedIndex < 0)
1798 focusedIndex = firstSelectableListIndex();
1799 if (focusedIndex < 0)
1800 return nullptr;
1801 HTMLElement* focused = listItems()[focusedIndex];
1802 return isHTMLOptionElement(focused) ? toHTMLOptionElement(focused) : nullptr;
1805 String HTMLSelectElement::itemText(const Element& element) const
1807 String itemString;
1808 if (isHTMLOptGroupElement(element))
1809 itemString = toHTMLOptGroupElement(element).groupLabelText();
1810 else if (isHTMLOptionElement(element))
1811 itemString = toHTMLOptionElement(element).textIndentedToRespectGroupLabel();
1813 if (layoutObject())
1814 applyTextTransform(layoutObject()->style(), itemString, ' ');
1815 return 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;
1824 return false;
1827 const ComputedStyle* HTMLSelectElement::itemComputedStyle(Element& element) const
1829 return element.computedStyle() ? element.computedStyle() : element.ensureComputedStyle();
1832 IntRect HTMLSelectElement::elementRectRelativeToViewport() const
1834 if (!layoutObject())
1835 return IntRect();
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();
1845 return 0;
1848 LayoutUnit HTMLSelectElement::clientPaddingRight() const
1850 if (layoutObject() && layoutObject()->isMenuList())
1851 return toLayoutMenuList(layoutObject())->clientPaddingRight();
1852 return 0;
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;
1867 if (layoutObject())
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
1883 // up.
1884 Document& doc = document();
1885 if (&doc != doc.frame()->document())
1886 return;
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())
1906 return;
1907 if (document().frameHost()->chromeClient().hasOpenedPopup())
1908 return;
1909 if (!layoutObject() || !layoutObject()->isMenuList())
1910 return;
1912 if (!m_popup)
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()
1926 if (m_popup)
1927 m_popup->hide();
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);
1940 if (m_popup)
1941 m_popup->disconnectClient();
1942 m_popupIsVisible = false;
1943 m_popup = nullptr;
1946 void HTMLSelectElement::resetTypeAheadSessionForTesting()
1948 m_typeAhead.resetSession();
1951 } // namespace blink