Skip to main content

script/dom/html/
htmlelement.rs

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