/src/serenity/Userland/Libraries/LibWeb/DOM/HTMLCollection.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2021-2022, Andreas Kling <kling@serenityos.org> |
3 | | * Copyright (c) 2021, Luke Wilde <lukew@serenityos.org> |
4 | | * |
5 | | * SPDX-License-Identifier: BSD-2-Clause |
6 | | */ |
7 | | |
8 | | #include <LibWeb/Bindings/HTMLCollectionPrototype.h> |
9 | | #include <LibWeb/Bindings/Intrinsics.h> |
10 | | #include <LibWeb/DOM/Document.h> |
11 | | #include <LibWeb/DOM/Element.h> |
12 | | #include <LibWeb/DOM/HTMLCollection.h> |
13 | | #include <LibWeb/DOM/ParentNode.h> |
14 | | #include <LibWeb/Namespace.h> |
15 | | |
16 | | namespace Web::DOM { |
17 | | |
18 | | JS_DEFINE_ALLOCATOR(HTMLCollection); |
19 | | |
20 | | JS::NonnullGCPtr<HTMLCollection> HTMLCollection::create(ParentNode& root, Scope scope, Function<bool(Element const&)> filter) |
21 | 0 | { |
22 | 0 | return root.heap().allocate<HTMLCollection>(root.realm(), root, scope, move(filter)); |
23 | 0 | } |
24 | | |
25 | | HTMLCollection::HTMLCollection(ParentNode& root, Scope scope, Function<bool(Element const&)> filter) |
26 | 0 | : PlatformObject(root.realm()) |
27 | 0 | , m_root(root) |
28 | 0 | , m_filter(move(filter)) |
29 | 0 | , m_scope(scope) |
30 | 0 | { |
31 | 0 | m_legacy_platform_object_flags = LegacyPlatformObjectFlags { |
32 | 0 | .supports_indexed_properties = true, |
33 | 0 | .supports_named_properties = true, |
34 | 0 | .has_legacy_unenumerable_named_properties_interface_extended_attribute = true, |
35 | 0 | }; |
36 | 0 | } |
37 | | |
38 | 0 | HTMLCollection::~HTMLCollection() = default; |
39 | | |
40 | | void HTMLCollection::initialize(JS::Realm& realm) |
41 | 0 | { |
42 | 0 | Base::initialize(realm); |
43 | 0 | WEB_SET_PROTOTYPE_FOR_INTERFACE(HTMLCollection); |
44 | 0 | } |
45 | | |
46 | | void HTMLCollection::visit_edges(Cell::Visitor& visitor) |
47 | 0 | { |
48 | 0 | Base::visit_edges(visitor); |
49 | 0 | visitor.visit(m_root); |
50 | 0 | visitor.visit(m_cached_elements); |
51 | 0 | if (m_cached_name_to_element_mappings) |
52 | 0 | visitor.visit(*m_cached_name_to_element_mappings); |
53 | 0 | } |
54 | | |
55 | | void HTMLCollection::update_name_to_element_mappings_if_needed() const |
56 | 0 | { |
57 | 0 | update_cache_if_needed(); |
58 | 0 | if (m_cached_name_to_element_mappings) |
59 | 0 | return; |
60 | 0 | m_cached_name_to_element_mappings = make<OrderedHashMap<FlyString, JS::NonnullGCPtr<Element>>>(); |
61 | 0 | for (auto const& element : m_cached_elements) { |
62 | | // 1. If element has an ID which is not in result, append element’s ID to result. |
63 | 0 | if (auto const& id = element->id(); id.has_value()) { |
64 | 0 | if (!id.value().is_empty() && !m_cached_name_to_element_mappings->contains(id.value())) |
65 | 0 | m_cached_name_to_element_mappings->set(id.value(), element); |
66 | 0 | } |
67 | | |
68 | | // 2. If element is in the HTML namespace and has a name attribute whose value is neither the empty string nor is in result, append element’s name attribute value to result. |
69 | 0 | if (element->namespace_uri() == Namespace::HTML && element->name().has_value()) { |
70 | 0 | auto element_name = element->name().value(); |
71 | 0 | if (!element_name.is_empty() && !m_cached_name_to_element_mappings->contains(element_name)) |
72 | 0 | m_cached_name_to_element_mappings->set(move(element_name), element); |
73 | 0 | } |
74 | 0 | } |
75 | 0 | } |
76 | | |
77 | | void HTMLCollection::update_cache_if_needed() const |
78 | 0 | { |
79 | | // Nothing to do, the DOM hasn't updated since we last built the cache. |
80 | 0 | if (m_cached_dom_tree_version == root()->document().dom_tree_version()) |
81 | 0 | return; |
82 | | |
83 | 0 | m_cached_elements.clear(); |
84 | 0 | m_cached_name_to_element_mappings = nullptr; |
85 | 0 | if (m_scope == Scope::Descendants) { |
86 | 0 | m_root->for_each_in_subtree_of_type<Element>([&](auto& element) { |
87 | 0 | if (m_filter(element)) |
88 | 0 | m_cached_elements.append(element); |
89 | 0 | return TraversalDecision::Continue; |
90 | 0 | }); |
91 | 0 | } else { |
92 | 0 | m_root->for_each_child_of_type<Element>([&](auto& element) { |
93 | 0 | if (m_filter(element)) |
94 | 0 | m_cached_elements.append(element); |
95 | 0 | return IterationDecision::Continue; |
96 | 0 | }); |
97 | 0 | } |
98 | 0 | m_cached_dom_tree_version = root()->document().dom_tree_version(); |
99 | 0 | } |
100 | | |
101 | | JS::MarkedVector<JS::NonnullGCPtr<Element>> HTMLCollection::collect_matching_elements() const |
102 | 0 | { |
103 | 0 | update_cache_if_needed(); |
104 | 0 | JS::MarkedVector<JS::NonnullGCPtr<Element>> elements(heap()); |
105 | 0 | for (auto& element : m_cached_elements) |
106 | 0 | elements.append(element); |
107 | 0 | return elements; |
108 | 0 | } |
109 | | |
110 | | // https://dom.spec.whatwg.org/#dom-htmlcollection-length |
111 | | size_t HTMLCollection::length() const |
112 | 0 | { |
113 | | // The length getter steps are to return the number of nodes represented by the collection. |
114 | 0 | update_cache_if_needed(); |
115 | 0 | return m_cached_elements.size(); |
116 | 0 | } |
117 | | |
118 | | // https://dom.spec.whatwg.org/#dom-htmlcollection-item |
119 | | Element* HTMLCollection::item(size_t index) const |
120 | 0 | { |
121 | | // The item(index) method steps are to return the indexth element in the collection. If there is no indexth element in the collection, then the method must return null. |
122 | 0 | update_cache_if_needed(); |
123 | 0 | if (index >= m_cached_elements.size()) |
124 | 0 | return nullptr; |
125 | 0 | return m_cached_elements[index]; |
126 | 0 | } |
127 | | |
128 | | // https://dom.spec.whatwg.org/#dom-htmlcollection-nameditem-key |
129 | | Element* HTMLCollection::named_item(FlyString const& key) const |
130 | 0 | { |
131 | | // 1. If key is the empty string, return null. |
132 | 0 | if (key.is_empty()) |
133 | 0 | return nullptr; |
134 | | |
135 | 0 | update_name_to_element_mappings_if_needed(); |
136 | 0 | if (auto it = m_cached_name_to_element_mappings->get(key); it.has_value()) |
137 | 0 | return it.value(); |
138 | 0 | return nullptr; |
139 | 0 | } |
140 | | |
141 | | // https://dom.spec.whatwg.org/#ref-for-dfn-supported-property-names |
142 | | bool HTMLCollection::is_supported_property_name(FlyString const& name) const |
143 | 0 | { |
144 | 0 | update_name_to_element_mappings_if_needed(); |
145 | 0 | return m_cached_name_to_element_mappings->contains(name); |
146 | 0 | } |
147 | | |
148 | | // https://dom.spec.whatwg.org/#ref-for-dfn-supported-property-names |
149 | | Vector<FlyString> HTMLCollection::supported_property_names() const |
150 | 0 | { |
151 | | // 1. Let result be an empty list. |
152 | 0 | Vector<FlyString> result; |
153 | | |
154 | | // 2. For each element represented by the collection, in tree order: |
155 | 0 | update_name_to_element_mappings_if_needed(); |
156 | 0 | for (auto const& it : *m_cached_name_to_element_mappings) { |
157 | 0 | result.append(it.key); |
158 | 0 | } |
159 | | |
160 | | // 3. Return result. |
161 | 0 | return result; |
162 | 0 | } |
163 | | |
164 | | Optional<JS::Value> HTMLCollection::item_value(size_t index) const |
165 | 0 | { |
166 | 0 | auto* element = item(index); |
167 | 0 | if (!element) |
168 | 0 | return {}; |
169 | 0 | return element; |
170 | 0 | } |
171 | | |
172 | | JS::Value HTMLCollection::named_item_value(FlyString const& name) const |
173 | 0 | { |
174 | 0 | auto* element = named_item(name); |
175 | 0 | if (!element) |
176 | 0 | return JS::js_undefined(); |
177 | 0 | return element; |
178 | 0 | } |
179 | | } |