diff --git a/Libraries/LibWeb/DOM/Element.cpp b/Libraries/LibWeb/DOM/Element.cpp index 4ad66bdb650..8b753a10e30 100644 --- a/Libraries/LibWeb/DOM/Element.cpp +++ b/Libraries/LibWeb/DOM/Element.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -1284,6 +1285,11 @@ void Element::removed_from(Node* old_parent, Node& old_root) } } +void Element::moved_from(GC::Ptr 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 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 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 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 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 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(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(); } diff --git a/Libraries/LibWeb/DOM/Element.h b/Libraries/LibWeb/DOM/Element.h index 0944eb77098..d2ff1173263 100644 --- a/Libraries/LibWeb/DOM/Element.h +++ b/Libraries/LibWeb/DOM/Element.h @@ -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 old_parent) override; + virtual void children_changed(ChildrenChangedMetadata const*) override; virtual i32 default_tab_index_value() const; diff --git a/Libraries/LibWeb/DOM/Node.cpp b/Libraries/LibWeb/DOM/Node.cpp index d78c60f31ff..1adb18b764c 100644 --- a/Libraries/LibWeb/DOM/Node.cpp +++ b/Libraries/LibWeb/DOM/Node.cpp @@ -3,6 +3,7 @@ * Copyright (c) 2021-2022, Linus Groh * Copyright (c) 2021, Luke Wilde * Copyright (c) 2024, Jelle Raaijmakers + * Copyright (c) 2025, Shannon Booth * * SPDX-License-Identifier: BSD-2-Clause */ @@ -1175,6 +1176,190 @@ WebIDL::ExceptionOr> Node::clone_node(Document* document, bool sub return GC::Ref { *copy }; } +// https://dom.spec.whatwg.org/#move +WebIDL::ExceptionOr 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(*this) && !is(*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(*this) && is(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(new_parent) && is(*this)) { + if (new_parent.has_child_of_type() || is(child) || (child && child->has_following_node_of_type_in_tree_order())) + 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(*old_parent)) { + auto& old_parent_slot = static_cast(*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([&](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(new_parent) && is(*this)) { + auto& this_element = static_cast(*this); + auto& new_parent_element = static_cast(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(new_parent)) { + auto& new_parent_slot = static_cast(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(inclusive_descendant)) { + auto& element = static_cast(inclusive_descendant); + + if (element.is_custom() && new_parent.is_connected()) { + GC::RootVector 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> 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) +{ +} + ParentNode* Node::parent_or_shadow_host() { if (is(*this)) diff --git a/Libraries/LibWeb/DOM/Node.h b/Libraries/LibWeb/DOM/Node.h index 23f54aba550..221b5f2653a 100644 --- a/Libraries/LibWeb/DOM/Node.h +++ b/Libraries/LibWeb/DOM/Node.h @@ -240,6 +240,8 @@ public: WebIDL::ExceptionOr> clone_single_node(Document&) const; WebIDL::ExceptionOr> clone_node_binding(bool subtree); + WebIDL::ExceptionOr 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 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 old_parent); + struct ChildrenChangedMetadata { enum class Type { Inserted, diff --git a/Libraries/LibWeb/DOM/ParentNode.cpp b/Libraries/LibWeb/DOM/ParentNode.cpp index 38fcce31076..ae0c2e8cc1f 100644 --- a/Libraries/LibWeb/DOM/ParentNode.cpp +++ b/Libraries/LibWeb/DOM/ParentNode.cpp @@ -244,6 +244,22 @@ WebIDL::ExceptionOr ParentNode::replace_children(Vector ParentNode::move_before(GC::Ref node, GC::Ptr 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 ParentNode::get_elements_by_class_name(StringView class_names) { diff --git a/Libraries/LibWeb/DOM/ParentNode.h b/Libraries/LibWeb/DOM/ParentNode.h index 74c4a938436..1076567897f 100644 --- a/Libraries/LibWeb/DOM/ParentNode.h +++ b/Libraries/LibWeb/DOM/ParentNode.h @@ -35,6 +35,7 @@ public: WebIDL::ExceptionOr prepend(Vector, String>> const& nodes); WebIDL::ExceptionOr append(Vector, String>> const& nodes); WebIDL::ExceptionOr replace_children(Vector, String>> const& nodes); + WebIDL::ExceptionOr move_before(GC::Ref node, GC::Ptr child); GC::Ref get_elements_by_class_name(StringView); diff --git a/Libraries/LibWeb/DOM/ParentNode.idl b/Libraries/LibWeb/DOM/ParentNode.idl index 981781db5c0..f17f4a1b3ce 100644 --- a/Libraries/LibWeb/DOM/ParentNode.idl +++ b/Libraries/LibWeb/DOM/ParentNode.idl @@ -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); }; diff --git a/Libraries/LibWeb/HTML/CustomElements/CustomElementReactionNames.h b/Libraries/LibWeb/HTML/CustomElements/CustomElementReactionNames.h index ee6b7922584..ef64b0c1c84 100644 --- a/Libraries/LibWeb/HTML/CustomElements/CustomElementReactionNames.h +++ b/Libraries/LibWeb/HTML/CustomElements/CustomElementReactionNames.h @@ -15,6 +15,7 @@ namespace Web::HTML::CustomElementReactionNames { __ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(connectedCallback) \ __ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(disconnectedCallback) \ __ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(adoptedCallback) \ + __ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(connectedMoveCallback) \ __ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(attributeChangedCallback) \ __ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(formAssociatedCallback) \ __ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(formDisabledCallback) \ diff --git a/Libraries/LibWeb/HTML/CustomElements/CustomElementRegistry.cpp b/Libraries/LibWeb/HTML/CustomElements/CustomElementRegistry.cpp index 2471cd8a86a..29dbb229e49 100644 --- a/Libraries/LibWeb/HTML/CustomElements/CustomElementRegistry.cpp +++ b/Libraries/LibWeb/HTML/CustomElements/CustomElementRegistry.cpp @@ -199,14 +199,15 @@ JS::ThrowCompletionOr CustomElementRegistry::define(String const& name, We auto& prototype = prototype_value.as_object(); // 3. Let lifecycleCallbacks be the ordered map «[ "connectedCallback" → null, "disconnectedCallback" → null, "adoptedCallback" → null, - // "attributeChangedCallback" → null ]». + // "connectedMoveCallback" → null, "attributeChangedCallback" → null ]». lifecycle_callbacks.set(CustomElementReactionNames::connectedCallback, {}); lifecycle_callbacks.set(CustomElementReactionNames::disconnectedCallback, {}); lifecycle_callbacks.set(CustomElementReactionNames::adoptedCallback, {}); + lifecycle_callbacks.set(CustomElementReactionNames::connectedMoveCallback, {}); lifecycle_callbacks.set(CustomElementReactionNames::attributeChangedCallback, {}); // 4. For each callbackName of the keys of lifecycleCallbacks: - for (auto const& callback_name : { CustomElementReactionNames::connectedCallback, CustomElementReactionNames::disconnectedCallback, CustomElementReactionNames::adoptedCallback, CustomElementReactionNames::attributeChangedCallback }) { + for (auto const& callback_name : { CustomElementReactionNames::connectedCallback, CustomElementReactionNames::disconnectedCallback, CustomElementReactionNames::adoptedCallback, CustomElementReactionNames::connectedMoveCallback, CustomElementReactionNames::attributeChangedCallback }) { // 1. Let callbackValue be ? Get(prototype, callbackName). auto callback_value = TRY(prototype.get(callback_name)); diff --git a/Libraries/LibWeb/HTML/FormAssociatedElement.cpp b/Libraries/LibWeb/HTML/FormAssociatedElement.cpp index 4f597becb14..29b9e3c0eae 100644 --- a/Libraries/LibWeb/HTML/FormAssociatedElement.cpp +++ b/Libraries/LibWeb/HTML/FormAssociatedElement.cpp @@ -116,6 +116,14 @@ void FormAssociatedElement::form_node_was_removed() reset_form_owner(); } +// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#association-of-controls-and-forms:attr-fae-form-2 +void FormAssociatedElement::form_node_was_moved() +{ + // When a listed form-associated element's form attribute is set, changed, or removed, then the user agent must reset the form owner of that element. + if (m_form && &form_associated_element_to_html_element().root() != &m_form->root()) + reset_form_owner(); +} + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#association-of-controls-and-forms:category-listed-3 void FormAssociatedElement::form_node_attribute_changed(FlyString const& name, Optional const& value) { diff --git a/Libraries/LibWeb/HTML/FormAssociatedElement.h b/Libraries/LibWeb/HTML/FormAssociatedElement.h index 36a98fc9596..ecba6f963a5 100644 --- a/Libraries/LibWeb/HTML/FormAssociatedElement.h +++ b/Libraries/LibWeb/HTML/FormAssociatedElement.h @@ -45,6 +45,13 @@ private: form_associated_element_was_removed(old_parent); \ } \ \ + virtual void moved_from(GC::Ptr old_parent) override \ + { \ + ElementBaseClass::moved_from(old_parent); \ + form_node_was_moved(); \ + form_associated_element_was_moved(old_parent); \ + } \ + \ virtual void attribute_changed(FlyString const& name, Optional const& old_value, Optional const& value, Optional const& namespace_) override \ { \ ElementBaseClass::attribute_changed(name, old_value, value, namespace_); \ @@ -137,10 +144,12 @@ protected: virtual void form_associated_element_was_inserted() { } virtual void form_associated_element_was_removed(DOM::Node*) { } + virtual void form_associated_element_was_moved(GC::Ptr) { } virtual void form_associated_element_attribute_changed(FlyString const&, Optional const&, Optional const&) { } void form_node_was_inserted(); void form_node_was_removed(); + void form_node_was_moved(); void form_node_attribute_changed(FlyString const&, Optional const&); private: diff --git a/Libraries/LibWeb/HTML/HTMLSourceElement.cpp b/Libraries/LibWeb/HTML/HTMLSourceElement.cpp index aabd1aa5272..e068e7dac59 100644 --- a/Libraries/LibWeb/HTML/HTMLSourceElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLSourceElement.cpp @@ -48,7 +48,16 @@ void HTMLSourceElement::inserted() // count this as a relevant mutation for child. } -// https://html.spec.whatwg.org/multipage/embedded-content.html#the-source-element:html-element-removing-steps +// https://html.spec.whatwg.org/multipage/embedded-content.html#the-source-element:the-source-element-17 +void HTMLSourceElement::moved_from(GC::Ptr old_parent) +{ + Base::moved_from(old_parent); + + // FIXME: 1. If oldParent is a picture element, then for each child of oldParent's children, if child is an img + // element, then count this as a relevant mutation for child. +} + +// https://html.spec.whatwg.org/multipage/embedded-content.html#the-source-element:the-source-element-18 void HTMLSourceElement::removed_from(DOM::Node* old_parent, DOM::Node& old_root) { // The source HTML element removing steps, given removedNode and oldParent, are: diff --git a/Libraries/LibWeb/HTML/HTMLSourceElement.h b/Libraries/LibWeb/HTML/HTMLSourceElement.h index 918f069ac25..5e23f44314b 100644 --- a/Libraries/LibWeb/HTML/HTMLSourceElement.h +++ b/Libraries/LibWeb/HTML/HTMLSourceElement.h @@ -24,6 +24,7 @@ private: virtual void inserted() override; virtual void removed_from(DOM::Node* old_parent, DOM::Node& old_root) override; + virtual void moved_from(GC::Ptr old_parent) override; }; } diff --git a/Tests/LibWeb/Crash/wpt-import/dom/nodes/moveBefore/tentative/chrome-338071841-crash.html b/Tests/LibWeb/Crash/wpt-import/dom/nodes/moveBefore/tentative/chrome-338071841-crash.html new file mode 100644 index 00000000000..26adfb1cbfa --- /dev/null +++ b/Tests/LibWeb/Crash/wpt-import/dom/nodes/moveBefore/tentative/chrome-338071841-crash.html @@ -0,0 +1,6 @@ + + +
+ diff --git a/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/Node-moveBefore.txt b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/Node-moveBefore.txt new file mode 100644 index 00000000000..a02099d3252 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/Node-moveBefore.txt @@ -0,0 +1,38 @@ +Harness status: OK + +Found 32 tests + +31 Pass +1 Fail +Pass If node is a host-including inclusive ancestor of parent, then throw a HierarchyRequestError DOMException. +Pass If node is not a DocumentFragment, DocumentType, Element, Text, ProcessingInstruction, or Comment node, then throw a HierarchyRequestError DOMException. +Pass If node is a Text node and parent is a document, then throw a HierarchyRequestError DOMException. +Pass If node is a doctype and parent is not a document, then throw a HierarchyRequestError DOMException. +Pass If node is a DocumentFragment with multiple elements and parent is a document, then throw a HierarchyRequestError DOMException. +Pass If node is a DocumentFragment with an element and parent is a document with another element, then throw a HierarchyRequestError DOMException. +Pass If node is an Element and parent is a document with another element, then throw a HierarchyRequestError DOMException. +Pass If node is a doctype and parent is a document with another doctype, then throw a HierarchyRequestError DOMException. +Pass If node is a doctype and parent is a document with an element, then throw a HierarchyRequestError DOMException. +Pass Calling moveBefore with a non-Node first argument must throw TypeError. +Pass Calling moveBefore with second argument missing, or other than Node, null, or undefined, must throw TypeError. +Pass moveBefore() method does not exist on non-ParentNode Nodes +Pass moveBefore() on disconnected parent throws a HierarchyRequestError +Pass moveBefore() with disconnected target node throws a HierarchyRequestError +Fail moveBefore() on a cross-document target node throws a HierarchyRequestError +Pass moveBefore() into a Document throws a HierarchyRequestError +Pass moveBefore() CharacterData into a Document +Pass moveBefore() with node being an inclusive ancestor of parent throws a HierarchyRequestError +Pass moveBefore() with a non-{Element, CharacterData} throws a HierarchyRequestError +Pass moveBefore with an Element or CharacterData succeeds +Pass moveBefore on a paragraph's Text node child +Pass moveBefore with reference child whose parent is NOT the destination parent (context node) throws a NotFoundError. +Pass moveBefore() returns undefined +Pass Moving a node before itself should not move the node +Pass Moving a node from a disconnected container to a disconnected new parent without a shared ancestor throws a HIERARCHY_REQUEST_ERR +Pass Moving a node from a disconnected container to a disconnected new parent in the same tree succeeds +Pass Moving a node from a disconnected container to a disconnected new parent in the same tree succeeds,also across shadow-roots +Pass Moving a node from disconnected->connected throws a HIERARCHY_REQUEST_ERR +Pass Moving a node from connected->disconnected throws a HIERARCHY_REQUEST_ERR +Pass No custom element callbacks are run during disconnected moveBefore() +Pass Invalid node hierarchy with null old parent does not crash +Pass Move disconnected iframe does not crash \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/custom-element-move-reactions.txt b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/custom-element-move-reactions.txt new file mode 100644 index 00000000000..3ca03942549 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/custom-element-move-reactions.txt @@ -0,0 +1,11 @@ +Harness status: OK + +Found 6 tests + +6 Pass +Pass the disconnected/connected callbacks should be called when no other callback is defined +Pass the element should stay connected during the callbacks +Pass When connectedMoveCallback is defined, it is called instead of disconnectedCallback/connectedCallback +Pass Reactions to atomic move are called in order of element, not in order of operation +Pass When connectedCallback is not defined, no crash +Pass When disconnectedCallback is not defined, no crash \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/focus-within.txt b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/focus-within.txt new file mode 100644 index 00000000000..37eff9dffdc --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/focus-within.txt @@ -0,0 +1,11 @@ +Harness status: OK + +Found 5 tests + +3 Pass +2 Fail +Pass focus-within should be updated when reparenting focused element directly +Pass focus-within should be updated when reparenting an element that has focus within +Pass focus-within should remain the same when moving to the same parent +Fail :focus-within should be eventually up to date when moving to an inert subtree +Fail :focus-within should be eventually up to date when moving to a subtree that would become inert via style \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/live-range-updates.txt b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/live-range-updates.txt new file mode 100644 index 00000000000..43d5685aee5 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/live-range-updates.txt @@ -0,0 +1,8 @@ +Harness status: OK + +Found 3 tests + +3 Pass +Pass moveBefore still results in range startContainer snapping up to parent when startContainer is moved +Pass moveBefore still causes range startContainer to snap up to parent, when startContainer ancestor is moved +Pass moveBefore still causes range endContainer to snap up to parent, when endContainer ancestor is moved \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/moveBefore-shadow-inside.txt b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/moveBefore-shadow-inside.txt new file mode 100644 index 00000000000..cec3470e421 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/moveBefore-shadow-inside.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass moveBefore-shadow-inside \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/moveBefore-shadow-root.txt b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/moveBefore-shadow-root.txt new file mode 100644 index 00000000000..9f17e287435 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/moveBefore-shadow-root.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass moveBefore() is allowed in ShadowRoots (i.e., connected DocumentFragments) \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/mutation-events.txt b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/mutation-events.txt new file mode 100644 index 00000000000..1ae64a001e5 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/mutation-events.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass MutationEvents (if supported by the UA) are suppressed during `moveBefore()` \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/mutation-observer.txt b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/mutation-observer.txt new file mode 100644 index 00000000000..126f10a940b --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/mutation-observer.txt @@ -0,0 +1,7 @@ +Harness status: OK + +Found 2 tests + +2 Pass +Pass [Connected move] MutationObserver removal + insertion is tracked by moveBefore() +Pass [Disconnected move] MutationObserver removal + insertion is tracked by moveBefore() \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/nonce.txt b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/nonce.txt new file mode 100644 index 00000000000..8ba85bff5e1 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/nonce.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Fail +Fail Element nonce content attribute is not cleared after move \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/object-crash-regression.txt b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/object-crash-regression.txt new file mode 100644 index 00000000000..0bc8f8a8935 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/object-crash-regression.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Moving an object element does not crash \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/popover-preserve.txt b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/popover-preserve.txt new file mode 100644 index 00000000000..f12bc71b808 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/popover-preserve.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass when reparenting an open popover, it shouldn't be closed automatically \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/style-applies.txt b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/style-applies.txt new file mode 100644 index 00000000000..eef74920ad6 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/moveBefore/tentative/style-applies.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Moving a style inside the document should not affect whether it's applied \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/Node-moveBefore.html b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/Node-moveBefore.html new file mode 100644 index 00000000000..348561a7e50 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/Node-moveBefore.html @@ -0,0 +1,349 @@ + +Node.moveBefore + + +
+ + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/custom-element-move-reactions.html b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/custom-element-move-reactions.html new file mode 100644 index 00000000000..b1e5ab892c0 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/custom-element-move-reactions.html @@ -0,0 +1,133 @@ + +Node.moveBefore custom element reactions + + + + + +
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/focus-within.html b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/focus-within.html new file mode 100644 index 00000000000..87d53b31061 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/focus-within.html @@ -0,0 +1,81 @@ + +moveBefore should handle focus bubbling correctly + + + +
+ +
+
+
+
+
+
+
+ + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/live-range-updates.html b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/live-range-updates.html new file mode 100644 index 00000000000..9bdc9dfcb94 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/live-range-updates.html @@ -0,0 +1,94 @@ + + + + + + + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/moveBefore-shadow-inside.html b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/moveBefore-shadow-inside.html new file mode 100644 index 00000000000..d8e4f816e83 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/moveBefore-shadow-inside.html @@ -0,0 +1,47 @@ + + + + + +
+
+ +
+
+
+ +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/moveBefore-shadow-root.html b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/moveBefore-shadow-root.html new file mode 100644 index 00000000000..6b602213779 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/moveBefore-shadow-root.html @@ -0,0 +1,22 @@ + + + + + +
+ + + diff --git a/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/mutation-events.html b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/mutation-events.html new file mode 100644 index 00000000000..e405c425e3e --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/mutation-events.html @@ -0,0 +1,20 @@ + +Mutation events are suppressed during moveBefore() + + + + +

