/* * Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). * Copyright (C) 1999 Lars Knoll (knoll@kde.org) * (C) 1999 Antti Koivisto (koivisto@kde.org) * (C) 2001 Dirk Mueller (mueller@kde.org) * Copyright (C) 2004, 2005, 2006, 2007, 2009, 2010, 2011 Apple Inc. All rights reserved. * (C) 2006 Alexey Proskuryakov (ap@nypop.com) * Copyright (C) 2010 Google Inc. All rights reserved. * Copyright (C) 2009 Torch Mobile Inc. All rights reserved. (http://www.torchmobile.com/) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * */ #include "config.h" #include "HTMLSelectElement.h" #include "AXObjectCache.h" #include "Attribute.h" #include "Chrome.h" #include "ChromeClient.h" #include "EventHandler.h" #include "EventNames.h" #include "ExceptionCodePlaceholder.h" #include "FormController.h" #include "FormDataList.h" #include "Frame.h" #include "HTMLFormElement.h" #include "HTMLNames.h" #include "HTMLOptGroupElement.h" #include "HTMLOptionElement.h" #include "HTMLOptionsCollection.h" #include "KeyboardEvent.h" #include "LocalizedStrings.h" #include "MouseEvent.h" #include "NodeRenderingContext.h" #include "NodeTraversal.h" #include "Page.h" #include "PlatformMouseEvent.h" #include "RenderListBox.h" #include "RenderMenuList.h" #include "RenderTheme.h" #include "ScriptEventListener.h" #include "Settings.h" #include "SpatialNavigation.h" using namespace std; using namespace WTF::Unicode; namespace WebCore { using namespace HTMLNames; // Upper limit agreed upon with representatives of Opera and Mozilla. static const unsigned maxSelectItems = 10000; HTMLSelectElement::HTMLSelectElement(const QualifiedName& tagName, Document* document, HTMLFormElement* form) : HTMLFormControlElementWithState(tagName, document, form) , m_typeAhead(this) , m_size(0) , m_lastOnChangeIndex(-1) , m_activeSelectionAnchorIndex(-1) , m_activeSelectionEndIndex(-1) , m_isProcessingUserDrivenChange(false) , m_multiple(false) , m_activeSelectionState(false) , m_shouldRecalcListItems(false) { ASSERT(hasTagName(selectTag)); } PassRefPtr HTMLSelectElement::create(const QualifiedName& tagName, Document* document, HTMLFormElement* form) { ASSERT(tagName.matches(selectTag)); return adoptRef(new HTMLSelectElement(tagName, document, form)); } const AtomicString& HTMLSelectElement::formControlType() const { DEFINE_STATIC_LOCAL(const AtomicString, selectMultiple, ("select-multiple", AtomicString::ConstructFromLiteral)); DEFINE_STATIC_LOCAL(const AtomicString, selectOne, ("select-one", AtomicString::ConstructFromLiteral)); return m_multiple ? selectMultiple : selectOne; } void HTMLSelectElement::deselectItems(HTMLOptionElement* excludeElement) { deselectItemsWithoutValidation(excludeElement); setNeedsValidityCheck(); } void HTMLSelectElement::optionSelectedByUser(int optionIndex, bool fireOnChangeNow, bool allowMultipleSelection) { // User interaction such as mousedown events can cause list box select elements to send change events. // This produces that same behavior for changes triggered by other code running on behalf of the user. if (!usesMenuList()) { updateSelectedState(optionToListIndex(optionIndex), allowMultipleSelection, false); setNeedsValidityCheck(); if (fireOnChangeNow) listBoxOnChange(); return; } // Bail out if this index is already the selected one, to avoid running unnecessary JavaScript that can mess up // autofill when there is no actual change (see https://bugs.webkit.org/show_bug.cgi?id=35256 and ). // The selectOption function does not behave this way, possibly because other callers need a change event even // in cases where the selected option is not change. if (optionIndex == selectedIndex()) return; selectOption(optionIndex, DeselectOtherOptions | (fireOnChangeNow ? DispatchChangeEvent : 0) | UserDriven); } bool HTMLSelectElement::hasPlaceholderLabelOption() const { // The select element has no placeholder label option if it has an attribute "multiple" specified or a display size of non-1. // // The condition "size() > 1" is not compliant with the HTML5 spec as of Dec 3, 2010. "size() != 1" is correct. // Using "size() > 1" here because size() may be 0 in WebKit. // See the discussion at https://bugs.webkit.org/show_bug.cgi?id=43887 // // "0 size()" happens when an attribute "size" is absent or an invalid size attribute is specified. // In this case, the display size should be assumed as the default. // The default display size is 1 for non-multiple select elements, and 4 for multiple select elements. // // Finally, if size() == 0 and non-multiple, the display size can be assumed as 1. if (multiple() || size() > 1) return false; int listIndex = optionToListIndex(0); ASSERT(listIndex >= 0); if (listIndex < 0) return false; HTMLOptionElement* option = toHTMLOptionElement(listItems()[listIndex]); return !listIndex && option->value().isEmpty(); } String HTMLSelectElement::validationMessage() const { if (!willValidate()) return String(); if (customError()) return customValidationMessage(); return valueMissing() ? validationMessageValueMissingForSelectText() : String(); } bool HTMLSelectElement::valueMissing() const { if (!willValidate()) return false; if (!isRequired()) return false; int firstSelectionIndex = selectedIndex(); // If a non-placeholer label option is selected (firstSelectionIndex > 0), it's not value-missing. return firstSelectionIndex < 0 || (!firstSelectionIndex && hasPlaceholderLabelOption()); } void HTMLSelectElement::listBoxSelectItem(int listIndex, bool allowMultiplySelections, bool shift, bool fireOnChangeNow) { if (!multiple()) optionSelectedByUser(listToOptionIndex(listIndex), fireOnChangeNow, false); else { updateSelectedState(listIndex, allowMultiplySelections, shift); setNeedsValidityCheck(); if (fireOnChangeNow) listBoxOnChange(); } } bool HTMLSelectElement::usesMenuList() const { const Page* page = document()->page(); RefPtr renderTheme = page ? page->theme() : RenderTheme::defaultTheme(); if (renderTheme->delegatesMenuListRendering()) return true; return !m_multiple && m_size <= 1; } int HTMLSelectElement::activeSelectionStartListIndex() const { if (m_activeSelectionAnchorIndex >= 0) return m_activeSelectionAnchorIndex; return optionToListIndex(selectedIndex()); } int HTMLSelectElement::activeSelectionEndListIndex() const { if (m_activeSelectionEndIndex >= 0) return m_activeSelectionEndIndex; return lastSelectedListIndex(); } void HTMLSelectElement::add(HTMLElement* element, HTMLElement* before, ExceptionCode& ec) { // Make sure the element is ref'd and deref'd so we don't leak it. RefPtr protectNewChild(element); if (!element || !(element->hasLocalName(optionTag) || element->hasLocalName(hrTag))) return; insertBefore(element, before, ec); setNeedsValidityCheck(); } void HTMLSelectElement::remove(int optionIndex) { int listIndex = optionToListIndex(optionIndex); if (listIndex < 0) return; listItems()[listIndex]->remove(IGNORE_EXCEPTION); } void HTMLSelectElement::remove(HTMLOptionElement* option) { if (option->ownerSelectElement() != this) return; option->remove(IGNORE_EXCEPTION); } String HTMLSelectElement::value() const { const Vector& items = listItems(); for (unsigned i = 0; i < items.size(); i++) { if (items[i]->hasLocalName(optionTag)) { HTMLOptionElement* option = toHTMLOptionElement(items[i]); if (option->selected()) return option->value(); } } return ""; } void HTMLSelectElement::setValue(const String &value) { // We clear the previously selected option(s) when needed, to guarantee calling setSelectedIndex() only once. if (value.isNull()) { setSelectedIndex(-1); return; } // Find the option with value() matching the given parameter and make it the current selection. const Vector& items = listItems(); unsigned optionIndex = 0; for (unsigned i = 0; i < items.size(); i++) { if (items[i]->hasLocalName(optionTag)) { if (toHTMLOptionElement(items[i])->value() == value) { setSelectedIndex(optionIndex); return; } optionIndex++; } } setSelectedIndex(-1); } bool HTMLSelectElement::isPresentationAttribute(const QualifiedName& name) const { if (name == alignAttr) { // Don't map 'align' attribute. This matches what Firefox, Opera and IE do. // See http://bugs.webkit.org/show_bug.cgi?id=12072 return false; } return HTMLFormControlElementWithState::isPresentationAttribute(name); } void HTMLSelectElement::parseAttribute(const QualifiedName& name, const AtomicString& value) { if (name == sizeAttr) { int oldSize = m_size; // Set the attribute value to a number. // This is important since the style rules for this attribute can determine the appearance property. int size = value.toInt(); String attrSize = String::number(size); if (attrSize != value) { // FIXME: This is horribly factored. if (Attribute* sizeAttribute = ensureUniqueElementData()->getAttributeItem(sizeAttr)) sizeAttribute->setValue(attrSize); } size = max(size, 1); // Ensure that we've determined selectedness of the items at least once prior to changing the size. if (oldSize != size) updateListItemSelectedStates(); m_size = size; setNeedsValidityCheck(); if (m_size != oldSize && attached()) { reattach(); setRecalcListItems(); } } else if (name == multipleAttr) parseMultipleAttribute(value); else if (name == accesskeyAttr) { // FIXME: ignore for the moment. // } else HTMLFormControlElementWithState::parseAttribute(name, value); } bool HTMLSelectElement::isKeyboardFocusable(KeyboardEvent* event) const { if (renderer()) return isFocusable(); return HTMLFormControlElementWithState::isKeyboardFocusable(event); } bool HTMLSelectElement::isMouseFocusable() const { if (renderer()) return isFocusable(); return HTMLFormControlElementWithState::isMouseFocusable(); } bool HTMLSelectElement::canSelectAll() const { return !usesMenuList(); } RenderObject* HTMLSelectElement::createRenderer(RenderArena* arena, RenderStyle*) { if (usesMenuList()) return new (arena) RenderMenuList(this); return new (arena) RenderListBox(this); } bool HTMLSelectElement::childShouldCreateRenderer(const NodeRenderingContext& childContext) const { if (!HTMLFormControlElementWithState::childShouldCreateRenderer(childContext)) return false; if (!usesMenuList()) return isHTMLOptionElement(childContext.node()) || isHTMLOptGroupElement(childContext.node()) || validationMessageShadowTreeContains(childContext.node()); return validationMessageShadowTreeContains(childContext.node()); } PassRefPtr HTMLSelectElement::selectedOptions() { return ensureCachedHTMLCollection(SelectedOptions); } PassRefPtr HTMLSelectElement::options() { return static_cast(ensureCachedHTMLCollection(SelectOptions).get()); } void HTMLSelectElement::updateListItemSelectedStates() { if (m_shouldRecalcListItems) recalcListItems(); } void HTMLSelectElement::childrenChanged(bool changedByParser, Node* beforeChange, Node* afterChange, int childCountDelta) { setRecalcListItems(); setNeedsValidityCheck(); m_lastOnChangeSelection.clear(); HTMLFormControlElementWithState::childrenChanged(changedByParser, beforeChange, afterChange, childCountDelta); } void HTMLSelectElement::optionElementChildrenChanged() { setRecalcListItems(); setNeedsValidityCheck(); if (renderer()) { if (AXObjectCache* cache = renderer()->document()->existingAXObjectCache()) cache->childrenChanged(this); } } void HTMLSelectElement::accessKeyAction(bool sendMouseEvents) { focus(); dispatchSimulatedClick(0, sendMouseEvents ? SendMouseUpDownEvents : SendNoEvents); } void HTMLSelectElement::setMultiple(bool multiple) { bool oldMultiple = this->multiple(); int oldSelectedIndex = selectedIndex(); setAttribute(multipleAttr, multiple ? "" : 0); // Restore selectedIndex after changing the multiple flag to preserve // selection as single-line and multi-line has different defaults. if (oldMultiple != this->multiple()) setSelectedIndex(oldSelectedIndex); } void HTMLSelectElement::setSize(int size) { setAttribute(sizeAttr, String::number(size)); } Node* HTMLSelectElement::namedItem(const AtomicString& name) { return options()->namedItem(name); } Node* HTMLSelectElement::item(unsigned index) { return options()->item(index); } void HTMLSelectElement::setOption(unsigned index, HTMLOptionElement* option, ExceptionCode& ec) { ec = 0; if (index > maxSelectItems - 1) index = maxSelectItems - 1; int diff = index - length(); RefPtr before = 0; // Out of array bounds? First insert empty dummies. if (diff > 0) { setLength(index, ec); // Replace an existing entry? } else if (diff < 0) { before = toHTMLElement(options()->item(index+1)); remove(index); } // Finally add the new element. if (!ec) { add(option, before.get(), ec); if (diff >= 0 && option->selected()) optionSelectionStateChanged(option, true); } } void HTMLSelectElement::setLength(unsigned newLen, ExceptionCode& ec) { ec = 0; if (newLen > maxSelectItems) newLen = maxSelectItems; int diff = length() - newLen; if (diff < 0) { // Add dummy elements. do { RefPtr option = document()->createElement(optionTag, false); ASSERT(option); add(toHTMLElement(option.get()), 0, ec); if (ec) break; } while (++diff); } else { const Vector& items = listItems(); // Removing children fires mutation events, which might mutate the DOM further, so we first copy out a list // of elements that we intend to remove then attempt to remove them one at a time. Vector > itemsToRemove; size_t optionIndex = 0; for (size_t i = 0; i < items.size(); ++i) { Element* item = items[i]; if (item->hasLocalName(optionTag) && optionIndex++ >= newLen) { ASSERT(item->parentNode()); itemsToRemove.append(item); } } for (size_t i = 0; i < itemsToRemove.size(); ++i) { Element* item = itemsToRemove[i].get(); if (item->parentNode()) item->parentNode()->removeChild(item, ec); } } setNeedsValidityCheck(); } bool HTMLSelectElement::isRequiredFormControl() const { return isRequired(); } // Returns the 1st valid item |skip| items from |listIndex| in direction |direction| if there is one. // Otherwise, it returns the valid item closest to that boundary which is past |listIndex| if there is one. // Otherwise, it returns |listIndex|. // Valid means that it is enabled and an option element. int HTMLSelectElement::nextValidIndex(int listIndex, SkipDirection direction, int skip) const { ASSERT(direction == -1 || direction == 1); const Vector& listItems = this->listItems(); int lastGoodIndex = listIndex; int size = listItems.size(); for (listIndex += direction; listIndex >= 0 && listIndex < size; listIndex += direction) { --skip; if (!listItems[listIndex]->isDisabledFormControl() && isHTMLOptionElement(listItems[listIndex])) { lastGoodIndex = listIndex; if (skip <= 0) break; } } return lastGoodIndex; } int HTMLSelectElement::nextSelectableListIndex(int startIndex) const { return nextValidIndex(startIndex, SkipForwards, 1); } int HTMLSelectElement::previousSelectableListIndex(int startIndex) const { if (startIndex == -1) startIndex = listItems().size(); return nextValidIndex(startIndex, SkipBackwards, 1); } int HTMLSelectElement::firstSelectableListIndex() const { const Vector& items = listItems(); int index = nextValidIndex(items.size(), SkipBackwards, INT_MAX); if (static_cast(index) == items.size()) return -1; return index; } int HTMLSelectElement::lastSelectableListIndex() const { return nextValidIndex(-1, SkipForwards, INT_MAX); } // Returns the index of the next valid item one page away from |startIndex| in direction |direction|. int HTMLSelectElement::nextSelectableListIndexPageAway(int startIndex, SkipDirection direction) const { const Vector& items = listItems(); // Can't use m_size because renderer forces a minimum size. int pageSize = 0; if (renderer()->isListBox()) pageSize = toRenderListBox(renderer())->size() - 1; // -1 so we still show context. // One page away, but not outside valid bounds. // If there is a valid option item one page away, the index is chosen. // If there is no exact one page away valid option, returns startIndex or the most far index. int edgeIndex = (direction == SkipForwards) ? 0 : (items.size() - 1); int skipAmount = pageSize + ((direction == SkipForwards) ? startIndex : (edgeIndex - startIndex)); return nextValidIndex(edgeIndex, direction, skipAmount); } void HTMLSelectElement::selectAll() { ASSERT(!usesMenuList()); if (!renderer() || !m_multiple) return; // Save the selection so it can be compared to the new selectAll selection // when dispatching change events. saveLastSelection(); m_activeSelectionState = true; setActiveSelectionAnchorIndex(nextSelectableListIndex(-1)); setActiveSelectionEndIndex(previousSelectableListIndex(-1)); if (m_activeSelectionAnchorIndex < 0) return; updateListBoxSelection(false); listBoxOnChange(); setNeedsValidityCheck(); } void HTMLSelectElement::saveLastSelection() { if (usesMenuList()) { m_lastOnChangeIndex = selectedIndex(); return; } m_lastOnChangeSelection.clear(); const Vector& items = listItems(); for (unsigned i = 0; i < items.size(); ++i) { HTMLElement* element = items[i]; m_lastOnChangeSelection.append(isHTMLOptionElement(element) && toHTMLOptionElement(element)->selected()); } } void HTMLSelectElement::setActiveSelectionAnchorIndex(int index) { m_activeSelectionAnchorIndex = index; // Cache the selection state so we can restore the old selection as the new // selection pivots around this anchor index. m_cachedStateForActiveSelection.clear(); const Vector& items = listItems(); for (unsigned i = 0; i < items.size(); ++i) { HTMLElement* element = items[i]; m_cachedStateForActiveSelection.append(isHTMLOptionElement(element) && toHTMLOptionElement(element)->selected()); } } void HTMLSelectElement::setActiveSelectionEndIndex(int index) { m_activeSelectionEndIndex = index; } void HTMLSelectElement::updateListBoxSelection(bool deselectOtherOptions) { ASSERT(renderer() && (renderer()->isListBox() || m_multiple)); ASSERT(!listItems().size() || m_activeSelectionAnchorIndex >= 0); unsigned start = min(m_activeSelectionAnchorIndex, m_activeSelectionEndIndex); unsigned end = max(m_activeSelectionAnchorIndex, m_activeSelectionEndIndex); const Vector& items = listItems(); for (unsigned i = 0; i < items.size(); ++i) { HTMLElement* element = items[i]; if (!isHTMLOptionElement(element) || toHTMLOptionElement(element)->isDisabledFormControl()) continue; if (i >= start && i <= end) toHTMLOptionElement(element)->setSelectedState(m_activeSelectionState); else if (deselectOtherOptions || i >= m_cachedStateForActiveSelection.size()) toHTMLOptionElement(element)->setSelectedState(false); else toHTMLOptionElement(element)->setSelectedState(m_cachedStateForActiveSelection[i]); } scrollToSelection(); setNeedsValidityCheck(); notifyFormStateChanged(); } void HTMLSelectElement::listBoxOnChange() { ASSERT(!usesMenuList() || m_multiple); const Vector& items = listItems(); // If the cached selection list is empty, or the size has changed, then fire // dispatchFormControlChangeEvent, and return early. if (m_lastOnChangeSelection.isEmpty() || m_lastOnChangeSelection.size() != items.size()) { dispatchFormControlChangeEvent(); return; } // Update m_lastOnChangeSelection and fire dispatchFormControlChangeEvent. bool fireOnChange = false; for (unsigned i = 0; i < items.size(); ++i) { HTMLElement* element = items[i]; bool selected = isHTMLOptionElement(element) && toHTMLOptionElement(element)->selected(); if (selected != m_lastOnChangeSelection[i]) fireOnChange = true; m_lastOnChangeSelection[i] = selected; } if (fireOnChange) dispatchFormControlChangeEvent(); } void HTMLSelectElement::dispatchChangeEventForMenuList() { ASSERT(usesMenuList()); int selected = selectedIndex(); if (m_lastOnChangeIndex != selected && m_isProcessingUserDrivenChange) { m_lastOnChangeIndex = selected; m_isProcessingUserDrivenChange = false; dispatchFormControlChangeEvent(); } } void HTMLSelectElement::scrollToSelection() { if (usesMenuList()) return; if (RenderObject* renderer = this->renderer()) toRenderListBox(renderer)->selectionChanged(); } void HTMLSelectElement::setOptionsChangedOnRenderer() { if (RenderObject* renderer = this->renderer()) { if (usesMenuList()) toRenderMenuList(renderer)->setOptionsChanged(true); else toRenderListBox(renderer)->setOptionsChanged(true); } } const Vector& HTMLSelectElement::listItems() const { if (m_shouldRecalcListItems) recalcListItems(); else { #if !ASSERT_DISABLED Vector items = m_listItems; recalcListItems(false); ASSERT(items == m_listItems); #endif } return m_listItems; } void HTMLSelectElement::invalidateSelectedItems() { if (HTMLCollection* collection = cachedHTMLCollection(SelectedOptions)) collection->invalidateCache(); } void HTMLSelectElement::setRecalcListItems() { m_shouldRecalcListItems = true; // Manual selection anchor is reset when manipulating the select programmatically. m_activeSelectionAnchorIndex = -1; setOptionsChangedOnRenderer(); setNeedsStyleRecalc(); if (!inDocument()) { if (HTMLCollection* collection = cachedHTMLCollection(SelectOptions)) collection->invalidateCache(); } if (!inDocument()) invalidateSelectedItems(); if (renderer()) { if (AXObjectCache* cache = renderer()->document()->existingAXObjectCache()) cache->childrenChanged(this); } } void HTMLSelectElement::recalcListItems(bool updateSelectedStates) const { m_listItems.clear(); m_shouldRecalcListItems = false; HTMLOptionElement* foundSelected = 0; HTMLOptionElement* firstOption = 0; for (Element* currentElement = ElementTraversal::firstWithin(this); currentElement; ) { if (!currentElement->isHTMLElement()) { currentElement = ElementTraversal::nextSkippingChildren(currentElement, this); continue; } HTMLElement* current = toHTMLElement(currentElement); // optgroup tags may not nest. However, both FireFox and IE will // flatten the tree automatically, so we follow suit. // (http://www.w3.org/TR/html401/interact/forms.html#h-17.6) if (isHTMLOptGroupElement(current)) { m_listItems.append(current); if (Element* nextElement = ElementTraversal::firstWithin(current)) { currentElement = nextElement; continue; } } if (isHTMLOptionElement(current)) { m_listItems.append(current); if (updateSelectedStates && !m_multiple) { HTMLOptionElement* option = toHTMLOptionElement(current); if (!firstOption) firstOption = option; if (option->selected()) { if (foundSelected) foundSelected->setSelectedState(false); foundSelected = option; } else if (m_size <= 1 && !foundSelected && !option->isDisabledFormControl()) { foundSelected = option; foundSelected->setSelectedState(true); } } } if (current->hasTagName(hrTag)) m_listItems.append(current); // In conforming HTML code, only and