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