script/dom/html/
htmlelement.rs

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