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