/* * 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 "Chrome.h" #include "ChromeClient.h" #include "Element.h" #include "EventHandler.h" #include "EventNames.h" #include "FormDataList.h" #include "Frame.h" #include "HTMLFormElement.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 #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; enum SkipDirection { SkipBackwards = -1, SkipForwards = 1 }; // 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. static int nextValidIndex(const Vector& listItems, int listIndex, SkipDirection direction, int skip) { ASSERT(direction == -1 || direction == 1); int lastGoodIndex = listIndex; int size = listItems.size(); for (listIndex += direction; listIndex >= 0 && listIndex < size; listIndex += direction) { --skip; if (!listItems[listIndex]->disabled() && isOptionElement(listItems[listIndex])) { lastGoodIndex = listIndex; if (skip <= 0) break; } } return lastGoodIndex; } static int nextSelectableListIndex(SelectElementData& data, Element* element, int startIndex) { return nextValidIndex(data.listItems(element), startIndex, SkipForwards, 1); } static int previousSelectableListIndex(SelectElementData& data, Element* element, int startIndex) { if (startIndex == -1) startIndex = data.listItems(element).size(); return nextValidIndex(data.listItems(element), startIndex, SkipBackwards, 1); } static int firstSelectableListIndex(SelectElementData& data, Element* element) { const Vector& items = data.listItems(element); int index = nextValidIndex(items, items.size(), SkipBackwards, INT_MAX); if (static_cast(index) == items.size()) return -1; return index; } static int lastSelectableListIndex(SelectElementData& data, Element* element) { return nextValidIndex(data.listItems(element), -1, SkipForwards, INT_MAX); } // Returns the index of the next valid item one page away from |startIndex| in direction |direction|. static int nextSelectableListIndexPageAway(SelectElementData& data, Element* element, int startIndex, SkipDirection direction) { const Vector& items = data.listItems(element); // Can't use data->size() because renderer forces a minimum size. int pageSize = 0; if (element->renderer()->isListBox()) pageSize = toRenderListBox(element->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(items, edgeIndex, direction, skipAmount); } 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()); } } 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]); } toSelectElement(element)->updateValidity(); 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