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 | |
60 | namespace WebCore { |
61 | |
62 | WTF_MAKE_ISO_ALLOCATED_IMPL(HTMLSelectElement); |
63 | |
64 | using namespace WTF::Unicode; |
65 | |
66 | using namespace HTMLNames; |
67 | |
68 | // Upper limit agreed upon with representatives of Opera and Mozilla. |
69 | static const unsigned maxSelectItems = 10000; |
70 | |
71 | HTMLSelectElement::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 | |
87 | Ref<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 | |
93 | void 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 | |
101 | const 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 | |
108 | void HTMLSelectElement::deselectItems(HTMLOptionElement* excludeElement) |
109 | { |
110 | deselectItemsWithoutValidation(excludeElement); |
111 | updateValidity(); |
112 | } |
113 | |
114 | void 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 | |
138 | bool 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 | |
162 | String 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 | |
173 | bool 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 | |
187 | void 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 | |
199 | bool HTMLSelectElement::() 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 | |
211 | int HTMLSelectElement::activeSelectionStartListIndex() const |
212 | { |
213 | if (m_activeSelectionAnchorIndex >= 0) |
214 | return m_activeSelectionAnchorIndex; |
215 | return optionToListIndex(selectedIndex()); |
216 | } |
217 | |
218 | int HTMLSelectElement::activeSelectionEndListIndex() const |
219 | { |
220 | if (m_activeSelectionEndIndex >= 0) |
221 | return m_activeSelectionEndIndex; |
222 | return lastSelectedListIndex(); |
223 | } |
224 | |
225 | ExceptionOr<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 | |
242 | void HTMLSelectElement::remove(int optionIndex) |
243 | { |
244 | int listIndex = optionToListIndex(optionIndex); |
245 | if (listIndex < 0) |
246 | return; |
247 | |
248 | listItems()[listIndex]->remove(); |
249 | } |
250 | |
251 | String 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 | |
263 | void 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 | |
280 | bool 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 | |
291 | void 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 | |
314 | bool HTMLSelectElement::isKeyboardFocusable(KeyboardEvent* event) const |
315 | { |
316 | if (renderer()) |
317 | return isFocusable(); |
318 | return HTMLFormControlElementWithState::isKeyboardFocusable(event); |
319 | } |
320 | |
321 | bool HTMLSelectElement::isMouseFocusable() const |
322 | { |
323 | if (renderer()) |
324 | return isFocusable(); |
325 | return HTMLFormControlElementWithState::isMouseFocusable(); |
326 | } |
327 | |
328 | bool HTMLSelectElement::canSelectAll() const |
329 | { |
330 | return !usesMenuList(); |
331 | } |
332 | |
333 | RenderPtr<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 | |
344 | bool 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 | |
355 | Ref<HTMLCollection> HTMLSelectElement::selectedOptions() |
356 | { |
357 | return ensureRareData().ensureNodeLists().addCachedCollection<GenericCachedHTMLCollection<CollectionTypeTraits<SelectedOptions>::traversalType>>(*this, SelectedOptions); |
358 | } |
359 | |
360 | Ref<HTMLOptionsCollection> HTMLSelectElement::options() |
361 | { |
362 | return ensureRareData().ensureNodeLists().addCachedCollection<HTMLOptionsCollection>(*this, SelectOptions); |
363 | } |
364 | |
365 | void HTMLSelectElement::updateListItemSelectedStates() |
366 | { |
367 | if (m_shouldRecalcListItems) |
368 | recalcListItems(); |
369 | } |
370 | |
371 | void HTMLSelectElement::childrenChanged(const ChildChange& change) |
372 | { |
373 | setRecalcListItems(); |
374 | updateValidity(); |
375 | m_lastOnChangeSelection.clear(); |
376 | |
377 | HTMLFormControlElementWithState::childrenChanged(change); |
378 | } |
379 | |
380 | void HTMLSelectElement::optionElementChildrenChanged() |
381 | { |
382 | setRecalcListItems(); |
383 | updateValidity(); |
384 | if (auto* cache = document().existingAXObjectCache()) |
385 | cache->childrenChanged(this); |
386 | } |
387 | |
388 | void HTMLSelectElement::accessKeyAction(bool sendMouseEvents) |
389 | { |
390 | focus(); |
391 | dispatchSimulatedClick(0, sendMouseEvents ? SendMouseUpDownEvents : SendNoEvents); |
392 | } |
393 | |
394 | void 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 | |
406 | void HTMLSelectElement::setSize(unsigned size) |
407 | { |
408 | setUnsignedIntegralAttribute(sizeAttr, limitToOnlyHTMLNonNegative(size)); |
409 | } |
410 | |
411 | HTMLOptionElement* HTMLSelectElement::namedItem(const AtomicString& name) |
412 | { |
413 | return options()->namedItem(name); |
414 | } |
415 | |
416 | HTMLOptionElement* HTMLSelectElement::item(unsigned index) |
417 | { |
418 | return options()->item(index); |
419 | } |
420 | |
421 | ExceptionOr<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 | |
456 | ExceptionOr<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 | |
494 | bool HTMLSelectElement::isRequiredFormControl() const |
495 | { |
496 | return isRequired(); |
497 | } |
498 | |
499 | bool 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. |
512 | int 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 | |
529 | int HTMLSelectElement::nextSelectableListIndex(int startIndex) const |
530 | { |
531 | return nextValidIndex(startIndex, SkipForwards, 1); |
532 | } |
533 | |
534 | int HTMLSelectElement::previousSelectableListIndex(int startIndex) const |
535 | { |
536 | if (startIndex == -1) |
537 | startIndex = listItems().size(); |
538 | return nextValidIndex(startIndex, SkipBackwards, 1); |
539 | } |
540 | |
541 | int 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 | |
550 | int 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|. |
556 | int 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 | |
574 | void 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 | |
595 | void 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 | |
607 | void 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 | |
619 | void HTMLSelectElement::setActiveSelectionEndIndex(int index) |
620 | { |
621 | m_activeSelectionEndIndex = index; |
622 | } |
623 | |
624 | void 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 | |
657 | void 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 | |
686 | void HTMLSelectElement::() |
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 | |
699 | void 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 | |
715 | void 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 | |
729 | const 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 | |
744 | void HTMLSelectElement::invalidateSelectedItems() |
745 | { |
746 | if (HTMLCollection* collection = cachedHTMLCollection(SelectedOptions)) |
747 | collection->invalidateCache(); |
748 | } |
749 | |
750 | void 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 | |
767 | void 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(¤t); |
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(¤t); |
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(¤t); |
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 | |
825 | int 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 | |
841 | void HTMLSelectElement::setSelectedIndex(int index) |
842 | { |
843 | selectOption(index, DeselectOtherOptions); |
844 | } |
845 | |
846 | void 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 | |
857 | void 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 | |
900 | int 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 | |
919 | int 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 | |
935 | void 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 | |
944 | void 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 | |
954 | void 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 | |
962 | FormControlState 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 | |
980 | size_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 | |
993 | void 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 | |
1029 | void HTMLSelectElement::parseMultipleAttribute(const AtomicString& value) |
1030 | { |
1031 | bool = usesMenuList(); |
1032 | m_multiple = !value.isNull(); |
1033 | updateValidity(); |
1034 | if (oldUsesMenuList != usesMenuList()) |
1035 | invalidateStyleAndRenderersForSubtree(); |
1036 | } |
1037 | |
1038 | bool 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 | |
1058 | void 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 | |
1090 | bool 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 | |
1121 | void 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& = 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& = downcast<RenderMenuList>(*renderer()); |
1260 | if (menuList.popupIsVisible()) |
1261 | menuList.hidePopup(); |
1262 | } |
1263 | #endif |
1264 | } |
1265 | |
1266 | void 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 | |
1318 | void 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 | |
1495 | void 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 | |
1528 | int 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 | |
1539 | int HTMLSelectElement::indexOfSelectedOption() const |
1540 | { |
1541 | return optionToListIndex(selectedIndex()); |
1542 | } |
1543 | |
1544 | int HTMLSelectElement::optionCount() const |
1545 | { |
1546 | return listItems().size(); |
1547 | } |
1548 | |
1549 | String 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 | |
1557 | void 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 | |
1567 | Node::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 | |
1576 | void 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 | |
1603 | unsigned 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 | |