ladybird/Libraries/LibWeb/HTML/HTMLDetailsElement.cpp
Andreas Kling a6dfc74e93 LibWeb: Only set prototype once for object with IDL interface
Before this change, we were going through the chain of base classes for
each IDL interface object and having them set the prototype to their
prototype.

Instead of doing that, reorder things so that we set the right prototype
immediately in Foo::initialize(), and then don't bother in all the base
class overrides.

This knocks off a ~1% profile item on Speedometer 3.
2025-04-20 18:43:11 +02:00

319 lines
13 KiB
C++

/*
* Copyright (c) 2020, the SerenityOS developers.
* Copyright (c) 2023-2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Bindings/HTMLDetailsElementPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/DOM/ElementFactory.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/DOM/Text.h>
#include <LibWeb/HTML/EventLoop/TaskQueue.h>
#include <LibWeb/HTML/HTMLDetailsElement.h>
#include <LibWeb/HTML/HTMLSlotElement.h>
#include <LibWeb/HTML/HTMLSummaryElement.h>
#include <LibWeb/HTML/ToggleEvent.h>
#include <LibWeb/Namespace.h>
namespace Web::HTML {
GC_DEFINE_ALLOCATOR(HTMLDetailsElement);
HTMLDetailsElement::HTMLDetailsElement(DOM::Document& document, DOM::QualifiedName qualified_name)
: HTMLElement(document, move(qualified_name))
{
}
HTMLDetailsElement::~HTMLDetailsElement() = default;
void HTMLDetailsElement::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_summary_slot);
visitor.visit(m_descendants_slot);
}
void HTMLDetailsElement::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(HTMLDetailsElement);
Base::initialize(realm);
}
// https://html.spec.whatwg.org/multipage/interactive-elements.html#the-details-element:html-element-insertion-steps
void HTMLDetailsElement::inserted()
{
Base::inserted();
// 1. Ensure details exclusivity by closing the given element if needed given insertedNode.
ensure_details_exclusivity_by_closing_the_given_element_if_needed();
create_shadow_tree_if_needed().release_value_but_fixme_should_propagate_errors();
update_shadow_tree_slots();
}
void HTMLDetailsElement::removed_from(DOM::Node* old_parent, DOM::Node& old_root)
{
Base::removed_from(old_parent, old_root);
set_shadow_root(nullptr);
}
// https://html.spec.whatwg.org/multipage/interactive-elements.html#the-details-element:concept-element-attributes-change-ext
void HTMLDetailsElement::attribute_changed(FlyString const& local_name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_)
{
Base::attribute_changed(local_name, old_value, value, namespace_);
// 1. If namespace is not null, then return.
if (namespace_.has_value())
return;
// 2. If localName is name, then ensure details exclusivity by closing the given element if needed given element.
if (local_name == HTML::AttributeNames::name) {
ensure_details_exclusivity_by_closing_the_given_element_if_needed();
update_shadow_tree_style();
}
// 3. If localName is open, then:
else if (local_name == HTML::AttributeNames::open) {
// 1. If one of oldValue or value is null and the other is not null, run the following steps, which are known as
// the details notification task steps, for this details element:
{
// 1. If oldValue is null, queue a details toggle event task given the details element, "closed", and "open".
if (!old_value.has_value()) {
queue_a_details_toggle_event_task("closed"_string, "open"_string);
}
// 2. Otherwise, queue a details toggle event task given the details element, "open", and "closed".
else {
queue_a_details_toggle_event_task("open"_string, "closed"_string);
}
}
// 2. If oldValue is null and value is not null, then ensure details exclusivity by closing other elements if
// needed given element.
if (!old_value.has_value() && value.has_value()) {
ensure_details_exclusivity_by_closing_other_elements_if_needed();
}
update_shadow_tree_style();
}
}
void HTMLDetailsElement::children_changed(ChildrenChangedMetadata const* metadata)
{
Base::children_changed(metadata);
update_shadow_tree_slots();
}
// https://html.spec.whatwg.org/multipage/interactive-elements.html#queue-a-details-toggle-event-task
void HTMLDetailsElement::queue_a_details_toggle_event_task(String old_state, String new_state)
{
// 1. If element's details toggle task tracker is not null, then:
if (m_details_toggle_task_tracker.has_value()) {
// 1. Set oldState to element's details toggle task tracker's old state.
old_state = move(m_details_toggle_task_tracker->old_state);
// 2. Remove element's details 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_details_toggle_task_tracker->task_id;
});
// 3. Set element's details toggle task tracker to null.
m_details_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)]() mutable {
// 1. Fire an event named toggle at element, using ToggleEvent, with the oldState attribute initialized to
// oldState and the newState attribute initialized to newState.
ToggleEventInit event_init {};
event_init.old_state = move(old_state);
event_init.new_state = move(new_state);
dispatch_event(ToggleEvent::create(realm(), HTML::EventNames::toggle, move(event_init)));
// 2. Set element's details toggle task tracker to null.
m_details_toggle_task_tracker = {};
});
// 3. Set element's details toggle task tracker to a struct with task set to the just-queued task and old state set to oldState.
m_details_toggle_task_tracker = ToggleTaskTracker {
.task_id = task_id,
.old_state = move(old_state),
};
}
// https://html.spec.whatwg.org/multipage/interactive-elements.html#details-name-group
template<typename Callback>
void for_each_element_in_details_name_group(HTMLDetailsElement& details, FlyString const& name, Callback&& callback)
{
// The details name group that contains a details element a also contains all the other details elements b that
// fulfill all of the following conditions:
auto name_group_contains_element = [&](auto const& element) {
// 1. Both a and b are in the same tree.
// NOTE: This is true due to the way we iterate the tree below.
// 2. They both have a name attribute, their name attributes are not the empty string, and the value of a's name
// attribute equals the value of b's name attribute.
return element.attribute(HTML::AttributeNames::name) == name;
};
details.root().for_each_in_inclusive_subtree_of_type<HTMLDetailsElement>([&](HTMLDetailsElement& candidate) {
if (&details != &candidate && name_group_contains_element(candidate))
return callback(candidate);
return TraversalDecision::Continue;
});
}
// https://html.spec.whatwg.org/multipage/interactive-elements.html#ensure-details-exclusivity-by-closing-other-elements-if-needed
void HTMLDetailsElement::ensure_details_exclusivity_by_closing_other_elements_if_needed()
{
// 1. Assert: element has an open attribute.
VERIFY(has_attribute(HTML::AttributeNames::open));
// 2. If element does not have a name attribute, or its name attribute is the empty string, then return.
auto name = attribute(HTML::AttributeNames::name);
if (!name.has_value() || name->is_empty())
return;
// 3. Let groupMembers be a list of elements, containing all elements in element's details name group except for
// element, in tree order.
// 4. For each element otherElement of groupMembers:
for_each_element_in_details_name_group(*this, *name, [&](HTMLDetailsElement& other_element) {
// 1. If the open attribute is set on otherElement, then:
if (other_element.has_attribute(HTML::AttributeNames::open)) {
// 1. Assert: otherElement is the only element in groupMembers that has the open attribute set.
// 2. Remove the open attribute on otherElement.
other_element.remove_attribute(HTML::AttributeNames::open);
// 3. Break.
return TraversalDecision::Break;
}
return TraversalDecision::Continue;
});
}
// https://html.spec.whatwg.org/multipage/interactive-elements.html#ensure-details-exclusivity-by-closing-the-given-element-if-needed
void HTMLDetailsElement::ensure_details_exclusivity_by_closing_the_given_element_if_needed()
{
// 1. If element does not have an open attribute, then return.
if (!has_attribute(HTML::AttributeNames::open))
return;
// 2. If element does not have a name attribute, or its name attribute is the empty string, then return.
auto name = attribute(HTML::AttributeNames::name);
if (!name.has_value() || name->is_empty())
return;
// 3. Let groupMembers be a list of elements, containing all elements in element's details name group except for
// element, in tree order.
// 4. For each element otherElement of groupMembers:
for_each_element_in_details_name_group(*this, *name, [&](HTMLDetailsElement const& other_element) {
// 1. If the open attribute is set on otherElement, then:
if (other_element.has_attribute(HTML::AttributeNames::open)) {
// 1. Remove the open attribute on element.
remove_attribute(HTML::AttributeNames::open);
// 2. Break.
return TraversalDecision::Break;
}
return TraversalDecision::Continue;
});
}
// https://html.spec.whatwg.org/multipage/rendering.html#the-details-and-summary-elements
WebIDL::ExceptionOr<void> HTMLDetailsElement::create_shadow_tree_if_needed()
{
if (shadow_root())
return {};
auto& realm = this->realm();
// The details element is expected to have an internal shadow tree with three child elements:
auto shadow_root = realm.create<DOM::ShadowRoot>(document(), *this, Bindings::ShadowRootMode::Closed);
shadow_root->set_slot_assignment(Bindings::SlotAssignmentMode::Manual);
// The first child element is a slot that is expected to take the details element's first summary element child, if any.
auto summary_slot = TRY(DOM::create_element(document(), HTML::TagNames::slot, Namespace::HTML));
MUST(shadow_root->append_child(summary_slot));
// The second child element is a slot that is expected to take the details element's remaining descendants, if any.
auto descendants_slot = TRY(DOM::create_element(document(), HTML::TagNames::slot, Namespace::HTML));
descendants_slot->set_use_pseudo_element(CSS::PseudoElement::DetailsContent);
MUST(shadow_root->append_child(descendants_slot));
// The third child element is either a link or style element with the following styles for the default summary:
auto style = TRY(DOM::create_element(document(), HTML::TagNames::style, Namespace::HTML));
style->set_text_content(R"~~~(
:host summary {
display: list-item;
counter-increment: list-item 0;
list-style: disclosure-closed inside;
}
:host([open]) summary {
list-style-type: disclosure-open;
}
)~~~"_string);
MUST(shadow_root->append_child(style));
m_summary_slot = static_cast<HTML::HTMLSlotElement&>(*summary_slot);
m_descendants_slot = static_cast<HTML::HTMLSlotElement&>(*descendants_slot);
set_shadow_root(shadow_root);
return {};
}
void HTMLDetailsElement::update_shadow_tree_slots()
{
if (!shadow_root())
return;
Vector<HTMLSlotElement::SlottableHandle> summary_assignment;
Vector<HTMLSlotElement::SlottableHandle> descendants_assignment;
auto* summary = first_child_of_type<HTMLSummaryElement>();
if (summary != nullptr)
summary_assignment.append(GC::make_root(static_cast<DOM::Element&>(*summary)));
for_each_in_subtree([&](auto& child) {
if (&child == summary)
return TraversalDecision::Continue;
if (!child.is_slottable())
return TraversalDecision::Continue;
child.as_slottable().visit([&](auto& node) {
descendants_assignment.append(GC::make_root(node));
});
return TraversalDecision::Continue;
});
m_summary_slot->assign(move(summary_assignment));
m_descendants_slot->assign(move(descendants_assignment));
update_shadow_tree_style();
}
// https://html.spec.whatwg.org/multipage/rendering.html#the-details-and-summary-elements%3Athe-details-element-6
void HTMLDetailsElement::update_shadow_tree_style()
{
if (!shadow_root())
return;
if (has_attribute(HTML::AttributeNames::open)) {
MUST(m_descendants_slot->set_attribute(HTML::AttributeNames::style, R"~~~(
display: block;
)~~~"_string));
} else {
MUST(m_descendants_slot->set_attribute(HTML::AttributeNames::style, R"~~~(
display: block;
content-visibility: hidden;
)~~~"_string));
}
}
}