diff options
author | cathiechen <cathiechen@igalia.com> | 2024-04-11 15:17:11 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-04-11 13:17:11 +0000 |
commit | 4e4a4c0a28fb571991470f26ea82b8a897153788 (patch) | |
tree | 711026c2592e76bf6c573d14864c4a9af80a1be1 /components/script/dom/elementinternals.rs | |
parent | 2eb959a159874fa62a0844b31791698b74f3c959 (diff) | |
download | servo-4e4a4c0a28fb571991470f26ea82b8a897153788.tar.gz servo-4e4a4c0a28fb571991470f26ea82b8a897153788.zip |
Implement form-associated custom elements and their ElementInternals (#31980)
* FACEs work, setFormValue test is awful so now has _mozilla backup
* 1. Impl Validatable in ElementInternals instead of HTMLElement. 2. Reuse the code in Validatable trait. 3. The form associated custom element is not a customized built-in element.
* add some comments
* support readonly attribute and complete barred from constraint validation
* Addressed the code review comments
* Updated the legacy-layout results
* Fixed the WPT failures in ElementInternals-validation.html
* Addressed the code review comments
* Review suggestions
* Fixed silly mistakes and update the test result outside elementinternals
* update the test results
---------
Co-authored-by: Patrick Shaughnessy <pshaughn@comcast.net>
Co-authored-by: Martin Robinson <mrobinson@igalia.com>
Diffstat (limited to 'components/script/dom/elementinternals.rs')
-rw-r--r-- | components/script/dom/elementinternals.rs | 366 |
1 files changed, 366 insertions, 0 deletions
diff --git a/components/script/dom/elementinternals.rs b/components/script/dom/elementinternals.rs new file mode 100644 index 00000000000..eeeb9c9234d --- /dev/null +++ b/components/script/dom/elementinternals.rs @@ -0,0 +1,366 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use std::cell::Cell; + +use dom_struct::dom_struct; +use html5ever::local_name; + +use crate::dom::bindings::cell::DomRefCell; +use crate::dom::bindings::codegen::Bindings::ElementInternalsBinding::{ + ElementInternalsMethods, ValidityStateFlags, +}; +use crate::dom::bindings::codegen::UnionTypes::FileOrUSVStringOrFormData; +use crate::dom::bindings::error::{Error, ErrorResult, Fallible}; +use crate::dom::bindings::inheritance::Castable; +use crate::dom::bindings::reflector::{reflect_dom_object, Reflector}; +use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom}; +use crate::dom::bindings::str::{DOMString, USVString}; +use crate::dom::element::Element; +use crate::dom::file::File; +use crate::dom::htmlelement::HTMLElement; +use crate::dom::htmlformelement::{FormDatum, FormDatumValue, HTMLFormElement}; +use crate::dom::node::{window_from_node, Node}; +use crate::dom::nodelist::NodeList; +use crate::dom::validation::{is_barred_by_datalist_ancestor, Validatable}; +use crate::dom::validitystate::{ValidationFlags, ValidityState}; + +#[derive(Clone, JSTraceable, MallocSizeOf)] +enum SubmissionValue { + File(DomRoot<File>), + FormData(Vec<FormDatum>), + USVString(USVString), + None, +} + +impl From<Option<&FileOrUSVStringOrFormData>> for SubmissionValue { + fn from(value: Option<&FileOrUSVStringOrFormData>) -> Self { + match value { + None => SubmissionValue::None, + Some(FileOrUSVStringOrFormData::File(file)) => { + SubmissionValue::File(DomRoot::from_ref(file)) + }, + Some(FileOrUSVStringOrFormData::USVString(usv_string)) => { + SubmissionValue::USVString(usv_string.clone()) + }, + Some(FileOrUSVStringOrFormData::FormData(form_data)) => { + SubmissionValue::FormData(form_data.datums()) + }, + } + } +} + +#[dom_struct] +pub struct ElementInternals { + reflector_: Reflector, + /// If `attached` is false, we're using this to hold form-related state + /// on an element for which `attachInternals()` wasn't called yet; this is + /// necessary because it might have a form owner. + attached: Cell<bool>, + target_element: Dom<HTMLElement>, + validity_state: MutNullableDom<ValidityState>, + validation_message: DomRefCell<DOMString>, + custom_validity_error_message: DomRefCell<DOMString>, + validation_anchor: MutNullableDom<HTMLElement>, + submission_value: DomRefCell<SubmissionValue>, + state: DomRefCell<SubmissionValue>, + form_owner: MutNullableDom<HTMLFormElement>, + labels_node_list: MutNullableDom<NodeList>, +} + +impl ElementInternals { + fn new_inherited(target_element: &HTMLElement) -> ElementInternals { + ElementInternals { + reflector_: Reflector::new(), + attached: Cell::new(false), + target_element: Dom::from_ref(target_element), + validity_state: Default::default(), + validation_message: DomRefCell::new(DOMString::new()), + custom_validity_error_message: DomRefCell::new(DOMString::new()), + validation_anchor: MutNullableDom::new(None), + submission_value: DomRefCell::new(SubmissionValue::None), + state: DomRefCell::new(SubmissionValue::None), + form_owner: MutNullableDom::new(None), + labels_node_list: MutNullableDom::new(None), + } + } + + pub fn new(element: &HTMLElement) -> DomRoot<ElementInternals> { + let global = window_from_node(element); + reflect_dom_object(Box::new(ElementInternals::new_inherited(element)), &*global) + } + + fn is_target_form_associated(&self) -> bool { + self.target_element.is_form_associated_custom_element() + } + + fn set_validation_message(&self, message: DOMString) { + *self.validation_message.borrow_mut() = message; + } + + fn set_custom_validity_error_message(&self, message: DOMString) { + *self.custom_validity_error_message.borrow_mut() = message; + } + + fn set_submission_value(&self, value: SubmissionValue) { + *self.submission_value.borrow_mut() = value; + } + + fn set_state(&self, value: SubmissionValue) { + *self.state.borrow_mut() = value; + } + + pub fn set_form_owner(&self, form: Option<&HTMLFormElement>) { + self.form_owner.set(form); + } + + pub fn form_owner(&self) -> Option<DomRoot<HTMLFormElement>> { + self.form_owner.get() + } + + pub fn set_attached(&self) { + self.attached.set(true); + } + + pub fn attached(&self) -> bool { + self.attached.get() + } + + pub fn perform_entry_construction(&self, entry_list: &mut Vec<FormDatum>) { + if self + .target_element + .upcast::<Element>() + .has_attribute(&local_name!("disabled")) + { + warn!("We are in perform_entry_construction on an element with disabled attribute!"); + } + if self.target_element.upcast::<Element>().disabled_state() { + warn!("We are in perform_entry_construction on an element with disabled bit!"); + } + if !self.target_element.upcast::<Element>().enabled_state() { + warn!("We are in perform_entry_construction on an element without enabled bit!"); + } + + if let SubmissionValue::FormData(datums) = &*self.submission_value.borrow() { + entry_list.extend(datums.iter().map(|d| d.clone())); + return; + } + let name = self + .target_element + .upcast::<Element>() + .get_string_attribute(&local_name!("name")); + if name.is_empty() { + return; + } + match &*self.submission_value.borrow() { + SubmissionValue::FormData(_) => unreachable!( + "The FormData submission value has been handled before name empty checking" + ), + SubmissionValue::None => {}, + SubmissionValue::USVString(string) => { + entry_list.push(FormDatum { + ty: DOMString::from("string"), + name: name, + value: FormDatumValue::String(DOMString::from(string.to_string())), + }); + }, + SubmissionValue::File(file) => { + entry_list.push(FormDatum { + ty: DOMString::from("file"), + name: name, + value: FormDatumValue::File(DomRoot::from_ref(&*file)), + }); + }, + } + } + + pub fn is_invalid(&self) -> bool { + self.is_target_form_associated() && + self.is_instance_validatable() && + !self.satisfies_constraints() + } +} + +impl ElementInternalsMethods for ElementInternals { + /// <https://html.spec.whatwg.org/multipage#dom-elementinternals-setformvalue> + fn SetFormValue( + &self, + value: Option<FileOrUSVStringOrFormData>, + maybe_state: Option<Option<FileOrUSVStringOrFormData>>, + ) -> ErrorResult { + // Steps 1-2: If element is not a form-associated custom element, then throw a "NotSupportedError" DOMException + if !self.is_target_form_associated() { + return Err(Error::NotSupported); + } + + // Step 3: Set target element's submission value + self.set_submission_value(value.as_ref().into()); + + match maybe_state { + // Step 4: If the state argument of the function is omitted, set element's state to its submission value + None => self.set_state(value.as_ref().into()), + // Steps 5-6: Otherwise, set element's state to state + Some(state) => self.set_state(state.as_ref().into()), + } + Ok(()) + } + + /// <https://html.spec.whatwg.org/multipage#dom-elementinternals-setvalidity> + fn SetValidity( + &self, + flags: &ValidityStateFlags, + message: Option<DOMString>, + anchor: Option<&HTMLElement>, + ) -> ErrorResult { + // Steps 1-2: Check form-associated custom element + if !self.is_target_form_associated() { + return Err(Error::NotSupported); + } + + // Step 3: If flags contains one or more true values and message is not given or is the empty + // string, then throw a TypeError. + let bits: ValidationFlags = flags.into(); + if !bits.is_empty() && !message.as_ref().map_or_else(|| false, |m| !m.is_empty()) { + return Err(Error::Type( + "Setting an element to invalid requires a message string as the second argument." + .to_string(), + )); + } + + // Step 4: For each entry `flag` → `value` of `flags`, set element's validity flag with the name + // `flag` to `value`. + self.validity_state().update_invalid_flags(bits); + self.validity_state().update_pseudo_classes(); + + // Step 5: Set element's validation message to the empty string if message is not given + // or all of element's validity flags are false, or to message otherwise. + if bits.is_empty() { + self.set_validation_message(DOMString::new()); + } else { + self.set_validation_message(message.unwrap_or_else(|| DOMString::new())); + } + + // Step 6: If element's customError validity flag is true, then set element's custom validity error + // message to element's validation message. Otherwise, set element's custom validity error + // message to the empty string. + if bits.contains(ValidationFlags::CUSTOM_ERROR) { + self.set_custom_validity_error_message(self.validation_message.borrow().clone()); + } else { + self.set_custom_validity_error_message(DOMString::new()); + } + + // Step 7: Set element's validation anchor to null if anchor is not given. + match anchor { + None => self.validation_anchor.set(None), + Some(a) => { + if a == &*self.target_element || + !self + .target_element + .upcast::<Node>() + .is_shadow_including_inclusive_ancestor_of(a.upcast::<Node>()) + { + return Err(Error::NotFound); + } + self.validation_anchor.set(Some(a)); + }, + } + Ok(()) + } + + /// <https://html.spec.whatwg.org/multipage#dom-elementinternals-validationmessage> + fn GetValidationMessage(&self) -> Fallible<DOMString> { + // This check isn't in the spec but it's in WPT tests and it maintains + // consistency with other methods that do specify it + if !self.is_target_form_associated() { + return Err(Error::NotSupported); + } + Ok(self.validation_message.borrow().clone()) + } + + /// <https://html.spec.whatwg.org/multipage#dom-elementinternals-validity> + fn GetValidity(&self) -> Fallible<DomRoot<ValidityState>> { + if !self.is_target_form_associated() { + return Err(Error::NotSupported); + } + Ok(self.validity_state()) + } + + /// <https://html.spec.whatwg.org/multipage#dom-elementinternals-labels> + fn GetLabels(&self) -> Fallible<DomRoot<NodeList>> { + if !self.is_target_form_associated() { + return Err(Error::NotSupported); + } + Ok(self.labels_node_list.or_init(|| { + NodeList::new_labels_list( + self.target_element.upcast::<Node>().owner_doc().window(), + &*self.target_element, + ) + })) + } + + /// <https://html.spec.whatwg.org/multipage#dom-elementinternals-willvalidate> + fn GetWillValidate(&self) -> Fallible<bool> { + if !self.is_target_form_associated() { + return Err(Error::NotSupported); + } + Ok(self.is_instance_validatable()) + } + + /// <https://html.spec.whatwg.org/multipage#dom-elementinternals-form> + fn GetForm(&self) -> Fallible<Option<DomRoot<HTMLFormElement>>> { + if !self.is_target_form_associated() { + return Err(Error::NotSupported); + } + Ok(self.form_owner.get()) + } + + /// <https://html.spec.whatwg.org/multipage#dom-elementinternals-checkvalidity> + fn CheckValidity(&self) -> Fallible<bool> { + if !self.is_target_form_associated() { + return Err(Error::NotSupported); + } + Ok(self.check_validity()) + } + + /// <https://html.spec.whatwg.org/multipage#dom-elementinternals-reportvalidity> + fn ReportValidity(&self) -> Fallible<bool> { + if !self.is_target_form_associated() { + return Err(Error::NotSupported); + } + Ok(self.report_validity()) + } +} + +// Form-associated custom elements also need the Validatable trait. +impl Validatable for ElementInternals { + fn as_element(&self) -> &Element { + debug_assert!(self.is_target_form_associated()); + self.target_element.upcast::<Element>() + } + + fn validity_state(&self) -> DomRoot<ValidityState> { + debug_assert!(self.is_target_form_associated()); + self.validity_state.or_init(|| { + ValidityState::new( + &window_from_node(self.target_element.upcast::<Node>()), + self.target_element.upcast(), + ) + }) + } + + /// <https://html.spec.whatwg.org/multipage#candidate-for-constraint-validation> + fn is_instance_validatable(&self) -> bool { + debug_assert!(self.is_target_form_associated()); + if !self.target_element.is_submittable_element() { + return false; + } + + // The form-associated custom element is barred from constraint validation, + // if the readonly attribute is specified, the element is disabled, + // or the element has a datalist element ancestor. + !self.as_element().read_write_state() && + !self.as_element().disabled_state() && + !is_barred_by_datalist_ancestor(self.target_element.upcast::<Node>()) + } +} |