reference

+

target

+ + + diff --git a/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/mutation-observer.html b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/mutation-observer.html new file mode 100644 index 00000000000..b74ca81fb75 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/mutation-observer.html @@ -0,0 +1,52 @@ + +slotchanged event + + + + +
+

+
+
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/nonce.html b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/nonce.html new file mode 100644 index 00000000000..4ae68e52674 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/nonce.html @@ -0,0 +1,24 @@ + +Nonce attribute is not cleared + + + + +
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/object-crash-regression.html b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/object-crash-regression.html new file mode 100644 index 00000000000..460dc9cdc36 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/object-crash-regression.html @@ -0,0 +1,15 @@ + +Object element moveBefore() regression test + + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/popover-preserve.html b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/popover-preserve.html new file mode 100644 index 00000000000..d902f1ccf9a --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/popover-preserve.html @@ -0,0 +1,22 @@ + +moveBefore should not close a popover + + + +
+
+Popover +
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/style-applies.html b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/style-applies.html new file mode 100644 index 00000000000..312527c781e --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/moveBefore/tentative/style-applies.html @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/dom/nodes/pre-insertion-validation-hierarchy.js b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/pre-insertion-validation-hierarchy.js new file mode 100644 index 00000000000..6ef2576df2c --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/pre-insertion-validation-hierarchy.js @@ -0,0 +1,86 @@ +/** + * Validations where `child` argument is irrelevant. + * @param {Function} methodName + */ +function preInsertionValidateHierarchy(methodName) { + function insert(parent, node) { + if (parent[methodName].length > 1) { + // This is for insertBefore(). We can't blindly pass `null` for all methods + // as doing so will move nodes before validation. + parent[methodName](node, null); + } else { + parent[methodName](node); + } + } + + // Step 2 + test(() => { + const doc = document.implementation.createHTMLDocument("title"); + assert_throws_dom("HierarchyRequestError", () => insert(doc.body, doc.body)); + assert_throws_dom("HierarchyRequestError", () => insert(doc.body, doc.documentElement)); + }, "If node is a host-including inclusive ancestor of parent, then throw a HierarchyRequestError DOMException."); + + // Step 4 + test(() => { + const doc = document.implementation.createHTMLDocument("title"); + const doc2 = document.implementation.createHTMLDocument("title2"); + assert_throws_dom("HierarchyRequestError", () => insert(doc, doc2)); + }, "If node is not a DocumentFragment, DocumentType, Element, Text, ProcessingInstruction, or Comment node, then throw a HierarchyRequestError DOMException."); + + // Step 5, in case of inserting a text node into a document + test(() => { + const doc = document.implementation.createHTMLDocument("title"); + assert_throws_dom("HierarchyRequestError", () => insert(doc, doc.createTextNode("text"))); + }, "If node is a Text node and parent is a document, then throw a HierarchyRequestError DOMException."); + + // Step 5, in case of inserting a doctype into a non-document + test(() => { + const doc = document.implementation.createHTMLDocument("title"); + const doctype = doc.childNodes[0]; + assert_throws_dom("HierarchyRequestError", () => insert(doc.createElement("a"), doctype)); + }, "If node is a doctype and parent is not a document, then throw a HierarchyRequestError DOMException.") + + // Step 6, in case of DocumentFragment including multiple elements + test(() => { + const doc = document.implementation.createHTMLDocument("title"); + doc.documentElement.remove(); + const df = doc.createDocumentFragment(); + df.appendChild(doc.createElement("a")); + df.appendChild(doc.createElement("b")); + assert_throws_dom("HierarchyRequestError", () => insert(doc, df)); + }, "If node is a DocumentFragment with multiple elements and parent is a document, then throw a HierarchyRequestError DOMException."); + + // Step 6, in case of DocumentFragment has multiple elements when document already has an element + test(() => { + const doc = document.implementation.createHTMLDocument("title"); + const df = doc.createDocumentFragment(); + df.appendChild(doc.createElement("a")); + assert_throws_dom("HierarchyRequestError", () => insert(doc, df)); + }, "If node is a DocumentFragment with an element and parent is a document with another element, then throw a HierarchyRequestError DOMException."); + + // Step 6, in case of an element + test(() => { + const doc = document.implementation.createHTMLDocument("title"); + const el = doc.createElement("a"); + assert_throws_dom("HierarchyRequestError", () => insert(doc, el)); + }, "If node is an Element and parent is a document with another element, then throw a HierarchyRequestError DOMException."); + + // Step 6, in case of a doctype when document already has another doctype + test(() => { + const doc = document.implementation.createHTMLDocument("title"); + const doctype = doc.childNodes[0].cloneNode(); + doc.documentElement.remove(); + assert_throws_dom("HierarchyRequestError", () => insert(doc, doctype)); + }, "If node is a doctype and parent is a document with another doctype, then throw a HierarchyRequestError DOMException."); + + // Step 6, in case of a doctype when document has an element + if (methodName !== "prepend") { + // Skip `.prepend` as this doesn't throw if `child` is an element + test(() => { + const doc = document.implementation.createHTMLDocument("title"); + const doctype = doc.childNodes[0].cloneNode(); + doc.childNodes[0].remove(); + assert_throws_dom("HierarchyRequestError", () => insert(doc, doctype)); + }, "If node is a doctype and parent is a document with an element, then throw a HierarchyRequestError DOMException."); + } +}