From a17074422e3735ae824b23fac441e06115d12483 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Sat, 3 Feb 2024 10:29:26 -0500 Subject: [PATCH] LibWeb: Reset form association when any element with an ID changes When an element with an ID is added to or removed from the DOM, or if an ID is added, removed, or changed, then we must reset the form owner of all form-associated elements who have a form attribute. We do this in 2 steps, using the DOM document as the messenger to handle these changes: 1. All form-associated elements with a form attribute are stored on the document. If the form attribute is removed, the element is removed from that list as well. 2. When a DOM element with an ID undergoes any of the aforementioned changes, it notifies the document of the change. The document then forwards that change to the stored form-associated elements. --- .../HTML/Form-named-property-access.txt | 5 ++- .../HTML/Form-named-property-access.html | 8 +++++ Userland/Libraries/LibWeb/DOM/Document.cpp | 27 ++++++++++++++++ Userland/Libraries/LibWeb/DOM/Document.h | 8 +++++ Userland/Libraries/LibWeb/DOM/Element.cpp | 18 +++++++++++ Userland/Libraries/LibWeb/DOM/Element.h | 2 ++ .../LibWeb/HTML/FormAssociatedElement.cpp | 32 ++++++++++++++++--- .../LibWeb/HTML/FormAssociatedElement.h | 3 ++ 8 files changed, 97 insertions(+), 6 deletions(-) diff --git a/Tests/LibWeb/Text/expected/HTML/Form-named-property-access.txt b/Tests/LibWeb/Text/expected/HTML/Form-named-property-access.txt index dee9ba8cbd..3476db93e7 100644 --- a/Tests/LibWeb/Text/expected/HTML/Form-named-property-access.txt +++ b/Tests/LibWeb/Text/expected/HTML/Form-named-property-access.txt @@ -1,4 +1,4 @@ - == Elements and Names == + == Elements and Names == formy.length: 12 elements.length: 12 elements[0] === form.foo @@ -38,3 +38,6 @@ elements in changeForFormAttribute: 1 elements in changeForFormAttribute: 0 elements in changeForFormAttribute: 1 elements in changeForFormAttribute: 0 +== Form element appears after a form-associated element == +elements in formAfterInput: 1 +typeof formAfterInput.inputBeforeForm: object diff --git a/Tests/LibWeb/Text/input/HTML/Form-named-property-access.html b/Tests/LibWeb/Text/input/HTML/Form-named-property-access.html index 8b492768e7..1f928a5ae0 100644 --- a/Tests/LibWeb/Text/input/HTML/Form-named-property-access.html +++ b/Tests/LibWeb/Text/input/HTML/Form-named-property-access.html @@ -63,6 +63,9 @@
+ +
+ diff --git a/Userland/Libraries/LibWeb/DOM/Document.cpp b/Userland/Libraries/LibWeb/DOM/Document.cpp index cc5e9e6a66..f283e0e069 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.cpp +++ b/Userland/Libraries/LibWeb/DOM/Document.cpp @@ -449,6 +449,9 @@ void Document::visit_edges(Cell::Visitor& visitor) visitor.visit(timeline); visitor.visit(m_list_of_available_images); + + for (auto* form_associated_element : m_form_associated_elements_with_form_attribute) + visitor.visit(form_associated_element->form_associated_element_to_html_element()); } // https://w3c.github.io/selection-api/#dom-document-getselection @@ -3683,4 +3686,28 @@ void Document::append_pending_animation_event(Web::DOM::Document::PendingAnimati m_pending_animation_event_queue.append(event); } +void Document::element_id_changed(Badge) +{ + for (auto* form_associated_element : m_form_associated_elements_with_form_attribute) + form_associated_element->element_id_changed({}); +} + +void Document::element_with_id_was_added_or_removed(Badge) +{ + for (auto* form_associated_element : m_form_associated_elements_with_form_attribute) + form_associated_element->element_with_id_was_added_or_removed({}); +} + +void Document::add_form_associated_element_with_form_attribute(HTML::FormAssociatedElement& form_associated_element) +{ + m_form_associated_elements_with_form_attribute.append(&form_associated_element); +} + +void Document::remove_form_associated_element_with_form_attribute(HTML::FormAssociatedElement& form_associated_element) +{ + m_form_associated_elements_with_form_attribute.remove_all_matching([&](auto* element) { + return element == &form_associated_element; + }); +} + } diff --git a/Userland/Libraries/LibWeb/DOM/Document.h b/Userland/Libraries/LibWeb/DOM/Document.h index 3d82547285..778ecb4b99 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.h +++ b/Userland/Libraries/LibWeb/DOM/Document.h @@ -554,6 +554,12 @@ public: JS::GCPtr latest_entry() const { return m_latest_entry; } void set_latest_entry(JS::GCPtr e) { m_latest_entry = e; } + void element_id_changed(Badge); + void element_with_id_was_added_or_removed(Badge); + + void add_form_associated_element_with_form_attribute(HTML::FormAssociatedElement&); + void remove_form_associated_element_with_form_attribute(HTML::FormAssociatedElement&); + protected: virtual void initialize(JS::Realm&) override; virtual void visit_edges(Cell::Visitor&) override; @@ -775,6 +781,8 @@ private: // https://html.spec.whatwg.org/multipage/browsing-the-web.html#scripts-may-run-for-the-newly-created-document bool m_ready_to_run_scripts { false }; + + Vector m_form_associated_elements_with_form_attribute; }; template<> diff --git a/Userland/Libraries/LibWeb/DOM/Element.cpp b/Userland/Libraries/LibWeb/DOM/Element.cpp index e129c24e96..9bc01bccfc 100644 --- a/Userland/Libraries/LibWeb/DOM/Element.cpp +++ b/Userland/Libraries/LibWeb/DOM/Element.cpp @@ -465,6 +465,8 @@ void Element::attribute_changed(FlyString const& name, Optional const& v m_id = {}; else m_id = value_or_empty; + + document().element_id_changed({}); } else if (name == HTML::AttributeNames::name) { if (!value.has_value()) m_name = {}; @@ -1026,6 +1028,22 @@ int Element::client_height() const return paintable_box()->absolute_padding_box_rect().height().to_int(); } +void Element::inserted() +{ + Base::inserted(); + + if (m_id.has_value()) + document().element_with_id_was_added_or_removed({}); +} + +void Element::removed_from(Node* node) +{ + Base::removed_from(node); + + if (m_id.has_value()) + document().element_with_id_was_added_or_removed({}); +} + void Element::children_changed() { Node::children_changed(); diff --git a/Userland/Libraries/LibWeb/DOM/Element.h b/Userland/Libraries/LibWeb/DOM/Element.h index fa1218c52e..6a0e1b7ccc 100644 --- a/Userland/Libraries/LibWeb/DOM/Element.h +++ b/Userland/Libraries/LibWeb/DOM/Element.h @@ -375,6 +375,8 @@ protected: Element(Document&, DOM::QualifiedName); virtual void initialize(JS::Realm&) override; + virtual void inserted() override; + virtual void removed_from(Node*) override; virtual void children_changed() override; virtual i32 default_tab_index_value() const; diff --git a/Userland/Libraries/LibWeb/HTML/FormAssociatedElement.cpp b/Userland/Libraries/LibWeb/HTML/FormAssociatedElement.cpp index 333d424830..7f375ba35b 100644 --- a/Userland/Libraries/LibWeb/HTML/FormAssociatedElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/FormAssociatedElement.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include @@ -73,24 +74,45 @@ void FormAssociatedElement::form_node_was_removed() } // 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&) +void FormAssociatedElement::form_node_attribute_changed(FlyString const& name, Optional const& value) { // 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 (name == HTML::AttributeNames::form) { + auto& html_element = form_associated_element_to_html_element(); + + if (value.has_value()) + html_element.document().add_form_associated_element_with_form_attribute(*this); + else + html_element.document().remove_form_associated_element_with_form_attribute(*this); + reset_form_owner(); } } +// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#association-of-controls-and-forms:category-listed-4 +void FormAssociatedElement::element_id_changed(Badge) +{ + // When a listed form-associated element has a form attribute and the ID of any of the elements in the tree changes, + // then the user agent must reset the form owner of that form-associated element. + VERIFY(form_associated_element_to_html_element().has_attribute(HTML::AttributeNames::form)); + reset_form_owner(); +} + +// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#association-of-controls-and-forms:category-listed-5 +void FormAssociatedElement::element_with_id_was_added_or_removed(Badge) +{ + // When a listed form-associated element has a form attribute and an element with an ID is inserted into or removed + // from the Document, then the user agent must reset the form owner of that form-associated element. + VERIFY(form_associated_element_to_html_element().has_attribute(HTML::AttributeNames::form)); + reset_form_owner(); +} + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#reset-the-form-owner void FormAssociatedElement::reset_form_owner() { auto& html_element = form_associated_element_to_html_element(); - // Although these aren't in the "reset form owner" algorithm, these here as they are triggers for this algorithm: - // FIXME: When a listed form-associated element has a form attribute and the ID of any of the elements in the tree changes, then the user agent must reset the form owner of that form-associated element. - // FIXME: When a listed form-associated element has a form attribute and an element with an ID is inserted into or removed from the Document, then the user agent must reset the form owner of that form-associated element. - // 1. Unset element's parser inserted flag. m_parser_inserted = false; diff --git a/Userland/Libraries/LibWeb/HTML/FormAssociatedElement.h b/Userland/Libraries/LibWeb/HTML/FormAssociatedElement.h index 90a69f7610..47a33e765d 100644 --- a/Userland/Libraries/LibWeb/HTML/FormAssociatedElement.h +++ b/Userland/Libraries/LibWeb/HTML/FormAssociatedElement.h @@ -55,6 +55,9 @@ public: void set_form(HTMLFormElement*); + void element_id_changed(Badge); + void element_with_id_was_added_or_removed(Badge); + bool enabled() const; void set_parser_inserted(Badge);