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