aboutsummaryrefslogtreecommitdiffstats
path: root/components
diff options
context:
space:
mode:
authorcathiechen <cathiechen@igalia.com>2024-04-11 15:17:11 +0200
committerGitHub <noreply@github.com>2024-04-11 13:17:11 +0000
commit4e4a4c0a28fb571991470f26ea82b8a897153788 (patch)
tree711026c2592e76bf6c573d14864c4a9af80a1be1 /components
parent2eb959a159874fa62a0844b31791698b74f3c959 (diff)
downloadservo-4e4a4c0a28fb571991470f26ea82b8a897153788.tar.gz
servo-4e4a4c0a28fb571991470f26ea82b8a897153788.zip
Implement form-associated custom elements and their ElementInternals (#31980)
* FACEs work, setFormValue test is awful so now has _mozilla backup * 1. Impl Validatable in ElementInternals instead of HTMLElement. 2. Reuse the code in Validatable trait. 3. The form associated custom element is not a customized built-in element. * add some comments * support readonly attribute and complete barred from constraint validation * Addressed the code review comments * Updated the legacy-layout results * Fixed the WPT failures in ElementInternals-validation.html * Addressed the code review comments * Review suggestions * Fixed silly mistakes and update the test result outside elementinternals * update the test results --------- Co-authored-by: Patrick Shaughnessy <pshaughn@comcast.net> Co-authored-by: Martin Robinson <mrobinson@igalia.com>
Diffstat (limited to 'components')
-rw-r--r--components/script/dom/customelementregistry.rs249
-rw-r--r--components/script/dom/document.rs24
-rw-r--r--components/script/dom/element.rs75
-rw-r--r--components/script/dom/elementinternals.rs366
-rw-r--r--components/script/dom/htmlelement.rs244
-rw-r--r--components/script/dom/htmlfieldsetelement.rs88
-rw-r--r--components/script/dom/htmlformelement.rs79
-rw-r--r--components/script/dom/mod.rs1
-rw-r--r--components/script/dom/node.rs14
-rw-r--r--components/script/dom/raredata.rs3
-rwxr-xr-xcomponents/script/dom/validation.rs23
-rwxr-xr-xcomponents/script/dom/validitystate.rs60
-rw-r--r--components/script/dom/webidls/ElementInternals.webidl41
-rw-r--r--components/script/dom/webidls/HTMLElement.webidl2
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) -> &Element;
- // 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;