Skip to main content

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