mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-08-22 10:19:20 +00:00
LibWeb: Implement 'State-preserving atomic move integration'
This was recently added to both the HTML and DOM specifications, introducing the new moveBefore DOM API, as well as the new internal 'removing steps'. See: *432e8fb
*eaf2ac7
This commit is contained in:
parent
a47c4dbc63
commit
31a3bc3681
Notes:
github-actions[bot]
2025-04-26 14:46:43 +00:00
Author: https://github.com/shannonbooth
Commit: 31a3bc3681
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/3855
Reviewed-by: https://github.com/ADKaster
Reviewed-by: https://github.com/kalenikaliaksandr
39 changed files with 1383 additions and 12 deletions
|
@ -8,6 +8,7 @@
|
|||
#include <AK/AnyOf.h>
|
||||
#include <AK/Debug.h>
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <LibJS/Runtime/NativeFunction.h>
|
||||
#include <LibUnicode/CharacterTypes.h>
|
||||
#include <LibUnicode/Locale.h>
|
||||
#include <LibWeb/Animations/Animation.h>
|
||||
|
@ -1284,6 +1285,11 @@ void Element::removed_from(Node* old_parent, Node& old_root)
|
|||
}
|
||||
}
|
||||
|
||||
void Element::moved_from(GC::Ptr<Node> old_parent)
|
||||
{
|
||||
Base::moved_from(old_parent);
|
||||
}
|
||||
|
||||
void Element::children_changed(ChildrenChangedMetadata const* metadata)
|
||||
{
|
||||
Node::children_changed(metadata);
|
||||
|
@ -2531,22 +2537,54 @@ void Element::enqueue_a_custom_element_upgrade_reaction(HTML::CustomElementDefin
|
|||
enqueue_an_element_on_the_appropriate_element_queue();
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/custom-elements.html#enqueue-a-custom-element-callback-reaction
|
||||
void Element::enqueue_a_custom_element_callback_reaction(FlyString const& callback_name, GC::RootVector<JS::Value> arguments)
|
||||
{
|
||||
// 1. Let definition be element's custom element definition.
|
||||
auto& definition = m_custom_element_definition;
|
||||
|
||||
// 2. Let callback be the value of the entry in definition's lifecycle callbacks with key callbackName.
|
||||
auto callback_iterator = definition->lifecycle_callbacks().find(callback_name);
|
||||
GC::Ptr<Web::WebIDL::CallbackType> callback;
|
||||
if (auto callback_iterator = definition->lifecycle_callbacks().find(callback_name); callback_iterator != definition->lifecycle_callbacks().end())
|
||||
callback = callback_iterator->value;
|
||||
|
||||
// 3. If callbackName is "connectedMoveCallback" and callback is null:
|
||||
if (callback_name == HTML::CustomElementReactionNames::connectedMoveCallback && !callback) {
|
||||
// 1. Let disconnectedCallback be the value of the entry in definition's lifecycle callbacks with key "disconnectedCallback".
|
||||
GC::Ptr<WebIDL::CallbackType> disconnected_callback;
|
||||
if (auto it = definition->lifecycle_callbacks().find(HTML::CustomElementReactionNames::disconnectedCallback); it != definition->lifecycle_callbacks().end())
|
||||
disconnected_callback = it->value;
|
||||
|
||||
// 2. Let connectedCallback be the value of the entry in definition's lifecycle callbacks with key "connectedCallback".
|
||||
GC::Ptr<WebIDL::CallbackType> connected_callback;
|
||||
if (auto it = definition->lifecycle_callbacks().find(HTML::CustomElementReactionNames::connectedCallback); it != definition->lifecycle_callbacks().end())
|
||||
connected_callback = it->value;
|
||||
|
||||
// 3. If connectedCallback and disconnectedCallback are null, then return.
|
||||
if (!connected_callback && !disconnected_callback)
|
||||
return;
|
||||
|
||||
// 4. Set callback to the following steps:
|
||||
auto steps = JS::NativeFunction::create(realm(), [this, disconnected_callback, connected_callback](JS::VM&) {
|
||||
GC::RootVector<JS::Value> no_arguments { heap() };
|
||||
|
||||
// 1. If disconnectedCallback is not null, then call disconnectedCallback with no arguments.
|
||||
if (disconnected_callback)
|
||||
(void)WebIDL::invoke_callback(*disconnected_callback, this, WebIDL::ExceptionBehavior::Report, no_arguments);
|
||||
|
||||
// 2. If connectedCallback is not null, then call connectedCallback with no arguments.
|
||||
if (connected_callback)
|
||||
(void)WebIDL::invoke_callback(*connected_callback, this, WebIDL::ExceptionBehavior::Report, no_arguments);
|
||||
|
||||
return JS::js_undefined(); }, 0, FlyString {}, &realm());
|
||||
callback = realm().heap().allocate<WebIDL::CallbackType>(steps, realm());
|
||||
}
|
||||
|
||||
// 3. If callback is null, then return.
|
||||
if (callback_iterator == definition->lifecycle_callbacks().end())
|
||||
if (!callback)
|
||||
return;
|
||||
|
||||
if (!callback_iterator->value)
|
||||
return;
|
||||
|
||||
// 4. If callbackName is "attributeChangedCallback", then:
|
||||
// 5. If callbackName is "attributeChangedCallback":
|
||||
if (callback_name == HTML::CustomElementReactionNames::attributeChangedCallback) {
|
||||
// 1. Let attributeName be the first element of args.
|
||||
VERIFY(!arguments.is_empty());
|
||||
|
@ -2559,10 +2597,10 @@ void Element::enqueue_a_custom_element_callback_reaction(FlyString const& callba
|
|||
return;
|
||||
}
|
||||
|
||||
// 5. Add a new callback reaction to element's custom element reaction queue, with callback function callback and arguments args.
|
||||
ensure_custom_element_reaction_queue().append(CustomElementCallbackReaction { .callback = callback_iterator->value, .arguments = move(arguments) });
|
||||
// 6. Add a new callback reaction to element's custom element reaction queue, with callback function callback and arguments args.
|
||||
ensure_custom_element_reaction_queue().append(CustomElementCallbackReaction { .callback = callback, .arguments = move(arguments) });
|
||||
|
||||
// 6. Enqueue an element on the appropriate element queue given element.
|
||||
// 7. Enqueue an element on the appropriate element queue given element.
|
||||
enqueue_an_element_on_the_appropriate_element_queue();
|
||||
}
|
||||
|
||||
|
|
|
@ -483,6 +483,8 @@ protected:
|
|||
|
||||
virtual void inserted() override;
|
||||
virtual void removed_from(Node* old_parent, Node& old_root) override;
|
||||
virtual void moved_from(GC::Ptr<Node> old_parent) override;
|
||||
|
||||
virtual void children_changed(ChildrenChangedMetadata const*) override;
|
||||
virtual i32 default_tab_index_value() const;
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
* Copyright (c) 2021-2022, Linus Groh <linusg@serenityos.org>
|
||||
* Copyright (c) 2021, Luke Wilde <lukew@serenityos.org>
|
||||
* Copyright (c) 2024, Jelle Raaijmakers <jelle@ladybird.org>
|
||||
* Copyright (c) 2025, Shannon Booth <shannon@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
@ -1175,6 +1176,190 @@ WebIDL::ExceptionOr<GC::Ref<Node>> Node::clone_node(Document* document, bool sub
|
|||
return GC::Ref { *copy };
|
||||
}
|
||||
|
||||
// https://dom.spec.whatwg.org/#move
|
||||
WebIDL::ExceptionOr<void> Node::move_node(Node& new_parent, Node* child)
|
||||
{
|
||||
// 1. If newParent’s shadow-including root is not the same as node’s shadow-including root, then throw a "HierarchyRequestError" DOMException.
|
||||
if (&new_parent.shadow_including_root() != &shadow_including_root())
|
||||
return WebIDL::HierarchyRequestError::create(realm(), "New parent is not in the same shadow tree"_string);
|
||||
|
||||
// NOTE: This has the side effect of ensuring that a move is only performed if newParent’s connected is node’s connected.
|
||||
|
||||
// 2. If node is a host-including inclusive ancestor of newParent, then throw a "HierarchyRequestError" DOMException.
|
||||
if (is_host_including_inclusive_ancestor_of(new_parent))
|
||||
return WebIDL::HierarchyRequestError::create(realm(), "New parent is an ancestor of this node"_string);
|
||||
|
||||
// 3. If child is non-null and its parent is not newParent, then throw a "NotFoundError" DOMException.
|
||||
if (child && child->parent() != &new_parent)
|
||||
return WebIDL::NotFoundError::create(realm(), "Child does not belong to the new parent"_string);
|
||||
|
||||
// 4. If node is not an Element or a CharacterData node, then throw a "HierarchyRequestError" DOMException.
|
||||
if (!is<Element>(*this) && !is<CharacterData>(*this))
|
||||
return WebIDL::HierarchyRequestError::create(realm(), "Invalid node type for insertion"_string);
|
||||
|
||||
// 5. If node is a Text node and newParent is a document, then throw a "HierarchyRequestError" DOMException.
|
||||
if (is<Text>(*this) && is<Document>(new_parent))
|
||||
return WebIDL::HierarchyRequestError::create(realm(), "Invalid node type for insertion"_string);
|
||||
|
||||
// 6. If newParent is a document, node is an Element node, and either newParent has an element child, child is a doctype,
|
||||
// or child is non-null and a doctype is following child then throw a "HierarchyRequestError" DOMException.
|
||||
if (is<Document>(new_parent) && is<Element>(*this)) {
|
||||
if (new_parent.has_child_of_type<Element>() || is<DocumentType>(child) || (child && child->has_following_node_of_type_in_tree_order<DocumentType>()))
|
||||
return WebIDL::HierarchyRequestError::create(realm(), "Invalid node type for insertion"_string);
|
||||
}
|
||||
|
||||
// 7. Let oldParent be node’s parent.
|
||||
auto* old_parent = this->parent();
|
||||
|
||||
// 8. Assert: oldParent is non-null.
|
||||
VERIFY(old_parent);
|
||||
|
||||
// 9. Run the live range pre-remove steps, given node.
|
||||
live_range_pre_remove();
|
||||
|
||||
// 10. For each NodeIterator object iterator whose root’s node document is node’s node document, run the NodeIterator pre-remove steps given node and iterator.
|
||||
document().for_each_node_iterator([&](NodeIterator& node_iterator) {
|
||||
node_iterator.run_pre_removing_steps(*this);
|
||||
});
|
||||
|
||||
// 11. Let oldPreviousSibling be node’s previous sibling.
|
||||
auto* old_previous_sibling = previous_sibling();
|
||||
|
||||
// 12. Let oldNextSibling be node’s next sibling.
|
||||
auto* old_next_sibling = next_sibling();
|
||||
|
||||
if (old_parent->is_connected()) {
|
||||
// Since the tree structure is about to change, we need to invalidate both style and layout.
|
||||
// In the future, we should find a way to only invalidate the parts that actually need it.
|
||||
old_parent->invalidate_style(StyleInvalidationReason::NodeRemove);
|
||||
|
||||
// NOTE: If we didn't have a layout node before, rebuilding the layout tree isn't gonna give us one
|
||||
// after we've been removed from the DOM.
|
||||
if (layout_node())
|
||||
old_parent->set_needs_layout_tree_update(true, SetNeedsLayoutTreeUpdateReason::NodeRemove);
|
||||
}
|
||||
|
||||
// 13. Remove node from oldParent’s children.
|
||||
old_parent->remove_child_impl(*this);
|
||||
|
||||
// 14. If node is assigned, then run assign slottables for node’s assigned slot.
|
||||
if (auto assigned_slot = assigned_slot_for_node(*this))
|
||||
assign_slottables(*assigned_slot);
|
||||
|
||||
// 15. If oldParent’s root is a shadow root, and oldParent is a slot whose assigned nodes is empty, then run signal a slot change for oldParent.
|
||||
auto& old_parent_root = old_parent->root();
|
||||
if (old_parent_root.is_shadow_root() && is<HTML::HTMLSlotElement>(*old_parent)) {
|
||||
auto& old_parent_slot = static_cast<HTML::HTMLSlotElement&>(*old_parent);
|
||||
if (old_parent_slot.assigned_nodes_internal().is_empty())
|
||||
signal_a_slot_change(old_parent_slot);
|
||||
}
|
||||
|
||||
// 16. If node has an inclusive descendant that is a slot:
|
||||
auto has_descendent_slot = false;
|
||||
|
||||
for_each_in_inclusive_subtree_of_type<HTML::HTMLSlotElement>([&](auto const&) {
|
||||
has_descendent_slot = true;
|
||||
return TraversalDecision::Break;
|
||||
});
|
||||
|
||||
if (has_descendent_slot) {
|
||||
// 1. Run assign slottables for a tree with oldParent’s root.
|
||||
assign_slottables_for_a_tree(old_parent_root);
|
||||
|
||||
// 2. Run assign slottables for a tree with node.
|
||||
assign_slottables_for_a_tree(*this);
|
||||
}
|
||||
|
||||
// 17. If child is non-null:
|
||||
if (child) {
|
||||
// 1. For each live range whose start node is newParent and start offset is greater than child’s index, increase its start offset by 1.
|
||||
for (auto& range : Range::live_ranges()) {
|
||||
if (range->start_container() == &new_parent && range->start_offset() > child->index())
|
||||
range->increase_start_offset({}, 1);
|
||||
}
|
||||
|
||||
// 2. For each live range whose end node is newParent and end offset is greater than child’s index, increase its end offset by 1.
|
||||
for (auto& range : Range::live_ranges()) {
|
||||
if (range->end_container() == &new_parent && range->end_offset() > child->index())
|
||||
range->increase_end_offset({}, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 18. Let newPreviousSibling be child’s previous sibling if child is non-null, and newParent’s last child otherwise.
|
||||
auto* new_previous_sibling = child ? child->previous_sibling() : new_parent.last_child();
|
||||
|
||||
// 19. If child is null, then append node to newParent’s children.
|
||||
if (!child) {
|
||||
new_parent.append_child_impl(*this);
|
||||
}
|
||||
// 20. Otherwise, insert node into newParent’s children before child’s index.
|
||||
else {
|
||||
new_parent.insert_before_impl(*this, child);
|
||||
}
|
||||
|
||||
new_parent.invalidate_style(StyleInvalidationReason::NodeInsertBefore);
|
||||
if (is_connected()) {
|
||||
new_parent.set_needs_layout_tree_update(true, SetNeedsLayoutTreeUpdateReason::NodeInsertBefore);
|
||||
}
|
||||
|
||||
// 21. If newParent is a shadow host whose shadow root’s slot assignment is "named" and node is a slottable, then assign a slot for node.
|
||||
if (is<Element>(new_parent) && is<Element>(*this)) {
|
||||
auto& this_element = static_cast<Element&>(*this);
|
||||
auto& new_parent_element = static_cast<Element&>(new_parent);
|
||||
|
||||
auto is_named_shadow_host = new_parent_element.is_shadow_host()
|
||||
&& new_parent_element.shadow_root()->slot_assignment() == Bindings::SlotAssignmentMode::Named;
|
||||
|
||||
if (is_named_shadow_host && this_element.is_slottable())
|
||||
assign_a_slot(this_element.as_slottable());
|
||||
}
|
||||
|
||||
// 22. If newParent’s root is a shadow root, and newParent is a slot whose assigned nodes is empty, then run signal a slot change for newParent.
|
||||
if (new_parent.root().is_shadow_root() && is<HTML::HTMLSlotElement>(new_parent)) {
|
||||
auto& new_parent_slot = static_cast<HTML::HTMLSlotElement&>(new_parent);
|
||||
if (new_parent_slot.assigned_nodes_internal().is_empty())
|
||||
signal_a_slot_change(new_parent_slot);
|
||||
}
|
||||
|
||||
// 23. Run assign slottables for a tree with node’s root.
|
||||
assign_slottables_for_a_tree(root());
|
||||
|
||||
// 24. For each shadow-including inclusive descendant inclusiveDescendant of node, in shadow-including tree order:
|
||||
for_each_shadow_including_inclusive_descendant([this, &new_parent, old_parent](Node& inclusive_descendant) {
|
||||
// 1. If inclusiveDescendant is node, then run the moving steps with inclusiveDescendant and oldParent. Otherwise, run the moving
|
||||
// steps with inclusiveDescendant and null.
|
||||
if (&inclusive_descendant == this)
|
||||
inclusive_descendant.moved_from(*old_parent);
|
||||
else
|
||||
inclusive_descendant.moved_from(nullptr);
|
||||
|
||||
// NOTE: Because the move algorithm is a separate primitive from insert and remove, it does not invoke the traditional insertion steps or
|
||||
// removing steps for inclusiveDescendant.
|
||||
|
||||
// 2. If inclusiveDescendant is custom and newParent is connected, then enqueue a custom element callback reaction with inclusiveDescendant,
|
||||
// callback name "connectedMoveCallback", and « ».
|
||||
if (is<DOM::Element>(inclusive_descendant)) {
|
||||
auto& element = static_cast<DOM::Element&>(inclusive_descendant);
|
||||
|
||||
if (element.is_custom() && new_parent.is_connected()) {
|
||||
GC::RootVector<JS::Value> empty_arguments { vm().heap() };
|
||||
element.enqueue_a_custom_element_callback_reaction(HTML::CustomElementReactionNames::connectedMoveCallback, move(empty_arguments));
|
||||
}
|
||||
}
|
||||
return TraversalDecision::Continue;
|
||||
});
|
||||
|
||||
// 25. Queue a tree mutation record for oldParent with « », « node », oldPreviousSibling, and oldNextSibling.
|
||||
old_parent->queue_tree_mutation_record({}, { *this }, old_previous_sibling, old_next_sibling);
|
||||
|
||||
// 26. Queue a tree mutation record for newParent with « node », « », newPreviousSibling, and child.
|
||||
new_parent.queue_tree_mutation_record({ *this }, {}, new_previous_sibling, child);
|
||||
|
||||
document().bump_dom_tree_version();
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// https://dom.spec.whatwg.org/#clone-a-single-node
|
||||
WebIDL::ExceptionOr<GC::Ref<Node>> Node::clone_single_node(Document& document) const
|
||||
{
|
||||
|
@ -1488,6 +1673,11 @@ void Node::removed_from(Node*, Node&)
|
|||
play_or_cancel_animations_after_display_property_change();
|
||||
}
|
||||
|
||||
// https://dom.spec.whatwg.org/#concept-node-move-ext
|
||||
void Node::moved_from(GC::Ptr<Node>)
|
||||
{
|
||||
}
|
||||
|
||||
ParentNode* Node::parent_or_shadow_host()
|
||||
{
|
||||
if (is<ShadowRoot>(*this))
|
||||
|
|
|
@ -240,6 +240,8 @@ public:
|
|||
WebIDL::ExceptionOr<GC::Ref<Node>> clone_single_node(Document&) const;
|
||||
WebIDL::ExceptionOr<GC::Ref<Node>> clone_node_binding(bool subtree);
|
||||
|
||||
WebIDL::ExceptionOr<void> move_node(Node& new_parent, Node* child);
|
||||
|
||||
// NOTE: This is intended for the JS bindings.
|
||||
bool has_child_nodes() const { return has_children(); }
|
||||
GC::Ref<NodeList> child_nodes();
|
||||
|
@ -298,6 +300,8 @@ public:
|
|||
virtual void inserted();
|
||||
virtual void post_connection();
|
||||
virtual void removed_from(Node* old_parent, Node& old_root);
|
||||
virtual void moved_from(GC::Ptr<Node> old_parent);
|
||||
|
||||
struct ChildrenChangedMetadata {
|
||||
enum class Type {
|
||||
Inserted,
|
||||
|
|
|
@ -244,6 +244,22 @@ WebIDL::ExceptionOr<void> ParentNode::replace_children(Vector<Variant<GC::Root<N
|
|||
return {};
|
||||
}
|
||||
|
||||
// https://dom.spec.whatwg.org/#dom-parentnode-movebefore
|
||||
WebIDL::ExceptionOr<void> ParentNode::move_before(GC::Ref<Node> node, GC::Ptr<Node> child)
|
||||
{
|
||||
// 1. Let referenceChild be child.
|
||||
auto reference_child = child;
|
||||
|
||||
// 2. If referenceChild is node, then set referenceChild to node’s next sibling.
|
||||
if (reference_child == node)
|
||||
reference_child = node->next_sibling();
|
||||
|
||||
// 3. Move node into this before referenceChild.
|
||||
TRY(node->move_node(*this, reference_child));
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// https://dom.spec.whatwg.org/#dom-document-getelementsbyclassname
|
||||
GC::Ref<HTMLCollection> ParentNode::get_elements_by_class_name(StringView class_names)
|
||||
{
|
||||
|
|
|
@ -35,6 +35,7 @@ public:
|
|||
WebIDL::ExceptionOr<void> prepend(Vector<Variant<GC::Root<Node>, String>> const& nodes);
|
||||
WebIDL::ExceptionOr<void> append(Vector<Variant<GC::Root<Node>, String>> const& nodes);
|
||||
WebIDL::ExceptionOr<void> replace_children(Vector<Variant<GC::Root<Node>, String>> const& nodes);
|
||||
WebIDL::ExceptionOr<void> move_before(GC::Ref<Node> node, GC::Ptr<Node> child);
|
||||
|
||||
GC::Ref<HTMLCollection> get_elements_by_class_name(StringView);
|
||||
|
||||
|
|
|
@ -12,6 +12,8 @@ interface mixin ParentNode {
|
|||
[CEReactions, Unscopable] undefined append((Node or DOMString)... nodes);
|
||||
[CEReactions, Unscopable] undefined replaceChildren((Node or DOMString)... nodes);
|
||||
|
||||
[CEReactions] undefined moveBefore(Node node, Node? child);
|
||||
|
||||
Element? querySelector(DOMString selectors);
|
||||
[NewObject] NodeList querySelectorAll(DOMString selectors);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue