/* * Copyright (c) 2020, the SerenityOS developers. * Copyright (c) 2022-2024, Andreas Kling * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Web::HTML { GC_DEFINE_ALLOCATOR(HTMLOptionElement); static u64 m_next_selectedness_update_index = 1; HTMLOptionElement::HTMLOptionElement(DOM::Document& document, DOM::QualifiedName qualified_name) : HTMLElement(document, move(qualified_name)) { } HTMLOptionElement::~HTMLOptionElement() = default; void HTMLOptionElement::initialize(JS::Realm& realm) { WEB_SET_PROTOTYPE_FOR_INTERFACE(HTMLOptionElement); Base::initialize(realm); } void HTMLOptionElement::visit_edges(Cell::Visitor& visitor) { Base::visit_edges(visitor); visitor.visit(m_cached_nearest_select_element); } // FIXME: This needs to be called any time a descendant's text is modified. void HTMLOptionElement::update_selection_label() { if (selected()) { if (auto* select_element = first_ancestor_of_type()) { select_element->update_inner_text_element({}); } } } void HTMLOptionElement::attribute_changed(FlyString const& name, Optional const& old_value, Optional const& value, Optional const& namespace_) { Base::attribute_changed(name, old_value, value, namespace_); if (name == HTML::AttributeNames::selected) { if (!value.has_value()) { // Whenever an option element's selected attribute is removed, if its dirtiness is false, its selectedness must be set to false. if (!m_dirty) set_selected_internal(false); } else { // Except where otherwise specified, when the element is created, its selectedness must be set to true // if the element has a selected attribute. Whenever an option element's selected attribute is added, // if its dirtiness is false, its selectedness must be set to true. if (!m_dirty) set_selected_internal(true); } } else if (name == HTML::AttributeNames::label) { update_selection_label(); } } // https://html.spec.whatwg.org/multipage/form-elements.html#dom-option-selected void HTMLOptionElement::set_selected(bool selected) { // On setting, it must set the element's selectedness to the new value, set its dirtiness to true, and then cause the element to ask for a reset. set_selected_internal(selected); m_dirty = true; ask_for_a_reset(); } void HTMLOptionElement::set_selected_internal(bool selected) { if (m_selected != selected) invalidate_style(DOM::StyleInvalidationReason::HTMLOptionElementSelectedChange); m_selected = selected; if (selected) m_selectedness_update_index = m_next_selectedness_update_index++; // this is here to invalidate the cache on the HTMLCollection in HTMLSelectElement::selected_options document().bump_dom_tree_version(); } // https://html.spec.whatwg.org/multipage/form-elements.html#dom-option-value Utf16String HTMLOptionElement::value() const { // The value of an option element is the value of the value content attribute, if there is one. // ...or, if there is not, the value of the element's text IDL attribute. if (auto value = attribute(HTML::AttributeNames::value); value.has_value()) return Utf16String::from_utf8(*value); return text(); } // https://html.spec.whatwg.org/multipage/form-elements.html#dom-option-value void HTMLOptionElement::set_value(Utf16String const& value) { set_attribute_value(HTML::AttributeNames::value, value.to_utf8_but_should_be_ported_to_utf16()); } static void concatenate_descendants_text_content(DOM::Node const* node, StringBuilder& builder) { if (is(node) || is(node)) return; if (is(node)) builder.append(as(node)->data()); node->for_each_child([&](auto const& node) { concatenate_descendants_text_content(&node, builder); return IterationDecision::Continue; }); } // https://html.spec.whatwg.org/multipage/form-elements.html#dom-option-label String HTMLOptionElement::label() const { // The label IDL attribute, on getting, if there is a label content attribute, // must return that attribute's value; otherwise, it must return the element's label. if (auto label = attribute(HTML::AttributeNames::label); label.has_value()) return label.release_value(); return text().to_utf8_but_should_be_ported_to_utf16(); } // https://html.spec.whatwg.org/multipage/form-elements.html#dom-option-label void HTMLOptionElement::set_label(String const& label) { set_attribute_value(HTML::AttributeNames::label, label); // Note: this causes attribute_changed() to be called, which will update the 's label } // https://html.spec.whatwg.org/multipage/form-elements.html#concept-option-index int HTMLOptionElement::index() const { // An option element's index is the number of option elements that are in the same list of options but that come before it in tree order. if (auto select_element = first_ancestor_of_type()) { int index = 0; for (auto const& option_element : select_element->list_of_options()) { if (option_element.ptr() == this) return index; ++index; } } // If the option element is not in a list of options, then the option element's index is zero. return 0; } // https://html.spec.whatwg.org/multipage/form-elements.html#ask-for-a-reset void HTMLOptionElement::ask_for_a_reset() { // If an option element in the list of options asks for a reset, then run that select element's selectedness setting algorithm. if (auto* select = first_ancestor_of_type()) { select->update_selectedness(); } } // https://html.spec.whatwg.org/multipage/form-elements.html#concept-option-disabled bool HTMLOptionElement::disabled() const { // An option element is disabled if its disabled attribute is present or if it is a child of an optgroup element whose disabled attribute is present. return has_attribute(AttributeNames::disabled) || (parent() && is(parent()) && static_cast(*parent()).has_attribute(AttributeNames::disabled)); } // https://html.spec.whatwg.org/multipage/form-elements.html#dom-option-form GC::Ptr HTMLOptionElement::form() const { // The form getter steps are: // 1. Let select be this's option element nearest ancestor select. auto select = nearest_select_element(); // 2. If select is null, then return null. if (!select) return {}; // 3. Return select's form owner. return select->form(); } // https://html.spec.whatwg.org/multipage/form-elements.html#update-an-option's-nearest-ancestor-select void HTMLOptionElement::update_nearest_select_element() { // 1. Let oldSelect be option's cached nearest ancestor select element. auto old_select = m_cached_nearest_select_element; // 2. Let newSelect be option's option element nearest ancestor select. auto new_select = compute_nearest_select_element(); // 3. If oldSelect is not newSelect: if (old_select != new_select) { // 1. If oldSelect is not null, then run the selectedness setting algorithm given oldSelect. if (old_select) old_select->update_selectedness(); // 2. If newSelect is not null, then run the selectedness setting algorithm given newSelect. if (new_select) new_select->update_selectedness(); } // 4. Set option's cached nearest ancestor select element to newSelect. m_cached_nearest_select_element = new_select; } // https://html.spec.whatwg.org/multipage/form-elements.html#option-element-nearest-ancestor-select GC::Ptr HTMLOptionElement::compute_nearest_select_element() { // 1. Let ancestorOptgroup be null. GC::Ptr ancestor_optgroup; // 2. For each ancestor of option's ancestors, in reverse tree order: for (auto* ancestor = parent(); ancestor; ancestor = ancestor->parent()) { // 1. If ancestor is a datalist, hr, or option element, then return null. if (is(*ancestor) || is(*ancestor) || is(*ancestor)) return nullptr; // 2. If ancestor is an optgroup element: if (auto* optgroup_element = as_if(*ancestor)) { // 1. If ancestorOptgroup is not null, then return null. if (ancestor_optgroup) return nullptr; // 2. Set ancestorOptgroup to ancestor. ancestor_optgroup = optgroup_element; } // 3. If ancestor is a select, then return ancestor. if (auto* select_element = as_if(*ancestor)) return select_element; } // 3. Return null. return nullptr; } Optional HTMLOptionElement::default_role() const { // https://www.w3.org/TR/html-aria/#el-option // TODO: Only an option element that is in a list of options or that represents a suggestion in a datalist should return option return ARIA::Role::option; } // https://html.spec.whatwg.org/multipage/form-elements.html#the-option-element:clone-an-option-into-a-selectedcontent WebIDL::ExceptionOr HTMLOptionElement::maybe_clone_into_selectedcontent() { // To maybe clone an option into selectedcontent, given an option option: // 1. Let select be option's option element nearest ancestor select. auto select = m_cached_nearest_select_element; // 2. If all of the following conditions are true: // - select is not null; // - option's selectedness is true; and // - select's enabled selectedcontent is not null, // then run clone an option into a selectedcontent given option and select's enabled selectedcontent. if (select && selected()) { if (auto selectedcontent = select->enabled_selectedcontent()) TRY(clone_into_selectedcontent(*selectedcontent)); } return {}; } // https://html.spec.whatwg.org/multipage/form-elements.html#clone-an-option-into-a-selectedcontent WebIDL::ExceptionOr HTMLOptionElement::clone_into_selectedcontent(GC::Ref selectedcontent) { // To clone an option into a selectedcontent, given an option element option and a selectedcontent element selectedcontent: // 1. Let documentFragment be a new DocumentFragment whose node document is option's node document. auto fragment = realm().create(document()); // 2. For each child of option's children: for (auto* child = first_child(); child; child = child->next_sibling()) { // 1. Let childClone be the result of running clone given child with subtree set to true. auto child_clone = TRY(child->clone_node(&document(), true)); // 2. Append childClone to documentFragment. TRY(fragment->append_child(child_clone)); } // 3. Replace all with documentFragment within selectedcontent. selectedcontent->replace_all(fragment); return {}; } void HTMLOptionElement::inserted() { Base::inserted(); set_selected_internal(selected()); // The option HTML element insertion steps, given insertedOption, // are to run update an option's nearest ancestor select given insertedOption. update_nearest_select_element(); } void HTMLOptionElement::removed_from(Node* old_parent, Node& old_root) { Base::removed_from(old_parent, old_root); // The option HTML element removing steps, given removedOption and oldParent, // are to run update an option's nearest ancestor select given removedOption. update_nearest_select_element(); } void HTMLOptionElement::children_changed(ChildrenChangedMetadata const* metadata) { Base::children_changed(metadata); update_selection_label(); } }