| /* |
| * 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 "core/html/HTMLOptionElement.h" |
| |
| #include "bindings/core/v8/ExceptionState.h" |
| #include "core/HTMLNames.h" |
| #include "core/dom/AXObjectCache.h" |
| #include "core/dom/Document.h" |
| #include "core/dom/NodeComputedStyle.h" |
| #include "core/dom/NodeTraversal.h" |
| #include "core/dom/ScriptLoader.h" |
| #include "core/dom/Text.h" |
| #include "core/dom/shadow/ShadowRoot.h" |
| #include "core/html/HTMLDataListElement.h" |
| #include "core/html/HTMLOptGroupElement.h" |
| #include "core/html/HTMLSelectElement.h" |
| #include "core/html/parser/HTMLParserIdioms.h" |
| #include "core/layout/LayoutTheme.h" |
| #include "wtf/Vector.h" |
| #include "wtf/text/StringBuilder.h" |
| |
| namespace blink { |
| |
| using namespace HTMLNames; |
| |
| HTMLOptionElement::HTMLOptionElement(Document& document) |
| : HTMLElement(optionTag, document) |
| , m_isSelected(false) |
| { |
| setHasCustomStyleCallbacks(); |
| } |
| |
| // An explicit empty destructor should be in HTMLOptionElement.cpp, because |
| // if an implicit destructor is used or an empty destructor is defined in |
| // HTMLOptionElement.h, when including HTMLOptionElement.h, |
| // msvc tries to expand the destructor and causes |
| // a compile error because of lack of ComputedStyle definition. |
| HTMLOptionElement::~HTMLOptionElement() |
| { |
| } |
| |
| HTMLOptionElement* HTMLOptionElement::create(Document& document) |
| { |
| HTMLOptionElement* option = new HTMLOptionElement(document); |
| option->ensureUserAgentShadowRoot(); |
| return option; |
| } |
| |
| HTMLOptionElement* HTMLOptionElement::createForJSConstructor(Document& document, const String& data, const AtomicString& value, |
| bool defaultSelected, bool selected, ExceptionState& exceptionState) |
| { |
| HTMLOptionElement* element = new HTMLOptionElement(document); |
| element->ensureUserAgentShadowRoot(); |
| element->appendChild(Text::create(document, data.isNull() ? "" : data), exceptionState); |
| if (exceptionState.hadException()) |
| return nullptr; |
| |
| if (!value.isNull()) |
| element->setValue(value); |
| if (defaultSelected) |
| element->setAttribute(selectedAttr, emptyAtom); |
| element->setSelected(selected); |
| |
| return element; |
| } |
| |
| void HTMLOptionElement::attach(const AttachContext& context) |
| { |
| AttachContext optionContext(context); |
| if (context.resolvedStyle) { |
| ASSERT(!m_style || m_style == context.resolvedStyle); |
| m_style = context.resolvedStyle; |
| } else if (parentComputedStyle()) { |
| updateNonComputedStyle(); |
| optionContext.resolvedStyle = m_style.get(); |
| } |
| HTMLElement::attach(optionContext); |
| } |
| |
| void HTMLOptionElement::detach(const AttachContext& context) |
| { |
| m_style.clear(); |
| HTMLElement::detach(context); |
| } |
| |
| bool HTMLOptionElement::supportsFocus() const |
| { |
| HTMLSelectElement* select = ownerSelectElement(); |
| if (select && select->usesMenuList()) |
| return false; |
| return HTMLElement::supportsFocus(); |
| } |
| |
| bool HTMLOptionElement::matchesDefaultPseudoClass() const |
| { |
| return fastHasAttribute(selectedAttr); |
| } |
| |
| bool HTMLOptionElement::matchesEnabledPseudoClass() const |
| { |
| return !isDisabledFormControl(); |
| } |
| |
| String HTMLOptionElement::displayLabel() const |
| { |
| Document& document = this->document(); |
| 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(); |
| |
| return text.stripWhiteSpace(isHTMLSpace<UChar>).simplifyWhiteSpace(isHTMLSpace<UChar>); |
| } |
| |
| String HTMLOptionElement::text() const |
| { |
| return collectOptionInnerText().stripWhiteSpace(isHTMLSpace<UChar>).simplifyWhiteSpace(isHTMLSpace<UChar>); |
| } |
| |
| void HTMLOptionElement::setText(const String &text, ExceptionState& exceptionState) |
| { |
| // 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. |
| HTMLSelectElement* select = ownerSelectElement(); |
| bool selectIsMenuList = select && select->usesMenuList(); |
| int oldSelectedIndex = selectIsMenuList ? select->selectedIndex() : -1; |
| |
| if (hasOneTextChild()) { |
| toText(firstChild())->setData(text); |
| } else { |
| removeChildren(); |
| appendChild(Text::create(document(), text), exceptionState); |
| } |
| |
| if (selectIsMenuList && select->selectedIndex() != oldSelectedIndex) |
| select->setSelectedIndex(oldSelectedIndex); |
| } |
| |
| void HTMLOptionElement::accessKeyAction(bool) |
| { |
| if (HTMLSelectElement* select = ownerSelectElement()) |
| 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 HeapVector<Member<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].get() == this) |
| return optionIndex; |
| ++optionIndex; |
| } |
| |
| return 0; |
| } |
| |
| int HTMLOptionElement::listIndex() const |
| { |
| if (HTMLSelectElement* selectElement = ownerSelectElement()) |
| return selectElement->listIndexForOption(*this); |
| return -1; |
| } |
| |
| void HTMLOptionElement::parseAttribute(const QualifiedName& name, const AtomicString& oldValue, const AtomicString& value) |
| { |
| if (name == valueAttr) { |
| if (HTMLDataListElement* dataList = ownerDataListElement()) |
| dataList->optionElementChildrenChanged(); |
| } else if (name == disabledAttr) { |
| if (oldValue.isNull() != value.isNull()) { |
| pseudoStateChanged(CSSSelector::PseudoDisabled); |
| pseudoStateChanged(CSSSelector::PseudoEnabled); |
| if (layoutObject()) |
| LayoutTheme::theme().controlStateChanged(*layoutObject(), EnabledControlState); |
| } |
| } else if (name == selectedAttr) { |
| if (oldValue.isNull() != value.isNull() && !m_isDirty) |
| setSelected(!value.isNull()); |
| pseudoStateChanged(CSSSelector::PseudoDefault); |
| } else if (name == labelAttr) { |
| updateLabel(); |
| } else { |
| HTMLElement::parseAttribute(name, oldValue, value); |
| } |
| } |
| |
| String HTMLOptionElement::value() const |
| { |
| const AtomicString& value = fastGetAttribute(valueAttr); |
| if (!value.isNull()) |
| return value; |
| return collectOptionInnerText().stripWhiteSpace(isHTMLSpace<UChar>).simplifyWhiteSpace(isHTMLSpace<UChar>); |
| } |
| |
| void HTMLOptionElement::setValue(const AtomicString& value) |
| { |
| setAttribute(valueAttr, value); |
| } |
| |
| bool HTMLOptionElement::selected() const |
| { |
| return m_isSelected; |
| } |
| |
| void HTMLOptionElement::setSelected(bool selected) |
| { |
| if (m_isSelected == selected) |
| return; |
| |
| setSelectedState(selected); |
| |
| if (HTMLSelectElement* select = ownerSelectElement()) |
| select->optionSelectionStateChanged(this, selected); |
| } |
| |
| bool HTMLOptionElement::selectedForBinding() const |
| { |
| return selected(); |
| } |
| |
| void HTMLOptionElement::setSelectedForBinding(bool selected) |
| { |
| bool wasSelected = m_isSelected; |
| setSelected(selected); |
| |
| // As of December 2015, the HTML specification says the dirtiness becomes |
| // true by |selected| setter unconditionally. However it caused a real bug, |
| // crbug.com/570367, and is not compatible with other browsers. |
| // Firefox seems not to set dirtiness if an option is owned by a select |
| // element and selectedness is not changed. |
| if (ownerSelectElement() && wasSelected == m_isSelected) |
| return; |
| |
| m_isDirty = true; |
| } |
| |
| void HTMLOptionElement::setSelectedState(bool selected) |
| { |
| if (m_isSelected == selected) |
| return; |
| |
| m_isSelected = selected; |
| pseudoStateChanged(CSSSelector::PseudoChecked); |
| |
| if (HTMLSelectElement* select = ownerSelectElement()) { |
| select->invalidateSelectedItems(); |
| |
| if (AXObjectCache* cache = document().existingAXObjectCache()) { |
| // If there is a layoutObject (most common), fire accessibility notifications |
| // only when it's a listbox (and not a menu list). If there's no layoutObject, |
| // fire them anyway just to be safe (to make sure the AX tree is in sync). |
| if (!select->layoutObject() || select->layoutObject()->isListBox()) { |
| cache->listboxOptionStateChanged(this); |
| cache->listboxSelectedChildrenChanged(select); |
| } |
| } |
| } |
| } |
| |
| void HTMLOptionElement::setDirty(bool value) |
| { |
| m_isDirty = true; |
| } |
| |
| void HTMLOptionElement::childrenChanged(const ChildrenChange& change) |
| { |
| if (HTMLDataListElement* dataList = ownerDataListElement()) |
| dataList->optionElementChildrenChanged(); |
| else if (HTMLSelectElement* select = ownerSelectElement()) |
| select->optionElementChildrenChanged(*this); |
| updateLabel(); |
| HTMLElement::childrenChanged(change); |
| } |
| |
| HTMLDataListElement* HTMLOptionElement::ownerDataListElement() const |
| { |
| return Traversal<HTMLDataListElement>::firstAncestor(*this); |
| } |
| |
| HTMLSelectElement* HTMLOptionElement::ownerSelectElement() const |
| { |
| if (!parentNode()) |
| return nullptr; |
| if (isHTMLSelectElement(*parentNode())) |
| return toHTMLSelectElement(parentNode()); |
| if (!isHTMLOptGroupElement(*parentNode())) |
| return nullptr; |
| Node* grandParent = parentNode()->parentNode(); |
| return isHTMLSelectElement(grandParent) ? toHTMLSelectElement(grandParent) : nullptr; |
| } |
| |
| String HTMLOptionElement::label() const |
| { |
| const AtomicString& label = fastGetAttribute(labelAttr); |
| if (!label.isNull()) |
| return label; |
| return collectOptionInnerText().stripWhiteSpace(isHTMLSpace<UChar>).simplifyWhiteSpace(isHTMLSpace<UChar>); |
| } |
| |
| void HTMLOptionElement::setLabel(const AtomicString& label) |
| { |
| setAttribute(labelAttr, label); |
| } |
| |
| void HTMLOptionElement::updateNonComputedStyle() |
| { |
| m_style = originalStyleForLayoutObject(); |
| if (HTMLSelectElement* select = ownerSelectElement()) |
| select->updateListOnLayoutObject(); |
| } |
| |
| ComputedStyle* HTMLOptionElement::nonLayoutObjectComputedStyle() const |
| { |
| return m_style.get(); |
| } |
| |
| PassRefPtr<ComputedStyle> HTMLOptionElement::customStyleForLayoutObject() |
| { |
| updateNonComputedStyle(); |
| return m_style; |
| } |
| |
| String HTMLOptionElement::textIndentedToRespectGroupLabel() const |
| { |
| ContainerNode* parent = parentNode(); |
| if (parent && isHTMLOptGroupElement(*parent)) |
| return " " + displayLabel(); |
| return displayLabel(); |
| } |
| |
| bool HTMLOptionElement::ownElementDisabled() const |
| { |
| return fastHasAttribute(disabledAttr); |
| } |
| |
| bool HTMLOptionElement::isDisabledFormControl() const |
| { |
| if (ownElementDisabled()) |
| return true; |
| if (Element* parent = parentElement()) |
| return isHTMLOptGroupElement(*parent) && parent->isDisabledFormControl(); |
| return false; |
| } |
| |
| String HTMLOptionElement::defaultToolTip() const |
| { |
| if (HTMLSelectElement* select = ownerSelectElement()) |
| return select->defaultToolTip(); |
| return String(); |
| } |
| |
| Node::InsertionNotificationRequest HTMLOptionElement::insertedInto(ContainerNode* insertionPoint) |
| { |
| HTMLElement::insertedInto(insertionPoint); |
| if (HTMLSelectElement* select = ownerSelectElement()) { |
| if (insertionPoint == select || (isHTMLOptGroupElement(*insertionPoint) && insertionPoint->parentNode() == select)) |
| select->optionInserted(*this, m_isSelected); |
| } |
| return InsertionDone; |
| } |
| |
| void HTMLOptionElement::removedFrom(ContainerNode* insertionPoint) |
| { |
| if (isHTMLSelectElement(*insertionPoint)) { |
| if (!parentNode() || isHTMLOptGroupElement(*parentNode())) |
| toHTMLSelectElement(insertionPoint)->optionRemoved(*this); |
| } else if (isHTMLOptGroupElement(*insertionPoint)) { |
| Node* parent = insertionPoint->parentNode(); |
| if (isHTMLSelectElement(parent)) |
| toHTMLSelectElement(parent)->optionRemoved(*this); |
| } |
| HTMLElement::removedFrom(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() && toScriptLoaderIfPossible(toElement(node))) |
| node = NodeTraversal::nextSkippingChildren(*node, this); |
| else |
| node = NodeTraversal::next(*node, this); |
| } |
| return text.toString(); |
| } |
| |
| HTMLFormElement* HTMLOptionElement::form() const |
| { |
| if (HTMLSelectElement* selectElement = ownerSelectElement()) |
| return selectElement->formOwner(); |
| |
| return nullptr; |
| } |
| |
| void HTMLOptionElement::didAddUserAgentShadowRoot(ShadowRoot& root) |
| { |
| updateLabel(); |
| } |
| |
| void HTMLOptionElement::updateLabel() |
| { |
| if (ShadowRoot* root = userAgentShadowRoot()) |
| root->setTextContent(displayLabel()); |
| } |
| |
| bool HTMLOptionElement::spatialNavigationFocused() const |
| { |
| HTMLSelectElement* select = ownerSelectElement(); |
| if (!select || !select->focused()) |
| return false; |
| return select->spatialNavigationFocusedOption() == this; |
| } |
| |
| bool HTMLOptionElement::isDisplayNone() const |
| { |
| // If m_style is not set, then the node is still unattached. |
| // We have to wait till it gets attached to read the display property. |
| if (!m_style) |
| return false; |
| |
| if (m_style->display() != NONE) { |
| // We need to check the parent's display property. Parent's |
| // display:none doesn't override children's display properties in |
| // ComputedStyle. |
| Element* parent = parentElement(); |
| ASSERT(parent); |
| if (isHTMLOptGroupElement(*parent)) { |
| const ComputedStyle* parentStyle = parent->computedStyle() ? parent->computedStyle() : parent->ensureComputedStyle(); |
| return !parentStyle || parentStyle->display() == NONE; |
| } |
| } |
| return m_style->display() == NONE; |
| } |
| |
| } // namespace blink |