Skip to main content

script/dom/
elementinternals.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::cell::Cell;
6
7use dom_struct::dom_struct;
8use html5ever::local_name;
9use js::context::JSContext;
10use script_bindings::cell::DomRefCell;
11use script_bindings::reflector::{Reflector, reflect_dom_object_with_cx};
12
13use crate::dom::bindings::codegen::Bindings::ElementInternalsBinding::{
14    ElementInternalsMethods, ValidityStateFlags,
15};
16use crate::dom::bindings::codegen::UnionTypes::FileOrUSVStringOrFormData;
17use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
18use crate::dom::bindings::inheritance::Castable;
19use crate::dom::bindings::root::{Dom, DomRoot, LayoutDom, MutNullableDom, ToLayoutOptional};
20use crate::dom::bindings::str::{DOMString, USVString};
21use crate::dom::customstateset::CustomStateSet;
22use crate::dom::element::Element;
23use crate::dom::file::File;
24use crate::dom::html::htmlelement::HTMLElement;
25use crate::dom::html::htmlformelement::{
26    FormDatum, FormDatumUnrooted, FormDatumValue, HTMLFormElement,
27};
28use crate::dom::node::{Node, NodeTraits};
29use crate::dom::nodelist::NodeList;
30use crate::dom::shadowroot::ShadowRoot;
31use crate::dom::validation::{Validatable, is_barred_by_datalist_ancestor};
32use crate::dom::validitystate::{ValidationFlags, ValidityState};
33
34#[derive(JSTraceable, MallocSizeOf)]
35#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
36enum SubmissionValue {
37    File(Dom<File>),
38    FormData(Vec<FormDatumUnrooted>),
39    USVString(USVString),
40    None,
41}
42
43impl From<Option<&FileOrUSVStringOrFormData>> for SubmissionValue {
44    fn from(value: Option<&FileOrUSVStringOrFormData>) -> Self {
45        match value {
46            None => SubmissionValue::None,
47            Some(FileOrUSVStringOrFormData::File(file)) => {
48                SubmissionValue::File(Dom::from_ref(file))
49            },
50            Some(FileOrUSVStringOrFormData::USVString(usv_string)) => {
51                SubmissionValue::USVString(usv_string.clone())
52            },
53            Some(FileOrUSVStringOrFormData::FormData(form_data)) => SubmissionValue::FormData(
54                form_data
55                    .datums()
56                    .into_iter()
57                    .map(|data| data.into())
58                    .collect(),
59            ),
60        }
61    }
62}
63
64#[dom_struct]
65pub(crate) struct ElementInternals {
66    reflector_: Reflector,
67    /// If `attached` is false, we're using this to hold form-related state
68    /// on an element for which `attachInternals()` wasn't called yet; this is
69    /// necessary because it might have a form owner.
70    attached: Cell<bool>,
71    target_element: Dom<HTMLElement>,
72    validity_state: MutNullableDom<ValidityState>,
73    validation_message: DomRefCell<DOMString>,
74    custom_validity_error_message: DomRefCell<DOMString>,
75    validation_anchor: MutNullableDom<HTMLElement>,
76    submission_value: DomRefCell<SubmissionValue>,
77    state: DomRefCell<SubmissionValue>,
78    form_owner: MutNullableDom<HTMLFormElement>,
79    labels_node_list: MutNullableDom<NodeList>,
80
81    /// <https://html.spec.whatwg.org/multipage/#dom-elementinternals-states>
82    states: MutNullableDom<CustomStateSet>,
83}
84
85impl ElementInternals {
86    fn new_inherited(target_element: &HTMLElement) -> ElementInternals {
87        ElementInternals {
88            reflector_: Reflector::new(),
89            attached: Cell::new(false),
90            target_element: Dom::from_ref(target_element),
91            validity_state: Default::default(),
92            validation_message: DomRefCell::new(DOMString::new()),
93            custom_validity_error_message: DomRefCell::new(DOMString::new()),
94            validation_anchor: MutNullableDom::new(None),
95            submission_value: DomRefCell::new(SubmissionValue::None),
96            state: DomRefCell::new(SubmissionValue::None),
97            form_owner: MutNullableDom::new(None),
98            labels_node_list: MutNullableDom::new(None),
99            states: MutNullableDom::new(None),
100        }
101    }
102
103    pub(crate) fn new(cx: &mut JSContext, element: &HTMLElement) -> DomRoot<ElementInternals> {
104        let global = element.owner_window();
105        reflect_dom_object_with_cx(
106            Box::new(ElementInternals::new_inherited(element)),
107            &*global,
108            cx,
109        )
110    }
111
112    fn is_target_form_associated(&self) -> bool {
113        self.target_element.is_form_associated_custom_element()
114    }
115
116    fn set_validation_message(&self, message: DOMString) {
117        *self.validation_message.borrow_mut() = message;
118    }
119
120    fn set_custom_validity_error_message(&self, message: DOMString) {
121        *self.custom_validity_error_message.borrow_mut() = message;
122    }
123
124    pub(crate) fn set_form_owner(&self, form: Option<&HTMLFormElement>) {
125        self.form_owner.set(form);
126    }
127
128    pub(crate) fn form_owner(&self) -> Option<DomRoot<HTMLFormElement>> {
129        self.form_owner.get()
130    }
131
132    pub(crate) fn set_attached(&self) {
133        self.attached.set(true);
134    }
135
136    pub(crate) fn attached(&self) -> bool {
137        self.attached.get()
138    }
139
140    pub(crate) fn perform_entry_construction(&self, entry_list: &mut Vec<FormDatum>) {
141        if self
142            .target_element
143            .upcast::<Element>()
144            .has_attribute(&local_name!("disabled"))
145        {
146            warn!("We are in perform_entry_construction on an element with disabled attribute!");
147        }
148        if self.target_element.upcast::<Element>().disabled_state() {
149            warn!("We are in perform_entry_construction on an element with disabled bit!");
150        }
151        if !self.target_element.upcast::<Element>().enabled_state() {
152            warn!("We are in perform_entry_construction on an element without enabled bit!");
153        }
154
155        if let SubmissionValue::FormData(datums) = &*self.submission_value.borrow() {
156            entry_list.extend(datums.iter().map(|data| data.root()));
157            return;
158        }
159        let name = self
160            .target_element
161            .upcast::<Element>()
162            .get_string_attribute(&local_name!("name"));
163        if name.is_empty() {
164            return;
165        }
166        match &*self.submission_value.borrow() {
167            SubmissionValue::FormData(_) => unreachable!(
168                "The FormData submission value has been handled before name empty checking"
169            ),
170            SubmissionValue::None => {},
171            SubmissionValue::USVString(string) => {
172                entry_list.push(FormDatum {
173                    ty: DOMString::from("string"),
174                    name,
175                    value: FormDatumValue::String(DOMString::from(string.to_string())),
176                });
177            },
178            SubmissionValue::File(file) => {
179                entry_list.push(FormDatum {
180                    ty: DOMString::from("file"),
181                    name,
182                    value: FormDatumValue::File(DomRoot::from_ref(file)),
183                });
184            },
185        }
186    }
187
188    pub(crate) fn is_invalid(&self, cx: &mut JSContext) -> bool {
189        self.is_target_form_associated() &&
190            self.is_instance_validatable() &&
191            !self.satisfies_constraints(cx)
192    }
193
194    pub(crate) fn custom_states_for_layout<'a>(&'a self) -> Option<LayoutDom<'a, CustomStateSet>> {
195        #[expect(unsafe_code)]
196        unsafe {
197            self.states.to_layout()
198        }
199    }
200}
201
202impl ElementInternalsMethods<crate::DomTypeHolder> for ElementInternals {
203    /// <https://html.spec.whatwg.org/multipage/#dom-elementinternals-shadowroot>
204    fn GetShadowRoot(&self) -> Option<DomRoot<ShadowRoot>> {
205        // Step 1. Let target be this's target element.
206        // Step 2. If target is not a shadow host, then return null.
207        // Step 3. Let shadow be target's shadow root.
208        let shadow = self.target_element.upcast::<Element>().shadow_root()?;
209
210        // Step 4. If shadow's available to element internals is false, then return null.
211        if !shadow.is_available_to_element_internals() {
212            return None;
213        }
214
215        // Step 5. Return shadow.
216        Some(shadow)
217    }
218
219    /// <https://html.spec.whatwg.org/multipage#dom-elementinternals-setformvalue>
220    fn SetFormValue(
221        &self,
222        value: Option<FileOrUSVStringOrFormData>,
223        maybe_state: Option<Option<FileOrUSVStringOrFormData>>,
224    ) -> ErrorResult {
225        // Steps 1-2: If element is not a form-associated custom element, then throw a "NotSupportedError" DOMException
226        if !self.is_target_form_associated() {
227            return Err(Error::NotSupported(Some(
228                "The target element is not a form-associated custom element".to_owned(),
229            )));
230        }
231
232        // Step 3: Set target element's submission value
233        *self.submission_value.borrow_mut() = value.as_ref().into();
234
235        match maybe_state {
236            // Step 4: If the state argument of the function is omitted, set element's state to its submission value
237            None => *self.state.borrow_mut() = value.as_ref().into(),
238            // Steps 5-6: Otherwise, set element's state to state
239            Some(state) => *self.state.borrow_mut() = state.as_ref().into(),
240        }
241        Ok(())
242    }
243
244    /// <https://html.spec.whatwg.org/multipage#dom-elementinternals-setvalidity>
245    fn SetValidity(
246        &self,
247        cx: &mut JSContext,
248        flags: &ValidityStateFlags,
249        message: Option<DOMString>,
250        anchor: Option<&HTMLElement>,
251    ) -> ErrorResult {
252        // Step 1. Let element be this's target element.
253        // Step 2: If element is not a form-associated custom element, then throw a "NotSupportedError" DOMException.
254        if !self.is_target_form_associated() {
255            return Err(Error::NotSupported(Some(
256                "The target element is not a form-associated custom element".to_owned(),
257            )));
258        }
259
260        // Step 3: If flags contains one or more true values and message is not given or is the empty
261        // string, then throw a TypeError.
262        let bits: ValidationFlags = flags.into();
263        if !bits.is_empty() && !message.as_ref().map_or_else(|| false, |m| !m.is_empty()) {
264            return Err(Error::Type(
265                c"Setting an element to invalid requires a message string as the second argument."
266                    .to_owned(),
267            ));
268        }
269
270        // Step 4: For each entry `flag` → `value` of `flags`, set element's validity flag with the name
271        // `flag` to `value`.
272        self.validity_state(cx).update_invalid_flags(bits);
273        self.validity_state(cx).update_pseudo_classes(cx);
274
275        // Step 5: Set element's validation message to the empty string if message is not given
276        // or all of element's validity flags are false, or to message otherwise.
277        if bits.is_empty() {
278            self.set_validation_message(DOMString::new());
279        } else {
280            self.set_validation_message(message.unwrap_or_default());
281        }
282
283        // Step 6: If element's customError validity flag is true, then set element's custom validity error
284        // message to element's validation message. Otherwise, set element's custom validity error
285        // message to the empty string.
286        if bits.contains(ValidationFlags::CUSTOM_ERROR) {
287            self.set_custom_validity_error_message(self.validation_message.borrow().clone());
288        } else {
289            self.set_custom_validity_error_message(DOMString::new());
290        }
291
292        let anchor = match anchor {
293            // Step 7: If anchor is not given, then set it to element.
294            None => &self.target_element,
295            // Step 8. Otherwise, if anchor is not a shadow-including inclusive descendant of element,
296            // then throw a "NotFoundError" DOMException.
297            Some(anchor) => {
298                if !self
299                    .target_element
300                    .upcast::<Node>()
301                    .is_shadow_including_inclusive_ancestor_of(anchor.upcast::<Node>())
302                {
303                    return Err(Error::NotFound(Some(
304                        "The anchor element is not a shadow-including inclusive descendant of the target element".to_owned(),
305                    )));
306                }
307                anchor
308            },
309        };
310
311        // Step 9. Set element's validation anchor to anchor.
312        self.validation_anchor.set(Some(anchor));
313
314        Ok(())
315    }
316
317    /// <https://html.spec.whatwg.org/multipage#dom-elementinternals-validationmessage>
318    fn GetValidationMessage(&self) -> Fallible<DOMString> {
319        // This check isn't in the spec but it's in WPT tests and it maintains
320        // consistency with other methods that do specify it
321        if !self.is_target_form_associated() {
322            return Err(Error::NotSupported(Some(
323                "The target element is not a form-associated custom element".to_owned(),
324            )));
325        }
326        Ok(self.validation_message.borrow().clone())
327    }
328
329    /// <https://html.spec.whatwg.org/multipage#dom-elementinternals-validity>
330    fn GetValidity(&self, cx: &mut JSContext) -> Fallible<DomRoot<ValidityState>> {
331        if !self.is_target_form_associated() {
332            return Err(Error::NotSupported(Some(
333                "The target element is not a form-associated custom element".to_owned(),
334            )));
335        }
336        Ok(self.validity_state(cx))
337    }
338
339    /// <https://html.spec.whatwg.org/multipage#dom-elementinternals-labels>
340    fn GetLabels(&self, cx: &mut JSContext) -> Fallible<DomRoot<NodeList>> {
341        if !self.is_target_form_associated() {
342            return Err(Error::NotSupported(Some(
343                "The target element is not a form-associated custom element".to_owned(),
344            )));
345        }
346        Ok(self.labels_node_list.or_init(|| {
347            NodeList::new_labels_list(
348                cx,
349                self.target_element.upcast::<Node>().owner_doc().window(),
350                &self.target_element,
351            )
352        }))
353    }
354
355    /// <https://html.spec.whatwg.org/multipage#dom-elementinternals-willvalidate>
356    fn GetWillValidate(&self) -> Fallible<bool> {
357        if !self.is_target_form_associated() {
358            return Err(Error::NotSupported(Some(
359                "The target element is not a form-associated custom element".to_owned(),
360            )));
361        }
362        Ok(self.is_instance_validatable())
363    }
364
365    /// <https://html.spec.whatwg.org/multipage#dom-elementinternals-form>
366    fn GetForm(&self) -> Fallible<Option<DomRoot<HTMLFormElement>>> {
367        if !self.is_target_form_associated() {
368            return Err(Error::NotSupported(Some(
369                "The target element is not a form-associated custom element".to_owned(),
370            )));
371        }
372        Ok(self.form_owner.get())
373    }
374
375    /// <https://html.spec.whatwg.org/multipage#dom-elementinternals-checkvalidity>
376    fn CheckValidity(&self, cx: &mut JSContext) -> Fallible<bool> {
377        if !self.is_target_form_associated() {
378            return Err(Error::NotSupported(Some(
379                "The target element is not a form-associated custom element".to_owned(),
380            )));
381        }
382        Ok(self.check_validity(cx))
383    }
384
385    /// <https://html.spec.whatwg.org/multipage#dom-elementinternals-reportvalidity>
386    fn ReportValidity(&self, cx: &mut JSContext) -> Fallible<bool> {
387        if !self.is_target_form_associated() {
388            return Err(Error::NotSupported(Some(
389                "The target element is not a form-associated custom element".to_owned(),
390            )));
391        }
392        Ok(self.report_validity(cx))
393    }
394
395    /// <https://html.spec.whatwg.org/multipage/#dom-elementinternals-states>
396    fn States(&self, cx: &mut JSContext) -> DomRoot<CustomStateSet> {
397        self.states.or_init(|| {
398            CustomStateSet::new(
399                cx,
400                &self.target_element.owner_window(),
401                &self.target_element,
402            )
403        })
404    }
405}
406
407// Form-associated custom elements also need the Validatable trait.
408impl Validatable for ElementInternals {
409    fn as_element(&self) -> &Element {
410        debug_assert!(self.is_target_form_associated());
411        self.target_element.upcast::<Element>()
412    }
413
414    fn validity_state(&self, cx: &mut JSContext) -> DomRoot<ValidityState> {
415        debug_assert!(self.is_target_form_associated());
416        self.validity_state.or_init(|| {
417            ValidityState::new(
418                cx,
419                &self.target_element.owner_window(),
420                self.target_element.upcast(),
421            )
422        })
423    }
424
425    /// <https://html.spec.whatwg.org/multipage#candidate-for-constraint-validation>
426    fn is_instance_validatable(&self) -> bool {
427        debug_assert!(self.is_target_form_associated());
428        if !self.target_element.is_submittable_element() {
429            return false;
430        }
431
432        // The form-associated custom element is barred from constraint validation,
433        // if the readonly attribute is specified, the element is disabled,
434        // or the element has a datalist element ancestor.
435        !self.as_element().read_write_state() &&
436            !self.as_element().disabled_state() &&
437            !is_barred_by_datalist_ancestor(self.target_element.upcast::<Node>())
438    }
439}