/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::collections::HashSet; use std::default::Default; use std::rc::Rc; use dom_struct::dom_struct; use html5ever::{LocalName, Prefix, QualName, local_name, ns}; use js::context::JSContext; use js::rust::HandleObject; use layout_api::{QueryMsg, ScrollContainerQueryFlags, ScrollContainerResponse}; use script_bindings::codegen::GenericBindings::DocumentBinding::DocumentMethods; use script_bindings::codegen::GenericBindings::ElementBinding::ScrollLogicalPosition; use script_bindings::codegen::GenericBindings::WindowBinding::ScrollBehavior; use style::attr::AttrValue; use stylo_dom::ElementState; use crate::dom::activation::Activatable; use crate::dom::attr::Attr; use crate::dom::bindings::codegen::Bindings::CharacterDataBinding::CharacterData_Binding::CharacterDataMethods; use crate::dom::bindings::codegen::Bindings::EventHandlerBinding::{ EventHandlerNonNull, OnErrorEventHandlerNonNull, }; use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods; use crate::dom::bindings::codegen::Bindings::HTMLLabelElementBinding::HTMLLabelElementMethods; use crate::dom::bindings::codegen::Bindings::HTMLOrSVGElementBinding::FocusOptions; use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods; use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::ShadowRoot_Binding::ShadowRootMethods; use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods; use crate::dom::bindings::error::{Error, ErrorResult, Fallible}; use crate::dom::bindings::inheritance::{Castable, ElementTypeId, HTMLElementTypeId, NodeTypeId}; use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom}; use crate::dom::bindings::str::DOMString; use crate::dom::characterdata::CharacterData; use crate::dom::css::cssstyledeclaration::{ CSSModificationAccess, CSSStyleDeclaration, CSSStyleOwner, }; use crate::dom::customelementregistry::{CallbackReaction, CustomElementState}; use crate::dom::document::Document; use crate::dom::document::focus::FocusableArea; use crate::dom::document_event_handler::character_to_code; use crate::dom::documentfragment::DocumentFragment; use crate::dom::domstringmap::DOMStringMap; use crate::dom::element::{ AttributeMutation, CustomElementCreationMode, Element, ElementCreator, is_element_affected_by_legacy_background_presentational_hint, }; use crate::dom::elementinternals::ElementInternals; use crate::dom::event::Event; use crate::dom::eventtarget::EventTarget; use crate::dom::html::htmlbodyelement::HTMLBodyElement; use crate::dom::html::htmldetailselement::HTMLDetailsElement; use crate::dom::html::htmlformelement::{FormControl, HTMLFormElement}; use crate::dom::html::htmlframesetelement::HTMLFrameSetElement; use crate::dom::html::htmlhtmlelement::HTMLHtmlElement; use crate::dom::html::htmllabelelement::HTMLLabelElement; use crate::dom::html::htmltextareaelement::HTMLTextAreaElement; use crate::dom::html::input_element::HTMLInputElement; use crate::dom::htmlformelement::FormControlElementHelpers; use crate::dom::input_element::input_type::InputType; use crate::dom::medialist::MediaList; use crate::dom::node::{ BindContext, MoveContext, Node, NodeTraits, ShadowIncluding, UnbindContext, from_untrusted_node_address, }; use crate::dom::scrolling_box::{ScrollAxisState, ScrollRequirement}; use crate::dom::shadowroot::ShadowRoot; use crate::dom::text::Text; use crate::dom::virtualmethods::VirtualMethods; use crate::script_runtime::CanGc; use crate::script_thread::ScriptThread; #[dom_struct] pub(crate) struct HTMLElement { element: Element, style_decl: MutNullableDom, dataset: MutNullableDom, } impl HTMLElement { pub(crate) fn new_inherited( tag_name: LocalName, prefix: Option, document: &Document, ) -> HTMLElement { HTMLElement::new_inherited_with_state(ElementState::empty(), tag_name, prefix, document) } pub(crate) fn new_inherited_with_state( state: ElementState, tag_name: LocalName, prefix: Option, document: &Document, ) -> HTMLElement { HTMLElement { element: Element::new_inherited_with_state( state, tag_name, ns!(html), prefix, document, ), style_decl: Default::default(), dataset: Default::default(), } } pub(crate) fn new( cx: &mut js::context::JSContext, local_name: LocalName, prefix: Option, document: &Document, proto: Option, ) -> DomRoot { Node::reflect_node_with_proto( cx, Box::new(HTMLElement::new_inherited(local_name, prefix, document)), document, proto, ) } fn is_body_or_frameset(&self) -> bool { let eventtarget = self.upcast::(); eventtarget.is::() || eventtarget.is::() } /// Calls into the layout engine to generate a plain text representation /// of a [`HTMLElement`] as specified when getting the `.innerText` or /// `.outerText` in JavaScript.` /// /// pub(crate) fn get_inner_outer_text(&self) -> DOMString { let node = self.upcast::(); let window = node.owner_window(); let element = self.as_element(); // Step 1. let element_not_rendered = !node.is_connected() || !element.has_css_layout_box(); if element_not_rendered { return node.GetTextContent().unwrap(); } window.layout_reflow(QueryMsg::ElementInnerOuterTextQuery); let text = window .layout() .query_element_inner_outer_text(node.to_trusted_node_address()); DOMString::from(text) } /// pub(crate) fn set_inner_text(&self, cx: &mut JSContext, input: DOMString) { // Step 1: Let fragment be the rendered text fragment for value given element's node // document. let fragment = self.rendered_text_fragment(cx, input); // Step 2: Replace all with fragment within element. Node::replace_all(cx, Some(fragment.upcast()), self.upcast::()); } /// pub(crate) fn media_attribute_matches_media_environment(&self) -> bool { // A string matches the environment of the user if it is the empty string, // a string consisting of only ASCII whitespace, or is a media query list that // matches the user's environment according to the definitions given in Media Queries. [MQ] self.element .get_attribute(&local_name!("media")) .is_none_or(|media| { MediaList::matches_environment(&self.owner_document(), &media.value()) }) } /// pub(crate) fn is_editing_host(&self) -> bool { // > An editing host is either an HTML element with its contenteditable attribute in the true state or plaintext-only state, matches!(&*self.ContentEditable().str(), "true" | "plaintext-only") // > or a child HTML element of a Document whose design mode enabled is true. // TODO } } impl HTMLElementMethods for HTMLElement { /// fn Style(&self, can_gc: CanGc) -> DomRoot { self.style_decl.or_init(|| { let global = self.owner_window(); CSSStyleDeclaration::new( &global, CSSStyleOwner::Element(Dom::from_ref(self.upcast())), None, CSSModificationAccess::ReadWrite, can_gc, ) }) } // https://html.spec.whatwg.org/multipage/#attr-title make_getter!(Title, "title"); // https://html.spec.whatwg.org/multipage/#attr-title make_setter!(SetTitle, "title"); // https://html.spec.whatwg.org/multipage/#attr-lang make_getter!(Lang, "lang"); // https://html.spec.whatwg.org/multipage/#attr-lang make_setter!(SetLang, "lang"); // https://html.spec.whatwg.org/multipage/#the-dir-attribute make_enumerated_getter!( Dir, "dir", "ltr" | "rtl" | "auto", missing => "", invalid => "" ); // https://html.spec.whatwg.org/multipage/#the-dir-attribute make_setter!(SetDir, "dir"); // https://html.spec.whatwg.org/multipage/#dom-hidden make_bool_getter!(Hidden, "hidden"); // https://html.spec.whatwg.org/multipage/#dom-hidden make_bool_setter!(SetHidden, "hidden"); // https://html.spec.whatwg.org/multipage/#globaleventhandlers global_event_handlers!(NoOnload); /// fn Dataset(&self, can_gc: CanGc) -> DomRoot { self.dataset.or_init(|| DOMStringMap::new(self, can_gc)) } /// fn GetOnerror(&self, can_gc: CanGc) -> Option> { if self.is_body_or_frameset() { let document = self.owner_document(); if document.has_browsing_context() { document.window().GetOnerror() } else { None } } else { self.upcast::() .get_event_handler_common("error", can_gc) } } /// fn SetOnerror(&self, listener: Option>) { if self.is_body_or_frameset() { let document = self.owner_document(); if document.has_browsing_context() { document.window().SetOnerror(listener) } } else { // special setter for error self.upcast::() .set_error_event_handler("error", listener) } } /// fn GetOnload(&self, can_gc: CanGc) -> Option> { if self.is_body_or_frameset() { let document = self.owner_document(); if document.has_browsing_context() { document.window().GetOnload() } else { None } } else { self.upcast::() .get_event_handler_common("load", can_gc) } } /// fn SetOnload(&self, listener: Option>) { if self.is_body_or_frameset() { let document = self.owner_document(); if document.has_browsing_context() { document.window().SetOnload(listener) } } else { self.upcast::() .set_event_handler_common("load", listener) } } /// fn GetOnblur(&self, can_gc: CanGc) -> Option> { if self.is_body_or_frameset() { let document = self.owner_document(); if document.has_browsing_context() { document.window().GetOnblur() } else { None } } else { self.upcast::() .get_event_handler_common("blur", can_gc) } } /// fn SetOnblur(&self, listener: Option>) { if self.is_body_or_frameset() { let document = self.owner_document(); if document.has_browsing_context() { document.window().SetOnblur(listener) } } else { self.upcast::() .set_event_handler_common("blur", listener) } } /// fn GetOnfocus(&self, can_gc: CanGc) -> Option> { if self.is_body_or_frameset() { let document = self.owner_document(); if document.has_browsing_context() { document.window().GetOnfocus() } else { None } } else { self.upcast::() .get_event_handler_common("focus", can_gc) } } /// fn SetOnfocus(&self, listener: Option>) { if self.is_body_or_frameset() { let document = self.owner_document(); if document.has_browsing_context() { document.window().SetOnfocus(listener) } } else { self.upcast::() .set_event_handler_common("focus", listener) } } /// fn GetOnresize(&self, can_gc: CanGc) -> Option> { if self.is_body_or_frameset() { let document = self.owner_document(); if document.has_browsing_context() { document.window().GetOnresize() } else { None } } else { self.upcast::() .get_event_handler_common("resize", can_gc) } } /// fn SetOnresize(&self, listener: Option>) { if self.is_body_or_frameset() { let document = self.owner_document(); if document.has_browsing_context() { document.window().SetOnresize(listener) } } else { self.upcast::() .set_event_handler_common("resize", listener) } } /// fn GetOnscroll(&self, can_gc: CanGc) -> Option> { if self.is_body_or_frameset() { let document = self.owner_document(); if document.has_browsing_context() { document.window().GetOnscroll() } else { None } } else { self.upcast::() .get_event_handler_common("scroll", can_gc) } } /// fn SetOnscroll(&self, listener: Option>) { if self.is_body_or_frameset() { let document = self.owner_document(); if document.has_browsing_context() { document.window().SetOnscroll(listener) } } else { self.upcast::() .set_event_handler_common("scroll", listener) } } /// fn Itemtypes(&self) -> Option> { let atoms = self .element .get_tokenlist_attribute(&local_name!("itemtype")); if atoms.is_empty() { return None; } #[expect(clippy::mutable_key_type)] // See `impl Hash for DOMString`. let mut item_attr_values = HashSet::new(); for attr_value in &atoms { item_attr_values.insert(DOMString::from(String::from(attr_value.trim()))); } Some(item_attr_values.into_iter().collect()) } /// fn PropertyNames(&self) -> Option> { let atoms = self .element .get_tokenlist_attribute(&local_name!("itemprop")); if atoms.is_empty() { return None; } #[expect(clippy::mutable_key_type)] // See `impl Hash for DOMString`. let mut item_attr_values = HashSet::new(); for attr_value in &atoms { item_attr_values.insert(DOMString::from(String::from(attr_value.trim()))); } Some(item_attr_values.into_iter().collect()) } /// fn Click(&self, can_gc: CanGc) { let element = self.as_element(); if element.disabled_state() { return; } if element.click_in_progress() { return; } element.set_click_in_progress(true); self.upcast::() .fire_synthetic_pointer_event_not_trusted(atom!("click"), can_gc); element.set_click_in_progress(false); } /// fn Focus(&self, options: &FocusOptions, can_gc: CanGc) { // 1. If the allow focus steps given this's node document return false, then return. // TODO: Implement this. // 2. Run the focusing steps for this. if !self.upcast::().run_the_focusing_steps(None, can_gc) { // The specification seems to imply we should scroll into view even if this element // is not a focusable area. No browser does this, so we return early in that case. // See https://github.com/whatwg/html/issues/12231. return; } // > 3. If options["focusVisible"] is true, or does not exist but in an // > implementation-defined way the user agent determines it would be best to do so, // > then indicate focus. TODO: Implement this. // > 4. If options["preventScroll"] is false, then scroll a target into view given this, // > "auto", "center", and "center". if !options.preventScroll { let scroll_axis = ScrollAxisState { position: ScrollLogicalPosition::Center, requirement: ScrollRequirement::IfNotVisible, }; self.upcast::().scroll_into_view_with_options( ScrollBehavior::Smooth, scroll_axis, scroll_axis, None, None, ); } } /// fn Blur(&self, can_gc: CanGc) { // TODO: Run the unfocusing steps. Focus the top-level document, not // the current document. if !self.as_element().focus_state() { return; } // self.owner_document() .focus_handler() .focus(FocusableArea::Viewport, can_gc); } /// #[expect(unsafe_code)] fn ScrollParent(&self) -> Option> { self.owner_window() .scroll_container_query( Some(self.upcast()), ScrollContainerQueryFlags::ForScrollParent, ) .and_then(|response| match response { ScrollContainerResponse::Viewport(_) => self.owner_document().GetScrollingElement(), ScrollContainerResponse::Element(parent_node_address, _) => { let node = unsafe { from_untrusted_node_address(parent_node_address) }; DomRoot::downcast(node) }, }) } /// fn GetOffsetParent(&self) -> Option> { if self.is::() || self.element.is_root() { return None; } let node = self.upcast::(); let window = self.owner_window(); let (element, _) = window.offset_parent_query(node); element } /// fn OffsetTop(&self) -> i32 { if self.is_body_element() { return 0; } let node = self.upcast::(); let window = self.owner_window(); let (_, rect) = window.offset_parent_query(node); rect.origin.y.to_nearest_px() } /// fn OffsetLeft(&self) -> i32 { if self.is_body_element() { return 0; } let node = self.upcast::(); let window = self.owner_window(); let (_, rect) = window.offset_parent_query(node); rect.origin.x.to_nearest_px() } /// fn OffsetWidth(&self) -> i32 { let node = self.upcast::(); let window = self.owner_window(); let (_, rect) = window.offset_parent_query(node); rect.size.width.to_nearest_px() } /// fn OffsetHeight(&self) -> i32 { let node = self.upcast::(); let window = self.owner_window(); let (_, rect) = window.offset_parent_query(node); rect.size.height.to_nearest_px() } /// fn InnerText(&self) -> DOMString { self.get_inner_outer_text() } /// fn SetInnerText(&self, cx: &mut JSContext, input: DOMString) { self.set_inner_text(cx, input) } /// fn GetOuterText(&self) -> Fallible { Ok(self.get_inner_outer_text()) } /// fn SetOuterText(&self, cx: &mut JSContext, input: DOMString) -> Fallible<()> { // Step 1: If this's parent is null, then throw a "NoModificationAllowedError" DOMException. let Some(parent) = self.upcast::().GetParentNode() else { return Err(Error::NoModificationAllowed(None)); }; let node = self.upcast::(); let document = self.owner_document(); // Step 2: Let next be this's next sibling. let next = node.GetNextSibling(); // Step 3: Let previous be this's previous sibling. let previous = node.GetPreviousSibling(); // Step 4: Let fragment be the rendered text fragment for the given value given this's node // document. let fragment = self.rendered_text_fragment(cx, input); // Step 5: If fragment has no children, then append a new Text node whose data is the empty // string and node document is this's node document to fragment. if fragment.upcast::().children_count() == 0 { let text_node = Text::new(cx, DOMString::from("".to_owned()), &document); fragment .upcast::() .AppendChild(cx, text_node.upcast())?; } // Step 6: Replace this with fragment within this's parent. parent.ReplaceChild(cx, fragment.upcast(), node)?; // Step 7: If next is non-null and next's previous sibling is a Text node, then merge with // the next text node given next's previous sibling. if let Some(next_sibling) = next { if let Some(node) = next_sibling.GetPreviousSibling() { Self::merge_with_the_next_text_node(cx, node); } } // Step 8: If previous is a Text node, then merge with the next text node given previous. if let Some(previous) = previous { Self::merge_with_the_next_text_node(cx, previous) } Ok(()) } /// fn Translate(&self) -> bool { self.as_element().is_translate_enabled() } /// fn SetTranslate(&self, yesno: bool, can_gc: CanGc) { self.as_element().set_string_attribute( &html5ever::local_name!("translate"), match yesno { true => DOMString::from("yes"), false => DOMString::from("no"), }, can_gc, ); } // https://html.spec.whatwg.org/multipage/#dom-contenteditable make_enumerated_getter!( ContentEditable, "contenteditable", "true" | "false" | "plaintext-only", missing => "inherit", invalid => "inherit", empty => "true" ); /// fn SetContentEditable(&self, value: DOMString, can_gc: CanGc) -> ErrorResult { let lower_value = value.to_ascii_lowercase(); let attr_name = &local_name!("contenteditable"); match lower_value.as_ref() { // > On setting, if the new value is an ASCII case-insensitive match for the string "inherit", then the content attribute must be removed, "inherit" => { self.element.remove_attribute_by_name(attr_name, can_gc); }, // > if the new value is an ASCII case-insensitive match for the string "true", then the content attribute must be set to the string "true", // > if the new value is an ASCII case-insensitive match for the string "plaintext-only", then the content attribute must be set to the string "plaintext-only", // > if the new value is an ASCII case-insensitive match for the string "false", then the content attribute must be set to the string "false", "true" | "false" | "plaintext-only" => { self.element .set_attribute(attr_name, AttrValue::String(lower_value), can_gc); }, // > and otherwise the attribute setter must throw a "SyntaxError" DOMException. _ => return Err(Error::Syntax(None)), }; Ok(()) } /// fn IsContentEditable(&self) -> bool { // > The isContentEditable IDL attribute, on getting, must return true if the element is either an editing host or editable, and false otherwise. self.upcast::().is_editable_or_editing_host() } /// fn AttachInternals(&self, can_gc: CanGc) -> Fallible> { // Step 1: If this's is value is not null, then throw a "NotSupportedError" DOMException if self.element.get_is().is_some() { return Err(Error::NotSupported(None)); } // Step 2: Let definition be the result of looking up a custom element definition // Note: the element can pass this check without yet being a custom // element, as long as there is a registered definition // that could upgrade it to one later. let registry = self.owner_window().CustomElements(); let definition = registry.lookup_definition(self.as_element().local_name(), None); // Step 3: If definition is null, then throw an "NotSupportedError" DOMException let definition = match definition { Some(definition) => definition, None => return Err(Error::NotSupported(None)), }; // Step 4: If definition's disable internals is true, then throw a "NotSupportedError" DOMException if definition.disable_internals { return Err(Error::NotSupported(None)); } // Step 5: If this's attached internals is non-null, then throw an "NotSupportedError" DOMException let internals = self.element.ensure_element_internals(can_gc); if internals.attached() { return Err(Error::NotSupported(None)); } // Step 6: If this's custom element state is not "precustomized" or "custom", // then throw a "NotSupportedError" DOMException. if !matches!( self.element.get_custom_element_state(), CustomElementState::Precustomized | CustomElementState::Custom ) { return Err(Error::NotSupported(None)); } if self.is_form_associated_custom_element() { self.element.init_state_for_internals(); } // Step 6-7: Set this's attached internals to a new ElementInternals instance internals.set_attached(); Ok(internals) } /// fn Nonce(&self) -> DOMString { self.as_element().nonce_value().into() } /// fn SetNonce(&self, value: DOMString) { self.as_element() .update_nonce_internal_slot(value.to_string()) } /// fn Autofocus(&self) -> bool { self.element.has_attribute(&local_name!("autofocus")) } /// fn SetAutofocus(&self, autofocus: bool, can_gc: CanGc) { self.element .set_bool_attribute(&local_name!("autofocus"), autofocus, can_gc); } /// fn TabIndex(&self) -> i32 { self.element.tab_index() } /// fn SetTabIndex(&self, tab_index: i32, can_gc: CanGc) { self.element .set_int_attribute(&local_name!("tabindex"), tab_index, can_gc); } // https://html.spec.whatwg.org/multipage/#dom-accesskey make_getter!(AccessKey, "accesskey"); // https://html.spec.whatwg.org/multipage/#dom-accesskey make_setter!(SetAccessKey, "accesskey"); /// fn AccessKeyLabel(&self) -> DOMString { // The accessKeyLabel IDL attribute must return a string that represents the element's // assigned access key, if any. If the element does not have one, then the IDL attribute // must return the empty string. if !self.element.has_attribute(&local_name!("accesskey")) { return Default::default(); } let access_key_string = self .element .get_string_attribute(&local_name!("accesskey")) .to_string(); #[cfg(target_os = "macos")] let access_key_label = format!("⌃⌥{access_key_string}"); #[cfg(not(target_os = "macos"))] let access_key_label = format!("Alt+Shift+{access_key_string}"); access_key_label.into() } } fn append_text_node_to_fragment( cx: &mut JSContext, document: &Document, fragment: &DocumentFragment, text: String, ) { let text = Text::new(cx, DOMString::from(text), document); fragment .upcast::() .AppendChild(cx, text.upcast()) .unwrap(); } impl HTMLElement { /// pub(crate) fn is_labelable_element(&self) -> bool { match self.upcast::().type_id() { NodeTypeId::Element(ElementTypeId::HTMLElement(type_id)) => match type_id { HTMLElementTypeId::HTMLInputElement => !matches!( *self.downcast::().unwrap().input_type(), InputType::Hidden(_) ), HTMLElementTypeId::HTMLButtonElement | HTMLElementTypeId::HTMLMeterElement | HTMLElementTypeId::HTMLOutputElement | HTMLElementTypeId::HTMLProgressElement | HTMLElementTypeId::HTMLSelectElement | HTMLElementTypeId::HTMLTextAreaElement => true, _ => self.is_form_associated_custom_element(), }, _ => false, } } /// pub(crate) fn is_form_associated_custom_element(&self) -> bool { if let Some(definition) = self.as_element().get_custom_element_definition() { definition.is_autonomous() && definition.form_associated } else { false } } /// pub(crate) fn is_listed_element(&self) -> bool { match self.upcast::().type_id() { NodeTypeId::Element(ElementTypeId::HTMLElement(type_id)) => match type_id { HTMLElementTypeId::HTMLButtonElement | HTMLElementTypeId::HTMLFieldSetElement | HTMLElementTypeId::HTMLInputElement | HTMLElementTypeId::HTMLObjectElement | HTMLElementTypeId::HTMLOutputElement | HTMLElementTypeId::HTMLSelectElement | HTMLElementTypeId::HTMLTextAreaElement => true, _ => self.is_form_associated_custom_element(), }, _ => false, } } /// pub(crate) fn is_body_element(&self) -> bool { let self_node = self.upcast::(); self_node.GetParentNode().is_some_and(|parent| { let parent_node = parent.upcast::(); (self_node.is::() || self_node.is::()) && parent_node.is::() && self_node .preceding_siblings() .all(|n| !n.is::() && !n.is::()) }) } /// pub(crate) fn is_submittable_element(&self) -> bool { match self.upcast::().type_id() { NodeTypeId::Element(ElementTypeId::HTMLElement(type_id)) => match type_id { HTMLElementTypeId::HTMLButtonElement | HTMLElementTypeId::HTMLInputElement | HTMLElementTypeId::HTMLSelectElement | HTMLElementTypeId::HTMLTextAreaElement => true, _ => self.is_form_associated_custom_element(), }, _ => false, } } // https://html.spec.whatwg.org/multipage/#dom-lfe-labels // This gets the nth label in tree order. pub(crate) fn label_at(&self, index: u32) -> Option> { let element = self.as_element(); // Traverse entire tree for