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