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 { } /// - /// 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 { 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, }) } /// - /// 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> { let cx = GlobalScope::get_cx(); @@ -212,10 +237,75 @@ impl CustomElementRegistry { _ => Err(Error::JSFailed), } } + + /// + /// Step 14.11: Get the value of `formAssociated`. + #[allow(unsafe_code)] + fn get_form_associated_value(&self, constructor: HandleObject) -> Fallible { + 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), + } + } + + /// + /// Step 14.7: Get `disabledFeatures` value + #[allow(unsafe_code)] + fn get_disabled_features(&self, constructor: HandleObject) -> Fallible> { + 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), + } + } } /// -/// 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::() .traverse_preorder(ShadowIncluding::Yes) @@ -489,6 +632,18 @@ pub struct LifecycleCallbacks { #[ignore_malloc_size_of = "Rc"] attribute_changed_callback: Option>, + + #[ignore_malloc_size_of = "Rc"] + form_associated_callback: Option>, + + #[ignore_malloc_size_of = "Rc"] + form_reset_callback: Option>, + + #[ignore_malloc_size_of = "Rc"] + form_disabled_callback: Option>, + + #[ignore_malloc_size_of = "Rc"] + form_state_restore_callback: Option>, } #[derive(Clone, JSTraceable, MallocSizeOf)] @@ -514,6 +669,12 @@ pub struct CustomElementDefinition { pub callbacks: LifecycleCallbacks, pub construction_stack: DomRefCell>, + + pub form_associated: bool, + + pub disable_internals: bool, + + pub disable_shadow: bool, } impl CustomElementDefinition { @@ -523,6 +684,9 @@ impl CustomElementDefinition { constructor: Rc, observed_attributes: Vec, 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, 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::() { + 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::().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, DomRoot), AttributeChanged(LocalName, Option, Option, Namespace), + FormAssociated(Option>), + FormDisabled(bool), + FormReset, } /// @@ -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::(); - 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 { 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::() + .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> { // 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::() { + template.Content().upcast::().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> { + self.rare_data() + .as_ref()? + .element_internals + .as_ref() + .map(|sr| DomRoot::from_ref(&**sr)) + } + + pub fn ensure_element_internals(&self) -> DomRoot { + let mut rare_data = self.ensure_rare_data(); + DomRoot::from_ref(rare_data.element_internals.get_or_insert_with(|| { + let elem = self + .downcast::() + .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: `
`, + // if `` is unbound, `
` 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::().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), + FormData(Vec), + USVString(USVString), + None, +} + +impl From> 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, + target_element: Dom, + validity_state: MutNullableDom, + validation_message: DomRefCell, + custom_validity_error_message: DomRefCell, + validation_anchor: MutNullableDom, + submission_value: DomRefCell, + state: DomRefCell, + form_owner: MutNullableDom, + labels_node_list: MutNullableDom, +} + +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 { + 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> { + 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) { + if self + .target_element + .upcast::() + .has_attribute(&local_name!("disabled")) + { + warn!("We are in perform_entry_construction on an element with disabled attribute!"); + } + if self.target_element.upcast::().disabled_state() { + warn!("We are in perform_entry_construction on an element with disabled bit!"); + } + if !self.target_element.upcast::().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::() + .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 { + /// + fn SetFormValue( + &self, + value: Option, + maybe_state: Option>, + ) -> 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(()) + } + + /// + fn SetValidity( + &self, + flags: &ValidityStateFlags, + message: Option, + 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::() + .is_shadow_including_inclusive_ancestor_of(a.upcast::()) + { + return Err(Error::NotFound); + } + self.validation_anchor.set(Some(a)); + }, + } + Ok(()) + } + + /// + fn GetValidationMessage(&self) -> Fallible { + // 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()) + } + + /// + fn GetValidity(&self) -> Fallible> { + if !self.is_target_form_associated() { + return Err(Error::NotSupported); + } + Ok(self.validity_state()) + } + + /// + fn GetLabels(&self) -> Fallible> { + 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::().owner_doc().window(), + &*self.target_element, + ) + })) + } + + /// + fn GetWillValidate(&self) -> Fallible { + if !self.is_target_form_associated() { + return Err(Error::NotSupported); + } + Ok(self.is_instance_validatable()) + } + + /// + fn GetForm(&self) -> Fallible>> { + if !self.is_target_form_associated() { + return Err(Error::NotSupported); + } + Ok(self.form_owner.get()) + } + + /// + fn CheckValidity(&self) -> Fallible { + if !self.is_target_form_associated() { + return Err(Error::NotSupported); + } + Ok(self.check_validity()) + } + + /// + fn ReportValidity(&self) -> Fallible { + 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::() + } + + fn validity_state(&self) -> DomRoot { + debug_assert!(self.is_target_form_associated()); + self.validity_state.or_init(|| { + ValidityState::new( + &window_from_node(self.target_element.upcast::()), + self.target_element.upcast(), + ) + }) + } + + /// + 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::()) + } +} 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::(); + 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::().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::(); let window = window_from_node(node); - let element = self.upcast::(); + 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::().is_translate_enabled() + self.as_element().is_translate_enabled() } // https://html.spec.whatwg.org/multipage/#dom-translate fn SetTranslate(&self, yesno: bool) { - self.upcast::().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::() + 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 } + /// + fn AttachInternals(&self) -> Fallible> { + 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::() + self.as_element() .set_custom_attribute(to_snake_case(name), value) } pub fn get_custom_attr(&self, local_name: DOMString) -> Option { // FIXME(ajeffrey): Convert directly from DOMString to LocalName let local_name = LocalName::from(to_snake_case(local_name)); - self.upcast::() + 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::() - .remove_attribute(&ns!(), &local_name); + self.as_element().remove_attribute(&ns!(), &local_name); } - // https://html.spec.whatwg.org/multipage/#category-label + /// pub fn is_labelable_element(&self) -> bool { match self.upcast::().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 + /// + 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 + } + } + + /// pub fn is_listed_element(&self) -> bool { match self.upcast::().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, + } + } + + /// + pub fn is_submittable_element(&self) -> bool { + match self.upcast::().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 { - let element = self.upcast::(); + 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> { - let element = self.upcast::(); + let element = self.as_element(); // Traverse entire tree for