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