/* * 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-2020 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 "Document.h" #include "ElementAncestorIterator.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 #include namespace WebCore { WTF_MAKE_ISO_ALLOCATED_IMPL(HTMLOptionElement); using namespace HTMLNames; HTMLOptionElement::HTMLOptionElement(const QualifiedName& tagName, Document& document) : HTMLElement(tagName, document) , m_disabled(false) , m_isSelected(false) { ASSERT(hasTagName(optionTag)); setHasCustomStyleResolveCallbacks(); } Ref HTMLOptionElement::create(Document& document) { return adoptRef(*new HTMLOptionElement(optionTag, document)); } Ref HTMLOptionElement::create(const QualifiedName& tagName, Document& document) { return adoptRef(*new HTMLOptionElement(tagName, document)); } ExceptionOr> HTMLOptionElement::createForLegacyFactoryFunction(Document& document, const String& text, const String& value, bool defaultSelected, bool selected) { auto element = create(document); if (!text.isEmpty()) { auto appendResult = element->appendChild(Text::create(document, text)); if (appendResult.hasException()) return appendResult.releaseException(); } if (!value.isNull()) element->setValue(value); if (defaultSelected) element->setAttributeWithoutSynchronization(selectedAttr, emptyAtom()); element->setSelected(selected); return element; } bool HTMLOptionElement::isFocusable() const { if (!supportsFocus()) return false; // Option elements do not have a renderer. auto* style = const_cast(*this).computedStyle(); return style && style->display() != DisplayType::None; } bool HTMLOptionElement::matchesDefaultPseudoClass() const { return hasAttributeWithoutSynchronization(selectedAttr); } String HTMLOptionElement::text() const { String 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 stripLeadingAndTrailingHTMLSpaces(document().displayStringModifiedByEncoding(text)).simplifyWhiteSpace(isHTMLSpace); } void HTMLOptionElement::setText(const String &text) { Ref protectedThis(*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 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. RefPtr child = firstChild(); if (is(child) && !child->nextSibling()) downcast(*child).setData(text); else { removeChildren(); appendChild(Text::create(document(), text)); } if (selectIsMenuList && select->selectedIndex() != oldSelectedIndex) select->setSelectedIndex(oldSelectedIndex); } bool HTMLOptionElement::accessKeyAction(bool) { RefPtr select = ownerSelectElement(); if (select) { select->accessKeySetSelectedIndex(index()); return true; } return false; } int HTMLOptionElement::index() const { // It would be faster to cache the index, but harder to get it right in all cases. RefPtr selectElement = ownerSelectElement(); if (!selectElement) return 0; int optionIndex = 0; for (auto& item : selectElement->listItems()) { if (!is(*item)) continue; if (item == this) return optionIndex; ++optionIndex; } return 0; } void HTMLOptionElement::parseAttribute(const QualifiedName& name, const AtomString& value) { #if ENABLE(DATALIST_ELEMENT) if (name == valueAttr) { for (auto& dataList : ancestorsOfType(*this)) dataList.optionElementChildrenChanged(); } else #endif if (name == disabledAttr) { bool oldDisabled = m_disabled; m_disabled = !value.isNull(); if (oldDisabled != m_disabled) { invalidateStyleForSubtree(); if (renderer() && renderer()->style().hasAppearance()) renderer()->theme().stateChanged(*renderer(), ControlStates::States::Enabled); } } else if (name == selectedAttr) { invalidateStyleForSubtree(); // 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 AtomString& value = attributeWithoutSynchronization(valueAttr); if (!value.isNull()) return value; return stripLeadingAndTrailingHTMLSpaces(collectOptionInnerText()).simplifyWhiteSpace(isHTMLSpace); } void HTMLOptionElement::setValue(const String& value) { setAttributeWithoutSynchronization(valueAttr, value); } bool HTMLOptionElement::selected() const { if (RefPtr select = ownerSelectElement()) select->updateListItemSelectedStates(); return m_isSelected; } void HTMLOptionElement::setSelected(bool selected) { if (m_isSelected == selected) return; setSelectedState(selected); if (RefPtr select = ownerSelectElement()) select->optionSelectionStateChanged(*this, selected); } void HTMLOptionElement::setSelectedState(bool selected) { if (m_isSelected == selected) return; m_isSelected = selected; invalidateStyleForSubtree(); } void HTMLOptionElement::childrenChanged(const ChildChange& change) { #if ENABLE(DATALIST_ELEMENT) for (auto& dataList : ancestorsOfType(*this)) dataList.optionElementChildrenChanged(); #endif if (RefPtr select = ownerSelectElement()) select->optionElementChildrenChanged(); HTMLElement::childrenChanged(change); } HTMLSelectElement* HTMLOptionElement::ownerSelectElement() const { return const_cast(ancestorsOfType(*this).first()); } String HTMLOptionElement::label() const { String label = attributeWithoutSynchronization(labelAttr); if (!label.isNull()) return stripLeadingAndTrailingHTMLSpaces(label); return stripLeadingAndTrailingHTMLSpaces(collectOptionInnerText()).simplifyWhiteSpace(isHTMLSpace); } // Same as label() but ignores the label content attribute in quirks mode for compatibility with other browsers. String HTMLOptionElement::displayLabel() const { if (document().inQuirksMode()) return stripLeadingAndTrailingHTMLSpaces(collectOptionInnerText()).simplifyWhiteSpace(isHTMLSpace); return label(); } void HTMLOptionElement::setLabel(const String& label) { setAttributeWithoutSynchronization(labelAttr, label); } void HTMLOptionElement::willResetComputedStyle() { // 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 { RefPtr parent = parentNode(); if (is(parent)) return " " + displayLabel(); return displayLabel(); } bool HTMLOptionElement::isDisabledFormControl() const { if (ownElementDisabled()) return true; if (!is(parentNode())) return false; return downcast(*parentNode()).isDisabledFormControl(); } Node::InsertedIntoAncestorResult HTMLOptionElement::insertedIntoAncestor(InsertionType insertionType, ContainerNode& parentOfInsertedTree) { if (RefPtr select = ownerSelectElement()) { select->setRecalcListItems(); select->updateValidity(); // 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::insertedIntoAncestor(insertionType, parentOfInsertedTree); } String HTMLOptionElement::collectOptionInnerText() const { StringBuilder text; for (RefPtr node = firstChild(); node; ) { if (is(*node)) text.append(node->nodeValue()); // Text nodes inside script elements are not part of the option text. if (is(*node) && isScriptElement(downcast(*node))) node = NodeTraversal::nextSkippingChildren(*node, this); else node = NodeTraversal::next(*node, this); } return text.toString(); } } // namespace