| /* 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, local_name, ns}; |
| use js::rust::HandleObject; |
| use layout_api::{QueryMsg, ScrollContainerQueryType, ScrollContainerResponse}; |
| use script_bindings::codegen::GenericBindings::DocumentBinding::DocumentMethods; |
| 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::cssstyledeclaration::{CSSModificationAccess, CSSStyleDeclaration, CSSStyleOwner}; |
| use crate::dom::customelementregistry::{CallbackReaction, CustomElementState}; |
| use crate::dom::document::{Document, FocusInitiator}; |
| use crate::dom::documentfragment::DocumentFragment; |
| use crate::dom::domstringmap::DOMStringMap; |
| use crate::dom::element::{AttributeMutation, Element}; |
| 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::htmlbrelement::HTMLBRElement; |
| 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::htmlinputelement::{HTMLInputElement, InputType}; |
| use crate::dom::html::htmllabelelement::HTMLLabelElement; |
| use crate::dom::html::htmltextareaelement::HTMLTextAreaElement; |
| use crate::dom::node::{ |
| BindContext, Node, NodeTraits, ShadowIncluding, UnbindContext, from_untrusted_node_address, |
| }; |
| 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<CSSStyleDeclaration>, |
| dataset: MutNullableDom<DOMStringMap>, |
| } |
| |
| impl HTMLElement { |
| pub(crate) fn new_inherited( |
| tag_name: LocalName, |
| prefix: Option<Prefix>, |
| 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<Prefix>, |
| document: &Document, |
| ) -> HTMLElement { |
| HTMLElement { |
| element: Element::new_inherited_with_state( |
| state, |
| tag_name, |
| ns!(html), |
| prefix, |
| document, |
| ), |
| style_decl: Default::default(), |
| dataset: Default::default(), |
| } |
| } |
| |
| #[cfg_attr(crown, allow(crown::unrooted_must_root))] |
| pub(crate) fn new( |
| local_name: LocalName, |
| prefix: Option<Prefix>, |
| document: &Document, |
| proto: Option<HandleObject>, |
| can_gc: CanGc, |
| ) -> DomRoot<HTMLElement> { |
| Node::reflect_node_with_proto( |
| Box::new(HTMLElement::new_inherited(local_name, prefix, document)), |
| document, |
| proto, |
| can_gc, |
| ) |
| } |
| |
| fn is_body_or_frameset(&self) -> bool { |
| let eventtarget = self.upcast::<EventTarget>(); |
| eventtarget.is::<HTMLBodyElement>() || eventtarget.is::<HTMLFrameSetElement>() |
| } |
| |
| /// Calls into the layout engine to generate a plain text representation |
| /// of a [`HTMLElement`] as specified when getting the `.innerText` or |
| /// `.outerText` in JavaScript.` |
| /// |
| /// <https://html.spec.whatwg.org/multipage/#get-the-text-steps> |
| pub(crate) fn get_inner_outer_text(&self) -> DOMString { |
| let node = self.upcast::<Node>(); |
| 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) |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#set-the-inner-text-steps> |
| pub(crate) fn set_inner_text(&self, input: DOMString, can_gc: CanGc) { |
| // Step 1: Let fragment be the rendered text fragment for value given element's node |
| // document. |
| let fragment = self.rendered_text_fragment(input, can_gc); |
| |
| // Step 2: Replace all with fragment within element. |
| Node::replace_all(Some(fragment.upcast()), self.upcast::<Node>(), can_gc); |
| } |
| } |
| |
| impl HTMLElementMethods<crate::DomTypeHolder> for HTMLElement { |
| // https://html.spec.whatwg.org/multipage/#the-style-attribute |
| fn Style(&self, can_gc: CanGc) -> DomRoot<CSSStyleDeclaration> { |
| 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); |
| |
| // https://html.spec.whatwg.org/multipage/#dom-dataset |
| fn Dataset(&self, can_gc: CanGc) -> DomRoot<DOMStringMap> { |
| self.dataset.or_init(|| DOMStringMap::new(self, can_gc)) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#handler-onerror |
| fn GetOnerror(&self, can_gc: CanGc) -> Option<Rc<OnErrorEventHandlerNonNull>> { |
| if self.is_body_or_frameset() { |
| let document = self.owner_document(); |
| if document.has_browsing_context() { |
| document.window().GetOnerror() |
| } else { |
| None |
| } |
| } else { |
| self.upcast::<EventTarget>() |
| .get_event_handler_common("error", can_gc) |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#handler-onerror |
| fn SetOnerror(&self, listener: Option<Rc<OnErrorEventHandlerNonNull>>) { |
| 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::<EventTarget>() |
| .set_error_event_handler("error", listener) |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#handler-onload |
| fn GetOnload(&self, can_gc: CanGc) -> Option<Rc<EventHandlerNonNull>> { |
| if self.is_body_or_frameset() { |
| let document = self.owner_document(); |
| if document.has_browsing_context() { |
| document.window().GetOnload() |
| } else { |
| None |
| } |
| } else { |
| self.upcast::<EventTarget>() |
| .get_event_handler_common("load", can_gc) |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#handler-onload |
| fn SetOnload(&self, listener: Option<Rc<EventHandlerNonNull>>) { |
| if self.is_body_or_frameset() { |
| let document = self.owner_document(); |
| if document.has_browsing_context() { |
| document.window().SetOnload(listener) |
| } |
| } else { |
| self.upcast::<EventTarget>() |
| .set_event_handler_common("load", listener) |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#handler-onblur |
| fn GetOnblur(&self, can_gc: CanGc) -> Option<Rc<EventHandlerNonNull>> { |
| if self.is_body_or_frameset() { |
| let document = self.owner_document(); |
| if document.has_browsing_context() { |
| document.window().GetOnblur() |
| } else { |
| None |
| } |
| } else { |
| self.upcast::<EventTarget>() |
| .get_event_handler_common("blur", can_gc) |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#handler-onblur |
| fn SetOnblur(&self, listener: Option<Rc<EventHandlerNonNull>>) { |
| if self.is_body_or_frameset() { |
| let document = self.owner_document(); |
| if document.has_browsing_context() { |
| document.window().SetOnblur(listener) |
| } |
| } else { |
| self.upcast::<EventTarget>() |
| .set_event_handler_common("blur", listener) |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#handler-onfocus |
| fn GetOnfocus(&self, can_gc: CanGc) -> Option<Rc<EventHandlerNonNull>> { |
| if self.is_body_or_frameset() { |
| let document = self.owner_document(); |
| if document.has_browsing_context() { |
| document.window().GetOnfocus() |
| } else { |
| None |
| } |
| } else { |
| self.upcast::<EventTarget>() |
| .get_event_handler_common("focus", can_gc) |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#handler-onfocus |
| fn SetOnfocus(&self, listener: Option<Rc<EventHandlerNonNull>>) { |
| if self.is_body_or_frameset() { |
| let document = self.owner_document(); |
| if document.has_browsing_context() { |
| document.window().SetOnfocus(listener) |
| } |
| } else { |
| self.upcast::<EventTarget>() |
| .set_event_handler_common("focus", listener) |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#handler-onresize |
| fn GetOnresize(&self, can_gc: CanGc) -> Option<Rc<EventHandlerNonNull>> { |
| if self.is_body_or_frameset() { |
| let document = self.owner_document(); |
| if document.has_browsing_context() { |
| document.window().GetOnresize() |
| } else { |
| None |
| } |
| } else { |
| self.upcast::<EventTarget>() |
| .get_event_handler_common("resize", can_gc) |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#handler-onresize |
| fn SetOnresize(&self, listener: Option<Rc<EventHandlerNonNull>>) { |
| if self.is_body_or_frameset() { |
| let document = self.owner_document(); |
| if document.has_browsing_context() { |
| document.window().SetOnresize(listener) |
| } |
| } else { |
| self.upcast::<EventTarget>() |
| .set_event_handler_common("resize", listener) |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#handler-onscroll |
| fn GetOnscroll(&self, can_gc: CanGc) -> Option<Rc<EventHandlerNonNull>> { |
| if self.is_body_or_frameset() { |
| let document = self.owner_document(); |
| if document.has_browsing_context() { |
| document.window().GetOnscroll() |
| } else { |
| None |
| } |
| } else { |
| self.upcast::<EventTarget>() |
| .get_event_handler_common("scroll", can_gc) |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#handler-onscroll |
| fn SetOnscroll(&self, listener: Option<Rc<EventHandlerNonNull>>) { |
| if self.is_body_or_frameset() { |
| let document = self.owner_document(); |
| if document.has_browsing_context() { |
| document.window().SetOnscroll(listener) |
| } |
| } else { |
| self.upcast::<EventTarget>() |
| .set_event_handler_common("scroll", listener) |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#attr-itemtype |
| fn Itemtypes(&self) -> Option<Vec<DOMString>> { |
| let atoms = self |
| .element |
| .get_tokenlist_attribute(&local_name!("itemtype")); |
| |
| if atoms.is_empty() { |
| return None; |
| } |
| |
| 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()) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#names:-the-itemprop-attribute |
| fn PropertyNames(&self) -> Option<Vec<DOMString>> { |
| let atoms = self |
| .element |
| .get_tokenlist_attribute(&local_name!("itemprop")); |
| |
| if atoms.is_empty() { |
| return None; |
| } |
| |
| 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()) |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-click> |
| 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::<Node>() |
| .fire_synthetic_pointer_event_not_trusted(DOMString::from("click"), can_gc); |
| element.set_click_in_progress(false); |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-focus> |
| fn Focus(&self, options: &FocusOptions, can_gc: CanGc) { |
| // TODO: Mark the element as locked for focus and run the focusing steps. |
| // <https://html.spec.whatwg.org/multipage/#focusing-steps> |
| let document = self.owner_document(); |
| document.request_focus_with_options( |
| Some(self.upcast()), |
| FocusInitiator::Local, |
| FocusOptions { |
| preventScroll: options.preventScroll, |
| }, |
| can_gc, |
| ); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-blur |
| 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; |
| } |
| // https://html.spec.whatwg.org/multipage/#unfocusing-steps |
| let document = self.owner_document(); |
| document.request_focus(None, FocusInitiator::Local, can_gc); |
| } |
| |
| /// <https://drafts.csswg.org/cssom-view/#dom-htmlelement-scrollparent> |
| #[allow(unsafe_code)] |
| fn GetScrollParent(&self) -> Option<DomRoot<Element>> { |
| self.owner_window() |
| .scroll_container_query(self.upcast(), ScrollContainerQueryType::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) |
| }, |
| }) |
| } |
| |
| /// <https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetparent> |
| fn GetOffsetParent(&self) -> Option<DomRoot<Element>> { |
| if self.is_body_element() || self.is::<HTMLHtmlElement>() { |
| return None; |
| } |
| |
| let node = self.upcast::<Node>(); |
| let window = self.owner_window(); |
| let (element, _) = window.offset_parent_query(node); |
| |
| element |
| } |
| |
| // https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsettop |
| fn OffsetTop(&self) -> i32 { |
| if self.is_body_element() { |
| return 0; |
| } |
| |
| let node = self.upcast::<Node>(); |
| let window = self.owner_window(); |
| let (_, rect) = window.offset_parent_query(node); |
| |
| rect.origin.y.to_nearest_px() |
| } |
| |
| // https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetleft |
| fn OffsetLeft(&self) -> i32 { |
| if self.is_body_element() { |
| return 0; |
| } |
| |
| let node = self.upcast::<Node>(); |
| let window = self.owner_window(); |
| let (_, rect) = window.offset_parent_query(node); |
| |
| rect.origin.x.to_nearest_px() |
| } |
| |
| // https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetwidth |
| fn OffsetWidth(&self) -> i32 { |
| let node = self.upcast::<Node>(); |
| let window = self.owner_window(); |
| let (_, rect) = window.offset_parent_query(node); |
| |
| rect.size.width.to_nearest_px() |
| } |
| |
| // https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetheight |
| fn OffsetHeight(&self) -> i32 { |
| let node = self.upcast::<Node>(); |
| let window = self.owner_window(); |
| let (_, rect) = window.offset_parent_query(node); |
| |
| rect.size.height.to_nearest_px() |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#the-innertext-idl-attribute> |
| fn InnerText(&self) -> DOMString { |
| self.get_inner_outer_text() |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#set-the-inner-text-steps> |
| fn SetInnerText(&self, input: DOMString, can_gc: CanGc) { |
| self.set_inner_text(input, can_gc) |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-outertext> |
| fn GetOuterText(&self) -> Fallible<DOMString> { |
| Ok(self.get_inner_outer_text()) |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#the-innertext-idl-attribute:dom-outertext-2> |
| fn SetOuterText(&self, input: DOMString, can_gc: CanGc) -> Fallible<()> { |
| // Step 1: If this's parent is null, then throw a "NoModificationAllowedError" DOMException. |
| let Some(parent) = self.upcast::<Node>().GetParentNode() else { |
| return Err(Error::NoModificationAllowed); |
| }; |
| |
| let node = self.upcast::<Node>(); |
| 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(input, can_gc); |
| |
| // 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::<Node>().children_count() == 0 { |
| let text_node = Text::new(DOMString::from("".to_owned()), &document, can_gc); |
| |
| fragment |
| .upcast::<Node>() |
| .AppendChild(text_node.upcast(), can_gc)?; |
| } |
| |
| // Step 6: Replace this with fragment within this's parent. |
| parent.ReplaceChild(fragment.upcast(), node, can_gc)?; |
| |
| // 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(node, can_gc); |
| } |
| } |
| |
| // 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(previous, can_gc) |
| } |
| |
| Ok(()) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-translate |
| fn Translate(&self) -> bool { |
| self.as_element().is_translate_enabled() |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-translate |
| 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 |
| fn ContentEditable(&self) -> DOMString { |
| // TODO: https://github.com/servo/servo/issues/12776 |
| self.as_element() |
| .get_attribute(&ns!(), &local_name!("contenteditable")) |
| .map(|attr| DOMString::from(&**attr.value())) |
| .unwrap_or_else(|| DOMString::from("inherit")) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-contenteditable |
| fn SetContentEditable(&self, _: DOMString) { |
| // TODO: https://github.com/servo/servo/issues/12776 |
| warn!("The contentEditable attribute is not implemented yet"); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-contenteditable |
| fn IsContentEditable(&self) -> bool { |
| // TODO: https://github.com/servo/servo/issues/12776 |
| false |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage#dom-attachinternals> |
| fn AttachInternals(&self, can_gc: CanGc) -> Fallible<DomRoot<ElementInternals>> { |
| let element = self.as_element(); |
| // Step 1: If this's is value is not null, then throw a "NotSupportedError" DOMException |
| if element.get_is().is_some() { |
| return Err(Error::NotSupported); |
| } |
| |
| // 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), |
| }; |
| |
| // Step 4: If definition's disable internals is true, then throw a "NotSupportedError" DOMException |
| if definition.disable_internals { |
| return Err(Error::NotSupported); |
| } |
| |
| // Step 5: If this's attached internals is non-null, then throw an "NotSupportedError" DOMException |
| let internals = element.ensure_element_internals(can_gc); |
| if internals.attached() { |
| return Err(Error::NotSupported); |
| } |
| |
| // Step 6: If this's custom element state is not "precustomized" or "custom", |
| // then throw a "NotSupportedError" DOMException. |
| if !matches!( |
| element.get_custom_element_state(), |
| CustomElementState::Precustomized | CustomElementState::Custom |
| ) { |
| return Err(Error::NotSupported); |
| } |
| |
| if self.is_form_associated_custom_element() { |
| element.init_state_for_internals(); |
| } |
| |
| // Step 6-7: Set this's attached internals to a new ElementInternals instance |
| internals.set_attached(); |
| Ok(internals) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-noncedelement-nonce |
| fn Nonce(&self) -> DOMString { |
| self.as_element().nonce_value().into() |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-noncedelement-nonce |
| fn SetNonce(&self, value: DOMString) { |
| self.as_element() |
| .update_nonce_internal_slot(value.to_string()) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-fe-autofocus |
| fn Autofocus(&self) -> bool { |
| self.element.has_attribute(&local_name!("autofocus")) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-fe-autofocus |
| fn SetAutofocus(&self, autofocus: bool, can_gc: CanGc) { |
| self.element |
| .set_bool_attribute(&local_name!("autofocus"), autofocus, can_gc); |
| } |
| } |
| |
| fn append_text_node_to_fragment( |
| document: &Document, |
| fragment: &DocumentFragment, |
| text: String, |
| can_gc: CanGc, |
| ) { |
| let text = Text::new(DOMString::from(text), document, can_gc); |
| fragment |
| .upcast::<Node>() |
| .AppendChild(text.upcast(), can_gc) |
| .unwrap(); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#attr-data-* |
| |
| static DATA_PREFIX: &str = "data-"; |
| static DATA_HYPHEN_SEPARATOR: char = '\x2d'; |
| |
| fn to_snake_case(name: DOMString) -> DOMString { |
| let mut attr_name = String::with_capacity(name.len() + DATA_PREFIX.len()); |
| attr_name.push_str(DATA_PREFIX); |
| for ch in name.chars() { |
| if ch.is_ascii_uppercase() { |
| attr_name.push(DATA_HYPHEN_SEPARATOR); |
| attr_name.push(ch.to_ascii_lowercase()); |
| } else { |
| attr_name.push(ch); |
| } |
| } |
| DOMString::from(attr_name) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#attr-data-* |
| // if this attribute is in snake case with a data- prefix, |
| // this function returns a name converted to camel case |
| // without the data prefix. |
| |
| fn to_camel_case(name: &str) -> Option<DOMString> { |
| if !name.starts_with(DATA_PREFIX) { |
| return None; |
| } |
| let name = &name[5..]; |
| let has_uppercase = name.chars().any(|curr_char| curr_char.is_ascii_uppercase()); |
| if has_uppercase { |
| return None; |
| } |
| let mut result = String::with_capacity(name.len().saturating_sub(DATA_PREFIX.len())); |
| let mut name_chars = name.chars(); |
| while let Some(curr_char) = name_chars.next() { |
| // check for hyphen followed by character |
| if curr_char == DATA_HYPHEN_SEPARATOR { |
| if let Some(next_char) = name_chars.next() { |
| if next_char.is_ascii_lowercase() { |
| result.push(next_char.to_ascii_uppercase()); |
| } else { |
| result.push(curr_char); |
| result.push(next_char); |
| } |
| } else { |
| result.push(curr_char); |
| } |
| } else { |
| result.push(curr_char); |
| } |
| } |
| Some(DOMString::from(result)) |
| } |
| |
| impl HTMLElement { |
| pub(crate) fn set_custom_attr( |
| &self, |
| name: DOMString, |
| value: DOMString, |
| can_gc: CanGc, |
| ) -> ErrorResult { |
| if name |
| .chars() |
| .skip_while(|&ch| ch != '\u{2d}') |
| .nth(1) |
| .is_some_and(|ch| ch.is_ascii_lowercase()) |
| { |
| return Err(Error::Syntax(None)); |
| } |
| self.as_element() |
| .set_custom_attribute(to_snake_case(name), value, can_gc) |
| } |
| |
| pub(crate) fn get_custom_attr(&self, local_name: DOMString) -> Option<DOMString> { |
| // FIXME(ajeffrey): Convert directly from DOMString to LocalName |
| let local_name = LocalName::from(to_snake_case(local_name)); |
| self.as_element() |
| .get_attribute(&ns!(), &local_name) |
| .map(|attr| { |
| DOMString::from(&**attr.value()) // FIXME(ajeffrey): Convert directly from AttrValue to DOMString |
| }) |
| } |
| |
| pub(crate) fn delete_custom_attr(&self, local_name: DOMString, can_gc: CanGc) { |
| // FIXME(ajeffrey): Convert directly from DOMString to LocalName |
| let local_name = LocalName::from(to_snake_case(local_name)); |
| self.as_element() |
| .remove_attribute(&ns!(), &local_name, can_gc); |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#category-label> |
| pub(crate) fn is_labelable_element(&self) -> bool { |
| match self.upcast::<Node>().type_id() { |
| NodeTypeId::Element(ElementTypeId::HTMLElement(type_id)) => match type_id { |
| HTMLElementTypeId::HTMLInputElement => { |
| self.downcast::<HTMLInputElement>().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, |
| } |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#form-associated-custom-element> |
| 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 |
| } |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#category-listed> |
| pub(crate) fn is_listed_element(&self) -> bool { |
| match self.upcast::<Node>().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, |
| } |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#the-body-element-2> |
| pub(crate) fn is_body_element(&self) -> bool { |
| let self_node = self.upcast::<Node>(); |
| self_node.GetParentNode().is_some_and(|parent| { |
| let parent_node = parent.upcast::<Node>(); |
| (self_node.is::<HTMLBodyElement>() || self_node.is::<HTMLFrameSetElement>()) && |
| parent_node.is::<HTMLHtmlElement>() && |
| self_node |
| .preceding_siblings() |
| .all(|n| !n.is::<HTMLBodyElement>() && !n.is::<HTMLFrameSetElement>()) |
| }) |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#category-submit> |
| pub(crate) fn is_submittable_element(&self) -> bool { |
| match self.upcast::<Node>().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, |
| } |
| } |
| |
| pub(crate) fn supported_prop_names_custom_attr(&self) -> Vec<DOMString> { |
| let element = self.as_element(); |
| element |
| .attrs() |
| .iter() |
| .filter_map(|attr| { |
| let raw_name = attr.local_name(); |
| to_camel_case(raw_name) |
| }) |
| .collect() |
| } |
| |
| // 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<DomRoot<Node>> { |
| let element = self.as_element(); |
| |
| // Traverse entire tree for <label> elements that have |
| // this as their control. |
| // There is room for performance optimization, as we don't need |
| // the actual result of GetControl, only whether the result |
| // would match self. |
| // (Even more room for performance optimization: do what |
| // nodelist ChildrenList does and keep a mutation-aware cursor |
| // around; this may be hard since labels need to keep working |
| // even as they get detached into a subtree and reattached to |
| // a document.) |
| let root_element = element.root_element(); |
| let root_node = root_element.upcast::<Node>(); |
| root_node |
| .traverse_preorder(ShadowIncluding::No) |
| .filter_map(DomRoot::downcast::<HTMLLabelElement>) |
| .filter(|elem| match elem.GetControl() { |
| Some(control) => &*control == self, |
| _ => false, |
| }) |
| .nth(index as usize) |
| .map(|n| DomRoot::from_ref(n.upcast::<Node>())) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-lfe-labels |
| // This counts the labels of the element, to support NodeList::Length |
| pub(crate) fn labels_count(&self) -> u32 { |
| // see label_at comments about performance |
| let element = self.as_element(); |
| let root_element = element.root_element(); |
| let root_node = root_element.upcast::<Node>(); |
| root_node |
| .traverse_preorder(ShadowIncluding::No) |
| .filter_map(DomRoot::downcast::<HTMLLabelElement>) |
| .filter(|elem| match elem.GetControl() { |
| Some(control) => &*control == self, |
| _ => false, |
| }) |
| .count() as u32 |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#the-directionality. |
| // returns Some if can infer direction by itself or from child nodes |
| // returns None if requires to go up to parent |
| pub(crate) fn directionality(&self) -> Option<String> { |
| let element_direction: &str = &self.Dir(); |
| |
| if element_direction == "ltr" { |
| return Some("ltr".to_owned()); |
| } |
| |
| if element_direction == "rtl" { |
| return Some("rtl".to_owned()); |
| } |
| |
| if let Some(input) = self.downcast::<HTMLInputElement>() { |
| if input.input_type() == InputType::Tel { |
| return Some("ltr".to_owned()); |
| } |
| } |
| |
| if element_direction == "auto" { |
| if let Some(directionality) = self |
| .downcast::<HTMLInputElement>() |
| .and_then(|input| input.auto_directionality()) |
| { |
| return Some(directionality); |
| } |
| |
| if let Some(area) = self.downcast::<HTMLTextAreaElement>() { |
| return Some(area.auto_directionality()); |
| } |
| } |
| |
| // TODO(NeverHappened): Implement condition |
| // If the element's dir attribute is in the auto state OR |
| // If the element is a bdi element and the dir attribute is not in a defined state |
| // (i.e. it is not present or has an invalid value) |
| // Requires bdi element implementation (https://html.spec.whatwg.org/multipage/#the-bdi-element) |
| |
| None |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#the-summary-element:activation-behaviour |
| pub(crate) fn summary_activation_behavior(&self) { |
| debug_assert!(self.as_element().local_name() == &local_name!("summary")); |
| |
| // Step 1. If this summary element is not the summary for its parent details, then return. |
| if !self.is_a_summary_for_its_parent_details() { |
| return; |
| } |
| |
| // Step 2. Let parent be this summary element's parent. |
| let parent = if self.is_implicit_summary_element() { |
| DomRoot::downcast::<HTMLDetailsElement>(self.containing_shadow_root().unwrap().Host()) |
| .unwrap() |
| } else { |
| self.upcast::<Node>() |
| .GetParentNode() |
| .and_then(DomRoot::downcast::<HTMLDetailsElement>) |
| .unwrap() |
| }; |
| |
| // Step 3. If the open attribute is present on parent, then remove it. |
| // Otherwise, set parent's open attribute to the empty string. |
| parent.toggle(); |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#summary-for-its-parent-details> |
| fn is_a_summary_for_its_parent_details(&self) -> bool { |
| if self.is_implicit_summary_element() { |
| return true; |
| } |
| |
| // Step 1. If this summary element has no parent, then return false. |
| // Step 2. Let parent be this summary element's parent. |
| let Some(parent) = self.upcast::<Node>().GetParentNode() else { |
| return false; |
| }; |
| |
| // Step 3. If parent is not a details element, then return false. |
| let Some(details) = parent.downcast::<HTMLDetailsElement>() else { |
| return false; |
| }; |
| |
| // Step 4. If parent's first summary element child is not this summary |
| // element, then return false. |
| // Step 5. Return true. |
| details |
| .find_corresponding_summary_element() |
| .is_some_and(|summary| &*summary == self.upcast()) |
| } |
| |
| /// Whether or not this is an implicitly generated `<summary>` |
| /// element for a UA `<details>` shadow tree |
| fn is_implicit_summary_element(&self) -> bool { |
| // Note that non-implicit summary elements are not actually inside |
| // the UA shadow tree, they're only assigned to a slot inside it. |
| // Therefore they don't cause false positives here |
| self.containing_shadow_root() |
| .as_deref() |
| .map(ShadowRoot::Host) |
| .is_some_and(|host| host.is::<HTMLDetailsElement>()) |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#rendered-text-fragment> |
| fn rendered_text_fragment(&self, input: DOMString, can_gc: CanGc) -> DomRoot<DocumentFragment> { |
| // Step 1: Let fragment be a new DocumentFragment whose node document is document. |
| let document = self.owner_document(); |
| let fragment = DocumentFragment::new(&document, can_gc); |
| |
| // Step 2: Let position be a position variable for input, initially pointing at the start |
| // of input. |
| let mut position = input.chars().peekable(); |
| |
| // Step 3: Let text be the empty string. |
| let mut text = String::new(); |
| |
| // Step 4 |
| while let Some(ch) = position.next() { |
| match ch { |
| // While position is not past the end of input, and the code point at position is |
| // either U+000A LF or U+000D CR: |
| '\u{000A}' | '\u{000D}' => { |
| if ch == '\u{000D}' && position.peek() == Some(&'\u{000A}') { |
| // a \r\n pair should only generate one <br>, |
| // so just skip the \r. |
| position.next(); |
| } |
| |
| if !text.is_empty() { |
| append_text_node_to_fragment(&document, &fragment, text, can_gc); |
| text = String::new(); |
| } |
| |
| let br = HTMLBRElement::new(local_name!("br"), None, &document, None, can_gc); |
| fragment |
| .upcast::<Node>() |
| .AppendChild(br.upcast(), can_gc) |
| .unwrap(); |
| }, |
| _ => { |
| // Collect a sequence of code points that are not U+000A LF or U+000D CR from |
| // input given position, and set text to the result. |
| text.push(ch); |
| }, |
| } |
| } |
| |
| // If text is not the empty string, then append a new Text node whose data is text and node |
| // document is document to fragment. |
| if !text.is_empty() { |
| append_text_node_to_fragment(&document, &fragment, text, can_gc); |
| } |
| |
| fragment |
| } |
| |
| /// Checks whether a given [`DomRoot<Node>`] and its next sibling are |
| /// of type [`Text`], and if so merges them into a single [`Text`] |
| /// node. |
| /// |
| /// <https://html.spec.whatwg.org/multipage/#merge-with-the-next-text-node> |
| fn merge_with_the_next_text_node(node: DomRoot<Node>, can_gc: CanGc) { |
| // Make sure node is a Text node |
| if !node.is::<Text>() { |
| return; |
| } |
| |
| // Step 1: Let next be node's next sibling. |
| let next = match node.GetNextSibling() { |
| Some(next) => next, |
| None => return, |
| }; |
| |
| // Step 2: If next is not a Text node, then return. |
| if !next.is::<Text>() { |
| return; |
| } |
| // Step 3: Replace data with node, node's data's length, 0, and next's data. |
| let node_chars = node.downcast::<CharacterData>().expect("Node is Text"); |
| let next_chars = next.downcast::<CharacterData>().expect("Next node is Text"); |
| node_chars |
| .ReplaceData(node_chars.Length(), 0, next_chars.Data()) |
| .expect("Got chars from Text"); |
| |
| // Step 4:Remove next. |
| next.remove_self(can_gc); |
| } |
| } |
| |
| impl VirtualMethods for HTMLElement { |
| fn super_type(&self) -> Option<&dyn VirtualMethods> { |
| Some(self.as_element() as &dyn VirtualMethods) |
| } |
| |
| fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation, can_gc: CanGc) { |
| self.super_type() |
| .unwrap() |
| .attribute_mutated(attr, mutation, can_gc); |
| let element = self.as_element(); |
| match (attr.local_name(), mutation) { |
| // https://html.spec.whatwg.org/multipage/#event-handler-attributes:event-handler-content-attributes-3 |
| (name, mutation) |
| if name.starts_with("on") && EventTarget::is_content_event_handler(name) => |
| { |
| let evtarget = self.upcast::<EventTarget>(); |
| let event_name = &name[2..]; |
| match mutation { |
| // https://html.spec.whatwg.org/multipage/#activate-an-event-handler |
| AttributeMutation::Set(_) => { |
| let source = &**attr.value(); |
| let source_line = 1; // TODO(#9604) get current JS execution line |
| evtarget.set_event_handler_uncompiled( |
| self.owner_window().get_url(), |
| source_line, |
| event_name, |
| source, |
| ); |
| }, |
| // https://html.spec.whatwg.org/multipage/#deactivate-an-event-handler |
| AttributeMutation::Removed => { |
| evtarget.set_event_handler_common::<EventHandlerNonNull>(event_name, None); |
| }, |
| } |
| }, |
| |
| (&local_name!("form"), mutation) if self.is_form_associated_custom_element() => { |
| self.form_attribute_mutated(mutation, can_gc); |
| }, |
| // Adding a "disabled" attribute disables an enabled form element. |
| (&local_name!("disabled"), AttributeMutation::Set(_)) |
| if self.is_form_associated_custom_element() && element.enabled_state() => |
| { |
| element.set_disabled_state(true); |
| element.set_enabled_state(false); |
| ScriptThread::enqueue_callback_reaction( |
| element, |
| CallbackReaction::FormDisabled(true), |
| None, |
| ); |
| }, |
| // Removing the "disabled" attribute may enable a disabled |
| // form element, but a fieldset ancestor may keep it disabled. |
| (&local_name!("disabled"), AttributeMutation::Removed) |
| if self.is_form_associated_custom_element() && element.disabled_state() => |
| { |
| element.set_disabled_state(false); |
| element.set_enabled_state(true); |
| element.check_ancestors_disabled_state_for_form_control(); |
| if element.enabled_state() { |
| ScriptThread::enqueue_callback_reaction( |
| element, |
| CallbackReaction::FormDisabled(false), |
| None, |
| ); |
| } |
| }, |
| (&local_name!("readonly"), mutation) if self.is_form_associated_custom_element() => { |
| match mutation { |
| AttributeMutation::Set(_) => { |
| element.set_read_write_state(true); |
| }, |
| AttributeMutation::Removed => { |
| element.set_read_write_state(false); |
| }, |
| } |
| }, |
| (&local_name!("nonce"), mutation) => match mutation { |
| AttributeMutation::Set(_) => { |
| let nonce = &**attr.value(); |
| element.update_nonce_internal_slot(nonce.to_owned()); |
| }, |
| AttributeMutation::Removed => { |
| element.update_nonce_internal_slot("".to_owned()); |
| }, |
| }, |
| _ => {}, |
| } |
| } |
| |
| fn bind_to_tree(&self, context: &BindContext, can_gc: CanGc) { |
| if let Some(super_type) = self.super_type() { |
| super_type.bind_to_tree(context, can_gc); |
| } |
| let element = self.as_element(); |
| element.update_sequentially_focusable_status(can_gc); |
| |
| // Binding to a tree can disable a form control if one of the new |
| // ancestors is a fieldset. |
| if self.is_form_associated_custom_element() && element.enabled_state() { |
| element.check_ancestors_disabled_state_for_form_control(); |
| if element.disabled_state() { |
| ScriptThread::enqueue_callback_reaction( |
| element, |
| CallbackReaction::FormDisabled(true), |
| None, |
| ); |
| } |
| } |
| } |
| |
| fn unbind_from_tree(&self, context: &UnbindContext, can_gc: CanGc) { |
| if let Some(super_type) = self.super_type() { |
| super_type.unbind_from_tree(context, can_gc); |
| } |
| |
| // Unbinding from a tree might enable a form control, if a |
| // fieldset ancestor is the only reason it was disabled. |
| // (The fact that it's enabled doesn't do much while it's |
| // disconnected, but it is an observable fact to keep track of.) |
| let element = self.as_element(); |
| if self.is_form_associated_custom_element() && element.disabled_state() { |
| element.check_disabled_attribute(); |
| element.check_ancestors_disabled_state_for_form_control(); |
| if element.enabled_state() { |
| ScriptThread::enqueue_callback_reaction( |
| element, |
| CallbackReaction::FormDisabled(false), |
| None, |
| ); |
| } |
| } |
| } |
| |
| fn parse_plain_attribute(&self, name: &LocalName, value: DOMString) -> AttrValue { |
| match *name { |
| local_name!("itemprop") => AttrValue::from_serialized_tokenlist(value.into()), |
| local_name!("itemtype") => AttrValue::from_serialized_tokenlist(value.into()), |
| _ => self |
| .super_type() |
| .unwrap() |
| .parse_plain_attribute(name, value), |
| } |
| } |
| } |
| |
| impl Activatable for HTMLElement { |
| fn as_element(&self) -> &Element { |
| self.upcast::<Element>() |
| } |
| |
| fn is_instance_activatable(&self) -> bool { |
| self.as_element().local_name() == &local_name!("summary") |
| } |
| |
| // Basically used to make the HTMLSummaryElement activatable (which has no IDL definition) |
| fn activation_behavior(&self, _event: &Event, _target: &EventTarget, _can_gc: CanGc) { |
| self.summary_activation_behavior(); |
| } |
| } |
| |
| // Form-associated custom elements are the same interface type as |
| // normal HTMLElements, so HTMLElement needs to have the FormControl trait |
| // even though it's usually more specific trait implementations, like the |
| // HTMLInputElement one, that we really want. (Alternately we could put |
| // the FormControl trait on ElementInternals, but that raises lifetime issues.) |
| impl FormControl for HTMLElement { |
| fn form_owner(&self) -> Option<DomRoot<HTMLFormElement>> { |
| debug_assert!(self.is_form_associated_custom_element()); |
| self.as_element() |
| .get_element_internals() |
| .and_then(|e| e.form_owner()) |
| } |
| |
| fn set_form_owner(&self, form: Option<&HTMLFormElement>) { |
| debug_assert!(self.is_form_associated_custom_element()); |
| self.as_element() |
| .ensure_element_internals(CanGc::note()) |
| .set_form_owner(form); |
| } |
| |
| fn to_element(&self) -> &Element { |
| self.as_element() |
| } |
| |
| fn is_listed(&self) -> bool { |
| debug_assert!(self.is_form_associated_custom_element()); |
| true |
| } |
| |
| // TODO satisfies_constraints traits |
| } |