Serenity Operating System
1/*
2 * Copyright (c) 2018-2023, Andreas Kling <kling@serenityos.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include <AK/AnyOf.h>
8#include <AK/Debug.h>
9#include <AK/StringBuilder.h>
10#include <LibWeb/Bindings/ElementPrototype.h>
11#include <LibWeb/Bindings/ExceptionOrUtils.h>
12#include <LibWeb/CSS/Parser/Parser.h>
13#include <LibWeb/CSS/PropertyID.h>
14#include <LibWeb/CSS/ResolvedCSSStyleDeclaration.h>
15#include <LibWeb/CSS/SelectorEngine.h>
16#include <LibWeb/DOM/DOMTokenList.h>
17#include <LibWeb/DOM/Document.h>
18#include <LibWeb/DOM/Element.h>
19#include <LibWeb/DOM/HTMLCollection.h>
20#include <LibWeb/DOM/ShadowRoot.h>
21#include <LibWeb/DOM/Text.h>
22#include <LibWeb/DOMParsing/InnerHTML.h>
23#include <LibWeb/Geometry/DOMRect.h>
24#include <LibWeb/Geometry/DOMRectList.h>
25#include <LibWeb/HTML/BrowsingContext.h>
26#include <LibWeb/HTML/EventLoop/EventLoop.h>
27#include <LibWeb/HTML/HTMLBodyElement.h>
28#include <LibWeb/HTML/HTMLButtonElement.h>
29#include <LibWeb/HTML/HTMLFieldSetElement.h>
30#include <LibWeb/HTML/HTMLFrameSetElement.h>
31#include <LibWeb/HTML/HTMLHtmlElement.h>
32#include <LibWeb/HTML/HTMLInputElement.h>
33#include <LibWeb/HTML/HTMLOptGroupElement.h>
34#include <LibWeb/HTML/HTMLOptionElement.h>
35#include <LibWeb/HTML/HTMLSelectElement.h>
36#include <LibWeb/HTML/HTMLTextAreaElement.h>
37#include <LibWeb/HTML/Parser/HTMLParser.h>
38#include <LibWeb/Infra/CharacterTypes.h>
39#include <LibWeb/Infra/Strings.h>
40#include <LibWeb/Layout/BlockContainer.h>
41#include <LibWeb/Layout/InlineNode.h>
42#include <LibWeb/Layout/ListItemBox.h>
43#include <LibWeb/Layout/TableBox.h>
44#include <LibWeb/Layout/TableCellBox.h>
45#include <LibWeb/Layout/TableRowBox.h>
46#include <LibWeb/Layout/TableRowGroupBox.h>
47#include <LibWeb/Layout/TreeBuilder.h>
48#include <LibWeb/Layout/Viewport.h>
49#include <LibWeb/Namespace.h>
50#include <LibWeb/Page/Page.h>
51#include <LibWeb/Painting/PaintableBox.h>
52#include <LibWeb/WebIDL/DOMException.h>
53#include <LibWeb/WebIDL/ExceptionOr.h>
54
55namespace Web::DOM {
56
57Element::Element(Document& document, DOM::QualifiedName qualified_name)
58 : ParentNode(document, NodeType::ELEMENT_NODE)
59 , m_qualified_name(move(qualified_name))
60{
61 make_html_uppercased_qualified_name();
62}
63
64Element::~Element() = default;
65
66JS::ThrowCompletionOr<void> Element::initialize(JS::Realm& realm)
67{
68 MUST_OR_THROW_OOM(Base::initialize(realm));
69 set_prototype(&Bindings::ensure_web_prototype<Bindings::ElementPrototype>(realm, "Element"));
70
71 m_attributes = TRY(Bindings::throw_dom_exception_if_needed(realm.vm(), [&]() {
72 return NamedNodeMap::create(*this);
73 }));
74
75 return {};
76}
77
78void Element::visit_edges(Cell::Visitor& visitor)
79{
80 Base::visit_edges(visitor);
81 visitor.visit(m_attributes.ptr());
82 visitor.visit(m_inline_style.ptr());
83 visitor.visit(m_class_list.ptr());
84 visitor.visit(m_shadow_root.ptr());
85 for (auto& pseudo_element_layout_node : m_pseudo_element_nodes)
86 visitor.visit(pseudo_element_layout_node);
87}
88
89// https://dom.spec.whatwg.org/#dom-element-getattribute
90DeprecatedString Element::get_attribute(DeprecatedFlyString const& name) const
91{
92 // 1. Let attr be the result of getting an attribute given qualifiedName and this.
93 auto const* attribute = m_attributes->get_attribute(name);
94
95 // 2. If attr is null, return null.
96 if (!attribute)
97 return {};
98
99 // 3. Return attr’s value.
100 return attribute->value();
101}
102
103// https://dom.spec.whatwg.org/#dom-element-getattributenode
104JS::GCPtr<Attr> Element::get_attribute_node(DeprecatedFlyString const& name) const
105{
106 // The getAttributeNode(qualifiedName) method steps are to return the result of getting an attribute given qualifiedName and this.
107 return m_attributes->get_attribute(name);
108}
109
110// https://dom.spec.whatwg.org/#dom-element-setattribute
111WebIDL::ExceptionOr<void> Element::set_attribute(DeprecatedFlyString const& name, DeprecatedString const& value)
112{
113 // 1. If qualifiedName does not match the Name production in XML, then throw an "InvalidCharacterError" DOMException.
114 // FIXME: Proper name validation
115 if (name.is_empty())
116 return WebIDL::InvalidCharacterError::create(realm(), "Attribute name must not be empty");
117
118 // 2. If this is in the HTML namespace and its node document is an HTML document, then set qualifiedName to qualifiedName in ASCII lowercase.
119 // FIXME: Handle the second condition, assume it is an HTML document for now.
120 bool insert_as_lowercase = namespace_uri() == Namespace::HTML;
121
122 // 3. Let attribute be the first attribute in this’s attribute list whose qualified name is qualifiedName, and null otherwise.
123 auto* attribute = m_attributes->get_attribute(name);
124
125 // 4. If attribute is null, create an attribute whose local name is qualifiedName, value is value, and node document is this’s node document, then append this attribute to this, and then return.
126 if (!attribute) {
127 auto new_attribute = TRY(Attr::create(document(), insert_as_lowercase ? name.to_lowercase() : name, value));
128 m_attributes->append_attribute(new_attribute);
129
130 attribute = new_attribute.ptr();
131 }
132
133 // 5. Change attribute to value.
134 else {
135 attribute->set_value(value);
136 }
137
138 parse_attribute(attribute->local_name(), value);
139
140 invalidate_style_after_attribute_change(name);
141
142 return {};
143}
144
145// https://dom.spec.whatwg.org/#validate-and-extract
146WebIDL::ExceptionOr<QualifiedName> validate_and_extract(JS::Realm& realm, DeprecatedFlyString namespace_, DeprecatedFlyString qualified_name)
147{
148 // 1. If namespace is the empty string, then set it to null.
149 if (namespace_.is_empty())
150 namespace_ = {};
151
152 // 2. Validate qualifiedName.
153 TRY(Document::validate_qualified_name(realm, qualified_name));
154
155 // 3. Let prefix be null.
156 DeprecatedFlyString prefix = {};
157
158 // 4. Let localName be qualifiedName.
159 auto local_name = qualified_name;
160
161 // 5. If qualifiedName contains a U+003A (:), then strictly split the string on it and set prefix to the part before and localName to the part after.
162 if (qualified_name.view().contains(':')) {
163 auto parts = qualified_name.view().split_view(':');
164 prefix = parts[0];
165 local_name = parts[1];
166 }
167
168 // 6. If prefix is non-null and namespace is null, then throw a "NamespaceError" DOMException.
169 if (!prefix.is_null() && namespace_.is_null())
170 return WebIDL::NamespaceError::create(realm, "Prefix is non-null and namespace is null.");
171
172 // 7. If prefix is "xml" and namespace is not the XML namespace, then throw a "NamespaceError" DOMException.
173 if (prefix == "xml"sv && namespace_ != Namespace::XML)
174 return WebIDL::NamespaceError::create(realm, "Prefix is 'xml' and namespace is not the XML namespace.");
175
176 // 8. If either qualifiedName or prefix is "xmlns" and namespace is not the XMLNS namespace, then throw a "NamespaceError" DOMException.
177 if ((qualified_name == "xmlns"sv || prefix == "xmlns"sv) && namespace_ != Namespace::XMLNS)
178 return WebIDL::NamespaceError::create(realm, "Either qualifiedName or prefix is 'xmlns' and namespace is not the XMLNS namespace.");
179
180 // 9. If namespace is the XMLNS namespace and neither qualifiedName nor prefix is "xmlns", then throw a "NamespaceError" DOMException.
181 if (namespace_ == Namespace::XMLNS && !(qualified_name == "xmlns"sv || prefix == "xmlns"sv))
182 return WebIDL::NamespaceError::create(realm, "Namespace is the XMLNS namespace and neither qualifiedName nor prefix is 'xmlns'.");
183
184 // 10. Return namespace, prefix, and localName.
185 return QualifiedName { local_name, prefix, namespace_ };
186}
187
188// https://dom.spec.whatwg.org/#dom-element-setattributens
189WebIDL::ExceptionOr<void> Element::set_attribute_ns(DeprecatedFlyString const& namespace_, DeprecatedFlyString const& qualified_name, DeprecatedString const& value)
190{
191 // 1. Let namespace, prefix, and localName be the result of passing namespace and qualifiedName to validate and extract.
192 auto extracted_qualified_name = TRY(validate_and_extract(realm(), namespace_, qualified_name));
193
194 // FIXME: 2. Set an attribute value for this using localName, value, and also prefix and namespace.
195
196 // FIXME: Don't just call through to setAttribute() here.
197 return set_attribute(extracted_qualified_name.local_name(), value);
198}
199
200// https://dom.spec.whatwg.org/#dom-element-setattributenode
201WebIDL::ExceptionOr<JS::GCPtr<Attr>> Element::set_attribute_node(Attr& attr)
202{
203 // The setAttributeNode(attr) and setAttributeNodeNS(attr) methods steps are to return the result of setting an attribute given attr and this.
204 return m_attributes->set_attribute(attr);
205}
206
207// https://dom.spec.whatwg.org/#dom-element-setattributenodens
208WebIDL::ExceptionOr<JS::GCPtr<Attr>> Element::set_attribute_node_ns(Attr& attr)
209{
210 // The setAttributeNode(attr) and setAttributeNodeNS(attr) methods steps are to return the result of setting an attribute given attr and this.
211 return m_attributes->set_attribute(attr);
212}
213
214// https://dom.spec.whatwg.org/#dom-element-removeattribute
215void Element::remove_attribute(DeprecatedFlyString const& name)
216{
217 m_attributes->remove_attribute(name);
218
219 did_remove_attribute(name);
220
221 invalidate_style_after_attribute_change(name);
222}
223
224// https://dom.spec.whatwg.org/#dom-element-hasattribute
225bool Element::has_attribute(DeprecatedFlyString const& name) const
226{
227 return m_attributes->get_attribute(name) != nullptr;
228}
229
230// https://dom.spec.whatwg.org/#dom-element-toggleattribute
231WebIDL::ExceptionOr<bool> Element::toggle_attribute(DeprecatedFlyString const& name, Optional<bool> force)
232{
233 // 1. If qualifiedName does not match the Name production in XML, then throw an "InvalidCharacterError" DOMException.
234 // FIXME: Proper name validation
235 if (name.is_empty())
236 return WebIDL::InvalidCharacterError::create(realm(), "Attribute name must not be empty");
237
238 // 2. If this is in the HTML namespace and its node document is an HTML document, then set qualifiedName to qualifiedName in ASCII lowercase.
239 // FIXME: Handle the second condition, assume it is an HTML document for now.
240 bool insert_as_lowercase = namespace_uri() == Namespace::HTML;
241
242 // 3. Let attribute be the first attribute in this’s attribute list whose qualified name is qualifiedName, and null otherwise.
243 auto* attribute = m_attributes->get_attribute(name);
244
245 // 4. If attribute is null, then:
246 if (!attribute) {
247 // 1. If force is not given or is true, create an attribute whose local name is qualifiedName, value is the empty string, and node document is this’s node document, then append this attribute to this, and then return true.
248 if (!force.has_value() || force.value()) {
249 auto new_attribute = TRY(Attr::create(document(), insert_as_lowercase ? name.to_lowercase() : name, ""));
250 m_attributes->append_attribute(new_attribute);
251
252 parse_attribute(new_attribute->local_name(), "");
253
254 invalidate_style_after_attribute_change(name);
255
256 return true;
257 }
258
259 // 2. Return false.
260 return false;
261 }
262
263 // 5. Otherwise, if force is not given or is false, remove an attribute given qualifiedName and this, and then return false.
264 if (!force.has_value() || !force.value()) {
265 m_attributes->remove_attribute(name);
266
267 did_remove_attribute(name);
268
269 invalidate_style_after_attribute_change(name);
270 }
271
272 // 6. Return true.
273 return true;
274}
275
276// https://dom.spec.whatwg.org/#dom-element-getattributenames
277Vector<DeprecatedString> Element::get_attribute_names() const
278{
279 // The getAttributeNames() method steps are to return the qualified names of the attributes in this’s attribute list, in order; otherwise a new list.
280 Vector<DeprecatedString> names;
281 for (size_t i = 0; i < m_attributes->length(); ++i) {
282 auto const* attribute = m_attributes->item(i);
283 names.append(attribute->name());
284 }
285 return names;
286}
287
288bool Element::has_class(FlyString const& class_name, CaseSensitivity case_sensitivity) const
289{
290 if (case_sensitivity == CaseSensitivity::CaseSensitive) {
291 return any_of(m_classes, [&](auto& it) {
292 return it == class_name;
293 });
294 } else {
295 return any_of(m_classes, [&](auto& it) {
296 return it.equals_ignoring_ascii_case(class_name);
297 });
298 }
299}
300
301JS::GCPtr<Layout::Node> Element::create_layout_node(NonnullRefPtr<CSS::StyleProperties> style)
302{
303 if (local_name() == "noscript" && document().is_scripting_enabled())
304 return nullptr;
305
306 auto display = style->display();
307 return create_layout_node_for_display_type(document(), display, move(style), this);
308}
309
310JS::GCPtr<Layout::Node> Element::create_layout_node_for_display_type(DOM::Document& document, CSS::Display const& display, NonnullRefPtr<CSS::StyleProperties> style, Element* element)
311{
312 if (display.is_table_inside())
313 return document.heap().allocate_without_realm<Layout::TableBox>(document, element, move(style));
314
315 if (display.is_list_item())
316 return document.heap().allocate_without_realm<Layout::ListItemBox>(document, element, move(style));
317
318 if (display.is_table_row())
319 return document.heap().allocate_without_realm<Layout::TableRowBox>(document, element, move(style));
320
321 if (display.is_table_cell())
322 return document.heap().allocate_without_realm<Layout::TableCellBox>(document, element, move(style));
323
324 if (display.is_table_row_group() || display.is_table_header_group() || display.is_table_footer_group())
325 return document.heap().allocate_without_realm<Layout::TableRowGroupBox>(document, element, move(style));
326
327 if (display.is_table_column() || display.is_table_column_group() || display.is_table_caption()) {
328 // FIXME: This is just an incorrect placeholder until we improve table layout support.
329 return document.heap().allocate_without_realm<Layout::BlockContainer>(document, element, move(style));
330 }
331
332 if (display.is_inline_outside()) {
333 if (display.is_flow_root_inside())
334 return document.heap().allocate_without_realm<Layout::BlockContainer>(document, element, move(style));
335 if (display.is_flow_inside())
336 return document.heap().allocate_without_realm<Layout::InlineNode>(document, element, move(style));
337 if (display.is_flex_inside())
338 return document.heap().allocate_without_realm<Layout::Box>(document, element, move(style));
339 dbgln_if(LIBWEB_CSS_DEBUG, "FIXME: Support display: {}", MUST(display.to_string()));
340 return document.heap().allocate_without_realm<Layout::InlineNode>(document, element, move(style));
341 }
342
343 if (display.is_flex_inside() || display.is_grid_inside())
344 return document.heap().allocate_without_realm<Layout::Box>(document, element, move(style));
345
346 if (display.is_flow_inside() || display.is_flow_root_inside())
347 return document.heap().allocate_without_realm<Layout::BlockContainer>(document, element, move(style));
348
349 TODO();
350}
351
352CSS::CSSStyleDeclaration const* Element::inline_style() const
353{
354 return m_inline_style.ptr();
355}
356
357void Element::parse_attribute(DeprecatedFlyString const& name, DeprecatedString const& value)
358{
359 if (name == HTML::AttributeNames::class_) {
360 auto new_classes = value.split_view(Infra::is_ascii_whitespace);
361 m_classes.clear();
362 m_classes.ensure_capacity(new_classes.size());
363 for (auto& new_class : new_classes) {
364 m_classes.unchecked_append(FlyString::from_utf8(new_class).release_value_but_fixme_should_propagate_errors());
365 }
366 if (m_class_list)
367 m_class_list->associated_attribute_changed(value);
368 } else if (name == HTML::AttributeNames::style) {
369 // https://drafts.csswg.org/cssom/#ref-for-cssstyledeclaration-updating-flag
370 if (m_inline_style && m_inline_style->is_updating())
371 return;
372 m_inline_style = parse_css_style_attribute(CSS::Parser::ParsingContext(document()), value, *this);
373 set_needs_style_update(true);
374 }
375}
376
377void Element::did_remove_attribute(DeprecatedFlyString const& name)
378{
379 if (name == HTML::AttributeNames::style) {
380 if (m_inline_style) {
381 m_inline_style = nullptr;
382 set_needs_style_update(true);
383 }
384 }
385}
386
387enum class RequiredInvalidation {
388 None,
389 RepaintOnly,
390 RebuildStackingContextTree,
391 Relayout,
392};
393
394static RequiredInvalidation compute_required_invalidation(CSS::StyleProperties const& old_style, CSS::StyleProperties const& new_style)
395{
396 if (&old_style.computed_font() != &new_style.computed_font())
397 return RequiredInvalidation::Relayout;
398 bool requires_repaint = false;
399 bool requires_stacking_context_tree_rebuild = false;
400 for (auto i = to_underlying(CSS::first_property_id); i <= to_underlying(CSS::last_property_id); ++i) {
401 auto property_id = static_cast<CSS::PropertyID>(i);
402 auto const& old_value = old_style.properties()[i];
403 auto const& new_value = new_style.properties()[i];
404 if (!old_value && !new_value)
405 continue;
406 if (!old_value || !new_value)
407 return RequiredInvalidation::Relayout;
408 if (*old_value == *new_value)
409 continue;
410 if (CSS::property_affects_layout(property_id))
411 return RequiredInvalidation::Relayout;
412 if (CSS::property_affects_stacking_context(property_id))
413 requires_stacking_context_tree_rebuild = true;
414 requires_repaint = true;
415 }
416 if (requires_stacking_context_tree_rebuild)
417 return RequiredInvalidation::RebuildStackingContextTree;
418 if (requires_repaint)
419 return RequiredInvalidation::RepaintOnly;
420 return RequiredInvalidation::None;
421}
422
423Element::NeedsRelayout Element::recompute_style()
424{
425 set_needs_style_update(false);
426 VERIFY(parent());
427
428 // FIXME propagate errors
429 auto new_computed_css_values = MUST(document().style_computer().compute_style(*this));
430
431 auto required_invalidation = RequiredInvalidation::Relayout;
432
433 if (m_computed_css_values)
434 required_invalidation = compute_required_invalidation(*m_computed_css_values, *new_computed_css_values);
435
436 if (required_invalidation == RequiredInvalidation::None)
437 return NeedsRelayout::No;
438
439 m_computed_css_values = move(new_computed_css_values);
440
441 if (required_invalidation == RequiredInvalidation::RepaintOnly && layout_node()) {
442 layout_node()->apply_style(*m_computed_css_values);
443 layout_node()->set_needs_display();
444 return NeedsRelayout::No;
445 }
446
447 if (required_invalidation == RequiredInvalidation::RebuildStackingContextTree && layout_node()) {
448 layout_node()->apply_style(*m_computed_css_values);
449 document().invalidate_stacking_context_tree();
450 layout_node()->set_needs_display();
451 return NeedsRelayout::No;
452 }
453
454 return NeedsRelayout::Yes;
455}
456
457NonnullRefPtr<CSS::StyleProperties> Element::resolved_css_values()
458{
459 auto element_computed_style = CSS::ResolvedCSSStyleDeclaration::create(*this).release_value_but_fixme_should_propagate_errors();
460 auto properties = CSS::StyleProperties::create();
461
462 for (auto i = to_underlying(CSS::first_property_id); i <= to_underlying(CSS::last_property_id); ++i) {
463 auto property_id = (CSS::PropertyID)i;
464 auto maybe_value = element_computed_style->property(property_id);
465 if (!maybe_value.has_value())
466 continue;
467 properties->set_property(property_id, maybe_value.release_value().value);
468 }
469
470 return properties;
471}
472
473DOMTokenList* Element::class_list()
474{
475 if (!m_class_list)
476 m_class_list = DOMTokenList::create(*this, HTML::AttributeNames::class_).release_value_but_fixme_should_propagate_errors();
477 return m_class_list;
478}
479
480// https://dom.spec.whatwg.org/#dom-element-attachshadow
481WebIDL::ExceptionOr<JS::NonnullGCPtr<ShadowRoot>> Element::attach_shadow(ShadowRootInit init)
482{
483 // 1. If this’s namespace is not the HTML namespace, then throw a "NotSupportedError" DOMException.
484 if (namespace_() != Namespace::HTML)
485 return WebIDL::NotSupportedError::create(realm(), "Element's namespace is not the HTML namespace"sv);
486
487 // 2. If this’s local name is not one of the following:
488 // FIXME: - a valid custom element name
489 // - "article", "aside", "blockquote", "body", "div", "footer", "h1", "h2", "h3", "h4", "h5", "h6", "header", "main", "nav", "p", "section", or "span"
490 if (!local_name().is_one_of("article", "aside", "blockquote", "body", "div", "footer", "h1", "h2", "h3", "h4", "h5", "h6", "header", "main", "nav", "p", "section", "span")) {
491 // then throw a "NotSupportedError" DOMException.
492 return WebIDL::NotSupportedError::create(realm(), DeprecatedString::formatted("Element '{}' cannot be a shadow host", local_name()));
493 }
494
495 // FIXME: 3. If this’s local name is a valid custom element name, or this’s is value is not null, then: ...
496
497 // 4. If this is a shadow host, then throw an "NotSupportedError" DOMException.
498 if (is_shadow_host())
499 return WebIDL::NotSupportedError::create(realm(), "Element already is a shadow host");
500
501 // 5. Let shadow be a new shadow root whose node document is this’s node document, host is this, and mode is init["mode"].
502 auto shadow = MUST_OR_THROW_OOM(heap().allocate<ShadowRoot>(realm(), document(), *this, init.mode));
503
504 // 6. Set shadow’s delegates focus to init["delegatesFocus"].
505 shadow->set_delegates_focus(init.delegates_focus);
506
507 // FIXME: 7. If this’s custom element state is "precustomized" or "custom", then set shadow’s available to element internals to true.
508
509 // FIXME: 8. Set shadow’s slot assignment to init["slotAssignment"].
510
511 // 9. Set this’s shadow root to shadow.
512 set_shadow_root(shadow);
513
514 // 10. Return shadow.
515 return shadow;
516}
517
518// https://dom.spec.whatwg.org/#dom-element-shadowroot
519JS::GCPtr<ShadowRoot> Element::shadow_root() const
520{
521 // 1. Let shadow be this’s shadow root.
522 auto shadow = m_shadow_root;
523
524 // 2. If shadow is null or its mode is "closed", then return null.
525 if (shadow == nullptr || shadow->mode() == Bindings::ShadowRootMode::Closed)
526 return nullptr;
527
528 // 3. Return shadow.
529 return shadow;
530}
531
532// https://dom.spec.whatwg.org/#dom-element-matches
533WebIDL::ExceptionOr<bool> Element::matches(StringView selectors) const
534{
535 auto maybe_selectors = parse_selector(CSS::Parser::ParsingContext(static_cast<ParentNode&>(const_cast<Element&>(*this))), selectors);
536 if (!maybe_selectors.has_value())
537 return WebIDL::SyntaxError::create(realm(), "Failed to parse selector");
538
539 auto sel = maybe_selectors.value();
540 for (auto& s : sel) {
541 if (SelectorEngine::matches(s, *this))
542 return true;
543 }
544 return false;
545}
546
547// https://dom.spec.whatwg.org/#dom-element-closest
548WebIDL::ExceptionOr<DOM::Element const*> Element::closest(StringView selectors) const
549{
550 auto maybe_selectors = parse_selector(CSS::Parser::ParsingContext(static_cast<ParentNode&>(const_cast<Element&>(*this))), selectors);
551 if (!maybe_selectors.has_value())
552 return WebIDL::SyntaxError::create(realm(), "Failed to parse selector");
553
554 auto matches_selectors = [](CSS::SelectorList const& selector_list, Element const* element) {
555 for (auto& selector : selector_list) {
556 if (!SelectorEngine::matches(selector, *element))
557 return false;
558 }
559 return true;
560 };
561
562 auto const selector_list = maybe_selectors.release_value();
563 for (auto* element = this; element; element = element->parent_element()) {
564 if (!matches_selectors(selector_list, element))
565 continue;
566
567 return element;
568 }
569
570 return nullptr;
571}
572
573WebIDL::ExceptionOr<void> Element::set_inner_html(DeprecatedString const& markup)
574{
575 TRY(DOMParsing::inner_html_setter(*this, markup));
576 return {};
577}
578
579// https://w3c.github.io/DOM-Parsing/#dom-innerhtml-innerhtml
580WebIDL::ExceptionOr<DeprecatedString> Element::inner_html() const
581{
582 return serialize_fragment(DOMParsing::RequireWellFormed::Yes);
583}
584
585bool Element::is_focused() const
586{
587 return document().focused_element() == this;
588}
589
590bool Element::is_active() const
591{
592 return document().active_element() == this;
593}
594
595JS::NonnullGCPtr<HTMLCollection> Element::get_elements_by_class_name(DeprecatedFlyString const& class_names)
596{
597 Vector<FlyString> list_of_class_names;
598 for (auto& name : class_names.view().split_view_if(Infra::is_ascii_whitespace)) {
599 list_of_class_names.append(FlyString::from_utf8(name).release_value_but_fixme_should_propagate_errors());
600 }
601 return HTMLCollection::create(*this, [list_of_class_names = move(list_of_class_names), quirks_mode = document().in_quirks_mode()](Element const& element) {
602 for (auto& name : list_of_class_names) {
603 if (!element.has_class(name, quirks_mode ? CaseSensitivity::CaseInsensitive : CaseSensitivity::CaseSensitive))
604 return false;
605 }
606 return true;
607 }).release_value_but_fixme_should_propagate_errors();
608}
609
610// https://dom.spec.whatwg.org/#element-shadow-host
611bool Element::is_shadow_host() const
612{
613 // An element is a shadow host if its shadow root is non-null.
614 return m_shadow_root != nullptr;
615}
616
617void Element::set_shadow_root(JS::GCPtr<ShadowRoot> shadow_root)
618{
619 if (m_shadow_root == shadow_root)
620 return;
621 if (m_shadow_root)
622 m_shadow_root->set_host(nullptr);
623 m_shadow_root = move(shadow_root);
624 if (m_shadow_root)
625 m_shadow_root->set_host(this);
626 invalidate_style();
627}
628
629CSS::CSSStyleDeclaration* Element::style_for_bindings()
630{
631 if (!m_inline_style)
632 m_inline_style = CSS::ElementInlineCSSStyleDeclaration::create(*this, {}, {}).release_value_but_fixme_should_propagate_errors();
633 return m_inline_style;
634}
635
636// https://dom.spec.whatwg.org/#element-html-uppercased-qualified-name
637void Element::make_html_uppercased_qualified_name()
638{
639 // This is allowed by the spec: "User agents could optimize qualified name and HTML-uppercased qualified name by storing them in internal slots."
640 if (namespace_() == Namespace::HTML && document().document_type() == Document::Type::HTML)
641 m_html_uppercased_qualified_name = qualified_name().to_uppercase();
642 else
643 m_html_uppercased_qualified_name = qualified_name();
644}
645
646// https://html.spec.whatwg.org/multipage/webappapis.html#queue-an-element-task
647void Element::queue_an_element_task(HTML::Task::Source source, JS::SafeFunction<void()> steps)
648{
649 auto task = HTML::Task::create(source, &document(), move(steps));
650 HTML::main_thread_event_loop().task_queue().add(move(task));
651}
652
653// https://html.spec.whatwg.org/multipage/syntax.html#void-elements
654bool Element::is_void_element() const
655{
656 return local_name().is_one_of(HTML::TagNames::area, HTML::TagNames::base, HTML::TagNames::br, HTML::TagNames::col, HTML::TagNames::embed, HTML::TagNames::hr, HTML::TagNames::img, HTML::TagNames::input, HTML::TagNames::link, HTML::TagNames::meta, HTML::TagNames::param, HTML::TagNames::source, HTML::TagNames::track, HTML::TagNames::wbr);
657}
658
659// https://html.spec.whatwg.org/multipage/parsing.html#serializes-as-void
660bool Element::serializes_as_void() const
661{
662 return is_void_element() || local_name().is_one_of(HTML::TagNames::basefont, HTML::TagNames::bgsound, HTML::TagNames::frame, HTML::TagNames::keygen);
663}
664
665// https://drafts.csswg.org/cssom-view/#dom-element-getboundingclientrect
666JS::NonnullGCPtr<Geometry::DOMRect> Element::get_bounding_client_rect() const
667{
668 // // NOTE: Ensure that layout is up-to-date before looking at metrics.
669 const_cast<Document&>(document()).update_layout();
670
671 // FIXME: Support inline layout nodes as well.
672 auto* paint_box = this->paint_box();
673 if (!paint_box)
674 return Geometry::DOMRect::construct_impl(realm(), 0, 0, 0, 0).release_value_but_fixme_should_propagate_errors();
675
676 VERIFY(document().browsing_context());
677 auto viewport_offset = document().browsing_context()->viewport_scroll_offset();
678
679 return Geometry::DOMRect::create(realm(), paint_box->absolute_rect().translated(-viewport_offset.x(), -viewport_offset.y()).to_type<float>()).release_value_but_fixme_should_propagate_errors();
680}
681
682// https://drafts.csswg.org/cssom-view/#dom-element-getclientrects
683JS::NonnullGCPtr<Geometry::DOMRectList> Element::get_client_rects() const
684{
685 Vector<JS::Handle<Geometry::DOMRect>> rects;
686
687 // NOTE: Ensure that layout is up-to-date before looking at metrics.
688 const_cast<Document&>(document()).update_layout();
689
690 // 1. If the element on which it was invoked does not have an associated layout box return an empty DOMRectList object and stop this algorithm.
691 if (!layout_node() || !layout_node()->is_box())
692 return Geometry::DOMRectList::create(realm(), move(rects)).release_value_but_fixme_should_propagate_errors();
693
694 // FIXME: 2. If the element has an associated SVG layout box return a DOMRectList object containing a single DOMRect object that describes
695 // the bounding box of the element as defined by the SVG specification, applying the transforms that apply to the element and its ancestors.
696
697 // FIXME: 3. Return a DOMRectList object containing DOMRect objects in content order, one for each box fragment,
698 // describing its border area (including those with a height or width of zero) with the following constraints:
699 // - Apply the transforms that apply to the element and its ancestors.
700 // - If the element on which the method was invoked has a computed value for the display property of table
701 // or inline-table include both the table box and the caption box, if any, but not the anonymous container box.
702 // - Replace each anonymous block box with its child box(es) and repeat this until no anonymous block boxes are left in the final list.
703
704 auto bounding_rect = get_bounding_client_rect();
705 rects.append(*bounding_rect);
706 return Geometry::DOMRectList::create(realm(), move(rects)).release_value_but_fixme_should_propagate_errors();
707}
708
709int Element::client_top() const
710{
711 // NOTE: Ensure that layout is up-to-date before looking at metrics.
712 const_cast<Document&>(document()).update_layout();
713
714 // 1. If the element has no associated CSS layout box or if the CSS layout box is inline, return zero.
715 if (!layout_node() || !layout_node()->is_box())
716 return 0;
717
718 // 2. Return the computed value of the border-top-width property
719 // plus the height of any scrollbar rendered between the top padding edge and the top border edge,
720 // ignoring any transforms that apply to the element and its ancestors.
721 return static_cast<Layout::Box const&>(*layout_node()).computed_values().border_top().width;
722}
723
724// https://drafts.csswg.org/cssom-view/#dom-element-clientleft
725int Element::client_left() const
726{
727 // NOTE: Ensure that layout is up-to-date before looking at metrics.
728 const_cast<Document&>(document()).update_layout();
729
730 // 1. If the element has no associated CSS layout box or if the CSS layout box is inline, return zero.
731 if (!layout_node() || !layout_node()->is_box())
732 return 0;
733
734 // 2. Return the computed value of the border-left-width property
735 // plus the width of any scrollbar rendered between the left padding edge and the left border edge,
736 // ignoring any transforms that apply to the element and its ancestors.
737 return static_cast<Layout::Box const&>(*layout_node()).computed_values().border_left().width;
738}
739
740// https://drafts.csswg.org/cssom-view/#dom-element-clientwidth
741int Element::client_width() const
742{
743 // NOTE: We do step 2 before step 1 here since step 2 can exit early without needing to perform layout.
744
745 // 2. If the element is the root element and the element’s node document is not in quirks mode,
746 // or if the element is the HTML body element and the element’s node document is in quirks mode,
747 // return the viewport width excluding the size of a rendered scroll bar (if any).
748 if ((is<HTML::HTMLHtmlElement>(*this) && !document().in_quirks_mode())
749 || (is<HTML::HTMLBodyElement>(*this) && document().in_quirks_mode())) {
750 return document().browsing_context()->viewport_rect().width().value();
751 }
752
753 // NOTE: Ensure that layout is up-to-date before looking at metrics.
754 const_cast<Document&>(document()).update_layout();
755
756 // 1. If the element has no associated CSS layout box or if the CSS layout box is inline, return zero.
757 if (!paint_box())
758 return 0;
759
760 // 3. Return the width of the padding edge excluding the width of any rendered scrollbar between the padding edge and the border edge,
761 // ignoring any transforms that apply to the element and its ancestors.
762 return paint_box()->absolute_padding_box_rect().width().value();
763}
764
765// https://drafts.csswg.org/cssom-view/#dom-element-clientheight
766int Element::client_height() const
767{
768 // NOTE: We do step 2 before step 1 here since step 2 can exit early without needing to perform layout.
769
770 // 2. If the element is the root element and the element’s node document is not in quirks mode,
771 // or if the element is the HTML body element and the element’s node document is in quirks mode,
772 // return the viewport height excluding the size of a rendered scroll bar (if any).
773 if ((is<HTML::HTMLHtmlElement>(*this) && !document().in_quirks_mode())
774 || (is<HTML::HTMLBodyElement>(*this) && document().in_quirks_mode())) {
775 return document().browsing_context()->viewport_rect().height().value();
776 }
777
778 // NOTE: Ensure that layout is up-to-date before looking at metrics.
779 const_cast<Document&>(document()).update_layout();
780
781 // 1. If the element has no associated CSS layout box or if the CSS layout box is inline, return zero.
782 if (!paint_box())
783 return 0;
784
785 // 3. Return the height of the padding edge excluding the height of any rendered scrollbar between the padding edge and the border edge,
786 // ignoring any transforms that apply to the element and its ancestors.
787 return paint_box()->absolute_padding_box_rect().height().value();
788}
789
790void Element::children_changed()
791{
792 Node::children_changed();
793 set_needs_style_update(true);
794}
795
796void Element::set_pseudo_element_node(Badge<Layout::TreeBuilder>, CSS::Selector::PseudoElement pseudo_element, JS::GCPtr<Layout::Node> pseudo_element_node)
797{
798 m_pseudo_element_nodes[to_underlying(pseudo_element)] = pseudo_element_node;
799}
800
801JS::GCPtr<Layout::Node> Element::get_pseudo_element_node(CSS::Selector::PseudoElement pseudo_element) const
802{
803 return m_pseudo_element_nodes[to_underlying(pseudo_element)];
804}
805
806void Element::clear_pseudo_element_nodes(Badge<Layout::TreeBuilder>)
807{
808 m_pseudo_element_nodes.fill({});
809}
810
811void Element::serialize_pseudo_elements_as_json(JsonArraySerializer<StringBuilder>& children_array) const
812{
813 for (size_t i = 0; i < m_pseudo_element_nodes.size(); ++i) {
814 auto& pseudo_element_node = m_pseudo_element_nodes[i];
815 if (!pseudo_element_node)
816 continue;
817 auto object = MUST(children_array.add_object());
818 MUST(object.add("name"sv, DeprecatedString::formatted("::{}", CSS::pseudo_element_name(static_cast<CSS::Selector::PseudoElement>(i)))));
819 MUST(object.add("type"sv, "pseudo-element"));
820 MUST(object.add("parent-id"sv, id()));
821 MUST(object.add("pseudo-element"sv, i));
822 MUST(object.finish());
823 }
824}
825
826// https://html.spec.whatwg.org/multipage/interaction.html#dom-tabindex
827i32 Element::default_tab_index_value() const
828{
829 // The default value is 0 if the element is an a, area, button, frame, iframe, input, object, select, textarea, or SVG a element, or is a summary element that is a summary for its parent details.
830 // The default value is −1 otherwise.
831 // Note: The varying default value based on element type is a historical artifact.
832 // FIXME: We currently do not have the SVG a element.
833 return -1;
834}
835
836// https://html.spec.whatwg.org/multipage/interaction.html#dom-tabindex
837i32 Element::tab_index() const
838{
839 // FIXME: I'm not sure if "to_int" exactly matches the specs "rules for parsing integers"
840 auto maybe_table_index = attribute(HTML::AttributeNames::tabindex).to_int<i32>();
841 if (!maybe_table_index.has_value())
842 return default_tab_index_value();
843 return maybe_table_index.value();
844}
845
846// https://html.spec.whatwg.org/multipage/interaction.html#dom-tabindex
847void Element::set_tab_index(i32 tab_index)
848{
849 MUST(set_attribute(HTML::AttributeNames::tabindex, DeprecatedString::number(tab_index)));
850}
851
852// https://drafts.csswg.org/cssom-view/#potentially-scrollable
853bool Element::is_potentially_scrollable() const
854{
855 // NOTE: Ensure that layout is up-to-date before looking at metrics.
856 const_cast<Document&>(document()).update_layout();
857
858 // An element body (which will be the body element) is potentially scrollable if all of the following conditions are true:
859 VERIFY(is<HTML::HTMLBodyElement>(this) || is<HTML::HTMLFrameSetElement>(this));
860
861 // Since this should always be the body element, the body element must have a <html> element parent. See Document::body().
862 VERIFY(parent());
863
864 // - body has an associated box.
865 // - body’s parent element’s computed value of the overflow-x or overflow-y properties is neither visible nor clip.
866 // - body’s computed value of the overflow-x or overflow-y properties is neither visible nor clip.
867 return layout_node()
868 && (parent()->layout_node()
869 && parent()->layout_node()->computed_values().overflow_x() != CSS::Overflow::Visible && parent()->layout_node()->computed_values().overflow_x() != CSS::Overflow::Clip
870 && parent()->layout_node()->computed_values().overflow_y() != CSS::Overflow::Visible && parent()->layout_node()->computed_values().overflow_y() != CSS::Overflow::Clip)
871 && (layout_node()->computed_values().overflow_x() != CSS::Overflow::Visible && layout_node()->computed_values().overflow_x() != CSS::Overflow::Clip
872 && layout_node()->computed_values().overflow_y() != CSS::Overflow::Visible && layout_node()->computed_values().overflow_y() != CSS::Overflow::Clip);
873}
874
875// https://drafts.csswg.org/cssom-view/#dom-element-scrolltop
876double Element::scroll_top() const
877{
878 // 1. Let document be the element’s node document.
879 auto& document = this->document();
880
881 // 2. If document is not the active document, return zero and terminate these steps.
882 if (!document.is_active())
883 return 0.0;
884
885 // 3. Let window be the value of document’s defaultView attribute.
886 auto* window = document.default_view();
887
888 // 4. If window is null, return zero and terminate these steps.
889 if (!window)
890 return 0.0;
891
892 // 5. If the element is the root element and document is in quirks mode, return zero and terminate these steps.
893 if (document.document_element() == this && document.in_quirks_mode())
894 return 0.0;
895
896 // NOTE: Ensure that layout is up-to-date before looking at metrics.
897 const_cast<Document&>(document).update_layout();
898
899 // 6. If the element is the root element return the value of scrollY on window.
900 if (document.document_element() == this)
901 return window->scroll_y();
902
903 // 7. If the element is the body element, document is in quirks mode, and the element is not potentially scrollable, return the value of scrollY on window.
904 if (document.body() == this && document.in_quirks_mode() && !is_potentially_scrollable())
905 return window->scroll_y();
906
907 // 8. If the element does not have any associated box, return zero and terminate these steps.
908 if (!layout_node() || !is<Layout::Box>(layout_node()))
909 return 0.0;
910
911 auto const* box = static_cast<Layout::Box const*>(layout_node());
912
913 // 9. Return the y-coordinate of the scrolling area at the alignment point with the top of the padding edge of the element.
914 // FIXME: Is this correct?
915 return box->scroll_offset().y().value();
916}
917
918double Element::scroll_left() const
919{
920 // 1. Let document be the element’s node document.
921 auto& document = this->document();
922
923 // 2. If document is not the active document, return zero and terminate these steps.
924 if (!document.is_active())
925 return 0.0;
926
927 // 3. Let window be the value of document’s defaultView attribute.
928 auto* window = document.default_view();
929
930 // 4. If window is null, return zero and terminate these steps.
931 if (!window)
932 return 0.0;
933
934 // 5. If the element is the root element and document is in quirks mode, return zero and terminate these steps.
935 if (document.document_element() == this && document.in_quirks_mode())
936 return 0.0;
937
938 // NOTE: Ensure that layout is up-to-date before looking at metrics.
939 const_cast<Document&>(document).update_layout();
940
941 // 6. If the element is the root element return the value of scrollX on window.
942 if (document.document_element() == this)
943 return window->scroll_x();
944
945 // 7. If the element is the body element, document is in quirks mode, and the element is not potentially scrollable, return the value of scrollX on window.
946 if (document.body() == this && document.in_quirks_mode() && !is_potentially_scrollable())
947 return window->scroll_x();
948
949 // 8. If the element does not have any associated box, return zero and terminate these steps.
950 if (!layout_node() || !is<Layout::Box>(layout_node()))
951 return 0.0;
952
953 auto const* box = static_cast<Layout::Box const*>(layout_node());
954
955 // 9. Return the x-coordinate of the scrolling area at the alignment point with the left of the padding edge of the element.
956 // FIXME: Is this correct?
957 return box->scroll_offset().x().value();
958}
959
960// https://drafts.csswg.org/cssom-view/#dom-element-scrollleft
961void Element::set_scroll_left(double x)
962{
963 // 1. Let x be the given value.
964
965 // 2. Normalize non-finite values for x.
966 if (!isfinite(x))
967 x = 0.0;
968
969 // 3. Let document be the element’s node document.
970 auto& document = this->document();
971
972 // 4. If document is not the active document, terminate these steps.
973 if (!document.is_active())
974 return;
975
976 // 5. Let window be the value of document’s defaultView attribute.
977 auto* window = document.default_view();
978
979 // 6. If window is null, terminate these steps.
980 if (!window)
981 return;
982
983 // 7. If the element is the root element and document is in quirks mode, terminate these steps.
984 if (document.document_element() == this && document.in_quirks_mode())
985 return;
986
987 // NOTE: Ensure that layout is up-to-date before looking at metrics or scrolling the page.
988 const_cast<Document&>(document).update_layout();
989
990 // 8. If the element is the root element invoke scroll() on window with x as first argument and scrollY on window as second argument, and terminate these steps.
991 if (document.document_element() == this) {
992 // FIXME: Implement this in terms of invoking scroll() on window.
993 if (auto* page = document.page())
994 page->client().page_did_request_scroll_to({ static_cast<float>(x), static_cast<float>(window->scroll_y()) });
995
996 return;
997 }
998
999 // 9. If the element is the body element, document is in quirks mode, and the element is not potentially scrollable, invoke scroll() on window with x as first argument and scrollY on window as second argument, and terminate these steps.
1000 if (document.body() == this && document.in_quirks_mode() && !is_potentially_scrollable()) {
1001 // FIXME: Implement this in terms of invoking scroll() on window.
1002 if (auto* page = document.page())
1003 page->client().page_did_request_scroll_to({ static_cast<float>(x), static_cast<float>(window->scroll_y()) });
1004
1005 return;
1006 }
1007
1008 // 10. If the element does not have any associated box, the element has no associated scrolling box, or the element has no overflow, terminate these steps.
1009 if (!layout_node() || !is<Layout::Box>(layout_node()))
1010 return;
1011
1012 auto* box = static_cast<Layout::Box*>(layout_node());
1013 if (!box->is_scrollable())
1014 return;
1015
1016 // FIXME: or the element has no overflow.
1017
1018 // 11. Scroll the element to x,scrollTop, with the scroll behavior being "auto".
1019 // FIXME: Implement this in terms of calling "scroll the element".
1020 auto scroll_offset = box->scroll_offset();
1021 scroll_offset.set_x(static_cast<float>(x));
1022 box->set_scroll_offset(scroll_offset);
1023}
1024
1025void Element::set_scroll_top(double y)
1026{
1027 // 1. Let y be the given value.
1028
1029 // 2. Normalize non-finite values for y.
1030 if (!isfinite(y))
1031 y = 0.0;
1032
1033 // 3. Let document be the element’s node document.
1034 auto& document = this->document();
1035
1036 // 4. If document is not the active document, terminate these steps.
1037 if (!document.is_active())
1038 return;
1039
1040 // 5. Let window be the value of document’s defaultView attribute.
1041 auto* window = document.default_view();
1042
1043 // 6. If window is null, terminate these steps.
1044 if (!window)
1045 return;
1046
1047 // 7. If the element is the root element and document is in quirks mode, terminate these steps.
1048 if (document.document_element() == this && document.in_quirks_mode())
1049 return;
1050
1051 // NOTE: Ensure that layout is up-to-date before looking at metrics or scrolling the page.
1052 const_cast<Document&>(document).update_layout();
1053
1054 // 8. If the element is the root element invoke scroll() on window with scrollX on window as first argument and y as second argument, and terminate these steps.
1055 if (document.document_element() == this) {
1056 // FIXME: Implement this in terms of invoking scroll() on window.
1057 if (auto* page = document.page())
1058 page->client().page_did_request_scroll_to({ static_cast<float>(window->scroll_x()), static_cast<float>(y) });
1059
1060 return;
1061 }
1062
1063 // 9. If the element is the body element, document is in quirks mode, and the element is not potentially scrollable, invoke scroll() on window with scrollX as first argument and y as second argument, and terminate these steps.
1064 if (document.body() == this && document.in_quirks_mode() && !is_potentially_scrollable()) {
1065 // FIXME: Implement this in terms of invoking scroll() on window.
1066 if (auto* page = document.page())
1067 page->client().page_did_request_scroll_to({ static_cast<float>(window->scroll_x()), static_cast<float>(y) });
1068
1069 return;
1070 }
1071
1072 // 10. If the element does not have any associated box, the element has no associated scrolling box, or the element has no overflow, terminate these steps.
1073 if (!layout_node() || !is<Layout::Box>(layout_node()))
1074 return;
1075
1076 auto* box = static_cast<Layout::Box*>(layout_node());
1077 if (!box->is_scrollable())
1078 return;
1079
1080 // FIXME: or the element has no overflow.
1081
1082 // 11. Scroll the element to scrollLeft,y, with the scroll behavior being "auto".
1083 // FIXME: Implement this in terms of calling "scroll the element".
1084 auto scroll_offset = box->scroll_offset();
1085 scroll_offset.set_y(static_cast<float>(y));
1086 box->set_scroll_offset(scroll_offset);
1087}
1088
1089int Element::scroll_width() const
1090{
1091 dbgln("FIXME: Implement Element::scroll_width() (called on element: {})", debug_description());
1092 return 0;
1093}
1094
1095int Element::scroll_height() const
1096{
1097 dbgln("FIXME: Implement Element::scroll_height() (called on element: {})", debug_description());
1098 return 0;
1099}
1100
1101// https://html.spec.whatwg.org/multipage/semantics-other.html#concept-element-disabled
1102bool Element::is_actually_disabled() const
1103{
1104 // An element is said to be actually disabled if it is one of the following:
1105 // - a button element that is disabled
1106 // - an input element that is disabled
1107 // - a select element that is disabled
1108 // - a textarea element that is disabled
1109 if (is<HTML::HTMLButtonElement>(this) || is<HTML::HTMLInputElement>(this) || is<HTML::HTMLSelectElement>(this) || is<HTML::HTMLTextAreaElement>(this)) {
1110 auto const* form_associated_element = dynamic_cast<HTML::FormAssociatedElement const*>(this);
1111 VERIFY(form_associated_element);
1112
1113 return !form_associated_element->enabled();
1114 }
1115
1116 // - an optgroup element that has a disabled attribute
1117 if (is<HTML::HTMLOptGroupElement>(this))
1118 return has_attribute(HTML::AttributeNames::disabled);
1119
1120 // - an option element that is disabled
1121 if (is<HTML::HTMLOptionElement>(this))
1122 return static_cast<HTML::HTMLOptionElement const&>(*this).disabled();
1123
1124 // - a fieldset element that is a disabled fieldset
1125 if (is<HTML::HTMLFieldSetElement>(this))
1126 return static_cast<HTML::HTMLFieldSetElement const&>(*this).is_disabled();
1127
1128 // FIXME: - a form-associated custom element that is disabled
1129 return false;
1130}
1131
1132// https://w3c.github.io/DOM-Parsing/#dom-element-insertadjacenthtml
1133WebIDL::ExceptionOr<void> Element::insert_adjacent_html(DeprecatedString position, DeprecatedString text)
1134{
1135 JS::GCPtr<Node> context;
1136 // 1. Use the first matching item from this list:
1137 // - If position is an ASCII case-insensitive match for the string "beforebegin"
1138 // - If position is an ASCII case-insensitive match for the string "afterend"
1139 if (Infra::is_ascii_case_insensitive_match(position, "beforebegin"sv)
1140 || Infra::is_ascii_case_insensitive_match(position, "afterend"sv)) {
1141 // Let context be the context object's parent.
1142 context = this->parent();
1143
1144 // If context is null or a Document, throw a "NoModificationAllowedError" DOMException.
1145 if (!context || context->is_document())
1146 return WebIDL::NoModificationAllowedError::create(realm(), "insertAdjacentHTML: context is null or a Document"sv);
1147 }
1148 // - If position is an ASCII case-insensitive match for the string "afterbegin"
1149 // - If position is an ASCII case-insensitive match for the string "beforeend"
1150 else if (Infra::is_ascii_case_insensitive_match(position, "afterbegin"sv)
1151 || Infra::is_ascii_case_insensitive_match(position, "beforeend"sv)) {
1152 // Let context be the context object.
1153 context = this;
1154 }
1155 // Otherwise
1156 else {
1157 // Throw a "SyntaxError" DOMException.
1158 return WebIDL::SyntaxError::create(realm(), "insertAdjacentHTML: invalid position argument"sv);
1159 }
1160
1161 // 2. If context is not an Element or the following are all true:
1162 // - context's node document is an HTML document,
1163 // - context's local name is "html", and
1164 // - context's namespace is the HTML namespace;
1165 if (!is<Element>(*context)
1166 || (context->document().document_type() == Document::Type::HTML
1167 && static_cast<Element const&>(*context).local_name() == "html"sv
1168 && static_cast<Element const&>(*context).namespace_() == Namespace::HTML)) {
1169 // FIXME: let context be a new Element with
1170 // - body as its local name,
1171 // - The HTML namespace as its namespace, and
1172 // - The context object's node document as its node document.
1173 TODO();
1174 }
1175
1176 // 3. Let fragment be the result of invoking the fragment parsing algorithm with text as markup, and context as the context element.
1177 auto fragment = TRY(DOMParsing::parse_fragment(text, verify_cast<Element>(*context)));
1178
1179 // 4. Use the first matching item from this list:
1180
1181 // - If position is an ASCII case-insensitive match for the string "beforebegin"
1182 if (Infra::is_ascii_case_insensitive_match(position, "beforebegin"sv)) {
1183 // Insert fragment into the context object's parent before the context object.
1184 parent()->insert_before(fragment, this);
1185 }
1186
1187 // - If position is an ASCII case-insensitive match for the string "afterbegin"
1188 else if (Infra::is_ascii_case_insensitive_match(position, "afterbegin"sv)) {
1189 // Insert fragment into the context object before its first child.
1190 insert_before(fragment, first_child());
1191 }
1192
1193 // - If position is an ASCII case-insensitive match for the string "beforeend"
1194 else if (Infra::is_ascii_case_insensitive_match(position, "beforeend"sv)) {
1195 // Append fragment to the context object.
1196 TRY(append_child(fragment));
1197 }
1198
1199 // - If position is an ASCII case-insensitive match for the string "afterend"
1200 else if (Infra::is_ascii_case_insensitive_match(position, "afterend"sv)) {
1201 // Insert fragment into the context object's parent before the context object's next sibling.
1202 parent()->insert_before(fragment, next_sibling());
1203 }
1204 return {};
1205}
1206
1207// https://dom.spec.whatwg.org/#insert-adjacent
1208WebIDL::ExceptionOr<JS::GCPtr<Node>> Element::insert_adjacent(DeprecatedString const& where, JS::NonnullGCPtr<Node> node)
1209{
1210 // To insert adjacent, given an element element, string where, and a node node, run the steps associated with the first ASCII case-insensitive match for where:
1211 if (Infra::is_ascii_case_insensitive_match(where, "beforebegin"sv)) {
1212 // -> "beforebegin"
1213 // If element’s parent is null, return null.
1214 if (!parent())
1215 return JS::GCPtr<Node> { nullptr };
1216
1217 // Return the result of pre-inserting node into element’s parent before element.
1218 return JS::GCPtr<Node> { TRY(parent()->pre_insert(move(node), this)) };
1219 }
1220
1221 if (Infra::is_ascii_case_insensitive_match(where, "afterbegin"sv)) {
1222 // -> "afterbegin"
1223 // Return the result of pre-inserting node into element before element’s first child.
1224 return JS::GCPtr<Node> { TRY(pre_insert(move(node), first_child())) };
1225 }
1226
1227 if (Infra::is_ascii_case_insensitive_match(where, "beforeend"sv)) {
1228 // -> "beforeend"
1229 // Return the result of pre-inserting node into element before null.
1230 return JS::GCPtr<Node> { TRY(pre_insert(move(node), nullptr)) };
1231 }
1232
1233 if (Infra::is_ascii_case_insensitive_match(where, "afterend"sv)) {
1234 // -> "afterend"
1235 // If element’s parent is null, return null.
1236 if (!parent())
1237 return JS::GCPtr<Node> { nullptr };
1238
1239 // Return the result of pre-inserting node into element’s parent before element’s next sibling.
1240 return JS::GCPtr<Node> { TRY(parent()->pre_insert(move(node), next_sibling())) };
1241 }
1242
1243 // -> Otherwise
1244 // Throw a "SyntaxError" DOMException.
1245 return WebIDL::SyntaxError::create(realm(), DeprecatedString::formatted("Unknown position '{}'. Must be one of 'beforebegin', 'afterbegin', 'beforeend' or 'afterend'"sv, where));
1246}
1247
1248// https://dom.spec.whatwg.org/#dom-element-insertadjacentelement
1249WebIDL::ExceptionOr<JS::GCPtr<Element>> Element::insert_adjacent_element(DeprecatedString const& where, JS::NonnullGCPtr<Element> element)
1250{
1251 // The insertAdjacentElement(where, element) method steps are to return the result of running insert adjacent, give this, where, and element.
1252 auto returned_node = TRY(insert_adjacent(where, move(element)));
1253 if (!returned_node)
1254 return JS::GCPtr<Element> { nullptr };
1255 return JS::GCPtr<Element> { verify_cast<Element>(*returned_node) };
1256}
1257
1258// https://dom.spec.whatwg.org/#dom-element-insertadjacenttext
1259WebIDL::ExceptionOr<void> Element::insert_adjacent_text(DeprecatedString const& where, DeprecatedString const& data)
1260{
1261 // 1. Let text be a new Text node whose data is data and node document is this’s node document.
1262 auto text = MUST_OR_THROW_OOM(heap().allocate<DOM::Text>(realm(), document(), data));
1263
1264 // 2. Run insert adjacent, given this, where, and text.
1265 // Spec Note: This method returns nothing because it existed before we had a chance to design it.
1266 (void)TRY(insert_adjacent(where, text));
1267 return {};
1268}
1269
1270// https://w3c.github.io/csswg-drafts/cssom-view-1/#scroll-an-element-into-view
1271static ErrorOr<void> scroll_an_element_into_view(DOM::Element& element, Bindings::ScrollBehavior behavior, Bindings::ScrollLogicalPosition block, Bindings::ScrollLogicalPosition inline_)
1272{
1273 // FIXME: The below is ad-hoc, since we don't yet have scrollable elements.
1274 // Return here and implement this according to spec once all overflow is made scrollable.
1275
1276 (void)behavior;
1277 (void)block;
1278 (void)inline_;
1279
1280 if (!element.document().browsing_context())
1281 return Error::from_string_view("Element has no browsing context."sv);
1282
1283 auto* page = element.document().browsing_context()->page();
1284 if (!page)
1285 return Error::from_string_view("Element has no page."sv);
1286
1287 // If this element doesn't have a layout node, we can't scroll it into view.
1288 element.document().update_layout();
1289 if (!element.layout_node())
1290 return Error::from_string_view("Element has no layout node."sv);
1291
1292 // Find the nearest layout node that is a box (since we need a box to get a usable rect)
1293 auto* layout_node = element.layout_node();
1294 while (layout_node && !layout_node->is_box())
1295 layout_node = layout_node->parent();
1296
1297 if (!layout_node)
1298 return Error::from_string_view("Element has no parent layout node that is a box."sv);
1299
1300 page->client().page_did_request_scroll_into_view(verify_cast<Layout::Box>(*layout_node).paint_box()->absolute_padding_box_rect());
1301
1302 return {};
1303}
1304
1305// https://w3c.github.io/csswg-drafts/cssom-view-1/#dom-element-scrollintoview
1306ErrorOr<void> Element::scroll_into_view(Optional<Variant<bool, ScrollIntoViewOptions>> arg)
1307{
1308 // 1. Let behavior be "auto".
1309 auto behavior = Bindings::ScrollBehavior::Auto;
1310
1311 // 2. Let block be "start".
1312 auto block = Bindings::ScrollLogicalPosition::Start;
1313
1314 // 3. Let inline be "nearest".
1315 auto inline_ = Bindings::ScrollLogicalPosition::Nearest;
1316
1317 // 4. If arg is a ScrollIntoViewOptions dictionary, then:
1318 if (arg.has_value() && arg->has<ScrollIntoViewOptions>()) {
1319 // 1. Set behavior to the behavior dictionary member of options.
1320 behavior = arg->get<ScrollIntoViewOptions>().behavior;
1321
1322 // 2. Set block to the block dictionary member of options.
1323 block = arg->get<ScrollIntoViewOptions>().block;
1324
1325 // 3. Set inline to the inline dictionary member of options.
1326 inline_ = arg->get<ScrollIntoViewOptions>().inline_;
1327 }
1328 // 5. Otherwise, if arg is false, then set block to "end".
1329 else if (arg.has_value() && arg->has<bool>() && arg->get<bool>() == false) {
1330 block = Bindings::ScrollLogicalPosition::End;
1331 }
1332
1333 // 6. If the element does not have any associated box, or is not available to user-agent features, then return.
1334 document().update_layout();
1335 if (!layout_node())
1336 return Error::from_string_view("Element has no associated box"sv);
1337
1338 // 7. Scroll the element into view with behavior, block, and inline.
1339 TRY(scroll_an_element_into_view(*this, behavior, block, inline_));
1340
1341 return {};
1342
1343 // FIXME: 8. Optionally perform some other action that brings the element to the user’s attention.
1344}
1345
1346void Element::invalidate_style_after_attribute_change(DeprecatedFlyString const& attribute_name)
1347{
1348 // FIXME: Only invalidate if the attribute can actually affect style.
1349 (void)attribute_name;
1350
1351 // FIXME: This will need to become smarter when we implement the :has() selector.
1352 invalidate_style();
1353}
1354
1355// https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion
1356bool Element::exclude_from_accessibility_tree() const
1357{
1358 // The following elements are not exposed via the accessibility API and user agents MUST NOT include them in the accessibility tree:
1359
1360 // Elements, including their descendent elements, that have host language semantics specifying that the element is not displayed, such as CSS display:none, visibility:hidden, or the HTML hidden attribute.
1361 if (!layout_node())
1362 return true;
1363
1364 // Elements with none or presentation as the first role in the role attribute. However, their exclusion is conditional. In addition, the element's descendants and text content are generally included. These exceptions and conditions are documented in the presentation (role) section.
1365 // FIXME: Handle exceptions to excluding presentation role
1366 auto role = role_or_default();
1367 if (role == ARIA::Role::none || role == ARIA::Role::presentation)
1368 return true;
1369
1370 // TODO: If not already excluded from the accessibility tree per the above rules, user agents SHOULD NOT include the following elements in the accessibility tree:
1371 // Elements, including their descendants, that have aria-hidden set to true. In other words, aria-hidden="true" on a parent overrides aria-hidden="false" on descendants.
1372 // Any descendants of elements that have the characteristic "Children Presentational: True" unless the descendant is not allowed to be presentational because it meets one of the conditions for exception described in Presentational Roles Conflict Resolution. However, the text content of any excluded descendants is included.
1373 // Elements with the following roles have the characteristic "Children Presentational: True":
1374 // button
1375 // checkbox
1376 // img
1377 // menuitemcheckbox
1378 // menuitemradio
1379 // meter
1380 // option
1381 // progressbar
1382 // radio
1383 // scrollbar
1384 // separator
1385 // slider
1386 // switch
1387 // tab
1388 return false;
1389}
1390
1391// https://www.w3.org/TR/wai-aria-1.2/#tree_inclusion
1392bool Element::include_in_accessibility_tree() const
1393{
1394 // If not excluded from or marked as hidden in the accessibility tree per the rules above in Excluding Elements in the Accessibility Tree, user agents MUST provide an accessible object in the accessibility tree for DOM elements that meet any of the following criteria:
1395 if (exclude_from_accessibility_tree())
1396 return false;
1397 // Elements that are not hidden and may fire an accessibility API event, including:
1398 // Elements that are currently focused, even if the element or one of its ancestor elements has its aria-hidden attribute set to true.
1399 if (is_focused())
1400 return true;
1401 // TODO: Elements that are a valid target of an aria-activedescendant attribute.
1402
1403 // Elements that have an explicit role or a global WAI-ARIA attribute and do not have aria-hidden set to true. (See Excluding Elements in the Accessibility Tree for additional guidance on aria-hidden.)
1404 // NOTE: The spec says only explicit roles count, but playing around in other browsers, this does not seem to be true in practice (for example button elements are always exposed with their implicit role if none is set)
1405 // This issue https://github.com/w3c/aria/issues/1851 seeks clarification on this point
1406 if ((role_or_default().has_value() || has_global_aria_attribute()) && aria_hidden() != "true")
1407 return true;
1408
1409 // TODO: Elements that are not hidden and have an ID that is referenced by another element via a WAI-ARIA property.
1410
1411 return false;
1412}
1413
1414}