aboutsummaryrefslogtreecommitdiffstats
path: root/components/script/dom/elementinternals.rs
diff options
context:
space:
mode:
authorcathiechen <cathiechen@igalia.com>2024-04-11 15:17:11 +0200
committerGitHub <noreply@github.com>2024-04-11 13:17:11 +0000
commit4e4a4c0a28fb571991470f26ea82b8a897153788 (patch)
tree711026c2592e76bf6c573d14864c4a9af80a1be1 /components/script/dom/elementinternals.rs
parent2eb959a159874fa62a0844b31791698b74f3c959 (diff)
downloadservo-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.rs366
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>())
+ }
+}