/* * 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 "SelectElement.h" #include "Attribute.h" #include "CharacterNames.h" #include "Chrome.h" #include "ChromeClient.h" #include "Element.h" #include "EventHandler.h" #include "EventNames.h" #include "FormDataList.h" #include "Frame.h" #include "HTMLFormElement.h" #include "HTMLKeygenElement.h" #include "HTMLNames.h" #include "HTMLSelectElement.h" #include "KeyboardEvent.h" #include "MouseEvent.h" #include "OptionElement.h" #include "OptionGroupElement.h" #include "Page.h" #include "RenderListBox.h" #include "RenderMenuList.h" #include "SpatialNavigation.h" #include #if ENABLE(WML) #include "WMLNames.h" #include "WMLSelectElement.h" #endif // Configure platform-specific behavior when focused pop-up receives arrow/space/return keystroke. // (PLATFORM(MAC) and PLATFORM(GTK) are always false in Chromium, hence the extra tests.) #if PLATFORM(MAC) || (PLATFORM(CHROMIUM) && OS(DARWIN)) #define ARROW_KEYS_POP_MENU 1 #define SPACE_OR_RETURN_POP_MENU 0 #elif PLATFORM(GTK) || (PLATFORM(CHROMIUM) && (OS(LINUX) || OS(FREEBSD))) #define ARROW_KEYS_POP_MENU 0 #define SPACE_OR_RETURN_POP_MENU 1 #else #define ARROW_KEYS_POP_MENU 0 #define SPACE_OR_RETURN_POP_MENU 0 #endif using std::min; using std::max; using namespace WTF; using namespace Unicode; namespace WebCore { static const DOMTimeStamp typeAheadTimeout = 1000; void SelectElement::selectAll(SelectElementData& data, Element* element) { ASSERT(!data.usesMenuList()); if (!element->renderer() || !data.multiple()) return; // Save the selection so it can be compared to the new selectAll selection when dispatching change events saveLastSelection(data, element); data.setActiveSelectionState(true); setActiveSelectionAnchorIndex(data, element, nextSelectableListIndex(data, element, -1)); setActiveSelectionEndIndex(data, previousSelectableListIndex(data, element, -1)); updateListBoxSelection(data, element, false); listBoxOnChange(data, element); } void SelectElement::saveLastSelection(SelectElementData& data, Element* element) { if (data.usesMenuList()) { data.setLastOnChangeIndex(selectedIndex(data, element)); return; } Vector& lastOnChangeSelection = data.lastOnChangeSelection(); lastOnChangeSelection.clear(); const Vector& items = data.listItems(element); for (unsigned i = 0; i < items.size(); ++i) { OptionElement* optionElement = toOptionElement(items[i]); lastOnChangeSelection.append(optionElement && optionElement->selected()); } } int SelectElement::nextSelectableListIndex(SelectElementData& data, Element* element, int startIndex) { const Vector& items = data.listItems(element); int index = startIndex + 1; while (index >= 0 && (unsigned) index < items.size() && (!isOptionElement(items[index]) || items[index]->disabled())) ++index; if ((unsigned) index == items.size()) return startIndex; return index; } int SelectElement::previousSelectableListIndex(SelectElementData& data, Element* element, int startIndex) { const Vector& items = data.listItems(element); if (startIndex == -1) startIndex = items.size(); int index = startIndex - 1; while (index >= 0 && (unsigned) index < items.size() && (!isOptionElement(items[index]) || items[index]->disabled())) --index; if (index == -1) return startIndex; return index; } void SelectElement::setActiveSelectionAnchorIndex(SelectElementData& data, Element* element, int index) { data.setActiveSelectionAnchorIndex(index); // Cache the selection state so we can restore the old selection as the new selection pivots around this anchor index Vector& cachedStateForActiveSelection = data.cachedStateForActiveSelection(); cachedStateForActiveSelection.clear(); const Vector& items = data.listItems(element); for (unsigned i = 0; i < items.size(); ++i) { OptionElement* optionElement = toOptionElement(items[i]); cachedStateForActiveSelection.append(optionElement && optionElement->selected()); } } void SelectElement::setActiveSelectionEndIndex(SelectElementData& data, int index) { data.setActiveSelectionEndIndex(index); } void SelectElement::updateListBoxSelection(SelectElementData& data, Element* element, bool deselectOtherOptions) { ASSERT(element->renderer() && (element->renderer()->isListBox() || data.multiple())); ASSERT(!data.listItems(element).size() || data.activeSelectionAnchorIndex() >= 0); unsigned start = min(data.activeSelectionAnchorIndex(), data.activeSelectionEndIndex()); unsigned end = max(data.activeSelectionAnchorIndex(), data.activeSelectionEndIndex()); Vector& cachedStateForActiveSelection = data.cachedStateForActiveSelection(); const Vector& items = data.listItems(element); for (unsigned i = 0; i < items.size(); ++i) { OptionElement* optionElement = toOptionElement(items[i]); if (!optionElement || items[i]->disabled()) continue; if (i >= start && i <= end) optionElement->setSelectedState(data.activeSelectionState()); else if (deselectOtherOptions || i >= cachedStateForActiveSelection.size()) optionElement->setSelectedState(false); else optionElement->setSelectedState(cachedStateForActiveSelection[i]); } scrollToSelection(data, element); } void SelectElement::listBoxOnChange(SelectElementData& data, Element* element) { ASSERT(!data.usesMenuList() || data.multiple()); Vector& lastOnChangeSelection = data.lastOnChangeSelection(); const Vector& items = data.listItems(element); // If the cached selection list is empty, or the size has changed, then fire dispatchFormControlChangeEvent, and return early. if (lastOnChangeSelection.isEmpty() || lastOnChangeSelection.size() != items.size()) { element->dispatchFormControlChangeEvent(); return; } // Update lastOnChangeSelection and fire dispatchFormControlChangeEvent bool fireOnChange = false; for (unsigned i = 0; i < items.size(); ++i) { OptionElement* optionElement = toOptionElement(items[i]); bool selected = optionElement && optionElement->selected(); if (selected != lastOnChangeSelection[i]) fireOnChange = true; lastOnChangeSelection[i] = selected; } if (fireOnChange) element->dispatchFormControlChangeEvent(); } void SelectElement::menuListOnChange(SelectElementData& data, Element* element) { ASSERT(data.usesMenuList()); int selected = selectedIndex(data, element); if (data.lastOnChangeIndex() != selected && data.userDrivenChange()) { data.setLastOnChangeIndex(selected); data.setUserDrivenChange(false); element->dispatchFormControlChangeEvent(); } } void SelectElement::scrollToSelection(SelectElementData& data, Element* element) { if (data.usesMenuList()) return; if (RenderObject* renderer = element->renderer()) toRenderListBox(renderer)->selectionChanged(); } void SelectElement::setOptionsChangedOnRenderer(SelectElementData& data, Element* element) { if (RenderObject* renderer = element->renderer()) { if (data.usesMenuList()) toRenderMenuList(renderer)->setOptionsChanged(true); else toRenderListBox(renderer)->setOptionsChanged(true); } } void SelectElement::setRecalcListItems(SelectElementData& data, Element* element) { data.setShouldRecalcListItems(true); data.setActiveSelectionAnchorIndex(-1); // Manual selection anchor is reset when manipulating the select programmatically. setOptionsChangedOnRenderer(data, element); element->setNeedsStyleRecalc(); } void SelectElement::recalcListItems(SelectElementData& data, const Element* element, bool updateSelectedStates) { Vector& listItems = data.rawListItems(); listItems.clear(); data.setShouldRecalcListItems(false); OptionElement* foundSelected = 0; for (Node* currentNode = element->firstChild(); currentNode;) { if (!currentNode->isElementNode()) { currentNode = currentNode->traverseNextSibling(element); continue; } Element* current = static_cast(currentNode); // 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 (isOptionGroupElement(current)) { listItems.append(current); if (current->firstChild()) { currentNode = current->firstChild(); continue; } } if (OptionElement* optionElement = toOptionElement(current)) { listItems.append(current); if (updateSelectedStates && !data.multiple()) { if (!foundSelected && (data.size() <= 1 || optionElement->selected())) { foundSelected = optionElement; foundSelected->setSelectedState(true); } else if (foundSelected && optionElement->selected()) { foundSelected->setSelectedState(false); foundSelected = optionElement; } } } if (current->hasTagName(HTMLNames::hrTag)) listItems.append(current); // In conforming HTML code, only and