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