blob: a6543542f77385a345527722e918ceb2e152d4f4 [file] [log] [blame]
/*
* Copyright (C) 1999 Lars Knoll (knoll@kde.org)
* (C) 1999 Antti Koivisto (koivisto@kde.org)
* (C) 2001 Dirk Mueller (mueller@kde.org)
* (C) 2006 Alexey Proskuryakov (ap@nypop.com)
* Copyright (C) 2004, 2005, 2006, 2010 Apple Inc. All rights reserved.
* Copyright (C) 2010 Google Inc. All rights reserved.
* Copyright (C) 2011 Motorola Mobility, Inc. All rights reserved.
*
* 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 "HTMLOptionElement.h"
#include "Attribute.h"
#include "Document.h"
#include "ExceptionCode.h"
#include "HTMLDataListElement.h"
#include "HTMLNames.h"
#include "HTMLOptGroupElement.h"
#include "HTMLParserIdioms.h"
#include "HTMLSelectElement.h"
#include "NodeRenderStyle.h"
#include "NodeTraversal.h"
#include "RenderMenuList.h"
#include "RenderTheme.h"
#include "ScriptElement.h"
#include "StyleResolver.h"
#include "Text.h"
#include <wtf/Ref.h>
#include <wtf/Vector.h>
#include <wtf/text/StringBuilder.h>
namespace WebCore {
using namespace HTMLNames;
HTMLOptionElement::HTMLOptionElement(const QualifiedName& tagName, Document& document)
: HTMLElement(tagName, document)
, m_disabled(false)
, m_isSelected(false)
{
ASSERT(hasTagName(optionTag));
setHasCustomStyleResolveCallbacks();
}
PassRefPtr<HTMLOptionElement> HTMLOptionElement::create(Document& document)
{
return adoptRef(new HTMLOptionElement(optionTag, document));
}
PassRefPtr<HTMLOptionElement> HTMLOptionElement::create(const QualifiedName& tagName, Document& document)
{
return adoptRef(new HTMLOptionElement(tagName, document));
}
PassRefPtr<HTMLOptionElement> HTMLOptionElement::createForJSConstructor(Document& document, const String& data, const String& value,
bool defaultSelected, bool selected, ExceptionCode& ec)
{
RefPtr<HTMLOptionElement> element = adoptRef(new HTMLOptionElement(optionTag, document));
RefPtr<Text> text = Text::create(document, data.isNull() ? "" : data);
ec = 0;
element->appendChild(text.release(), ec);
if (ec)
return 0;
if (!value.isNull())
element->setValue(value);
if (defaultSelected)
element->setAttribute(selectedAttr, emptyAtom);
element->setSelected(selected);
return element.release();
}
void HTMLOptionElement::didAttachRenderers()
{
// If after attaching nothing called styleForRenderer() on this node we
// manually cache the value. This happens if our parent doesn't have a
// renderer like <optgroup> or if it doesn't allow children like <select>.
if (!m_style && parentNode()->renderStyle())
updateNonRenderStyle();
}
void HTMLOptionElement::willDetachRenderers()
{
m_style.clear();
}
bool HTMLOptionElement::isFocusable() const
{
// Option elements do not have a renderer so we check the renderStyle instead.
return supportsFocus() && renderStyle() && renderStyle()->display() != NONE;
}
String HTMLOptionElement::text() const
{
String text;
// WinIE does not use the label attribute, so as a quirk, we ignore it.
if (!document().inQuirksMode())
text = fastGetAttribute(labelAttr);
// FIXME: The following treats an element with the label attribute set to
// the empty string the same as an element with no label attribute at all.
// Is that correct? If it is, then should the label function work the same way?
if (text.isEmpty())
text = collectOptionInnerText();
// FIXME: Is displayStringModifiedByEncoding helpful here?
// If it's correct here, then isn't it needed in the value and label functions too?
return document().displayStringModifiedByEncoding(text).stripWhiteSpace(isHTMLSpace).simplifyWhiteSpace(isHTMLSpace);
}
void HTMLOptionElement::setText(const String &text, ExceptionCode& ec)
{
Ref<HTMLOptionElement> protectFromMutationEvents(*this);
// Changing the text causes a recalc of a select's items, which will reset the selected
// index to the first item if the select is single selection with a menu list. We attempt to
// preserve the selected item.
RefPtr<HTMLSelectElement> select = ownerSelectElement();
bool selectIsMenuList = select && select->usesMenuList();
int oldSelectedIndex = selectIsMenuList ? select->selectedIndex() : -1;
// Handle the common special case where there's exactly 1 child node, and it's a text node.
Node* child = firstChild();
if (child && child->isTextNode() && !child->nextSibling())
toText(child)->setData(text, ec);
else {
removeChildren();
appendChild(Text::create(document(), text), ec);
}
if (selectIsMenuList && select->selectedIndex() != oldSelectedIndex)
select->setSelectedIndex(oldSelectedIndex);
}
void HTMLOptionElement::accessKeyAction(bool)
{
HTMLSelectElement* select = ownerSelectElement();
if (select)
select->accessKeySetSelectedIndex(index());
}
int HTMLOptionElement::index() const
{
// It would be faster to cache the index, but harder to get it right in all cases.
HTMLSelectElement* selectElement = ownerSelectElement();
if (!selectElement)
return 0;
int optionIndex = 0;
const Vector<HTMLElement*>& items = selectElement->listItems();
size_t length = items.size();
for (size_t i = 0; i < length; ++i) {
if (!isHTMLOptionElement(items[i]))
continue;
if (items[i] == this)
return optionIndex;
++optionIndex;
}
return 0;
}
void HTMLOptionElement::parseAttribute(const QualifiedName& name, const AtomicString& value)
{
#if ENABLE(DATALIST_ELEMENT)
if (name == valueAttr) {
if (HTMLDataListElement* dataList = ownerDataListElement())
dataList->optionElementChildrenChanged();
} else
#endif
if (name == disabledAttr) {
bool oldDisabled = m_disabled;
m_disabled = !value.isNull();
if (oldDisabled != m_disabled) {
didAffectSelector(AffectedSelectorDisabled | AffectedSelectorEnabled);
if (renderer() && renderer()->style().hasAppearance())
renderer()->theme()->stateChanged(renderer(), EnabledState);
}
} else if (name == selectedAttr) {
// FIXME: This doesn't match what the HTML specification says.
// The specification implies that removing the selected attribute or
// changing the value of a selected attribute that is already present
// has no effect on whether the element is selected. Further, it seems
// that we need to do more than just set m_isSelected to select in that
// case; we'd need to do the other work from the setSelected function.
m_isSelected = !value.isNull();
} else
HTMLElement::parseAttribute(name, value);
}
String HTMLOptionElement::value() const
{
const AtomicString& value = fastGetAttribute(valueAttr);
if (!value.isNull())
return value;
return collectOptionInnerText().stripWhiteSpace(isHTMLSpace).simplifyWhiteSpace(isHTMLSpace);
}
void HTMLOptionElement::setValue(const String& value)
{
setAttribute(valueAttr, value);
}
bool HTMLOptionElement::selected()
{
if (HTMLSelectElement* select = ownerSelectElement())
select->updateListItemSelectedStates();
return m_isSelected;
}
void HTMLOptionElement::setSelected(bool selected)
{
if (m_isSelected == selected)
return;
setSelectedState(selected);
if (HTMLSelectElement* select = ownerSelectElement())
select->optionSelectionStateChanged(this, selected);
}
void HTMLOptionElement::setSelectedState(bool selected)
{
if (m_isSelected == selected)
return;
m_isSelected = selected;
didAffectSelector(AffectedSelectorChecked);
if (HTMLSelectElement* select = ownerSelectElement())
select->invalidateSelectedItems();
}
void HTMLOptionElement::childrenChanged(const ChildChange& change)
{
#if ENABLE(DATALIST_ELEMENT)
if (HTMLDataListElement* dataList = ownerDataListElement())
dataList->optionElementChildrenChanged();
else
#endif
if (HTMLSelectElement* select = ownerSelectElement())
select->optionElementChildrenChanged();
HTMLElement::childrenChanged(change);
}
#if ENABLE(DATALIST_ELEMENT)
HTMLDataListElement* HTMLOptionElement::ownerDataListElement() const
{
for (ContainerNode* parent = parentNode(); parent ; parent = parent->parentNode()) {
if (parent->hasTagName(datalistTag))
return static_cast<HTMLDataListElement*>(parent);
}
return 0;
}
#endif
HTMLSelectElement* HTMLOptionElement::ownerSelectElement() const
{
ContainerNode* select = parentNode();
while (select && !select->hasTagName(selectTag))
select = select->parentNode();
if (!select)
return 0;
return toHTMLSelectElement(select);
}
String HTMLOptionElement::label() const
{
const AtomicString& label = fastGetAttribute(labelAttr);
if (!label.isNull())
return label;
return collectOptionInnerText().stripWhiteSpace(isHTMLSpace).simplifyWhiteSpace(isHTMLSpace);
}
void HTMLOptionElement::setLabel(const String& label)
{
setAttribute(labelAttr, label);
}
void HTMLOptionElement::updateNonRenderStyle()
{
m_style = document().ensureStyleResolver().styleForElement(this);
}
RenderStyle* HTMLOptionElement::nonRendererStyle() const
{
return m_style.get();
}
PassRefPtr<RenderStyle> HTMLOptionElement::customStyleForRenderer()
{
// styleForRenderer is called whenever a new style should be associated
// with an Element so now is a good time to update our cached style.
updateNonRenderStyle();
return m_style;
}
void HTMLOptionElement::didRecalcStyle(Style::Change)
{
// FIXME: This is nasty, we ask our owner select to repaint even if the new
// style is exactly the same.
if (auto select = ownerSelectElement()) {
if (auto renderer = select->renderer())
renderer->repaint();
}
}
String HTMLOptionElement::textIndentedToRespectGroupLabel() const
{
ContainerNode* parent = parentNode();
if (parent && isHTMLOptGroupElement(parent))
return " " + text();
return text();
}
bool HTMLOptionElement::isDisabledFormControl() const
{
if (ownElementDisabled())
return true;
if (!parentNode() || !parentNode()->isHTMLElement())
return false;
HTMLElement* parentElement = static_cast<HTMLElement*>(parentNode());
return isHTMLOptGroupElement(parentElement) && parentElement->isDisabledFormControl();
}
Node::InsertionNotificationRequest HTMLOptionElement::insertedInto(ContainerNode& insertionPoint)
{
if (HTMLSelectElement* select = ownerSelectElement()) {
select->setRecalcListItems();
// Do not call selected() since calling updateListItemSelectedStates()
// at this time won't do the right thing. (Why, exactly?)
// FIXME: Might be better to call this unconditionally, always passing m_isSelected,
// rather than only calling it if we are selected.
if (m_isSelected)
select->optionSelectionStateChanged(this, true);
select->scrollToSelection();
}
return HTMLElement::insertedInto(insertionPoint);
}
String HTMLOptionElement::collectOptionInnerText() const
{
StringBuilder text;
for (Node* node = firstChild(); node; ) {
if (node->isTextNode())
text.append(node->nodeValue());
// Text nodes inside script elements are not part of the option text.
if (node->isElementNode() && toScriptElementIfPossible(toElement(node)))
node = NodeTraversal::nextSkippingChildren(node, this);
else
node = NodeTraversal::next(node, this);
}
return text.toString();
}
} // namespace