summaryrefslogtreecommitdiffstats
path: root/WebCore/html/HTMLSelectElement.cpp
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:30:52 -0800
committerThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:30:52 -0800
commit8e35f3cfc7fba1d1c829dc557ebad6409cbe16a2 (patch)
tree11425ea0b299d6fb89c6d3618a22d97d5bf68d0f /WebCore/html/HTMLSelectElement.cpp
parent648161bb0edfc3d43db63caed5cc5213bc6cb78f (diff)
downloadexternal_webkit-8e35f3cfc7fba1d1c829dc557ebad6409cbe16a2.zip
external_webkit-8e35f3cfc7fba1d1c829dc557ebad6409cbe16a2.tar.gz
external_webkit-8e35f3cfc7fba1d1c829dc557ebad6409cbe16a2.tar.bz2
auto import from //depot/cupcake/@135843
Diffstat (limited to 'WebCore/html/HTMLSelectElement.cpp')
-rw-r--r--WebCore/html/HTMLSelectElement.cpp1134
1 files changed, 1134 insertions, 0 deletions
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 <math.h>
+#include <wtf/Vector.h>
+
+#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 <optgroup> 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<RenderMenuList*>(renderer())->setOptionsChanged(true);
+ else
+ static_cast<RenderListBox*>(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<HTMLElement*>& items = listItems();
+ for (unsigned int i = 0; i < items.size(); i++) {
+ if (items[i]->hasLocalName(optionTag)) {
+ if (static_cast<HTMLOptionElement*>(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<HTMLElement*>& items = listItems();
+ for (unsigned int i = 0; i < items.size(); i++) {
+ if (items[i]->hasLocalName(optionTag)) {
+ if (static_cast<HTMLOptionElement*>(items[i])->selected()) {
+ index = i;
+ found = true;
+ }
+ }
+ }
+ return found ? (int) index : -1;
+}
+
+void HTMLSelectElement::deselectItems(HTMLOptionElement* excludeElement)
+{
+ const Vector<HTMLElement*>& items = listItems();
+ unsigned i;
+ for (i = 0; i < items.size(); i++) {
+ if (items[i]->hasLocalName(optionTag) && (items[i] != excludeElement)) {
+ HTMLOptionElement* element = static_cast<HTMLOptionElement*>(items[i]);
+ element->setSelectedState(false);
+ }
+ }
+}
+
+void HTMLSelectElement::setSelectedIndex(int optionIndex, bool deselect, bool fireOnChange)
+{
+ const Vector<HTMLElement*>& 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<HTMLOptionElement*>(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<HTMLElement*>& 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<HTMLElement> 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<HTMLElement*>& 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<HTMLElement*>& items = listItems();
+ for (i = 0; i < items.size(); i++) {
+ if (items[i]->hasLocalName(optionTag) && static_cast<HTMLOptionElement*>(items[i])->selected())
+ return static_cast<HTMLOptionElement*>(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<HTMLElement*>& items = listItems();
+ unsigned optionIndex = 0;
+ for (unsigned i = 0; i < items.size(); i++)
+ if (items[i]->hasLocalName(optionTag)) {
+ if (static_cast<HTMLOptionElement*>(items[i])->value() == value) {
+ setSelectedIndex(optionIndex, true);
+ return;
+ }
+ optionIndex++;
+ }
+}
+
+bool HTMLSelectElement::saveState(String& value) const
+{
+ const Vector<HTMLElement*>& items = listItems();
+ int l = items.size();
+ Vector<char, 1024> characters(l);
+ for (int i = 0; i < l; ++i) {
+ HTMLElement* e = items[i];
+ bool selected = e->hasLocalName(optionTag) && static_cast<HTMLOptionElement*>(e)->selected();
+ characters[i] = selected ? 'X' : '.';
+ }
+ value = String(characters.data(), l);
+ return true;
+}
+
+void HTMLSelectElement::restoreState(const String& state)
+{
+ recalcListItems();
+
+ const Vector<HTMLElement*>& items = listItems();
+ int l = items.size();
+ for (int i = 0; i < l; i++)
+ if (items[i]->hasLocalName(optionTag))
+ static_cast<HTMLOptionElement*>(items[i])->setSelectedState(state[i] == 'X');
+
+ setChanged();
+}
+
+bool HTMLSelectElement::insertBefore(PassRefPtr<Node> newChild, Node* refChild, ExceptionCode& ec, bool shouldLazyAttach)
+{
+ bool result = HTMLFormControlElementWithState::insertBefore(newChild, refChild, ec, shouldLazyAttach);
+ if (result)
+ setRecalcListItems();
+ return result;
+}
+
+bool HTMLSelectElement::replaceChild(PassRefPtr<Node> 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<Node> 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<HTMLElement*>& items = listItems();
+
+ unsigned i;
+ for (i = 0; i < items.size(); i++) {
+ if (items[i]->hasLocalName(optionTag)) {
+ HTMLOptionElement *option = static_cast<HTMLOptionElement*>(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<HTMLOptionElement*>(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<HTMLElement*>& 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<HTMLElement*>& 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<HTMLOptionsCollection> 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<HTMLElement*>(current));
+ current = current->firstChild();
+ // FIXME: It doesn't make sense to handle an <optgroup> inside another <optgroup>
+ // 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<HTMLElement*>(current));
+ if (updateSelectedStates) {
+ if (!foundSelected && (usesMenuList() || (!m_multiple && static_cast<HTMLOptionElement*>(current)->selected()))) {
+ foundSelected = static_cast<HTMLOptionElement*>(current);
+ foundSelected->setSelectedState(true);
+ } else if (foundSelected && !m_multiple && static_cast<HTMLOptionElement*>(current)->selected()) {
+ foundSelected->setSelectedState(false);
+ foundSelected = static_cast<HTMLOptionElement*>(current);
+ }
+ }
+ }
+ if (current->hasTagName(hrTag))
+ m_listItems.append(static_cast<HTMLElement*>(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<RenderMenuList*>(renderer())->setOptionsChanged(true);
+ else
+ static_cast<RenderListBox*>(renderer())->setOptionsChanged(true);
+ }
+ if (!inDocument())
+ m_collectionInfo.reset();
+ setChanged();
+}
+
+void HTMLSelectElement::reset()
+{
+ bool optionSelected = false;
+ HTMLOptionElement* firstOption = 0;
+ const Vector<HTMLElement*>& items = listItems();
+ unsigned i;
+ for (i = 0; i < items.size(); i++) {
+ if (items[i]->hasLocalName(optionTag)) {
+ HTMLOptionElement *option = static_cast<HTMLOptionElement*>(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<KeyboardEvent*>(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<RenderMenuList*>(renderer());
+
+ if (evt->type() == eventNames().keydownEvent) {
+ if (!renderer() || !evt->isKeyboardEvent())
+ return;
+ String keyIdentifier = static_cast<KeyboardEvent*>(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<KeyboardEvent*>(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<MouseEvent*>(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<MouseEvent*>(evt)->button() == LeftButton) {
+ focus();
+
+ MouseEvent* mEvt = static_cast<MouseEvent*>(evt);
+ int listIndex = static_cast<RenderListBox*>(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<HTMLOptionElement*>(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<MouseEvent*>(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<KeyboardEvent*>(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<KeyboardEvent*>(evt)->shiftKey();
+ if (m_activeSelectionAnchorIndex < 0 || deselectOthers) {
+ m_activeSelectionState = true;
+ if (deselectOthers)
+ deselectItems();
+ setActiveSelectionAnchorIndex(m_activeSelectionEndIndex);
+ }
+
+ static_cast<RenderListBox*>(renderer())->scrollToRevealElementAtListIndex(endIndex);
+ updateListBoxSelection(deselectOthers);
+ listBoxOnChange();
+ evt->setDefaultHandled();
+ }
+ } else if (evt->type() == eventNames().keypressEvent) {
+ if (!evt->isKeyboardEvent())
+ return;
+ int keyCode = static_cast<KeyboardEvent*>(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<HTMLElement*>& items = listItems();
+ m_cachedStateForActiveSelection.clear();
+ for (unsigned i = 0; i < items.size(); i++) {
+ if (items[i]->hasLocalName(optionTag)) {
+ HTMLOptionElement* option = static_cast<HTMLOptionElement*>(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<HTMLElement*>& items = listItems();
+ for (unsigned i = 0; i < items.size(); i++) {
+ if (items[i]->hasLocalName(optionTag)) {
+ HTMLOptionElement* option = static_cast<HTMLOptionElement*>(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<HTMLElement*>& 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<HTMLOptionElement*>(items[i])->selected();
+ if (selected != m_lastOnChangeSelection[i])
+ fireOnChange = true;
+ m_lastOnChangeSelection[i] = selected;
+ }
+ if (fireOnChange)
+ onChange();
+}
+
+void HTMLSelectElement::saveLastSelection()
+{
+ const Vector<HTMLElement*>& 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<HTMLOptionElement*>(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<HTMLElement*>& 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<HTMLOptionElement*>(items[index])->optionText()).startsWith(prefix, false)) {
+ setSelectedIndex(listToOptionIndex(index));
+ if(!usesMenuList())
+ listBoxOnChange();
+ setChanged();
+ return;
+ }
+ }
+}
+
+int HTMLSelectElement::nextSelectableListIndex(int startIndex)
+{
+ const Vector<HTMLElement*>& 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<HTMLElement*>& 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<HTMLOptionElement*>(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<HTMLElement*>(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<Element> option = document()->createElement("option", ec);
+ if (!option)
+ break;
+ add(static_cast<HTMLElement*>(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<RenderListBox*>(renderer())->selectionChanged();
+}
+
+#ifndef NDEBUG
+
+void HTMLSelectElement::checkListItems() const
+{
+ Vector<HTMLElement*> items = m_listItems;
+ recalcListItems(false);
+ ASSERT(items == m_listItems);
+}
+
+#endif
+
+} // namespace