/* * Copyright (c) 2018-2024, Andreas Kling * Copyright (c) 2025, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Web::HTML { GC_DEFINE_ALLOCATOR(HTMLElement); HTMLElement::HTMLElement(DOM::Document& document, DOM::QualifiedName qualified_name) : Element(document, move(qualified_name)) { } HTMLElement::~HTMLElement() = default; void HTMLElement::initialize(JS::Realm& realm) { WEB_SET_PROTOTYPE_FOR_INTERFACE(HTMLElement); Base::initialize(realm); } void HTMLElement::visit_edges(Cell::Visitor& visitor) { Base::visit_edges(visitor); HTMLOrSVGElement::visit_edges(visitor); visitor.visit(m_labels); visitor.visit(m_attached_internals); visitor.visit(m_popover_trigger); visitor.visit(m_popover_close_watcher); } // https://html.spec.whatwg.org/multipage/dom.html#dom-translate bool HTMLElement::translate() const { // The translate IDL attribute must, on getting, return true if the element's translation mode is // translate-enabled, and false otherwise return translation_mode() == TranslationMode::TranslateEnabled; } // https://html.spec.whatwg.org/multipage/dom.html#dom-translate void HTMLElement::set_translate(bool new_value) { // On setting, it must set the content attribute's value to "yes" if the new value is true, and set the content // attribute's value to "no" otherwise. if (new_value) set_attribute_value(HTML::AttributeNames::translate, "yes"_string); else set_attribute_value(HTML::AttributeNames::translate, "no"_string); } // https://html.spec.whatwg.org/multipage/dom.html#dom-dir StringView HTMLElement::dir() const { // FIXME: This should probably be `Reflect` in the IDL. // The dir IDL attribute on an element must reflect the dir content attribute of that element, limited to only known values. auto dir = get_attribute_value(HTML::AttributeNames::dir); #define __ENUMERATE_HTML_ELEMENT_DIR_ATTRIBUTE(keyword) \ if (dir.equals_ignoring_ascii_case(#keyword##sv)) \ return #keyword##sv; ENUMERATE_HTML_ELEMENT_DIR_ATTRIBUTES #undef __ENUMERATE_HTML_ELEMENT_DIR_ATTRIBUTE return {}; } void HTMLElement::set_dir(String const& dir) { set_attribute_value(HTML::AttributeNames::dir, dir); } bool HTMLElement::is_focusable() const { return Base::is_focusable() || is_editing_host(); } // https://html.spec.whatwg.org/multipage/interaction.html#dom-iscontenteditable bool HTMLElement::is_content_editable() const { // The isContentEditable IDL attribute, on getting, must return true if the element is either an editing host or // editable, and false otherwise. return is_editable_or_editing_host(); } StringView HTMLElement::content_editable() const { switch (m_content_editable_state) { case ContentEditableState::True: return "true"sv; case ContentEditableState::False: return "false"sv; case ContentEditableState::PlaintextOnly: return "plaintext-only"sv; case ContentEditableState::Inherit: return "inherit"sv; } VERIFY_NOT_REACHED(); } // https://html.spec.whatwg.org/multipage/interaction.html#contenteditable WebIDL::ExceptionOr HTMLElement::set_content_editable(StringView content_editable) { if (content_editable.equals_ignoring_ascii_case("inherit"sv)) { remove_attribute(HTML::AttributeNames::contenteditable); return {}; } if (content_editable.equals_ignoring_ascii_case("true"sv)) { set_attribute_value(HTML::AttributeNames::contenteditable, "true"_string); return {}; } if (content_editable.equals_ignoring_ascii_case("plaintext-only"sv)) { set_attribute_value(HTML::AttributeNames::contenteditable, "plaintext-only"_string); return {}; } if (content_editable.equals_ignoring_ascii_case("false"sv)) { set_attribute_value(HTML::AttributeNames::contenteditable, "false"_string); return {}; } return WebIDL::SyntaxError::create(realm(), "Invalid contentEditable value, must be 'true', 'false', 'plaintext-only' or 'inherit'"_utf16); } // https://html.spec.whatwg.org/multipage/dom.html#set-the-inner-text-steps void HTMLElement::set_inner_text(Utf16View const& text) { // 1. Let fragment be the rendered text fragment for value given element's node document. auto fragment = rendered_text_fragment(text); // 2. Replace all with fragment within element. replace_all(fragment); set_needs_style_update(true); } // https://html.spec.whatwg.org/multipage/dom.html#merge-with-the-next-text-node static void merge_with_the_next_text_node(DOM::Text& node) { // 1. Let next be node's next sibling. auto next = node.next_sibling(); // 2. If next is not a Text node, then return. auto* text = as_if(next); if (!text) return; // 3. Replace data with node, node's data's length, 0, and next's data. MUST(node.replace_data(node.length_in_utf16_code_units(), 0, text->data())); // 4. Remove next. next->remove(); } // https://html.spec.whatwg.org/multipage/dom.html#the-innertext-idl-attribute:dom-outertext-2 WebIDL::ExceptionOr HTMLElement::set_outer_text(Utf16View const& value) { // 1. If this's parent is null, then throw a "NoModificationAllowedError" DOMException. if (!parent()) return WebIDL::NoModificationAllowedError::create(realm(), "setOuterText: parent is null"_utf16); // 2. Let next be this's next sibling. auto* next = next_sibling(); // 3. Let previous be this's previous sibling. auto* previous = previous_sibling(); // 4. Let fragment be the rendered text fragment for the given value given this's node document. auto fragment = rendered_text_fragment(value); // 5. If fragment has no children, then append a new Text node whose data is the empty string and node document is this's node document to fragment. if (!fragment->has_children()) MUST(fragment->append_child(document().create_text_node({}))); // 6. Replace this with fragment within this's parent. MUST(parent()->replace_child(fragment, *this)); // 7. If next is non-null and next's previous sibling is a Text node, then merge with the next text node given next's previous sibling. if (next && is(next->previous_sibling())) merge_with_the_next_text_node(static_cast(*next->previous_sibling())); // 8. If previous is a Text node, then merge with the next text node given previous. if (auto* previous_text = as_if(previous)) merge_with_the_next_text_node(*previous_text); set_needs_style_update(true); return {}; } // https://html.spec.whatwg.org/multipage/dom.html#rendered-text-fragment GC::Ref HTMLElement::rendered_text_fragment(Utf16View const& input) { // 1. Let fragment be a new DocumentFragment whose node document is document. auto fragment = realm().create(document()); // 2. Let position be a position variable for input, initially pointing at the start of input. size_t position = 0; // 3. Let text be the empty string. // 4. While position is not past the end of input: while (position < input.length_in_code_units()) { auto start = position; // 1. Collect a sequence of code points that are not U+000A LF or U+000D CR from input given position, and set // text to the result. while (position < input.length_in_code_units() && !first_is_one_of(input.code_unit_at(position), u'\n', u'\r')) ++position; auto text = input.substring_view(start, position - start); // 2. If text is not the empty string, then append a new Text node whose data is text and node document is // document to fragment. if (!text.is_empty()) { MUST(fragment->append_child(document().create_text_node(Utf16String::from_utf16(text)))); } // 3. While position is not past the end of input, and the code point at position is either U+000A LF or U+000D CR: while (position < input.length_in_code_units() && first_is_one_of(input.code_unit_at(position), u'\n', u'\r')) { // 1. If the code point at position is U+000D CR and the next code point is U+000A LF, then advance position // to the next code point in input. if (input.code_unit_at(position) == '\r') { if (position + 1 < input.length_in_code_units() && input.code_unit_at(position + 1) == '\n') ++position; } // 2. Advance position to the next code point in input. ++position; // 3. Append the result of creating an element given document, "br", and the HTML namespace to fragment. auto br_element = MUST(DOM::create_element(document(), HTML::TagNames::br, Namespace::HTML)); MUST(fragment->append_child(br_element)); } } // 5. Return fragment. return fragment; } struct RequiredLineBreakCount { int count { 0 }; }; // https://html.spec.whatwg.org/multipage/dom.html#rendered-text-collection-steps static Vector> rendered_text_collection_steps(DOM::Node const& node) { // 1. Let items be the result of running the rendered text collection steps with each child node of node in tree // order, and then concatenating the results to a single list. Vector> items; node.for_each_child([&](auto const& child) { auto child_items = rendered_text_collection_steps(child); items.extend(move(child_items)); return IterationDecision::Continue; }); // NOTE: Steps are re-ordered here a bit. // 3. If node is not being rendered, then return items. // For the purpose of this step, the following elements must act as described // if the computed value of the 'display' property is not 'none': // FIXME: - select elements have an associated non-replaced inline CSS box whose child boxes include only those of optgroup and option element child nodes; // FIXME: - optgroup elements have an associated non-replaced block-level CSS box whose child boxes include only those of option element child nodes; and // FIXME: - option elements have an associated non-replaced block-level CSS box whose child boxes are as normal for non-replaced block-level CSS boxes. auto* layout_node = node.layout_node(); if (!layout_node) return items; auto const& computed_values = layout_node->computed_values(); // 2. If node's computed value of 'visibility' is not 'visible', then return items. if (computed_values.visibility() != CSS::Visibility::Visible) return items; // AD-HOC: If node's computed value of 'content-visibility' is 'hidden', then return items. if (computed_values.content_visibility() == CSS::ContentVisibility::Hidden) return items; // 4. If node is a Text node, then for each CSS text box produced by node, in content order, compute the text of the // box after application of the CSS 'white-space' processing rules and 'text-transform' rules, set items to the // list of the resulting strings, and return items. // FIXME: The CSS 'white-space' processing rules are slightly modified: collapsible spaces at the end of lines are // always collapsed, but they are only removed if the line is the last line of the block, or it ends with a br // element. Soft hyphens should be preserved. [CSSTEXT] if (auto const* layout_text_node = as_if(layout_node)) { Layout::TextNode::ChunkIterator iterator { *layout_text_node, false, false }; while (true) { auto chunk = iterator.next(); if (!chunk.has_value()) break; items.append(Utf16String::from_utf16(chunk.release_value().view)); } return items; } // 5. If node is a br element, then append a string containing a single U+000A LF code point to items. if (is(node)) { items.append("\n"_utf16); return items; } auto display = computed_values.display(); // 6. If node's computed value of 'display' is 'table-cell', and node's CSS box is not the last 'table-cell' box of its enclosing 'table-row' box, then append a string containing a single U+0009 TAB code point to items. if (display.is_table_cell() && node.next_sibling()) items.append("\t"_utf16); // 7. If node's computed value of 'display' is 'table-row', and node's CSS box is not the last 'table-row' box of the nearest ancestor 'table' box, then append a string containing a single U+000A LF code point to items. if (display.is_table_row() && node.next_sibling()) items.append("\n"_utf16); // 8. If node is a p element, then append 2 (a required line break count) at the beginning and end of items. if (is(node)) { items.prepend(RequiredLineBreakCount { 2 }); items.append(RequiredLineBreakCount { 2 }); } // 9. If node's used value of 'display' is block-level or 'table-caption', then append 1 (a required line break count) at the beginning and end of items. [CSSDISPLAY] if (display.is_block_outside() || display.is_table_caption()) { items.prepend(RequiredLineBreakCount { 1 }); items.append(RequiredLineBreakCount { 1 }); } // 10. Return items. return items; } // https://html.spec.whatwg.org/multipage/dom.html#get-the-text-steps Utf16String HTMLElement::get_the_text_steps() { // 1. If element is not being rendered or if the user agent is a non-CSS user agent, then return element's descendant text content. document().update_layout(DOM::UpdateLayoutReason::HTMLElementGetTheTextSteps); if (!layout_node()) return descendant_text_content(); // 2. Let results be a new empty list. Vector> results; // 3. For each child node node of element: for_each_child([&](Node const& node) { // 1. Let current be the list resulting in running the rendered text collection steps with node. // Each item in results will either be a string or a positive integer (a required line break count). auto current = rendered_text_collection_steps(node); // 2. For each item item in current, append item to results. results.extend(move(current)); return IterationDecision::Continue; }); // 4. Remove any items from results that are the empty string. results.remove_all_matching([](auto& item) { return item.visit( [](Utf16String const& string) { return string.is_empty(); }, [](RequiredLineBreakCount const&) { return false; }); }); // 5. Remove any runs of consecutive required line break count items at the start or end of results. while (!results.is_empty() && results.first().has()) results.take_first(); while (!results.is_empty() && results.last().has()) results.take_last(); // 6. Replace each remaining run of consecutive required line break count items with a string consisting of as many // U+000A LF code points as the maximum of the values in the required line break count items. StringBuilder builder(StringBuilder::Mode::UTF16); for (size_t i = 0; i < results.size(); ++i) { results[i].visit( [&](Utf16String const& string) { builder.append(string); }, [&](RequiredLineBreakCount const& line_break_count) { int max_line_breaks = line_break_count.count; size_t j = i + 1; while (j < results.size() && results[j].has()) { max_line_breaks = max(max_line_breaks, results[j].get().count); ++j; } // Skip over the run of required line break counts. i = j - 1; builder.append_repeated('\n', max_line_breaks); }); } // 7. Return the concatenation of the string items in results. return builder.to_utf16_string(); } // https://html.spec.whatwg.org/multipage/dom.html#dom-innertext Utf16String HTMLElement::inner_text() { // The innerText and outerText getter steps are to return the result of running get the text steps with this. return get_the_text_steps(); } // https://html.spec.whatwg.org/multipage/dom.html#dom-outertext Utf16String HTMLElement::outer_text() { // The innerText and outerText getter steps are to return the result of running get the text steps with this. return get_the_text_steps(); } // https://drafts.csswg.org/cssom-view/#dom-htmlelement-scrollparent GC::Ptr HTMLElement::scroll_parent() const { // NOTE: We have to ensure that the layout is up-to-date before querying the layout tree. const_cast(document()).update_layout(DOM::UpdateLayoutReason::HTMLElementScrollParent); // 1. If any of the following holds true, return null and terminate this algorithm: // - The element does not have an associated box. // - The element is the root element. // - The element is the body element. // - FIXME: The element’s computed value of the position property is fixed and no ancestor establishes a fixed position containing block. if (!layout_node()) return nullptr; if (is_document_element()) return nullptr; if (is_html_body_element()) return nullptr; // 2. Let ancestor be the containing block of the element in the flat tree and repeat these substeps: auto ancestor = layout_node()->containing_block(); while (ancestor) { // 1. If ancestor is the initial containing block, return the scrollingElement for the element’s document if it // is not closed-shadow-hidden from the element, otherwise return null. if (ancestor->is_viewport()) { auto const scrolling_element = document().scrolling_element(); if (scrolling_element && !scrolling_element->is_closed_shadow_hidden_from(*this)) return const_cast(scrolling_element.ptr()); return nullptr; } // 2. If ancestor is not closed-shadow-hidden from the element, and is a scroll container, terminate this // algorithm and return ancestor. if ((ancestor->dom_node() && !ancestor->dom_node()->is_closed_shadow_hidden_from(*this)) && ancestor->is_scroll_container()) { return const_cast(static_cast(ancestor->dom_node())); } // FIXME: 3. If the computed value of the position property of ancestor is fixed, and no ancestor establishes a fixed // position containing block, terminate this algorithm and return null. // 4. Let ancestor be the containing block of ancestor in the flat tree. ancestor = ancestor->containing_block(); } return nullptr; } // https://www.w3.org/TR/cssom-view-1/#dom-htmlelement-offsetparent GC::Ptr HTMLElement::offset_parent() const { const_cast(document()).update_layout(DOM::UpdateLayoutReason::HTMLElementOffsetParent); // 1. If any of the following holds true return null and terminate this algorithm: // - The element does not have an associated box. // - The element is the root element. // - The element is the HTML body element. // - The element’s computed value of the position property is fixed. if (!layout_node()) return nullptr; if (is_document_element()) return nullptr; if (is_html_body_element()) return nullptr; if (layout_node()->is_fixed_position()) return nullptr; // 2. Let ancestor be the parent of the element in the flat tree and repeat these substeps: auto ancestor = shadow_including_first_ancestor_of_type(); while (true) { bool ancestor_is_closed_shadow_hidden = ancestor->is_closed_shadow_hidden_from(*this); // 1. If ancestor is closed-shadow-hidden from the element and its computed value of the position property is // fixed, terminate this algorithm and return null. if (ancestor_is_closed_shadow_hidden && ancestor->computed_properties()->position() == CSS::Positioning::Fixed) return nullptr; // 2. If ancestor is not closed-shadow-hidden from the element and satisfies at least one of the following, // terminate this algorithm and return ancestor. if (!ancestor_is_closed_shadow_hidden) { // - ancestor is a containing block of absolutely-positioned descendants (regardless of whether there are // any absolutely-positioned descendants). if (ancestor->layout_node()->is_positioned()) return const_cast(ancestor); // - FIXME: The element has a different effective zoom than ancestor. // - It is the body element. if (ancestor->is_html_body_element()) return const_cast(ancestor); // - The computed value of the position property of the element is static and the ancestor is one of the following HTML elements: td, th, or table. if (computed_properties()->position() == CSS::Positioning::Static && ancestor->local_name().is_one_of(HTML::TagNames::td, HTML::TagNames::th, HTML::TagNames::table)) return const_cast(ancestor); } // 3. If there is no more parent of ancestor in the flat tree, terminate this algorithm and return null. auto parent_of_ancestor = ancestor->shadow_including_first_ancestor_of_type(); if (!parent_of_ancestor) return nullptr; // 4. Let ancestor be the parent of ancestor in the flat tree. ancestor = parent_of_ancestor; } } // https://www.w3.org/TR/cssom-view-1/#dom-htmlelement-offsettop int HTMLElement::offset_top() const { // 1. If the element is the HTML body element or does not have any associated CSS layout box // return zero and terminate this algorithm. if (is(*this)) return 0; // NOTE: Ensure that layout is up-to-date before looking at metrics. const_cast(document()).update_layout(DOM::UpdateLayoutReason::HTMLElementOffsetTop); if (!paintable_box()) return 0; CSSPixels top_border_edge_of_element = paintable_box()->absolute_united_border_box_rect().y(); // 2. If the offsetParent of the element is null // return the y-coordinate of the top border edge of the first CSS layout box associated with the element, // relative to the initial containing block origin, // ignoring any transforms that apply to the element and its ancestors, and terminate this algorithm. auto offset_parent = this->offset_parent(); if (!offset_parent || !offset_parent->layout_node()) { return top_border_edge_of_element.to_int(); } // 3. Return the result of subtracting the y-coordinate of the top padding edge // of the first box associated with the offsetParent of the element // from the y-coordinate of the top border edge of the first box associated with the element, // relative to the initial containing block origin, // ignoring any transforms that apply to the element and its ancestors. // NOTE: We give special treatment to the body element to match other browsers. // Spec bug: https://github.com/w3c/csswg-drafts/issues/10549 CSSPixels top_padding_edge_of_offset_parent; if (offset_parent->is_html_body_element() && !offset_parent->paintable_box()->is_positioned()) { top_padding_edge_of_offset_parent = 0; } else { top_padding_edge_of_offset_parent = offset_parent->paintable_box()->absolute_united_padding_box_rect().y(); } return (top_border_edge_of_element - top_padding_edge_of_offset_parent).to_int(); } // https://www.w3.org/TR/cssom-view-1/#dom-htmlelement-offsetleft int HTMLElement::offset_left() const { // 1. If the element is the HTML body element or does not have any associated CSS layout box return zero and terminate this algorithm. if (is(*this)) return 0; // NOTE: Ensure that layout is up-to-date before looking at metrics. const_cast(document()).update_layout(DOM::UpdateLayoutReason::HTMLElementOffsetLeft); if (!paintable_box()) return 0; CSSPixels left_border_edge_of_element = paintable_box()->absolute_united_border_box_rect().x(); // 2. If the offsetParent of the element is null // return the x-coordinate of the left border edge of the first CSS layout box associated with the element, // relative to the initial containing block origin, // ignoring any transforms that apply to the element and its ancestors, and terminate this algorithm. auto offset_parent = this->offset_parent(); if (!offset_parent || !offset_parent->layout_node()) { return left_border_edge_of_element.to_int(); } // 3. Return the result of subtracting the x-coordinate of the left padding edge // of the first CSS layout box associated with the offsetParent of the element // from the x-coordinate of the left border edge of the first CSS layout box associated with the element, // relative to the initial containing block origin, // ignoring any transforms that apply to the element and its ancestors. // NOTE: We give special treatment to the body element to match other browsers. // Spec bug: https://github.com/w3c/csswg-drafts/issues/10549 CSSPixels left_padding_edge_of_offset_parent; if (offset_parent->is_html_body_element() && !offset_parent->paintable_box()->is_positioned()) { left_padding_edge_of_offset_parent = 0; } else { left_padding_edge_of_offset_parent = offset_parent->paintable_box()->absolute_united_padding_box_rect().x(); } return (left_border_edge_of_element - left_padding_edge_of_offset_parent).to_int(); } // https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetwidth int HTMLElement::offset_width() const { // NOTE: Ensure that layout is up-to-date before looking at metrics. const_cast(document()).update_layout(DOM::UpdateLayoutReason::HTMLElementOffsetWidth); // 1. If the element does not have any associated box return zero and terminate this algorithm. auto const* box = paintable_box(); if (!box) return 0; // 2. Return the unscaled width of the axis-aligned bounding box of the border boxes of all fragments generated by // the element’s principal box, ignoring any transforms that apply to the element and its ancestors. // // If the element’s principal box is an inline-level box which was "split" by a block-level descendant, also // include fragments generated by the block-level descendants, unless they are zero width or height. return box->absolute_united_border_box_rect().width().to_int(); } // https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetheight int HTMLElement::offset_height() const { // NOTE: Ensure that layout is up-to-date before looking at metrics. const_cast(document()).update_layout(DOM::UpdateLayoutReason::HTMLElementOffsetHeight); // 1. If the element does not have any associated box return zero and terminate this algorithm. auto const* box = paintable_box(); if (!box) return 0; // 2. Return the unscaled height of the axis-aligned bounding box of the border boxes of all fragments generated by // the element’s principal box, ignoring any transforms that apply to the element and its ancestors. // // If the element’s principal box is an inline-level box which was "split" by a block-level descendant, also // include fragments generated by the block-level descendants, unless they are zero width or height. return box->absolute_united_border_box_rect().height().to_int(); } // https://html.spec.whatwg.org/multipage/links.html#cannot-navigate bool HTMLElement::cannot_navigate() const { // An element element cannot navigate if one of the following is true: // - element's node document is not fully active if (!document().is_fully_active()) return true; // - element is not an a element and is not connected. return !is(this) && !is_connected(); } void HTMLElement::attribute_changed(FlyString const& name, Optional const& old_value, Optional const& value, Optional const& namespace_) { Base::attribute_changed(name, old_value, value, namespace_); HTMLOrSVGElement::attribute_changed(name, old_value, value, namespace_); if (name == HTML::AttributeNames::contenteditable) { if (!value.has_value()) { // No value maps to the "inherit" state. m_content_editable_state = ContentEditableState::Inherit; } else if (value->is_empty() || value->equals_ignoring_ascii_case("true"sv)) { // "true", an empty string or a missing value map to the "true" state. m_content_editable_state = ContentEditableState::True; } else if (value->equals_ignoring_ascii_case("false"sv)) { // "false" maps to the "false" state. m_content_editable_state = ContentEditableState::False; } else if (value->equals_ignoring_ascii_case("plaintext-only"sv)) { // "plaintext-only" maps to the "plaintext-only" state. m_content_editable_state = ContentEditableState::PlaintextOnly; } else { // Having an invalid value maps to the "inherit" state. m_content_editable_state = ContentEditableState::Inherit; } } else if (name == HTML::AttributeNames::inert) { // https://html.spec.whatwg.org/multipage/interaction.html#the-inert-attribute // The inert attribute is a boolean attribute that indicates, by its presence, that the element and all its flat tree descendants which don't otherwise escape inertness // (such as modal dialogs) are to be made inert by the user agent. auto is_inert = value.has_value(); set_subtree_inertness(is_inert); } // 1. If namespace is not null, or localName is not the name of an event handler content attribute on element, then return. // FIXME: Add the namespace part once we support attribute namespaces. #undef __ENUMERATE #define __ENUMERATE(attribute_name, event_name) \ if (name == HTML::AttributeNames::attribute_name) { \ element_event_handler_attribute_changed(event_name, value); \ } ENUMERATE_GLOBAL_EVENT_HANDLERS(__ENUMERATE) #undef __ENUMERATE [&]() { // https://html.spec.whatwg.org/multipage/popover.html#the-popover-attribute:concept-element-attributes-change-ext // https://whatpr.org/html/9457/popover.html#the-popover-attribute:concept-element-attributes-change-ext // The following attribute change steps, given element, localName, oldValue, value, and namespace, are used for all HTML elements: // 1. If namespace is not null, then return. if (namespace_.has_value()) return; // 2. If localName is not popover, then return. if (name != HTML::AttributeNames::popover) return; // 3. If element's popover visibility state is in the showing state // and oldValue and value are in different states, // then run the hide popover algorithm given element, true, true, false, true, and null. if (m_popover_visibility_state == PopoverVisibilityState::Showing && popover_value_to_state(old_value) != popover_value_to_state(value)) MUST(hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::No, IgnoreDomState::Yes, nullptr)); }(); } void HTMLElement::set_subtree_inertness(bool is_inert) { set_inert(is_inert); for_each_in_subtree_of_type([&](auto& html_element) { if (html_element.has_attribute(HTML::AttributeNames::inert)) return TraversalDecision::SkipChildrenAndContinue; // FIXME: Exclude elements that should escape inertness. html_element.set_inert(is_inert); return TraversalDecision::Continue; }); if (auto paintable_box = this->paintable_box()) paintable_box->set_needs_paint_only_properties_update(true); } WebIDL::ExceptionOr HTMLElement::cloned(Web::DOM::Node& copy, bool clone_children) const { TRY(Base::cloned(copy, clone_children)); TRY(HTMLOrSVGElement::cloned(copy, clone_children)); return {}; } void HTMLElement::inserted() { Base::inserted(); HTMLOrSVGElement::inserted(); if (auto* parent_html_element = first_ancestor_of_type(); parent_html_element && parent_html_element->is_inert() && !has_attribute(HTML::AttributeNames::inert)) set_subtree_inertness(true); } // https://html.spec.whatwg.org/multipage/webappapis.html#fire-a-synthetic-pointer-event bool HTMLElement::fire_a_synthetic_pointer_event(FlyString const& type, DOM::Element& target, bool not_trusted) { // 1. Let event be the result of creating an event using PointerEvent. // 2. Initialize event's type attribute to e. auto event = UIEvents::PointerEvent::create(realm(), type); // 3. Initialize event's bubbles and cancelable attributes to true. event->set_bubbles(true); event->set_cancelable(true); // 4. Set event's composed flag. event->set_composed(true); // 5. If the not trusted flag is set, initialize event's isTrusted attribute to false. if (not_trusted) { event->set_is_trusted(false); } // FIXME: 6. Initialize event's ctrlKey, shiftKey, altKey, and metaKey attributes according to the current state // of the key input device, if any (false for any keys that are not available). // FIXME: 7. Initialize event's view attribute to target's node document's Window object, if any, and null otherwise. // FIXME: 8. event's getModifierState() method is to return values appropriately describing the current state of the key input device. // 9. Return the result of dispatching event at target. return target.dispatch_event(event); } // https://html.spec.whatwg.org/multipage/forms.html#dom-lfe-labels-dev GC::Ptr HTMLElement::labels() { // Labelable elements and all input elements have a live NodeList object associated with them that represents the list of label elements, in tree order, // whose labeled control is the element in question. The labels IDL attribute of labelable elements that are not form-associated custom elements, // and the labels IDL attribute of input elements, on getting, must return that NodeList object, and that same value must always be returned, // unless this element is an input element whose type attribute is in the Hidden state, in which case it must instead return null. if (!is_labelable()) return {}; if (!m_labels) { m_labels = DOM::LiveNodeList::create(realm(), root(), DOM::LiveNodeList::Scope::Descendants, [&](auto& node) { auto* label_element = as_if(node); return label_element && label_element->control() == this; }); } return m_labels; } // https://html.spec.whatwg.org/multipage/interaction.html#dom-hidden Variant HTMLElement::hidden() const { // 1. If the hidden attribute is in the hidden until found state, then return "until-found". auto const& hidden = get_attribute(HTML::AttributeNames::hidden); if (hidden.has_value() && hidden->equals_ignoring_ascii_case("until-found"sv)) return "until-found"_string; // 2. If the hidden attribute is set, then return true. if (hidden.has_value()) return true; // 3. Return false. return false; } void HTMLElement::set_hidden(Variant const& given_value) { // 1. If the given value is a string that is an ASCII case-insensitive match for "until-found", then set the hidden attribute to "until-found". if (given_value.has()) { auto const& string = given_value.get(); if (string.equals_ignoring_ascii_case("until-found"sv)) { set_attribute_value(HTML::AttributeNames::hidden, "until-found"_string); return; } // 3. Otherwise, if the given value is the empty string, then remove the hidden attribute. if (string.is_empty()) { remove_attribute(HTML::AttributeNames::hidden); return; } // 4. Otherwise, if the given value is null, then remove the hidden attribute. if (string.equals_ignoring_ascii_case("null"sv) || string.equals_ignoring_ascii_case("undefined"sv)) { remove_attribute(HTML::AttributeNames::hidden); return; } } // 2. Otherwise, if the given value is false, then remove the hidden attribute. else if (given_value.has()) { if (!given_value.get()) { remove_attribute(HTML::AttributeNames::hidden); return; } } // 5. Otherwise, if the given value is 0, then remove the hidden attribute. // 6. Otherwise, if the given value is NaN, then remove the hidden attribute. else if (given_value.has()) { auto const& double_value = given_value.get(); if (double_value == 0 || isnan(double_value)) { remove_attribute(HTML::AttributeNames::hidden); return; } } // 7. Otherwise, set the hidden attribute to the empty string. set_attribute_value(HTML::AttributeNames::hidden, ""_string); } // https://html.spec.whatwg.org/multipage/interaction.html#dom-click void HTMLElement::click() { // 1. If this element is a form control that is disabled, then return. if (auto* form_control = as_if(this)) { if (!form_control->enabled()) return; } // 2. If this element's click in progress flag is set, then return. if (m_click_in_progress) return; // 3. Set this element's click in progress flag. m_click_in_progress = true; // 4. Fire a synthetic pointer event named click at this element, with the not trusted flag set. fire_a_synthetic_pointer_event(HTML::EventNames::click, *this, true); // 5. Unset this element's click in progress flag. m_click_in_progress = false; } // https://html.spec.whatwg.org/multipage/custom-elements.html#form-associated-custom-element bool HTMLElement::is_form_associated_custom_element() { // An autonomous custom element is called a form-associated custom element if the element is associated with a // custom element definition whose form-associated field is set to true. auto definition = document().lookup_custom_element_definition(namespace_uri(), local_name(), is_value()); return definition->form_associated(); } Optional HTMLElement::default_role() const { // https://www.w3.org/TR/html-aria/#el-address if (local_name() == TagNames::address) return ARIA::Role::group; // https://www.w3.org/TR/html-aria/#el-article if (local_name() == TagNames::article) return ARIA::Role::article; // https://www.w3.org/TR/html-aria/#el-aside if (local_name() == TagNames::aside) { // https://w3c.github.io/html-aam/#el-aside for (auto ancestor = parent_element(); ancestor; ancestor = ancestor->parent_element()) { if (ancestor->local_name().is_one_of(TagNames::article, TagNames::aside, TagNames::nav, TagNames::section) && accessible_name(document()).value().is_empty()) return ARIA::Role::generic; } // https://w3c.github.io/html-aam/#el-aside-ancestorbodymain return ARIA::Role::complementary; } // https://www.w3.org/TR/html-aria/#el-b if (local_name() == TagNames::b) return ARIA::Role::generic; // https://www.w3.org/TR/html-aria/#el-bdi if (local_name() == TagNames::bdi) return ARIA::Role::generic; // https://www.w3.org/TR/html-aria/#el-bdo if (local_name() == TagNames::bdo) return ARIA::Role::generic; // https://www.w3.org/TR/html-aria/#el-code if (local_name() == TagNames::code) return ARIA::Role::code; // https://w3c.github.io/html-aam/#el-dd if (local_name() == TagNames::dd) return ARIA::Role::definition; // https://wpt.fyi/results/html-aam/dir-role.tentative.html if (local_name() == TagNames::dir) return ARIA::Role::list; // https://w3c.github.io/html-aam/#el-dt if (local_name() == TagNames::dt) return ARIA::Role::term; // https://www.w3.org/TR/html-aria/#el-dfn if (local_name() == TagNames::dfn) return ARIA::Role::term; // https://www.w3.org/TR/html-aria/#el-em if (local_name() == TagNames::em) return ARIA::Role::emphasis; // https://www.w3.org/TR/html-aria/#el-figure if (local_name() == TagNames::figure) return ARIA::Role::figure; // https://www.w3.org/TR/html-aria/#el-footer // https://www.w3.org/TR/html-aria/#el-header if (local_name() == TagNames::footer || local_name() == TagNames::header) { // If not a descendant of an article, aside, main, nav or section element, or an element with role=article, // complementary, main, navigation or region then (footer) role=contentinfo (header) role=banner. Otherwise, // role=generic. for (auto ancestor = parent_element(); ancestor; ancestor = ancestor->parent_element()) { if (ancestor->local_name().is_one_of(TagNames::article, TagNames::aside, TagNames::main, TagNames::nav, TagNames::section)) { if (local_name() == TagNames::footer) return ARIA::Role::sectionfooter; return ARIA::Role::sectionheader; } if (first_is_one_of(ancestor->role_or_default(), ARIA::Role::article, ARIA::Role::complementary, ARIA::Role::main, ARIA::Role::navigation, ARIA::Role::region)) { if (local_name() == TagNames::footer) return ARIA::Role::sectionfooter; return ARIA::Role::sectionheader; } } // then (footer) role=contentinfo. if (local_name() == TagNames::footer) return ARIA::Role::contentinfo; // (header) role=banner return ARIA::Role::banner; } // https://www.w3.org/TR/html-aria/#el-hgroup if (local_name() == TagNames::hgroup) return ARIA::Role::group; // https://www.w3.org/TR/html-aria/#el-i if (local_name() == TagNames::i) return ARIA::Role::generic; // https://www.w3.org/TR/html-aria/#el-main if (local_name() == TagNames::main) return ARIA::Role::main; // https://www.w3.org/TR/html-aria/#el-mark if (local_name() == TagNames::mark) return ARIA::Role::mark; // https://www.w3.org/TR/html-aria/#el-nav if (local_name() == TagNames::nav) return ARIA::Role::navigation; // https://www.w3.org/TR/html-aria/#el-s if (local_name() == TagNames::s) return ARIA::Role::deletion; // https://www.w3.org/TR/html-aria/#el-samp if (local_name() == TagNames::samp) return ARIA::Role::generic; // https://www.w3.org/TR/html-aria/#el-search if (local_name() == TagNames::search) return ARIA::Role::search; // https://www.w3.org/TR/html-aria/#el-section if (local_name() == TagNames::section) { // role=region if the section element has an accessible name if (!accessible_name(document()).value().is_empty()) return ARIA::Role::region; // Otherwise, role=generic return ARIA::Role::generic; } // https://www.w3.org/TR/html-aria/#el-small if (local_name() == TagNames::small) return ARIA::Role::generic; // https://www.w3.org/TR/html-aria/#el-strong if (local_name() == TagNames::strong) return ARIA::Role::strong; // https://www.w3.org/TR/html-aria/#el-sub if (local_name() == TagNames::sub) return ARIA::Role::subscript; // https://www.w3.org/TR/html-aria/#el-summary if (local_name() == TagNames::summary) return ARIA::Role::button; // https://www.w3.org/TR/html-aria/#el-sup if (local_name() == TagNames::sup) return ARIA::Role::superscript; // https://www.w3.org/TR/html-aria/#el-u if (local_name() == TagNames::u) return ARIA::Role::generic; return {}; } // https://html.spec.whatwg.org/multipage/semantics.html#get-an-element's-target String HTMLElement::get_an_elements_target(Optional target) const { // To get an element's target, given an a, area, or form element element, and an optional string-or-null target (default null), run these steps: // 1. If target is null, then: if (!target.has_value()) { // 1. If element has a target attribute, then set target to that attribute's value. if (auto maybe_target = attribute(AttributeNames::target); maybe_target.has_value()) { target = maybe_target.release_value(); } // 2. Otherwise, if element's node document contains a base element with a target attribute, // set target to the value of the target attribute of the first such base element. if (auto base_element = document().first_base_element_with_target_in_tree_order()) target = base_element->attribute(AttributeNames::target); } // 2. If target is not null, and contains an ASCII tab or newline and a U+003C (<), then set target to "_blank". if (target.has_value() && target->bytes_as_string_view().contains("\t\n\r"sv) && target->contains('<')) target = "_blank"_string; // 3. Return target. return target.value_or({}); } // https://html.spec.whatwg.org/multipage/links.html#get-an-element's-noopener TokenizedFeature::NoOpener HTMLElement::get_an_elements_noopener(URL::URL const& url, StringView target) const { // To get an element's noopener, given an a, area, or form element element, a URL record url, and a string target, // perform the following steps. They return a boolean. auto rel = MUST(get_attribute_value(HTML::AttributeNames::rel).to_lowercase()); auto link_types = rel.bytes_as_string_view().split_view_if(Infra::is_ascii_whitespace); // 1. If element's link types include the noopener or noreferrer keyword, then return true. if (link_types.contains_slow("noopener"sv) || link_types.contains_slow("noreferrer"sv)) return TokenizedFeature::NoOpener::Yes; // 2. If element's link types do not include the opener keyword and // target is an ASCII case-insensitive match for "_blank", then return true. if (!link_types.contains_slow("opener"sv) && target.equals_ignoring_ascii_case("_blank"sv)) return TokenizedFeature::NoOpener::Yes; // 3. If url's blob URL entry is not null: if (url.blob_url_entry().has_value()) { // 1. Let blobOrigin be url's blob URL entry's environment's origin. auto const& blob_origin = url.blob_url_entry()->environment.origin; // 2. Let topLevelOrigin be element's relevant settings object's top-level origin. auto const& top_level_origin = relevant_settings_object(*this).top_level_origin; // 3. If blobOrigin is not same site with topLevelOrigin, then return true. if (!blob_origin.is_same_site(top_level_origin.value())) return TokenizedFeature::NoOpener::Yes; } // 4. Return false. return TokenizedFeature::NoOpener::No; } WebIDL::ExceptionOr> HTMLElement::attach_internals() { // 1. If this's is value is not null, then throw a "NotSupportedError" DOMException. if (is_value().has_value()) return WebIDL::NotSupportedError::create(realm(), "ElementInternals cannot be attached to a customized built-in element"_utf16); // 2. Let definition be the result of looking up a custom element definition given this's node document, its namespace, its local name, and null as the is value. auto definition = document().lookup_custom_element_definition(namespace_uri(), local_name(), is_value()); // 3. If definition is null, then throw an "NotSupportedError" DOMException. if (!definition) return WebIDL::NotSupportedError::create(realm(), "ElementInternals cannot be attached to an element that is not a custom element"_utf16); // 4. If definition's disable internals is true, then throw a "NotSupportedError" DOMException. if (definition->disable_internals()) return WebIDL::NotSupportedError::create(realm(), "ElementInternals are disabled for this custom element"_utf16); // 5. If this's attached internals is non-null, then throw an "NotSupportedError" DOMException. if (m_attached_internals) return WebIDL::NotSupportedError::create(realm(), "ElementInternals already attached"_utf16); // 6. If this's custom element state is not "precustomized" or "custom", then throw a "NotSupportedError" DOMException. if (!first_is_one_of(custom_element_state(), DOM::CustomElementState::Precustomized, DOM::CustomElementState::Custom)) return WebIDL::NotSupportedError::create(realm(), "Custom element is in an invalid state to attach ElementInternals"_utf16); // 7. Set this's attached internals to a new ElementInternals instance whose target element is this. auto internals = ElementInternals::create(realm(), *this); m_attached_internals = internals; // 8. Return this's attached internals. return { internals }; } Optional HTMLElement::popover_value_to_state(Optional value) { if (!value.has_value()) return {}; if (value.value().is_empty() || value.value().equals_ignoring_ascii_case("auto"sv)) return "auto"_string; if (value.value().equals_ignoring_ascii_case("hint"sv)) return "hint"_string; return "manual"_string; } // https://html.spec.whatwg.org/multipage/popover.html#dom-popover Optional HTMLElement::popover() const { // FIXME: This should probably be `Reflect` in the IDL. // The popover IDL attribute must reflect the popover attribute, limited to only known values. auto value = get_attribute(HTML::AttributeNames::popover); return popover_value_to_state(value); } // https://html.spec.whatwg.org/multipage/popover.html#dom-popover void HTMLElement::set_popover(Optional value) { // FIXME: This should probably be `Reflect` in the IDL. // The popover IDL attribute must reflect the popover attribute, limited to only known values. if (value.has_value()) set_attribute_value(HTML::AttributeNames::popover, value.release_value()); else remove_attribute(HTML::AttributeNames::popover); } void HTMLElement::adjust_computed_style(CSS::ComputedProperties& style) { // https://drafts.csswg.org/css-display-3/#unbox if (local_name() == HTML::TagNames::wbr) { if (style.display().is_contents()) style.set_property(CSS::PropertyID::Display, CSS::DisplayStyleValue::create(CSS::Display::from_short(CSS::Display::Short::None))); } } // https://html.spec.whatwg.org/multipage/popover.html#check-popover-validity // https://whatpr.org/html/9457/popover.html#check-popover-validity WebIDL::ExceptionOr HTMLElement::check_popover_validity(ExpectedToBeShowing expected_to_be_showing, ThrowExceptions throw_exceptions, GC::Ptr expected_document, IgnoreDomState ignore_dom_state) { // 1. If ignoreDomState is false and element's popover attribute is in the No Popover state, then: if (ignore_dom_state == IgnoreDomState::No && !popover().has_value()) { // 1.1. If throwExceptions is true, then throw a "NotSupportedError" DOMException. if (throw_exceptions == ThrowExceptions::Yes) return WebIDL::NotSupportedError::create(realm(), "Element is not a popover"_utf16); // 1.2. Return false. return false; } // 2. If any of the following are true: // - expectedToBeShowing is true and element's popover visibility state is not showing; or // - expectedToBeShowing is false and element's popover visibility state is not hidden, if ((expected_to_be_showing == ExpectedToBeShowing::Yes && m_popover_visibility_state != PopoverVisibilityState::Showing) || (expected_to_be_showing == ExpectedToBeShowing::No && m_popover_visibility_state != PopoverVisibilityState::Hidden)) { // then return false. return false; } // 3. If any of the following are true: // - ignoreDomState is false and element is not connected; // - element's node document is not fully active; // - ignoreDomState is false and expectedDocument is not null and element's node document is not expectedDocument; // - element is a dialog element and its is modal flag is set to true; or // - FIXME: element's fullscreen flag is set, // then: // 3.1 If throwExceptions is true, then throw an "InvalidStateError" DOMException. // 3.2 Return false. if ((ignore_dom_state == IgnoreDomState::No && !is_connected()) || !document().is_fully_active() || (ignore_dom_state == IgnoreDomState::No && expected_document && &document() != expected_document) || (is(*this) && as(*this).is_modal())) { if (throw_exceptions == ThrowExceptions::Yes) return WebIDL::InvalidStateError::create(realm(), "Element is not in a valid state to show a popover"_utf16); return false; } // 4. Return true. return true; } // https://html.spec.whatwg.org/multipage/popover.html#dom-showpopover WebIDL::ExceptionOr HTMLElement::show_popover_for_bindings(ShowPopoverOptions const& options) { // 1. Let source be options["source"] if it exists; otherwise, null. auto source = options.source; // 2. Run show popover given this, true, and source. return show_popover(ThrowExceptions::Yes, source); } // https://html.spec.whatwg.org/multipage/popover.html#show-popover // https://whatpr.org/html/9457/popover.html#show-popover WebIDL::ExceptionOr HTMLElement::show_popover(ThrowExceptions throw_exceptions, GC::Ptr source) { // 1. If the result of running check popover validity given element, false, throwExceptions, null and false is false, then return. if (!TRY(check_popover_validity(ExpectedToBeShowing::No, throw_exceptions, nullptr, IgnoreDomState::No))) return {}; // 2. Let document be element's node document. auto& document = this->document(); // 3. Assert: element's popover trigger is null. VERIFY(!m_popover_trigger); // 4. Assert: element is not in document's top layer. VERIFY(!in_top_layer()); // 5. Let nestedShow be element's popover showing or hiding. auto nested_show = m_popover_showing_or_hiding; // 6. Let fireEvents be the boolean negation of nestedShow. FireEvents fire_events = nested_show ? FireEvents::No : FireEvents::Yes; // 7. Set element's popover showing or hiding to true. m_popover_showing_or_hiding = true; // 8. Let cleanupShowingFlag be the following steps: auto cleanup_showing_flag = [&nested_show, this] { // 8.1. If nestedShow is false, then set element's popover showing or hiding to false. if (!nested_show) m_popover_showing_or_hiding = false; }; // 9. If the result of firing an event named beforetoggle, using ToggleEvent, with the cancelable attribute // initialized to true, the oldState attribute initialized to "closed", the newState attribute initialized to // "open" at element, and the source attribute initialized to source at element is false, // then run cleanupShowingFlag and return. ToggleEventInit event_init {}; event_init.old_state = "closed"_string; event_init.new_state = "open"_string; event_init.cancelable = true; event_init.source = source; if (!dispatch_event(ToggleEvent::create(realm(), HTML::EventNames::beforetoggle, move(event_init)))) { cleanup_showing_flag(); return {}; } // 10. If the result of running check popover validity given element, false, throwExceptions, document, and false is false, then run cleanupShowingFlag and return. if (!TRY(check_popover_validity(ExpectedToBeShowing::No, throw_exceptions, document, IgnoreDomState::No))) { cleanup_showing_flag(); return {}; } // 11. Let shouldRestoreFocus be false. auto should_restore_focus = FocusPreviousElement::No; // 12. Let originalType be the current state of element's popover attribute. auto original_type = popover(); // 13. Let stackToAppendTo be null. enum class StackToAppendTo : u8 { Null, Auto, Hint, }; StackToAppendTo stack_to_append_to = StackToAppendTo::Null; // NB: Steps 14 and 15 are implemented inside step 17 instead, see note below. // 16. If originalType is the Auto state, then: if (original_type == "auto"sv) { // 1. Run close entire popover list given document's showing hint popover list, shouldRestoreFocus, and fireEvents. close_entire_popover_list(document.showing_hint_popover_list(), should_restore_focus, fire_events); // 2. Let ancestor be the result of running the topmost popover ancestor algorithm given element, document's showing auto popover list, source, and true. Variant, GC::Ptr> ancestor = topmost_popover_ancestor(this, document.showing_auto_popover_list(), source, IsPopover::Yes); // 3. If ancestor is null, then set ancestor to document. if (!ancestor.get>()) ancestor = GC::Ptr(document); // 4. Run hide all popovers until given ancestor, shouldRestoreFocus, and fireEvents. hide_all_popovers_until(ancestor, should_restore_focus, fire_events); // 5. Set stackToAppendTo to "auto". stack_to_append_to = StackToAppendTo::Auto; } // 17. If originalType is the hint state, then: if (original_type == "hint"sv) { // AD-HOC: Steps 14 and 15 have been moved here to avoid hitting the `popover != manual` assertion in the topmost popover ancestor algorithm. // Spec issue: https://github.com/whatwg/html/issues/10988. // 14. Let autoAncestor be the result of running the topmost popover ancestor algorithm given element, document's showing auto popover list, invoker, and true. auto auto_ancestor = topmost_popover_ancestor(this, document.showing_auto_popover_list(), source, IsPopover::Yes); // 15. Let hintAncestor be the result of running the topmost popover ancestor algorithm given element, document's showing hint popover list, invoker, and true. auto hint_ancestor = topmost_popover_ancestor(this, document.showing_hint_popover_list(), source, IsPopover::Yes); // 1. If hintAncestor is not null, then: if (hint_ancestor) { // 1. Run hide all popovers until given hintAncestor, shouldRestoreFocus, and fireEvents. hide_all_popovers_until(hint_ancestor, should_restore_focus, fire_events); // 2. Set stackToAppendTo to "hint". stack_to_append_to = StackToAppendTo::Hint; } // 2. Otherwise: else { // 1. Run close entire popover list given document's showing hint popover list, shouldRestoreFocus, and fireEvents. close_entire_popover_list(document.showing_hint_popover_list(), should_restore_focus, fire_events); // 2. If autoAncestor is not null, then: if (auto_ancestor) { // 1. Run hide all popovers until given autoAncestor, shouldRestoreFocus, and fireEvents. hide_all_popovers_until(auto_ancestor, should_restore_focus, fire_events); // 2. Set stackToAppendTo to "auto". stack_to_append_to = StackToAppendTo::Auto; } // 3. Otherwise, set stackToAppendTo to "hint". else { stack_to_append_to = StackToAppendTo::Hint; } } } // 18. If originalType is auto or hint, then: if (original_type.has_value() && original_type.value().is_one_of("auto", "hint")) { // 1. Assert: stackToAppendTo is not null. VERIFY(stack_to_append_to != StackToAppendTo::Null); // 2. If originalType is not equal to the value of element's popover attribute, then: if (original_type != popover()) { // 1. If throwExceptions is true, then throw an "InvalidStateError" DOMException. if (throw_exceptions == ThrowExceptions::Yes) return WebIDL::InvalidStateError::create(realm(), "Element is not in a valid state to show a popover"_utf16); // 2. Return. return {}; } // 3. If the result of running check popover validity given element, false, throwExceptions, document, and false is false, then run cleanupShowingFlag and return. if (!TRY(check_popover_validity(ExpectedToBeShowing::No, throw_exceptions, document, IgnoreDomState::No))) { cleanup_showing_flag(); return {}; } // FIXME: 4. If the result of running topmost auto or hint popover on document is null, then set shouldRestoreFocus to true. // 5. If stackToAppendTo is "auto": if (stack_to_append_to == StackToAppendTo::Auto) { // 1. Assert: document's showing auto popover list does not contain element. VERIFY(!document.showing_auto_popover_list().contains_slow(GC::Ref(*this))); // AD-HOC: Append element to the document's showing auto popover list. // Spec issue: https://github.com/whatwg/html/issues/11007 document.showing_auto_popover_list().append(*this); // 2. Set element's opened in popover mode to "auto". m_opened_in_popover_mode = "auto"_string; } // Otherwise: else { // 1. Assert: stackToAppendTo is "hint". VERIFY(stack_to_append_to == StackToAppendTo::Hint); // 2. Assert: document's showing hint popover list does not contain element. VERIFY(!document.showing_hint_popover_list().contains_slow(GC::Ref(*this))); // AD-HOC: Append element to the document's showing hint popover list. // Spec issue: https://github.com/whatwg/html/issues/11007 document.showing_hint_popover_list().append(*this); // 3. Set element's opened in popover mode to "hint". m_opened_in_popover_mode = "hint"_string; } // 6. Set element's popover close watcher to the result of establishing a close watcher given element's relevant global object, with: m_popover_close_watcher = CloseWatcher::establish(*document.window()); // - cancelAction being to return true. // We simply don't add an event listener for the cancel action. // - closeAction being to hide a popover given element, true, true, false, and null. auto close_callback_function = JS::NativeFunction::create( realm(), [this](JS::VM&) { MUST(hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::No, IgnoreDomState::No, nullptr)); return JS::js_undefined(); }, 0, Utf16FlyString {}, &realm()); auto close_callback = realm().heap().allocate(*close_callback_function, realm()); m_popover_close_watcher->add_event_listener_without_options(HTML::EventNames::close, DOM::IDLEventListener::create(realm(), close_callback)); // - getEnabledState being to return true. m_popover_close_watcher->set_enabled(true); } // FIXME: 19. Set element's previously focused element to null. // FIXME: 20. Let originallyFocusedElement be document's focused area of the document's DOM anchor. // 21. Add an element to the top layer given element. document.add_an_element_to_the_top_layer(*this); // 22. Set element's popover visibility state to showing. m_popover_visibility_state = PopoverVisibilityState::Showing; // 23. Set element's popover trigger to source. m_popover_trigger = source; // FIXME: 24. Set element's implicit anchor element to source. // FIXME: 25. Run the popover focusing steps given element. // FIXME: 26. If shouldRestoreFocus is true and element's popover attribute is not in the No Popover state, then set element's previously focused element to originallyFocusedElement. // 27. Queue a popover toggle event task given element, "closed", "open", and source. queue_a_popover_toggle_event_task("closed"_string, "open"_string, source); // 28. Run cleanupShowingFlag. cleanup_showing_flag(); return {}; } // https://html.spec.whatwg.org/multipage/popover.html#dom-hidepopover // https://whatpr.org/html/9457/popover.html#dom-hidepopover WebIDL::ExceptionOr HTMLElement::hide_popover_for_bindings() { // The hidePopover() method steps are to run the hide popover algorithm given this, true, true, true, false, and null. return hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::Yes, IgnoreDomState::No, nullptr); } // https://html.spec.whatwg.org/multipage/popover.html#hide-popover-algorithm // https://whatpr.org/html/9457/popover.html#hide-popover-algorithm WebIDL::ExceptionOr HTMLElement::hide_popover(FocusPreviousElement focus_previous_element, FireEvents fire_events, ThrowExceptions throw_exceptions, IgnoreDomState ignore_dom_state, GC::Ptr source) { // 1. If the result of running check popover validity given element, true, throwExceptions, null and ignoreDomState is false, then return. if (!TRY(check_popover_validity(ExpectedToBeShowing::Yes, throw_exceptions, nullptr, ignore_dom_state))) return {}; // 2. Let document be element's node document. auto& document = this->document(); // 3. Let nestedHide be element's popover showing or hiding. auto nested_hide = m_popover_showing_or_hiding; // 4. Set element's popover showing or hiding to true. m_popover_showing_or_hiding = true; // 5. If nestedHide is true, then set fireEvents to false. if (nested_hide) fire_events = FireEvents::No; // 6. Let cleanupSteps be the following steps: auto cleanup_steps = [&nested_hide, this] { // 6.1. If nestedHide is false, then set element's popover showing or hiding to false. if (nested_hide) m_popover_showing_or_hiding = false; // 6.2. If element's popover close watcher is not null, then: if (m_popover_close_watcher) { // 6.2.1. Destroy element's popover close watcher. m_popover_close_watcher->destroy(); // 6.2.2. Set element's popover close watcher to null. m_popover_close_watcher = nullptr; } }; // 7. If element's opened in popover mode is "auto" or "hint", then: if (m_opened_in_popover_mode.has_value() && m_opened_in_popover_mode.value().is_one_of("auto", "hint")) { // 7.1. Run hide all popovers until given element, focusPreviousElement, and fireEvents. hide_all_popovers_until(GC::Ptr(this), focus_previous_element, fire_events); // 7.2. If the result of running check popover validity given element, true, throwExceptions, and ignoreDomState is false, then run cleanupSteps and return. if (!TRY(check_popover_validity(ExpectedToBeShowing::Yes, throw_exceptions, nullptr, ignore_dom_state))) { cleanup_steps(); return {}; } } // 8. Let autoPopoverListContainsElement be true if document's showing auto popover list's last item is element, otherwise false. auto const& showing_popovers = document.showing_auto_popover_list(); bool auto_popover_list_contains_element = !showing_popovers.is_empty() && showing_popovers.last() == this; // 9. If fireEvents is true: if (fire_events == FireEvents::Yes) { // 1. Fire an event named beforetoggle, using ToggleEvent, with the oldState attribute initialized to "open", // the newState attribute initialized to "closed", and the source attribute set to source at element. ToggleEventInit event_init {}; event_init.old_state = "open"_string; event_init.new_state = "closed"_string; event_init.source = source; dispatch_event(ToggleEvent::create(realm(), HTML::EventNames::beforetoggle, move(event_init))); // 2. If autoPopoverListContainsElement is true and document's showing auto popover list's last item is not // element, then run hide all popovers until given element, focusPreviousElement, and false. if (auto_popover_list_contains_element && (showing_popovers.is_empty() || showing_popovers.last() != this)) hide_all_popovers_until(GC::Ptr(this), focus_previous_element, FireEvents::No); // 3. If the result of running check popover validity given element, true, throwExceptions, null, and // ignoreDomState is false, then run cleanupSteps and return. if (!TRY(check_popover_validity(ExpectedToBeShowing::Yes, throw_exceptions, nullptr, ignore_dom_state))) { cleanup_steps(); return {}; } // 9.4. Request an element to be removed from the top layer given element. document.request_an_element_to_be_remove_from_the_top_layer(*this); } // 10. Otherwise, remove an element from the top layer immediately given element. else { document.remove_an_element_from_the_top_layer_immediately(*this); } // AD-HOC: The following block of code is all ad-hoc. // Spec issue: https://github.com/whatwg/html/issues/11007 // If element's opened in popover mode is "auto" or "hint": if (m_opened_in_popover_mode.has_value() && m_opened_in_popover_mode.value().is_one_of("auto", "hint")) { // If document's showing hint popover list's last item is element: auto& hint_popovers = document.showing_hint_popover_list(); if (!hint_popovers.is_empty() && hint_popovers.last() == this) { // Assert: element's opened in popover mode is "hint". VERIFY(m_opened_in_popover_mode == "hint"sv); // Remove the last item from document's showing hint popover list. hint_popovers.remove(hint_popovers.size() - 1); } // Otherwise: else { // Assert: document's showing auto popover list's last item is element. auto& auto_popovers = document.showing_auto_popover_list(); VERIFY(!auto_popovers.is_empty() && auto_popovers.last() == this); // Remove the last item from document's showing auto popover list. auto_popovers.remove(auto_popovers.size() - 1); } } // 11. Set element's popover trigger to null. m_popover_trigger = nullptr; // 12. Set element's opened in popover mode to null. m_opened_in_popover_mode = {}; // 13. Set element's popover visibility state to hidden. m_popover_visibility_state = PopoverVisibilityState::Hidden; // 14. If fireEvents is true, then queue a popover toggle event task given element, "open", "closed", and source. if (fire_events == FireEvents::Yes) queue_a_popover_toggle_event_task("open"_string, "closed"_string, source); // FIXME: 15. Let previouslyFocusedElement be element's previously focused element. // FIXME: 16. If previouslyFocusedElement is not null, then: // FIXME: 16.1. Set element's previously focused element to null. // FIXME: 16.2. If focusPreviousElement is true and document's focused area of the document's DOM anchor is a shadow-including inclusive descendant of element, then run the focusing steps for previouslyFocusedElement; the viewport should not be scrolled by doing this step. // 17. Run cleanupSteps. cleanup_steps(); return {}; } // https://html.spec.whatwg.org/multipage/popover.html#dom-togglepopover // https://whatpr.org/html/9457/popover.html#dom-togglepopover WebIDL::ExceptionOr HTMLElement::toggle_popover(TogglePopoverOptionsOrForceBoolean const& options) { // 1. Let force be null. Optional force; GC::Ptr source; // 2. If options is a boolean, set force to options. options.visit( [&force](bool forceBool) { force = forceBool; }, [&force, &source](TogglePopoverOptions options) { // 3. Otherwise, if options["force"] exists, set force to options["force"]. force = options.force; // 4. Let source be options["source"] if it exists; otherwise, null. source = options.source; }); // 5. If this's popover visibility state is showing, and force is null or false, then run the hide popover algorithm given this, true, true, true, false, and null. if (popover_visibility_state() == PopoverVisibilityState::Showing && (!force.has_value() || !force.value())) TRY(hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::Yes, IgnoreDomState::No, nullptr)); // 6. Otherwise, if force is not present or true, then run show popover given this true, and source. else if (!force.has_value() || force.value()) TRY(show_popover(ThrowExceptions::Yes, source)); // 7. Otherwise: else { // 7.1 Let expectedToBeShowing be true if this's popover visibility state is showing; otherwise false. ExpectedToBeShowing expected_to_be_showing = popover_visibility_state() == PopoverVisibilityState::Showing ? ExpectedToBeShowing::Yes : ExpectedToBeShowing::No; // 7.2 Run check popover validity given expectedToBeShowing, true, null, and false. TRY(check_popover_validity(expected_to_be_showing, ThrowExceptions::Yes, nullptr, IgnoreDomState::No)); } // 8. Return true if this's popover visibility state is showing; otherwise false. return popover_visibility_state() == PopoverVisibilityState::Showing; } // AD-HOC: This implementation checks "opened in popover mode" instead of the current popover state. // Spec issue: https://github.com/whatwg/html/issues/10996. // https://html.spec.whatwg.org/multipage/popover.html#hide-all-popovers-until void HTMLElement::hide_all_popovers_until(Variant, GC::Ptr> endpoint, FocusPreviousElement focus_previous_element, FireEvents fire_events) { // To hide all popovers until, given an HTML element or Document endpoint, a boolean focusPreviousElement, and a boolean fireEvents: // 1. If endpoint is an HTML element and endpoint is not in the popover showing state, then return. if (endpoint.has>() && endpoint.get>()->popover_visibility_state() != PopoverVisibilityState::Showing) return; // 2. Let document be endpoint's node document. auto const* document = endpoint.visit([](auto endpoint) { return &endpoint->document(); }); // 3. Assert: endpoint is a Document or endpoint's popover visibility state is showing. VERIFY(endpoint.has>() || endpoint.get>()->popover_visibility_state() == PopoverVisibilityState::Showing); // 4. Assert: endpoint is a Document or endpoint's popover attribute is in the auto state or endpoint's popover attribute is in the hint state. VERIFY(endpoint.has>() || endpoint.get>()->m_opened_in_popover_mode->is_one_of("auto", "hint")); // 5. If endpoint is a Document: if (endpoint.has>()) { // 1. Run close entire popover list given document's showing hint popover list, focusPreviousElement, and fireEvents. close_entire_popover_list(document->showing_hint_popover_list(), focus_previous_element, fire_events); // 2. Run close entire popover list given document's showing auto popover list, focusPreviousElement, and fireEvents. close_entire_popover_list(document->showing_auto_popover_list(), focus_previous_element, fire_events); // 3. Return. return; } // 6. If document's showing hint popover list contains endpoint: auto endpoint_element = endpoint.get>(); if (document->showing_hint_popover_list().contains_slow(GC::Ref(*endpoint_element))) { // 1. Assert: endpoint's popover attribute is in the hint state. VERIFY(endpoint_element->m_opened_in_popover_mode == "hint"sv); // 2. Run hide popover stack until given endpoint, document's showing hint popover list, focusPreviousElement, and fireEvents. endpoint_element->hide_popover_stack_until(document->showing_hint_popover_list(), focus_previous_element, fire_events); // 3. Return. return; } // 7. Run close entire popover list given document's showing hint popover list, focusPreviousElement, and fireEvents. close_entire_popover_list(document->showing_hint_popover_list(), focus_previous_element, fire_events); // 8. If document's showing auto popover list does not contain endpoint, then return. if (!document->showing_auto_popover_list().contains_slow(GC::Ref(*endpoint_element))) return; // 9. Run hide popover stack until given endpoint, document's showing auto popover list, focusPreviousElement, and fireEvents. endpoint_element->hide_popover_stack_until(document->showing_auto_popover_list(), focus_previous_element, fire_events); } // https://html.spec.whatwg.org/multipage/popover.html#hide-popover-stack-until void HTMLElement::hide_popover_stack_until(Vector> const& popover_list, FocusPreviousElement focus_previous_element, FireEvents fire_events) { // To hide popover stack until, given an HTML element endpoint, a list popoverList, a boolean focusPreviousElement, and a boolean fireEvents: // 1. Let repeatingHide be false. bool repeating_hide = false; // 2. Perform the following steps at least once: do { // 1. Let lastToHide be null. GC::Ptr last_to_hide; // 2. For each popover in popoverList: // AD-HOC: This needs to be iterated in reverse because step 4 hides items in reverse. for (auto const& popover : popover_list.in_reverse()) { // 1. If popover is endpoint, then break. if (popover == this) break; // 2. Set lastToHide to popover. last_to_hide = popover; } // 3. If lastToHide is null, then return. if (!last_to_hide) return; // 4. While lastToHide's popover visibility state is showing: while (last_to_hide->popover_visibility_state() == PopoverVisibilityState::Showing) { // 1. Assert: popoverList is not empty. VERIFY(!popover_list.is_empty()); // 2. Run the hide popover algorithm given the last item in popoverList, focusPreviousElement, fireEvents, false, and null. MUST(popover_list.last()->hide_popover(focus_previous_element, fire_events, ThrowExceptions::No, IgnoreDomState::No, nullptr)); } // 5. Assert: repeatingHide is false or popoverList's last item is endpoint. VERIFY(!repeating_hide || popover_list.last() == this); // 6. Set repeatingHide to true if popoverList contains endpoint and popoverList's last item is not endpoint, otherwise false. repeating_hide = popover_list.contains_slow(GC::Ref(*this)) && popover_list.last() != this; // 7. If repeatingHide is true, then set fireEvents to false. if (repeating_hide) fire_events = FireEvents::No; } while (repeating_hide); // and keep performing them while repeatingHide is true. } // https://html.spec.whatwg.org/multipage/popover.html#close-entire-popover-list void HTMLElement::close_entire_popover_list(Vector> const& popover_list, FocusPreviousElement focus_previous_element, FireEvents fire_events) { // To close entire popover list given a list popoverList, a boolean focusPreviousElement, and a boolean fireEvents: // FIXME: If an event handler opens a new popover then this could be an infinite loop. // 1. While popoverList is not empty: while (!popover_list.is_empty()) { // 1. Run the hide popover algorithm given popoverList's last item, focusPreviousElement, fireEvents, false, and null. MUST(popover_list.last()->hide_popover(focus_previous_element, fire_events, ThrowExceptions::No, IgnoreDomState::No, nullptr)); } } // https://html.spec.whatwg.org/multipage/popover.html#topmost-popover-ancestor GC::Ptr HTMLElement::topmost_popover_ancestor(GC::Ptr new_popover_or_top_layer_element, Vector> const& popover_list, GC::Ptr source, IsPopover is_popover) { // To find the topmost popover ancestor, given a Node newPopoverOrTopLayerElement, a list popoverList, an HTML // element or null source, and a boolean isPopover, perform the following steps. They return an HTML element or null. // 1. If isPopover is true: auto* new_popover = as_if(*new_popover_or_top_layer_element); if (is_popover == IsPopover::Yes) { // 1. Assert: newPopoverOrTopLayerElement is an HTML element. VERIFY(new_popover); // 2. Assert: newPopoverOrTopLayerElement's popover attribute is not in the No Popover state or the manual state. VERIFY(!new_popover->popover().has_value() || new_popover->popover().value() != "manual"sv); // 3. Assert: newPopoverOrTopLayerElement's popover visibility state is not in the popover showing state. VERIFY(new_popover->popover_visibility_state() != PopoverVisibilityState::Showing); } // 2. Otherwise: else { // 1. Assert: source is null. VERIFY(!source); } // 3. Let popoverPositions be an empty ordered map. OrderedHashMap, int> popover_positions; // 4. Let index be 0. int index = 0; // 5. For each popover of popoverList: for (auto const& popover : popover_list) { // 1. Set popoverPositions[popover] to index. popover_positions.set(*popover, index); // 2. Increment index by 1. index++; } // 6. If isPopover is true, then set popoverPositions[newPopoverOrTopLayerElement] to index. if (is_popover == IsPopover::Yes) popover_positions.set(*new_popover, index); // 7. Increment index by 1. index++; // 8. Let topmostPopoverAncestor be null. GC::Ptr topmost_popover_ancestor; // 9. Let checkAncestor be an algorithm which performs the following steps given candidate: auto check_ancestor = [&](auto candidate) { // 1. If candidate is null, then return. if (!candidate) return; // 2. Let okNesting be false. bool ok_nesting = false; // 3. Let candidateAncestor be null. GC::Ptr candidate_ancestor; // 4. While okNesting is false: while (!ok_nesting) { // 1. Set candidateAncestor to the result of running nearest inclusive open popover given candidate. candidate_ancestor = candidate->nearest_inclusive_open_popover(); // 2. If candidateAncestor is null or popoverPositions does not contain candidateAncestor, then return. if (!candidate_ancestor || !popover_positions.contains(*candidate_ancestor)) return; // 3. Assert: candidateAncestor's popover attribute is not in the manual or none state. VERIFY(!candidate_ancestor->popover().has_value() || candidate_ancestor->popover().value() != "manual"sv); // 4. Set okNesting to true if isPopover is false, newPopoverOrTopLayerElement's popover attribute is in the hint state, or candidateAncestor's popover attribute is in the auto state. if (is_popover == IsPopover::No || new_popover->popover() == "hint"sv || candidate_ancestor->popover() == "auto"sv) ok_nesting = true; // 5. If okNesting is false, then set candidate to candidateAncestor's parent in the flat tree. if (!ok_nesting) candidate = candidate_ancestor->shadow_including_first_ancestor_of_type(); } // 5. Let candidatePosition be popoverPositions[candidateAncestor]. auto candidate_position = popover_positions.get(*candidate_ancestor).value(); // 6. If topmostPopoverAncestor is null or popoverPositions[topmostPopoverAncestor] is less than candidatePosition, then set topmostPopoverAncestor to candidateAncestor. if (!topmost_popover_ancestor || popover_positions.get(*topmost_popover_ancestor).value() < candidate_position) topmost_popover_ancestor = candidate_ancestor; }; // 10. Run checkAncestor given newPopoverOrTopLayerElement's parent node within the flat tree. check_ancestor(new_popover_or_top_layer_element->shadow_including_first_ancestor_of_type()); // 11. Run checkAncestor given source. check_ancestor(source.ptr()); // 12. Return topmostPopoverAncestor. return topmost_popover_ancestor; } // https://html.spec.whatwg.org/multipage/popover.html#nearest-inclusive-open-popover GC::Ptr HTMLElement::nearest_inclusive_open_popover() { // To find the nearest inclusive open popover given a Node node, perform the following steps. They return an HTML element or null. // 1. Let currentNode be node. auto* current_node = this; // 2. While currentNode is not null: while (current_node) { // 1. If currentNode's popover attribute is in the Auto state or the Hint state, and currentNode's popover visibility state is showing, then return currentNode. if (current_node->popover().has_value() && current_node->popover().value().is_one_of("auto", "hint") && current_node->popover_visibility_state() == PopoverVisibilityState::Showing) return current_node; // 2. Set currentNode to currentNode's parent in the flat tree. current_node = current_node->shadow_including_first_ancestor_of_type(); } // 3. Return null. return {}; } // https://html.spec.whatwg.org/multipage/popover.html#nearest-inclusive-target-popover GC::Ptr HTMLElement::nearest_inclusive_target_popover() { // To find the nearest inclusive target popover given a Node node: // 1. Let currentNode be node. auto* current_node = this; // 2. While currentNode is not null: while (current_node) { // 1. Let targetPopover be currentNode's popover target element. auto target_popover = PopoverTargetAttributes::get_the_popover_target_element(*current_node); // 2. If targetPopover is not null and targetPopover's popover attribute is in the Auto state or the Hint state, and targetPopover's popover visibility state is showing, then return targetPopover. if (target_popover) { if (target_popover->popover().has_value() && target_popover->popover().value().is_one_of("auto", "hint") && target_popover->popover_visibility_state() == PopoverVisibilityState::Showing) return target_popover; } // 3. Set currentNode to currentNode's ancestor in the flat tree. current_node = current_node->shadow_including_first_ancestor_of_type(); } return {}; } // https://html.spec.whatwg.org/multipage/popover.html#queue-a-popover-toggle-event-task void HTMLElement::queue_a_popover_toggle_event_task(String old_state, String new_state, GC::Ptr source) { // 1. If element's popover toggle task tracker is not null, then: if (m_popover_toggle_task_tracker.has_value()) { // 1. Set oldState to element's popover toggle task tracker's old state. old_state = move(m_popover_toggle_task_tracker->old_state); // 2. Remove element's popover toggle task tracker's task from its task queue. HTML::main_thread_event_loop().task_queue().remove_tasks_matching([&](auto const& task) { return task.id() == m_popover_toggle_task_tracker->task_id; }); // 3. Set element's popover toggle task tracker to null. m_popover_toggle_task_tracker->task_id = {}; } // 2. Queue an element task given the DOM manipulation task source and element to run the following steps: auto task_id = queue_an_element_task(HTML::Task::Source::DOMManipulation, [this, old_state, new_state = move(new_state), source]() mutable { // 1. Fire an event named toggle at element, using ToggleEvent, with the oldState attribute initialized to // oldState, the newState attribute initialized to newState, and the source attribute initialized to source. ToggleEventInit event_init {}; event_init.old_state = move(old_state); event_init.new_state = move(new_state); event_init.source = source; dispatch_event(ToggleEvent::create(realm(), HTML::EventNames::toggle, move(event_init))); // 2. Set element's popover toggle task tracker to null. m_popover_toggle_task_tracker = {}; }); // 3. Set element's popover toggle task tracker to a struct with task set to the just-queued task and old state set to oldState. m_popover_toggle_task_tracker = ToggleTaskTracker { .task_id = task_id, .old_state = move(old_state), }; } // https://html.spec.whatwg.org/multipage/popover.html#light-dismiss-open-popovers void HTMLElement::light_dismiss_open_popovers(UIEvents::PointerEvent const& event, GC::Ptr const target) { // To light dismiss open popovers, given a PointerEvent event: // 1. Assert: event's isTrusted attribute is true. VERIFY(event.is_trusted()); // 2. Let target be event's target. // FIXME: The event's target hasn't been initialized yet, so it's passed as an argument // 3. Let document be target's node document. auto& document = target->document(); // 4. Let topmostPopover be the result of running topmost auto popover given document. auto topmost_popover = document.topmost_auto_or_hint_popover(); // 5. If topmostPopover is null, then return. if (!topmost_popover) return; // 6. If event's type is "pointerdown", then: set document's popover pointerdown target to the result of running topmost clicked popover given target. if (event.type() == UIEvents::EventNames::pointerdown) document.set_popover_pointerdown_target(topmost_clicked_popover(target)); // 7. If event's type is "pointerup", then: if (event.type() == UIEvents::EventNames::pointerup) { // 1. Let ancestor be the result of running topmost clicked popover given target. auto const ancestor = topmost_clicked_popover(target); // 2. Let sameTarget be true if ancestor is document's popover pointerdown target. bool const same_target = ancestor == document.popover_pointerdown_target(); // 3. Set document's popover pointerdown target to null. document.set_popover_pointerdown_target({}); // 4. If ancestor is null, then set ancestor to document. Variant, GC::Ptr> ancestor_or_document = ancestor; if (!ancestor) ancestor_or_document = GC::Ptr(document); // 5. If sameTarget is true, then run hide all popovers until given ancestor, false, and true. if (same_target) hide_all_popovers_until(ancestor_or_document, FocusPreviousElement::No, FireEvents::Yes); } } // https://html.spec.whatwg.org/multipage/popover.html#get-the-popover-stack-position size_t HTMLElement::popover_stack_position() { // To get the popover stack position, given an HTML element popover: // 1. Let hintList be popover's node document's showing hint popover list. auto const& hint_list = document().showing_hint_popover_list(); // 2. Let autoList be popover's node document's showing auto popover list. auto const& auto_list = document().showing_auto_popover_list(); // 3. If popover is in hintList, then return the index of popover in hintList + the size of autoList + 1. if (hint_list.contains_slow(GC::Ref(*this))) return hint_list.find_first_index(GC::Ref(*this)).value() + auto_list.size() + 1; // 4. If popover is in autoList, then return the index of popover in autoList + 1. if (auto_list.contains_slow(GC::Ref(*this))) return auto_list.find_first_index(GC::Ref(*this)).value() + 1; // 5. Return 0. return 0; } // https://html.spec.whatwg.org/multipage/popover.html#topmost-clicked-popover GC::Ptr HTMLElement::topmost_clicked_popover(GC::Ptr node) { // To find the topmost clicked popover, given a Node node: GC::Ptr nearest_element = as_if(*node); if (!nearest_element) nearest_element = node->shadow_including_first_ancestor_of_type(); if (!nearest_element) return {}; // 1. Let clickedPopover be the result of running nearest inclusive open popover given node. auto clicked_popover = nearest_element->nearest_inclusive_open_popover(); // 2. Let targetPopover be the result of running nearest inclusive target popover given node. auto target_popover = nearest_element->nearest_inclusive_target_popover(); if (!clicked_popover) return target_popover; if (!target_popover) return clicked_popover; // 3. If the result of getting the popover stack position given clickedPopover is greater than the result of // getting the popover stack position given targetPopover, then return clickedPopover. if (clicked_popover->popover_stack_position() > target_popover->popover_stack_position()) return clicked_popover; // 4. Return targetPopover. return target_popover; } void HTMLElement::did_receive_focus() { if (!first_is_one_of(m_content_editable_state, ContentEditableState::True, ContentEditableState::PlaintextOnly)) return; auto editing_host = document().editing_host_manager(); editing_host->set_active_contenteditable_element(this); // Don't update the selection if we're already part of the active range. if (auto range = document().get_selection()->range()) { if (is_inclusive_ancestor_of(range->start_container()) || is_inclusive_ancestor_of(range->end_container())) return; } DOM::Text* text = nullptr; for_each_in_inclusive_subtree_of_type([&](auto& node) { text = &node; return TraversalDecision::Continue; }); if (!text) { editing_host->set_selection_anchor(*this, 0); return; } editing_host->set_selection_anchor(*text, text->length()); } void HTMLElement::did_lose_focus() { if (!first_is_one_of(m_content_editable_state, ContentEditableState::True, ContentEditableState::PlaintextOnly)) return; document().editing_host_manager()->set_active_contenteditable_element(nullptr); } void HTMLElement::removed_from(Node* old_parent, Node& old_root) { Element::removed_from(old_parent, old_root); // https://html.spec.whatwg.org/multipage/infrastructure.html#dom-trees:concept-node-remove-ext // If removedNode's popover attribute is not in the No Popover state, then run the hide popover algorithm given removedNode, false, false, false, true, and null. if (popover().has_value()) MUST(hide_popover(FocusPreviousElement::No, FireEvents::No, ThrowExceptions::No, IgnoreDomState::Yes, nullptr)); if (old_parent) { auto* parent_html_element = as_if(old_parent); if (!parent_html_element) parent_html_element = old_parent->first_ancestor_of_type(); if (parent_html_element && parent_html_element->is_inert() && !has_attribute(HTML::AttributeNames::inert)) set_subtree_inertness(false); } } // https://html.spec.whatwg.org/multipage/interaction.html#dom-accesskeylabel String HTMLElement::access_key_label() const { dbgln("FIXME: Implement HTMLElement::access_key_label()"); return String {}; } // https://html.spec.whatwg.org/multipage/dnd.html#dom-draggable bool HTMLElement::draggable() const { auto attribute = get_attribute(HTML::AttributeNames::draggable); // If an element's draggable content attribute has the state True, the draggable IDL attribute must return true. if (attribute.has_value() && attribute->equals_ignoring_ascii_case("true"sv)) { return true; } // If an element's draggable content attribute has the state False, the draggable IDL attribute must return false. if (attribute.has_value() && attribute->equals_ignoring_ascii_case("false"sv)) { return false; } // Otherwise, the element's draggable content attribute has the state Auto. // If the element is an img element, the draggable IDL attribute must return true. if (is(*this)) { return true; } // If the element is an object element that represents an image, the draggable IDL attribute must return true. if (is(*this)) { if (auto type_attribute = get_attribute(HTML::AttributeNames::type); type_attribute.has_value() && type_attribute->equals_ignoring_ascii_case("image"sv)) return true; } // If the element is an a element with an href content attribute, the draggable IDL attribute must return true. if (is(*this) && has_attribute(HTML::AttributeNames::href)) { return true; } // Otherwise, the draggable IDL attribute must return false. return false; } void HTMLElement::set_draggable(bool draggable) { set_attribute_value(HTML::AttributeNames::draggable, draggable ? "true"_string : "false"_string); } // https://html.spec.whatwg.org/multipage/interaction.html#dom-spellcheck bool HTMLElement::spellcheck() const { // The spellcheck attribute is an enumerated attribute with the following keywords and states: // Keyword | State | Brief description // true | True | Spelling and grammar will be checked. // (the empty string) | | // false | False | and grammar will not be checked. // The attribute's missing value default and invalid value default are both the Default state. The default state // indicates that the element is to act according to a default behavior, possibly based on the parent element's // own spellcheck state, as defined below. // For each element, user agents must establish a default behavior, either through defaults or through preferences // expressed by the user. There are three possible default behaviors for each element: // true-by-default // The element will be checked for spelling and grammar if its contents are editable and spellchecking is not // explicitly disabled through the spellcheck attribute. // false-by-default // The element will never be checked for spelling and grammar unless spellchecking is explicitly enabled // through the spellcheck attribute. // inherit-by-default // The element's default behavior is the same as its parent element's. Elements that have no parent element // cannot have this as their default behavior. // NOTE: We use "true-by-default" for elements which are editable, editing hosts, or form associated text control // elements "false-by-default" for root elements, and "inherit-by-default" for other elements. auto maybe_spellcheck_attribute = attribute(HTML::AttributeNames::spellcheck); // The spellcheck IDL attribute, on getting, must return true if the element's spellcheck content attribute is in the True state, if (maybe_spellcheck_attribute.has_value() && (maybe_spellcheck_attribute.value().equals_ignoring_ascii_case("true"sv) || maybe_spellcheck_attribute.value().is_empty())) return true; if (!maybe_spellcheck_attribute.has_value() || !maybe_spellcheck_attribute.value().equals_ignoring_ascii_case("false"sv)) { // or if the element's spellcheck content attribute is in the Default state and the element's default behavior is true-by-default, if (is_editable_or_editing_host() || is(this)) return true; // or if the element's spellcheck content attribute is in the Default state and the element's default behavior is inherit-by-default if (auto* parent_html_element = first_ancestor_of_type()) { // and the element's parent element's spellcheck IDL attribute would return true; if (parent_html_element->spellcheck()) return true; } } // if none of those conditions applies, then the attribute must instead return false. return false; } // https://html.spec.whatwg.org/multipage/interaction.html#dom-spellcheck void HTMLElement::set_spellcheck(bool spellcheck) { // On setting, if the new value is true, then the element's spellcheck content attribute must be set to "true", otherwise it must be set to "false". if (spellcheck) set_attribute_value(HTML::AttributeNames::spellcheck, "true"_string); else set_attribute_value(HTML::AttributeNames::spellcheck, "false"_string); } // https://html.spec.whatwg.org/multipage/interaction.html#dom-writingsuggestions String HTMLElement::writing_suggestions() const { // The writingsuggestions content attribute is an enumerated attribute with the following keywords and states: // Keyword | State | Brief description // true | True | Writing suggestions should be offered on this element. // (the empty string) | | // false | False | Writing suggestions should not be offered on this element. // The attribute's missing value default is the Default state. The default state indicates that the element is to // act according to a default behavior, possibly based on the parent element's own writingsuggestions state, as // defined below. // The attribute's invalid value default is the True state. // 1. If element's writingsuggestions content attribute is in the False state, return "false". auto maybe_writing_suggestions_attribute = attribute(HTML::AttributeNames::writingsuggestions); if (maybe_writing_suggestions_attribute.has_value() && maybe_writing_suggestions_attribute.value().equals_ignoring_ascii_case("false"sv)) return "false"_string; // 2. If element's writingsuggestions content attribute is in the Default state, element has a parent element, and the computed writing suggestions value of element's parent element is "false", then return "false". if (!maybe_writing_suggestions_attribute.has_value() && first_ancestor_of_type() && first_ancestor_of_type()->writing_suggestions() == "false"sv) { return "false"_string; } // 3. Return "true". return "true"_string; } // https://html.spec.whatwg.org/multipage/interaction.html#dom-writingsuggestions void HTMLElement::set_writing_suggestions(String const& given_value) { // 1. Set this's writingsuggestions content attribute to the given value. set_attribute_value(HTML::AttributeNames::writingsuggestions, given_value); } // https://html.spec.whatwg.org/multipage/interaction.html#own-autocapitalization-hint HTMLElement::AutocapitalizationHint HTMLElement::own_autocapitalization_hint() const { // The autocapitalization processing model is based on selecting among five autocapitalization hints, defined as follows: // // Default // The user agent and input method should make their own determination of whether or not to enable autocapitalization. // none // No autocapitalization should be applied (all letters should default to lowercase). // Sentences // The first letter of each sentence should default to a capital letter; all other letters should default to lowercase. // Words // The first letter of each word should default to a capital letter; all other letters should default to lowercase. // Characters // All letters should default to uppercase. // The autocapitalize attribute is an enumerated attribute whose states are the possible autocapitalization hints. // The autocapitalization hint specified by the attribute's state combines with other considerations to form the // used autocapitalization hint, which informs the behavior of the user agent. The keywords for this attribute and // their state mappings are as follows: // Keyword | State // off | None // none | // on | Sentences // sentences | // words | Words // characters | Characters // The attribute's missing value default is the Default state, and its invalid value default is the Sentences state. // To compute the own autocapitalization hint of an element element, run the following steps: // 1. If the autocapitalize content attribute is present on element, and its value is not the empty string, return the // state of the attribute. auto maybe_autocapitalize_attribute = attribute(HTML::AttributeNames::autocapitalize); if (maybe_autocapitalize_attribute.has_value() && !maybe_autocapitalize_attribute.value().is_empty()) { auto autocapitalize_attribute_string_view = maybe_autocapitalize_attribute.value().bytes_as_string_view(); if (autocapitalize_attribute_string_view.is_one_of_ignoring_ascii_case("off"sv, "none"sv)) return AutocapitalizationHint::None; if (autocapitalize_attribute_string_view.equals_ignoring_ascii_case("words"sv)) return AutocapitalizationHint::Words; if (autocapitalize_attribute_string_view.equals_ignoring_ascii_case("characters"sv)) return AutocapitalizationHint::Characters; return AutocapitalizationHint::Sentences; } // 2. If element is an autocapitalize-and-autocorrect inheriting element and has a non-null form owner, return the // own autocapitalization hint of element's form owner. auto const* form_associated_element = as_if(this); if (form_associated_element && form_associated_element->is_autocapitalize_and_autocorrect_inheriting() && form_associated_element->form()) return form_associated_element->form()->own_autocapitalization_hint(); // 3. Return Default. return AutocapitalizationHint::Default; } // https://html.spec.whatwg.org/multipage/interaction.html#attr-autocapitalize String HTMLElement::autocapitalize() const { // The autocapitalize getter steps are to: // 1. Let state be the own autocapitalization hint of this. auto state = own_autocapitalization_hint(); // 2. If state is Default, then return the empty string. // 3. If state is None, then return "none". // 4. If state is Sentences, then return "sentences". // 5. Return the keyword value corresponding to state. switch (state) { case AutocapitalizationHint::Default: return String {}; case AutocapitalizationHint::None: return "none"_string; case AutocapitalizationHint::Sentences: return "sentences"_string; case AutocapitalizationHint::Words: return "words"_string; case AutocapitalizationHint::Characters: return "characters"_string; } VERIFY_NOT_REACHED(); } void HTMLElement::set_autocapitalize(String const& given_value) { // The autocapitalize setter steps are to set the autocapitalize content attribute to the given value. set_attribute_value(HTML::AttributeNames::autocapitalize, given_value); } // https://html.spec.whatwg.org/multipage/interaction.html#used-autocorrection-state HTMLElement::AutocorrectionState HTMLElement::used_autocorrection_state() const { // The autocorrect attribute is an enumerated attribute with the following keywords and states: // Keyword | State | Brief description // on | On | The user agent is permitted to automatically correct spelling errors while the user // (the empty string) | | types. Whether spelling is automatically corrected while typing left is for the user // | | agent to decide, and may depend on the element as well as the user's preferences. // off | Off | The user agent is not allowed to automatically correct spelling while the user types. // The attribute's invalid value default and missing value default are both the On state. auto autocorrect_attribute_state = [](Optional attribute) { if (attribute.has_value() && attribute.value().equals_ignoring_ascii_case("off"sv)) return AutocorrectionState::Off; return AutocorrectionState::On; }; // To compute the used autocorrection state of an element element, run these steps: // 1. If element is an input element whose type attribute is in one of the URL, E-mail, or Password states, then // return Off. if (auto const* input_element = as_if(this)) { if (first_is_one_of(input_element->type_state(), HTMLInputElement::TypeAttributeState::URL, HTMLInputElement::TypeAttributeState::Email, HTMLInputElement::TypeAttributeState::Password)) return AutocorrectionState::Off; } // 2. If the autocorrect content attribute is present on element, then return the state of the attribute. auto maybe_autocorrect_attribute = attribute(HTML::AttributeNames::autocorrect); if (maybe_autocorrect_attribute.has_value()) return autocorrect_attribute_state(maybe_autocorrect_attribute); // 3. If element is an autocapitalize-and-autocorrect inheriting element and has a non-null form owner, then return // the state of element's form owner's autocorrect attribute. if (auto const* form_associated_element = as_if(this)) { if (form_associated_element->is_autocapitalize_and_autocorrect_inheriting() && form_associated_element->form()) return autocorrect_attribute_state(form_associated_element->form()->attribute(HTML::AttributeNames::autocorrect)); } // 4. Return On. return AutocorrectionState::On; } // https://html.spec.whatwg.org/multipage/interaction.html#dom-autocorrect bool HTMLElement::autocorrect() const { // The autocorrect getter steps are: return true if the element's used autocorrection state is On and false if the // element's used autocorrection state is Off. return used_autocorrection_state() == AutocorrectionState::On; } // https://html.spec.whatwg.org/multipage/interaction.html#dom-autocorrect void HTMLElement::set_autocorrect(bool given_value) { // The setter steps are: if the given value is true, then the element's autocorrect attribute must be set to "on"; // otherwise it must be set to "off". if (given_value) set_attribute_value(HTML::AttributeNames::autocorrect, "on"_string); else set_attribute_value(HTML::AttributeNames::autocorrect, "off"_string); } }