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/)
10 *
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.
15 *
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.
20 *
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.
25 *
26 */
27
28#include "config.h"
29#include "HTMLSelectElement.h"
30
31#include "AXObjectCache.h"
32#include "DOMFormData.h"
33#include "ElementTraversal.h"
34#include "EventHandler.h"
35#include "EventNames.h"
36#include "FormController.h"
37#include "Frame.h"
38#include "GenericCachedHTMLCollection.h"
39#include "HTMLFormElement.h"
40#include "HTMLHRElement.h"
41#include "HTMLNames.h"
42#include "HTMLOptGroupElement.h"
43#include "HTMLOptionElement.h"
44#include "HTMLOptionsCollection.h"
45#include "HTMLParserIdioms.h"
46#include "KeyboardEvent.h"
47#include "LocalizedStrings.h"
48#include "MouseEvent.h"
49#include "NodeRareData.h"
50#include "Page.h"
51#include "PlatformMouseEvent.h"
52#include "RenderListBox.h"
53#include "RenderMenuList.h"
54#include "RenderTheme.h"
55#include "Settings.h"
56#include "SpatialNavigation.h"
57#include <wtf/IsoMallocInlines.h>
58#include <wtf/text/StringConcatenateNumbers.h>
59
60namespace WebCore {
61
62WTF_MAKE_ISO_ALLOCATED_IMPL(HTMLSelectElement);
63
64using namespace WTF::Unicode;
65
66using namespace HTMLNames;
67
68// Upper limit agreed upon with representatives of Opera and Mozilla.
69static const unsigned maxSelectItems = 10000;
70
71HTMLSelectElement::HTMLSelectElement(const QualifiedName& tagName, Document& document, HTMLFormElement* form)
72 : HTMLFormControlElementWithState(tagName, document, form)
73 , m_typeAhead(this)
74 , m_size(0)
75 , m_lastOnChangeIndex(-1)
76 , m_activeSelectionAnchorIndex(-1)
77 , m_activeSelectionEndIndex(-1)
78 , m_isProcessingUserDrivenChange(false)
79 , m_multiple(false)
80 , m_activeSelectionState(false)
81 , m_allowsNonContiguousSelection(false)
82 , m_shouldRecalcListItems(false)
83{
84 ASSERT(hasTagName(selectTag));
85}
86
87Ref<HTMLSelectElement> HTMLSelectElement::create(const QualifiedName& tagName, Document& document, HTMLFormElement* form)
88{
89 ASSERT(tagName.matches(selectTag));
90 return adoptRef(*new HTMLSelectElement(tagName, document, form));
91}
92
93void HTMLSelectElement::didRecalcStyle(Style::Change styleChange)
94{
95 // Even though the options didn't necessarily change, we will call setOptionsChangedOnRenderer for its side effect
96 // of recomputing the width of the element. We need to do that if the style change included a change in zoom level.
97 setOptionsChangedOnRenderer();
98 HTMLFormControlElement::didRecalcStyle(styleChange);
99}
100
101const AtomicString& HTMLSelectElement::formControlType() const
102{
103 static NeverDestroyed<const AtomicString> selectMultiple("select-multiple", AtomicString::ConstructFromLiteral);
104 static NeverDestroyed<const AtomicString> selectOne("select-one", AtomicString::ConstructFromLiteral);
105 return m_multiple ? selectMultiple : selectOne;
106}
107
108void HTMLSelectElement::deselectItems(HTMLOptionElement* excludeElement)
109{
110 deselectItemsWithoutValidation(excludeElement);
111 updateValidity();
112}
113
114void HTMLSelectElement::optionSelectedByUser(int optionIndex, bool fireOnChangeNow, bool allowMultipleSelection)
115{
116 // User interaction such as mousedown events can cause list box select elements to send change events.
117 // This produces that same behavior for changes triggered by other code running on behalf of the user.
118 if (!usesMenuList()) {
119 updateSelectedState(optionToListIndex(optionIndex), allowMultipleSelection, false);
120 updateValidity();
121 if (auto* renderer = this->renderer())
122 renderer->updateFromElement();
123 if (fireOnChangeNow)
124 listBoxOnChange();
125 return;
126 }
127
128 // Bail out if this index is already the selected one, to avoid running unnecessary JavaScript that can mess up
129 // autofill when there is no actual change (see https://bugs.webkit.org/show_bug.cgi?id=35256 and <rdar://7467917>).
130 // The selectOption function does not behave this way, possibly because other callers need a change event even
131 // in cases where the selected option is not change.
132 if (optionIndex == selectedIndex())
133 return;
134
135 selectOption(optionIndex, DeselectOtherOptions | (fireOnChangeNow ? DispatchChangeEvent : 0) | UserDriven);
136}
137
138bool HTMLSelectElement::hasPlaceholderLabelOption() const
139{
140 // The select element has no placeholder label option if it has an attribute "multiple" specified or a display size of non-1.
141 //
142 // The condition "size() > 1" is not compliant with the HTML5 spec as of Dec 3, 2010. "size() != 1" is correct.
143 // Using "size() > 1" here because size() may be 0 in WebKit.
144 // See the discussion at https://bugs.webkit.org/show_bug.cgi?id=43887
145 //
146 // "0 size()" happens when an attribute "size" is absent or an invalid size attribute is specified.
147 // In this case, the display size should be assumed as the default.
148 // The default display size is 1 for non-multiple select elements, and 4 for multiple select elements.
149 //
150 // Finally, if size() == 0 and non-multiple, the display size can be assumed as 1.
151 if (multiple() || size() > 1)
152 return false;
153
154 int listIndex = optionToListIndex(0);
155 ASSERT(listIndex >= 0);
156 if (listIndex < 0)
157 return false;
158 HTMLOptionElement& option = downcast<HTMLOptionElement>(*listItems()[listIndex]);
159 return !listIndex && option.value().isEmpty();
160}
161
162String HTMLSelectElement::validationMessage() const
163{
164 if (!willValidate())
165 return String();
166
167 if (customError())
168 return customValidationMessage();
169
170 return valueMissing() ? validationMessageValueMissingForSelectText() : String();
171}
172
173bool HTMLSelectElement::valueMissing() const
174{
175 if (!willValidate())
176 return false;
177
178 if (!isRequired())
179 return false;
180
181 int firstSelectionIndex = selectedIndex();
182
183 // If a non-placeholer label option is selected (firstSelectionIndex > 0), it's not value-missing.
184 return firstSelectionIndex < 0 || (!firstSelectionIndex && hasPlaceholderLabelOption());
185}
186
187void HTMLSelectElement::listBoxSelectItem(int listIndex, bool allowMultiplySelections, bool shift, bool fireOnChangeNow)
188{
189 if (!multiple())
190 optionSelectedByUser(listToOptionIndex(listIndex), fireOnChangeNow, false);
191 else {
192 updateSelectedState(listIndex, allowMultiplySelections, shift);
193 updateValidity();
194 if (fireOnChangeNow)
195 listBoxOnChange();
196 }
197}
198
199bool HTMLSelectElement::usesMenuList() const
200{
201#if !PLATFORM(IOS_FAMILY)
202 if (RenderTheme::singleton().delegatesMenuListRendering())
203 return true;
204
205 return !m_multiple && m_size <= 1;
206#else
207 return !m_multiple;
208#endif
209}
210
211int HTMLSelectElement::activeSelectionStartListIndex() const
212{
213 if (m_activeSelectionAnchorIndex >= 0)
214 return m_activeSelectionAnchorIndex;
215 return optionToListIndex(selectedIndex());
216}
217
218int HTMLSelectElement::activeSelectionEndListIndex() const
219{
220 if (m_activeSelectionEndIndex >= 0)
221 return m_activeSelectionEndIndex;
222 return lastSelectedListIndex();
223}
224
225ExceptionOr<void> HTMLSelectElement::add(const OptionOrOptGroupElement& element, const Optional<HTMLElementOrInt>& before)
226{
227 RefPtr<HTMLElement> beforeElement;
228 if (before) {
229 beforeElement = WTF::switchOn(before.value(),
230 [](const RefPtr<HTMLElement>& element) -> HTMLElement* { return element.get(); },
231 [this](int index) -> HTMLElement* { return item(index); }
232 );
233 }
234 HTMLElement& toInsert = WTF::switchOn(element,
235 [](const auto& htmlElement) -> HTMLElement& { return *htmlElement; }
236 );
237
238
239 return insertBefore(toInsert, beforeElement.get());
240}
241
242void HTMLSelectElement::remove(int optionIndex)
243{
244 int listIndex = optionToListIndex(optionIndex);
245 if (listIndex < 0)
246 return;
247
248 listItems()[listIndex]->remove();
249}
250
251String HTMLSelectElement::value() const
252{
253 for (auto* item : listItems()) {
254 if (is<HTMLOptionElement>(*item)) {
255 HTMLOptionElement& option = downcast<HTMLOptionElement>(*item);
256 if (option.selected())
257 return option.value();
258 }
259 }
260 return emptyString();
261}
262
263void HTMLSelectElement::setValue(const String& value)
264{
265 // Find the option with value() matching the given parameter and make it the current selection.
266 unsigned optionIndex = 0;
267 for (auto* item : listItems()) {
268 if (is<HTMLOptionElement>(*item)) {
269 if (downcast<HTMLOptionElement>(*item).value() == value) {
270 setSelectedIndex(optionIndex);
271 return;
272 }
273 ++optionIndex;
274 }
275 }
276
277 setSelectedIndex(-1);
278}
279
280bool HTMLSelectElement::isPresentationAttribute(const QualifiedName& name) const
281{
282 if (name == alignAttr) {
283 // Don't map 'align' attribute. This matches what Firefox, Opera and IE do.
284 // See http://bugs.webkit.org/show_bug.cgi?id=12072
285 return false;
286 }
287
288 return HTMLFormControlElementWithState::isPresentationAttribute(name);
289}
290
291void HTMLSelectElement::parseAttribute(const QualifiedName& name, const AtomicString& value)
292{
293 if (name == sizeAttr) {
294 unsigned oldSize = m_size;
295 unsigned size = limitToOnlyHTMLNonNegative(value);
296
297 // Ensure that we've determined selectedness of the items at least once prior to changing the size.
298 if (oldSize != size)
299 updateListItemSelectedStates();
300
301 m_size = size;
302 updateValidity();
303 if (m_size != oldSize) {
304 invalidateStyleAndRenderersForSubtree();
305 setRecalcListItems();
306 updateValidity();
307 }
308 } else if (name == multipleAttr)
309 parseMultipleAttribute(value);
310 else
311 HTMLFormControlElementWithState::parseAttribute(name, value);
312}
313
314bool HTMLSelectElement::isKeyboardFocusable(KeyboardEvent* event) const
315{
316 if (renderer())
317 return isFocusable();
318 return HTMLFormControlElementWithState::isKeyboardFocusable(event);
319}
320
321bool HTMLSelectElement::isMouseFocusable() const
322{
323 if (renderer())
324 return isFocusable();
325 return HTMLFormControlElementWithState::isMouseFocusable();
326}
327
328bool HTMLSelectElement::canSelectAll() const
329{
330 return !usesMenuList();
331}
332
333RenderPtr<RenderElement> HTMLSelectElement::createElementRenderer(RenderStyle&& style, const RenderTreePosition&)
334{
335#if !PLATFORM(IOS_FAMILY)
336 if (usesMenuList())
337 return createRenderer<RenderMenuList>(*this, WTFMove(style));
338 return createRenderer<RenderListBox>(*this, WTFMove(style));
339#else
340 return createRenderer<RenderMenuList>(*this, WTFMove(style));
341#endif
342}
343
344bool HTMLSelectElement::childShouldCreateRenderer(const Node& child) const
345{
346 if (!HTMLFormControlElementWithState::childShouldCreateRenderer(child))
347 return false;
348#if !PLATFORM(IOS_FAMILY)
349 if (!usesMenuList())
350 return is<HTMLOptionElement>(child) || is<HTMLOptGroupElement>(child) || validationMessageShadowTreeContains(child);
351#endif
352 return validationMessageShadowTreeContains(child);
353}
354
355Ref<HTMLCollection> HTMLSelectElement::selectedOptions()
356{
357 return ensureRareData().ensureNodeLists().addCachedCollection<GenericCachedHTMLCollection<CollectionTypeTraits<SelectedOptions>::traversalType>>(*this, SelectedOptions);
358}
359
360Ref<HTMLOptionsCollection> HTMLSelectElement::options()
361{
362 return ensureRareData().ensureNodeLists().addCachedCollection<HTMLOptionsCollection>(*this, SelectOptions);
363}
364
365void HTMLSelectElement::updateListItemSelectedStates()
366{
367 if (m_shouldRecalcListItems)
368 recalcListItems();
369}
370
371void HTMLSelectElement::childrenChanged(const ChildChange& change)
372{
373 setRecalcListItems();
374 updateValidity();
375 m_lastOnChangeSelection.clear();
376
377 HTMLFormControlElementWithState::childrenChanged(change);
378}
379
380void HTMLSelectElement::optionElementChildrenChanged()
381{
382 setRecalcListItems();
383 updateValidity();
384 if (auto* cache = document().existingAXObjectCache())
385 cache->childrenChanged(this);
386}
387
388void HTMLSelectElement::accessKeyAction(bool sendMouseEvents)
389{
390 focus();
391 dispatchSimulatedClick(0, sendMouseEvents ? SendMouseUpDownEvents : SendNoEvents);
392}
393
394void HTMLSelectElement::setMultiple(bool multiple)
395{
396 bool oldMultiple = this->multiple();
397 int oldSelectedIndex = selectedIndex();
398 setAttributeWithoutSynchronization(multipleAttr, multiple ? emptyAtom() : nullAtom());
399
400 // Restore selectedIndex after changing the multiple flag to preserve
401 // selection as single-line and multi-line has different defaults.
402 if (oldMultiple != this->multiple())
403 setSelectedIndex(oldSelectedIndex);
404}
405
406void HTMLSelectElement::setSize(unsigned size)
407{
408 setUnsignedIntegralAttribute(sizeAttr, limitToOnlyHTMLNonNegative(size));
409}
410
411HTMLOptionElement* HTMLSelectElement::namedItem(const AtomicString& name)
412{
413 return options()->namedItem(name);
414}
415
416HTMLOptionElement* HTMLSelectElement::item(unsigned index)
417{
418 return options()->item(index);
419}
420
421ExceptionOr<void> HTMLSelectElement::setItem(unsigned index, HTMLOptionElement* option)
422{
423 if (!option) {
424 remove(index);
425 return { };
426 }
427
428 if (index > maxSelectItems - 1)
429 index = maxSelectItems - 1;
430
431 int diff = index - length();
432
433 RefPtr<HTMLOptionElement> before;
434 // Out of array bounds? First insert empty dummies.
435 if (diff > 0) {
436 auto result = setLength(index);
437 if (result.hasException())
438 return result;
439 // Replace an existing entry?
440 } else if (diff < 0) {
441 before = item(index + 1);
442 remove(index);
443 }
444
445 // Finally add the new element.
446 auto result = add(option, HTMLElementOrInt { before.get() });
447 if (result.hasException())
448 return result;
449
450 if (diff >= 0 && option->selected())
451 optionSelectionStateChanged(*option, true);
452
453 return { };
454}
455
456ExceptionOr<void> HTMLSelectElement::setLength(unsigned newLength)
457{
458 if (newLength > length() && newLength > maxSelectItems) {
459 document().addConsoleMessage(MessageSource::Other, MessageLevel::Warning, makeString("Blocked attempt to expand the option list to ", newLength, " items. The maximum number of items allowed is ", maxSelectItems, '.'));
460 return { };
461 }
462
463 int diff = length() - newLength;
464
465 if (diff < 0) { // Add dummy elements.
466 do {
467 auto result = add(HTMLOptionElement::create(document()).ptr(), WTF::nullopt);
468 if (result.hasException())
469 return result;
470 } while (++diff);
471 } else {
472 auto& items = listItems();
473
474 // Removing children fires mutation events, which might mutate the DOM further, so we first copy out a list
475 // of elements that we intend to remove then attempt to remove them one at a time.
476 Vector<Ref<HTMLOptionElement>> itemsToRemove;
477 size_t optionIndex = 0;
478 for (auto& item : items) {
479 if (is<HTMLOptionElement>(*item) && optionIndex++ >= newLength) {
480 ASSERT(item->parentNode());
481 itemsToRemove.append(downcast<HTMLOptionElement>(*item));
482 }
483 }
484
485 // FIXME: Clients can detect what order we remove the options in; is it good to remove them in ascending order?
486 // FIXME: This ignores exceptions. A previous version passed through the exception only for the last item removed.
487 // What exception behavior do we want?
488 for (auto& item : itemsToRemove)
489 item->remove();
490 }
491 return { };
492}
493
494bool HTMLSelectElement::isRequiredFormControl() const
495{
496 return isRequired();
497}
498
499bool HTMLSelectElement::willRespondToMouseClickEvents()
500{
501#if PLATFORM(IOS_FAMILY)
502 return !isDisabledFormControl();
503#else
504 return HTMLFormControlElementWithState::willRespondToMouseClickEvents();
505#endif
506}
507
508// Returns the 1st valid item |skip| items from |listIndex| in direction |direction| if there is one.
509// Otherwise, it returns the valid item closest to that boundary which is past |listIndex| if there is one.
510// Otherwise, it returns |listIndex|.
511// Valid means that it is enabled and an option element.
512int HTMLSelectElement::nextValidIndex(int listIndex, SkipDirection direction, int skip) const
513{
514 ASSERT(direction == -1 || direction == 1);
515 auto& listItems = this->listItems();
516 int lastGoodIndex = listIndex;
517 int size = listItems.size();
518 for (listIndex += direction; listIndex >= 0 && listIndex < size; listIndex += direction) {
519 --skip;
520 if (!listItems[listIndex]->isDisabledFormControl() && is<HTMLOptionElement>(*listItems[listIndex])) {
521 lastGoodIndex = listIndex;
522 if (skip <= 0)
523 break;
524 }
525 }
526 return lastGoodIndex;
527}
528
529int HTMLSelectElement::nextSelectableListIndex(int startIndex) const
530{
531 return nextValidIndex(startIndex, SkipForwards, 1);
532}
533
534int HTMLSelectElement::previousSelectableListIndex(int startIndex) const
535{
536 if (startIndex == -1)
537 startIndex = listItems().size();
538 return nextValidIndex(startIndex, SkipBackwards, 1);
539}
540
541int HTMLSelectElement::firstSelectableListIndex() const
542{
543 auto& items = listItems();
544 int index = nextValidIndex(items.size(), SkipBackwards, INT_MAX);
545 if (static_cast<size_t>(index) == items.size())
546 return -1;
547 return index;
548}
549
550int HTMLSelectElement::lastSelectableListIndex() const
551{
552 return nextValidIndex(-1, SkipForwards, INT_MAX);
553}
554
555// Returns the index of the next valid item one page away from |startIndex| in direction |direction|.
556int HTMLSelectElement::nextSelectableListIndexPageAway(int startIndex, SkipDirection direction) const
557{
558 auto& items = listItems();
559
560 // Can't use m_size because renderer forces a minimum size.
561 int pageSize = 0;
562 auto* renderer = this->renderer();
563 if (is<RenderListBox>(*renderer))
564 pageSize = downcast<RenderListBox>(*renderer).size() - 1; // -1 so we still show context.
565
566 // One page away, but not outside valid bounds.
567 // If there is a valid option item one page away, the index is chosen.
568 // If there is no exact one page away valid option, returns startIndex or the most far index.
569 int edgeIndex = direction == SkipForwards ? 0 : items.size() - 1;
570 int skipAmount = pageSize + (direction == SkipForwards ? startIndex : edgeIndex - startIndex);
571 return nextValidIndex(edgeIndex, direction, skipAmount);
572}
573
574void HTMLSelectElement::selectAll()
575{
576 ASSERT(!usesMenuList());
577 if (!renderer() || !m_multiple)
578 return;
579
580 // Save the selection so it can be compared to the new selectAll selection
581 // when dispatching change events.
582 saveLastSelection();
583
584 m_activeSelectionState = true;
585 setActiveSelectionAnchorIndex(nextSelectableListIndex(-1));
586 setActiveSelectionEndIndex(previousSelectableListIndex(-1));
587 if (m_activeSelectionAnchorIndex < 0)
588 return;
589
590 updateListBoxSelection(false);
591 listBoxOnChange();
592 updateValidity();
593}
594
595void HTMLSelectElement::saveLastSelection()
596{
597 if (usesMenuList()) {
598 m_lastOnChangeIndex = selectedIndex();
599 return;
600 }
601
602 m_lastOnChangeSelection.clear();
603 for (auto& element : listItems())
604 m_lastOnChangeSelection.append(is<HTMLOptionElement>(*element) && downcast<HTMLOptionElement>(*element).selected());
605}
606
607void HTMLSelectElement::setActiveSelectionAnchorIndex(int index)
608{
609 m_activeSelectionAnchorIndex = index;
610
611 // Cache the selection state so we can restore the old selection as the new
612 // selection pivots around this anchor index.
613 m_cachedStateForActiveSelection.clear();
614
615 for (auto& element : listItems())
616 m_cachedStateForActiveSelection.append(is<HTMLOptionElement>(*element) && downcast<HTMLOptionElement>(*element).selected());
617}
618
619void HTMLSelectElement::setActiveSelectionEndIndex(int index)
620{
621 m_activeSelectionEndIndex = index;
622}
623
624void HTMLSelectElement::updateListBoxSelection(bool deselectOtherOptions)
625{
626 ASSERT(renderer());
627
628#if !PLATFORM(IOS_FAMILY)
629 ASSERT(renderer()->isListBox() || m_multiple);
630#else
631 ASSERT(renderer()->isMenuList() || m_multiple);
632#endif
633
634 ASSERT(!listItems().size() || m_activeSelectionAnchorIndex >= 0);
635
636 unsigned start = std::min(m_activeSelectionAnchorIndex, m_activeSelectionEndIndex);
637 unsigned end = std::max(m_activeSelectionAnchorIndex, m_activeSelectionEndIndex);
638
639 auto& items = listItems();
640 for (unsigned i = 0; i < items.size(); ++i) {
641 auto& element = *items[i];
642 if (!is<HTMLOptionElement>(element) || downcast<HTMLOptionElement>(element).isDisabledFormControl())
643 continue;
644
645 if (i >= start && i <= end)
646 downcast<HTMLOptionElement>(element).setSelectedState(m_activeSelectionState);
647 else if (deselectOtherOptions || i >= m_cachedStateForActiveSelection.size())
648 downcast<HTMLOptionElement>(element).setSelectedState(false);
649 else
650 downcast<HTMLOptionElement>(element).setSelectedState(m_cachedStateForActiveSelection[i]);
651 }
652
653 scrollToSelection();
654 updateValidity();
655}
656
657void HTMLSelectElement::listBoxOnChange()
658{
659 ASSERT(!usesMenuList() || m_multiple);
660
661 auto& items = listItems();
662
663 // If the cached selection list is empty, or the size has changed, then fire
664 // dispatchFormControlChangeEvent, and return early.
665 if (m_lastOnChangeSelection.isEmpty() || m_lastOnChangeSelection.size() != items.size()) {
666 dispatchFormControlChangeEvent();
667 return;
668 }
669
670 // Update m_lastOnChangeSelection and fire dispatchFormControlChangeEvent.
671 bool fireOnChange = false;
672 for (unsigned i = 0; i < items.size(); ++i) {
673 auto& element = *items[i];
674 bool selected = is<HTMLOptionElement>(element) && downcast<HTMLOptionElement>(element).selected();
675 if (selected != m_lastOnChangeSelection[i])
676 fireOnChange = true;
677 m_lastOnChangeSelection[i] = selected;
678 }
679
680 if (fireOnChange) {
681 dispatchInputEvent();
682 dispatchFormControlChangeEvent();
683 }
684}
685
686void HTMLSelectElement::dispatchChangeEventForMenuList()
687{
688 ASSERT(usesMenuList());
689
690 int selected = selectedIndex();
691 if (m_lastOnChangeIndex != selected && m_isProcessingUserDrivenChange) {
692 m_lastOnChangeIndex = selected;
693 m_isProcessingUserDrivenChange = false;
694 dispatchInputEvent();
695 dispatchFormControlChangeEvent();
696 }
697}
698
699void HTMLSelectElement::scrollToSelection()
700{
701#if !PLATFORM(IOS_FAMILY)
702 if (usesMenuList())
703 return;
704
705 auto* renderer = this->renderer();
706 if (!is<RenderListBox>(renderer))
707 return;
708 downcast<RenderListBox>(*renderer).selectionChanged();
709#else
710 if (auto* renderer = this->renderer())
711 renderer->repaint();
712#endif
713}
714
715void HTMLSelectElement::setOptionsChangedOnRenderer()
716{
717 if (auto* renderer = this->renderer()) {
718#if !PLATFORM(IOS_FAMILY)
719 if (is<RenderMenuList>(*renderer))
720 downcast<RenderMenuList>(*renderer).setOptionsChanged(true);
721 else
722 downcast<RenderListBox>(*renderer).setOptionsChanged(true);
723#else
724 downcast<RenderMenuList>(*renderer).setOptionsChanged(true);
725#endif
726 }
727}
728
729const Vector<HTMLElement*>& HTMLSelectElement::listItems() const
730{
731 if (m_shouldRecalcListItems)
732 recalcListItems();
733 else {
734#if !ASSERT_DISABLED
735 Vector<HTMLElement*> items = m_listItems;
736 recalcListItems(false);
737 ASSERT(items == m_listItems);
738#endif
739 }
740
741 return m_listItems;
742}
743
744void HTMLSelectElement::invalidateSelectedItems()
745{
746 if (HTMLCollection* collection = cachedHTMLCollection(SelectedOptions))
747 collection->invalidateCache();
748}
749
750void HTMLSelectElement::setRecalcListItems()
751{
752 m_shouldRecalcListItems = true;
753 // Manual selection anchor is reset when manipulating the select programmatically.
754 m_activeSelectionAnchorIndex = -1;
755 setOptionsChangedOnRenderer();
756 invalidateStyleForSubtree();
757 if (!isConnected()) {
758 if (HTMLCollection* collection = cachedHTMLCollection(SelectOptions))
759 collection->invalidateCache();
760 }
761 if (!isConnected())
762 invalidateSelectedItems();
763 if (auto* cache = document().existingAXObjectCache())
764 cache->childrenChanged(this);
765}
766
767void HTMLSelectElement::recalcListItems(bool updateSelectedStates) const
768{
769 m_listItems.clear();
770
771 m_shouldRecalcListItems = false;
772
773 RefPtr<HTMLOptionElement> foundSelected;
774 RefPtr<HTMLOptionElement> firstOption;
775 for (RefPtr<Element> currentElement = ElementTraversal::firstWithin(*this); currentElement; ) {
776 if (!is<HTMLElement>(*currentElement)) {
777 currentElement = ElementTraversal::nextSkippingChildren(*currentElement, this);
778 continue;
779 }
780 HTMLElement& current = downcast<HTMLElement>(*currentElement);
781
782 // Only consider optgroup elements that are direct children of the select element.
783 if (is<HTMLOptGroupElement>(current) && current.parentNode() == this) {
784 m_listItems.append(&current);
785 if (RefPtr<Element> nextElement = ElementTraversal::firstWithin(current)) {
786 currentElement = nextElement;
787 continue;
788 }
789 }
790
791 if (is<HTMLOptionElement>(current)) {
792 m_listItems.append(&current);
793
794 if (updateSelectedStates && !m_multiple) {
795 HTMLOptionElement& option = downcast<HTMLOptionElement>(current);
796 if (!firstOption)
797 firstOption = &option;
798 if (option.selected()) {
799 if (foundSelected)
800 foundSelected->setSelectedState(false);
801 foundSelected = &option;
802 } else if (m_size <= 1 && !foundSelected && !option.isDisabledFormControl()) {
803 foundSelected = &option;
804 foundSelected->setSelectedState(true);
805 }
806 }
807 }
808
809 if (current.hasTagName(hrTag))
810 m_listItems.append(&current);
811
812 // In conforming HTML code, only <optgroup> and <option> will be found
813 // within a <select>. We call NodeTraversal::nextSkippingChildren so that we only step
814 // into those tags that we choose to. For web-compat, we should cope
815 // with the case where odd tags like a <div> have been added but we
816 // handle this because such tags have already been removed from the
817 // <select>'s subtree at this point.
818 currentElement = ElementTraversal::nextSkippingChildren(*currentElement, this);
819 }
820
821 if (!foundSelected && m_size <= 1 && firstOption && !firstOption->selected())
822 firstOption->setSelectedState(true);
823}
824
825int HTMLSelectElement::selectedIndex() const
826{
827 unsigned index = 0;
828
829 // Return the number of the first option selected.
830 for (auto& element : listItems()) {
831 if (is<HTMLOptionElement>(*element)) {
832 if (downcast<HTMLOptionElement>(*element).selected())
833 return index;
834 ++index;
835 }
836 }
837
838 return -1;
839}
840
841void HTMLSelectElement::setSelectedIndex(int index)
842{
843 selectOption(index, DeselectOtherOptions);
844}
845
846void HTMLSelectElement::optionSelectionStateChanged(HTMLOptionElement& option, bool optionIsSelected)
847{
848 ASSERT(option.ownerSelectElement() == this);
849 if (optionIsSelected)
850 selectOption(option.index());
851 else if (!usesMenuList())
852 selectOption(-1);
853 else
854 selectOption(nextSelectableListIndex(-1));
855}
856
857void HTMLSelectElement::selectOption(int optionIndex, SelectOptionFlags flags)
858{
859 bool shouldDeselect = !m_multiple || (flags & DeselectOtherOptions);
860
861 auto& items = listItems();
862 int listIndex = optionToListIndex(optionIndex);
863
864 RefPtr<HTMLElement> element;
865 if (listIndex >= 0)
866 element = items[listIndex];
867
868 if (shouldDeselect)
869 deselectItemsWithoutValidation(element.get());
870
871 if (is<HTMLOptionElement>(element)) {
872 if (m_activeSelectionAnchorIndex < 0 || shouldDeselect)
873 setActiveSelectionAnchorIndex(listIndex);
874 if (m_activeSelectionEndIndex < 0 || shouldDeselect)
875 setActiveSelectionEndIndex(listIndex);
876 downcast<HTMLOptionElement>(*element).setSelectedState(true);
877 }
878
879 updateValidity();
880
881 // For the menu list case, this is what makes the selected element appear.
882 if (auto* renderer = this->renderer())
883 renderer->updateFromElement();
884
885 scrollToSelection();
886
887 if (usesMenuList()) {
888 m_isProcessingUserDrivenChange = flags & UserDriven;
889 if (flags & DispatchChangeEvent)
890 dispatchChangeEventForMenuList();
891 if (auto* renderer = this->renderer()) {
892 if (is<RenderMenuList>(*renderer))
893 downcast<RenderMenuList>(*renderer).didSetSelectedIndex(listIndex);
894 else
895 downcast<RenderListBox>(*renderer).selectionChanged();
896 }
897 }
898}
899
900int HTMLSelectElement::optionToListIndex(int optionIndex) const
901{
902 auto& items = listItems();
903 int listSize = static_cast<int>(items.size());
904 if (optionIndex < 0 || optionIndex >= listSize)
905 return -1;
906
907 int optionIndex2 = -1;
908 for (int listIndex = 0; listIndex < listSize; ++listIndex) {
909 if (is<HTMLOptionElement>(*items[listIndex])) {
910 ++optionIndex2;
911 if (optionIndex2 == optionIndex)
912 return listIndex;
913 }
914 }
915
916 return -1;
917}
918
919int HTMLSelectElement::listToOptionIndex(int listIndex) const
920{
921 auto& items = listItems();
922 if (listIndex < 0 || listIndex >= static_cast<int>(items.size()) || !is<HTMLOptionElement>(*items[listIndex]))
923 return -1;
924
925 // Actual index of option not counting OPTGROUP entries that may be in list.
926 int optionIndex = 0;
927 for (int i = 0; i < listIndex; ++i) {
928 if (is<HTMLOptionElement>(*items[i]))
929 ++optionIndex;
930 }
931
932 return optionIndex;
933}
934
935void HTMLSelectElement::dispatchFocusEvent(RefPtr<Element>&& oldFocusedElement, FocusDirection direction)
936{
937 // Save the selection so it can be compared to the new selection when
938 // dispatching change events during blur event dispatch.
939 if (usesMenuList())
940 saveLastSelection();
941 HTMLFormControlElementWithState::dispatchFocusEvent(WTFMove(oldFocusedElement), direction);
942}
943
944void HTMLSelectElement::dispatchBlurEvent(RefPtr<Element>&& newFocusedElement)
945{
946 // We only need to fire change events here for menu lists, because we fire
947 // change events for list boxes whenever the selection change is actually made.
948 // This matches other browsers' behavior.
949 if (usesMenuList())
950 dispatchChangeEventForMenuList();
951 HTMLFormControlElementWithState::dispatchBlurEvent(WTFMove(newFocusedElement));
952}
953
954void HTMLSelectElement::deselectItemsWithoutValidation(HTMLElement* excludeElement)
955{
956 for (auto& element : listItems()) {
957 if (element != excludeElement && is<HTMLOptionElement>(*element))
958 downcast<HTMLOptionElement>(*element).setSelectedState(false);
959 }
960}
961
962FormControlState HTMLSelectElement::saveFormControlState() const
963{
964 FormControlState state;
965 auto& items = listItems();
966 state.reserveInitialCapacity(items.size());
967 for (auto& element : items) {
968 if (!is<HTMLOptionElement>(*element))
969 continue;
970 auto& option = downcast<HTMLOptionElement>(*element);
971 if (!option.selected())
972 continue;
973 state.uncheckedAppend(option.value());
974 if (!multiple())
975 break;
976 }
977 return state;
978}
979
980size_t HTMLSelectElement::searchOptionsForValue(const String& value, size_t listIndexStart, size_t listIndexEnd) const
981{
982 auto& items = listItems();
983 size_t loopEndIndex = std::min(items.size(), listIndexEnd);
984 for (size_t i = listIndexStart; i < loopEndIndex; ++i) {
985 if (!is<HTMLOptionElement>(*items[i]))
986 continue;
987 if (downcast<HTMLOptionElement>(*items[i]).value() == value)
988 return i;
989 }
990 return notFound;
991}
992
993void HTMLSelectElement::restoreFormControlState(const FormControlState& state)
994{
995 recalcListItems();
996
997 auto& items = listItems();
998 size_t itemsSize = items.size();
999 if (!itemsSize)
1000 return;
1001
1002 for (auto& element : items) {
1003 if (!is<HTMLOptionElement>(*element))
1004 continue;
1005 downcast<HTMLOptionElement>(*element).setSelectedState(false);
1006 }
1007
1008 if (!multiple()) {
1009 size_t foundIndex = searchOptionsForValue(state[0], 0, itemsSize);
1010 if (foundIndex != notFound)
1011 downcast<HTMLOptionElement>(*items[foundIndex]).setSelectedState(true);
1012 } else {
1013 size_t startIndex = 0;
1014 for (auto& value : state) {
1015 size_t foundIndex = searchOptionsForValue(value, startIndex, itemsSize);
1016 if (foundIndex == notFound)
1017 foundIndex = searchOptionsForValue(value, 0, startIndex);
1018 if (foundIndex == notFound)
1019 continue;
1020 downcast<HTMLOptionElement>(*items[foundIndex]).setSelectedState(true);
1021 startIndex = foundIndex + 1;
1022 }
1023 }
1024
1025 setOptionsChangedOnRenderer();
1026 updateValidity();
1027}
1028
1029void HTMLSelectElement::parseMultipleAttribute(const AtomicString& value)
1030{
1031 bool oldUsesMenuList = usesMenuList();
1032 m_multiple = !value.isNull();
1033 updateValidity();
1034 if (oldUsesMenuList != usesMenuList())
1035 invalidateStyleAndRenderersForSubtree();
1036}
1037
1038bool HTMLSelectElement::appendFormData(DOMFormData& formData, bool)
1039{
1040 const AtomicString& name = this->name();
1041 if (name.isEmpty())
1042 return false;
1043
1044 bool successful = false;
1045 for (auto& element : listItems()) {
1046 if (is<HTMLOptionElement>(*element) && downcast<HTMLOptionElement>(*element).selected() && !downcast<HTMLOptionElement>(*element).isDisabledFormControl()) {
1047 formData.append(name, downcast<HTMLOptionElement>(*element).value());
1048 successful = true;
1049 }
1050 }
1051
1052 // It's possible that this is a menulist with multiple options and nothing
1053 // will be submitted (!successful). We won't send a unselected non-disabled
1054 // option as fallback. This behavior matches to other browsers.
1055 return successful;
1056}
1057
1058void HTMLSelectElement::reset()
1059{
1060 RefPtr<HTMLOptionElement> firstOption;
1061 RefPtr<HTMLOptionElement> selectedOption;
1062
1063 for (auto& element : listItems()) {
1064 if (!is<HTMLOptionElement>(*element))
1065 continue;
1066
1067 HTMLOptionElement& option = downcast<HTMLOptionElement>(*element);
1068 if (option.hasAttributeWithoutSynchronization(selectedAttr)) {
1069 if (selectedOption && !m_multiple)
1070 selectedOption->setSelectedState(false);
1071 option.setSelectedState(true);
1072 selectedOption = &option;
1073 } else
1074 option.setSelectedState(false);
1075
1076 if (!firstOption)
1077 firstOption = &option;
1078 }
1079
1080 if (!selectedOption && firstOption && !m_multiple && m_size <= 1)
1081 firstOption->setSelectedState(true);
1082
1083 setOptionsChangedOnRenderer();
1084 invalidateStyleForSubtree();
1085 updateValidity();
1086}
1087
1088#if !PLATFORM(WIN)
1089
1090bool HTMLSelectElement::platformHandleKeydownEvent(KeyboardEvent* event)
1091{
1092 if (!RenderTheme::singleton().popsMenuByArrowKeys())
1093 return false;
1094
1095 if (!isSpatialNavigationEnabled(document().frame())) {
1096 if (event->keyIdentifier() == "Down" || event->keyIdentifier() == "Up") {
1097 focus();
1098 // Calling focus() may cause us to lose our renderer. Return true so
1099 // that our caller doesn't process the event further, but don't set
1100 // the event as handled.
1101 auto* renderer = this->renderer();
1102 if (!is<RenderMenuList>(renderer))
1103 return true;
1104
1105 // Save the selection so it can be compared to the new selection
1106 // when dispatching change events during selectOption, which
1107 // gets called from RenderMenuList::valueChanged, which gets called
1108 // after the user makes a selection from the menu.
1109 saveLastSelection();
1110 downcast<RenderMenuList>(*renderer).showPopup();
1111 event->setDefaultHandled();
1112 }
1113 return true;
1114 }
1115
1116 return false;
1117}
1118
1119#endif
1120
1121void HTMLSelectElement::menuListDefaultEventHandler(Event& event)
1122{
1123 ASSERT(renderer());
1124 ASSERT(renderer()->isMenuList());
1125
1126 if (event.type() == eventNames().keydownEvent) {
1127 if (!is<KeyboardEvent>(event))
1128 return;
1129
1130 KeyboardEvent& keyboardEvent = downcast<KeyboardEvent>(event);
1131 if (platformHandleKeydownEvent(&keyboardEvent))
1132 return;
1133
1134 // When using spatial navigation, we want to be able to navigate away
1135 // from the select element when the user hits any of the arrow keys,
1136 // instead of changing the selection.
1137 if (isSpatialNavigationEnabled(document().frame())) {
1138 if (!m_activeSelectionState)
1139 return;
1140 }
1141
1142 const String& keyIdentifier = keyboardEvent.keyIdentifier();
1143 bool handled = true;
1144 auto& listItems = this->listItems();
1145 int listIndex = optionToListIndex(selectedIndex());
1146
1147 // When using caret browsing, we want to be able to move the focus
1148 // out of the select element when user hits a left or right arrow key.
1149 if (document().settings().caretBrowsingEnabled()) {
1150 if (keyIdentifier == "Left" || keyIdentifier == "Right")
1151 return;
1152 }
1153
1154 if (keyIdentifier == "Down" || keyIdentifier == "Right")
1155 listIndex = nextValidIndex(listIndex, SkipForwards, 1);
1156 else if (keyIdentifier == "Up" || keyIdentifier == "Left")
1157 listIndex = nextValidIndex(listIndex, SkipBackwards, 1);
1158 else if (keyIdentifier == "PageDown")
1159 listIndex = nextValidIndex(listIndex, SkipForwards, 3);
1160 else if (keyIdentifier == "PageUp")
1161 listIndex = nextValidIndex(listIndex, SkipBackwards, 3);
1162 else if (keyIdentifier == "Home")
1163 listIndex = nextValidIndex(-1, SkipForwards, 1);
1164 else if (keyIdentifier == "End")
1165 listIndex = nextValidIndex(listItems.size(), SkipBackwards, 1);
1166 else
1167 handled = false;
1168
1169 if (handled && static_cast<size_t>(listIndex) < listItems.size())
1170 selectOption(listToOptionIndex(listIndex), DeselectOtherOptions | DispatchChangeEvent | UserDriven);
1171
1172 if (handled)
1173 keyboardEvent.setDefaultHandled();
1174 }
1175
1176 // Use key press event here since sending simulated mouse events
1177 // on key down blocks the proper sending of the key press event.
1178 if (event.type() == eventNames().keypressEvent) {
1179 if (!is<KeyboardEvent>(event))
1180 return;
1181
1182 KeyboardEvent& keyboardEvent = downcast<KeyboardEvent>(event);
1183 int keyCode = keyboardEvent.keyCode();
1184 bool handled = false;
1185
1186 if (keyCode == ' ' && isSpatialNavigationEnabled(document().frame())) {
1187 // Use space to toggle arrow key handling for selection change or spatial navigation.
1188 m_activeSelectionState = !m_activeSelectionState;
1189 keyboardEvent.setDefaultHandled();
1190 return;
1191 }
1192
1193 if (RenderTheme::singleton().popsMenuBySpaceOrReturn()) {
1194 if (keyCode == ' ' || keyCode == '\r') {
1195 focus();
1196
1197 // Calling focus() may remove the renderer or change the renderer type.
1198 auto* renderer = this->renderer();
1199 if (!is<RenderMenuList>(renderer))
1200 return;
1201
1202 // Save the selection so it can be compared to the new selection
1203 // when dispatching change events during selectOption, which
1204 // gets called from RenderMenuList::valueChanged, which gets called
1205 // after the user makes a selection from the menu.
1206 saveLastSelection();
1207 downcast<RenderMenuList>(*renderer).showPopup();
1208 handled = true;
1209 }
1210 } else if (RenderTheme::singleton().popsMenuByArrowKeys()) {
1211 if (keyCode == ' ') {
1212 focus();
1213
1214 // Calling focus() may remove the renderer or change the renderer type.
1215 auto* renderer = this->renderer();
1216 if (!is<RenderMenuList>(renderer))
1217 return;
1218
1219 // Save the selection so it can be compared to the new selection
1220 // when dispatching change events during selectOption, which
1221 // gets called from RenderMenuList::valueChanged, which gets called
1222 // after the user makes a selection from the menu.
1223 saveLastSelection();
1224 downcast<RenderMenuList>(*renderer).showPopup();
1225 handled = true;
1226 } else if (keyCode == '\r') {
1227 if (form())
1228 form()->submitImplicitly(keyboardEvent, false);
1229 dispatchChangeEventForMenuList();
1230 handled = true;
1231 }
1232 }
1233
1234 if (handled)
1235 keyboardEvent.setDefaultHandled();
1236 }
1237
1238 if (event.type() == eventNames().mousedownEvent && is<MouseEvent>(event) && downcast<MouseEvent>(event).button() == LeftButton) {
1239 focus();
1240#if !PLATFORM(IOS_FAMILY)
1241 auto* renderer = this->renderer();
1242 if (is<RenderMenuList>(renderer)) {
1243 auto& menuList = downcast<RenderMenuList>(*renderer);
1244 ASSERT(!menuList.popupIsVisible());
1245 // Save the selection so it can be compared to the new
1246 // selection when we call onChange during selectOption,
1247 // which gets called from RenderMenuList::valueChanged,
1248 // which gets called after the user makes a selection from
1249 // the menu.
1250 saveLastSelection();
1251 menuList.showPopup();
1252 }
1253#endif
1254 event.setDefaultHandled();
1255 }
1256
1257#if !PLATFORM(IOS_FAMILY)
1258 if (event.type() == eventNames().blurEvent && !focused()) {
1259 auto& menuList = downcast<RenderMenuList>(*renderer());
1260 if (menuList.popupIsVisible())
1261 menuList.hidePopup();
1262 }
1263#endif
1264}
1265
1266void HTMLSelectElement::updateSelectedState(int listIndex, bool multi, bool shift)
1267{
1268 auto& items = listItems();
1269 int listSize = static_cast<int>(items.size());
1270 if (listIndex < 0 || listIndex >= listSize)
1271 return;
1272
1273 // Save the selection so it can be compared to the new selection when
1274 // dispatching change events during mouseup, or after autoscroll finishes.
1275 saveLastSelection();
1276
1277 m_activeSelectionState = true;
1278
1279 bool shiftSelect = m_multiple && shift;
1280 bool multiSelect = m_multiple && multi && !shift;
1281
1282 auto& clickedElement = *items[listIndex];
1283 if (is<HTMLOptionElement>(clickedElement)) {
1284 // Keep track of whether an active selection (like during drag
1285 // selection), should select or deselect.
1286 if (downcast<HTMLOptionElement>(clickedElement).selected() && multiSelect)
1287 m_activeSelectionState = false;
1288 if (!m_activeSelectionState)
1289 downcast<HTMLOptionElement>(clickedElement).setSelectedState(false);
1290 }
1291
1292 // If we're not in any special multiple selection mode, then deselect all
1293 // other items, excluding the clicked option. If no option was clicked, then
1294 // this will deselect all items in the list.
1295 if (!shiftSelect && !multiSelect)
1296 deselectItemsWithoutValidation(&clickedElement);
1297
1298 // If the anchor hasn't been set, and we're doing a single selection or a
1299 // shift selection, then initialize the anchor to the first selected index.
1300 if (m_activeSelectionAnchorIndex < 0 && !multiSelect)
1301 setActiveSelectionAnchorIndex(selectedIndex());
1302
1303 // Set the selection state of the clicked option.
1304 if (is<HTMLOptionElement>(clickedElement) && !downcast<HTMLOptionElement>(clickedElement).isDisabledFormControl())
1305 downcast<HTMLOptionElement>(clickedElement).setSelectedState(true);
1306
1307 // If there was no selectedIndex() for the previous initialization, or If
1308 // we're doing a single selection, or a multiple selection (using cmd or
1309 // ctrl), then initialize the anchor index to the listIndex that just got
1310 // clicked.
1311 if (m_activeSelectionAnchorIndex < 0 || !shiftSelect)
1312 setActiveSelectionAnchorIndex(listIndex);
1313
1314 setActiveSelectionEndIndex(listIndex);
1315 updateListBoxSelection(!multiSelect);
1316}
1317
1318void HTMLSelectElement::listBoxDefaultEventHandler(Event& event)
1319{
1320 auto& listItems = this->listItems();
1321
1322 if (event.type() == eventNames().mousedownEvent && is<MouseEvent>(event) && downcast<MouseEvent>(event).button() == LeftButton) {
1323 focus();
1324
1325 // Calling focus() may remove or change our renderer, in which case we don't want to handle the event further.
1326 auto* renderer = this->renderer();
1327 if (!is<RenderListBox>(renderer))
1328 return;
1329 auto& renderListBox = downcast<RenderListBox>(*renderer);
1330
1331 // Convert to coords relative to the list box if needed.
1332 MouseEvent& mouseEvent = downcast<MouseEvent>(event);
1333 IntPoint localOffset = roundedIntPoint(renderListBox.absoluteToLocal(mouseEvent.absoluteLocation(), UseTransforms));
1334 int listIndex = renderListBox.listIndexAtOffset(toIntSize(localOffset));
1335 if (listIndex >= 0) {
1336 if (!isDisabledFormControl()) {
1337#if PLATFORM(COCOA)
1338 updateSelectedState(listIndex, mouseEvent.metaKey(), mouseEvent.shiftKey());
1339#else
1340 updateSelectedState(listIndex, mouseEvent.ctrlKey(), mouseEvent.shiftKey());
1341#endif
1342 }
1343 if (RefPtr<Frame> frame = document().frame())
1344 frame->eventHandler().setMouseDownMayStartAutoscroll();
1345
1346 mouseEvent.setDefaultHandled();
1347 }
1348 } else if (event.type() == eventNames().mousemoveEvent && is<MouseEvent>(event) && !downcast<RenderListBox>(*renderer()).canBeScrolledAndHasScrollableArea()) {
1349 MouseEvent& mouseEvent = downcast<MouseEvent>(event);
1350 if (mouseEvent.button() != LeftButton || !mouseEvent.buttonDown())
1351 return;
1352
1353 auto& renderListBox = downcast<RenderListBox>(*renderer());
1354 IntPoint localOffset = roundedIntPoint(renderListBox.absoluteToLocal(mouseEvent.absoluteLocation(), UseTransforms));
1355 int listIndex = renderListBox.listIndexAtOffset(toIntSize(localOffset));
1356 if (listIndex >= 0) {
1357 if (!isDisabledFormControl()) {
1358 if (m_multiple) {
1359 // Only extend selection if there is something selected.
1360 if (m_activeSelectionAnchorIndex < 0)
1361 return;
1362
1363 setActiveSelectionEndIndex(listIndex);
1364 updateListBoxSelection(false);
1365 } else {
1366 setActiveSelectionAnchorIndex(listIndex);
1367 setActiveSelectionEndIndex(listIndex);
1368 updateListBoxSelection(true);
1369 }
1370 }
1371 mouseEvent.setDefaultHandled();
1372 }
1373 } else if (event.type() == eventNames().mouseupEvent && is<MouseEvent>(event) && downcast<MouseEvent>(event).button() == LeftButton && document().frame()->eventHandler().autoscrollRenderer() != renderer()) {
1374 // This click or drag event was not over any of the options.
1375 if (m_lastOnChangeSelection.isEmpty())
1376 return;
1377 // This makes sure we fire dispatchFormControlChangeEvent for a single
1378 // click. For drag selection, onChange will fire when the autoscroll
1379 // timer stops.
1380 listBoxOnChange();
1381 } else if (event.type() == eventNames().keydownEvent) {
1382 if (!is<KeyboardEvent>(event))
1383 return;
1384
1385 KeyboardEvent& keyboardEvent = downcast<KeyboardEvent>(event);
1386 const String& keyIdentifier = keyboardEvent.keyIdentifier();
1387
1388 bool handled = false;
1389 int endIndex = 0;
1390 if (m_activeSelectionEndIndex < 0) {
1391 // Initialize the end index
1392 if (keyIdentifier == "Down" || keyIdentifier == "PageDown") {
1393 int startIndex = lastSelectedListIndex();
1394 handled = true;
1395 if (keyIdentifier == "Down")
1396 endIndex = nextSelectableListIndex(startIndex);
1397 else
1398 endIndex = nextSelectableListIndexPageAway(startIndex, SkipForwards);
1399 } else if (keyIdentifier == "Up" || keyIdentifier == "PageUp") {
1400 int startIndex = optionToListIndex(selectedIndex());
1401 handled = true;
1402 if (keyIdentifier == "Up")
1403 endIndex = previousSelectableListIndex(startIndex);
1404 else
1405 endIndex = nextSelectableListIndexPageAway(startIndex, SkipBackwards);
1406 }
1407 } else {
1408 // Set the end index based on the current end index.
1409 if (keyIdentifier == "Down") {
1410 endIndex = nextSelectableListIndex(m_activeSelectionEndIndex);
1411 handled = true;
1412 } else if (keyIdentifier == "Up") {
1413 endIndex = previousSelectableListIndex(m_activeSelectionEndIndex);
1414 handled = true;
1415 } else if (keyIdentifier == "PageDown") {
1416 endIndex = nextSelectableListIndexPageAway(m_activeSelectionEndIndex, SkipForwards);
1417 handled = true;
1418 } else if (keyIdentifier == "PageUp") {
1419 endIndex = nextSelectableListIndexPageAway(m_activeSelectionEndIndex, SkipBackwards);
1420 handled = true;
1421 }
1422 }
1423 if (keyIdentifier == "Home") {
1424 endIndex = firstSelectableListIndex();
1425 handled = true;
1426 } else if (keyIdentifier == "End") {
1427 endIndex = lastSelectableListIndex();
1428 handled = true;
1429 }
1430
1431 if (isSpatialNavigationEnabled(document().frame()))
1432 // Check if the selection moves to the boundary.
1433 if (keyIdentifier == "Left" || keyIdentifier == "Right" || ((keyIdentifier == "Down" || keyIdentifier == "Up") && endIndex == m_activeSelectionEndIndex))
1434 return;
1435
1436 if (endIndex >= 0 && handled) {
1437 // Save the selection so it can be compared to the new selection
1438 // when dispatching change events immediately after making the new
1439 // selection.
1440 saveLastSelection();
1441
1442 ASSERT_UNUSED(listItems, !listItems.size() || static_cast<size_t>(endIndex) < listItems.size());
1443 setActiveSelectionEndIndex(endIndex);
1444
1445#if PLATFORM(COCOA)
1446 m_allowsNonContiguousSelection = m_multiple && isSpatialNavigationEnabled(document().frame());
1447#else
1448 m_allowsNonContiguousSelection = m_multiple && (isSpatialNavigationEnabled(document().frame()) || keyboardEvent.ctrlKey());
1449#endif
1450 bool selectNewItem = keyboardEvent.shiftKey() || !m_allowsNonContiguousSelection;
1451
1452 if (selectNewItem)
1453 m_activeSelectionState = true;
1454 // If the anchor is unitialized, or if we're going to deselect all
1455 // other options, then set the anchor index equal to the end index.
1456 bool deselectOthers = !m_multiple || (!keyboardEvent.shiftKey() && selectNewItem);
1457 if (m_activeSelectionAnchorIndex < 0 || deselectOthers) {
1458 if (deselectOthers)
1459 deselectItemsWithoutValidation();
1460 setActiveSelectionAnchorIndex(m_activeSelectionEndIndex);
1461 }
1462
1463 downcast<RenderListBox>(*renderer()).scrollToRevealElementAtListIndex(endIndex);
1464 if (selectNewItem) {
1465 updateListBoxSelection(deselectOthers);
1466 listBoxOnChange();
1467 } else
1468 scrollToSelection();
1469
1470 keyboardEvent.setDefaultHandled();
1471 }
1472 } else if (event.type() == eventNames().keypressEvent) {
1473 if (!is<KeyboardEvent>(event))
1474 return;
1475 KeyboardEvent& keyboardEvent = downcast<KeyboardEvent>(event);
1476 int keyCode = keyboardEvent.keyCode();
1477
1478 if (keyCode == '\r') {
1479 if (form())
1480 form()->submitImplicitly(keyboardEvent, false);
1481 keyboardEvent.setDefaultHandled();
1482 } else if (m_multiple && keyCode == ' ' && m_allowsNonContiguousSelection) {
1483 // Use space to toggle selection change.
1484 m_activeSelectionState = !m_activeSelectionState;
1485 ASSERT(m_activeSelectionEndIndex >= 0);
1486 ASSERT(m_activeSelectionEndIndex < static_cast<int>(listItems.size()));
1487 ASSERT(is<HTMLOptionElement>(*listItems[m_activeSelectionEndIndex]));
1488 updateSelectedState(m_activeSelectionEndIndex, true /*multi*/, false /*shift*/);
1489 listBoxOnChange();
1490 keyboardEvent.setDefaultHandled();
1491 }
1492 }
1493}
1494
1495void HTMLSelectElement::defaultEventHandler(Event& event)
1496{
1497 auto* renderer = this->renderer();
1498 if (!renderer)
1499 return;
1500
1501#if !PLATFORM(IOS_FAMILY)
1502 if (isDisabledFormControl()) {
1503 HTMLFormControlElementWithState::defaultEventHandler(event);
1504 return;
1505 }
1506
1507 if (renderer->isMenuList())
1508 menuListDefaultEventHandler(event);
1509 else
1510 listBoxDefaultEventHandler(event);
1511#else
1512 menuListDefaultEventHandler(event);
1513#endif
1514 if (event.defaultHandled())
1515 return;
1516
1517 if (event.type() == eventNames().keypressEvent && is<KeyboardEvent>(event)) {
1518 KeyboardEvent& keyboardEvent = downcast<KeyboardEvent>(event);
1519 if (!keyboardEvent.ctrlKey() && !keyboardEvent.altKey() && !keyboardEvent.metaKey() && u_isprint(keyboardEvent.charCode())) {
1520 typeAheadFind(keyboardEvent);
1521 event.setDefaultHandled();
1522 return;
1523 }
1524 }
1525 HTMLFormControlElementWithState::defaultEventHandler(event);
1526}
1527
1528int HTMLSelectElement::lastSelectedListIndex() const
1529{
1530 auto& items = listItems();
1531 for (size_t i = items.size(); i;) {
1532 auto& element = *items[--i];
1533 if (is<HTMLOptionElement>(element) && downcast<HTMLOptionElement>(element).selected())
1534 return i;
1535 }
1536 return -1;
1537}
1538
1539int HTMLSelectElement::indexOfSelectedOption() const
1540{
1541 return optionToListIndex(selectedIndex());
1542}
1543
1544int HTMLSelectElement::optionCount() const
1545{
1546 return listItems().size();
1547}
1548
1549String HTMLSelectElement::optionAtIndex(int index) const
1550{
1551 auto& element = *listItems()[index];
1552 if (!is<HTMLOptionElement>(element) || downcast<HTMLOptionElement>(element).isDisabledFormControl())
1553 return String();
1554 return downcast<HTMLOptionElement>(element).textIndentedToRespectGroupLabel();
1555}
1556
1557void HTMLSelectElement::typeAheadFind(KeyboardEvent& event)
1558{
1559 int index = m_typeAhead.handleEvent(&event, TypeAhead::MatchPrefix | TypeAhead::CycleFirstChar);
1560 if (index < 0)
1561 return;
1562 selectOption(listToOptionIndex(index), DeselectOtherOptions | DispatchChangeEvent | UserDriven);
1563 if (!usesMenuList())
1564 listBoxOnChange();
1565}
1566
1567Node::InsertedIntoAncestorResult HTMLSelectElement::insertedIntoAncestor(InsertionType insertionType, ContainerNode& parentOfInsertedTree)
1568{
1569 // When the element is created during document parsing, it won't have any
1570 // items yet - but for innerHTML and related methods, this method is called
1571 // after the whole subtree is constructed.
1572 recalcListItems();
1573 return HTMLFormControlElementWithState::insertedIntoAncestor(insertionType, parentOfInsertedTree);
1574}
1575
1576void HTMLSelectElement::accessKeySetSelectedIndex(int index)
1577{
1578 // First bring into focus the list box.
1579 if (!focused())
1580 accessKeyAction(false);
1581
1582 // If this index is already selected, unselect. otherwise update the selected index.
1583 auto& items = listItems();
1584 int listIndex = optionToListIndex(index);
1585 if (listIndex >= 0) {
1586 auto& element = *items[listIndex];
1587 if (is<HTMLOptionElement>(element)) {
1588 if (downcast<HTMLOptionElement>(element).selected())
1589 downcast<HTMLOptionElement>(element).setSelectedState(false);
1590 else
1591 selectOption(index, DispatchChangeEvent | UserDriven);
1592 }
1593 }
1594
1595 if (usesMenuList())
1596 dispatchChangeEventForMenuList();
1597 else
1598 listBoxOnChange();
1599
1600 scrollToSelection();
1601}
1602
1603unsigned HTMLSelectElement::length() const
1604{
1605 unsigned options = 0;
1606
1607 auto& items = listItems();
1608 for (unsigned i = 0; i < items.size(); ++i) {
1609 if (is<HTMLOptionElement>(*items[i]))
1610 ++options;
1611 }
1612
1613 return options;
1614}
1615
1616} // namespace
1617