LibWeb: Implement HTMLAllCollection

This collection has some pretty strange behaviour, particularly with the
IsHTMLDDA slot which is defined in the javascript spec specifically for
this object.

This commit implements pretty much all of this interface, besides from
the custom [[Call]].

There is also no caching over this collection. Since it is a live
collection over the entire document, the performance is never going to
be great, and I am not convinced any speedup for this legacy interface
is worth a massive cache.
This commit is contained in:
Shannon Booth 2024-04-01 08:44:24 +02:00 committed by Andreas Kling
parent 897f55ca8a
commit 1f59e21829
6 changed files with 312 additions and 0 deletions

View file

@ -277,6 +277,7 @@ set(SOURCES
HTML/FormDataEvent.cpp
HTML/GlobalEventHandlers.cpp
HTML/History.cpp
HTML/HTMLAllCollection.cpp
HTML/HTMLAnchorElement.cpp
HTML/HTMLAreaElement.cpp
HTML/HTMLAudioElement.cpp

View file

@ -347,6 +347,7 @@ class EventLoop;
class FormAssociatedElement;
class FormDataEvent;
class History;
class HTMLAllCollection;
class HTMLAnchorElement;
class HTMLAreaElement;
class HTMLAudioElement;

View file

@ -0,0 +1,233 @@
/*
* Copyright (c) 2024, Shannon Booth <shannon@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/PropertyKey.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/ParentNode.h>
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/HTMLAllCollection.h>
#include <LibWeb/HTML/HTMLButtonElement.h>
#include <LibWeb/HTML/HTMLEmbedElement.h>
#include <LibWeb/HTML/HTMLFormElement.h>
#include <LibWeb/HTML/HTMLFrameElement.h>
#include <LibWeb/HTML/HTMLFrameSetElement.h>
#include <LibWeb/HTML/HTMLIFrameElement.h>
#include <LibWeb/HTML/HTMLImageElement.h>
#include <LibWeb/HTML/HTMLInputElement.h>
#include <LibWeb/HTML/HTMLLinkElement.h>
#include <LibWeb/HTML/HTMLMapElement.h>
#include <LibWeb/HTML/HTMLMetaElement.h>
#include <LibWeb/HTML/HTMLObjectElement.h>
#include <LibWeb/HTML/HTMLSelectElement.h>
#include <LibWeb/HTML/HTMLTextAreaElement.h>
#include <LibWeb/Namespace.h>
namespace Web::HTML {
JS_DEFINE_ALLOCATOR(HTMLAllCollection);
JS::NonnullGCPtr<HTMLAllCollection> HTMLAllCollection::create(DOM::ParentNode& root, Scope scope, Function<bool(DOM::Element const&)> filter)
{
return root.heap().allocate<HTMLAllCollection>(root.realm(), root, scope, move(filter));
}
HTMLAllCollection::HTMLAllCollection(DOM::ParentNode& root, Scope scope, Function<bool(DOM::Element const&)> filter)
: PlatformObject(root.realm())
, m_root(root)
, m_filter(move(filter))
, m_scope(scope)
{
m_legacy_platform_object_flags = LegacyPlatformObjectFlags {
.supports_indexed_properties = true,
.supports_named_properties = true,
.has_legacy_unenumerable_named_properties_interface_extended_attribute = true,
};
}
HTMLAllCollection::~HTMLAllCollection() = default;
void HTMLAllCollection::initialize(JS::Realm& realm)
{
Base::initialize(realm);
WEB_SET_PROTOTYPE_FOR_INTERFACE(HTMLAllCollection);
}
void HTMLAllCollection::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_root);
}
// https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#all-named-elements
static bool is_all_named_element(DOM::Element const& element)
{
// The following elements are "all"-named elements: a, button, embed, form, frame, frameset, iframe, img, input, map, meta, object, select, and textarea
return is<HTML::HTMLLinkElement>(element)
|| is<HTML::HTMLButtonElement>(element)
|| is<HTML::HTMLEmbedElement>(element)
|| is<HTML::HTMLFormElement>(element)
|| is<HTML::HTMLFrameElement>(element)
|| is<HTML::HTMLFrameSetElement>(element)
|| is<HTML::HTMLIFrameElement>(element)
|| is<HTML::HTMLImageElement>(element)
|| is<HTML::HTMLInputElement>(element)
|| is<HTML::HTMLMapElement>(element)
|| is<HTML::HTMLMetaElement>(element)
|| is<HTML::HTMLObjectElement>(element)
|| is<HTML::HTMLSelectElement>(element)
|| is<HTML::HTMLTextAreaElement>(element);
}
JS::MarkedVector<JS::NonnullGCPtr<DOM::Element>> HTMLAllCollection::collect_matching_elements() const
{
JS::MarkedVector<JS::NonnullGCPtr<DOM::Element>> elements(m_root->heap());
if (m_scope == Scope::Descendants) {
m_root->for_each_in_subtree_of_type<DOM::Element>([&](auto& element) {
if (m_filter(element))
elements.append(element);
return IterationDecision::Continue;
});
} else {
m_root->for_each_child_of_type<DOM::Element>([&](auto& element) {
if (m_filter(element))
elements.append(element);
return IterationDecision::Continue;
});
}
return elements;
}
// https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#dom-htmlallcollection-length
size_t HTMLAllCollection::length() const
{
// The length getter steps are to return the number of nodes represented by the collection.
return collect_matching_elements().size();
}
// https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#dom-htmlallcollection-item
Variant<JS::NonnullGCPtr<DOM::HTMLCollection>, JS::NonnullGCPtr<DOM::Element>, Empty> HTMLAllCollection::item(Optional<FlyString> const& name_or_index) const
{
// 1. If nameOrIndex was not provided, return null.
if (!name_or_index.has_value())
return Empty {};
// 2. Return the result of getting the "all"-indexed or named element(s) from this, given nameOrIndex.
return get_the_all_indexed_or_named_elements(name_or_index.value());
}
// https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#dom-htmlallcollection-nameditem
Variant<JS::NonnullGCPtr<DOM::HTMLCollection>, JS::NonnullGCPtr<DOM::Element>, Empty> HTMLAllCollection::named_item(FlyString const& name) const
{
// The namedItem(name) method steps are to return the result of getting the "all"-named element(s) from this given name.
return get_the_all_named_elements(name);
}
// https://dom.spec.whatwg.org/#ref-for-dfn-supported-property-names
Vector<FlyString> HTMLAllCollection::supported_property_names() const
{
// The supported property names consist of the non-empty values of all the id attributes of all the
// elements represented by the collection, and the non-empty values of all the name attributes of
// all the "all"-named elements represented by the collection, in tree order, ignoring later duplicates,
// with the id of an element preceding its name if it contributes both, they differ from each other, and
// neither is the duplicate of an earlier entry.
Vector<FlyString> result;
auto elements = collect_matching_elements();
for (auto const& element : elements) {
if (auto const& id = element->id(); id.has_value() && !id->is_empty()) {
if (!result.contains_slow(id.value()))
result.append(id.value());
}
if (is_all_named_element(*element) && element->name().has_value() && !element->name()->is_empty()) {
auto name = element->name().value();
if (!name.is_empty() && !result.contains_slow(name))
result.append(move(name));
}
}
// 3. Return result.
return result;
}
// https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#concept-get-all-named
Variant<JS::NonnullGCPtr<DOM::HTMLCollection>, JS::NonnullGCPtr<DOM::Element>, Empty> HTMLAllCollection::get_the_all_named_elements(FlyString const& name) const
{
// 1. If name is the empty string, return null.
if (name.is_empty())
return Empty {};
// 2. Let subCollection be an HTMLCollection object rooted at the same Document as collection, whose filter matches only elements that are either:
auto sub_collection = DOM::HTMLCollection::create(m_root, DOM::HTMLCollection::Scope::Descendants, [name](DOM::Element const& element) {
// * "all"-named elements with a name attribute equal to name, or,
if (is_all_named_element(element) && element.name() == name)
return true;
// * elements with an ID equal to name.
return element.id() == name;
});
// 3. If there is exactly one element in subCollection, then return that element.
auto matching_elements = sub_collection->collect_matching_elements();
if (matching_elements.size() == 1)
return matching_elements.first();
// 4. Otherwise, if subCollection is empty, return null.
if (matching_elements.is_empty())
return Empty {};
// 5. Otherwise, return subCollection.
return sub_collection;
}
// https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#concept-get-all-indexed
JS::GCPtr<DOM::Element> HTMLAllCollection::get_the_all_indexed_element(u32 index) const
{
// To get the "all"-indexed element from an HTMLAllCollection collection given an index index, return the indexth
// element in collection, or null if there is no such indexth element.
auto elements = collect_matching_elements();
if (index >= elements.size())
return nullptr;
return elements[index];
}
// https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#concept-get-all-indexed-or-named
Variant<JS::NonnullGCPtr<DOM::HTMLCollection>, JS::NonnullGCPtr<DOM::Element>, Empty> HTMLAllCollection::get_the_all_indexed_or_named_elements(JS::PropertyKey const& name_or_index) const
{
// 1. If nameOrIndex, converted to a JavaScript String value, is an array index property name, return the result of getting the "all"-indexed element from
// collection given the number represented by nameOrIndex.
if (name_or_index.is_number()) {
auto maybe_element = get_the_all_indexed_element(name_or_index.as_number());
if (!maybe_element)
return Empty {};
return JS::NonnullGCPtr<DOM::Element> { *maybe_element };
}
// 2. Return the result of getting the "all"-named element(s) from collection given nameOrIndex.
return get_the_all_named_elements(MUST(FlyString::from_deprecated_fly_string(name_or_index.as_string())));
}
bool HTMLAllCollection::is_supported_property_index(u32 index) const
{
return index < collect_matching_elements().size();
}
WebIDL::ExceptionOr<JS::Value> HTMLAllCollection::item_value(size_t index) const
{
return get_the_all_indexed_element(index);
}
WebIDL::ExceptionOr<JS::Value> HTMLAllCollection::named_item_value(FlyString const& name) const
{
return named_item(name).visit(
[](Empty) -> JS::Value { return JS::js_undefined(); },
[](auto const& value) -> JS::Value { return value; });
}
}

View file

@ -0,0 +1,63 @@
/*
* Copyright (c) 2024, Shannon Booth <shannon@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Function.h>
#include <LibJS/Forward.h>
#include <LibJS/Heap/GCPtr.h>
#include <LibWeb/Bindings/PlatformObject.h>
#include <LibWeb/DOM/HTMLCollection.h>
#include <LibWeb/Forward.h>
namespace Web::HTML {
// FIXME: Should be part of HTML namespace!
class HTMLAllCollection : public Bindings::PlatformObject {
WEB_PLATFORM_OBJECT(HTMLAllCollection, Bindings::PlatformObject);
JS_DECLARE_ALLOCATOR(HTMLAllCollection);
public:
enum class Scope {
Children,
Descendants,
};
[[nodiscard]] static JS::NonnullGCPtr<HTMLAllCollection> create(DOM::ParentNode& root, Scope, Function<bool(DOM::Element const&)> filter);
virtual ~HTMLAllCollection() override;
size_t length() const;
Variant<JS::NonnullGCPtr<DOM::HTMLCollection>, JS::NonnullGCPtr<DOM::Element>, Empty> item(Optional<FlyString> const& name_or_index) const;
Variant<JS::NonnullGCPtr<DOM::HTMLCollection>, JS::NonnullGCPtr<DOM::Element>, Empty> named_item(FlyString const& name) const;
JS::MarkedVector<JS::NonnullGCPtr<DOM::Element>> collect_matching_elements() const;
virtual WebIDL::ExceptionOr<JS::Value> item_value(size_t index) const override;
virtual WebIDL::ExceptionOr<JS::Value> named_item_value(FlyString const& name) const override;
virtual Vector<FlyString> supported_property_names() const override;
virtual bool is_supported_property_index(u32) const override;
protected:
HTMLAllCollection(DOM::ParentNode& root, Scope, Function<bool(DOM::Element const&)> filter);
virtual void initialize(JS::Realm&) override;
virtual bool is_htmldda() const override { return true; }
private:
Variant<JS::NonnullGCPtr<DOM::HTMLCollection>, JS::NonnullGCPtr<DOM::Element>, Empty> get_the_all_named_elements(FlyString const& name) const;
JS::GCPtr<DOM::Element> get_the_all_indexed_element(u32 index) const;
Variant<JS::NonnullGCPtr<DOM::HTMLCollection>, JS::NonnullGCPtr<DOM::Element>, Empty> get_the_all_indexed_or_named_elements(JS::PropertyKey const& name_or_index) const;
virtual void visit_edges(Cell::Visitor&) override;
JS::NonnullGCPtr<DOM::ParentNode> m_root;
Function<bool(DOM::Element const&)> m_filter;
Scope m_scope { Scope::Descendants };
};
}

View file

@ -0,0 +1,13 @@
#import <DOM/Element.idl>
#import <DOM/HTMLCollection.idl>
[Exposed=Window,
LegacyUnenumerableNamedProperties]
interface HTMLAllCollection {
readonly attribute unsigned long length;
getter Element (unsigned long index);
getter (HTMLCollection or Element)? namedItem([FlyString] DOMString name);
(HTMLCollection or Element)? item(optional [FlyString] DOMString nameOrIndex);
// Note: HTMLAllCollection objects have a custom [[Call]] internal method and an [[IsHTMLDDA]] internal slot.
};

View file

@ -102,6 +102,7 @@ libweb_js_bindings(HTML/DataTransfer)
libweb_js_bindings(HTML/ErrorEvent)
libweb_js_bindings(HTML/FormDataEvent)
libweb_js_bindings(HTML/History)
libweb_js_bindings(HTML/HTMLAllCollection)
libweb_js_bindings(HTML/HTMLAnchorElement)
libweb_js_bindings(HTML/HTMLAreaElement)
libweb_js_bindings(HTML/HTMLAudioElement)