mirror of
https://github.com/servo/servo.git
synced 2025-06-11 01:50:10 +00:00
Event dispatch rewritten to align to spec, activate on clicks better I went over the changes to the event dispatch spec that had accumulated over the past few years, rewriting dispatch/invoke/inner-invoke almost completely and modifying other code where it was relevant. Most of the remaining obvious deviations from spec are things that will only come up when we start handling events in shadow DOM. I am pushing now because I want to see CI test results, but please do not approve this PR just if automated test improvements look good. I may have broken some actual UI interactions in the course of fixing synthetic events, and some manual testing is needed, including checking that manual interactions with interactive content continue to fire the events they're supposed to. --- <!-- Thank you for contributing to Servo! Please replace each `[ ]` by `[X]` when the step is complete, and replace `___` with appropriate data: --> - [X] `./mach build -d` does not report any errors - [X] `./mach test-tidy` does not report any errors - [X] These changes fix #25384 and fix #22783 and fix #25199 <!-- Either: --> - [ ] There are automated tests for the synthetic-click parts of these changes, BUT the effects on real UI events need some manual testing before merging <!-- Also, please make sure that "Allow edits from maintainers" checkbox is checked, so that we can help you if you get stuck somewhere along the way.--> <!-- Pull requests that do not address these steps are welcome, but they will require additional verification as part of the review process. -->
812 lines
29 KiB
Rust
812 lines
29 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 crate::dom::attr::Attr;
|
|
use crate::dom::bindings::codegen::Bindings::EventHandlerBinding::EventHandlerNonNull;
|
|
use crate::dom::bindings::codegen::Bindings::EventHandlerBinding::OnErrorEventHandlerNonNull;
|
|
use crate::dom::bindings::codegen::Bindings::HTMLElementBinding;
|
|
use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
|
|
use crate::dom::bindings::codegen::Bindings::HTMLLabelElementBinding::HTMLLabelElementMethods;
|
|
use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeBinding::NodeMethods;
|
|
use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
|
|
use crate::dom::bindings::error::{Error, ErrorResult};
|
|
use crate::dom::bindings::inheritance::Castable;
|
|
use crate::dom::bindings::inheritance::{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::document::{Document, FocusType};
|
|
use crate::dom::documentfragment::DocumentFragment;
|
|
use crate::dom::domstringmap::DOMStringMap;
|
|
use crate::dom::element::{AttributeMutation, Element};
|
|
use crate::dom::eventtarget::EventTarget;
|
|
use crate::dom::htmlbodyelement::HTMLBodyElement;
|
|
use crate::dom::htmlbrelement::HTMLBRElement;
|
|
use crate::dom::htmlframesetelement::HTMLFrameSetElement;
|
|
use crate::dom::htmlhtmlelement::HTMLHtmlElement;
|
|
use crate::dom::htmlinputelement::{HTMLInputElement, InputType};
|
|
use crate::dom::htmllabelelement::HTMLLabelElement;
|
|
use crate::dom::node::{document_from_node, window_from_node};
|
|
use crate::dom::node::{BindContext, Node, NodeFlags, ShadowIncluding};
|
|
use crate::dom::text::Text;
|
|
use crate::dom::virtualmethods::VirtualMethods;
|
|
use dom_struct::dom_struct;
|
|
use html5ever::{LocalName, Prefix};
|
|
use script_layout_interface::message::QueryMsg;
|
|
use std::collections::HashSet;
|
|
use std::default::Default;
|
|
use std::rc::Rc;
|
|
use style::attr::AttrValue;
|
|
use style::element_state::*;
|
|
|
|
#[dom_struct]
|
|
pub struct HTMLElement {
|
|
element: Element,
|
|
style_decl: MutNullableDom<CSSStyleDeclaration>,
|
|
dataset: MutNullableDom<DOMStringMap>,
|
|
}
|
|
|
|
impl HTMLElement {
|
|
pub fn new_inherited(
|
|
tag_name: LocalName,
|
|
prefix: Option<Prefix>,
|
|
document: &Document,
|
|
) -> HTMLElement {
|
|
HTMLElement::new_inherited_with_state(ElementState::empty(), tag_name, prefix, document)
|
|
}
|
|
|
|
pub 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(),
|
|
}
|
|
}
|
|
|
|
#[allow(unrooted_must_root)]
|
|
pub fn new(
|
|
local_name: LocalName,
|
|
prefix: Option<Prefix>,
|
|
document: &Document,
|
|
) -> DomRoot<HTMLElement> {
|
|
Node::reflect_node(
|
|
Box::new(HTMLElement::new_inherited(local_name, prefix, document)),
|
|
document,
|
|
HTMLElementBinding::Wrap,
|
|
)
|
|
}
|
|
|
|
fn is_body_or_frameset(&self) -> bool {
|
|
let eventtarget = self.upcast::<EventTarget>();
|
|
eventtarget.is::<HTMLBodyElement>() || eventtarget.is::<HTMLFrameSetElement>()
|
|
}
|
|
|
|
fn update_sequentially_focusable_status(&self) {
|
|
let element = self.upcast::<Element>();
|
|
let node = self.upcast::<Node>();
|
|
if element.has_attribute(&local_name!("tabindex")) {
|
|
node.set_flag(NodeFlags::SEQUENTIALLY_FOCUSABLE, true);
|
|
} else {
|
|
match node.type_id() {
|
|
NodeTypeId::Element(ElementTypeId::HTMLElement(
|
|
HTMLElementTypeId::HTMLButtonElement,
|
|
)) |
|
|
NodeTypeId::Element(ElementTypeId::HTMLElement(
|
|
HTMLElementTypeId::HTMLSelectElement,
|
|
)) |
|
|
NodeTypeId::Element(ElementTypeId::HTMLElement(
|
|
HTMLElementTypeId::HTMLIFrameElement,
|
|
)) |
|
|
NodeTypeId::Element(ElementTypeId::HTMLElement(
|
|
HTMLElementTypeId::HTMLTextAreaElement,
|
|
)) => node.set_flag(NodeFlags::SEQUENTIALLY_FOCUSABLE, true),
|
|
NodeTypeId::Element(ElementTypeId::HTMLElement(
|
|
HTMLElementTypeId::HTMLLinkElement,
|
|
)) |
|
|
NodeTypeId::Element(ElementTypeId::HTMLElement(
|
|
HTMLElementTypeId::HTMLAnchorElement,
|
|
)) => {
|
|
if element.has_attribute(&local_name!("href")) {
|
|
node.set_flag(NodeFlags::SEQUENTIALLY_FOCUSABLE, true);
|
|
}
|
|
},
|
|
_ => {
|
|
if let Some(attr) = element.get_attribute(&ns!(), &local_name!("draggable")) {
|
|
let value = attr.value();
|
|
let is_true = match *value {
|
|
AttrValue::String(ref string) => string == "true",
|
|
_ => false,
|
|
};
|
|
node.set_flag(NodeFlags::SEQUENTIALLY_FOCUSABLE, is_true);
|
|
} else {
|
|
node.set_flag(NodeFlags::SEQUENTIALLY_FOCUSABLE, false);
|
|
}
|
|
//TODO set SEQUENTIALLY_FOCUSABLE flag if editing host
|
|
//TODO set SEQUENTIALLY_FOCUSABLE flag if "sorting interface th elements"
|
|
},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl HTMLElementMethods for HTMLElement {
|
|
// https://html.spec.whatwg.org/multipage/#the-style-attribute
|
|
fn Style(&self) -> DomRoot<CSSStyleDeclaration> {
|
|
self.style_decl.or_init(|| {
|
|
let global = window_from_node(self);
|
|
CSSStyleDeclaration::new(
|
|
&global,
|
|
CSSStyleOwner::Element(Dom::from_ref(self.upcast())),
|
|
None,
|
|
CSSModificationAccess::ReadWrite,
|
|
)
|
|
})
|
|
}
|
|
|
|
// 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/#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/#documentandelementeventhandlers
|
|
document_and_element_event_handlers!();
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-dataset
|
|
fn Dataset(&self) -> DomRoot<DOMStringMap> {
|
|
self.dataset.or_init(|| DOMStringMap::new(self))
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#handler-onerror
|
|
fn GetOnerror(&self) -> Option<Rc<OnErrorEventHandlerNonNull>> {
|
|
if self.is_body_or_frameset() {
|
|
let document = document_from_node(self);
|
|
if document.has_browsing_context() {
|
|
document.window().GetOnerror()
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
self.upcast::<EventTarget>()
|
|
.get_event_handler_common("error")
|
|
}
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#handler-onerror
|
|
fn SetOnerror(&self, listener: Option<Rc<OnErrorEventHandlerNonNull>>) {
|
|
if self.is_body_or_frameset() {
|
|
let document = document_from_node(self);
|
|
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) -> Option<Rc<EventHandlerNonNull>> {
|
|
if self.is_body_or_frameset() {
|
|
let document = document_from_node(self);
|
|
if document.has_browsing_context() {
|
|
document.window().GetOnload()
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
self.upcast::<EventTarget>()
|
|
.get_event_handler_common("load")
|
|
}
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#handler-onload
|
|
fn SetOnload(&self, listener: Option<Rc<EventHandlerNonNull>>) {
|
|
if self.is_body_or_frameset() {
|
|
let document = document_from_node(self);
|
|
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) -> Option<Rc<EventHandlerNonNull>> {
|
|
if self.is_body_or_frameset() {
|
|
let document = document_from_node(self);
|
|
if document.has_browsing_context() {
|
|
document.window().GetOnblur()
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
self.upcast::<EventTarget>()
|
|
.get_event_handler_common("blur")
|
|
}
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#handler-onblur
|
|
fn SetOnblur(&self, listener: Option<Rc<EventHandlerNonNull>>) {
|
|
if self.is_body_or_frameset() {
|
|
let document = document_from_node(self);
|
|
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) -> Option<Rc<EventHandlerNonNull>> {
|
|
if self.is_body_or_frameset() {
|
|
let document = document_from_node(self);
|
|
if document.has_browsing_context() {
|
|
document.window().GetOnfocus()
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
self.upcast::<EventTarget>()
|
|
.get_event_handler_common("focus")
|
|
}
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#handler-onfocus
|
|
fn SetOnfocus(&self, listener: Option<Rc<EventHandlerNonNull>>) {
|
|
if self.is_body_or_frameset() {
|
|
let document = document_from_node(self);
|
|
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) -> Option<Rc<EventHandlerNonNull>> {
|
|
if self.is_body_or_frameset() {
|
|
let document = document_from_node(self);
|
|
if document.has_browsing_context() {
|
|
document.window().GetOnresize()
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
self.upcast::<EventTarget>()
|
|
.get_event_handler_common("resize")
|
|
}
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#handler-onresize
|
|
fn SetOnresize(&self, listener: Option<Rc<EventHandlerNonNull>>) {
|
|
if self.is_body_or_frameset() {
|
|
let document = document_from_node(self);
|
|
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) -> Option<Rc<EventHandlerNonNull>> {
|
|
if self.is_body_or_frameset() {
|
|
let document = document_from_node(self);
|
|
if document.has_browsing_context() {
|
|
document.window().GetOnscroll()
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
self.upcast::<EventTarget>()
|
|
.get_event_handler_common("scroll")
|
|
}
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#handler-onscroll
|
|
fn SetOnscroll(&self, listener: Option<Rc<EventHandlerNonNull>>) {
|
|
if self.is_body_or_frameset() {
|
|
let document = document_from_node(self);
|
|
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) {
|
|
let element = self.upcast::<Element>();
|
|
if element.disabled_state() {
|
|
return;
|
|
}
|
|
if element.click_in_progress() {
|
|
return;
|
|
}
|
|
element.set_click_in_progress(true);
|
|
|
|
self.upcast::<Node>()
|
|
.fire_synthetic_mouse_event_not_trusted(DOMString::from("click"));
|
|
element.set_click_in_progress(false);
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-focus
|
|
fn Focus(&self) {
|
|
// TODO: Mark the element as locked for focus and run the focusing steps.
|
|
// https://html.spec.whatwg.org/multipage/#focusing-steps
|
|
let document = document_from_node(self);
|
|
document.begin_focus_transaction();
|
|
document.request_focus(self.upcast());
|
|
document.commit_focus_transaction(FocusType::Element);
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-blur
|
|
fn Blur(&self) {
|
|
// TODO: Run the unfocusing steps.
|
|
if !self.upcast::<Element>().focus_state() {
|
|
return;
|
|
}
|
|
// https://html.spec.whatwg.org/multipage/#unfocusing-steps
|
|
let document = document_from_node(self);
|
|
document.begin_focus_transaction();
|
|
// If `request_focus` is not called, focus will be set to None.
|
|
document.commit_focus_transaction(FocusType::Element);
|
|
}
|
|
|
|
// https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetparent
|
|
fn GetOffsetParent(&self) -> Option<DomRoot<Element>> {
|
|
if self.is::<HTMLBodyElement>() || self.is::<HTMLHtmlElement>() {
|
|
return None;
|
|
}
|
|
|
|
let node = self.upcast::<Node>();
|
|
let window = window_from_node(self);
|
|
let (element, _) = window.offset_parent_query(node);
|
|
|
|
element
|
|
}
|
|
|
|
// https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsettop
|
|
fn OffsetTop(&self) -> i32 {
|
|
if self.is::<HTMLBodyElement>() {
|
|
return 0;
|
|
}
|
|
|
|
let node = self.upcast::<Node>();
|
|
let window = window_from_node(self);
|
|
let (_, rect) = window.offset_parent_query(node);
|
|
|
|
rect.origin.y.to_nearest_px()
|
|
}
|
|
|
|
// https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetleft
|
|
fn OffsetLeft(&self) -> i32 {
|
|
if self.is::<HTMLBodyElement>() {
|
|
return 0;
|
|
}
|
|
|
|
let node = self.upcast::<Node>();
|
|
let window = window_from_node(self);
|
|
let (_, rect) = window.offset_parent_query(node);
|
|
|
|
rect.origin.x.to_nearest_px()
|
|
}
|
|
|
|
// https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetwidth
|
|
fn OffsetWidth(&self) -> i32 {
|
|
let node = self.upcast::<Node>();
|
|
let window = window_from_node(self);
|
|
let (_, rect) = window.offset_parent_query(node);
|
|
|
|
rect.size.width.to_nearest_px()
|
|
}
|
|
|
|
// https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetheight
|
|
fn OffsetHeight(&self) -> i32 {
|
|
let node = self.upcast::<Node>();
|
|
let window = window_from_node(self);
|
|
let (_, rect) = window.offset_parent_query(node);
|
|
|
|
rect.size.height.to_nearest_px()
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#the-innertext-idl-attribute
|
|
fn InnerText(&self) -> DOMString {
|
|
let node = self.upcast::<Node>();
|
|
let window = window_from_node(node);
|
|
let element = self.upcast::<Element>();
|
|
|
|
// Step 1.
|
|
let element_not_rendered = !node.is_connected() || !element.has_css_layout_box();
|
|
if element_not_rendered {
|
|
return node.GetTextContent().unwrap();
|
|
}
|
|
|
|
window.layout_reflow(QueryMsg::ElementInnerTextQuery(
|
|
node.to_trusted_node_address(),
|
|
));
|
|
DOMString::from(window.layout().element_inner_text())
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#the-innertext-idl-attribute
|
|
fn SetInnerText(&self, input: DOMString) {
|
|
// Step 1.
|
|
let document = document_from_node(self);
|
|
|
|
// Step 2.
|
|
let fragment = DocumentFragment::new(&document);
|
|
|
|
// Step 3. The given value is already named 'input'.
|
|
|
|
// Step 4.
|
|
let mut position = input.chars().peekable();
|
|
|
|
// Step 5.
|
|
let mut text = String::new();
|
|
|
|
// Step 6.
|
|
while let Some(ch) = position.next() {
|
|
match ch {
|
|
'\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);
|
|
text = String::new();
|
|
}
|
|
|
|
let br = HTMLBRElement::new(local_name!("br"), None, &document);
|
|
fragment.upcast::<Node>().AppendChild(&br.upcast()).unwrap();
|
|
},
|
|
_ => {
|
|
text.push(ch);
|
|
},
|
|
}
|
|
}
|
|
|
|
if !text.is_empty() {
|
|
append_text_node_to_fragment(&document, &fragment, text);
|
|
}
|
|
|
|
// Step 7.
|
|
Node::replace_all(Some(fragment.upcast()), self.upcast::<Node>());
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-translate
|
|
fn Translate(&self) -> bool {
|
|
self.upcast::<Element>().is_translate_enabled()
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-translate
|
|
fn SetTranslate(&self, yesno: bool) {
|
|
self.upcast::<Element>().set_string_attribute(
|
|
// TODO change this to local_name! when html5ever updates
|
|
&LocalName::from("translate"),
|
|
match yesno {
|
|
true => DOMString::from("yes"),
|
|
false => DOMString::from("no"),
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
fn append_text_node_to_fragment(document: &Document, fragment: &DocumentFragment, text: String) {
|
|
let text = Text::new(DOMString::from(text), document);
|
|
fragment
|
|
.upcast::<Node>()
|
|
.AppendChild(&text.upcast())
|
|
.unwrap();
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#attr-data-*
|
|
|
|
static DATA_PREFIX: &str = "data-";
|
|
static DATA_HYPHEN_SEPARATOR: char = '\x2d';
|
|
|
|
fn is_ascii_uppercase(c: char) -> bool {
|
|
'A' <= c && c <= 'Z'
|
|
}
|
|
|
|
fn is_ascii_lowercase(c: char) -> bool {
|
|
'a' <= c && c <= 'w'
|
|
}
|
|
|
|
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 is_ascii_uppercase(ch) {
|
|
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| is_ascii_uppercase(curr_char));
|
|
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 is_ascii_lowercase(next_char) {
|
|
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 fn set_custom_attr(&self, name: DOMString, value: DOMString) -> ErrorResult {
|
|
if name
|
|
.chars()
|
|
.skip_while(|&ch| ch != '\u{2d}')
|
|
.nth(1)
|
|
.map_or(false, |ch| ch >= 'a' && ch <= 'z')
|
|
{
|
|
return Err(Error::Syntax);
|
|
}
|
|
self.upcast::<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>()
|
|
.get_attribute(&ns!(), &local_name)
|
|
.map(|attr| {
|
|
DOMString::from(&**attr.value()) // FIXME(ajeffrey): Convert directly from AttrValue to DOMString
|
|
})
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#category-label
|
|
pub fn is_labelable_element(&self) -> bool {
|
|
// Note: HTMLKeygenElement is omitted because Servo doesn't currently implement it
|
|
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,
|
|
_ => false,
|
|
},
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#category-listed
|
|
pub fn is_listed_element(&self) -> bool {
|
|
// Servo does not implement HTMLKeygenElement
|
|
// https://github.com/servo/servo/issues/2782
|
|
if self.upcast::<Element>().local_name() == &local_name!("keygen") {
|
|
return true;
|
|
}
|
|
|
|
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,
|
|
_ => false,
|
|
},
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
pub fn supported_prop_names_custom_attr(&self) -> Vec<DOMString> {
|
|
let element = self.upcast::<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 fn label_at(&self, index: u32) -> Option<DomRoot<Node>> {
|
|
let element = self.upcast::<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 fn labels_count(&self) -> u32 {
|
|
// see label_at comments about performance
|
|
let element = self.upcast::<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
|
|
}
|
|
}
|
|
|
|
impl VirtualMethods for HTMLElement {
|
|
fn super_type(&self) -> Option<&dyn VirtualMethods> {
|
|
Some(self.upcast::<Element>() as &dyn VirtualMethods)
|
|
}
|
|
|
|
fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation) {
|
|
self.super_type().unwrap().attribute_mutated(attr, mutation);
|
|
match (attr.local_name(), mutation) {
|
|
(name, AttributeMutation::Set(_)) if name.starts_with("on") => {
|
|
let evtarget = self.upcast::<EventTarget>();
|
|
let source_line = 1; //TODO(#9604) get current JS execution line
|
|
evtarget.set_event_handler_uncompiled(
|
|
window_from_node(self).get_url(),
|
|
source_line,
|
|
&name[2..],
|
|
// FIXME(ajeffrey): Convert directly from AttrValue to DOMString
|
|
DOMString::from(&**attr.value()),
|
|
);
|
|
},
|
|
_ => {},
|
|
}
|
|
}
|
|
|
|
fn bind_to_tree(&self, context: &BindContext) {
|
|
if let Some(ref s) = self.super_type() {
|
|
s.bind_to_tree(context);
|
|
}
|
|
self.update_sequentially_focusable_status();
|
|
}
|
|
|
|
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),
|
|
}
|
|
}
|
|
}
|