blob: 1bde2dab925a692fc8819b1ca5dfad2947ecd06a [file]
/*
* 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-2026 Apple Inc. All rights reserved.
* Copyright (C) 2010-2017 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 "AXObjectCache.h"
#include "ContainerNodeInlines.h"
#include "Document.h"
#include "ElementAncestorIteratorInlines.h"
#include "EventNames.h"
#include "HTMLDataListElement.h"
#include "HTMLHRElement.h"
#include "HTMLNames.h"
#include "HTMLOptGroupElement.h"
#include "HTMLSelectElement.h"
#include "HTMLSelectedContentElement.h"
#include "HTMLSlotElement.h"
#include "HTMLSpanElement.h"
#include "KeyboardEvent.h"
#include "MouseEvent.h"
#include "NodeName.h"
#include "NodeRenderStyle.h"
#include "NodeTraversal.h"
#include "PseudoClassChangeInvalidation.h"
#include "RenderTheme.h"
#include "ScriptDisallowedScope.h"
#include "ScriptElement.h"
#include "SelectPopoverElement.h"
#include "StyleComputedStyle+GettersInlines.h"
#include "StyleResolver.h"
#include "Text.h"
#include <wtf/Ref.h>
#include <wtf/TZoneMallocInlines.h>
#include <wtf/text/MakeString.h>
namespace WebCore {
WTF_MAKE_TZONE_ALLOCATED_IMPL(HTMLOptionElement);
using namespace HTMLNames;
HTMLOptionElement::HTMLOptionElement(const QualifiedName& tagName, Document& document)
: HTMLElement(tagName, document, TypeFlag::HasCustomStyleResolveCallbacks)
{
ASSERT(hasTagName(optionTag));
}
Ref<HTMLOptionElement> HTMLOptionElement::create(Document& document)
{
return adoptRef(*new HTMLOptionElement(optionTag, document));
}
Ref<HTMLOptionElement> HTMLOptionElement::create(const QualifiedName& tagName, Document& document)
{
return adoptRef(*new HTMLOptionElement(tagName, document));
}
ExceptionOr<Ref<HTMLOptionElement>> HTMLOptionElement::createForLegacyFactoryFunction(Document& document, String&& text, const AtomString& value, bool defaultSelected, bool selected)
{
Ref element = create(document);
if (!text.isEmpty()) {
auto appendResult = element->appendChild(Text::create(document, WTF::move(text)));
if (appendResult.hasException())
return appendResult.releaseException();
}
if (!value.isNull())
element->setAttributeWithoutSynchronization(valueAttr, value);
if (defaultSelected)
element->setAttributeWithoutSynchronization(selectedAttr, emptyAtom());
element->setSelected(selected);
return element;
}
void HTMLOptionElement::didAddUserAgentShadowRoot(ShadowRoot& root)
{
Ref document = this->document();
ScriptDisallowedScope::EventAllowedScope rootScope { root };
Ref labelContainer = HTMLSpanElement::create(document);
root.appendChild(labelContainer);
m_labelContainer = WTF::move(labelContainer);
Ref slot = HTMLSlotElement::create(slotTag, document);
root.appendChild(slot);
m_slot = WTF::move(slot);
}
void HTMLOptionElement::invalidateShadowTree()
{
if (!document().settings().htmlEnhancedSelectEnabled())
return;
if (m_shadowTreeNeedsUpdate)
return;
m_shadowTreeNeedsUpdate = true;
if (isConnected())
protect(document())->addElementWithPendingUserAgentShadowTreeUpdate(*this);
}
void HTMLOptionElement::updateUserAgentShadowTree()
{
if (!m_shadowTreeNeedsUpdate)
return;
m_shadowTreeNeedsUpdate = false;
protect(document())->removeElementWithPendingUserAgentShadowTreeUpdate(*this);
if (!m_ownerSelect && !userAgentShadowRoot())
return;
if (!userAgentShadowRoot()) {
if (attributeWithoutSynchronization(labelAttr).isNull())
return;
ensureUserAgentShadowRoot();
}
Ref labelContainer = *m_labelContainer;
Ref slot = *m_slot;
auto labelValue = attributeWithoutSynchronization(labelAttr);
ScriptDisallowedScope::EventAllowedScope labelContainerScope { labelContainer };
ScriptDisallowedScope::EventAllowedScope slotScope { slot };
labelContainer->setTextContent(String { labelValue });
if (m_ownerSelect && !labelValue.isNull()) {
labelContainer->setInlineStyleProperty(CSSPropertyDisplay, CSSValueInline);
slot->setInlineStyleProperty(CSSPropertyDisplay, CSSValueNone);
} else {
labelContainer->setInlineStyleProperty(CSSPropertyDisplay, CSSValueNone);
slot->setInlineStyleProperty(CSSPropertyDisplay, CSSValueContents);
}
}
auto HTMLOptionElement::insertionSteps(InsertionType insertionType, ContainerNode& parentOfInsertedTree) -> NeedsPostConnectionSteps
{
auto result = HTMLElement::insertionSteps(insertionType, parentOfInsertedTree);
if (document().settings().htmlEnhancedSelectParsingEnabled() && !m_ownerSelect) {
if (RefPtr select = HTMLSelectElement::findOwnerSelect(parentNode(), HTMLSelectElement::ExcludeOptGroup::No)) {
m_ownerSelect = select.get();
select->setRecalcListItems();
// If this non-selected option is the first non-disabled option in a
// single-select that has no explicitly selected option, select it by
// default. This maintains m_isSelected incrementally so that
// finishParsingChildren() can use selectedWithoutUpdate() (O(1))
// instead of selected() which triggers O(n) recalcListItems().
// Only do this during parsing — for API insertions, the existing
// childrenChanged → optionToSelectFromChildChangeScope path handles it.
if (!select->isFinishedParsingChildren() && !selectedWithoutUpdate() && !m_disabled)
select->selectDefaultOptionIfNeeded(*this);
}
}
if (insertionType.connectedToDocument) {
if (RefPtr select = ownerSelectElement())
select->invalidateButtonText();
if (m_shadowTreeNeedsUpdate)
protect(document())->addElementWithPendingUserAgentShadowTreeUpdate(*this);
}
return result;
}
void HTMLOptionElement::removingSteps(RemovalType removalType, ContainerNode& oldParentOfRemovedTree)
{
HTMLElement::removingSteps(removalType, oldParentOfRemovedTree);
if (removalType.disconnectedFromDocument && m_shadowTreeNeedsUpdate)
protect(document())->removeElementWithPendingUserAgentShadowTreeUpdate(*this);
if (!document().settings().htmlEnhancedSelectParsingEnabled() || !m_ownerSelect)
return;
if (auto* select = HTMLSelectElement::findOwnerSelect(parentNode(), HTMLSelectElement::ExcludeOptGroup::No)) {
ASSERT_UNUSED(select, select == m_ownerSelect.get());
return;
}
if (RefPtr select = std::exchange(m_ownerSelect, nullptr).get()) {
select->setRecalcListItems();
select->invalidateButtonText();
invalidateShadowTree();
}
}
void HTMLOptionElement::movingSteps(IsSubtreeRoot isSubtreeRoot, ContainerNode& oldParent)
{
HTMLElement::movingSteps(isSubtreeRoot, oldParent);
if (isSubtreeRoot == IsSubtreeRoot::No)
return;
if (!document().settings().htmlEnhancedSelectParsingEnabled())
return;
RefPtr oldSelect = m_ownerSelect;
RefPtr newSelect = HTMLSelectElement::findOwnerSelect(parentNode(), HTMLSelectElement::ExcludeOptGroup::No);
if (oldSelect == newSelect) {
if (newSelect) {
newSelect->setRecalcListItems();
newSelect->invalidateButtonText();
}
return;
}
m_ownerSelect = newSelect.get();
if (oldSelect) {
oldSelect->setRecalcListItems();
oldSelect->invalidateButtonText();
}
if (newSelect) {
newSelect->setRecalcListItems();
newSelect->invalidateButtonText();
}
invalidateShadowTree();
}
void HTMLOptionElement::finishParsingChildren()
{
if (!document().settings().htmlEnhancedSelectEnabled())
return;
if (document().settings().mutationEventsEnabled())
return;
ASSERT(document().settings().htmlEnhancedSelectParsingEnabled());
if (m_disabled)
return;
RefPtr select = m_ownerSelect;
if (!select)
return;
// When the owning <select> is still being parsed, use selectedWithoutUpdate()
// instead of selected() to avoid triggering recalcListItems() for each
// option, which would be O(n²). The selection state (m_isSelected) is
// maintained incrementally at insertion time — see insertionSteps() and
// optionToSelectFromChildChangeScope().
bool isSelected = select->isFinishedParsingChildren() ? selected() : selectedWithoutUpdate();
if (!isSelected)
return;
// Pass `this` to avoid updateSelectedContent() calling listItems() to find
// the selected option, which would also trigger recalcListItems().
select->updateSelectedContent(this);
}
bool HTMLOptionElement::supportsFocus() const
{
return HTMLElement::supportsFocus() || belongsToBaseAppearancePicker();
}
bool HTMLOptionElement::isFocusable() const
{
RefPtr select = ownerSelectElement();
if (select && select->usesMenuList() && !select->usesBaseAppearancePicker())
return false;
return HTMLElement::isFocusable();
}
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 protect(document())->displayStringModifiedByEncoding(text).trim(isASCIIWhitespace).simplifyWhiteSpace(isASCIIWhitespace);
}
void HTMLOptionElement::setText(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->usesMenuListDeprecated();
int oldSelectedIndex = selectIsMenuList ? select->selectedIndex() : -1;
setTextContent(WTF::move(text));
if (selectIsMenuList && select->selectedIndex() != oldSelectedIndex)
select->setSelectedIndex(oldSelectedIndex);
}
bool HTMLOptionElement::accessKeyAction(bool)
{
RefPtr select = ownerSelectElement();
if (!select)
return false;
if (select->usesBaseAppearancePicker()) {
select->optionSelectedByUser(index(), true);
select->hidePickerPopoverElement();
} else
select->accessKeySetSelectedIndex(index());
return true;
}
void HTMLOptionElement::defaultEventHandler(Event& event)
{
if (!event.isTrusted())
return HTMLElement::defaultEventHandler(event);
RefPtr select = ownerSelectElement();
if (!select || !select->document().settings().htmlEnhancedSelectEnabled() || !select->usesBaseAppearancePicker())
return HTMLElement::defaultEventHandler(event);
auto& eventNames = WebCore::eventNames();
if (event.type() == eventNames.keydownEvent) {
RefPtr keyboardEvent = dynamicDowncast<KeyboardEvent>(event);
if (!keyboardEvent)
return HTMLElement::defaultEventHandler(event);
const String& keyIdentifier = keyboardEvent->keyIdentifier();
// [Shift+]Tab closes the picker; fall through to move focus.
if (keyIdentifier == "U+0009"_s) {
select->hidePickerPopoverElement();
return HTMLElement::defaultEventHandler(event);
}
int currentIndex = select->optionToListIndex(index());
int listIndex = select->computeNavigationIndex(keyIdentifier, currentIndex, select->pickerNavigationKeyIdentifiers());
if (listIndex >= 0) {
auto scrollMode = HTMLSelectElement::PickerScrollMode::Nearest;
if (keyIdentifier == "PageDown"_s)
scrollMode = HTMLSelectElement::PickerScrollMode::AlignBottom;
else if (keyIdentifier == "PageUp"_s)
scrollMode = HTMLSelectElement::PickerScrollMode::AlignTop;
select->focusOptionAtIndex(listIndex, std::nullopt, scrollMode);
keyboardEvent->setDefaultHandled();
return;
}
}
if (event.type() == eventNames.keypressEvent) {
RefPtr keyboardEvent = dynamicDowncast<KeyboardEvent>(event);
if (!keyboardEvent)
return HTMLElement::defaultEventHandler(event);
int keyCode = keyboardEvent->keyCode();
if (keyCode == '\r' || keyCode == ' ') {
select->optionSelectedByUser(index(), true);
select->hidePickerPopoverElement();
keyboardEvent->setDefaultHandled();
return;
}
if (!keyboardEvent->ctrlKey() && !keyboardEvent->altKey() && !keyboardEvent->metaKey() && u_isprint(keyboardEvent->charCode())) {
int listIndex = select->typeAheadMatchIndex(*keyboardEvent);
if (listIndex >= 0)
select->focusOptionAtIndex(listIndex);
keyboardEvent->setDefaultHandled();
return;
}
}
if (RefPtr mouseEvent = dynamicDowncast<MouseEvent>(event); mouseEvent && event.type() == eventNames.mousedownEvent && mouseEvent->button() == MouseButton::Left) {
select->optionSelectedByUser(index(), true);
select->hidePickerPopoverElement();
event.setDefaultHandled();
return;
}
HTMLElement::defaultEventHandler(event);
}
HTMLFormElement* HTMLOptionElement::form() const
{
if (auto* selectElement = ownerSelectElement())
return selectElement->form();
return nullptr;
}
RefPtr<HTMLFormElement> HTMLOptionElement::formForBindings() const
{
// FIXME: The downcast should be unnecessary, but the WPT was written before https://github.com/WICG/webcomponents/issues/1072 was resolved. Update once the WPT has been updated.
return dynamicDowncast<HTMLFormElement>(retargetReferenceTargetForBindings(form()));
}
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<HTMLOptionElement>(*item))
continue;
if (item == this)
return optionIndex;
++optionIndex;
}
return 0;
}
void HTMLOptionElement::attributeChanged(const QualifiedName& name, const AtomString& oldValue, const AtomString& newValue, AttributeModificationReason attributeModificationReason)
{
switch (name.nodeName()) {
case AttributeNames::disabledAttr: {
bool newDisabled = !newValue.isNull();
if (m_disabled != newDisabled) {
Style::PseudoClassChangeInvalidation disabledInvalidation(*this, { { CSSSelector::PseudoClass::Disabled, newDisabled }, { CSSSelector::PseudoClass::Enabled, !newDisabled } });
m_disabled = newDisabled;
if (CheckedPtr renderer = this->renderer(); renderer && renderer->style().hasUsedAppearance())
renderer->repaint();
}
break;
}
case AttributeNames::selectedAttr: {
// FIXME: Use PseudoClassChangeInvalidation in other elements that implement matchesDefaultPseudoClass().
Style::PseudoClassChangeInvalidation defaultInvalidation(*this, CSSSelector::PseudoClass::Default, !newValue.isNull());
m_isDefault = !newValue.isNull();
// https://html.spec.whatwg.org/multipage/form-elements.html#concept-option-selectedness
if (oldValue.isNull() != newValue.isNull() && !m_isDirty)
setSelected(!newValue.isNull());
break;
}
case AttributeNames::labelAttr: {
if (RefPtr select = ownerSelectElement())
select->optionElementChildrenChanged();
invalidateShadowTree();
break;
}
case AttributeNames::valueAttr:
for (Ref dataList : ancestorsOfType<HTMLDataListElement>(*this))
dataList->optionElementChildrenChanged();
break;
default:
HTMLElement::attributeChanged(name, oldValue, newValue, attributeModificationReason);
break;
}
}
String HTMLOptionElement::value() const
{
const AtomString& value = attributeWithoutSynchronization(valueAttr);
if (!value.isNull())
return value;
return collectOptionInnerTextCollapsingWhitespace();
}
bool HTMLOptionElement::selected(AllowStyleInvalidation allowStyleInvalidation) const
{
if (RefPtr select = ownerSelectElement())
select->updateListItemSelectedStates(allowStyleInvalidation);
return m_isSelected;
}
void HTMLOptionElement::setSelected(bool selected)
{
if (m_isSelected == selected)
return;
setSelectedState(selected);
if (RefPtr select = ownerSelectElement())
select->optionSelectionStateChanged(*this, selected);
}
bool HTMLOptionElement::selectedForBindings() const
{
return selected();
}
void HTMLOptionElement::setSelectedForBindings(bool selected)
{
bool wasSelected = m_isSelected;
setSelected(selected);
// https://html.spec.whatwg.org/multipage/form-elements.html#concept-option-dirtiness
// The spec says dirtiness becomes true unconditionally. However, for web
// compatibility, don't set dirtiness if the option is owned by a select
// element and selectedness did not actually change.
if (ownerSelectElement() && wasSelected == m_isSelected)
return;
m_isDirty = true;
}
void HTMLOptionElement::setSelectedState(bool selected, AllowStyleInvalidation allowStyleInvalidation)
{
if (m_isSelected == selected)
return;
std::optional<Style::PseudoClassChangeInvalidation> checkedInvalidation;
if (allowStyleInvalidation == AllowStyleInvalidation::Yes)
emplace(checkedInvalidation, *this, { { CSSSelector::PseudoClass::Checked, selected } });
m_isSelected = selected;
if (CheckedPtr cache = protect(document())->existingAXObjectCache())
cache->onSelectedOptionChanged(*this);
}
void HTMLOptionElement::childrenChanged(const ChildChange& change)
{
Vector<Ref<HTMLDataListElement>> ancestors;
for (Ref dataList : ancestorsOfType<HTMLDataListElement>(*this))
ancestors.append(WTF::move(dataList));
for (auto& dataList : ancestors)
dataList->optionElementChildrenChanged();
if (change.source != ChildChange::Source::Clone) {
if (RefPtr select = ownerSelectElement())
select->optionElementChildrenChanged();
}
HTMLElement::childrenChanged(change);
}
void HTMLOptionElement::willResetComputedStyle()
{
if (RefPtr select = ownerSelectElement()) {
if (CheckedPtr selectRenderer = select->renderer())
selectRenderer->repaint();
}
}
HTMLSelectElement* HTMLOptionElement::ownerSelectElement() const
{
if (document().settings().htmlEnhancedSelectParsingEnabled())
return m_ownerSelect.get();
if (auto* parent = parentElement()) {
if (auto* select = dynamicDowncast<HTMLSelectElement>(*parent))
return select;
if (auto* optGroup = dynamicDowncast<HTMLOptGroupElement>(*parent))
return optGroup->ownerSelectElement();
}
return nullptr;
}
bool HTMLOptionElement::belongsToBaseAppearancePicker() const
{
RefPtr select = ownerSelectElement();
return select && select->usesBaseAppearancePicker();
}
String HTMLOptionElement::label() const
{
String label = attributeWithoutSynchronization(labelAttr);
if (!label.isNull())
return label;
return collectOptionInnerTextCollapsingWhitespace();
}
String HTMLOptionElement::displayLabel() const
{
String label = attributeWithoutSynchronization(labelAttr);
if (label.isEmpty())
return collectOptionInnerTextCollapsingWhitespace();
return label;
}
String HTMLOptionElement::textIndentedToRespectGroupLabel() const
{
if (!document().settings().htmlEnhancedSelectParsingEnabled()) {
if (is<HTMLOptGroupElement>(parentNode()))
return makeString(" "_s, displayLabel());
return displayLabel();
}
for (Ref ancestor : ancestorsOfType<HTMLElement>(*this)) {
if (is<HTMLOptGroupElement>(ancestor))
return makeString(" "_s, displayLabel());
if (isAnyOf<HTMLDataListElement, HTMLSelectElement, HTMLOptionElement, HTMLHRElement>(ancestor))
return displayLabel();
}
return displayLabel();
}
bool HTMLOptionElement::isDisabledFormControl() const
{
if (ownElementDisabled())
return true;
if (!document().settings().htmlEnhancedSelectParsingEnabled()) {
RefPtr parentOptGroup = dynamicDowncast<HTMLOptGroupElement>(parentNode());
return parentOptGroup && parentOptGroup->isDisabledFormControl();
}
for (Ref ancestor : ancestorsOfType<HTMLElement>(*this)) {
if (RefPtr optGroup = dynamicDowncast<HTMLOptGroupElement>(ancestor))
return optGroup->isDisabledFormControl();
if (isAnyOf<HTMLDataListElement, HTMLSelectElement, HTMLOptionElement, HTMLHRElement>(ancestor))
return false;
}
return false;
}
bool HTMLOptionElement::isActuallyDisabled() const
{
if (HTMLElement::isActuallyDisabled())
return true;
RefPtr select = ownerSelectElement();
return select && select->isDisabledFormControl();
}
String HTMLOptionElement::collectOptionInnerText() const
{
StringBuilder text;
// Text nodes inside script elements are not part of the option text.
for (RefPtr node = firstChild(); node; node = isScriptElement(*node) ? NodeTraversal::nextSkippingChildren(*node, this) : NodeTraversal::next(*node, this)) {
if (auto* textNode = dynamicDowncast<Text>(*node))
text.append(textNode->data());
}
return text.toString();
}
String HTMLOptionElement::collectOptionInnerTextCollapsingWhitespace() const
{
return collectOptionInnerText().trim(isASCIIWhitespace).simplifyWhiteSpace(isASCIIWhitespace);
}
void HTMLOptionElement::cloneIntoSelectedContent(HTMLSelectedContentElement& selectedContent)
{
ASSERT(document().settings().htmlEnhancedSelectParsingEnabled());
ASSERT(document().settings().htmlEnhancedSelectEnabled());
ASSERT(!selectedContent.document().settings().mutationEventsEnabled());
NodeVector newChildren;
for (RefPtr child = firstChild(); child; child = child->nextSibling())
newChildren.append(child->cloneNode(true));
selectedContent.replaceChildrenWithoutValidityCheck(WTF::move(newChildren));
}
} // namespace