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);