Skip to main content

script/dom/html/
htmlelement.rs

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