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 | |
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')
-rw-r--r-- | components/script/dom/customelementregistry.rs | 249 | ||||
-rw-r--r-- | components/script/dom/document.rs | 24 | ||||
-rw-r--r-- | components/script/dom/element.rs | 75 | ||||
-rw-r--r-- | components/script/dom/elementinternals.rs | 366 | ||||
-rw-r--r-- | components/script/dom/htmlelement.rs | 244 | ||||
-rw-r--r-- | components/script/dom/htmlfieldsetelement.rs | 88 | ||||
-rw-r--r-- | components/script/dom/htmlformelement.rs | 79 | ||||
-rw-r--r-- | components/script/dom/mod.rs | 1 | ||||
-rw-r--r-- | components/script/dom/node.rs | 14 | ||||
-rw-r--r-- | components/script/dom/raredata.rs | 3 | ||||
-rwxr-xr-x | components/script/dom/validation.rs | 23 | ||||
-rwxr-xr-x | components/script/dom/validitystate.rs | 60 | ||||
-rw-r--r-- | components/script/dom/webidls/ElementInternals.webidl | 41 | ||||
-rw-r--r-- | components/script/dom/webidls/HTMLElement.webidl | 2 |
14 files changed, 1122 insertions, 147 deletions
diff --git a/components/script/dom/customelementregistry.rs b/components/script/dom/customelementregistry.rs index 19315f55b0f..b704f675e50 100644 --- a/components/script/dom/customelementregistry.rs +++ b/components/script/dom/customelementregistry.rs @@ -12,7 +12,7 @@ use html5ever::{namespace_url, ns, LocalName, Namespace, Prefix}; use js::conversions::ToJSValConvertible; use js::glue::UnwrapObjectStatic; use js::jsapi::{HandleValueArray, Heap, IsCallable, IsConstructor, JSAutoRealm, JSObject}; -use js::jsval::{JSVal, NullValue, ObjectValue, UndefinedValue}; +use js::jsval::{BooleanValue, JSVal, NullValue, ObjectValue, UndefinedValue}; use js::rust::wrappers::{Construct1, JS_GetProperty, SameValue}; use js::rust::{HandleObject, HandleValue, MutableHandleValue}; @@ -41,6 +41,7 @@ use crate::dom::domexception::{DOMErrorName, DOMException}; use crate::dom::element::Element; use crate::dom::globalscope::GlobalScope; use crate::dom::htmlelement::HTMLElement; +use crate::dom::htmlformelement::{FormControl, HTMLFormElement}; use crate::dom::node::{document_from_node, window_from_node, Node, ShadowIncluding}; use crate::dom::promise::Promise; use crate::dom::window::Window; @@ -164,7 +165,8 @@ impl CustomElementRegistry { } /// <https://html.spec.whatwg.org/multipage/#dom-customelementregistry-define> - /// Steps 10.3, 10.4 + /// This function includes both steps 14.3 and 14.4 which add the callbacks to a map and + /// process them. #[allow(unsafe_code)] unsafe fn get_callbacks(&self, prototype: HandleObject) -> Fallible<LifecycleCallbacks> { let cx = GlobalScope::get_cx(); @@ -175,11 +177,34 @@ impl CustomElementRegistry { disconnected_callback: get_callback(cx, prototype, b"disconnectedCallback\0")?, adopted_callback: get_callback(cx, prototype, b"adoptedCallback\0")?, attribute_changed_callback: get_callback(cx, prototype, b"attributeChangedCallback\0")?, + + form_associated_callback: None, + form_disabled_callback: None, + form_reset_callback: None, + form_state_restore_callback: None, }) } /// <https://html.spec.whatwg.org/multipage/#dom-customelementregistry-define> - /// Step 10.6 + /// Step 14.13: Add form associated callbacks to LifecycleCallbacks + #[allow(unsafe_code)] + unsafe fn add_form_associated_callbacks( + &self, + prototype: HandleObject, + callbacks: &mut LifecycleCallbacks, + ) -> ErrorResult { + let cx = self.window.get_cx(); + + callbacks.form_associated_callback = + get_callback(cx, prototype, b"formAssociatedCallback\0")?; + callbacks.form_reset_callback = get_callback(cx, prototype, b"formResetCallback\0")?; + callbacks.form_disabled_callback = get_callback(cx, prototype, b"formDisabledCallback\0")?; + callbacks.form_state_restore_callback = + get_callback(cx, prototype, b"formStateRestoreCallback\0")?; + + Ok(()) + } + #[allow(unsafe_code)] fn get_observed_attributes(&self, constructor: HandleObject) -> Fallible<Vec<DOMString>> { let cx = GlobalScope::get_cx(); @@ -212,10 +237,75 @@ impl CustomElementRegistry { _ => Err(Error::JSFailed), } } + + /// <https://html.spec.whatwg.org/multipage/#dom-customelementregistry-define> + /// Step 14.11: Get the value of `formAssociated`. + #[allow(unsafe_code)] + fn get_form_associated_value(&self, constructor: HandleObject) -> Fallible<bool> { + let cx = self.window.get_cx(); + rooted!(in(*cx) let mut form_associated_value = UndefinedValue()); + if unsafe { + !JS_GetProperty( + *cx, + constructor, + b"formAssociated\0".as_ptr() as *const _, + form_associated_value.handle_mut(), + ) + } { + return Err(Error::JSFailed); + } + + if form_associated_value.is_undefined() { + return Ok(false); + } + + let conversion = + unsafe { FromJSValConvertible::from_jsval(*cx, form_associated_value.handle(), ()) }; + match conversion { + Ok(ConversionResult::Success(flag)) => Ok(flag), + Ok(ConversionResult::Failure(error)) => Err(Error::Type(error.into())), + _ => Err(Error::JSFailed), + } + } + + /// <https://html.spec.whatwg.org/multipage/#dom-customelementregistry-define> + /// Step 14.7: Get `disabledFeatures` value + #[allow(unsafe_code)] + fn get_disabled_features(&self, constructor: HandleObject) -> Fallible<Vec<DOMString>> { + let cx = self.window.get_cx(); + rooted!(in(*cx) let mut disabled_features = UndefinedValue()); + if unsafe { + !JS_GetProperty( + *cx, + constructor, + b"disabledFeatures\0".as_ptr() as *const _, + disabled_features.handle_mut(), + ) + } { + return Err(Error::JSFailed); + } + + if disabled_features.is_undefined() { + return Ok(Vec::new()); + } + + let conversion = unsafe { + FromJSValConvertible::from_jsval( + *cx, + disabled_features.handle(), + StringificationBehavior::Default, + ) + }; + match conversion { + Ok(ConversionResult::Success(attributes)) => Ok(attributes), + Ok(ConversionResult::Failure(error)) => Err(Error::Type(error.into())), + _ => Err(Error::JSFailed), + } + } } /// <https://html.spec.whatwg.org/multipage/#dom-customelementregistry-define> -/// Step 10.4 +/// Step 14.4: Get `callbackValue` for all `callbackName` in `lifecycleCallbacks`. #[allow(unsafe_code)] fn get_callback( cx: JSContext, @@ -323,7 +413,10 @@ impl CustomElementRegistryMethods for CustomElementRegistry { // Step 9 self.element_definition_is_running.set(true); - // Steps 10.1 - 10.2 + // Steps 10-13: Initialize `formAssociated`, `disableInternals`, `disableShadow`, and + // `observedAttributes` with default values, but this is done later. + + // Steps 14.1 - 14.2: Get the value of the prototype. rooted!(in(*cx) let mut prototype = UndefinedValue()); { let _ac = JSAutoRealm::new(*cx, constructor.get()); @@ -334,8 +427,12 @@ impl CustomElementRegistryMethods for CustomElementRegistry { }; // Steps 10.3 - 10.4 + // It would be easier to get all the callbacks in one pass after + // we know whether this definition is going to be form-associated, + // but the order of operations is specified and it's observable + // if one of the callback getters throws an exception. rooted!(in(*cx) let proto_object = prototype.to_object()); - let callbacks = { + let mut callbacks = { let _ac = JSAutoRealm::new(*cx, proto_object.get()); match unsafe { self.get_callbacks(proto_object.handle()) } { Ok(callbacks) => callbacks, @@ -346,7 +443,8 @@ impl CustomElementRegistryMethods for CustomElementRegistry { } }; - // Step 10.5 - 10.6 + // Step 14.5: Handle the case where with `attributeChangedCallback` on `lifecycleCallbacks` + // is not null. let observed_attributes = if callbacks.attribute_changed_callback.is_some() { let _ac = JSAutoRealm::new(*cx, constructor.get()); match self.get_observed_attributes(constructor.handle()) { @@ -360,26 +458,71 @@ impl CustomElementRegistryMethods for CustomElementRegistry { Vec::new() }; + // Steps 14.6 - 14.10: Handle `disabledFeatures`. + let (disable_internals, disable_shadow) = { + let _ac = JSAutoRealm::new(*cx, constructor.get()); + match self.get_disabled_features(constructor.handle()) { + Ok(sequence) => ( + sequence.iter().any(|s| *s == "internals"), + sequence.iter().any(|s| *s == "shadow"), + ), + Err(error) => { + self.element_definition_is_running.set(false); + return Err(error); + }, + } + }; + + // Step 14.11 - 14.12: Handle `formAssociated`. + let form_associated = { + let _ac = JSAutoRealm::new(*cx, constructor.get()); + match self.get_form_associated_value(constructor.handle()) { + Ok(flag) => flag, + Err(error) => { + self.element_definition_is_running.set(false); + return Err(error); + }, + } + }; + + // Steps 14.13: Add the `formAssociated` callbacks. + if form_associated { + let _ac = JSAutoRealm::new(*cx, proto_object.get()); + unsafe { + match self.add_form_associated_callbacks(proto_object.handle(), &mut callbacks) { + Err(error) => { + self.element_definition_is_running.set(false); + return Err(error); + }, + Ok(()) => {}, + } + } + } + self.element_definition_is_running.set(false); - // Step 11 + // Step 15: Set up the new custom element definition. let definition = Rc::new(CustomElementDefinition::new( name.clone(), local_name.clone(), constructor_, observed_attributes, callbacks, + form_associated, + disable_internals, + disable_shadow, )); - // Step 12 + // Step 16: Add definition to this CustomElementRegistry. self.definitions .borrow_mut() .insert(name.clone(), definition.clone()); - // Step 13 + // Step 17: Let document be this CustomElementRegistry's relevant global object's + // associated Document. let document = self.window.Document(); - // Steps 14-15 + // Steps 18-19: Enqueue custom elements upgrade reaction for upgrade candidates. for candidate in document .upcast::<Node>() .traverse_preorder(ShadowIncluding::Yes) @@ -489,6 +632,18 @@ pub struct LifecycleCallbacks { #[ignore_malloc_size_of = "Rc"] attribute_changed_callback: Option<Rc<Function>>, + + #[ignore_malloc_size_of = "Rc"] + form_associated_callback: Option<Rc<Function>>, + + #[ignore_malloc_size_of = "Rc"] + form_reset_callback: Option<Rc<Function>>, + + #[ignore_malloc_size_of = "Rc"] + form_disabled_callback: Option<Rc<Function>>, + + #[ignore_malloc_size_of = "Rc"] + form_state_restore_callback: Option<Rc<Function>>, } #[derive(Clone, JSTraceable, MallocSizeOf)] @@ -514,6 +669,12 @@ pub struct CustomElementDefinition { pub callbacks: LifecycleCallbacks, pub construction_stack: DomRefCell<Vec<ConstructionStackEntry>>, + + pub form_associated: bool, + + pub disable_internals: bool, + + pub disable_shadow: bool, } impl CustomElementDefinition { @@ -523,6 +684,9 @@ impl CustomElementDefinition { constructor: Rc<CustomElementConstructor>, observed_attributes: Vec<DOMString>, callbacks: LifecycleCallbacks, + form_associated: bool, + disable_internals: bool, + disable_shadow: bool, ) -> CustomElementDefinition { CustomElementDefinition { name, @@ -531,6 +695,9 @@ impl CustomElementDefinition { observed_attributes, callbacks, construction_stack: Default::default(), + form_associated: form_associated, + disable_internals: disable_internals, + disable_shadow: disable_shadow, } } @@ -676,7 +843,43 @@ pub fn upgrade_element(definition: Rc<CustomElementDefinition>, element: &Elemen return; } - // TODO Step 9: "If element is a form-associated custom element..." + // Step 9: handle with form-associated custom element + if let Some(html_element) = element.downcast::<HTMLElement>() { + if html_element.is_form_associated_custom_element() { + // We know this element is is form-associated, so we can use the implementation of + // `FormControl` for HTMLElement, which makes that assumption. + // Step 9.1: Reset the form owner of element + html_element.reset_form_owner(); + if let Some(form) = html_element.form_owner() { + // Even though the tree hasn't structurally mutated, + // HTMLCollections need to be invalidated. + form.upcast::<Node>().rev_version(); + // The spec tells us specifically to enqueue a formAssociated reaction + // here, but it also says to do that for resetting form owner in general, + // and we don't need two reactions. + } + + // Either enabled_state or disabled_state needs to be set, + // and the possibility of a disabled fieldset ancestor needs + // to be accounted for. (In the spec, being disabled is + // a fact that's true or false about a node at a given time, + // not a flag that belongs to the node and is updated, + // so it doesn't describe this check as an action.) + element.check_disabled_attribute(); + element.check_ancestors_disabled_state_for_form_control(); + element.update_read_write_state_from_readonly_attribute(); + + // Step 9.2: If element is disabled, then enqueue a custom element callback reaction + // with element. + if element.disabled_state() { + ScriptThread::enqueue_callback_reaction( + element, + CallbackReaction::FormDisabled(true), + Some(definition.clone()), + ) + } + } + } // Step 10 element.set_custom_element_state(CustomElementState::Custom); @@ -796,6 +999,9 @@ pub enum CallbackReaction { Disconnected, Adopted(DomRoot<Document>, DomRoot<Document>), AttributeChanged(LocalName, Option<DOMString>, Option<DOMString>, Namespace), + FormAssociated(Option<DomRoot<HTMLFormElement>>), + FormDisabled(bool), + FormReset, } /// <https://html.spec.whatwg.org/multipage/#processing-the-backup-element-queue> @@ -963,6 +1169,25 @@ impl CustomElementReactionStack { args, ) }, + CallbackReaction::FormAssociated(form) => { + let args = vec![Heap::default()]; + if let Some(form) = form { + args[0].set(ObjectValue(form.reflector().get_jsobject().get())); + } else { + args[0].set(NullValue()); + } + (definition.callbacks.form_associated_callback.clone(), args) + }, + CallbackReaction::FormDisabled(disabled) => { + let cx = GlobalScope::get_cx(); + rooted!(in(*cx) let mut disabled_value = BooleanValue(disabled)); + let args = vec![Heap::default()]; + args[0].set(disabled_value.get()); + (definition.callbacks.form_disabled_callback.clone(), args) + }, + CallbackReaction::FormReset => { + (definition.callbacks.form_reset_callback.clone(), Vec::new()) + }, }; // Step 3 diff --git a/components/script/dom/document.rs b/components/script/dom/document.rs index dbf0c3b3381..c66b82619ea 100644 --- a/components/script/dom/document.rs +++ b/components/script/dom/document.rs @@ -1256,7 +1256,8 @@ impl Document { debug!("{} on {:?}", mouse_event_type_string, node.debug_str()); // Prevent click event if form control element is disabled. if let MouseEventType::Click = mouse_event_type { - if el.click_event_filter_by_disabled_state() { + // The click event is filtered by the disabled state. + if el.is_actually_disabled() { return; } @@ -3975,27 +3976,6 @@ impl Document { } } -impl Element { - fn click_event_filter_by_disabled_state(&self) -> bool { - let node = self.upcast::<Node>(); - matches!(node.type_id(), NodeTypeId::Element(ElementTypeId::HTMLElement( - HTMLElementTypeId::HTMLButtonElement, - )) | - NodeTypeId::Element(ElementTypeId::HTMLElement( - HTMLElementTypeId::HTMLInputElement, - )) | - NodeTypeId::Element(ElementTypeId::HTMLElement( - HTMLElementTypeId::HTMLOptionElement, - )) | - NodeTypeId::Element(ElementTypeId::HTMLElement( - HTMLElementTypeId::HTMLSelectElement, - )) | - NodeTypeId::Element(ElementTypeId::HTMLElement( - HTMLElementTypeId::HTMLTextAreaElement, - )) if self.disabled_state()) - } -} - impl ProfilerMetadataFactory for Document { fn new_metadata(&self) -> Option<TimerMetadata> { Some(TimerMetadata { diff --git a/components/script/dom/element.rs b/components/script/dom/element.rs index e91bdf33992..d1337bbba82 100644 --- a/components/script/dom/element.rs +++ b/components/script/dom/element.rs @@ -102,6 +102,7 @@ use crate::dom::document::{ use crate::dom::documentfragment::DocumentFragment; use crate::dom::domrect::DOMRect; use crate::dom::domtokenlist::DOMTokenList; +use crate::dom::elementinternals::ElementInternals; use crate::dom::eventtarget::EventTarget; use crate::dom::htmlanchorelement::HTMLAnchorElement; use crate::dom::htmlbodyelement::{HTMLBodyElement, HTMLBodyElementLayoutHelpers}; @@ -145,6 +146,7 @@ use crate::dom::servoparser::ServoParser; use crate::dom::shadowroot::{IsUserAgentWidget, ShadowRoot}; use crate::dom::text::Text; use crate::dom::validation::Validatable; +use crate::dom::validitystate::ValidationFlags; use crate::dom::virtualmethods::{vtable_for, VirtualMethods}; use crate::dom::window::ReflowReason; use crate::script_thread::ScriptThread; @@ -1419,6 +1421,12 @@ impl Element { NodeTypeId::Element(ElementTypeId::HTMLElement( HTMLElementTypeId::HTMLOptionElement, )) => self.disabled_state(), + NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLElement)) => { + self.downcast::<HTMLElement>() + .unwrap() + .is_form_associated_custom_element() && + self.disabled_state() + }, // TODO: // an optgroup element that has a disabled attribute // a menuitem element that has a disabled attribute @@ -1857,10 +1865,17 @@ impl Element { // https://w3c.github.io/DOM-Parsing/#parsing pub fn parse_fragment(&self, markup: DOMString) -> Fallible<DomRoot<DocumentFragment>> { // Steps 1-2. - let context_document = document_from_node(self); // TODO(#11995): XML case. let new_children = ServoParser::parse_html_fragment(self, markup); // Step 3. + // See https://github.com/w3c/DOM-Parsing/issues/61. + let context_document = { + if let Some(template) = self.downcast::<HTMLTemplateElement>() { + template.Content().upcast::<Node>().owner_doc() + } else { + document_from_node(self) + } + }; let fragment = DocumentFragment::new(&context_document); // Step 4. for child in new_children { @@ -1973,6 +1988,24 @@ impl Element { document.perform_focus_fixup_rule(self); } } + + pub fn get_element_internals(&self) -> Option<DomRoot<ElementInternals>> { + self.rare_data() + .as_ref()? + .element_internals + .as_ref() + .map(|sr| DomRoot::from_ref(&**sr)) + } + + pub fn ensure_element_internals(&self) -> DomRoot<ElementInternals> { + let mut rare_data = self.ensure_rare_data(); + DomRoot::from_ref(rare_data.element_internals.get_or_insert_with(|| { + let elem = self + .downcast::<HTMLElement>() + .expect("ensure_element_internals should only be called for an HTMLElement"); + Dom::from_ref(&*ElementInternals::new(elem)) + })) + } } impl ElementMethods for Element { @@ -3098,6 +3131,9 @@ impl VirtualMethods for Element { self.super_type().unwrap().unbind_from_tree(context); if let Some(f) = self.as_maybe_form_control() { + // TODO: The valid state of ancestors might be wrong if the form control element + // has a fieldset ancestor, for instance: `<form><fieldset><input>`, + // if `<input>` is unbound, `<form><fieldset>` should trigger a call to `update_validity()`. f.unbind_form_control_from_tree(); } @@ -3543,6 +3579,38 @@ impl Element { element } + pub fn is_invalid(&self, needs_update: bool) -> bool { + if let Some(validatable) = self.as_maybe_validatable() { + if needs_update { + validatable + .validity_state() + .perform_validation_and_update(ValidationFlags::all()); + } + return validatable.is_instance_validatable() && !validatable.satisfies_constraints(); + } + + if let Some(internals) = self.get_element_internals() { + return internals.is_invalid(); + } + false + } + + pub fn is_instance_validatable(&self) -> bool { + if let Some(validatable) = self.as_maybe_validatable() { + return validatable.is_instance_validatable(); + } + if let Some(internals) = self.get_element_internals() { + return internals.is_instance_validatable(); + } + false + } + + pub fn init_state_for_internals(&self) { + self.set_enabled_state(true); + self.set_state(ElementState::VALID, true); + self.set_state(ElementState::INVALID, false); + } + pub fn click_in_progress(&self) -> bool { self.upcast::<Node>().get_flag(NodeFlags::CLICK_IN_PROGRESS) } @@ -3743,6 +3811,11 @@ impl Element { self.set_disabled_state(has_disabled_attrib); self.set_enabled_state(!has_disabled_attrib); } + + pub fn update_read_write_state_from_readonly_attribute(&self) { + let has_readonly_attribute = self.has_attribute(&local_name!("readonly")); + self.set_read_write_state(has_readonly_attribute); + } } #[derive(Clone, Copy)] 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>()) + } +} diff --git a/components/script/dom/htmlelement.rs b/components/script/dom/htmlelement.rs index 1eca57e2b60..4b62672e07e 100644 --- a/components/script/dom/htmlelement.rs +++ b/components/script/dom/htmlelement.rs @@ -22,28 +22,34 @@ use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMeth use crate::dom::bindings::codegen::Bindings::HTMLLabelElementBinding::HTMLLabelElementMethods; use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods; use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods; -use crate::dom::bindings::error::{Error, ErrorResult}; +use crate::dom::bindings::error::{Error, ErrorResult, Fallible}; use crate::dom::bindings::inheritance::{Castable, ElementTypeId, HTMLElementTypeId, NodeTypeId}; use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom}; use crate::dom::bindings::str::DOMString; use crate::dom::cssstyledeclaration::{CSSModificationAccess, CSSStyleDeclaration, CSSStyleOwner}; +use crate::dom::customelementregistry::CallbackReaction; use crate::dom::document::{Document, FocusType}; use crate::dom::documentfragment::DocumentFragment; use crate::dom::domstringmap::DOMStringMap; use crate::dom::element::{AttributeMutation, Element}; +use crate::dom::elementinternals::ElementInternals; use crate::dom::event::Event; use crate::dom::eventtarget::EventTarget; use crate::dom::htmlbodyelement::HTMLBodyElement; use crate::dom::htmlbrelement::HTMLBRElement; use crate::dom::htmldetailselement::HTMLDetailsElement; +use crate::dom::htmlformelement::{FormControl, HTMLFormElement}; use crate::dom::htmlframesetelement::HTMLFrameSetElement; use crate::dom::htmlhtmlelement::HTMLHtmlElement; use crate::dom::htmlinputelement::{HTMLInputElement, InputType}; use crate::dom::htmllabelelement::HTMLLabelElement; use crate::dom::htmltextareaelement::HTMLTextAreaElement; -use crate::dom::node::{document_from_node, window_from_node, Node, ShadowIncluding}; +use crate::dom::node::{ + document_from_node, window_from_node, BindContext, Node, ShadowIncluding, UnbindContext, +}; use crate::dom::text::Text; use crate::dom::virtualmethods::VirtualMethods; +use crate::script_thread::ScriptThread; #[dom_struct] pub struct HTMLElement { @@ -352,7 +358,7 @@ impl HTMLElementMethods for HTMLElement { // https://html.spec.whatwg.org/multipage/#dom-click fn Click(&self) { - let element = self.upcast::<Element>(); + let element = self.as_element(); if element.disabled_state() { return; } @@ -377,7 +383,7 @@ impl HTMLElementMethods for HTMLElement { // https://html.spec.whatwg.org/multipage/#dom-blur fn Blur(&self) { // TODO: Run the unfocusing steps. - if !self.upcast::<Element>().focus_state() { + if !self.as_element().focus_state() { return; } // https://html.spec.whatwg.org/multipage/#unfocusing-steps @@ -446,7 +452,7 @@ impl HTMLElementMethods for HTMLElement { fn InnerText(&self) -> DOMString { let node = self.upcast::<Node>(); let window = window_from_node(node); - let element = self.upcast::<Element>(); + let element = self.as_element(); // Step 1. let element_not_rendered = !node.is_connected() || !element.has_css_layout_box(); @@ -511,12 +517,12 @@ impl HTMLElementMethods for HTMLElement { // https://html.spec.whatwg.org/multipage/#dom-translate fn Translate(&self) -> bool { - self.upcast::<Element>().is_translate_enabled() + self.as_element().is_translate_enabled() } // https://html.spec.whatwg.org/multipage/#dom-translate fn SetTranslate(&self, yesno: bool) { - self.upcast::<Element>().set_string_attribute( + self.as_element().set_string_attribute( &html5ever::local_name!("translate"), match yesno { true => DOMString::from("yes"), @@ -528,7 +534,7 @@ impl HTMLElementMethods for HTMLElement { // https://html.spec.whatwg.org/multipage/#dom-contenteditable fn ContentEditable(&self) -> DOMString { // TODO: https://github.com/servo/servo/issues/12776 - self.upcast::<Element>() + self.as_element() .get_attribute(&ns!(), &local_name!("contenteditable")) .map(|attr| DOMString::from(&**attr.value())) .unwrap_or_else(|| DOMString::from("inherit")) @@ -545,6 +551,46 @@ impl HTMLElementMethods for HTMLElement { // TODO: https://github.com/servo/servo/issues/12776 false } + /// <https://html.spec.whatwg.org/multipage#dom-attachinternals> + fn AttachInternals(&self) -> Fallible<DomRoot<ElementInternals>> { + let element = self.as_element(); + // Step 1: If this's is value is not null, then throw a "NotSupportedError" DOMException + if element.get_is().is_some() { + return Err(Error::NotSupported); + } + + // Step 2: Let definition be the result of looking up a custom element definition + // Note: the element can pass this check without yet being a custom + // element, as long as there is a registered definition + // that could upgrade it to one later. + let registry = document_from_node(self).window().CustomElements(); + let definition = registry.lookup_definition(self.as_element().local_name(), None); + + // Step 3: If definition is null, then throw an "NotSupportedError" DOMException + let definition = match definition { + Some(definition) => definition, + None => return Err(Error::NotSupported), + }; + + // Step 4: If definition's disable internals is true, then throw a "NotSupportedError" DOMException + if definition.disable_internals { + return Err(Error::NotSupported); + } + + // Step 5: If this's attached internals is non-null, then throw an "NotSupportedError" DOMException + let internals = element.ensure_element_internals(); + if internals.attached() { + return Err(Error::NotSupported); + } + + if self.is_form_associated_custom_element() { + element.init_state_for_internals(); + } + + // Step 6-7: Set this's attached internals to a new ElementInternals instance + internals.set_attached(); + Ok(internals) + } } fn append_text_node_to_fragment(document: &Document, fragment: &DocumentFragment, text: String) { @@ -620,14 +666,14 @@ impl HTMLElement { { return Err(Error::Syntax); } - self.upcast::<Element>() + self.as_element() .set_custom_attribute(to_snake_case(name), value) } pub fn get_custom_attr(&self, local_name: DOMString) -> Option<DOMString> { // FIXME(ajeffrey): Convert directly from DOMString to LocalName let local_name = LocalName::from(to_snake_case(local_name)); - self.upcast::<Element>() + self.as_element() .get_attribute(&ns!(), &local_name) .map(|attr| { DOMString::from(&**attr.value()) // FIXME(ajeffrey): Convert directly from AttrValue to DOMString @@ -637,11 +683,10 @@ impl HTMLElement { pub fn delete_custom_attr(&self, local_name: DOMString) { // FIXME(ajeffrey): Convert directly from DOMString to LocalName let local_name = LocalName::from(to_snake_case(local_name)); - self.upcast::<Element>() - .remove_attribute(&ns!(), &local_name); + self.as_element().remove_attribute(&ns!(), &local_name); } - // https://html.spec.whatwg.org/multipage/#category-label + /// <https://html.spec.whatwg.org/multipage/#category-label> pub fn is_labelable_element(&self) -> bool { match self.upcast::<Node>().type_id() { NodeTypeId::Element(ElementTypeId::HTMLElement(type_id)) => match type_id { @@ -654,31 +699,54 @@ impl HTMLElement { HTMLElementTypeId::HTMLProgressElement | HTMLElementTypeId::HTMLSelectElement | HTMLElementTypeId::HTMLTextAreaElement => true, - _ => false, + _ => self.is_form_associated_custom_element(), }, _ => false, } } - // https://html.spec.whatwg.org/multipage/#category-listed + /// <https://html.spec.whatwg.org/multipage/#form-associated-custom-element> + pub fn is_form_associated_custom_element(&self) -> bool { + if let Some(definition) = self.as_element().get_custom_element_definition() { + definition.is_autonomous() && definition.form_associated + } else { + false + } + } + + /// <https://html.spec.whatwg.org/multipage/#category-listed> pub fn is_listed_element(&self) -> bool { match self.upcast::<Node>().type_id() { - NodeTypeId::Element(ElementTypeId::HTMLElement(type_id)) => matches!( - type_id, + NodeTypeId::Element(ElementTypeId::HTMLElement(type_id)) => match type_id { HTMLElementTypeId::HTMLButtonElement | - HTMLElementTypeId::HTMLFieldSetElement | - HTMLElementTypeId::HTMLInputElement | - HTMLElementTypeId::HTMLObjectElement | - HTMLElementTypeId::HTMLOutputElement | - HTMLElementTypeId::HTMLSelectElement | - HTMLElementTypeId::HTMLTextAreaElement - ), + HTMLElementTypeId::HTMLFieldSetElement | + HTMLElementTypeId::HTMLInputElement | + HTMLElementTypeId::HTMLObjectElement | + HTMLElementTypeId::HTMLOutputElement | + HTMLElementTypeId::HTMLSelectElement | + HTMLElementTypeId::HTMLTextAreaElement => true, + _ => self.is_form_associated_custom_element(), + }, + _ => false, + } + } + + /// <https://html.spec.whatwg.org/multipage/#category-submit> + pub fn is_submittable_element(&self) -> bool { + match self.upcast::<Node>().type_id() { + NodeTypeId::Element(ElementTypeId::HTMLElement(type_id)) => match type_id { + HTMLElementTypeId::HTMLButtonElement | + HTMLElementTypeId::HTMLInputElement | + HTMLElementTypeId::HTMLSelectElement | + HTMLElementTypeId::HTMLTextAreaElement => true, + _ => self.is_form_associated_custom_element(), + }, _ => false, } } pub fn supported_prop_names_custom_attr(&self) -> Vec<DOMString> { - let element = self.upcast::<Element>(); + let element = self.as_element(); element .attrs() .iter() @@ -692,7 +760,7 @@ impl HTMLElement { // https://html.spec.whatwg.org/multipage/#dom-lfe-labels // This gets the nth label in tree order. pub fn label_at(&self, index: u32) -> Option<DomRoot<Node>> { - let element = self.upcast::<Element>(); + let element = self.as_element(); // Traverse entire tree for <label> elements that have // this as their control. @@ -721,7 +789,7 @@ impl HTMLElement { // This counts the labels of the element, to support NodeList::Length pub fn labels_count(&self) -> u32 { // see label_at comments about performance - let element = self.upcast::<Element>(); + let element = self.as_element(); let root_element = element.root_element(); let root_node = root_element.upcast::<Node>(); root_node @@ -814,7 +882,7 @@ impl HTMLElement { .child_elements() .find(|el| el.local_name() == &local_name!("summary")); match first_summary_element { - Some(first_summary) => &*first_summary == self.upcast::<Element>(), + Some(first_summary) => &*first_summary == self.as_element(), None => false, } } @@ -822,11 +890,12 @@ impl HTMLElement { impl VirtualMethods for HTMLElement { fn super_type(&self) -> Option<&dyn VirtualMethods> { - Some(self.upcast::<Element>() as &dyn VirtualMethods) + Some(self.as_element() as &dyn VirtualMethods) } fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation) { self.super_type().unwrap().attribute_mutated(attr, mutation); + let element = self.as_element(); match (attr.local_name(), mutation) { (name, AttributeMutation::Set(_)) if name.starts_with("on") => { let evtarget = self.upcast::<EventTarget>(); @@ -839,10 +908,95 @@ impl VirtualMethods for HTMLElement { DOMString::from(&**attr.value()), ); }, + (&local_name!("form"), mutation) if self.is_form_associated_custom_element() => { + self.form_attribute_mutated(mutation); + }, + // Adding a "disabled" attribute disables an enabled form element. + (&local_name!("disabled"), AttributeMutation::Set(_)) + if self.is_form_associated_custom_element() && element.enabled_state() => + { + element.set_disabled_state(true); + element.set_enabled_state(false); + ScriptThread::enqueue_callback_reaction( + element, + CallbackReaction::FormDisabled(true), + None, + ); + }, + // Removing the "disabled" attribute may enable a disabled + // form element, but a fieldset ancestor may keep it disabled. + (&local_name!("disabled"), AttributeMutation::Removed) + if self.is_form_associated_custom_element() && element.disabled_state() => + { + element.set_disabled_state(false); + element.set_enabled_state(true); + element.check_ancestors_disabled_state_for_form_control(); + if element.enabled_state() { + ScriptThread::enqueue_callback_reaction( + element, + CallbackReaction::FormDisabled(false), + None, + ); + } + }, + (&local_name!("readonly"), mutation) if self.is_form_associated_custom_element() => { + match mutation { + AttributeMutation::Set(_) => { + element.set_read_write_state(true); + }, + AttributeMutation::Removed => { + element.set_read_write_state(false); + }, + } + }, _ => {}, } } + fn bind_to_tree(&self, context: &BindContext) { + if let Some(ref super_type) = self.super_type() { + super_type.bind_to_tree(context); + } + let element = self.as_element(); + element.update_sequentially_focusable_status(); + + // Binding to a tree can disable a form control if one of the new + // ancestors is a fieldset. + if self.is_form_associated_custom_element() && element.enabled_state() { + element.check_ancestors_disabled_state_for_form_control(); + if element.disabled_state() { + ScriptThread::enqueue_callback_reaction( + element, + CallbackReaction::FormDisabled(true), + None, + ); + } + } + } + + fn unbind_from_tree(&self, context: &UnbindContext) { + if let Some(ref super_type) = self.super_type() { + super_type.unbind_from_tree(context); + } + + // Unbinding from a tree might enable a form control, if a + // fieldset ancestor is the only reason it was disabled. + // (The fact that it's enabled doesn't do much while it's + // disconnected, but it is an observable fact to keep track of.) + let element = self.as_element(); + if self.is_form_associated_custom_element() && element.disabled_state() { + element.check_disabled_attribute(); + element.check_ancestors_disabled_state_for_form_control(); + if element.enabled_state() { + ScriptThread::enqueue_callback_reaction( + element, + CallbackReaction::FormDisabled(false), + None, + ); + } + } + } + fn parse_plain_attribute(&self, name: &LocalName, value: DOMString) -> AttrValue { match *name { local_name!("itemprop") => AttrValue::from_serialized_tokenlist(value.into()), @@ -869,3 +1023,35 @@ impl Activatable for HTMLElement { self.summary_activation_behavior(); } } +// Form-associated custom elements are the same interface type as +// normal HTMLElements, so HTMLElement needs to have the FormControl trait +// even though it's usually more specific trait implementations, like the +// HTMLInputElement one, that we really want. (Alternately we could put +// the FormControl trait on ElementInternals, but that raises lifetime issues.) +impl FormControl for HTMLElement { + fn form_owner(&self) -> Option<DomRoot<HTMLFormElement>> { + debug_assert!(self.is_form_associated_custom_element()); + self.as_element() + .get_element_internals() + .and_then(|e| e.form_owner()) + } + + fn set_form_owner(&self, form: Option<&HTMLFormElement>) { + debug_assert!(self.is_form_associated_custom_element()); + self.as_element() + .ensure_element_internals() + .set_form_owner(form); + } + + fn to_element<'a>(&'a self) -> &'a Element { + debug_assert!(self.is_form_associated_custom_element()); + self.as_element() + } + + fn is_listed(&self) -> bool { + debug_assert!(self.is_form_associated_custom_element()); + true + } + + // TODO candidate_for_validation, satisfies_constraints traits +} diff --git a/components/script/dom/htmlfieldsetelement.rs b/components/script/dom/htmlfieldsetelement.rs index 60d812fcff1..8e30889bb64 100644 --- a/components/script/dom/htmlfieldsetelement.rs +++ b/components/script/dom/htmlfieldsetelement.rs @@ -14,6 +14,7 @@ use crate::dom::bindings::codegen::Bindings::HTMLFieldSetElementBinding::HTMLFie use crate::dom::bindings::inheritance::{Castable, ElementTypeId, HTMLElementTypeId, NodeTypeId}; use crate::dom::bindings::root::{DomRoot, MutNullableDom}; use crate::dom::bindings::str::DOMString; +use crate::dom::customelementregistry::CallbackReaction; use crate::dom::document::Document; use crate::dom::element::{AttributeMutation, Element}; use crate::dom::htmlcollection::{CollectionFilter, HTMLCollection}; @@ -24,6 +25,7 @@ use crate::dom::node::{window_from_node, Node, ShadowIncluding}; use crate::dom::validation::Validatable; use crate::dom::validitystate::ValidityState; use crate::dom::virtualmethods::VirtualMethods; +use crate::script_thread::ScriptThread; #[dom_struct] pub struct HTMLFieldSetElement { @@ -71,14 +73,7 @@ impl HTMLFieldSetElement { .upcast::<Node>() .traverse_preorder(ShadowIncluding::No) .flat_map(DomRoot::downcast::<Element>) - .any(|element| { - if let Some(validatable) = element.as_maybe_validatable() { - validatable.is_instance_validatable() && - !validatable.validity_state().invalid_flags().is_empty() - } else { - false - } - }); + .any(|element| element.is_invalid(false)); self.upcast::<Element>() .set_state(ElementState::VALID, !has_invalid_child); @@ -169,9 +164,9 @@ impl VirtualMethods for HTMLFieldSetElement { AttributeMutation::Removed => false, }; let node = self.upcast::<Node>(); - let el = self.upcast::<Element>(); - el.set_disabled_state(disabled_state); - el.set_enabled_state(!disabled_state); + let element = self.upcast::<Element>(); + element.set_disabled_state(disabled_state); + element.set_enabled_state(!disabled_state); let mut found_legend = false; let children = node.children().filter(|node| { if found_legend { @@ -186,37 +181,64 @@ impl VirtualMethods for HTMLFieldSetElement { let fields = children.flat_map(|child| { child .traverse_preorder(ShadowIncluding::No) - .filter(|descendant| { - matches!( - descendant.type_id(), - NodeTypeId::Element(ElementTypeId::HTMLElement( - HTMLElementTypeId::HTMLButtonElement, - )) | NodeTypeId::Element(ElementTypeId::HTMLElement( - HTMLElementTypeId::HTMLInputElement, - )) | NodeTypeId::Element(ElementTypeId::HTMLElement( - HTMLElementTypeId::HTMLSelectElement, - )) | NodeTypeId::Element(ElementTypeId::HTMLElement( - HTMLElementTypeId::HTMLTextAreaElement, - )) - ) + .filter(|descendant| match descendant.type_id() { + NodeTypeId::Element(ElementTypeId::HTMLElement( + HTMLElementTypeId::HTMLButtonElement | + HTMLElementTypeId::HTMLInputElement | + HTMLElementTypeId::HTMLSelectElement | + HTMLElementTypeId::HTMLTextAreaElement, + )) => true, + NodeTypeId::Element(ElementTypeId::HTMLElement( + HTMLElementTypeId::HTMLElement, + )) => descendant + .downcast::<HTMLElement>() + .unwrap() + .is_form_associated_custom_element(), + _ => false, }) }); if disabled_state { for field in fields { - let el = field.downcast::<Element>().unwrap(); - el.set_disabled_state(true); - el.set_enabled_state(false); - el.update_sequentially_focusable_status(); + let element = field.downcast::<Element>().unwrap(); + if element.enabled_state() { + element.set_disabled_state(true); + element.set_enabled_state(false); + if element + .downcast::<HTMLElement>() + .map_or(false, |h| h.is_form_associated_custom_element()) + { + ScriptThread::enqueue_callback_reaction( + element, + CallbackReaction::FormDisabled(true), + None, + ); + } + } + element.update_sequentially_focusable_status(); } } else { for field in fields { - let el = field.downcast::<Element>().unwrap(); - el.check_disabled_attribute(); - el.check_ancestors_disabled_state_for_form_control(); - el.update_sequentially_focusable_status(); + let element = field.downcast::<Element>().unwrap(); + if element.disabled_state() { + element.check_disabled_attribute(); + element.check_ancestors_disabled_state_for_form_control(); + // Fire callback only if this has actually enabled the custom element + if element.enabled_state() && + element + .downcast::<HTMLElement>() + .map_or(false, |h| h.is_form_associated_custom_element()) + { + ScriptThread::enqueue_callback_reaction( + element, + CallbackReaction::FormDisabled(false), + None, + ); + } + } + element.update_sequentially_focusable_status(); } } - el.update_sequentially_focusable_status(); + element.update_sequentially_focusable_status(); }, local_name!("form") => { self.form_attribute_mutated(mutation); diff --git a/components/script/dom/htmlformelement.rs b/components/script/dom/htmlformelement.rs index 8ac0d17a7aa..e84a9fdd85d 100644 --- a/components/script/dom/htmlformelement.rs +++ b/components/script/dom/htmlformelement.rs @@ -46,6 +46,7 @@ use crate::dom::bindings::reflector::DomObject; use crate::dom::bindings::root::{Dom, DomOnceCell, DomRoot, MutNullableDom}; use crate::dom::bindings::str::DOMString; use crate::dom::blob::Blob; +use crate::dom::customelementregistry::CallbackReaction; use crate::dom::document::Document; use crate::dom::domtokenlist::DOMTokenList; use crate::dom::element::{AttributeMutation, Element}; @@ -77,9 +78,9 @@ use crate::dom::node::{ use crate::dom::nodelist::{NodeList, RadioListMode}; use crate::dom::radionodelist::RadioNodeList; use crate::dom::submitevent::SubmitEvent; -use crate::dom::validitystate::ValidationFlags; use crate::dom::virtualmethods::VirtualMethods; use crate::dom::window::Window; +use crate::script_thread::ScriptThread; use crate::task_source::TaskSource; #[derive(Clone, Copy, JSTraceable, MallocSizeOf, PartialEq)] @@ -363,6 +364,14 @@ impl HTMLFormElementMethods for HTMLFormElement { HTMLElementTypeId::HTMLTextAreaElement => { elem.downcast::<HTMLTextAreaElement>().unwrap().form_owner() }, + HTMLElementTypeId::HTMLElement => { + let html_element = elem.downcast::<HTMLElement>().unwrap(); + if html_element.is_form_associated_custom_element() { + html_element.form_owner() + } else { + return false; + } + }, _ => { debug_assert!(!elem .downcast::<HTMLElement>() @@ -673,14 +682,7 @@ impl HTMLFormElement { pub fn update_validity(&self) { let controls = self.controls.borrow(); - - let is_any_invalid = controls - .iter() - .filter_map(|control| control.as_maybe_validatable()) - .any(|validatable| { - validatable.is_instance_validatable() && - !validatable.validity_state().invalid_flags().is_empty() - }); + let is_any_invalid = controls.iter().any(|control| control.is_invalid(false)); self.upcast::<Element>() .set_state(ElementState::VALID, !is_any_invalid); @@ -1027,6 +1029,8 @@ impl HTMLFormElement { } } + // If it's form-associated and has a validation anchor, point the + // user there instead of the element itself. // Step 4 Err(()) } @@ -1039,20 +1043,11 @@ impl HTMLFormElement { let invalid_controls = controls .iter() .filter_map(|field| { - if let Some(el) = field.downcast::<Element>() { - let validatable = match el.as_maybe_validatable() { - Some(v) => v, - None => return None, - }; - validatable - .validity_state() - .perform_validation_and_update(ValidationFlags::all()); - if !validatable.is_instance_validatable() || - validatable.validity_state().invalid_flags().is_empty() - { - None + if let Some(element) = field.downcast::<Element>() { + if element.is_invalid(true) { + Some(DomRoot::from_ref(element)) } else { - Some(DomRoot::from_ref(el)) + None } } else { None @@ -1135,6 +1130,15 @@ impl HTMLFormElement { }); } }, + HTMLElementTypeId::HTMLElement => { + let custom = child.downcast::<HTMLElement>().unwrap(); + if custom.is_form_associated_custom_element() { + // https://html.spec.whatwg.org/multipage/#face-entry-construction + let internals = custom.upcast::<Element>().ensure_element_internals(); + internals.perform_entry_construction(&mut data_set); + // Otherwise no form value has been set so there is nothing to do. + } + }, _ => (), } } @@ -1287,6 +1291,16 @@ impl HTMLFormElement { )) => { child.downcast::<HTMLOutputElement>().unwrap().reset(); }, + NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLElement)) => { + let html_element = child.downcast::<HTMLElement>().unwrap(); + if html_element.is_form_associated_custom_element() { + ScriptThread::enqueue_callback_reaction( + html_element.upcast::<Element>(), + CallbackReaction::FormReset, + None, + ) + } + }, _ => {}, } } @@ -1550,6 +1564,19 @@ pub trait FormControl: DomObject { if let Some(ref new_owner) = new_owner { new_owner.add_control(self); } + // https://html.spec.whatwg.org/multipage/#custom-element-reactions:reset-the-form-owner + if let Some(html_elem) = elem.downcast::<HTMLElement>() { + if html_elem.is_form_associated_custom_element() { + ScriptThread::enqueue_callback_reaction( + elem, + CallbackReaction::FormAssociated(match new_owner { + None => None, + Some(ref form) => Some(DomRoot::from_ref(&**form)), + }), + None, + ) + } + } self.set_form_owner(new_owner.as_deref()); } } @@ -1745,7 +1772,13 @@ impl FormControlElementHelpers for Element { NodeTypeId::Element(ElementTypeId::HTMLElement( HTMLElementTypeId::HTMLTextAreaElement, )) => Some(self.downcast::<HTMLTextAreaElement>().unwrap() as &dyn FormControl), - _ => None, + _ => self.downcast::<HTMLElement>().and_then(|elem| { + if elem.is_form_associated_custom_element() { + Some(elem as &dyn FormControl) + } else { + None + } + }), } } } diff --git a/components/script/dom/mod.rs b/components/script/dom/mod.rs index 590a90f9370..3c738b52d3b 100644 --- a/components/script/dom/mod.rs +++ b/components/script/dom/mod.rs @@ -299,6 +299,7 @@ pub mod domstringmap; pub mod domtokenlist; pub mod dynamicmoduleowner; pub mod element; +pub mod elementinternals; pub mod errorevent; pub mod event; pub mod eventsource; diff --git a/components/script/dom/node.rs b/components/script/dom/node.rs index f37f51562e2..9e6a98a63dc 100644 --- a/components/script/dom/node.rs +++ b/components/script/dom/node.rs @@ -738,7 +738,7 @@ impl Node { parent.ancestors().any(|ancestor| &*ancestor == self) } - fn is_shadow_including_inclusive_ancestor_of(&self, node: &Node) -> bool { + pub fn is_shadow_including_inclusive_ancestor_of(&self, node: &Node) -> bool { node.inclusive_ancestors(ShadowIncluding::Yes) .any(|ancestor| &*ancestor == self) } @@ -2092,19 +2092,17 @@ impl Node { .traverse_preorder(ShadowIncluding::Yes) .filter_map(DomRoot::downcast::<Element>) { - // Step 7.7.2. - if descendant.is_connected() { - if descendant.get_custom_element_definition().is_some() { - // Step 7.7.2.1. + // Step 7.7.2, whatwg/dom#833 + if descendant.get_custom_element_definition().is_some() { + if descendant.is_connected() { ScriptThread::enqueue_callback_reaction( &descendant, CallbackReaction::Connected, None, ); - } else { - // Step 7.7.2.2. - try_upgrade_element(&descendant); } + } else { + try_upgrade_element(&*descendant); } } } diff --git a/components/script/dom/raredata.rs b/components/script/dom/raredata.rs index 836e15d2a48..df32b27f007 100644 --- a/components/script/dom/raredata.rs +++ b/components/script/dom/raredata.rs @@ -11,6 +11,7 @@ use crate::dom::bindings::root::Dom; use crate::dom::customelementregistry::{ CustomElementDefinition, CustomElementReaction, CustomElementState, }; +use crate::dom::elementinternals::ElementInternals; use crate::dom::mutationobserver::RegisteredObserver; use crate::dom::node::UniqueId; use crate::dom::shadowroot::ShadowRoot; @@ -54,4 +55,6 @@ pub struct ElementRareData { /// The client rect reported by layout. #[no_trace] pub client_rect: Option<LayoutValue<Rect<i32>>>, + /// <https://html.spec.whatwg.org/multipage#elementinternals> + pub element_internals: Option<Dom<ElementInternals>>, } diff --git a/components/script/dom/validation.rs b/components/script/dom/validation.rs index ab92d5b07f5..dadef038600 100755 --- a/components/script/dom/validation.rs +++ b/components/script/dom/validation.rs @@ -17,10 +17,10 @@ use crate::dom::validitystate::{ValidationFlags, ValidityState}; pub trait Validatable { fn as_element(&self) -> ∈ - // https://html.spec.whatwg.org/multipage/#dom-cva-validity + /// <https://html.spec.whatwg.org/multipage/#dom-cva-validity> fn validity_state(&self) -> DomRoot<ValidityState>; - // https://html.spec.whatwg.org/multipage/#candidate-for-constraint-validation + /// <https://html.spec.whatwg.org/multipage/#candidate-for-constraint-validation> fn is_instance_validatable(&self) -> bool; // Check if element satisfies its constraints, excluding custom errors @@ -28,9 +28,14 @@ pub trait Validatable { ValidationFlags::empty() } - // https://html.spec.whatwg.org/multipage/#check-validity-steps + /// <https://html.spec.whatwg.org/multipage/#concept-fv-valid> + fn satisfies_constraints(&self) -> bool { + self.validity_state().invalid_flags().is_empty() + } + + /// <https://html.spec.whatwg.org/multipage/#check-validity-steps> fn check_validity(&self) -> bool { - if self.is_instance_validatable() && !self.validity_state().invalid_flags().is_empty() { + if self.is_instance_validatable() && !self.satisfies_constraints() { self.as_element() .upcast::<EventTarget>() .fire_cancelable_event(atom!("invalid")); @@ -40,15 +45,14 @@ pub trait Validatable { } } - // https://html.spec.whatwg.org/multipage/#report-validity-steps + /// <https://html.spec.whatwg.org/multipage/#report-validity-steps> fn report_validity(&self) -> bool { // Step 1. if !self.is_instance_validatable() { return true; } - let flags = self.validity_state().invalid_flags(); - if flags.is_empty() { + if self.satisfies_constraints() { return true; } @@ -60,6 +64,7 @@ pub trait Validatable { // Step 1.2. if !event.DefaultPrevented() { + let flags = self.validity_state().invalid_flags(); println!( "Validation error: {}", validation_message_for_flags(&self.validity_state(), flags) @@ -73,7 +78,7 @@ pub trait Validatable { false } - // https://html.spec.whatwg.org/multipage/#dom-cva-validationmessage + /// <https://html.spec.whatwg.org/multipage/#dom-cva-validationmessage> fn validation_message(&self) -> DOMString { if self.is_instance_validatable() { let flags = self.validity_state().invalid_flags(); @@ -84,7 +89,7 @@ pub trait Validatable { } } -// https://html.spec.whatwg.org/multipage/#the-datalist-element%3Abarred-from-constraint-validation +/// <https://html.spec.whatwg.org/multipage/#the-datalist-element%3Abarred-from-constraint-validation> pub fn is_barred_by_datalist_ancestor(elem: &Node) -> bool { elem.upcast::<Node>() .ancestors() diff --git a/components/script/dom/validitystate.rs b/components/script/dom/validitystate.rs index a3dff8b585f..d91fb345d60 100755 --- a/components/script/dom/validitystate.rs +++ b/components/script/dom/validitystate.rs @@ -10,6 +10,7 @@ use dom_struct::dom_struct; use itertools::Itertools; use style_traits::dom::ElementState; +use super::bindings::codegen::Bindings::ElementInternalsBinding::ValidityStateFlags; use crate::dom::bindings::cell::{DomRefCell, Ref}; use crate::dom::bindings::codegen::Bindings::ValidityStateBinding::ValidityStateMethods; use crate::dom::bindings::inheritance::Castable; @@ -129,20 +130,22 @@ impl ValidityState { self.update_pseudo_classes(); } + pub fn update_invalid_flags(&self, update_flags: ValidationFlags) { + self.invalid_flags.set(update_flags); + } + pub fn invalid_flags(&self) -> ValidationFlags { self.invalid_flags.get() } - fn update_pseudo_classes(&self) { - if let Some(validatable) = self.element.as_maybe_validatable() { - if validatable.is_instance_validatable() { - let is_valid = self.invalid_flags.get().is_empty(); - self.element.set_state(ElementState::VALID, is_valid); - self.element.set_state(ElementState::INVALID, !is_valid); - } else { - self.element.set_state(ElementState::VALID, false); - self.element.set_state(ElementState::INVALID, false); - } + pub fn update_pseudo_classes(&self) { + if self.element.is_instance_validatable() { + let is_valid = self.invalid_flags.get().is_empty(); + self.element.set_state(ElementState::VALID, is_valid); + self.element.set_state(ElementState::INVALID, !is_valid); + } else { + self.element.set_state(ElementState::VALID, false); + self.element.set_state(ElementState::INVALID, false); } if let Some(form_control) = self.element.as_maybe_form_control() { @@ -225,3 +228,40 @@ impl ValidityStateMethods for ValidityState { self.invalid_flags().is_empty() } } + +impl From<&ValidityStateFlags> for ValidationFlags { + fn from(flags: &ValidityStateFlags) -> Self { + let mut bits = ValidationFlags::empty(); + if flags.valueMissing { + bits |= ValidationFlags::VALUE_MISSING; + } + if flags.typeMismatch { + bits |= ValidationFlags::TYPE_MISMATCH; + } + if flags.patternMismatch { + bits |= ValidationFlags::PATTERN_MISMATCH; + } + if flags.tooLong { + bits |= ValidationFlags::TOO_LONG; + } + if flags.tooShort { + bits |= ValidationFlags::TOO_SHORT; + } + if flags.rangeUnderflow { + bits |= ValidationFlags::RANGE_UNDERFLOW; + } + if flags.rangeOverflow { + bits |= ValidationFlags::RANGE_OVERFLOW; + } + if flags.stepMismatch { + bits |= ValidationFlags::STEP_MISMATCH; + } + if flags.badInput { + bits |= ValidationFlags::BAD_INPUT; + } + if flags.customError { + bits |= ValidationFlags::CUSTOM_ERROR; + } + bits + } +} diff --git a/components/script/dom/webidls/ElementInternals.webidl b/components/script/dom/webidls/ElementInternals.webidl new file mode 100644 index 00000000000..fbb0e720733 --- /dev/null +++ b/components/script/dom/webidls/ElementInternals.webidl @@ -0,0 +1,41 @@ +/* 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/. */ + +// https://html.spec.whatwg.org/multipage/#elementinternals +[Exposed=Window] +interface ElementInternals { + // Form-associated custom elements + + [Throws] undefined setFormValue((File or USVString or FormData)? value, + optional (File or USVString or FormData)? state); + + [Throws] readonly attribute HTMLFormElement? form; + + // flags shouldn't be optional here, #25704 + [Throws] undefined setValidity(optional ValidityStateFlags flags = {}, + optional DOMString message, + optional HTMLElement anchor); + [Throws] readonly attribute boolean willValidate; + [Throws] readonly attribute ValidityState validity; + [Throws] readonly attribute DOMString validationMessage; + [Throws] boolean checkValidity(); + [Throws] boolean reportValidity(); + + [Throws] readonly attribute NodeList labels; +}; + +// https://html.spec.whatwg.org/multipage/#elementinternals +dictionary ValidityStateFlags { + boolean valueMissing = false; + boolean typeMismatch = false; + boolean patternMismatch = false; + boolean tooLong = false; + boolean tooShort = false; + boolean rangeUnderflow = false; + boolean rangeOverflow = false; + boolean stepMismatch = false; + boolean badInput = false; + boolean customError = false; +}; + diff --git a/components/script/dom/webidls/HTMLElement.webidl b/components/script/dom/webidls/HTMLElement.webidl index ebe52aa854b..9e97c1b49e8 100644 --- a/components/script/dom/webidls/HTMLElement.webidl +++ b/components/script/dom/webidls/HTMLElement.webidl @@ -50,6 +50,8 @@ interface HTMLElement : Element { attribute [LegacyNullToEmptyString] DOMString innerText; + [Throws] ElementInternals attachInternals(); + // command API // readonly attribute DOMString? commandType; // readonly attribute DOMString? commandLabel; |