| /* |
| * 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 <wtf/Assertions.h> |
| #include <wtf/unicode/CharacterNames.h> |
| |
| #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<Element*>& 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<Element*>& items = data.listItems(element); |
| int index = nextValidIndex(items, items.size(), SkipBackwards, INT_MAX); |
| if (static_cast<unsigned>(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<Element*>& 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<bool>& lastOnChangeSelection = data.lastOnChangeSelection(); |
| lastOnChangeSelection.clear(); |
| |
| const Vector<Element*>& 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<bool>& cachedStateForActiveSelection = data.cachedStateForActiveSelection(); |
| cachedStateForActiveSelection.clear(); |
| |
| const Vector<Element*>& 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<bool>& cachedStateForActiveSelection = data.cachedStateForActiveSelection(); |
| |
| const Vector<Element*>& 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<bool>& lastOnChangeSelection = data.lastOnChangeSelection(); |
| const Vector<Element*>& 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<Element*>& 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<Element*>(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 <optgroup> and <option> will be found |
| // within a <select>. We call traverseNextSibling so that we only step |
| // into those tags that we choose to. For web-compat, we should cope |
| // with the case where odd tags like a <div> have been added but we |
| // handle this because such tags have already been removed from the |
| // <select>'s subtree at this point. |
| currentNode = currentNode->traverseNextSibling(element); |
| } |
| } |
| |
| int SelectElement::selectedIndex(const SelectElementData& data, const Element* element) |
| { |
| unsigned index = 0; |
| |
| // return the number of the first option selected |
| const Vector<Element*>& items = data.listItems(element); |
| for (size_t i = 0; i < items.size(); ++i) { |
| if (OptionElement* optionElement = toOptionElement(items[i])) { |
| if (optionElement->selected()) |
| return index; |
| ++index; |
| } |
| } |
| |
| return -1; |
| } |
| |
| void SelectElement::setSelectedIndex(SelectElementData& data, Element* element, int optionIndex, bool deselect, bool fireOnChangeNow, bool userDrivenChange) |
| { |
| if (optionIndex == -1 && !deselect && !data.multiple()) |
| optionIndex = nextSelectableListIndex(data, element, -1); |
| if (!data.multiple()) |
| deselect = true; |
| |
| const Vector<Element*>& items = data.listItems(element); |
| int listIndex = optionToListIndex(data, element, optionIndex); |
| |
| Element* excludeElement = 0; |
| if (OptionElement* optionElement = (listIndex >= 0 ? toOptionElement(items[listIndex]) : 0)) { |
| excludeElement = items[listIndex]; |
| if (data.activeSelectionAnchorIndex() < 0 || deselect) |
| setActiveSelectionAnchorIndex(data, element, listIndex); |
| if (data.activeSelectionEndIndex() < 0 || deselect) |
| setActiveSelectionEndIndex(data, listIndex); |
| optionElement->setSelectedState(true); |
| } |
| |
| if (deselect) |
| deselectItems(data, element, excludeElement); |
| |
| // For the menu list case, this is what makes the selected element appear. |
| if (RenderObject* renderer = element->renderer()) |
| renderer->updateFromElement(); |
| |
| scrollToSelection(data, element); |
| |
| // This only gets called with fireOnChangeNow for menu lists. |
| if (data.usesMenuList()) { |
| data.setUserDrivenChange(userDrivenChange); |
| if (fireOnChangeNow) |
| menuListOnChange(data, element); |
| RenderObject* renderer = element->renderer(); |
| if (renderer) { |
| if (data.usesMenuList()) |
| toRenderMenuList(renderer)->didSetSelectedIndex(); |
| else if (renderer->isListBox()) |
| toRenderListBox(renderer)->selectionChanged(); |
| } |
| } |
| |
| if (Frame* frame = element->document()->frame()) |
| frame->page()->chrome()->client()->formStateDidChange(element); |
| } |
| |
| int SelectElement::optionToListIndex(const SelectElementData& data, const Element* element, int optionIndex) |
| { |
| const Vector<Element*>& items = data.listItems(element); |
| int listSize = (int) items.size(); |
| if (optionIndex < 0 || optionIndex >= listSize) |
| return -1; |
| |
| int optionIndex2 = -1; |
| for (int listIndex = 0; listIndex < listSize; ++listIndex) { |
| if (isOptionElement(items[listIndex])) { |
| ++optionIndex2; |
| if (optionIndex2 == optionIndex) |
| return listIndex; |
| } |
| } |
| |
| return -1; |
| } |
| |
| int SelectElement::listToOptionIndex(const SelectElementData& data, const Element* element, int listIndex) |
| { |
| const Vector<Element*>& items = data.listItems(element); |
| if (listIndex < 0 || listIndex >= int(items.size()) || |
| !isOptionElement(items[listIndex])) |
| return -1; |
| |
| int optionIndex = 0; // actual index of option not counting OPTGROUP entries that may be in list |
| for (int i = 0; i < listIndex; ++i) |
| if (isOptionElement(items[i])) |
| ++optionIndex; |
| |
| return optionIndex; |
| } |
| |
| void SelectElement::dispatchFocusEvent(SelectElementData& data, Element* element) |
| { |
| // Save the selection so it can be compared to the new selection when dispatching change events during blur event dispatchal |
| if (data.usesMenuList()) |
| saveLastSelection(data, element); |
| } |
| |
| void SelectElement::dispatchBlurEvent(SelectElementData& data, Element* element) |
| { |
| // We only need to fire change events here for menu lists, because we fire change events for list boxes whenever the selection change is actually made. |
| // This matches other browsers' behavior. |
| if (data.usesMenuList()) |
| menuListOnChange(data, element); |
| } |
| |
| void SelectElement::deselectItems(SelectElementData& data, Element* element, Element* excludeElement) |
| { |
| const Vector<Element*>& items = data.listItems(element); |
| for (unsigned i = 0; i < items.size(); ++i) { |
| if (items[i] == excludeElement) |
| continue; |
| |
| if (OptionElement* optionElement = toOptionElement(items[i])) |
| optionElement->setSelectedState(false); |
| } |
| } |
| |
| bool SelectElement::saveFormControlState(const SelectElementData& data, const Element* element, String& value) |
| { |
| const Vector<Element*>& items = data.listItems(element); |
| int length = items.size(); |
| |
| // FIXME: Change this code to use the new StringImpl::createUninitialized code path. |
| Vector<char, 1024> characters(length); |
| for (int i = 0; i < length; ++i) { |
| OptionElement* optionElement = toOptionElement(items[i]); |
| bool selected = optionElement && optionElement->selected(); |
| characters[i] = selected ? 'X' : '.'; |
| } |
| |
| value = String(characters.data(), length); |
| return true; |
| } |
| |
| void SelectElement::restoreFormControlState(SelectElementData& data, Element* element, const String& state) |
| { |
| recalcListItems(data, element); |
| |
| const Vector<Element*>& items = data.listItems(element); |
| int length = items.size(); |
| |
| for (int i = 0; i < length; ++i) { |
| if (OptionElement* optionElement = toOptionElement(items[i])) |
| optionElement->setSelectedState(state[i] == 'X'); |
| } |
| |
| setOptionsChangedOnRenderer(data, element); |
| } |
| |
| void SelectElement::parseMultipleAttribute(SelectElementData& data, Element* element, Attribute* attribute) |
| { |
| bool oldUsesMenuList = data.usesMenuList(); |
| data.setMultiple(!attribute->isNull()); |
| toSelectElement(element)->updateValidity(); |
| if (oldUsesMenuList != data.usesMenuList() && element->attached()) { |
| element->detach(); |
| element->attach(); |
| } |
| } |
| |
| bool SelectElement::appendFormData(SelectElementData& data, Element* element, FormDataList& list) |
| { |
| const AtomicString& name = element->formControlName(); |
| if (name.isEmpty()) |
| return false; |
| |
| bool successful = false; |
| const Vector<Element*>& items = data.listItems(element); |
| |
| for (unsigned i = 0; i < items.size(); ++i) { |
| OptionElement* optionElement = toOptionElement(items[i]); |
| if (optionElement && optionElement->selected() && !optionElement->disabled()) { |
| list.appendData(name, optionElement->value()); |
| successful = true; |
| } |
| } |
| |
| // It's possible that this is a menulist with multiple options and nothing |
| // will be submitted (!successful). We won't send a unselected non-disabled |
| // option as fallback. This behavior matches to other browsers. |
| return successful; |
| } |
| |
| void SelectElement::reset(SelectElementData& data, Element* element) |
| { |
| OptionElement* firstOption = 0; |
| OptionElement* selectedOption = 0; |
| |
| const Vector<Element*>& items = data.listItems(element); |
| for (unsigned i = 0; i < items.size(); ++i) { |
| OptionElement* optionElement = toOptionElement(items[i]); |
| if (!optionElement) |
| continue; |
| |
| if (items[i]->fastHasAttribute(HTMLNames::selectedAttr)) { |
| if (selectedOption && !data.multiple()) |
| selectedOption->setSelectedState(false); |
| optionElement->setSelectedState(true); |
| selectedOption = optionElement; |
| } else |
| optionElement->setSelectedState(false); |
| |
| if (!firstOption) |
| firstOption = optionElement; |
| } |
| |
| if (!selectedOption && firstOption && !data.multiple() && data.size() <= 1) |
| firstOption->setSelectedState(true); |
| |
| setOptionsChangedOnRenderer(data, element); |
| element->setNeedsStyleRecalc(); |
| } |
| |
| void SelectElement::menuListDefaultEventHandler(SelectElementData& data, Element* element, Event* event, HTMLFormElement* htmlForm) |
| { |
| if (event->type() == eventNames().keydownEvent) { |
| if (!element->renderer() || !event->isKeyboardEvent()) |
| return; |
| |
| const String& keyIdentifier = static_cast<KeyboardEvent*>(event)->keyIdentifier(); |
| bool handled = false; |
| |
| #if ARROW_KEYS_POP_MENU |
| if (!isSpatialNavigationEnabled(element->document()->frame())) { |
| if (keyIdentifier == "Down" || keyIdentifier == "Up") { |
| element->focus(); |
| |
| if (!element->renderer()) // Calling focus() may cause us to lose our renderer, in which case do not want to handle the event. |
| return; |
| |
| // Save the selection so it can be compared to the new selection when dispatching change events during setSelectedIndex, |
| // which gets called from RenderMenuList::valueChanged, which gets called after the user makes a selection from the menu. |
| saveLastSelection(data, element); |
| if (RenderMenuList* menuList = toRenderMenuList(element->renderer())) |
| menuList->showPopup(); |
| |
| event->setDefaultHandled(); |
| } |
| return; |
| } |
| #endif |
| // When using spatial navigation, we want to be able to navigate away from the select element |
| // when the user hits any of the arrow keys, instead of changing the selection. |
| if (isSpatialNavigationEnabled(element->document()->frame())) |
| if (!data.activeSelectionState()) |
| return; |
| |
| UNUSED_PARAM(htmlForm); |
| const Vector<Element*>& listItems = data.listItems(element); |
| |
| int listIndex = optionToListIndex(data, element, selectedIndex(data, element)); |
| if (keyIdentifier == "Down" || keyIdentifier == "Right") { |
| listIndex = nextValidIndex(listItems, listIndex, SkipForwards, 1); |
| handled = true; |
| } else if (keyIdentifier == "Up" || keyIdentifier == "Left") { |
| listIndex = nextValidIndex(listItems, listIndex, SkipBackwards, 1); |
| handled = true; |
| } else if (keyIdentifier == "PageDown") { |
| listIndex = nextValidIndex(listItems, listIndex, SkipForwards, 3); |
| handled = true; |
| } else if (keyIdentifier == "PageUp") { |
| listIndex = nextValidIndex(listItems, listIndex, SkipBackwards, 3); |
| handled = true; |
| } else if (keyIdentifier == "Home") { |
| listIndex = nextValidIndex(listItems, -1, SkipForwards, 1); |
| handled = true; |
| } else if (keyIdentifier == "End") { |
| listIndex = nextValidIndex(listItems, listItems.size(), SkipBackwards, 1); |
| handled = true; |
| } |
| |
| if (handled && listIndex >= 0 && static_cast<unsigned>(listIndex) < listItems.size()) |
| setSelectedIndex(data, element, listToOptionIndex(data, element, listIndex)); |
| |
| if (handled) |
| event->setDefaultHandled(); |
| } |
| |
| // Use key press event here since sending simulated mouse events |
| // on key down blocks the proper sending of the key press event. |
| if (event->type() == eventNames().keypressEvent) { |
| if (!element->renderer() || !event->isKeyboardEvent()) |
| return; |
| |
| int keyCode = static_cast<KeyboardEvent*>(event)->keyCode(); |
| bool handled = false; |
| |
| if (keyCode == ' ' && isSpatialNavigationEnabled(element->document()->frame())) { |
| // Use space to toggle arrow key handling for selection change or spatial navigation. |
| data.setActiveSelectionState(!data.activeSelectionState()); |
| event->setDefaultHandled(); |
| return; |
| } |
| |
| #if SPACE_OR_RETURN_POP_MENU |
| if (keyCode == ' ' || keyCode == '\r') { |
| element->focus(); |
| |
| if (!element->renderer()) // Calling focus() may cause us to lose our renderer, in which case do not want to handle the event. |
| return; |
| |
| // Save the selection so it can be compared to the new selection when dispatching change events during setSelectedIndex, |
| // which gets called from RenderMenuList::valueChanged, which gets called after the user makes a selection from the menu. |
| saveLastSelection(data, element); |
| if (RenderMenuList* menuList = toRenderMenuList(element->renderer())) |
| menuList->showPopup(); |
| handled = true; |
| } |
| #elif ARROW_KEYS_POP_MENU |
| if (keyCode == ' ') { |
| element->focus(); |
| |
| if (!element->renderer()) // Calling focus() may cause us to lose our renderer, in which case do not want to handle the event. |
| return; |
| |
| // Save the selection so it can be compared to the new selection when dispatching change events during setSelectedIndex, |
| // which gets called from RenderMenuList::valueChanged, which gets called after the user makes a selection from the menu. |
| saveLastSelection(data, element); |
| if (RenderMenuList* menuList = toRenderMenuList(element->renderer())) |
| menuList->showPopup(); |
| handled = true; |
| } else if (keyCode == '\r') { |
| if (htmlForm) |
| htmlForm->submitImplicitly(event, false); |
| menuListOnChange(data, element); |
| handled = true; |
| } |
| #else |
| int listIndex = optionToListIndex(data, element, selectedIndex(data, element)); |
| if (keyCode == '\r') { |
| // listIndex should already be selected, but this will fire the onchange handler. |
| setSelectedIndex(data, element, listToOptionIndex(data, element, listIndex), true, true); |
| handled = true; |
| } |
| #endif |
| if (handled) |
| event->setDefaultHandled(); |
| } |
| |
| if (event->type() == eventNames().mousedownEvent && event->isMouseEvent() && static_cast<MouseEvent*>(event)->button() == LeftButton) { |
| element->focus(); |
| if (element->renderer() && element->renderer()->isMenuList()) { |
| if (RenderMenuList* menuList = toRenderMenuList(element->renderer())) { |
| if (menuList->popupIsVisible()) |
| menuList->hidePopup(); |
| else { |
| // Save the selection so it can be compared to the new selection when we call onChange during setSelectedIndex, |
| // which gets called from RenderMenuList::valueChanged, which gets called after the user makes a selection from the menu. |
| saveLastSelection(data, element); |
| menuList->showPopup(); |
| } |
| } |
| } |
| event->setDefaultHandled(); |
| } |
| } |
| |
| void SelectElement::updateSelectedState(SelectElementData& data, Element* element, int listIndex, |
| bool multi, bool shift) |
| { |
| ASSERT(listIndex >= 0); |
| |
| // Save the selection so it can be compared to the new selection when dispatching change events during mouseup, or after autoscroll finishes. |
| saveLastSelection(data, element); |
| |
| data.setActiveSelectionState(true); |
| |
| bool shiftSelect = data.multiple() && shift; |
| bool multiSelect = data.multiple() && multi && !shift; |
| |
| Element* clickedElement = data.listItems(element)[listIndex]; |
| OptionElement* option = toOptionElement(clickedElement); |
| if (option) { |
| // Keep track of whether an active selection (like during drag selection), should select or deselect |
| if (option->selected() && multi) |
| data.setActiveSelectionState(false); |
| |
| if (!data.activeSelectionState()) |
| option->setSelectedState(false); |
| } |
| |
| // If we're not in any special multiple selection mode, then deselect all other items, excluding the clicked option. |
| // If no option was clicked, then this will deselect all items in the list. |
| if (!shiftSelect && !multiSelect) |
| deselectItems(data, element, clickedElement); |
| |
| // If the anchor hasn't been set, and we're doing a single selection or a shift selection, then initialize the anchor to the first selected index. |
| if (data.activeSelectionAnchorIndex() < 0 && !multiSelect) |
| setActiveSelectionAnchorIndex(data, element, selectedIndex(data, element)); |
| |
| // Set the selection state of the clicked option |
| if (option && !clickedElement->disabled()) |
| option->setSelectedState(true); |
| |
| // If there was no selectedIndex() for the previous initialization, or |
| // If we're doing a single selection, or a multiple selection (using cmd or ctrl), then initialize the anchor index to the listIndex that just got clicked. |
| if (data.activeSelectionAnchorIndex() < 0 || !shiftSelect) |
| setActiveSelectionAnchorIndex(data, element, listIndex); |
| |
| setActiveSelectionEndIndex(data, listIndex); |
| updateListBoxSelection(data, element, !multiSelect); |
| } |
| |
| void SelectElement::listBoxDefaultEventHandler(SelectElementData& data, Element* element, Event* event, HTMLFormElement* htmlForm) |
| { |
| const Vector<Element*>& listItems = data.listItems(element); |
| |
| if (event->type() == eventNames().mousedownEvent && event->isMouseEvent() && static_cast<MouseEvent*>(event)->button() == LeftButton) { |
| element->focus(); |
| |
| if (!element->renderer()) // Calling focus() may cause us to lose our renderer, in which case do not want to handle the event. |
| return; |
| |
| // Convert to coords relative to the list box if needed. |
| MouseEvent* mouseEvent = static_cast<MouseEvent*>(event); |
| IntPoint localOffset = roundedIntPoint(element->renderer()->absoluteToLocal(mouseEvent->absoluteLocation(), false, true)); |
| int listIndex = toRenderListBox(element->renderer())->listIndexAtOffset(localOffset.x(), localOffset.y()); |
| if (listIndex >= 0) { |
| #if PLATFORM(MAC) || (PLATFORM(CHROMIUM) && OS(DARWIN)) |
| updateSelectedState(data, element, listIndex, mouseEvent->metaKey(), mouseEvent->shiftKey()); |
| #else |
| updateSelectedState(data, element, listIndex, mouseEvent->ctrlKey(), mouseEvent->shiftKey()); |
| #endif |
| if (Frame* frame = element->document()->frame()) |
| frame->eventHandler()->setMouseDownMayStartAutoscroll(); |
| |
| event->setDefaultHandled(); |
| } |
| } else if (event->type() == eventNames().mouseupEvent && event->isMouseEvent() && static_cast<MouseEvent*>(event)->button() == LeftButton && element->document()->frame()->eventHandler()->autoscrollRenderer() != element->renderer()) { |
| // This makes sure we fire dispatchFormControlChangeEvent for a single click. For drag selection, onChange will fire when the autoscroll timer stops. |
| listBoxOnChange(data, element); |
| } else if (event->type() == eventNames().keydownEvent) { |
| if (!event->isKeyboardEvent()) |
| return; |
| const String& keyIdentifier = static_cast<KeyboardEvent*>(event)->keyIdentifier(); |
| |
| bool handled = false; |
| int endIndex = 0; |
| if (data.activeSelectionEndIndex() < 0) { |
| // Initialize the end index |
| if (keyIdentifier == "Down" || keyIdentifier == "PageDown") { |
| int startIndex = lastSelectedListIndex(data, element); |
| handled = true; |
| if (keyIdentifier == "Down") |
| endIndex = nextSelectableListIndex(data, element, startIndex); |
| else |
| endIndex = nextSelectableListIndexPageAway(data, element, startIndex, SkipForwards); |
| } else if (keyIdentifier == "Up" || keyIdentifier == "PageUp") { |
| int startIndex = optionToListIndex(data, element, selectedIndex(data, element)); |
| handled = true; |
| if (keyIdentifier == "Up") |
| endIndex = previousSelectableListIndex(data, element, startIndex); |
| else |
| endIndex = nextSelectableListIndexPageAway(data, element, startIndex, SkipBackwards); |
| } |
| } else { |
| // Set the end index based on the current end index |
| if (keyIdentifier == "Down") { |
| endIndex = nextSelectableListIndex(data, element, data.activeSelectionEndIndex()); |
| handled = true; |
| } else if (keyIdentifier == "Up") { |
| endIndex = previousSelectableListIndex(data, element, data.activeSelectionEndIndex()); |
| handled = true; |
| } else if (keyIdentifier == "PageDown") { |
| endIndex = nextSelectableListIndexPageAway(data, element, data.activeSelectionEndIndex(), SkipForwards); |
| handled = true; |
| } else if (keyIdentifier == "PageUp") { |
| endIndex = nextSelectableListIndexPageAway(data, element, data.activeSelectionEndIndex(), SkipBackwards); |
| handled = true; |
| } |
| } |
| if (keyIdentifier == "Home") { |
| endIndex = firstSelectableListIndex(data, element); |
| handled = true; |
| } else if (keyIdentifier == "End") { |
| endIndex = lastSelectableListIndex(data, element); |
| handled = true; |
| } |
| |
| if (isSpatialNavigationEnabled(element->document()->frame())) |
| // Check if the selection moves to the boundary. |
| if (keyIdentifier == "Left" || keyIdentifier == "Right" || ((keyIdentifier == "Down" || keyIdentifier == "Up") && endIndex == data.activeSelectionEndIndex())) |
| return; |
| |
| if (endIndex >= 0 && handled) { |
| // Save the selection so it can be compared to the new selection when dispatching change events immediately after making the new selection. |
| saveLastSelection(data, element); |
| |
| ASSERT_UNUSED(listItems, !listItems.size() || (endIndex >= 0 && static_cast<unsigned>(endIndex) < listItems.size())); |
| setActiveSelectionEndIndex(data, endIndex); |
| |
| bool selectNewItem = !data.multiple() || static_cast<KeyboardEvent*>(event)->shiftKey() || !isSpatialNavigationEnabled(element->document()->frame()); |
| if (selectNewItem) |
| data.setActiveSelectionState(true); |
| // If the anchor is unitialized, or if we're going to deselect all other options, then set the anchor index equal to the end index. |
| bool deselectOthers = !data.multiple() || (!static_cast<KeyboardEvent*>(event)->shiftKey() && selectNewItem); |
| if (data.activeSelectionAnchorIndex() < 0 || deselectOthers) { |
| if (deselectOthers) |
| deselectItems(data, element); |
| setActiveSelectionAnchorIndex(data, element, data.activeSelectionEndIndex()); |
| } |
| |
| toRenderListBox(element->renderer())->scrollToRevealElementAtListIndex(endIndex); |
| if (selectNewItem) { |
| updateListBoxSelection(data, element, deselectOthers); |
| listBoxOnChange(data, element); |
| } else |
| scrollToSelection(data, element); |
| |
| event->setDefaultHandled(); |
| } |
| } else if (event->type() == eventNames().keypressEvent) { |
| if (!event->isKeyboardEvent()) |
| return; |
| int keyCode = static_cast<KeyboardEvent*>(event)->keyCode(); |
| |
| if (keyCode == '\r') { |
| if (htmlForm) |
| htmlForm->submitImplicitly(event, false); |
| event->setDefaultHandled(); |
| } else if (data.multiple() && keyCode == ' ' && isSpatialNavigationEnabled(element->document()->frame())) { |
| // Use space to toggle selection change. |
| data.setActiveSelectionState(!data.activeSelectionState()); |
| updateSelectedState(data, element, listToOptionIndex(data, element, data.activeSelectionEndIndex()), true /*multi*/, false /*shift*/); |
| listBoxOnChange(data, element); |
| event->setDefaultHandled(); |
| } |
| } |
| } |
| |
| void SelectElement::defaultEventHandler(SelectElementData& data, Element* element, Event* event, HTMLFormElement* htmlForm) |
| { |
| if (!element->renderer()) |
| return; |
| |
| if (data.usesMenuList()) |
| menuListDefaultEventHandler(data, element, event, htmlForm); |
| else |
| listBoxDefaultEventHandler(data, element, event, htmlForm); |
| |
| if (event->defaultHandled()) |
| return; |
| |
| if (event->type() == eventNames().keypressEvent && event->isKeyboardEvent()) { |
| KeyboardEvent* keyboardEvent = static_cast<KeyboardEvent*>(event); |
| if (!keyboardEvent->ctrlKey() && !keyboardEvent->altKey() && !keyboardEvent->metaKey() && isPrintableChar(keyboardEvent->charCode())) { |
| typeAheadFind(data, element, keyboardEvent); |
| event->setDefaultHandled(); |
| return; |
| } |
| } |
| } |
| |
| int SelectElement::lastSelectedListIndex(const SelectElementData& data, const Element* element) |
| { |
| // return the number of the last option selected |
| unsigned index = 0; |
| bool found = false; |
| const Vector<Element*>& items = data.listItems(element); |
| for (size_t i = 0; i < items.size(); ++i) { |
| if (OptionElement* optionElement = toOptionElement(items[i])) { |
| if (optionElement->selected()) { |
| index = i; |
| found = true; |
| } |
| } |
| } |
| |
| return found ? (int) index : -1; |
| } |
| |
| static String stripLeadingWhiteSpace(const String& string) |
| { |
| int length = string.length(); |
| |
| int i; |
| for (i = 0; i < length; ++i) { |
| if (string[i] != noBreakSpace && (string[i] <= 0x7F ? !isASCIISpace(string[i]) : (direction(string[i]) != WhiteSpaceNeutral))) |
| break; |
| } |
| |
| return string.substring(i, length - i); |
| } |
| |
| void SelectElement::typeAheadFind(SelectElementData& data, Element* element, KeyboardEvent* event) |
| { |
| if (event->timeStamp() < data.lastCharTime()) |
| return; |
| |
| DOMTimeStamp delta = event->timeStamp() - data.lastCharTime(); |
| data.setLastCharTime(event->timeStamp()); |
| |
| UChar c = event->charCode(); |
| |
| String prefix; |
| int searchStartOffset = 1; |
| if (delta > typeAheadTimeout) { |
| prefix = String(&c, 1); |
| data.setTypedString(prefix); |
| data.setRepeatingChar(c); |
| } else { |
| data.typedString().append(c); |
| |
| if (c == data.repeatingChar()) |
| // The user is likely trying to cycle through all the items starting with this character, so just search on the character |
| prefix = String(&c, 1); |
| else { |
| data.setRepeatingChar(0); |
| prefix = data.typedString(); |
| searchStartOffset = 0; |
| } |
| } |
| |
| const Vector<Element*>& items = data.listItems(element); |
| int itemCount = items.size(); |
| if (itemCount < 1) |
| return; |
| |
| int selected = selectedIndex(data, element); |
| int index = (optionToListIndex(data, element, selected >= 0 ? selected : 0) + searchStartOffset) % itemCount; |
| ASSERT(index >= 0); |
| |
| // Compute a case-folded copy of the prefix string before beginning the search for |
| // a matching element. This code uses foldCase to work around the fact that |
| // String::startWith does not fold non-ASCII characters. This code can be changed |
| // to use startWith once that is fixed. |
| String prefixWithCaseFolded(prefix.foldCase()); |
| for (int i = 0; i < itemCount; ++i, index = (index + 1) % itemCount) { |
| OptionElement* optionElement = toOptionElement(items[index]); |
| if (!optionElement || items[index]->disabled()) |
| continue; |
| |
| // Fold the option string and check if its prefix is equal to the folded prefix. |
| String text = optionElement->textIndentedToRespectGroupLabel(); |
| if (stripLeadingWhiteSpace(text).foldCase().startsWith(prefixWithCaseFolded)) { |
| setSelectedIndex(data, element, listToOptionIndex(data, element, index)); |
| if (!data.usesMenuList()) |
| listBoxOnChange(data, element); |
| |
| setOptionsChangedOnRenderer(data, element); |
| element->setNeedsStyleRecalc(); |
| return; |
| } |
| } |
| } |
| |
| void SelectElement::insertedIntoTree(SelectElementData& data, Element* element) |
| { |
| // When the element is created during document parsing, it won't have any items yet - but for innerHTML |
| // and related methods, this method is called after the whole subtree is constructed. |
| recalcListItems(data, element, true); |
| } |
| |
| void SelectElement::accessKeySetSelectedIndex(SelectElementData& data, Element* element, int index) |
| { |
| // first bring into focus the list box |
| if (!element->focused()) |
| element->accessKeyAction(false); |
| |
| // if this index is already selected, unselect. otherwise update the selected index |
| const Vector<Element*>& items = data.listItems(element); |
| int listIndex = optionToListIndex(data, element, index); |
| if (OptionElement* optionElement = (listIndex >= 0 ? toOptionElement(items[listIndex]) : 0)) { |
| if (optionElement->selected()) |
| optionElement->setSelectedState(false); |
| else |
| setSelectedIndex(data, element, index, false, true); |
| } |
| |
| if (data.usesMenuList()) |
| menuListOnChange(data, element); |
| else |
| listBoxOnChange(data, element); |
| |
| scrollToSelection(data, element); |
| } |
| |
| unsigned SelectElement::optionCount(const SelectElementData& data, const Element* element) |
| { |
| unsigned options = 0; |
| |
| const Vector<Element*>& items = data.listItems(element); |
| for (unsigned i = 0; i < items.size(); ++i) { |
| if (isOptionElement(items[i])) |
| ++options; |
| } |
| |
| return options; |
| } |
| |
| // SelectElementData |
| SelectElementData::SelectElementData() |
| : m_multiple(false) |
| , m_size(0) |
| , m_lastOnChangeIndex(-1) |
| , m_activeSelectionState(false) |
| , m_activeSelectionAnchorIndex(-1) |
| , m_activeSelectionEndIndex(-1) |
| , m_recalcListItems(false) |
| , m_repeatingChar(0) |
| , m_lastCharTime(0) |
| { |
| } |
| |
| SelectElementData::~SelectElementData() |
| { |
| } |
| |
| void SelectElementData::checkListItems(const Element* element) const |
| { |
| #if !ASSERT_DISABLED |
| Vector<Element*> items = m_listItems; |
| SelectElement::recalcListItems(*const_cast<SelectElementData*>(this), element, false); |
| ASSERT(items == m_listItems); |
| #else |
| UNUSED_PARAM(element); |
| #endif |
| } |
| |
| Vector<Element*>& SelectElementData::listItems(const Element* element) |
| { |
| if (m_recalcListItems) |
| SelectElement::recalcListItems(*this, element); |
| else |
| checkListItems(element); |
| |
| return m_listItems; |
| } |
| |
| const Vector<Element*>& SelectElementData::listItems(const Element* element) const |
| { |
| if (m_recalcListItems) |
| SelectElement::recalcListItems(*const_cast<SelectElementData*>(this), element); |
| else |
| checkListItems(element); |
| |
| return m_listItems; |
| } |
| |
| SelectElement* toSelectElement(Element* element) |
| { |
| if (element->isHTMLElement() && element->hasTagName(HTMLNames::selectTag)) |
| return static_cast<HTMLSelectElement*>(element); |
| |
| #if ENABLE(WML) |
| if (element->isWMLElement() && element->hasTagName(WMLNames::selectTag)) |
| return static_cast<WMLSelectElement*>(element); |
| #endif |
| |
| return 0; |
| } |
| |
| } |