Skip to main content

script/dom/html/
htmlelement.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::collections::HashSet;
6use std::default::Default;
7use std::rc::Rc;
8
9use dom_struct::dom_struct;
10use html5ever::{LocalName, Prefix, QualName, local_name, ns};
11use js::context::JSContext;
12use js::rust::HandleObject;
13use layout_api::{QueryMsg, ScrollContainerQueryFlags, ScrollContainerResponse};
14use script_bindings::codegen::GenericBindings::DocumentBinding::DocumentMethods;
15use script_bindings::codegen::GenericBindings::ElementBinding::ScrollLogicalPosition;
16use script_bindings::codegen::GenericBindings::WindowBinding::ScrollBehavior;
17use style::attr::AttrValue;
18use stylo_dom::ElementState;
19
20use crate::dom::activation::Activatable;
21use crate::dom::bindings::codegen::Bindings::CharacterDataBinding::CharacterData_Binding::CharacterDataMethods;
22use crate::dom::bindings::codegen::Bindings::EventHandlerBinding::{
23    EventHandlerNonNull, OnErrorEventHandlerNonNull,
24};
25use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
26use crate::dom::bindings::codegen::Bindings::HTMLLabelElementBinding::HTMLLabelElementMethods;
27use crate::dom::bindings::codegen::Bindings::HTMLOrSVGElementBinding::FocusOptions;
28use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods;
29use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::ShadowRoot_Binding::ShadowRootMethods;
30use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
31use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
32use crate::dom::bindings::inheritance::{Castable, ElementTypeId, HTMLElementTypeId, NodeTypeId};
33use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
34use crate::dom::bindings::str::DOMString;
35use crate::dom::characterdata::CharacterData;
36use crate::dom::css::cssstyledeclaration::{
37    CSSModificationAccess, CSSStyleDeclaration, CSSStyleOwner,
38};
39use crate::dom::customelementregistry::{CallbackReaction, CustomElementState};
40use crate::dom::document::Document;
41use crate::dom::document::focus::FocusableArea;
42use crate::dom::document_event_handler::character_to_code;
43use crate::dom::documentfragment::DocumentFragment;
44use crate::dom::domstringmap::DOMStringMap;
45use crate::dom::element::attributes::storage::AttrRef;
46use crate::dom::element::{
47    AttributeMutation, CustomElementCreationMode, Element, ElementCreator,
48    is_element_affected_by_legacy_background_presentational_hint,
49};
50use crate::dom::elementinternals::ElementInternals;
51use crate::dom::event::Event;
52use crate::dom::eventtarget::EventTarget;
53use crate::dom::html::htmlbodyelement::HTMLBodyElement;
54use crate::dom::html::htmldetailselement::HTMLDetailsElement;
55use crate::dom::html::htmlformelement::{FormControl, HTMLFormElement};
56use crate::dom::html::htmlframesetelement::HTMLFrameSetElement;
57use crate::dom::html::htmlhtmlelement::HTMLHtmlElement;
58use crate::dom::html::htmllabelelement::HTMLLabelElement;
59use crate::dom::html::htmltextareaelement::HTMLTextAreaElement;
60use crate::dom::html::input_element::HTMLInputElement;
61use crate::dom::htmlformelement::FormControlElementHelpers;
62use crate::dom::input_element::input_type::InputType;
63use crate::dom::iterators::ShadowIncluding;
64use crate::dom::medialist::MediaList;
65use crate::dom::node::virtualmethods::VirtualMethods;
66use crate::dom::node::{
67    BindContext, MoveContext, Node, NodeTraits, UnbindContext, from_untrusted_node_address,
68};
69use crate::dom::scrolling_box::{ScrollAxisState, ScrollRequirement};
70use crate::dom::shadowroot::ShadowRoot;
71use crate::dom::text::Text;
72use crate::script_runtime::CanGc;
73use crate::script_thread::ScriptThread;
74
75#[dom_struct]
76pub(crate) struct HTMLElement {
77    element: Element,
78    style_decl: MutNullableDom<CSSStyleDeclaration>,
79    dataset: MutNullableDom<DOMStringMap>,
80}
81
82impl HTMLElement {
83    pub(crate) fn new_inherited(
84        tag_name: LocalName,
85        prefix: Option<Prefix>,
86        document: &Document,
87    ) -> HTMLElement {
88        HTMLElement::new_inherited_with_state(ElementState::empty(), tag_name, prefix, document)
89    }
90
91    pub(crate) fn new_inherited_with_state(
92        state: ElementState,
93        tag_name: LocalName,
94        prefix: Option<Prefix>,
95        document: &Document,
96    ) -> HTMLElement {
97        HTMLElement {
98            element: Element::new_inherited_with_state(
99                state,
100                tag_name,
101                ns!(html),
102                prefix,
103                document,
104            ),
105            style_decl: Default::default(),
106            dataset: Default::default(),
107        }
108    }
109
110    pub(crate) fn new(
111        cx: &mut js::context::JSContext,
112        local_name: LocalName,
113        prefix: Option<Prefix>,
114        document: &Document,
115        proto: Option<HandleObject>,
116    ) -> DomRoot<HTMLElement> {
117        Node::reflect_node_with_proto(
118            cx,
119            Box::new(HTMLElement::new_inherited(local_name, prefix, document)),
120            document,
121            proto,
122        )
123    }
124
125    fn is_body_or_frameset(&self) -> bool {
126        let eventtarget = self.upcast::<EventTarget>();
127        eventtarget.is::<HTMLBodyElement>() || eventtarget.is::<HTMLFrameSetElement>()
128    }
129
130    /// Calls into the layout engine to generate a plain text representation
131    /// of a [`HTMLElement`] as specified when getting the `.innerText` or
132    /// `.outerText` in JavaScript.`
133    ///
134    /// <https://html.spec.whatwg.org/multipage/#get-the-text-steps>
135    pub(crate) fn get_inner_outer_text(&self) -> DOMString {
136        let node = self.upcast::<Node>();
137        let window = node.owner_window();
138        let element = self.as_element();
139
140        // Step 1.
141        let element_not_rendered = !node.is_connected() || !element.has_css_layout_box();
142        if element_not_rendered {
143            return node.GetTextContent().unwrap();
144        }
145
146        window.layout_reflow(QueryMsg::ElementInnerOuterTextQuery);
147        let text = window
148            .layout()
149            .query_element_inner_outer_text(node.to_trusted_node_address());
150
151        DOMString::from(text)
152    }
153
154    /// <https://html.spec.whatwg.org/multipage/#set-the-inner-text-steps>
155    pub(crate) fn set_inner_text(&self, cx: &mut JSContext, input: DOMString) {
156        // Step 1: Let fragment be the rendered text fragment for value given element's node
157        // document.
158        let fragment = self.rendered_text_fragment(cx, input);
159
160        // Step 2: Replace all with fragment within element.
161        Node::replace_all(cx, Some(fragment.upcast()), self.upcast::<Node>());
162    }
163
164    /// <https://html.spec.whatwg.org/multipage/#matches-the-environment>
165    pub(crate) fn media_attribute_matches_media_environment(&self) -> bool {
166        // A string matches the environment of the user if it is the empty string,
167        // a string consisting of only ASCII whitespace, or is a media query list that
168        // matches the user's environment according to the definitions given in Media Queries. [MQ]
169        self.element
170            .get_attribute_string_value(&local_name!("media"))
171            .is_none_or(|media| MediaList::matches_environment(&self.owner_document(), &media))
172    }
173
174    /// <https://html.spec.whatwg.org/multipage/#editing-host>
175    pub(crate) fn is_editing_host(&self) -> bool {
176        // > An editing host is either an HTML element with its contenteditable attribute in the true state or plaintext-only state,
177        matches!(&*self.ContentEditable().str(), "true" | "plaintext-only")
178        // > or a child HTML element of a Document whose design mode enabled is true.
179        // TODO
180    }
181
182    pub(crate) fn previously_focused_element(&self) -> Option<DomRoot<Element>> {
183        self.upcast::<Element>()
184            .ensure_rare_data()
185            .previously_focused_element
186            .get()
187    }
188
189    pub(crate) fn set_previously_focused_element(&self, element: Option<&Element>) {
190        self.upcast::<Element>()
191            .ensure_rare_data()
192            .previously_focused_element
193            .set(element);
194    }
195}
196
197impl HTMLElementMethods<crate::DomTypeHolder> for HTMLElement {
198    /// <https://html.spec.whatwg.org/multipage/#the-style-attribute>
199    fn Style(&self, cx: &mut JSContext) -> DomRoot<CSSStyleDeclaration> {
200        self.style_decl.or_init(|| {
201            let global = self.owner_window();
202            CSSStyleDeclaration::new(
203                cx,
204                &global,
205                CSSStyleOwner::Element(Dom::from_ref(self.upcast())),
206                None,
207                CSSModificationAccess::ReadWrite,
208            )
209        })
210    }
211
212    // https://html.spec.whatwg.org/multipage/#attr-title
213    make_getter!(Title, "title");
214    // https://html.spec.whatwg.org/multipage/#attr-title
215    make_setter!(cx, SetTitle, "title");
216
217    // https://html.spec.whatwg.org/multipage/#attr-lang
218    make_getter!(Lang, "lang");
219    // https://html.spec.whatwg.org/multipage/#attr-lang
220    make_setter!(cx, SetLang, "lang");
221
222    // https://html.spec.whatwg.org/multipage/#the-dir-attribute
223    make_enumerated_getter!(
224        Dir,
225        "dir",
226        "ltr" | "rtl" | "auto",
227        missing => "",
228        invalid => ""
229    );
230
231    // https://html.spec.whatwg.org/multipage/#the-dir-attribute
232    make_setter!(cx, SetDir, "dir");
233
234    // https://html.spec.whatwg.org/multipage/#dom-hidden
235    make_bool_getter!(Hidden, "hidden");
236    // https://html.spec.whatwg.org/multipage/#dom-hidden
237    make_bool_setter!(cx, SetHidden, "hidden");
238
239    // https://html.spec.whatwg.org/multipage/#globaleventhandlers
240    global_event_handlers!(NoOnload);
241
242    /// <https://html.spec.whatwg.org/multipage/#dom-dataset>
243    fn Dataset(&self, cx: &mut JSContext) -> DomRoot<DOMStringMap> {
244        self.dataset.or_init(|| DOMStringMap::new(cx, self))
245    }
246
247    /// <https://html.spec.whatwg.org/multipage/#handler-onerror>
248    fn GetOnerror(&self, cx: &mut JSContext) -> Option<Rc<OnErrorEventHandlerNonNull>> {
249        if self.is_body_or_frameset() {
250            let document = self.owner_document();
251            if document.has_browsing_context() {
252                document.window().GetOnerror(cx)
253            } else {
254                None
255            }
256        } else {
257            self.upcast::<EventTarget>()
258                .get_event_handler_common(cx, "error")
259        }
260    }
261
262    /// <https://html.spec.whatwg.org/multipage/#handler-onerror>
263    fn SetOnerror(&self, cx: &mut JSContext, listener: Option<Rc<OnErrorEventHandlerNonNull>>) {
264        if self.is_body_or_frameset() {
265            let document = self.owner_document();
266            if document.has_browsing_context() {
267                document.window().SetOnerror(cx, listener)
268            }
269        } else {
270            // special setter for error
271            self.upcast::<EventTarget>()
272                .set_error_event_handler(cx, "error", listener)
273        }
274    }
275
276    /// <https://html.spec.whatwg.org/multipage/#handler-onload>
277    fn GetOnload(&self, cx: &mut JSContext) -> Option<Rc<EventHandlerNonNull>> {
278        if self.is_body_or_frameset() {
279            let document = self.owner_document();
280            if document.has_browsing_context() {
281                document.window().GetOnload(cx)
282            } else {
283                None
284            }
285        } else {
286            self.upcast::<EventTarget>()
287                .get_event_handler_common(cx, "load")
288        }
289    }
290
291    /// <https://html.spec.whatwg.org/multipage/#handler-onload>
292    fn SetOnload(&self, cx: &mut JSContext, listener: Option<Rc<EventHandlerNonNull>>) {
293        if self.is_body_or_frameset() {
294            let document = self.owner_document();
295            if document.has_browsing_context() {
296                document.window().SetOnload(cx, listener)
297            }
298        } else {
299            self.upcast::<EventTarget>()
300                .set_event_handler_common(cx, "load", listener)
301        }
302    }
303
304    /// <https://html.spec.whatwg.org/multipage/#handler-onblur>
305    fn GetOnblur(&self, cx: &mut JSContext) -> Option<Rc<EventHandlerNonNull>> {
306        if self.is_body_or_frameset() {
307            let document = self.owner_document();
308            if document.has_browsing_context() {
309                document.window().GetOnblur(cx)
310            } else {
311                None
312            }
313        } else {
314            self.upcast::<EventTarget>()
315                .get_event_handler_common(cx, "blur")
316        }
317    }
318
319    /// <https://html.spec.whatwg.org/multipage/#handler-onblur>
320    fn SetOnblur(&self, cx: &mut JSContext, listener: Option<Rc<EventHandlerNonNull>>) {
321        if self.is_body_or_frameset() {
322            let document = self.owner_document();
323            if document.has_browsing_context() {
324                document.window().SetOnblur(cx, listener)
325            }
326        } else {
327            self.upcast::<EventTarget>()
328                .set_event_handler_common(cx, "blur", listener)
329        }
330    }
331
332    /// <https://html.spec.whatwg.org/multipage/#handler-onfocus>
333    fn GetOnfocus(&self, cx: &mut JSContext) -> Option<Rc<EventHandlerNonNull>> {
334        if self.is_body_or_frameset() {
335            let document = self.owner_document();
336            if document.has_browsing_context() {
337                document.window().GetOnfocus(cx)
338            } else {
339                None
340            }
341        } else {
342            self.upcast::<EventTarget>()
343                .get_event_handler_common(cx, "focus")
344        }
345    }
346
347    /// <https://html.spec.whatwg.org/multipage/#handler-onfocus>
348    fn SetOnfocus(&self, cx: &mut JSContext, listener: Option<Rc<EventHandlerNonNull>>) {
349        if self.is_body_or_frameset() {
350            let document = self.owner_document();
351            if document.has_browsing_context() {
352                document.window().SetOnfocus(cx, listener)
353            }
354        } else {
355            self.upcast::<EventTarget>()
356                .set_event_handler_common(cx, "focus", listener)
357        }
358    }
359
360    /// <https://html.spec.whatwg.org/multipage/#handler-onresize>
361    fn GetOnresize(&self, cx: &mut JSContext) -> Option<Rc<EventHandlerNonNull>> {
362        if self.is_body_or_frameset() {
363            let document = self.owner_document();
364            if document.has_browsing_context() {
365                document.window().GetOnresize(cx)
366            } else {
367                None
368            }
369        } else {
370            self.upcast::<EventTarget>()
371                .get_event_handler_common(cx, "resize")
372        }
373    }
374
375    /// <https://html.spec.whatwg.org/multipage/#handler-onresize>
376    fn SetOnresize(&self, cx: &mut JSContext, listener: Option<Rc<EventHandlerNonNull>>) {
377        if self.is_body_or_frameset() {
378            let document = self.owner_document();
379            if document.has_browsing_context() {
380                document.window().SetOnresize(cx, listener)
381            }
382        } else {
383            self.upcast::<EventTarget>()
384                .set_event_handler_common(cx, "resize", listener)
385        }
386    }
387
388    /// <https://html.spec.whatwg.org/multipage/#handler-onscroll>
389    fn GetOnscroll(&self, cx: &mut JSContext) -> Option<Rc<EventHandlerNonNull>> {
390        if self.is_body_or_frameset() {
391            let document = self.owner_document();
392            if document.has_browsing_context() {
393                document.window().GetOnscroll(cx)
394            } else {
395                None
396            }
397        } else {
398            self.upcast::<EventTarget>()
399                .get_event_handler_common(cx, "scroll")
400        }
401    }
402
403    /// <https://html.spec.whatwg.org/multipage/#handler-onscroll>
404    fn SetOnscroll(&self, cx: &mut JSContext, listener: Option<Rc<EventHandlerNonNull>>) {
405        if self.is_body_or_frameset() {
406            let document = self.owner_document();
407            if document.has_browsing_context() {
408                document.window().SetOnscroll(cx, listener)
409            }
410        } else {
411            self.upcast::<EventTarget>()
412                .set_event_handler_common(cx, "scroll", listener)
413        }
414    }
415
416    /// <https://html.spec.whatwg.org/multipage/#attr-itemtype>
417    fn Itemtypes(&self) -> Option<Vec<DOMString>> {
418        let atoms = self
419            .element
420            .get_tokenlist_attribute(&local_name!("itemtype"));
421
422        if atoms.is_empty() {
423            return None;
424        }
425
426        #[expect(clippy::mutable_key_type)]
427        // See `impl Hash for DOMString`.
428        let mut item_attr_values = HashSet::new();
429        for attr_value in &atoms {
430            item_attr_values.insert(DOMString::from(String::from(attr_value.trim())));
431        }
432
433        Some(item_attr_values.into_iter().collect())
434    }
435
436    /// <https://html.spec.whatwg.org/multipage/#names:-the-itemprop-attribute>
437    fn PropertyNames(&self) -> Option<Vec<DOMString>> {
438        let atoms = self
439            .element
440            .get_tokenlist_attribute(&local_name!("itemprop"));
441
442        if atoms.is_empty() {
443            return None;
444        }
445
446        #[expect(clippy::mutable_key_type)]
447        // See `impl Hash for DOMString`.
448        let mut item_attr_values = HashSet::new();
449        for attr_value in &atoms {
450            item_attr_values.insert(DOMString::from(String::from(attr_value.trim())));
451        }
452
453        Some(item_attr_values.into_iter().collect())
454    }
455
456    /// <https://html.spec.whatwg.org/multipage/#dom-click>
457    fn Click(&self, cx: &mut JSContext) {
458        let element = self.as_element();
459        if element.disabled_state() {
460            return;
461        }
462        if element.click_in_progress() {
463            return;
464        }
465        element.set_click_in_progress(true);
466
467        self.upcast::<Node>()
468            .fire_synthetic_pointer_event_not_trusted(cx, atom!("click"));
469        element.set_click_in_progress(false);
470    }
471
472    /// <https://html.spec.whatwg.org/multipage/#dom-focus>
473    fn Focus(&self, cx: &mut JSContext, options: &FocusOptions) {
474        // 1. If the allow focus steps given this's node document return false, then return.
475        // TODO: Implement this.
476
477        // 2. Run the focusing steps for this.
478        if !self.upcast::<Node>().run_the_focusing_steps(cx, None) {
479            // The specification seems to imply we should scroll into view even if this element
480            // is not a focusable area. No browser does this, so we return early in that case.
481            // See https://github.com/whatwg/html/issues/12231.
482            return;
483        }
484
485        // > 3. If options["focusVisible"] is true, or does not exist but in an
486        // >    implementation-defined  way the user agent determines it would be best to do so,
487        // >    then indicate focus. TODO: Implement this.
488
489        // > 4. If options["preventScroll"] is false, then scroll a target into view given this,
490        // >    "auto", "center", and "center".
491        if !options.preventScroll {
492            let scroll_axis = ScrollAxisState {
493                position: ScrollLogicalPosition::Center,
494                requirement: ScrollRequirement::IfNotVisible,
495            };
496            self.upcast::<Element>().scroll_into_view_with_options(
497                cx,
498                ScrollBehavior::Smooth,
499                scroll_axis,
500                scroll_axis,
501                None,
502                None,
503            );
504        }
505    }
506
507    /// <https://html.spec.whatwg.org/multipage/#dom-blur>
508    fn Blur(&self, cx: &mut JSContext) {
509        // TODO: Run the unfocusing steps. Focus the top-level document, not
510        //       the current document.
511        if !self.as_element().focus_state() {
512            return;
513        }
514        // <https://html.spec.whatwg.org/multipage/#unfocusing-steps>
515        self.owner_document()
516            .focus_handler()
517            .focus(cx, FocusableArea::Viewport);
518    }
519
520    /// <https://drafts.csswg.org/cssom-view/#dom-htmlelement-scrollparent>
521    #[expect(unsafe_code)]
522    fn ScrollParent(&self) -> Option<DomRoot<Element>> {
523        self.owner_window()
524            .scroll_container_query(
525                Some(self.upcast()),
526                ScrollContainerQueryFlags::ForScrollParent,
527            )
528            .and_then(|response| match response {
529                ScrollContainerResponse::Viewport(_) => self.owner_document().GetScrollingElement(),
530                ScrollContainerResponse::Element(parent_node_address, _) => {
531                    let node = unsafe { from_untrusted_node_address(parent_node_address) };
532                    DomRoot::downcast(node)
533                },
534            })
535    }
536
537    /// <https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetparent>
538    fn GetOffsetParent(&self) -> Option<DomRoot<Element>> {
539        if self.is::<HTMLBodyElement>() || self.element.is_root() {
540            return None;
541        }
542
543        let node = self.upcast::<Node>();
544        let window = self.owner_window();
545        let (element, _) = window.offset_parent_query(node);
546
547        element
548    }
549
550    /// <https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsettop>
551    fn OffsetTop(&self) -> i32 {
552        if self.is_body_element() {
553            return 0;
554        }
555
556        let node = self.upcast::<Node>();
557        let window = self.owner_window();
558        let (_, rect) = window.offset_parent_query(node);
559
560        rect.origin.y.to_nearest_px()
561    }
562
563    /// <https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetleft>
564    fn OffsetLeft(&self) -> i32 {
565        if self.is_body_element() {
566            return 0;
567        }
568
569        let node = self.upcast::<Node>();
570        let window = self.owner_window();
571        let (_, rect) = window.offset_parent_query(node);
572
573        rect.origin.x.to_nearest_px()
574    }
575
576    /// <https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetwidth>
577    fn OffsetWidth(&self) -> i32 {
578        let node = self.upcast::<Node>();
579        let window = self.owner_window();
580        let (_, rect) = window.offset_parent_query(node);
581
582        rect.size.width.to_nearest_px()
583    }
584
585    /// <https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetheight>
586    fn OffsetHeight(&self) -> i32 {
587        let node = self.upcast::<Node>();
588        let window = self.owner_window();
589        let (_, rect) = window.offset_parent_query(node);
590
591        rect.size.height.to_nearest_px()
592    }
593
594    /// <https://html.spec.whatwg.org/multipage/#the-innertext-idl-attribute>
595    fn InnerText(&self) -> DOMString {
596        self.get_inner_outer_text()
597    }
598
599    /// <https://html.spec.whatwg.org/multipage/#set-the-inner-text-steps>
600    fn SetInnerText(&self, cx: &mut JSContext, input: DOMString) {
601        self.set_inner_text(cx, input)
602    }
603
604    /// <https://html.spec.whatwg.org/multipage/#dom-outertext>
605    fn GetOuterText(&self) -> Fallible<DOMString> {
606        Ok(self.get_inner_outer_text())
607    }
608
609    /// <https://html.spec.whatwg.org/multipage/#the-innertext-idl-attribute:dom-outertext-2>
610    fn SetOuterText(&self, cx: &mut JSContext, input: DOMString) -> Fallible<()> {
611        // Step 1: If this's parent is null, then throw a "NoModificationAllowedError" DOMException.
612        let Some(parent) = self.upcast::<Node>().GetParentNode() else {
613            return Err(Error::NoModificationAllowed(None));
614        };
615
616        let node = self.upcast::<Node>();
617        let document = self.owner_document();
618
619        // Step 2: Let next be this's next sibling.
620        let next = node.GetNextSibling();
621
622        // Step 3: Let previous be this's previous sibling.
623        let previous = node.GetPreviousSibling();
624
625        // Step 4: Let fragment be the rendered text fragment for the given value given this's node
626        // document.
627        let fragment = self.rendered_text_fragment(cx, input);
628
629        // Step 5: If fragment has no children, then append a new Text node whose data is the empty
630        // string and node document is this's node document to fragment.
631        if fragment.upcast::<Node>().children_count() == 0 {
632            let text_node = Text::new(cx, DOMString::from("".to_owned()), &document);
633
634            fragment
635                .upcast::<Node>()
636                .AppendChild(cx, text_node.upcast())?;
637        }
638
639        // Step 6: Replace this with fragment within this's parent.
640        parent.ReplaceChild(cx, fragment.upcast(), node)?;
641
642        // Step 7: If next is non-null and next's previous sibling is a Text node, then merge with
643        // the next text node given next's previous sibling.
644        if let Some(next_sibling) = next &&
645            let Some(node) = next_sibling.GetPreviousSibling()
646        {
647            Self::merge_with_the_next_text_node(cx, node);
648        }
649
650        // Step 8: If previous is a Text node, then merge with the next text node given previous.
651        if let Some(previous) = previous {
652            Self::merge_with_the_next_text_node(cx, previous)
653        }
654
655        Ok(())
656    }
657
658    /// <https://html.spec.whatwg.org/multipage/#dom-translate>
659    fn Translate(&self) -> bool {
660        self.as_element().is_translate_enabled()
661    }
662
663    /// <https://html.spec.whatwg.org/multipage/#dom-translate>
664    fn SetTranslate(&self, cx: &mut JSContext, yesno: bool) {
665        self.as_element().set_string_attribute(
666            cx,
667            &html5ever::local_name!("translate"),
668            match yesno {
669                true => DOMString::from("yes"),
670                false => DOMString::from("no"),
671            },
672        );
673    }
674
675    // https://html.spec.whatwg.org/multipage/#dom-contenteditable
676    make_enumerated_getter!(
677        ContentEditable,
678        "contenteditable",
679        "true" | "false" | "plaintext-only",
680        missing => "inherit",
681        invalid => "inherit",
682        empty => "true"
683    );
684
685    /// <https://html.spec.whatwg.org/multipage/#dom-contenteditable>
686    fn SetContentEditable(&self, cx: &mut JSContext, value: DOMString) -> ErrorResult {
687        let lower_value = value.to_ascii_lowercase();
688        let attr_name = &local_name!("contenteditable");
689        match lower_value.as_ref() {
690            // > On setting, if the new value is an ASCII case-insensitive match for the string "inherit", then the content attribute must be removed,
691            "inherit" => {
692                self.element.remove_attribute_by_name(cx, attr_name);
693            },
694            // > 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",
695            // > 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",
696            // > 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",
697            "true" | "false" | "plaintext-only" => {
698                self.element
699                    .set_attribute(cx, attr_name, AttrValue::String(lower_value));
700            },
701            // > and otherwise the attribute setter must throw a "SyntaxError" DOMException.
702            _ => return Err(Error::Syntax(None)),
703        };
704        Ok(())
705    }
706
707    /// <https://html.spec.whatwg.org/multipage/#dom-iscontenteditable>
708    fn IsContentEditable(&self) -> bool {
709        // > The isContentEditable IDL attribute, on getting, must return true if the element is either an editing host or editable, and false otherwise.
710        self.upcast::<Node>().is_editable_or_editing_host()
711    }
712
713    /// <https://html.spec.whatwg.org/multipage#dom-attachinternals>
714    fn AttachInternals(&self, cx: &mut JSContext) -> Fallible<DomRoot<ElementInternals>> {
715        // Step 1: If this's is value is not null, then throw a "NotSupportedError" DOMException
716        if self.element.get_is().is_some() {
717            return Err(Error::NotSupported(None));
718        }
719
720        // Step 2: Let definition be the result of looking up a custom element definition
721        // Note: the element can pass this check without yet being a custom
722        // element, as long as there is a registered definition
723        // that could upgrade it to one later.
724        let registry = self.owner_window().CustomElements(cx);
725        let definition = registry.lookup_definition(self.as_element().local_name(), None);
726
727        // Step 3: If definition is null, then throw an "NotSupportedError" DOMException
728        let definition = match definition {
729            Some(definition) => definition,
730            None => return Err(Error::NotSupported(None)),
731        };
732
733        // Step 4: If definition's disable internals is true, then throw a "NotSupportedError" DOMException
734        if definition.disable_internals {
735            return Err(Error::NotSupported(None));
736        }
737
738        // Step 5: If this's attached internals is non-null, then throw an "NotSupportedError" DOMException
739        let internals = self.element.ensure_element_internals(CanGc::from_cx(cx));
740        if internals.attached() {
741            return Err(Error::NotSupported(None));
742        }
743
744        // Step 6: If this's custom element state is not "precustomized" or "custom",
745        // then throw a "NotSupportedError" DOMException.
746        if !matches!(
747            self.element.get_custom_element_state(),
748            CustomElementState::Precustomized | CustomElementState::Custom
749        ) {
750            return Err(Error::NotSupported(None));
751        }
752
753        if self.is_form_associated_custom_element() {
754            self.element.init_state_for_internals();
755        }
756
757        // Step 6-7: Set this's attached internals to a new ElementInternals instance
758        internals.set_attached();
759        Ok(internals)
760    }
761
762    /// <https://html.spec.whatwg.org/multipage/#dom-noncedelement-nonce>
763    fn Nonce(&self) -> DOMString {
764        self.as_element().nonce_value().into()
765    }
766
767    /// <https://html.spec.whatwg.org/multipage/#dom-noncedelement-nonce>
768    fn SetNonce(&self, _cx: &mut JSContext, value: DOMString) {
769        self.as_element()
770            .update_nonce_internal_slot(String::from(value))
771    }
772
773    /// <https://html.spec.whatwg.org/multipage/#dom-fe-autofocus>
774    fn Autofocus(&self) -> bool {
775        self.element.has_attribute(&local_name!("autofocus"))
776    }
777
778    /// <https://html.spec.whatwg.org/multipage/#dom-fe-autofocus>
779    fn SetAutofocus(&self, cx: &mut JSContext, autofocus: bool) {
780        self.element
781            .set_bool_attribute(cx, &local_name!("autofocus"), autofocus);
782    }
783
784    /// <https://html.spec.whatwg.org/multipage/#dom-tabindex>
785    fn TabIndex(&self) -> i32 {
786        self.element.tab_index()
787    }
788
789    /// <https://html.spec.whatwg.org/multipage/#dom-tabindex>
790    fn SetTabIndex(&self, cx: &mut JSContext, tab_index: i32) {
791        self.element
792            .set_attribute(cx, &local_name!("tabindex"), tab_index.into());
793    }
794
795    // https://html.spec.whatwg.org/multipage/#dom-accesskey
796    make_getter!(AccessKey, "accesskey");
797
798    // https://html.spec.whatwg.org/multipage/#dom-accesskey
799    make_setter!(cx, SetAccessKey, "accesskey");
800
801    /// <https://html.spec.whatwg.org/multipage/#dom-accesskeylabel>
802    fn AccessKeyLabel(&self) -> DOMString {
803        // The accessKeyLabel IDL attribute must return a string that represents the element's
804        // assigned access key, if any. If the element does not have one, then the IDL attribute
805        // must return the empty string.
806        if !self.element.has_attribute(&local_name!("accesskey")) {
807            return Default::default();
808        }
809
810        let access_key_string =
811            String::from(self.element.get_string_attribute(&local_name!("accesskey")));
812
813        #[cfg(target_os = "macos")]
814        let access_key_label = format!("⌃⌥{access_key_string}");
815        #[cfg(not(target_os = "macos"))]
816        let access_key_label = format!("Alt+Shift+{access_key_string}");
817
818        access_key_label.into()
819    }
820}
821
822fn append_text_node_to_fragment(
823    cx: &mut JSContext,
824    document: &Document,
825    fragment: &DocumentFragment,
826    text: String,
827) {
828    let text = Text::new(cx, DOMString::from(text), document);
829    fragment
830        .upcast::<Node>()
831        .AppendChild(cx, text.upcast())
832        .unwrap();
833}
834
835impl HTMLElement {
836    /// <https://html.spec.whatwg.org/multipage/#category-label>
837    pub(crate) fn is_labelable_element(&self) -> bool {
838        match self.upcast::<Node>().type_id() {
839            NodeTypeId::Element(ElementTypeId::HTMLElement(type_id)) => match type_id {
840                HTMLElementTypeId::HTMLInputElement => !matches!(
841                    *self.downcast::<HTMLInputElement>().unwrap().input_type(),
842                    InputType::Hidden(_)
843                ),
844                HTMLElementTypeId::HTMLButtonElement |
845                HTMLElementTypeId::HTMLMeterElement |
846                HTMLElementTypeId::HTMLOutputElement |
847                HTMLElementTypeId::HTMLProgressElement |
848                HTMLElementTypeId::HTMLSelectElement |
849                HTMLElementTypeId::HTMLTextAreaElement => true,
850                _ => self.is_form_associated_custom_element(),
851            },
852            _ => false,
853        }
854    }
855
856    /// <https://html.spec.whatwg.org/multipage/#form-associated-custom-element>
857    pub(crate) fn is_form_associated_custom_element(&self) -> bool {
858        if let Some(definition) = self.as_element().get_custom_element_definition() {
859            definition.is_autonomous() && definition.form_associated
860        } else {
861            false
862        }
863    }
864
865    /// <https://html.spec.whatwg.org/multipage/#category-listed>
866    pub(crate) fn is_listed_element(&self) -> bool {
867        match self.upcast::<Node>().type_id() {
868            NodeTypeId::Element(ElementTypeId::HTMLElement(type_id)) => match type_id {
869                HTMLElementTypeId::HTMLButtonElement |
870                HTMLElementTypeId::HTMLFieldSetElement |
871                HTMLElementTypeId::HTMLInputElement |
872                HTMLElementTypeId::HTMLObjectElement |
873                HTMLElementTypeId::HTMLOutputElement |
874                HTMLElementTypeId::HTMLSelectElement |
875                HTMLElementTypeId::HTMLTextAreaElement => true,
876                _ => self.is_form_associated_custom_element(),
877            },
878            _ => false,
879        }
880    }
881
882    /// <https://html.spec.whatwg.org/multipage/#the-body-element-2>
883    pub(crate) fn is_body_element(&self) -> bool {
884        let self_node = self.upcast::<Node>();
885        self_node.GetParentNode().is_some_and(|parent| {
886            let parent_node = parent.upcast::<Node>();
887            (self_node.is::<HTMLBodyElement>() || self_node.is::<HTMLFrameSetElement>()) &&
888                parent_node.is::<HTMLHtmlElement>() &&
889                self_node
890                    .preceding_siblings()
891                    .all(|n| !n.is::<HTMLBodyElement>() && !n.is::<HTMLFrameSetElement>())
892        })
893    }
894
895    /// <https://html.spec.whatwg.org/multipage/#category-submit>
896    pub(crate) fn is_submittable_element(&self) -> bool {
897        match self.upcast::<Node>().type_id() {
898            NodeTypeId::Element(ElementTypeId::HTMLElement(type_id)) => match type_id {
899                HTMLElementTypeId::HTMLButtonElement |
900                HTMLElementTypeId::HTMLInputElement |
901                HTMLElementTypeId::HTMLSelectElement |
902                HTMLElementTypeId::HTMLTextAreaElement => true,
903                _ => self.is_form_associated_custom_element(),
904            },
905            _ => false,
906        }
907    }
908
909    // https://html.spec.whatwg.org/multipage/#dom-lfe-labels
910    // This gets the nth label in tree order.
911    pub(crate) fn label_at(&self, index: u32) -> Option<DomRoot<Node>> {
912        let element = self.as_element();
913
914        // Traverse entire tree for <label> elements that have
915        // this as their control.
916        // There is room for performance optimization, as we don't need
917        // the actual result of GetControl, only whether the result
918        // would match self.
919        // (Even more room for performance optimization: do what
920        // nodelist ChildrenList does and keep a mutation-aware cursor
921        // around; this may be hard since labels need to keep working
922        // even as they get detached into a subtree and reattached to
923        // a document.)
924        let root_element = element.root_element();
925        let root_node = root_element.upcast::<Node>();
926        root_node
927            .traverse_preorder(ShadowIncluding::No)
928            .filter_map(DomRoot::downcast::<HTMLLabelElement>)
929            .filter(|elem| match elem.GetControl() {
930                Some(control) => &*control == self,
931                _ => false,
932            })
933            .nth(index as usize)
934            .map(|n| DomRoot::from_ref(n.upcast::<Node>()))
935    }
936
937    // https://html.spec.whatwg.org/multipage/#dom-lfe-labels
938    // This counts the labels of the element, to support NodeList::Length
939    pub(crate) fn labels_count(&self) -> u32 {
940        // see label_at comments about performance
941        let element = self.as_element();
942        let root_element = element.root_element();
943        let root_node = root_element.upcast::<Node>();
944        root_node
945            .traverse_preorder(ShadowIncluding::No)
946            .filter_map(DomRoot::downcast::<HTMLLabelElement>)
947            .filter(|elem| match elem.GetControl() {
948                Some(control) => &*control == self,
949                _ => false,
950            })
951            .count() as u32
952    }
953
954    // https://html.spec.whatwg.org/multipage/#the-directionality.
955    // returns Some if can infer direction by itself or from child nodes
956    // returns None if requires to go up to parent
957    pub(crate) fn directionality(&self) -> Option<String> {
958        let element_direction = &self.Dir();
959
960        if element_direction == "ltr" {
961            return Some("ltr".to_owned());
962        }
963
964        if element_direction == "rtl" {
965            return Some("rtl".to_owned());
966        }
967
968        if let Some(input) = self.downcast::<HTMLInputElement>() &&
969            matches!(*input.input_type(), InputType::Tel(_))
970        {
971            return Some("ltr".to_owned());
972        }
973
974        if element_direction == "auto" {
975            if let Some(directionality) = self
976                .downcast::<HTMLInputElement>()
977                .and_then(|input| input.auto_directionality())
978            {
979                return Some(directionality);
980            }
981
982            if let Some(area) = self.downcast::<HTMLTextAreaElement>() {
983                return Some(area.auto_directionality());
984            }
985        }
986
987        // TODO(NeverHappened): Implement condition
988        // If the element's dir attribute is in the auto state OR
989        // If the element is a bdi element and the dir attribute is not in a defined state
990        // (i.e. it is not present or has an invalid value)
991        // Requires bdi element implementation (https://html.spec.whatwg.org/multipage/#the-bdi-element)
992
993        None
994    }
995
996    // https://html.spec.whatwg.org/multipage/#the-summary-element:activation-behaviour
997    pub(crate) fn summary_activation_behavior(&self, cx: &mut js::context::JSContext) {
998        debug_assert!(self.as_element().local_name() == &local_name!("summary"));
999
1000        // Step 1. If this summary element is not the summary for its parent details, then return.
1001        let is_implicit_summary_element = self.is_implicit_summary_element();
1002        if !is_implicit_summary_element && !self.is_a_summary_for_its_parent_details() {
1003            return;
1004        }
1005
1006        // Step 2. Let parent be this summary element's parent.
1007        let parent = if is_implicit_summary_element {
1008            DomRoot::downcast::<HTMLDetailsElement>(self.containing_shadow_root().unwrap().Host())
1009                .unwrap()
1010        } else {
1011            self.upcast::<Node>()
1012                .GetParentNode()
1013                .and_then(DomRoot::downcast::<HTMLDetailsElement>)
1014                .unwrap()
1015        };
1016
1017        // Step 3. If the open attribute is present on parent, then remove it.
1018        // Otherwise, set parent's open attribute to the empty string.
1019        parent.toggle(cx);
1020    }
1021
1022    /// <https://html.spec.whatwg.org/multipage/#summary-for-its-parent-details>
1023    pub(crate) fn is_a_summary_for_its_parent_details(&self) -> bool {
1024        // Step 1. If this summary element has no parent, then return false.
1025        // Step 2. Let parent be this summary element's parent.
1026        let Some(parent) = self.upcast::<Node>().GetParentNode() else {
1027            return false;
1028        };
1029
1030        // Step 3. If parent is not a details element, then return false.
1031        let Some(details) = parent.downcast::<HTMLDetailsElement>() else {
1032            return false;
1033        };
1034
1035        // Step 4. If parent's first summary element child is not this summary
1036        // element, then return false.
1037        // Step 5. Return true.
1038        details
1039            .find_corresponding_summary_element()
1040            .is_some_and(|summary| &*summary == self.upcast())
1041    }
1042
1043    /// Whether or not this is an implicitly generated `<summary>`
1044    /// element for a UA `<details>` shadow tree
1045    fn is_implicit_summary_element(&self) -> bool {
1046        // Note that non-implicit summary elements are not actually inside
1047        // the UA shadow tree, they're only assigned to a slot inside it.
1048        // Therefore they don't cause false positives here
1049        self.containing_shadow_root()
1050            .as_deref()
1051            .map(ShadowRoot::Host)
1052            .is_some_and(|host| host.is::<HTMLDetailsElement>())
1053    }
1054
1055    /// <https://html.spec.whatwg.org/multipage/#rendered-text-fragment>
1056    fn rendered_text_fragment(
1057        &self,
1058        cx: &mut JSContext,
1059        input: DOMString,
1060    ) -> DomRoot<DocumentFragment> {
1061        // Step 1: Let fragment be a new DocumentFragment whose node document is document.
1062        let document = self.owner_document();
1063        let fragment = DocumentFragment::new(cx, &document);
1064
1065        // Step 2: Let position be a position variable for input, initially pointing at the start
1066        // of input.
1067        let input = input.str();
1068        let mut position = input.chars().peekable();
1069
1070        // Step 3: Let text be the empty string.
1071        let mut text = String::new();
1072
1073        // Step 4
1074        while let Some(ch) = position.next() {
1075            match ch {
1076                // While position is not past the end of input, and the code point at position is
1077                // either U+000A LF or U+000D CR:
1078                '\u{000A}' | '\u{000D}' => {
1079                    if ch == '\u{000D}' && position.peek() == Some(&'\u{000A}') {
1080                        // a \r\n pair should only generate one <br>,
1081                        // so just skip the \r.
1082                        position.next();
1083                    }
1084
1085                    if !text.is_empty() {
1086                        append_text_node_to_fragment(cx, &document, &fragment, text);
1087                        text = String::new();
1088                    }
1089
1090                    let br = Element::create(
1091                        cx,
1092                        QualName::new(None, ns!(html), local_name!("br")),
1093                        None,
1094                        &document,
1095                        ElementCreator::ScriptCreated,
1096                        CustomElementCreationMode::Asynchronous,
1097                        None,
1098                    );
1099                    fragment
1100                        .upcast::<Node>()
1101                        .AppendChild(cx, br.upcast())
1102                        .unwrap();
1103                },
1104                _ => {
1105                    // Collect a sequence of code points that are not U+000A LF or U+000D CR from
1106                    // input given position, and set text to the result.
1107                    text.push(ch);
1108                },
1109            }
1110        }
1111
1112        // If text is not the empty string, then append a new Text node whose data is text and node
1113        // document is document to fragment.
1114        if !text.is_empty() {
1115            append_text_node_to_fragment(cx, &document, &fragment, text);
1116        }
1117
1118        fragment
1119    }
1120
1121    /// Checks whether a given [`DomRoot<Node>`] and its next sibling are
1122    /// of type [`Text`], and if so merges them into a single [`Text`]
1123    /// node.
1124    ///
1125    /// <https://html.spec.whatwg.org/multipage/#merge-with-the-next-text-node>
1126    fn merge_with_the_next_text_node(cx: &mut JSContext, node: DomRoot<Node>) {
1127        // Make sure node is a Text node
1128        if !node.is::<Text>() {
1129            return;
1130        }
1131
1132        // Step 1: Let next be node's next sibling.
1133        let next = match node.GetNextSibling() {
1134            Some(next) => next,
1135            None => return,
1136        };
1137
1138        // Step 2: If next is not a Text node, then return.
1139        if !next.is::<Text>() {
1140            return;
1141        }
1142        // Step 3: Replace data with node, node's data's length, 0, and next's data.
1143        let node_chars = node.downcast::<CharacterData>().expect("Node is Text");
1144        let next_chars = next.downcast::<CharacterData>().expect("Next node is Text");
1145        node_chars
1146            .ReplaceData(cx, node_chars.Length(), 0, next_chars.Data())
1147            .expect("Got chars from Text");
1148
1149        // Step 4:Remove next.
1150        next.remove_self(cx);
1151    }
1152
1153    /// <https://html.spec.whatwg.org/multipage/#keyboard-shortcuts-processing-model>
1154    /// > Whenever an element's accesskey attribute is set, changed, or removed, the user agent must
1155    /// > update the element's assigned access key by running the following steps:
1156    fn update_assigned_access_key(&self) {
1157        // 1. If the element has no accesskey attribute, then skip to the fallback step below.
1158        if !self.element.has_attribute(&local_name!("accesskey")) {
1159            // This is the same as steps 4 and 5 below.
1160            self.owner_document()
1161                .event_handler()
1162                .unassign_access_key(self);
1163        }
1164
1165        // 2. Otherwise, split the attribute's value on ASCII whitespace, and let keys be the resulting tokens.
1166        let attribute_value = self.element.get_string_attribute(&local_name!("accesskey"));
1167        let string_view = attribute_value.str();
1168        let values = string_view.split_html_space_characters();
1169
1170        // 3. For each value in keys in turn, in the order the tokens appeared in the attribute's
1171        //    value, run the following substeps:
1172        for value in values {
1173            // 1. If the value is not a string exactly one code point in length, then skip the
1174            //    remainder of these steps for this value.
1175            let mut characters = value.chars();
1176            let Some(character) = characters.next() else {
1177                continue;
1178            };
1179            if characters.count() > 0 {
1180                continue;
1181            }
1182
1183            // 2. If the value does not correspond to a key on the system's keyboard, then skip the
1184            //    remainder of these steps for this value.
1185            let Some(code) = character_to_code(character) else {
1186                continue;
1187            };
1188
1189            // 3. If the user agent can find a mix of zero or more modifier keys that, combined with
1190            //    the key that corresponds to the value given in the attribute, can be used as the
1191            //    access key, then the user agent may assign that combination of keys as the element's
1192            //    assigned access key and return.
1193            self.owner_document()
1194                .event_handler()
1195                .assign_access_key(self, code);
1196            return;
1197        }
1198
1199        // 4. Fallback: Optionally, the user agent may assign a key combination of its choosing as
1200        //    the element's assigned access key and then return.
1201        // We do not do this.
1202
1203        // 5. If this step is reached, the element has no assigned access key.
1204        self.owner_document()
1205            .event_handler()
1206            .unassign_access_key(self);
1207    }
1208}
1209
1210impl VirtualMethods for HTMLElement {
1211    fn super_type(&self) -> Option<&dyn VirtualMethods> {
1212        Some(self.as_element() as &dyn VirtualMethods)
1213    }
1214
1215    fn attribute_mutated(
1216        &self,
1217        cx: &mut JSContext,
1218        attr: AttrRef<'_>,
1219        mutation: AttributeMutation,
1220    ) {
1221        self.super_type()
1222            .unwrap()
1223            .attribute_mutated(cx, attr, mutation);
1224        let element = self.as_element();
1225        match (attr.local_name(), mutation) {
1226            (&local_name!("accesskey"), ..) => {
1227                self.update_assigned_access_key();
1228            },
1229            (&local_name!("form"), mutation) if self.is_form_associated_custom_element() => {
1230                self.form_attribute_mutated(cx, mutation);
1231            },
1232            // Adding a "disabled" attribute disables an enabled form element.
1233            (&local_name!("disabled"), AttributeMutation::Set(..))
1234                if self.is_form_associated_custom_element() && element.enabled_state() =>
1235            {
1236                element.set_disabled_state(true);
1237                element.set_enabled_state(false);
1238                ScriptThread::enqueue_callback_reaction(
1239                    cx,
1240                    element,
1241                    CallbackReaction::FormDisabled(true),
1242                    None,
1243                );
1244            },
1245            // Removing the "disabled" attribute may enable a disabled
1246            // form element, but a fieldset ancestor may keep it disabled.
1247            (&local_name!("disabled"), AttributeMutation::Removed)
1248                if self.is_form_associated_custom_element() && element.disabled_state() =>
1249            {
1250                element.set_disabled_state(false);
1251                element.set_enabled_state(true);
1252                element.check_ancestors_disabled_state_for_form_control();
1253                if element.enabled_state() {
1254                    ScriptThread::enqueue_callback_reaction(
1255                        cx,
1256                        element,
1257                        CallbackReaction::FormDisabled(false),
1258                        None,
1259                    );
1260                }
1261            },
1262            (&local_name!("readonly"), mutation) if self.is_form_associated_custom_element() => {
1263                match mutation {
1264                    AttributeMutation::Set(..) => {
1265                        element.set_read_write_state(true);
1266                    },
1267                    AttributeMutation::Removed => {
1268                        element.set_read_write_state(false);
1269                    },
1270                }
1271            },
1272            (&local_name!("nonce"), mutation) => match mutation {
1273                AttributeMutation::Set(..) => {
1274                    let nonce = &**attr.value();
1275                    element.update_nonce_internal_slot(nonce.to_owned());
1276                },
1277                AttributeMutation::Removed => {
1278                    element.update_nonce_internal_slot("".to_owned());
1279                },
1280            },
1281            _ => {},
1282        }
1283    }
1284
1285    fn bind_to_tree(&self, cx: &mut JSContext, context: &BindContext) {
1286        if let Some(super_type) = self.super_type() {
1287            super_type.bind_to_tree(cx, context);
1288        }
1289
1290        // Binding to a tree can disable a form control if one of the new
1291        // ancestors is a fieldset.
1292        let element = self.as_element();
1293        if self.is_form_associated_custom_element() && element.enabled_state() {
1294            element.check_ancestors_disabled_state_for_form_control();
1295            if element.disabled_state() {
1296                ScriptThread::enqueue_callback_reaction(
1297                    cx,
1298                    element,
1299                    CallbackReaction::FormDisabled(true),
1300                    None,
1301                );
1302            }
1303        }
1304
1305        if element.has_attribute(&local_name!("accesskey")) {
1306            self.update_assigned_access_key();
1307        }
1308    }
1309
1310    /// <https://html.spec.whatwg.org/multipage#dom-trees:concept-node-remove-ext>
1311    ///
1312    /// TODO: These are the node removal steps, so this should be done for all Nodes.
1313    fn unbind_from_tree(&self, cx: &mut js::context::JSContext, context: &UnbindContext) {
1314        // 1. Let document be removedNode's node document.
1315        let document = self.owner_document();
1316
1317        // 2. If document's focused area is removedNode, then set document's focused area to
1318        // document's viewport, and set document's relevant global object's navigation API's focus
1319        // changed during ongoing navigation to false.
1320        //
1321        // We are not calling the focusing steps on purpose here. There is a note about this in
1322        // the specification that reads:
1323        //
1324        // > This does not perform the unfocusing steps, focusing steps, or focus update steps, and
1325        // > thus no blur or change events are fired.
1326        let element = self.as_element();
1327        if document
1328            .focus_handler()
1329            .focused_area()
1330            .element()
1331            .is_some_and(|focused_element| focused_element == element)
1332        {
1333            document
1334                .focus_handler()
1335                .set_focused_area(FocusableArea::Viewport);
1336        }
1337
1338        // 3. If removedNode is an element whose namespace is the HTML namespace, and this standard
1339        // defines HTML element removing steps for removedNode's local name, then run the
1340        // corresponding HTML element removing steps given removedNode, isSubtreeRoot, and
1341        // oldAncestor.
1342        if let Some(super_type) = self.super_type() {
1343            super_type.unbind_from_tree(cx, context);
1344        }
1345
1346        // 4. If removedNode is a form-associated element with a non-null form owner and removedNode
1347        // and its form owner are no longer in the same tree, then reset the form owner of
1348        // removedNode.
1349        //
1350        // Unbinding from a tree might enable a form control, if a
1351        // fieldset ancestor is the only reason it was disabled.
1352        // (The fact that it's enabled doesn't do much while it's
1353        // disconnected, but it is an observable fact to keep track of.)
1354        //
1355        // TODO: This should likely just call reset on form owner.
1356        if self.is_form_associated_custom_element() && element.disabled_state() {
1357            element.check_disabled_attribute();
1358            element.check_ancestors_disabled_state_for_form_control();
1359            if element.enabled_state() {
1360                ScriptThread::enqueue_callback_reaction(
1361                    cx,
1362                    element,
1363                    CallbackReaction::FormDisabled(false),
1364                    None,
1365                );
1366            }
1367        }
1368
1369        if element.has_attribute(&local_name!("accesskey")) {
1370            self.owner_document()
1371                .event_handler()
1372                .unassign_access_key(self);
1373        }
1374    }
1375
1376    fn attribute_affects_presentational_hints(&self, attr: AttrRef<'_>) -> bool {
1377        if is_element_affected_by_legacy_background_presentational_hint(
1378            self.element.namespace(),
1379            self.element.local_name(),
1380        ) && attr.local_name() == &local_name!("background")
1381        {
1382            return true;
1383        }
1384
1385        self.super_type()
1386            .unwrap()
1387            .attribute_affects_presentational_hints(attr)
1388    }
1389
1390    fn parse_plain_attribute(&self, name: &LocalName, value: DOMString) -> AttrValue {
1391        match *name {
1392            local_name!("itemprop") => AttrValue::from_serialized_tokenlist(value.into()),
1393            local_name!("itemtype") => AttrValue::from_serialized_tokenlist(value.into()),
1394            local_name!("background")
1395                if is_element_affected_by_legacy_background_presentational_hint(
1396                    self.element.namespace(),
1397                    self.element.local_name(),
1398                ) =>
1399            {
1400                AttrValue::from_resolved_url(
1401                    &self.owner_document().base_url().get_arc(),
1402                    value.into(),
1403                )
1404            },
1405            _ => self
1406                .super_type()
1407                .unwrap()
1408                .parse_plain_attribute(name, value),
1409        }
1410    }
1411
1412    /// <https://html.spec.whatwg.org/multipage/#dom-trees:html-element-moving-steps>
1413    fn moving_steps(&self, cx: &mut JSContext, context: &MoveContext) {
1414        // Step 1. If movedNode is an element whose namespace is the HTML namespace, and this
1415        // standard defines HTML element moving steps for movedNode's local name, then run the
1416        // corresponding HTML element moving steps given movedNode.
1417        if let Some(super_type) = self.super_type() {
1418            super_type.moving_steps(cx, context);
1419        }
1420
1421        // Step 2. If movedNode is a form-associated element with a non-null form owner and
1422        // movedNode and its form owner are no longer in the same tree, then reset the form owner of
1423        // movedNode.
1424        if let Some(form_control) = self.element.as_maybe_form_control() {
1425            form_control.moving_steps(cx)
1426        }
1427    }
1428}
1429
1430impl Activatable for HTMLElement {
1431    fn as_element(&self) -> &Element {
1432        &self.element
1433    }
1434
1435    fn is_instance_activatable(&self) -> bool {
1436        self.element.local_name() == &local_name!("summary")
1437    }
1438
1439    // Basically used to make the HTMLSummaryElement activatable (which has no IDL definition)
1440    fn activation_behavior(
1441        &self,
1442        cx: &mut js::context::JSContext,
1443        _event: &Event,
1444        _target: &EventTarget,
1445    ) {
1446        self.summary_activation_behavior(cx);
1447    }
1448}
1449
1450// Form-associated custom elements are the same interface type as
1451// normal HTMLElements, so HTMLElement needs to have the FormControl trait
1452// even though it's usually more specific trait implementations, like the
1453// HTMLInputElement one, that we really want. (Alternately we could put
1454// the FormControl trait on ElementInternals, but that raises lifetime issues.)
1455impl FormControl for HTMLElement {
1456    fn form_owner(&self) -> Option<DomRoot<HTMLFormElement>> {
1457        debug_assert!(self.is_form_associated_custom_element());
1458        self.element
1459            .get_element_internals()
1460            .and_then(|e| e.form_owner())
1461    }
1462
1463    fn set_form_owner(&self, form: Option<&HTMLFormElement>) {
1464        debug_assert!(self.is_form_associated_custom_element());
1465        self.element
1466            .ensure_element_internals(CanGc::deprecated_note())
1467            .set_form_owner(form);
1468    }
1469
1470    fn to_element(&self) -> &Element {
1471        &self.element
1472    }
1473
1474    fn is_listed(&self) -> bool {
1475        debug_assert!(self.is_form_associated_custom_element());
1476        true
1477    }
1478
1479    // TODO satisfies_constraints traits
1480}