diff --git a/Tests/LibWeb/Text/expected/HTML/Form-indexed-property-access.txt b/Tests/LibWeb/Text/expected/HTML/Form-indexed-property-access.txt new file mode 100644 index 0000000000..7c43bf7a6b --- /dev/null +++ b/Tests/LibWeb/Text/expected/HTML/Form-indexed-property-access.txt @@ -0,0 +1,14 @@ + form.length: 12 +elements.length: 12 +form[0].name: foo +form[1].name: bar +form[2].name: baz +form[3].name: qux +form[4].name: quux +form[5].name: corge +form[6].name: foo2 +form[7].name: bar2 +form[8].name: baz2 +form[9].name: qux2 +form[10].name: quux2 +form[11].name: corge2 diff --git a/Tests/LibWeb/Text/expected/HTML/Form-named-property-access.txt b/Tests/LibWeb/Text/expected/HTML/Form-named-property-access.txt new file mode 100644 index 0000000000..1344a3d714 --- /dev/null +++ b/Tests/LibWeb/Text/expected/HTML/Form-named-property-access.txt @@ -0,0 +1,34 @@ + == Elements and Names == +formy.length: 12 +elements.length: 12 +elements[0] === form.foo +elements[1] === form.bar +elements[2] === form.baz +elements[3] === form.qux +elements[4] === form.quux +elements[5] === form.corge +elements[6] === form.foo2 +elements[7] === form.bar2 +elements[8] === form.baz2 +elements[9] === form.qux2 +elements[10] === form.quux2 +elements[11] === form.corge2 +== If no listed elements, picks img == +form.inside == image: true +== Form association == +elements in form2: 2 +elements in form3: 2 +== Same name and id for many elements == +elements in samename: 6 +samename.a.length: 6 +typeof samename.a: object +elements in sameid: 6 +sameid.a.length: 6 +typeof sameid.a: object +== Changing name/id == +elements in changy: 1 +hello is goodbye? true +Can we still use the same name?: true +new hello is goodbye? false +new hello is old hello? false +new hello is newInput? true diff --git a/Tests/LibWeb/Text/input/HTML/Form-indexed-property-access.html b/Tests/LibWeb/Text/input/HTML/Form-indexed-property-access.html new file mode 100644 index 0000000000..e4795c9c6c --- /dev/null +++ b/Tests/LibWeb/Text/input/HTML/Form-indexed-property-access.html @@ -0,0 +1,44 @@ +
+ + + + + +
+ + +
+ +
+ + + + + + +
+ + +
+ + + + diff --git a/Tests/LibWeb/Text/input/HTML/Form-named-property-access.html b/Tests/LibWeb/Text/input/HTML/Form-named-property-access.html new file mode 100644 index 0000000000..17abba5682 --- /dev/null +++ b/Tests/LibWeb/Text/input/HTML/Form-named-property-access.html @@ -0,0 +1,121 @@ +
+ + + + + +
+ + +
+ +
+ + + + + + +
+ + +
+ +
+
+
+ + + +
+
+ +
+ + + + + +
+ + +
+ +
+ +
+ + + + + +
+ + +
+ +
+ +
+ +
+ + + diff --git a/Userland/Libraries/LibWeb/HTML/HTMLFormElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLFormElement.cpp index 33423a307e..28933878b3 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLFormElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/HTMLFormElement.cpp @@ -6,6 +6,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include @@ -18,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +38,12 @@ JS_DEFINE_ALLOCATOR(HTMLFormElement); HTMLFormElement::HTMLFormElement(DOM::Document& document, DOM::QualifiedName qualified_name) : HTMLElement(document, move(qualified_name)) { + m_legacy_platform_object_flags = LegacyPlatformObjectFlags { + .supports_indexed_properties = true, + .supports_named_properties = true, + .has_legacy_unenumerable_named_properties_interface_extended_attribute = true, + .has_legacy_override_built_ins_interface_extended_attribute = true, + }; } HTMLFormElement::~HTMLFormElement() = default; @@ -299,6 +307,9 @@ void HTMLFormElement::add_associated_element(Badge, HTMLE void HTMLFormElement::remove_associated_element(Badge, HTMLElement& element) { m_associated_elements.remove_first_matching([&](auto& entry) { return entry.ptr() == &element; }); + + // If an element listed in a form element's past names map changes form owner, then its entries must be removed from that map. + m_past_names_map.remove_all_matching([&](auto&, auto const& entry) { return entry.node == &element; }); } // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-fs-action @@ -382,10 +393,17 @@ HTMLFormElement::EncodingTypeAttributeState HTMLFormElement::encoding_type_state return encoding_type_attribute_to_encoding_type_state(this->deprecated_attribute(AttributeNames::enctype)); } -static bool is_form_control(DOM::Element const& element) +// https://html.spec.whatwg.org/multipage/forms.html#category-listed +static bool is_listed_element(DOM::Element const& element) { + // Denotes elements that are listed in the form.elements and fieldset.elements APIs. + // These elements also have a form content attribute, and a matching form IDL attribute, + // that allow authors to specify an explicit form owner. + // => button, fieldset, input, object, output, select, textarea, form-associated custom elements + if (is(element) || is(element) + || is(element) || is(element) || is(element) || is(element) @@ -393,22 +411,40 @@ static bool is_form_control(DOM::Element const& element) return true; } - if (is(element) - && !element.deprecated_get_attribute(HTML::AttributeNames::type).equals_ignoring_ascii_case("image"sv)) { - return true; - } - // FIXME: Form-associated custom elements return also true return false; } +static bool is_form_control(DOM::Element const& element, HTMLFormElement const& form) +{ + // The elements IDL attribute must return an HTMLFormControlsCollection rooted at the form element's root, + // whose filter matches listed elements whose form owner is the form element, + // with the exception of input elements whose type attribute is in the Image Button state, which must, + // for historical reasons, be excluded from this particular collection. + + if (!is_listed_element(element)) + return false; + + if (is(element) + && static_cast(element).type_state() == HTMLInputElement::TypeAttributeState::ImageButton) { + return false; + } + + auto const& form_associated_element = dynamic_cast(element); + if (form_associated_element.form() != &form) + return false; + + return true; +} + // https://html.spec.whatwg.org/multipage/forms.html#dom-form-elements JS::NonnullGCPtr HTMLFormElement::elements() const { if (!m_elements) { - m_elements = DOM::HTMLFormControlsCollection::create(const_cast(*this), DOM::HTMLCollection::Scope::Descendants, [](Element const& element) { - return is_form_control(element); + auto& root = verify_cast(const_cast(this)->root()); + m_elements = DOM::HTMLFormControlsCollection::create(root, DOM::HTMLCollection::Scope::Descendants, [this](Element const& element) { + return is_form_control(element, *this); }); } return *m_elements; @@ -815,4 +851,176 @@ void HTMLFormElement::plan_to_navigate_to(AK::URL url, Variantlength(); +} + +// https://html.spec.whatwg.org/multipage/forms.html#dom-form-item +WebIDL::ExceptionOr HTMLFormElement::item_value(size_t index) const +{ + // To determine the value of an indexed property for a form element, the user agent must return the value returned by + // the item method on the elements collection, when invoked with the given index as its argument. + return elements()->item(index); +} + +// https://html.spec.whatwg.org/multipage/forms.html#the-form-element:supported-property-names +Vector HTMLFormElement::supported_property_names() const +{ + // The supported property names consist of the names obtained from the following algorithm, in the order obtained from this algorithm: + + // 1. Let sourced names be an initially empty ordered list of tuples consisting of a string, an element, a source, + // where the source is either id, name, or past, and, if the source is past, an age. + struct SourcedName { + FlyString name; + JS::GCPtr element; + enum class Source { + Id, + Name, + Past, + } source; + Duration age; + }; + Vector sourced_names; + + // 2. For each listed element candidate whose form owner is the form element, with the exception of any + // input elements whose type attribute is in the Image Button state: + for (auto const& candidate : m_associated_elements) { + if (!is_form_control(*candidate, *this)) + continue; + + // 1. If candidate has an id attribute, add an entry to sourced names with that id attribute's value as the + // string, candidate as the element, and id as the source. + if (candidate->has_attribute(HTML::AttributeNames::id)) + sourced_names.append(SourcedName { candidate->id().value(), candidate, SourcedName::Source::Id, {} }); + + // 2. If candidate has a name attribute, add an entry to sourced names with that name attribute's value as the + // string, candidate as the element, and name as the source. + if (candidate->has_attribute(HTML::AttributeNames::name)) + sourced_names.append(SourcedName { candidate->attribute(HTML::AttributeNames::name).value(), candidate, SourcedName::Source::Name, {} }); + } + + // 3. For each img element candidate whose form owner is the form element: + for (auto const& candidate : m_associated_elements) { + if (!is(*candidate)) + continue; + + // Every element in m_associated_elements has this as the form owner. + + // 1. If candidate has an id attribute, add an entry to sourced names with that id attribute's value as the + // string, candidate as the element, and id as the source. + if (candidate->has_attribute(HTML::AttributeNames::id)) + sourced_names.append(SourcedName { candidate->id().value(), candidate, SourcedName::Source::Id, {} }); + + // 2. If candidate has a name attribute, add an entry to sourced names with that name attribute's value as the + // string, candidate as the element, and name as the source. + if (candidate->has_attribute(HTML::AttributeNames::name)) + sourced_names.append(SourcedName { candidate->attribute(HTML::AttributeNames::name).value(), candidate, SourcedName::Source::Name, {} }); + } + + // 4. For each entry past entry in the past names map add an entry to sourced names with the past entry's name as + // the string, past entry's element as the element, past as the source, and the length of time past entry has + // been in the past names map as the age. + auto const now = MonotonicTime::now(); + for (auto const& entry : m_past_names_map) + sourced_names.append(SourcedName { entry.key, static_cast(entry.value.node.ptr()), SourcedName::Source::Past, now - entry.value.insertion_time }); + + // 5. Sort sourced names by tree order of the element entry of each tuple, sorting entries with the same element by + // putting entries whose source is id first, then entries whose source is name, and finally entries whose source + // is past, and sorting entries with the same element and source by their age, oldest first. + // FIXME: Require less const casts here by changing the signature of DOM::Node::compare_document_position + quick_sort(sourced_names, [](auto const& lhs, auto const& rhs) -> bool { + if (lhs.element != rhs.element) + return const_cast(lhs.element.ptr())->compare_document_position(const_cast(rhs.element.ptr())) & DOM::Node::DOCUMENT_POSITION_FOLLOWING; + if (lhs.source != rhs.source) + return lhs.source < rhs.source; + return lhs.age < rhs.age; + }); + + // FIXME: Surely there's a more efficient way to do this without so many FlyStrings and collections? + // 6. Remove any entries in sourced names that have the empty string as their name. + // 7. Remove any entries in sourced names that have the same name as an earlier entry in the map. + // 8. Return the list of names from sourced names, maintaining their relative order. + OrderedHashTable names; + names.ensure_capacity(sourced_names.size()); + for (auto const& entry : sourced_names) { + if (entry.name.is_empty()) + continue; + names.set(entry.name, AK::HashSetExistingEntryBehavior::Keep); + } + + Vector result; + result.ensure_capacity(names.size()); + for (auto const& name : names) + result.unchecked_append(name); + + return result; +} + +// https://html.spec.whatwg.org/multipage/forms.html#dom-form-nameditem +WebIDL::ExceptionOr HTMLFormElement::named_item_value(FlyString const& name) const +{ + auto& realm = this->realm(); + auto& root = verify_cast(this->root()); + + // To determine the value of a named property name for a form element, the user agent must run the following steps: + + // 1. Let candidates be a live RadioNodeList object containing all the listed elements, whose form owner is the form + // element, that have either an id attribute or a name attribute equal to name, with the exception of input + // elements whose type attribute is in the Image Button state, in tree order. + auto candidates = DOM::RadioNodeList::create(realm, root, DOM::LiveNodeList::Scope::Descendants, [this, name](auto& node) -> bool { + if (!is(node)) + return false; + auto const& element = static_cast(node); + + // Form controls are defined as listed elements, with the exception of input elements in the Image Button state, + // whose form owner is the form element. + if (!is_form_control(element, *this)) + return false; + + // FIXME: DOM::Element::name() isn't cached + return name == element.id() || name == element.attribute(HTML::AttributeNames::name); + }); + + // 2. If candidates is empty, let candidates be a live RadioNodeList object containing all the img elements, + // whose form owner is the form element, that have either an id attribute or a name attribute equal to name, + // in tree order. + if (candidates->length() == 0) { + candidates = DOM::RadioNodeList::create(realm, root, DOM::LiveNodeList::Scope::Descendants, [this, name](auto& node) -> bool { + if (!is(node)) + return false; + + auto const& element = static_cast(node); + if (element.form() != this) + return false; + + // FIXME: DOM::Element::name() isn't cached + return name == element.id() || name == element.attribute(HTML::AttributeNames::name); + }); + } + + auto length = candidates->length(); + + // 3. If candidates is empty, name is the name of one of the entries in the form element's past names map: return the object associated with name in that map. + if (length == 0) { + auto it = m_past_names_map.find(name); + if (it != m_past_names_map.end()) + return it->value.node; + } + + // 4. If candidates contains more than one node, return candidates. + if (length > 1) + return candidates; + + // 5. Otherwise, candidates contains exactly one node. Add a mapping from name to the node in candidates in the form + // element's past names map, replacing the previous entry with the same name, if any. + auto const* node = candidates->item(0); + m_past_names_map.set(name, HTMLFormElement::PastNameEntry { .node = node, .insertion_time = MonotonicTime::now() }); + + // 6. Return the node in candidates. + return node; +} + } diff --git a/Userland/Libraries/LibWeb/HTML/HTMLFormElement.h b/Userland/Libraries/LibWeb/HTML/HTMLFormElement.h index 94988bfa69..8d574ad5a0 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLFormElement.h +++ b/Userland/Libraries/LibWeb/HTML/HTMLFormElement.h @@ -8,6 +8,7 @@ #pragma once +#include #include #include #include @@ -95,6 +96,12 @@ private: virtual void initialize(JS::Realm&) override; virtual void visit_edges(Cell::Visitor&) override; + // ^PlatformObject + virtual WebIDL::ExceptionOr item_value(size_t index) const override; + virtual WebIDL::ExceptionOr named_item_value(FlyString const& name) const override; + virtual Vector supported_property_names() const override; + virtual bool is_supported_property_index(u32) const override; + ErrorOr populate_vector_with_submittable_elements_in_tree_order(JS::NonnullGCPtr element, Vector>& elements); ErrorOr pick_an_encoding() const; @@ -113,6 +120,13 @@ private: Vector> m_associated_elements; + // https://html.spec.whatwg.org/multipage/forms.html#past-names-map + struct PastNameEntry { + JS::GCPtr node; + MonotonicTime insertion_time; + }; + HashMap mutable m_past_names_map; + JS::GCPtr mutable m_elements; bool m_constructing_entry_list { false }; diff --git a/Userland/Libraries/LibWeb/HTML/HTMLFormElement.idl b/Userland/Libraries/LibWeb/HTML/HTMLFormElement.idl index d6c99826b9..44cca996ea 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLFormElement.idl +++ b/Userland/Libraries/LibWeb/HTML/HTMLFormElement.idl @@ -2,7 +2,7 @@ #import // https://html.spec.whatwg.org/multipage/semantics.html#htmlformelement -[Exposed=Window] +[Exposed=Window, LegacyOverrideBuiltIns, LegacyUnenumerableNamedProperties] interface HTMLFormElement : HTMLElement { [HTMLConstructor] constructor(); @@ -21,8 +21,8 @@ interface HTMLFormElement : HTMLElement { [SameObject] readonly attribute HTMLFormControlsCollection elements; readonly attribute unsigned long length; - // FIXME: getter Element (unsigned long index); - // FIXME: getter (RadioNodeList or Element) (DOMString name); + getter Element (unsigned long index); + getter (RadioNodeList or Element) (DOMString name); undefined submit(); // FIXME: undefined requestSubmit(optional HTMLElement? submitter = null);