script/dom/html/
htmlelement.rs

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