mirror of
https://github.com/servo/servo.git
synced 2025-08-03 04:30:10 +01:00
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>
This commit is contained in:
parent
2eb959a159
commit
4e4a4c0a28
67 changed files with 1641 additions and 619 deletions
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)]
|
||||
|
|
366
components/script/dom/elementinternals.rs
Normal file
366
components/script/dom/elementinternals.rs
Normal file
|
@ -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>())
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>>,
|
||||
}
|
||||
|
|
|
@ -17,10 +17,10 @@ use crate::dom::validitystate::{ValidationFlags, ValidityState};
|
|||
pub trait Validatable {
|
||||
fn as_element(&self) -> ∈
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-cva-validity
|
||||
/// <https://html.spec.whatwg.org/multipage/#dom-cva-validity>
|
||||
fn validity_state(&self) -> DomRoot<ValidityState>;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#candidate-for-constraint-validation
|
||||
/// <https://html.spec.whatwg.org/multipage/#candidate-for-constraint-validation>
|
||||
fn is_instance_validatable(&self) -> bool;
|
||||
|
||||
// Check if element satisfies its constraints, excluding custom errors
|
||||
|
@ -28,9 +28,14 @@ pub trait Validatable {
|
|||
ValidationFlags::empty()
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#check-validity-steps
|
||||
/// <https://html.spec.whatwg.org/multipage/#concept-fv-valid>
|
||||
fn satisfies_constraints(&self) -> bool {
|
||||
self.validity_state().invalid_flags().is_empty()
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#check-validity-steps>
|
||||
fn check_validity(&self) -> bool {
|
||||
if self.is_instance_validatable() && !self.validity_state().invalid_flags().is_empty() {
|
||||
if self.is_instance_validatable() && !self.satisfies_constraints() {
|
||||
self.as_element()
|
||||
.upcast::<EventTarget>()
|
||||
.fire_cancelable_event(atom!("invalid"));
|
||||
|
@ -40,15 +45,14 @@ pub trait Validatable {
|
|||
}
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#report-validity-steps
|
||||
/// <https://html.spec.whatwg.org/multipage/#report-validity-steps>
|
||||
fn report_validity(&self) -> bool {
|
||||
// Step 1.
|
||||
if !self.is_instance_validatable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
let flags = self.validity_state().invalid_flags();
|
||||
if flags.is_empty() {
|
||||
if self.satisfies_constraints() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -60,6 +64,7 @@ pub trait Validatable {
|
|||
|
||||
// Step 1.2.
|
||||
if !event.DefaultPrevented() {
|
||||
let flags = self.validity_state().invalid_flags();
|
||||
println!(
|
||||
"Validation error: {}",
|
||||
validation_message_for_flags(&self.validity_state(), flags)
|
||||
|
@ -73,7 +78,7 @@ pub trait Validatable {
|
|||
false
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-cva-validationmessage
|
||||
/// <https://html.spec.whatwg.org/multipage/#dom-cva-validationmessage>
|
||||
fn validation_message(&self) -> DOMString {
|
||||
if self.is_instance_validatable() {
|
||||
let flags = self.validity_state().invalid_flags();
|
||||
|
@ -84,7 +89,7 @@ pub trait Validatable {
|
|||
}
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#the-datalist-element%3Abarred-from-constraint-validation
|
||||
/// <https://html.spec.whatwg.org/multipage/#the-datalist-element%3Abarred-from-constraint-validation>
|
||||
pub fn is_barred_by_datalist_ancestor(elem: &Node) -> bool {
|
||||
elem.upcast::<Node>()
|
||||
.ancestors()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
41
components/script/dom/webidls/ElementInternals.webidl
Normal file
41
components/script/dom/webidls/ElementInternals.webidl
Normal file
|
@ -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;
|
||||
};
|
||||
|
|
@ -50,6 +50,8 @@ interface HTMLElement : Element {
|
|||
|
||||
attribute [LegacyNullToEmptyString] DOMString innerText;
|
||||
|
||||
[Throws] ElementInternals attachInternals();
|
||||
|
||||
// command API
|
||||
// readonly attribute DOMString? commandType;
|
||||
// readonly attribute DOMString? commandLabel;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue