HTMLSelectElement.cpp [plain text]
#include "config.h"
#include "HTMLSelectElement.h"
#include "CSSPropertyNames.h"
#include "Document.h"
#include "Event.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 "RenderMenuList.h"
#include "cssstyleselector.h"
#include <wtf/Vector.h>
#include <math.h>
#if PLATFORM(MAC)
#define ARROW_KEYS_POP_MENU 1
#else
#define ARROW_KEYS_POP_MENU 0
#endif
#define RenderListBox RenderMenuList
using namespace std;
namespace WebCore {
using namespace EventNames;
using namespace HTMLNames;
HTMLSelectElement::HTMLSelectElement(Document* doc, HTMLFormElement* f)
: HTMLGenericFormElement(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)
, m_typedString(String())
{
document()->registerFormElementWithState(this);
}
HTMLSelectElement::HTMLSelectElement(const QualifiedName& tagName, Document* doc, HTMLFormElement* f)
: HTMLGenericFormElement(tagName, doc, f), m_minwidth(0), m_size(0), m_multiple(false), m_recalcListItems(false)
{
document()->registerFormElementWithState(this);
}
HTMLSelectElement::~HTMLSelectElement()
{
document()->unregisterFormElementWithState(this);
}
bool HTMLSelectElement::checkDTD(const Node* newChild)
{
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);
}
HTMLGenericFormElement::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
{
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
{
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->m_selected = false;
element->setChanged();
}
}
}
void HTMLSelectElement::setSelectedIndex(int optionIndex, bool deselect, bool fireOnChange)
{
const Vector<HTMLElement*>& items = listItems();
int listIndex = optionToListIndex(optionIndex);
HTMLOptionElement* element = 0;
if (listIndex >= 0) {
element = static_cast<HTMLOptionElement*>(items[listIndex]);
element->setSelected(true);
}
if (deselect)
deselectItems(element);
if (listIndex >= 0) {
if (m_activeSelectionAnchorIndex < 0 || deselect)
setActiveSelectionAnchorIndex(listIndex);
if (m_activeSelectionEndIndex < 0 || deselect)
setActiveSelectionEndIndex(listIndex);
}
if (usesMenuList() && fireOnChange && m_lastOnChangeIndex != optionIndex) {
m_lastOnChangeIndex = optionIndex;
onChange();
}
}
int HTMLSelectElement::length() const
{
int len = 0;
unsigned i;
const Vector<HTMLElement*>& items = listItems();
for (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);
if (!element || !(element->hasLocalName(optionTag) || element->hasLocalName(hrTag)))
return;
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;
removeChild(items[listIndex], 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;
const Vector<HTMLElement*>& items = listItems();
for (unsigned i = 0; i < items.size(); i++)
if (items[i]->hasLocalName(optionTag) && static_cast<HTMLOptionElement*>(items[i])->value() == value) {
static_cast<HTMLOptionElement*>(items[i])->setSelected(true);
return;
}
}
String HTMLSelectElement::stateValue() 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' : '.';
}
return String(characters, l);
}
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])->setSelected(state[i] == 'X');
setChanged(true);
}
bool HTMLSelectElement::insertBefore(PassRefPtr<Node> newChild, Node* refChild, ExceptionCode& ec)
{
bool result = HTMLGenericFormElement::insertBefore(newChild, refChild, ec);
if (result)
setRecalcListItems();
return result;
}
bool HTMLSelectElement::replaceChild(PassRefPtr<Node> newChild, Node *oldChild, ExceptionCode& ec)
{
bool result = HTMLGenericFormElement::replaceChild(newChild, oldChild, ec);
if (result)
setRecalcListItems();
return result;
}
bool HTMLSelectElement::removeChild(Node* oldChild, ExceptionCode& ec)
{
bool result = HTMLGenericFormElement::removeChild(oldChild, ec);
if (result)
setRecalcListItems();
return result;
}
bool HTMLSelectElement::appendChild(PassRefPtr<Node> newChild, ExceptionCode& ec)
{
bool result = HTMLGenericFormElement::appendChild(newChild, ec);
if (result)
setRecalcListItems();
return result;
}
ContainerNode* HTMLSelectElement::addChild(PassRefPtr<Node> newChild)
{
ContainerNode* result = HTMLGenericFormElement::addChild(newChild);
if (result)
setRecalcListItems();
return result;
}
void HTMLSelectElement::parseMappedAttribute(MappedAttribute *attr)
{
bool oldUsesMenuList = usesMenuList();
if (attr->name() == sizeAttr) {
m_size = max(attr->value().toInt(), 1);
if (oldUsesMenuList != usesMenuList() && attached()) {
detach();
attach();
}
} 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) {
} else if (attr->name() == onfocusAttr) {
setHTMLEventListener(focusEvent, attr);
} else if (attr->name() == onblurAttr) {
setHTMLEventListener(blurEvent, attr);
} else if (attr->name() == onchangeAttr) {
setHTMLEventListener(changeEvent, attr);
} else
HTMLGenericFormElement::parseMappedAttribute(attr);
}
bool HTMLSelectElement::isKeyboardFocusable() const
{
if (renderer())
return isFocusable();
return HTMLGenericFormElement::isKeyboardFocusable();
}
bool HTMLSelectElement::isMouseFocusable() const
{
if (renderer())
return isFocusable();
return HTMLGenericFormElement::isMouseFocusable();
}
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;
}
}
}
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();
if (optionIndex < 0 || optionIndex >= int(items.size()))
return -1;
int listIndex = 0;
int optionIndex2 = 0;
for (;
optionIndex2 < int(items.size()) && optionIndex2 <= optionIndex;
listIndex++) { if (items[listIndex]->hasLocalName(optionTag))
optionIndex2++;
}
listIndex--;
return listIndex;
}
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; for (int i = 0; i < listIndex; i++)
if (items[i]->hasLocalName(optionTag))
optionIndex++;
return optionIndex;
}
PassRefPtr<HTMLOptionsCollection> HTMLSelectElement::options()
{
return new HTMLOptionsCollection(this);
}
void HTMLSelectElement::recalcListItems() const
{
Node* current = firstChild();
m_listItems.clear();
HTMLOptionElement* foundSelected = 0;
while (current) {
if (current->hasTagName(optgroupTag) && current->firstChild()) {
m_listItems.append(static_cast<HTMLElement*>(current));
current = current->firstChild();
}
if (current->hasTagName(optionTag)) {
m_listItems.append(static_cast<HTMLElement*>(current));
if (!foundSelected && (usesMenuList() || (!m_multiple && static_cast<HTMLOptionElement*>(current)->selected()))) {
foundSelected = static_cast<HTMLOptionElement*>(current);
foundSelected->m_selected = true;
} else if (foundSelected && !m_multiple && static_cast<HTMLOptionElement*>(current)->selected()) {
foundSelected->m_selected = false;
foundSelected = static_cast<HTMLOptionElement*>(current);
}
}
if (current->hasTagName(hrTag))
m_listItems.append(static_cast<HTMLElement*>(current));
Node* parent = current->parentNode();
current = current->nextSibling();
if (!current) {
if (parent != this)
current = parent->nextSibling();
}
}
m_recalcListItems = false;
}
void HTMLSelectElement::childrenChanged()
{
setRecalcListItems();
HTMLGenericFormElement::childrenChanged();
}
void HTMLSelectElement::setRecalcListItems()
{
m_recalcListItems = true;
if (renderer()) {
if (usesMenuList())
static_cast<RenderMenuList*>(renderer())->setOptionsChanged(true);
else
static_cast<RenderListBox*>(renderer())->setOptionsChanged(true);
}
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->setSelected(true);
optionSelected = true;
} else
option->setSelected(false);
if (!firstOption)
firstOption = option;
}
}
if (!optionSelected && firstOption)
firstOption->setSelected(true);
setChanged(true);
m_lastOnChangeIndex = -1;
}
void HTMLSelectElement::notifyOptionSelected(HTMLOptionElement *selectedOption, bool selected)
{
if (selected && !m_multiple)
deselectItems(selectedOption);
m_lastOnChangeIndex = selectedOption->index();
setChanged(true);
}
void HTMLSelectElement::dispatchBlurEvent()
{
#if !ARROW_KEYS_POP_MENU
if (usesMenuList() && selectedIndex() != m_lastOnChangeIndex) {
m_lastOnChangeIndex = selectedIndex();
onChange();
}
#endif
HTMLGenericFormElement::dispatchBlurEvent();
}
void HTMLSelectElement::defaultEventHandler(Event* evt)
{
if (usesMenuList())
menuListDefaultEventHandler(evt);
else
listBoxDefaultEventHandler(evt);
if (!evt->defaultHandled() && evt->type() == keypressEvent && evt->isKeyboardEvent()) {
KeyboardEvent* keyboardEvent = static_cast<KeyboardEvent*>(evt);
if (!keyboardEvent->ctrlKey() && !keyboardEvent->altKey() && !keyboardEvent->metaKey()
&& isprint(static_cast<KeyboardEvent*>(evt)->charCode())) {
typeAheadFind(static_cast<KeyboardEvent*>(evt));
evt->setDefaultHandled();
}
}
HTMLGenericFormElement::defaultEventHandler(evt);
}
void HTMLSelectElement::menuListDefaultEventHandler(Event* evt)
{
RenderMenuList* menuList = static_cast<RenderMenuList*>(renderer());
if (evt->type() == keypressEvent) {
if (!renderer() || !evt->isKeyboardEvent())
return;
String keyIdentifier = static_cast<KeyboardEvent*>(evt)->keyIdentifier();
bool handled = false;
#if ARROW_KEYS_POP_MENU
if (form() && keyIdentifier == "Enter") {
blur();
if (form())
form()->submitClick();
handled = true;
}
if ((keyIdentifier == "Down" || keyIdentifier == "Up" || keyIdentifier == "U+000020") && renderer() && usesMenuList()) {
focus();
menuList->showPopup();
handled = true;
}
#else
int listIndex = optionToListIndex(selectedIndex());
if (keyIdentifier == "Down" || keyIdentifier == "Right") {
size_t 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") {
size_t 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 == "Enter") {
setSelectedIndex(listToOptionIndex(listIndex), true, true);
}
#endif
if (handled)
evt->setDefaultHandled();
}
if (evt->type() == mousedownEvent) {
focus();
if (menuList->popupIsVisible())
menuList->hidePopup();
else
menuList->showPopup();
evt->setDefaultHandled();
}
}
void HTMLSelectElement::listBoxDefaultEventHandler(Event* evt)
{
}
void HTMLSelectElement::setActiveSelectionAnchorIndex(int index)
{
m_activeSelectionAnchorIndex = 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)
{
}
void HTMLSelectElement::listBoxOnChange()
{
const Vector<HTMLElement*>& items = listItems();
if (m_lastOnChangeSelection.isEmpty() || m_lastOnChangeSelection.size() != items.size()) {
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);
}
onChange();
return;
}
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();
}
static String stripLeadingWhiteSpace(const String& string)
{
const UChar nonBreakingSpace = 0xA0;
int length = string.length();
int i;
for (i = 0; i < length; ++i)
if (string[i] != nonBreakingSpace && (string[i] <= 0x7F ? !isspace(string[i]) : (u_charDirection(string[i]) != U_WHITE_SPACE_NEUTRAL)))
break;
return string.substring(i, length - i);
}
const DOMTimeStamp typeAheadTimeout = 1000;
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)
prefix = String(&c, 1);
else {
m_repeatingChar = 0;
prefix = m_typedString;
searchStartOffset = 0;
}
}
const Vector<HTMLElement*>& items = listItems();
int itemCount = items.size();
int index = (optionToListIndex(selectedIndex()) + searchStartOffset) % itemCount;
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));
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)
{
click(sendToAnyElement);
}
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));
}
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;
if (diff > 0) {
setLength(index, ec);
} else if (diff < 0) {
before = static_cast<HTMLElement*>(options()->item(index+1));
remove(index);
}
if (ec == 0) {
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) { do {
RefPtr<Element> option = ownerDocument()->createElement("option", ec);
if (!option)
break;
add(static_cast<HTMLElement*>(option.get()), 0, ec);
if (ec)
break;
} while (++diff);
}
else while (diff-- > 0)
remove(newLen);
}
bool HTMLSelectElement::willRespondToMouseClickEvents()
{
return !disabled();
}
}