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