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
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue