From 8e35f3cfc7fba1d1c829dc557ebad6409cbe16a2 Mon Sep 17 00:00:00 2001 From: The Android Open Source Project Date: Tue, 3 Mar 2009 19:30:52 -0800 Subject: auto import from //depot/cupcake/@135843 --- WebCore/html/HTMLSelectElement.cpp | 1134 ++++++++++++++++++++++++++++++++++++ 1 file changed, 1134 insertions(+) create mode 100644 WebCore/html/HTMLSelectElement.cpp (limited to 'WebCore/html/HTMLSelectElement.cpp') diff --git a/WebCore/html/HTMLSelectElement.cpp b/WebCore/html/HTMLSelectElement.cpp new file mode 100644 index 0000000..e95bfd3 --- /dev/null +++ b/WebCore/html/HTMLSelectElement.cpp @@ -0,0 +1,1134 @@ +/* + * 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 Apple Inc. All rights reserved. + * (C) 2006 Alexey Proskuryakov (ap@nypop.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 "CSSPropertyNames.h" +#include "CSSStyleSelector.h" +#include "CharacterNames.h" +#include "Document.h" +#include "Event.h" +#include "EventHandler.h" +#include "EventNames.h" +#include "FormDataList.h" +#include "Frame.h" +#include "HTMLFormElement.h" +#include "HTMLNames.h" +#include "HTMLOptionElement.h" +#include "HTMLOptionsCollection.h" +#include "KeyboardEvent.h" +#include "MouseEvent.h" +#include "RenderListBox.h" +#include "RenderMenuList.h" +#include +#include + +#if PLATFORM(MAC) +#define ARROW_KEYS_POP_MENU 1 +#else +#define ARROW_KEYS_POP_MENU 0 +#endif + +using namespace std; +using namespace WTF; +using namespace Unicode; + +namespace WebCore { + +using namespace HTMLNames; + +static const DOMTimeStamp typeAheadTimeout = 1000; + +HTMLSelectElement::HTMLSelectElement(Document* doc, HTMLFormElement* f) + : HTMLFormControlElementWithState(selectTag, doc, f) + , m_minwidth(0) + , m_size(0) + , m_multiple(false) + , m_recalcListItems(false) + , m_lastOnChangeIndex(-1) + , m_activeSelectionAnchorIndex(-1) + , m_activeSelectionEndIndex(-1) + , m_activeSelectionState(false) + , m_repeatingChar(0) + , m_lastCharTime(0) +{ +} + +HTMLSelectElement::HTMLSelectElement(const QualifiedName& tagName, Document* doc, HTMLFormElement* f) + : HTMLFormControlElementWithState(tagName, doc, f) + , m_minwidth(0) + , m_size(0) + , m_multiple(false) + , m_recalcListItems(false) + , m_lastOnChangeIndex(-1) + , m_activeSelectionAnchorIndex(-1) + , m_activeSelectionEndIndex(-1) + , m_activeSelectionState(false) + , m_repeatingChar(0) + , m_lastCharTime(0) +{ +} + +bool HTMLSelectElement::checkDTD(const Node* newChild) +{ + // Make sure to keep in sync with this. + return newChild->isTextNode() || newChild->hasTagName(optionTag) || newChild->hasTagName(optgroupTag) || newChild->hasTagName(hrTag) || + newChild->hasTagName(scriptTag); +} + +void HTMLSelectElement::recalcStyle( StyleChange ch ) +{ + if (hasChangedChild() && renderer()) { + if (usesMenuList()) + static_cast(renderer())->setOptionsChanged(true); + else + static_cast(renderer())->setOptionsChanged(true); + } else if (m_recalcListItems) + recalcListItems(); + + HTMLFormControlElementWithState::recalcStyle(ch); +} + +const AtomicString& HTMLSelectElement::type() const +{ + static const AtomicString selectMultiple("select-multiple"); + static const AtomicString selectOne("select-one"); + return m_multiple ? selectMultiple : selectOne; +} + +int HTMLSelectElement::selectedIndex() const +{ + // return the number of the first option selected + unsigned index = 0; + const Vector& items = listItems(); + for (unsigned int i = 0; i < items.size(); i++) { + if (items[i]->hasLocalName(optionTag)) { + if (static_cast(items[i])->selected()) + return index; + index++; + } + } + return -1; +} + +int HTMLSelectElement::lastSelectedListIndex() const +{ + // return the number of the last option selected + unsigned index = 0; + bool found = false; + const Vector& items = listItems(); + for (unsigned int i = 0; i < items.size(); i++) { + if (items[i]->hasLocalName(optionTag)) { + if (static_cast(items[i])->selected()) { + index = i; + found = true; + } + } + } + return found ? (int) index : -1; +} + +void HTMLSelectElement::deselectItems(HTMLOptionElement* excludeElement) +{ + const Vector& items = listItems(); + unsigned i; + for (i = 0; i < items.size(); i++) { + if (items[i]->hasLocalName(optionTag) && (items[i] != excludeElement)) { + HTMLOptionElement* element = static_cast(items[i]); + element->setSelectedState(false); + } + } +} + +void HTMLSelectElement::setSelectedIndex(int optionIndex, bool deselect, bool fireOnChange) +{ + const Vector& items = listItems(); + int listIndex = optionToListIndex(optionIndex); + HTMLOptionElement* element = 0; + + if (!multiple()) + deselect = true; + + if (listIndex >= 0) { + if (m_activeSelectionAnchorIndex < 0 || deselect) + setActiveSelectionAnchorIndex(listIndex); + if (m_activeSelectionEndIndex < 0 || deselect) + setActiveSelectionEndIndex(listIndex); + element = static_cast(items[listIndex]); + element->setSelectedState(true); + } + + if (deselect) + deselectItems(element); + + scrollToSelection(); + + // This only gets called with fireOnChange for menu lists. + if (fireOnChange && usesMenuList()) + menuListOnChange(); +} + +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(); +} + +unsigned HTMLSelectElement::length() const +{ + unsigned len = 0; + const Vector& items = listItems(); + for (unsigned i = 0; i < items.size(); ++i) { + if (items[i]->hasLocalName(optionTag)) + ++len; + } + return len; +} + +void HTMLSelectElement::add(HTMLElement *element, HTMLElement *before, ExceptionCode& ec) +{ + RefPtr protectNewChild(element); // make sure the element is ref'd and deref'd so we don't leak it + + if (!element || !(element->hasLocalName(optionTag) || element->hasLocalName(hrTag))) + return; + + ec = 0; + insertBefore(element, before, ec); + if (!ec) + setRecalcListItems(); +} + +void HTMLSelectElement::remove(int index) +{ + ExceptionCode ec = 0; + int listIndex = optionToListIndex(index); + + const Vector& items = listItems(); + if (listIndex < 0 || index >= int(items.size())) + return; // ### what should we do ? remove the last item? + + Element *item = items[listIndex]; + ASSERT(item->parentNode()); + item->parentNode()->removeChild(item, ec); + if (!ec) + setRecalcListItems(); +} + +String HTMLSelectElement::value() +{ + unsigned i; + const Vector& items = listItems(); + for (i = 0; i < items.size(); i++) { + if (items[i]->hasLocalName(optionTag) && static_cast(items[i])->selected()) + return static_cast(items[i])->value(); + } + return String(""); +} + +void HTMLSelectElement::setValue(const String &value) +{ + if (value.isNull()) + 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 (static_cast(items[i])->value() == value) { + setSelectedIndex(optionIndex, true); + return; + } + optionIndex++; + } +} + +bool HTMLSelectElement::saveState(String& value) const +{ + const Vector& items = listItems(); + int l = items.size(); + Vector characters(l); + for (int i = 0; i < l; ++i) { + HTMLElement* e = items[i]; + bool selected = e->hasLocalName(optionTag) && static_cast(e)->selected(); + characters[i] = selected ? 'X' : '.'; + } + value = String(characters.data(), l); + return true; +} + +void HTMLSelectElement::restoreState(const String& state) +{ + recalcListItems(); + + const Vector& items = listItems(); + int l = items.size(); + for (int i = 0; i < l; i++) + if (items[i]->hasLocalName(optionTag)) + static_cast(items[i])->setSelectedState(state[i] == 'X'); + + setChanged(); +} + +bool HTMLSelectElement::insertBefore(PassRefPtr newChild, Node* refChild, ExceptionCode& ec, bool shouldLazyAttach) +{ + bool result = HTMLFormControlElementWithState::insertBefore(newChild, refChild, ec, shouldLazyAttach); + if (result) + setRecalcListItems(); + return result; +} + +bool HTMLSelectElement::replaceChild(PassRefPtr newChild, Node *oldChild, ExceptionCode& ec, bool shouldLazyAttach) +{ + bool result = HTMLFormControlElementWithState::replaceChild(newChild, oldChild, ec, shouldLazyAttach); + if (result) + setRecalcListItems(); + return result; +} + +bool HTMLSelectElement::removeChild(Node* oldChild, ExceptionCode& ec) +{ + bool result = HTMLFormControlElementWithState::removeChild(oldChild, ec); + if (result) + setRecalcListItems(); + return result; +} + +bool HTMLSelectElement::appendChild(PassRefPtr newChild, ExceptionCode& ec, bool shouldLazyAttach) +{ + bool result = HTMLFormControlElementWithState::appendChild(newChild, ec, shouldLazyAttach); + if (result) + setRecalcListItems(); + return result; +} + +bool HTMLSelectElement::removeChildren() +{ + bool result = HTMLFormControlElementWithState::removeChildren(); + if (result) + setRecalcListItems(); + return result; +} + +void HTMLSelectElement::parseMappedAttribute(MappedAttribute *attr) +{ + bool oldUsesMenuList = usesMenuList(); + if (attr->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 = attr->value().toInt(); + String attrSize = String::number(size); + if (attrSize != attr->value()) + attr->setValue(attrSize); + + m_size = max(size, 1); + if ((oldUsesMenuList != usesMenuList() || !oldUsesMenuList && m_size != oldSize) && attached()) { + detach(); + attach(); + setRecalcListItems(); + } + } else if (attr->name() == widthAttr) { + m_minwidth = max(attr->value().toInt(), 0); + } else if (attr->name() == multipleAttr) { + m_multiple = (!attr->isNull()); + if (oldUsesMenuList != usesMenuList() && attached()) { + detach(); + attach(); + } + } else if (attr->name() == accesskeyAttr) { + // FIXME: ignore for the moment + } else if (attr->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 + } else if (attr->name() == onfocusAttr) { + setInlineEventListenerForTypeAndAttribute(eventNames().focusEvent, attr); + } else if (attr->name() == onblurAttr) { + setInlineEventListenerForTypeAndAttribute(eventNames().blurEvent, attr); + } else if (attr->name() == onchangeAttr) { + setInlineEventListenerForTypeAndAttribute(eventNames().changeEvent, attr); + } else + HTMLFormControlElementWithState::parseMappedAttribute(attr); +} + +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(); +} + +void HTMLSelectElement::selectAll() +{ + ASSERT(!usesMenuList()); + if (!renderer() || !multiple()) + return; + + // Save the selection so it can be compared to the new selectAll selection when we call onChange + saveLastSelection(); + + m_activeSelectionState = true; + setActiveSelectionAnchorIndex(nextSelectableListIndex(-1)); + setActiveSelectionEndIndex(previousSelectableListIndex(-1)); + + updateListBoxSelection(false); + listBoxOnChange(); +} + +RenderObject *HTMLSelectElement::createRenderer(RenderArena *arena, RenderStyle *style) +{ + if (usesMenuList()) + return new (arena) RenderMenuList(this); + return new (arena) RenderListBox(this); +} + +bool HTMLSelectElement::appendFormData(FormDataList& list, bool) +{ + bool successful = false; + const Vector& items = listItems(); + + unsigned i; + for (i = 0; i < items.size(); i++) { + if (items[i]->hasLocalName(optionTag)) { + HTMLOptionElement *option = static_cast(items[i]); + if (option->selected()) { + list.appendData(name(), option->value()); + successful = true; + } + } + } + + // ### this case should not happen. make sure that we select the first option + // in any case. otherwise we have no consistency with the DOM interface. FIXME! + // we return the first one if it was a combobox select + if (!successful && !m_multiple && m_size <= 1 && items.size() && + (items[0]->hasLocalName(optionTag))) { + HTMLOptionElement *option = static_cast(items[0]); + if (option->value().isNull()) + list.appendData(name(), option->text().stripWhiteSpace()); + else + list.appendData(name(), option->value()); + successful = true; + } + + return successful; +} + +int HTMLSelectElement::optionToListIndex(int optionIndex) const +{ + const Vector& items = listItems(); + int listSize = (int)items.size(); + if (optionIndex < 0 || optionIndex >= listSize) + return -1; + + int optionIndex2 = -1; + for (int listIndex = 0; listIndex < listSize; listIndex++) { + if (items[listIndex]->hasLocalName(optionTag)) { + optionIndex2++; + if (optionIndex2 == optionIndex) + return listIndex; + } + } + return -1; +} + +int HTMLSelectElement::listToOptionIndex(int listIndex) const +{ + const Vector& items = listItems(); + if (listIndex < 0 || listIndex >= int(items.size()) || + !items[listIndex]->hasLocalName(optionTag)) + 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 (items[i]->hasLocalName(optionTag)) + optionIndex++; + return optionIndex; +} + +PassRefPtr HTMLSelectElement::options() +{ + return HTMLOptionsCollection::create(this); +} + +void HTMLSelectElement::recalcListItems(bool updateSelectedStates) const +{ + m_listItems.clear(); + HTMLOptionElement* foundSelected = 0; + for (Node* current = firstChild(); current; current = current->traverseNextSibling(this)) { + if (current->hasTagName(optgroupTag) && current->firstChild()) { + // FIXME: It doesn't make sense to add an optgroup to the list items, + // when it has children, but not to add it if it happens to have, + // children (say some comment nodes or text nodes), yet that's what + // this code does! + m_listItems.append(static_cast(current)); + current = current->firstChild(); + // FIXME: It doesn't make sense to handle an inside another + // if it's not the first child, but not handle it if it happens to be the first + // child, yet that's what this code does! + } + + if (current->hasTagName(optionTag)) { + m_listItems.append(static_cast(current)); + if (updateSelectedStates) { + if (!foundSelected && (usesMenuList() || (!m_multiple && static_cast(current)->selected()))) { + foundSelected = static_cast(current); + foundSelected->setSelectedState(true); + } else if (foundSelected && !m_multiple && static_cast(current)->selected()) { + foundSelected->setSelectedState(false); + foundSelected = static_cast(current); + } + } + } + if (current->hasTagName(hrTag)) + m_listItems.append(static_cast(current)); + } + m_recalcListItems = false; +} + +void HTMLSelectElement::childrenChanged(bool changedByParser, Node* beforeChange, Node* afterChange, int childCountDelta) +{ + setRecalcListItems(); + HTMLFormControlElementWithState::childrenChanged(changedByParser, beforeChange, afterChange, childCountDelta); + + if (AXObjectCache::accessibilityEnabled() && renderer()) + renderer()->document()->axObjectCache()->childrenChanged(renderer()); +} + +void HTMLSelectElement::setRecalcListItems() +{ + m_recalcListItems = true; + if (renderer()) { + if (usesMenuList()) + static_cast(renderer())->setOptionsChanged(true); + else + static_cast(renderer())->setOptionsChanged(true); + } + if (!inDocument()) + m_collectionInfo.reset(); + setChanged(); +} + +void HTMLSelectElement::reset() +{ + bool optionSelected = false; + HTMLOptionElement* firstOption = 0; + const Vector& items = listItems(); + unsigned i; + for (i = 0; i < items.size(); i++) { + if (items[i]->hasLocalName(optionTag)) { + HTMLOptionElement *option = static_cast(items[i]); + if (!option->getAttribute(selectedAttr).isNull()) { + option->setSelectedState(true); + optionSelected = true; + } else + option->setSelectedState(false); + if (!firstOption) + firstOption = option; + } + } + if (!optionSelected && firstOption && usesMenuList()) + firstOption->setSelectedState(true); + + setChanged(); +} + +void HTMLSelectElement::dispatchFocusEvent() +{ + if (usesMenuList()) + // Save the selection so it can be compared to the new selection when we call onChange during dispatchBlurEvent. + saveLastSelection(); + HTMLFormControlElementWithState::dispatchFocusEvent(); +} + +void HTMLSelectElement::dispatchBlurEvent() +{ + // We only need to fire onChange here for menu lists, because we fire onChange for list boxes whenever the selection change is actually made. + // This matches other browsers' behavior. + if (usesMenuList()) + menuListOnChange(); + HTMLFormControlElementWithState::dispatchBlurEvent(); +} + +void HTMLSelectElement::defaultEventHandler(Event* evt) +{ + if (!renderer()) + return; + + if (usesMenuList()) + menuListDefaultEventHandler(evt); + else + listBoxDefaultEventHandler(evt); + + if (evt->defaultHandled()) + return; + + if (evt->type() == eventNames().keypressEvent && evt->isKeyboardEvent()) { + KeyboardEvent* keyboardEvent = static_cast(evt); + + if (!keyboardEvent->ctrlKey() && !keyboardEvent->altKey() && !keyboardEvent->metaKey() && + isPrintableChar(keyboardEvent->charCode())) { + typeAheadFind(keyboardEvent); + evt->setDefaultHandled(); + return; + } + } + + HTMLFormControlElementWithState::defaultEventHandler(evt); +} + +void HTMLSelectElement::menuListDefaultEventHandler(Event* evt) +{ + RenderMenuList* menuList = static_cast(renderer()); + + if (evt->type() == eventNames().keydownEvent) { + if (!renderer() || !evt->isKeyboardEvent()) + return; + String keyIdentifier = static_cast(evt)->keyIdentifier(); + bool handled = false; +#if ARROW_KEYS_POP_MENU + if (keyIdentifier == "Down" || keyIdentifier == "Up") { + focus(); + // 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(); + menuList->showPopup(); + handled = true; + } +#elif defined ANDROID_KEYBOARD_NAVIGATION + if ("Enter" == keyIdentifier && usesMenuList()) { + menuList->showPopup(); + handled = true; + } +#else + int listIndex = optionToListIndex(selectedIndex()); + if (keyIdentifier == "Down" || keyIdentifier == "Right") { + int size = listItems().size(); + for (listIndex += 1; + listIndex >= 0 && listIndex < size && (listItems()[listIndex]->disabled() || !listItems()[listIndex]->hasTagName(optionTag)); + ++listIndex) { } + + if (listIndex >= 0 && listIndex < size) + setSelectedIndex(listToOptionIndex(listIndex)); + handled = true; + } else if (keyIdentifier == "Up" || keyIdentifier == "Left") { + int size = listItems().size(); + for (listIndex -= 1; + listIndex >= 0 && listIndex < size && (listItems()[listIndex]->disabled() || !listItems()[listIndex]->hasTagName(optionTag)); + --listIndex) { } + + if (listIndex >= 0 && listIndex < size) + setSelectedIndex(listToOptionIndex(listIndex)); + handled = true; + } +#endif + if (handled) + evt->setDefaultHandled(); + } + + // Use key press event here since sending simulated mouse events + // on key down blocks the proper sending of the key press event. + if (evt->type() == eventNames().keypressEvent) { + if (!renderer() || !evt->isKeyboardEvent()) + return; + int keyCode = static_cast(evt)->keyCode(); + bool handled = false; +#if ARROW_KEYS_POP_MENU + if (keyCode == ' ') { + focus(); + // 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(); + menuList->showPopup(); + handled = true; + } + if (keyCode == '\r') { + menuListOnChange(); + if (form()) + form()->submitClick(evt); + handled = true; + } +#else + int listIndex = optionToListIndex(selectedIndex()); + if (keyCode == '\r') { + // listIndex should already be selected, but this will fire the onchange handler. + setSelectedIndex(listToOptionIndex(listIndex), true, true); + handled = true; + } +#endif + if (handled) + evt->setDefaultHandled(); + } + + if (evt->type() == eventNames().mousedownEvent && evt->isMouseEvent() && static_cast(evt)->button() == LeftButton) { + focus(); + 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(); + menuList->showPopup(); + } + evt->setDefaultHandled(); + } +} + +void HTMLSelectElement::listBoxDefaultEventHandler(Event* evt) +{ + if (evt->type() == eventNames().mousedownEvent && evt->isMouseEvent() && static_cast(evt)->button() == LeftButton) { + focus(); + + MouseEvent* mEvt = static_cast(evt); + int listIndex = static_cast(renderer())->listIndexAtOffset(mEvt->offsetX(), mEvt->offsetY()); + if (listIndex >= 0) { + // Save the selection so it can be compared to the new selection when we call onChange during mouseup, or after autoscroll finishes. + saveLastSelection(); + + m_activeSelectionState = true; + + bool multiSelectKeyPressed = false; +#if PLATFORM(MAC) + multiSelectKeyPressed = mEvt->metaKey(); +#else + multiSelectKeyPressed = mEvt->ctrlKey(); +#endif + + bool shiftSelect = multiple() && mEvt->shiftKey(); + bool multiSelect = multiple() && multiSelectKeyPressed && !mEvt->shiftKey(); + + HTMLElement* clickedElement = listItems()[listIndex]; + HTMLOptionElement* option = 0; + if (clickedElement->hasLocalName(optionTag)) { + option = static_cast(clickedElement); + + // Keep track of whether an active selection (like during drag selection), should select or deselect + if (option->selected() && multiSelectKeyPressed) + m_activeSelectionState = false; + + if (!m_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(option); + + // 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 (m_activeSelectionAnchorIndex < 0 && !multiSelect) + setActiveSelectionAnchorIndex(selectedIndex()); + + // Set the selection state of the clicked option + if (option && !option->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 (listIndex >= 0 && (m_activeSelectionAnchorIndex < 0 || !shiftSelect)) + setActiveSelectionAnchorIndex(listIndex); + + setActiveSelectionEndIndex(listIndex); + updateListBoxSelection(!multiSelect); + + if (Frame* frame = document()->frame()) + frame->eventHandler()->setMouseDownMayStartAutoscroll(); + + evt->setDefaultHandled(); + } + } else if (evt->type() == eventNames().mouseupEvent && evt->isMouseEvent() && static_cast(evt)->button() == LeftButton && document()->frame()->eventHandler()->autoscrollRenderer() != renderer()) + // This makes sure we fire onChange for a single click. For drag selection, onChange will fire when the autoscroll timer stops. + listBoxOnChange(); + else if (evt->type() == eventNames().keydownEvent) { + if (!evt->isKeyboardEvent()) + return; + String keyIdentifier = static_cast(evt)->keyIdentifier(); + + int endIndex = 0; + if (m_activeSelectionEndIndex < 0) { + // Initialize the end index + if (keyIdentifier == "Down") + endIndex = nextSelectableListIndex(lastSelectedListIndex()); + else if (keyIdentifier == "Up") + endIndex = previousSelectableListIndex(optionToListIndex(selectedIndex())); + } else { + // Set the end index based on the current end index + if (keyIdentifier == "Down") + endIndex = nextSelectableListIndex(m_activeSelectionEndIndex); + else if (keyIdentifier == "Up") + endIndex = previousSelectableListIndex(m_activeSelectionEndIndex); + } + + if (keyIdentifier == "Down" || keyIdentifier == "Up") { + // Save the selection so it can be compared to the new selection when we call onChange immediately after making the new selection. + saveLastSelection(); + + ASSERT(endIndex >= 0 && (unsigned)endIndex < listItems().size()); + setActiveSelectionEndIndex(endIndex); + + // 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 = !multiple() || !static_cast(evt)->shiftKey(); + if (m_activeSelectionAnchorIndex < 0 || deselectOthers) { + m_activeSelectionState = true; + if (deselectOthers) + deselectItems(); + setActiveSelectionAnchorIndex(m_activeSelectionEndIndex); + } + + static_cast(renderer())->scrollToRevealElementAtListIndex(endIndex); + updateListBoxSelection(deselectOthers); + listBoxOnChange(); + evt->setDefaultHandled(); + } + } else if (evt->type() == eventNames().keypressEvent) { + if (!evt->isKeyboardEvent()) + return; + int keyCode = static_cast(evt)->keyCode(); + + if (keyCode == '\r') { + if (form()) + form()->submitClick(evt); + evt->setDefaultHandled(); + return; + } + } +} + +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 + const Vector& items = listItems(); + m_cachedStateForActiveSelection.clear(); + for (unsigned i = 0; i < items.size(); i++) { + if (items[i]->hasLocalName(optionTag)) { + HTMLOptionElement* option = static_cast(items[i]); + m_cachedStateForActiveSelection.append(option->selected()); + } else + m_cachedStateForActiveSelection.append(false); + } +} + +void HTMLSelectElement::updateListBoxSelection(bool deselectOtherOptions) +{ + ASSERT(renderer() && renderer()->isListBox()); + + unsigned start; + unsigned end; + ASSERT(m_activeSelectionAnchorIndex >= 0); + start = min(m_activeSelectionAnchorIndex, m_activeSelectionEndIndex); + end = max(m_activeSelectionAnchorIndex, m_activeSelectionEndIndex); + + const Vector& items = listItems(); + for (unsigned i = 0; i < items.size(); i++) { + if (items[i]->hasLocalName(optionTag)) { + HTMLOptionElement* option = static_cast(items[i]); + if (!option->disabled()) { + if (i >= start && i <= end) + option->setSelectedState(m_activeSelectionState); + else if (deselectOtherOptions) + option->setSelectedState(false); + else + option->setSelectedState(m_cachedStateForActiveSelection[i]); + } + } + } + + scrollToSelection(); +} + +void HTMLSelectElement::menuListOnChange() +{ + ASSERT(usesMenuList()); + int selected = selectedIndex(); + if (m_lastOnChangeIndex != selected) { + m_lastOnChangeIndex = selected; + onChange(); + } +} + +void HTMLSelectElement::listBoxOnChange() +{ + ASSERT(!usesMenuList()); + + const Vector& items = listItems(); + + // If the cached selection list is empty, or the size has changed, then fire onChange, and return early. + if (m_lastOnChangeSelection.isEmpty() || m_lastOnChangeSelection.size() != items.size()) { + onChange(); + return; + } + + // Update m_lastOnChangeSelection and fire onChange + bool fireOnChange = false; + for (unsigned i = 0; i < items.size(); i++) { + bool selected = false; + if (items[i]->hasLocalName(optionTag)) + selected = static_cast(items[i])->selected(); + if (selected != m_lastOnChangeSelection[i]) + fireOnChange = true; + m_lastOnChangeSelection[i] = selected; + } + if (fireOnChange) + onChange(); +} + +void HTMLSelectElement::saveLastSelection() +{ + const Vector& items = listItems(); + + if (usesMenuList()) { + m_lastOnChangeIndex = selectedIndex(); + return; + } + + m_lastOnChangeSelection.clear(); + for (unsigned i = 0; i < items.size(); i++) { + if (items[i]->hasLocalName(optionTag)) { + HTMLOptionElement* option = static_cast(items[i]); + m_lastOnChangeSelection.append(option->selected()); + } else + m_lastOnChangeSelection.append(false); + } +} + +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 HTMLSelectElement::typeAheadFind(KeyboardEvent* event) +{ + if (event->timeStamp() < m_lastCharTime) + return; + + DOMTimeStamp delta = event->timeStamp() - m_lastCharTime; + + m_lastCharTime = event->timeStamp(); + + UChar c = event->charCode(); + + String prefix; + int searchStartOffset = 1; + if (delta > typeAheadTimeout) { + m_typedString = prefix = String(&c, 1); + m_repeatingChar = c; + } else { + m_typedString.append(c); + + if (c == m_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 { + m_repeatingChar = 0; + prefix = m_typedString; + searchStartOffset = 0; + } + } + + const Vector& items = listItems(); + int itemCount = items.size(); + if (itemCount < 1) + return; + + int selected = selectedIndex(); + int index = (optionToListIndex(selected >= 0 ? selected : 0) + searchStartOffset) % itemCount; + ASSERT(index >= 0); + for (int i = 0; i < itemCount; i++, index = (index + 1) % itemCount) { + if (!items[index]->hasTagName(optionTag) || items[index]->disabled()) + continue; + + if (stripLeadingWhiteSpace(static_cast(items[index])->optionText()).startsWith(prefix, false)) { + setSelectedIndex(listToOptionIndex(index)); + if(!usesMenuList()) + listBoxOnChange(); + setChanged(); + return; + } + } +} + +int HTMLSelectElement::nextSelectableListIndex(int startIndex) +{ + const Vector& items = listItems(); + int index = startIndex + 1; + while (index >= 0 && (unsigned)index < items.size() && (!items[index]->hasLocalName(optionTag) || items[index]->disabled())) + index++; + if ((unsigned) index == items.size()) + return startIndex; + return index; +} + +int HTMLSelectElement::previousSelectableListIndex(int startIndex) +{ + const Vector& items = listItems(); + if (startIndex == -1) + startIndex = items.size(); + int index = startIndex - 1; + while (index >= 0 && (unsigned)index < items.size() && (!items[index]->hasLocalName(optionTag) || items[index]->disabled())) + index--; + if (index == -1) + return startIndex; + return index; +} + +void HTMLSelectElement::accessKeyAction(bool sendToAnyElement) +{ + focus(); + dispatchSimulatedClick(0, sendToAnyElement); +} + +void HTMLSelectElement::accessKeySetSelectedIndex(int index) +{ + // first bring into focus the list box + if (!focused()) + accessKeyAction(false); + + // if this index is already selected, unselect. otherwise update the selected index + Node* listNode = item(index); + if (listNode && listNode->hasTagName(optionTag)) { + HTMLOptionElement* listElement = static_cast(listNode); + if (listElement->selected()) + listElement->setSelectedState(false); + else + setSelectedIndex(index, false, true); + } + + listBoxOnChange(); + scrollToSelection(); +} + +void HTMLSelectElement::setMultiple(bool multiple) +{ + setAttribute(multipleAttr, multiple ? "" : 0); +} + +void HTMLSelectElement::setSize(int size) +{ + setAttribute(sizeAttr, String::number(size)); +} + +Node* HTMLSelectElement::namedItem(const String &name, bool caseSensitive) +{ + return options()->namedItem(name, caseSensitive); +} + +Node* HTMLSelectElement::item(unsigned index) +{ + return options()->item(index); +} + +void HTMLSelectElement::setOption(unsigned index, HTMLOptionElement* option, ExceptionCode& ec) +{ + ec = 0; + if (index > INT_MAX) + index = INT_MAX; + int diff = index - length(); + HTMLElement* 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 = static_cast(options()->item(index+1)); + remove(index); + } + // finally add the new element + if (!ec) { + add(option, before, ec); + if (diff >= 0 && option->selected()) + setSelectedIndex(index, !m_multiple); + } +} + +void HTMLSelectElement::setLength(unsigned newLen, ExceptionCode& ec) +{ + ec = 0; + if (newLen > INT_MAX) + newLen = INT_MAX; + int diff = length() - newLen; + + if (diff < 0) { // add dummy elements + do { + RefPtr option = document()->createElement("option", ec); + if (!option) + break; + add(static_cast(option.get()), 0, ec); + if (ec) + break; + } while (++diff); + } + else // remove elements + while (diff-- > 0) + remove(newLen); +} + +void HTMLSelectElement::scrollToSelection() +{ + if (renderer() && !usesMenuList()) + static_cast(renderer())->selectionChanged(); +} + +#ifndef NDEBUG + +void HTMLSelectElement::checkListItems() const +{ + Vector items = m_listItems; + recalcListItems(false); + ASSERT(items == m_listItems); +} + +#endif + +} // namespace -- cgit v1.1