Serenity Operating System
at master 1414 lines 63 kB view raw
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}