servo/components/script/dom/htmlelement.rs
Keith Yeung da1c49299b
Update the list of global and window event handlers (#36894)
There's an expanded list of event handlers content attributes that need
to be forwarded to the corresponding window of the body element, and
this PR adds the remaining ones. The full list can be seen in [this
link](https://html.spec.whatwg.org/multipage/webappapis.html#event-handlers-on-elements%2C-document-objects%2C-and-window-objects%3Aevent-handlers-6).

Testing: Covered by various WPT tests

---------

Signed-off-by: Keith Yeung <kungfukeith11@gmail.com>
2025-05-10 15:54:13 +00:00

1247 lines
46 KiB
Rust

/* 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::collections::HashSet;
use std::default::Default;
use std::rc::Rc;
use dom_struct::dom_struct;
use html5ever::{LocalName, Prefix, local_name, ns};
use js::rust::HandleObject;
use script_layout_interface::QueryMsg;
use style::attr::AttrValue;
use stylo_dom::ElementState;
use super::customelementregistry::CustomElementState;
use crate::dom::activation::Activatable;
use crate::dom::attr::Attr;
use crate::dom::bindings::codegen::Bindings::CharacterDataBinding::CharacterData_Binding::CharacterDataMethods;
use crate::dom::bindings::codegen::Bindings::EventHandlerBinding::{
EventHandlerNonNull, OnErrorEventHandlerNonNull,
};
use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
use crate::dom::bindings::codegen::Bindings::HTMLLabelElementBinding::HTMLLabelElementMethods;
use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods;
use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::ShadowRoot_Binding::ShadowRootMethods;
use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
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::characterdata::CharacterData;
use crate::dom::cssstyledeclaration::{CSSModificationAccess, CSSStyleDeclaration, CSSStyleOwner};
use crate::dom::customelementregistry::CallbackReaction;
use crate::dom::document::{Document, FocusInitiator};
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::{BindContext, Node, NodeTraits, ShadowIncluding, UnbindContext};
use crate::dom::shadowroot::ShadowRoot;
use crate::dom::text::Text;
use crate::dom::virtualmethods::VirtualMethods;
use crate::script_runtime::CanGc;
use crate::script_thread::ScriptThread;
#[dom_struct]
pub(crate) struct HTMLElement {
element: Element,
style_decl: MutNullableDom<CSSStyleDeclaration>,
dataset: MutNullableDom<DOMStringMap>,
}
impl HTMLElement {
pub(crate) fn new_inherited(
tag_name: LocalName,
prefix: Option<Prefix>,
document: &Document,
) -> HTMLElement {
HTMLElement::new_inherited_with_state(ElementState::empty(), tag_name, prefix, document)
}
pub(crate) fn new_inherited_with_state(
state: ElementState,
tag_name: LocalName,
prefix: Option<Prefix>,
document: &Document,
) -> HTMLElement {
HTMLElement {
element: Element::new_inherited_with_state(
state,
tag_name,
ns!(html),
prefix,
document,
),
style_decl: Default::default(),
dataset: Default::default(),
}
}
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
pub(crate) fn new(
local_name: LocalName,
prefix: Option<Prefix>,
document: &Document,
proto: Option<HandleObject>,
can_gc: CanGc,
) -> DomRoot<HTMLElement> {
Node::reflect_node_with_proto(
Box::new(HTMLElement::new_inherited(local_name, prefix, document)),
document,
proto,
can_gc,
)
}
fn is_body_or_frameset(&self) -> bool {
let eventtarget = self.upcast::<EventTarget>();
eventtarget.is::<HTMLBodyElement>() || eventtarget.is::<HTMLFrameSetElement>()
}
/// Calls into the layout engine to generate a plain text representation
/// of a [`HTMLElement`] as specified when getting the `.innerText` or
/// `.outerText` in JavaScript.`
///
/// <https://html.spec.whatwg.org/multipage/#get-the-text-steps>
pub(crate) fn get_inner_outer_text(&self, can_gc: CanGc) -> DOMString {
let node = self.upcast::<Node>();
let window = node.owner_window();
let element = self.as_element();
// Step 1.
let element_not_rendered = !node.is_connected() || !element.has_css_layout_box(can_gc);
if element_not_rendered {
return node.GetTextContent().unwrap();
}
window.layout_reflow(QueryMsg::ElementInnerOuterTextQuery, can_gc);
let text = window
.layout()
.query_element_inner_outer_text(node.to_trusted_node_address());
DOMString::from(text)
}
/// <https://html.spec.whatwg.org/multipage/#set-the-inner-text-steps>
pub(crate) fn set_inner_text(&self, input: DOMString, can_gc: CanGc) {
// Step 1: Let fragment be the rendered text fragment for value given element's node
// document.
let fragment = self.rendered_text_fragment(input, can_gc);
// Step 2: Replace all with fragment within element.
Node::replace_all(Some(fragment.upcast()), self.upcast::<Node>(), can_gc);
}
}
impl HTMLElementMethods<crate::DomTypeHolder> for HTMLElement {
// https://html.spec.whatwg.org/multipage/#the-style-attribute
fn Style(&self, can_gc: CanGc) -> DomRoot<CSSStyleDeclaration> {
self.style_decl.or_init(|| {
let global = self.owner_window();
CSSStyleDeclaration::new(
&global,
CSSStyleOwner::Element(Dom::from_ref(self.upcast())),
None,
CSSModificationAccess::ReadWrite,
can_gc,
)
})
}
// https://html.spec.whatwg.org/multipage/#attr-title
make_getter!(Title, "title");
// https://html.spec.whatwg.org/multipage/#attr-title
make_setter!(SetTitle, "title");
// https://html.spec.whatwg.org/multipage/#attr-lang
make_getter!(Lang, "lang");
// https://html.spec.whatwg.org/multipage/#attr-lang
make_setter!(SetLang, "lang");
// https://html.spec.whatwg.org/multipage/#the-dir-attribute
make_enumerated_getter!(
Dir,
"dir",
"ltr" | "rtl" | "auto",
missing => "",
invalid => ""
);
// https://html.spec.whatwg.org/multipage/#the-dir-attribute
make_setter!(SetDir, "dir");
// https://html.spec.whatwg.org/multipage/#dom-hidden
make_bool_getter!(Hidden, "hidden");
// https://html.spec.whatwg.org/multipage/#dom-hidden
make_bool_setter!(SetHidden, "hidden");
// https://html.spec.whatwg.org/multipage/#globaleventhandlers
global_event_handlers!(NoOnload);
// https://html.spec.whatwg.org/multipage/#dom-dataset
fn Dataset(&self, can_gc: CanGc) -> DomRoot<DOMStringMap> {
self.dataset.or_init(|| DOMStringMap::new(self, can_gc))
}
// https://html.spec.whatwg.org/multipage/#handler-onerror
fn GetOnerror(&self, can_gc: CanGc) -> Option<Rc<OnErrorEventHandlerNonNull>> {
if self.is_body_or_frameset() {
let document = self.owner_document();
if document.has_browsing_context() {
document.window().GetOnerror()
} else {
None
}
} else {
self.upcast::<EventTarget>()
.get_event_handler_common("error", can_gc)
}
}
// https://html.spec.whatwg.org/multipage/#handler-onerror
fn SetOnerror(&self, listener: Option<Rc<OnErrorEventHandlerNonNull>>) {
if self.is_body_or_frameset() {
let document = self.owner_document();
if document.has_browsing_context() {
document.window().SetOnerror(listener)
}
} else {
// special setter for error
self.upcast::<EventTarget>()
.set_error_event_handler("error", listener)
}
}
// https://html.spec.whatwg.org/multipage/#handler-onload
fn GetOnload(&self, can_gc: CanGc) -> Option<Rc<EventHandlerNonNull>> {
if self.is_body_or_frameset() {
let document = self.owner_document();
if document.has_browsing_context() {
document.window().GetOnload()
} else {
None
}
} else {
self.upcast::<EventTarget>()
.get_event_handler_common("load", can_gc)
}
}
// https://html.spec.whatwg.org/multipage/#handler-onload
fn SetOnload(&self, listener: Option<Rc<EventHandlerNonNull>>) {
if self.is_body_or_frameset() {
let document = self.owner_document();
if document.has_browsing_context() {
document.window().SetOnload(listener)
}
} else {
self.upcast::<EventTarget>()
.set_event_handler_common("load", listener)
}
}
// https://html.spec.whatwg.org/multipage/#handler-onblur
fn GetOnblur(&self, can_gc: CanGc) -> Option<Rc<EventHandlerNonNull>> {
if self.is_body_or_frameset() {
let document = self.owner_document();
if document.has_browsing_context() {
document.window().GetOnblur()
} else {
None
}
} else {
self.upcast::<EventTarget>()
.get_event_handler_common("blur", can_gc)
}
}
// https://html.spec.whatwg.org/multipage/#handler-onblur
fn SetOnblur(&self, listener: Option<Rc<EventHandlerNonNull>>) {
if self.is_body_or_frameset() {
let document = self.owner_document();
if document.has_browsing_context() {
document.window().SetOnblur(listener)
}
} else {
self.upcast::<EventTarget>()
.set_event_handler_common("blur", listener)
}
}
// https://html.spec.whatwg.org/multipage/#handler-onfocus
fn GetOnfocus(&self, can_gc: CanGc) -> Option<Rc<EventHandlerNonNull>> {
if self.is_body_or_frameset() {
let document = self.owner_document();
if document.has_browsing_context() {
document.window().GetOnfocus()
} else {
None
}
} else {
self.upcast::<EventTarget>()
.get_event_handler_common("focus", can_gc)
}
}
// https://html.spec.whatwg.org/multipage/#handler-onfocus
fn SetOnfocus(&self, listener: Option<Rc<EventHandlerNonNull>>) {
if self.is_body_or_frameset() {
let document = self.owner_document();
if document.has_browsing_context() {
document.window().SetOnfocus(listener)
}
} else {
self.upcast::<EventTarget>()
.set_event_handler_common("focus", listener)
}
}
// https://html.spec.whatwg.org/multipage/#handler-onresize
fn GetOnresize(&self, can_gc: CanGc) -> Option<Rc<EventHandlerNonNull>> {
if self.is_body_or_frameset() {
let document = self.owner_document();
if document.has_browsing_context() {
document.window().GetOnresize()
} else {
None
}
} else {
self.upcast::<EventTarget>()
.get_event_handler_common("resize", can_gc)
}
}
// https://html.spec.whatwg.org/multipage/#handler-onresize
fn SetOnresize(&self, listener: Option<Rc<EventHandlerNonNull>>) {
if self.is_body_or_frameset() {
let document = self.owner_document();
if document.has_browsing_context() {
document.window().SetOnresize(listener)
}
} else {
self.upcast::<EventTarget>()
.set_event_handler_common("resize", listener)
}
}
// https://html.spec.whatwg.org/multipage/#handler-onscroll
fn GetOnscroll(&self, can_gc: CanGc) -> Option<Rc<EventHandlerNonNull>> {
if self.is_body_or_frameset() {
let document = self.owner_document();
if document.has_browsing_context() {
document.window().GetOnscroll()
} else {
None
}
} else {
self.upcast::<EventTarget>()
.get_event_handler_common("scroll", can_gc)
}
}
// https://html.spec.whatwg.org/multipage/#handler-onscroll
fn SetOnscroll(&self, listener: Option<Rc<EventHandlerNonNull>>) {
if self.is_body_or_frameset() {
let document = self.owner_document();
if document.has_browsing_context() {
document.window().SetOnscroll(listener)
}
} else {
self.upcast::<EventTarget>()
.set_event_handler_common("scroll", listener)
}
}
// https://html.spec.whatwg.org/multipage/#attr-itemtype
fn Itemtypes(&self) -> Option<Vec<DOMString>> {
let atoms = self
.element
.get_tokenlist_attribute(&local_name!("itemtype"));
if atoms.is_empty() {
return None;
}
let mut item_attr_values = HashSet::new();
for attr_value in &atoms {
item_attr_values.insert(DOMString::from(String::from(attr_value.trim())));
}
Some(item_attr_values.into_iter().collect())
}
// https://html.spec.whatwg.org/multipage/#names:-the-itemprop-attribute
fn PropertyNames(&self) -> Option<Vec<DOMString>> {
let atoms = self
.element
.get_tokenlist_attribute(&local_name!("itemprop"));
if atoms.is_empty() {
return None;
}
let mut item_attr_values = HashSet::new();
for attr_value in &atoms {
item_attr_values.insert(DOMString::from(String::from(attr_value.trim())));
}
Some(item_attr_values.into_iter().collect())
}
/// <https://html.spec.whatwg.org/multipage/#dom-click>
fn Click(&self, can_gc: CanGc) {
let element = self.as_element();
if element.disabled_state() {
return;
}
if element.click_in_progress() {
return;
}
element.set_click_in_progress(true);
self.upcast::<Node>()
.fire_synthetic_pointer_event_not_trusted(DOMString::from("click"), can_gc);
element.set_click_in_progress(false);
}
// https://html.spec.whatwg.org/multipage/#dom-focus
fn Focus(&self, can_gc: CanGc) {
// TODO: Mark the element as locked for focus and run the focusing steps.
// https://html.spec.whatwg.org/multipage/#focusing-steps
let document = self.owner_document();
document.request_focus(Some(self.upcast()), FocusInitiator::Local, can_gc);
}
// https://html.spec.whatwg.org/multipage/#dom-blur
fn Blur(&self, can_gc: CanGc) {
// TODO: Run the unfocusing steps. Focus the top-level document, not
// the current document.
if !self.as_element().focus_state() {
return;
}
// https://html.spec.whatwg.org/multipage/#unfocusing-steps
let document = self.owner_document();
document.request_focus(None, FocusInitiator::Local, can_gc);
}
// https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetparent
fn GetOffsetParent(&self, can_gc: CanGc) -> Option<DomRoot<Element>> {
if self.is::<HTMLBodyElement>() || self.is::<HTMLHtmlElement>() {
return None;
}
let node = self.upcast::<Node>();
let window = self.owner_window();
let (element, _) = window.offset_parent_query(node, can_gc);
element
}
// https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsettop
fn OffsetTop(&self, can_gc: CanGc) -> i32 {
if self.is::<HTMLBodyElement>() {
return 0;
}
let node = self.upcast::<Node>();
let window = self.owner_window();
let (_, rect) = window.offset_parent_query(node, can_gc);
rect.origin.y.to_nearest_px()
}
// https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetleft
fn OffsetLeft(&self, can_gc: CanGc) -> i32 {
if self.is::<HTMLBodyElement>() {
return 0;
}
let node = self.upcast::<Node>();
let window = self.owner_window();
let (_, rect) = window.offset_parent_query(node, can_gc);
rect.origin.x.to_nearest_px()
}
// https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetwidth
fn OffsetWidth(&self, can_gc: CanGc) -> i32 {
let node = self.upcast::<Node>();
let window = self.owner_window();
let (_, rect) = window.offset_parent_query(node, can_gc);
rect.size.width.to_nearest_px()
}
// https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetheight
fn OffsetHeight(&self, can_gc: CanGc) -> i32 {
let node = self.upcast::<Node>();
let window = self.owner_window();
let (_, rect) = window.offset_parent_query(node, can_gc);
rect.size.height.to_nearest_px()
}
/// <https://html.spec.whatwg.org/multipage/#the-innertext-idl-attribute>
fn InnerText(&self, can_gc: CanGc) -> DOMString {
self.get_inner_outer_text(can_gc)
}
/// <https://html.spec.whatwg.org/multipage/#set-the-inner-text-steps>
fn SetInnerText(&self, input: DOMString, can_gc: CanGc) {
self.set_inner_text(input, can_gc)
}
/// <https://html.spec.whatwg.org/multipage/#dom-outertext>
fn GetOuterText(&self, can_gc: CanGc) -> Fallible<DOMString> {
Ok(self.get_inner_outer_text(can_gc))
}
/// <https://html.spec.whatwg.org/multipage/#the-innertext-idl-attribute:dom-outertext-2>
fn SetOuterText(&self, input: DOMString, can_gc: CanGc) -> Fallible<()> {
// Step 1: If this's parent is null, then throw a "NoModificationAllowedError" DOMException.
let Some(parent) = self.upcast::<Node>().GetParentNode() else {
return Err(Error::NoModificationAllowed);
};
let node = self.upcast::<Node>();
let document = self.owner_document();
// Step 2: Let next be this's next sibling.
let next = node.GetNextSibling();
// Step 3: Let previous be this's previous sibling.
let previous = node.GetPreviousSibling();
// Step 4: Let fragment be the rendered text fragment for the given value given this's node
// document.
let fragment = self.rendered_text_fragment(input, can_gc);
// Step 5: If fragment has no children, then append a new Text node whose data is the empty
// string and node document is this's node document to fragment.
if fragment.upcast::<Node>().children_count() == 0 {
let text_node = Text::new(DOMString::from("".to_owned()), &document, can_gc);
fragment
.upcast::<Node>()
.AppendChild(text_node.upcast(), can_gc)?;
}
// Step 6: Replace this with fragment within this's parent.
parent.ReplaceChild(fragment.upcast(), node, can_gc)?;
// Step 7: If next is non-null and next's previous sibling is a Text node, then merge with
// the next text node given next's previous sibling.
if let Some(next_sibling) = next {
if let Some(node) = next_sibling.GetPreviousSibling() {
Self::merge_with_the_next_text_node(node, can_gc);
}
}
// Step 8: If previous is a Text node, then merge with the next text node given previous.
if let Some(previous) = previous {
Self::merge_with_the_next_text_node(previous, can_gc)
}
Ok(())
}
// https://html.spec.whatwg.org/multipage/#dom-translate
fn Translate(&self) -> bool {
self.as_element().is_translate_enabled()
}
// https://html.spec.whatwg.org/multipage/#dom-translate
fn SetTranslate(&self, yesno: bool, can_gc: CanGc) {
self.as_element().set_string_attribute(
&html5ever::local_name!("translate"),
match yesno {
true => DOMString::from("yes"),
false => DOMString::from("no"),
},
can_gc,
);
}
// https://html.spec.whatwg.org/multipage/#dom-contenteditable
fn ContentEditable(&self) -> DOMString {
// TODO: https://github.com/servo/servo/issues/12776
self.as_element()
.get_attribute(&ns!(), &local_name!("contenteditable"))
.map(|attr| DOMString::from(&**attr.value()))
.unwrap_or_else(|| DOMString::from("inherit"))
}
// https://html.spec.whatwg.org/multipage/#dom-contenteditable
fn SetContentEditable(&self, _: DOMString) {
// TODO: https://github.com/servo/servo/issues/12776
warn!("The contentEditable attribute is not implemented yet");
}
// https://html.spec.whatwg.org/multipage/#dom-contenteditable
fn IsContentEditable(&self) -> bool {
// TODO: https://github.com/servo/servo/issues/12776
false
}
/// <https://html.spec.whatwg.org/multipage#dom-attachinternals>
fn AttachInternals(&self, can_gc: CanGc) -> 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 = self.owner_document().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(can_gc);
if internals.attached() {
return Err(Error::NotSupported);
}
// Step 6: If this's custom element state is not "precustomized" or "custom",
// then throw a "NotSupportedError" DOMException.
if !matches!(
element.get_custom_element_state(),
CustomElementState::Precustomized | CustomElementState::Custom
) {
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)
}
// FIXME: The nonce should be stored in an internal slot instead of an
// attribute (https://html.spec.whatwg.org/multipage/#cryptographicnonce)
// https://html.spec.whatwg.org/multipage/#dom-noncedelement-nonce
make_getter!(Nonce, "nonce");
// https://html.spec.whatwg.org/multipage/#dom-noncedelement-nonce
make_setter!(SetNonce, "nonce");
// https://html.spec.whatwg.org/multipage/#dom-fe-autofocus
fn Autofocus(&self) -> bool {
self.element.has_attribute(&local_name!("autofocus"))
}
// https://html.spec.whatwg.org/multipage/#dom-fe-autofocus
fn SetAutofocus(&self, autofocus: bool, can_gc: CanGc) {
self.element
.set_bool_attribute(&local_name!("autofocus"), autofocus, can_gc);
}
}
fn append_text_node_to_fragment(
document: &Document,
fragment: &DocumentFragment,
text: String,
can_gc: CanGc,
) {
let text = Text::new(DOMString::from(text), document, can_gc);
fragment
.upcast::<Node>()
.AppendChild(text.upcast(), can_gc)
.unwrap();
}
// https://html.spec.whatwg.org/multipage/#attr-data-*
static DATA_PREFIX: &str = "data-";
static DATA_HYPHEN_SEPARATOR: char = '\x2d';
fn to_snake_case(name: DOMString) -> DOMString {
let mut attr_name = String::with_capacity(name.len() + DATA_PREFIX.len());
attr_name.push_str(DATA_PREFIX);
for ch in name.chars() {
if ch.is_ascii_uppercase() {
attr_name.push(DATA_HYPHEN_SEPARATOR);
attr_name.push(ch.to_ascii_lowercase());
} else {
attr_name.push(ch);
}
}
DOMString::from(attr_name)
}
// https://html.spec.whatwg.org/multipage/#attr-data-*
// if this attribute is in snake case with a data- prefix,
// this function returns a name converted to camel case
// without the data prefix.
fn to_camel_case(name: &str) -> Option<DOMString> {
if !name.starts_with(DATA_PREFIX) {
return None;
}
let name = &name[5..];
let has_uppercase = name.chars().any(|curr_char| curr_char.is_ascii_uppercase());
if has_uppercase {
return None;
}
let mut result = String::with_capacity(name.len().saturating_sub(DATA_PREFIX.len()));
let mut name_chars = name.chars();
while let Some(curr_char) = name_chars.next() {
//check for hyphen followed by character
if curr_char == DATA_HYPHEN_SEPARATOR {
if let Some(next_char) = name_chars.next() {
if next_char.is_ascii_lowercase() {
result.push(next_char.to_ascii_uppercase());
} else {
result.push(curr_char);
result.push(next_char);
}
} else {
result.push(curr_char);
}
} else {
result.push(curr_char);
}
}
Some(DOMString::from(result))
}
impl HTMLElement {
pub(crate) fn set_custom_attr(
&self,
name: DOMString,
value: DOMString,
can_gc: CanGc,
) -> ErrorResult {
if name
.chars()
.skip_while(|&ch| ch != '\u{2d}')
.nth(1)
.is_some_and(|ch| ch.is_ascii_lowercase())
{
return Err(Error::Syntax);
}
self.as_element()
.set_custom_attribute(to_snake_case(name), value, can_gc)
}
pub(crate) 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.as_element()
.get_attribute(&ns!(), &local_name)
.map(|attr| {
DOMString::from(&**attr.value()) // FIXME(ajeffrey): Convert directly from AttrValue to DOMString
})
}
pub(crate) fn delete_custom_attr(&self, local_name: DOMString, can_gc: CanGc) {
// FIXME(ajeffrey): Convert directly from DOMString to LocalName
let local_name = LocalName::from(to_snake_case(local_name));
self.as_element()
.remove_attribute(&ns!(), &local_name, can_gc);
}
/// <https://html.spec.whatwg.org/multipage/#category-label>
pub(crate) fn is_labelable_element(&self) -> bool {
match self.upcast::<Node>().type_id() {
NodeTypeId::Element(ElementTypeId::HTMLElement(type_id)) => match type_id {
HTMLElementTypeId::HTMLInputElement => {
self.downcast::<HTMLInputElement>().unwrap().input_type() != InputType::Hidden
},
HTMLElementTypeId::HTMLButtonElement |
HTMLElementTypeId::HTMLMeterElement |
HTMLElementTypeId::HTMLOutputElement |
HTMLElementTypeId::HTMLProgressElement |
HTMLElementTypeId::HTMLSelectElement |
HTMLElementTypeId::HTMLTextAreaElement => true,
_ => self.is_form_associated_custom_element(),
},
_ => false,
}
}
/// <https://html.spec.whatwg.org/multipage/#form-associated-custom-element>
pub(crate) 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(crate) fn is_listed_element(&self) -> bool {
match self.upcast::<Node>().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 => true,
_ => self.is_form_associated_custom_element(),
},
_ => false,
}
}
/// <https://html.spec.whatwg.org/multipage/#category-submit>
pub(crate) 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(crate) fn supported_prop_names_custom_attr(&self) -> Vec<DOMString> {
let element = self.as_element();
element
.attrs()
.iter()
.filter_map(|attr| {
let raw_name = attr.local_name();
to_camel_case(raw_name)
})
.collect()
}
// https://html.spec.whatwg.org/multipage/#dom-lfe-labels
// This gets the nth label in tree order.
pub(crate) fn label_at(&self, index: u32) -> Option<DomRoot<Node>> {
let element = self.as_element();
// Traverse entire tree for <label> elements that have
// this as their control.
// There is room for performance optimization, as we don't need
// the actual result of GetControl, only whether the result
// would match self.
// (Even more room for performance optimization: do what
// nodelist ChildrenList does and keep a mutation-aware cursor
// around; this may be hard since labels need to keep working
// even as they get detached into a subtree and reattached to
// a document.)
let root_element = element.root_element();
let root_node = root_element.upcast::<Node>();
root_node
.traverse_preorder(ShadowIncluding::No)
.filter_map(DomRoot::downcast::<HTMLLabelElement>)
.filter(|elem| match elem.GetControl() {
Some(control) => &*control == self,
_ => false,
})
.nth(index as usize)
.map(|n| DomRoot::from_ref(n.upcast::<Node>()))
}
// https://html.spec.whatwg.org/multipage/#dom-lfe-labels
// This counts the labels of the element, to support NodeList::Length
pub(crate) fn labels_count(&self) -> u32 {
// see label_at comments about performance
let element = self.as_element();
let root_element = element.root_element();
let root_node = root_element.upcast::<Node>();
root_node
.traverse_preorder(ShadowIncluding::No)
.filter_map(DomRoot::downcast::<HTMLLabelElement>)
.filter(|elem| match elem.GetControl() {
Some(control) => &*control == self,
_ => false,
})
.count() as u32
}
// https://html.spec.whatwg.org/multipage/#the-directionality.
// returns Some if can infer direction by itself or from child nodes
// returns None if requires to go up to parent
pub(crate) fn directionality(&self) -> Option<String> {
let element_direction: &str = &self.Dir();
if element_direction == "ltr" {
return Some("ltr".to_owned());
}
if element_direction == "rtl" {
return Some("rtl".to_owned());
}
if let Some(input) = self.downcast::<HTMLInputElement>() {
if input.input_type() == InputType::Tel {
return Some("ltr".to_owned());
}
}
if element_direction == "auto" {
if let Some(directionality) = self
.downcast::<HTMLInputElement>()
.and_then(|input| input.auto_directionality())
{
return Some(directionality);
}
if let Some(area) = self.downcast::<HTMLTextAreaElement>() {
return Some(area.auto_directionality());
}
}
// TODO(NeverHappened): Implement condition
// If the element's dir attribute is in the auto state OR
// If the element is a bdi element and the dir attribute is not in a defined state
// (i.e. it is not present or has an invalid value)
// Requires bdi element implementation (https://html.spec.whatwg.org/multipage/#the-bdi-element)
None
}
// https://html.spec.whatwg.org/multipage/#the-summary-element:activation-behaviour
pub(crate) fn summary_activation_behavior(&self) {
debug_assert!(self.as_element().local_name() == &local_name!("summary"));
// Step 1. If this summary element is not the summary for its parent details, then return.
if !self.is_a_summary_for_its_parent_details() {
return;
}
// Step 2. Let parent be this summary element's parent.
let parent = if self.is_implicit_summary_element() {
DomRoot::downcast::<HTMLDetailsElement>(self.containing_shadow_root().unwrap().Host())
.unwrap()
} else {
self.upcast::<Node>()
.GetParentNode()
.and_then(DomRoot::downcast::<HTMLDetailsElement>)
.unwrap()
};
// Step 3. If the open attribute is present on parent, then remove it.
// Otherwise, set parent's open attribute to the empty string.
parent.toggle();
}
/// <https://html.spec.whatwg.org/multipage/#summary-for-its-parent-details>
fn is_a_summary_for_its_parent_details(&self) -> bool {
if self.is_implicit_summary_element() {
return true;
}
// Step 1. If this summary element has no parent, then return false.
// Step 2. Let parent be this summary element's parent.
let Some(parent) = self.upcast::<Node>().GetParentNode() else {
return false;
};
// Step 3. If parent is not a details element, then return false.
let Some(details) = parent.downcast::<HTMLDetailsElement>() else {
return false;
};
// Step 4. If parent's first summary element child is not this summary
// element, then return false.
// Step 5. Return true.
details
.find_corresponding_summary_element()
.is_some_and(|summary| &*summary == self.upcast())
}
/// Whether or not this is an implicitly generated `<summary>`
/// element for a UA `<details>` shadow tree
fn is_implicit_summary_element(&self) -> bool {
// Note that non-implicit summary elements are not actually inside
// the UA shadow tree, they're only assigned to a slot inside it.
// Therefore they don't cause false positives here
self.containing_shadow_root()
.as_deref()
.map(ShadowRoot::Host)
.is_some_and(|host| host.is::<HTMLDetailsElement>())
}
/// <https://html.spec.whatwg.org/multipage/#rendered-text-fragment>
fn rendered_text_fragment(&self, input: DOMString, can_gc: CanGc) -> DomRoot<DocumentFragment> {
// Step 1: Let fragment be a new DocumentFragment whose node document is document.
let document = self.owner_document();
let fragment = DocumentFragment::new(&document, can_gc);
// Step 2: Let position be a position variable for input, initially pointing at the start
// of input.
let mut position = input.chars().peekable();
// Step 3: Let text be the empty string.
let mut text = String::new();
// Step 4
while let Some(ch) = position.next() {
match ch {
// While position is not past the end of input, and the code point at position is
// either U+000A LF or U+000D CR:
'\u{000A}' | '\u{000D}' => {
if ch == '\u{000D}' && position.peek() == Some(&'\u{000A}') {
// a \r\n pair should only generate one <br>,
// so just skip the \r.
position.next();
}
if !text.is_empty() {
append_text_node_to_fragment(&document, &fragment, text, can_gc);
text = String::new();
}
let br = HTMLBRElement::new(local_name!("br"), None, &document, None, can_gc);
fragment
.upcast::<Node>()
.AppendChild(br.upcast(), can_gc)
.unwrap();
},
_ => {
// Collect a sequence of code points that are not U+000A LF or U+000D CR from
// input given position, and set text to the result.
text.push(ch);
},
}
}
// If text is not the empty string, then append a new Text node whose data is text and node
// document is document to fragment.
if !text.is_empty() {
append_text_node_to_fragment(&document, &fragment, text, can_gc);
}
fragment
}
/// Checks whether a given [`DomRoot<Node>`] and its next sibling are
/// of type [`Text`], and if so merges them into a single [`Text`]
/// node.
///
/// <https://html.spec.whatwg.org/multipage/#merge-with-the-next-text-node>
fn merge_with_the_next_text_node(node: DomRoot<Node>, can_gc: CanGc) {
// Make sure node is a Text node
if !node.is::<Text>() {
return;
}
// Step 1: Let next be node's next sibling.
let next = match node.GetNextSibling() {
Some(next) => next,
None => return,
};
// Step 2: If next is not a Text node, then return.
if !next.is::<Text>() {
return;
}
// Step 3: Replace data with node, node's data's length, 0, and next's data.
let node_chars = node.downcast::<CharacterData>().expect("Node is Text");
let next_chars = next.downcast::<CharacterData>().expect("Next node is Text");
node_chars
.ReplaceData(node_chars.Length(), 0, next_chars.Data())
.expect("Got chars from Text");
// Step 4:Remove next.
next.remove_self(can_gc);
}
}
impl VirtualMethods for HTMLElement {
fn super_type(&self) -> Option<&dyn VirtualMethods> {
Some(self.as_element() as &dyn VirtualMethods)
}
fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation, can_gc: CanGc) {
self.super_type()
.unwrap()
.attribute_mutated(attr, mutation, can_gc);
let element = self.as_element();
match (attr.local_name(), mutation) {
(name, AttributeMutation::Set(_)) if name.starts_with("on") => {
let source = &**attr.value();
let evtarget = self.upcast::<EventTarget>();
let source_line = 1; //TODO(#9604) get current JS execution line
evtarget.set_event_handler_uncompiled(
self.owner_window().get_url(),
source_line,
&name[2..],
source,
);
},
(&local_name!("form"), mutation) if self.is_form_associated_custom_element() => {
self.form_attribute_mutated(mutation, can_gc);
},
// 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, can_gc: CanGc) {
if let Some(super_type) = self.super_type() {
super_type.bind_to_tree(context, can_gc);
}
let element = self.as_element();
element.update_sequentially_focusable_status(can_gc);
// 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, can_gc: CanGc) {
if let Some(super_type) = self.super_type() {
super_type.unbind_from_tree(context, can_gc);
}
// 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()),
local_name!("itemtype") => AttrValue::from_serialized_tokenlist(value.into()),
_ => self
.super_type()
.unwrap()
.parse_plain_attribute(name, value),
}
}
}
impl Activatable for HTMLElement {
fn as_element(&self) -> &Element {
self.upcast::<Element>()
}
fn is_instance_activatable(&self) -> bool {
self.as_element().local_name() == &local_name!("summary")
}
// Basically used to make the HTMLSummaryElement activatable (which has no IDL definition)
fn activation_behavior(&self, _event: &Event, _target: &EventTarget, _can_gc: CanGc) {
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(CanGc::note())
.set_form_owner(form);
}
fn to_element(&self) -> &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
}