ladybird/Libraries/LibWeb/HTML/HTMLDetailsElement.cpp
Sam Atkins eb1ad8655e LibWeb/CSS: Move and rename PseudoElement types to prep for code gen
The upcoming generated types will match those for pseudo-classes: A
PseudoElementSelector type, that then holds a PseudoElement enum
defining what it is. That enum will be at the top level in the Web::CSS
namespace.

In order to keep the diffs clearer, this commit renames and moves the
types, and then a following one will replace the handwritten enum with
a generated one.
2025-03-21 12:06:37 +00: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)
{
Base::initialize(realm);
WEB_SET_PROTOTYPE_FOR_INTERFACE(HTMLDetailsElement);
}
// 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));
}
}
}