diff options
author | Simon Wülker <simon.wuelker@arcor.de> | 2025-01-19 15:05:05 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-01-19 14:05:05 +0000 |
commit | dabe162d44a2b8a47b7a9dc8566e7f51ae678d05 (patch) | |
tree | 0b915202184ab6924ecb38cc961bbd4dd8ed830e /components/script | |
parent | 8bb50fa3c9d82e2575b02cefb6558467a8187439 (diff) | |
download | servo-dabe162d44a2b8a47b7a9dc8566e7f51ae678d05.tar.gz servo-dabe162d44a2b8a47b7a9dc8566e7f51ae678d05.zip |
Implement shadow dom slots (#35013)
* Implement slot-related algorithms
Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
* Hook up slot elements to DOM creation logic
Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
* Set a slot assignment mode for servo-internal shadow roots
Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
* Assign slots when a slottable's slot attribute changes
Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
* Properly compute slot name
Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
* ./mach test-tidy
Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
* Update <slot> name when name attribute changes
Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
* Implement fast path for Node::assign_slottables_for_a_tree
assign_slottables_for_a_tree traverses all descendants of the node
and is potentially very expensive. If the node is not a shadow root
then assigning slottables to it won't have any effect, so we
take a fast path out.
Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
* Move slottable data into ElementRareData
This shrinks all element descendants back to their
original size.
Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
* Address review comments
Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
* Update WPT expectations
Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
---------
Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
Diffstat (limited to 'components/script')
-rw-r--r-- | components/script/dom/create.rs | 2 | ||||
-rw-r--r-- | components/script/dom/element.rs | 73 | ||||
-rw-r--r-- | components/script/dom/htmlmediaelement.rs | 11 | ||||
-rw-r--r-- | components/script/dom/htmlslotelement.rs | 547 | ||||
-rw-r--r-- | components/script/dom/mod.rs | 1 | ||||
-rw-r--r-- | components/script/dom/node.rs | 31 | ||||
-rw-r--r-- | components/script/dom/raredata.rs | 3 | ||||
-rw-r--r-- | components/script/dom/shadowroot.rs | 23 | ||||
-rw-r--r-- | components/script/dom/text.rs | 22 | ||||
-rw-r--r-- | components/script/dom/virtualmethods.rs | 4 | ||||
-rw-r--r-- | components/script/dom/webidls/Element.webidl | 2 | ||||
-rw-r--r-- | components/script/dom/webidls/HTMLSlotElement.webidl | 25 | ||||
-rw-r--r-- | components/script/dom/webidls/ShadowRoot.webidl | 4 |
13 files changed, 736 insertions, 12 deletions
diff --git a/components/script/dom/create.rs b/components/script/dom/create.rs index cde5bcbb13f..5d4e09915c6 100644 --- a/components/script/dom/create.rs +++ b/components/script/dom/create.rs @@ -66,6 +66,7 @@ use crate::dom::htmlprogresselement::HTMLProgressElement; use crate::dom::htmlquoteelement::HTMLQuoteElement; use crate::dom::htmlscriptelement::HTMLScriptElement; use crate::dom::htmlselectelement::HTMLSelectElement; +use crate::dom::htmlslotelement::HTMLSlotElement; use crate::dom::htmlsourceelement::HTMLSourceElement; use crate::dom::htmlspanelement::HTMLSpanElement; use crate::dom::htmlstyleelement::HTMLStyleElement; @@ -357,6 +358,7 @@ pub(crate) fn create_native_html_element( local_name!("script") => make!(HTMLScriptElement, creator), local_name!("section") => make!(HTMLElement), local_name!("select") => make!(HTMLSelectElement), + local_name!("slot") => make!(HTMLSlotElement), local_name!("small") => make!(HTMLElement), local_name!("source") => make!(HTMLSourceElement), // https://html.spec.whatwg.org/multipage/#other-elements,-attributes-and-apis:spacer diff --git a/components/script/dom/element.rs b/components/script/dom/element.rs index 166b9707e31..58a4d26a6a5 100644 --- a/components/script/dom/element.rs +++ b/components/script/dom/element.rs @@ -74,7 +74,7 @@ use crate::dom::bindings::codegen::Bindings::FunctionBinding::Function; use crate::dom::bindings::codegen::Bindings::HTMLTemplateElementBinding::HTMLTemplateElementMethods; use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods; use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::{ - ShadowRootMethods, ShadowRootMode, + ShadowRootMethods, ShadowRootMode, SlotAssignmentMode, }; use crate::dom::bindings::codegen::Bindings::WindowBinding::{ ScrollBehavior, ScrollToOptions, WindowMethods, @@ -105,6 +105,7 @@ use crate::dom::domrectlist::DOMRectList; use crate::dom::domtokenlist::DOMTokenList; use crate::dom::elementinternals::ElementInternals; use crate::dom::eventtarget::EventTarget; +use crate::dom::globalscope::GlobalScope; use crate::dom::htmlanchorelement::HTMLAnchorElement; use crate::dom::htmlbodyelement::{HTMLBodyElement, HTMLBodyElementLayoutHelpers}; use crate::dom::htmlbuttonelement::HTMLButtonElement; @@ -124,6 +125,7 @@ use crate::dom::htmlobjectelement::HTMLObjectElement; use crate::dom::htmloptgroupelement::HTMLOptGroupElement; use crate::dom::htmloutputelement::HTMLOutputElement; use crate::dom::htmlselectelement::HTMLSelectElement; +use crate::dom::htmlslotelement::{HTMLSlotElement, Slottable}; use crate::dom::htmlstyleelement::HTMLStyleElement; use crate::dom::htmltablecellelement::{HTMLTableCellElement, HTMLTableCellElementLayoutHelpers}; use crate::dom::htmltableelement::{HTMLTableElement, HTMLTableElementLayoutHelpers}; @@ -510,6 +512,7 @@ impl Element { is_ua_widget: IsUserAgentWidget, mode: ShadowRootMode, clonable: bool, + slot_assignment_mode: SlotAssignmentMode, ) -> Fallible<DomRoot<ShadowRoot>> { // Step 1. // If element’s namespace is not the HTML namespace, @@ -536,7 +539,13 @@ impl Element { } // Steps 4, 5 and 6. - let shadow_root = ShadowRoot::new(self, &self.node.owner_doc(), mode, clonable); + let shadow_root = ShadowRoot::new( + self, + &self.node.owner_doc(), + mode, + slot_assignment_mode, + clonable, + ); self.ensure_rare_data().shadow_root = Some(Dom::from_ref(&*shadow_root)); shadow_root .upcast::<Node>() @@ -603,6 +612,43 @@ impl Element { Some(node) => node.is::<Document>(), } } + + pub(crate) fn assigned_slot(&self) -> Option<DomRoot<HTMLSlotElement>> { + let assigned_slot = self + .rare_data + .borrow() + .as_ref()? + .slottable_data + .assigned_slot + .as_ref()? + .as_rooted(); + Some(assigned_slot) + } + + pub(crate) fn set_assigned_slot(&self, assigned_slot: DomRoot<HTMLSlotElement>) { + self.ensure_rare_data().slottable_data.assigned_slot = Some(assigned_slot.as_traced()); + } + + pub(crate) fn manual_slot_assignment(&self) -> Option<DomRoot<HTMLSlotElement>> { + let manually_assigned_slot = self + .rare_data + .borrow() + .as_ref()? + .slottable_data + .manual_slot_assignment + .as_ref()? + .as_rooted(); + Some(manually_assigned_slot) + } + + pub(crate) fn set_manual_slot_assignment( + &self, + manually_assigned_slot: Option<&HTMLSlotElement>, + ) { + self.ensure_rare_data() + .slottable_data + .manual_slot_assignment = manually_assigned_slot.map(Dom::from_ref); + } } /// <https://dom.spec.whatwg.org/#valid-shadow-host-name> @@ -3084,7 +3130,12 @@ impl ElementMethods<crate::DomTypeHolder> for Element { fn AttachShadow(&self, init: &ShadowRootInit) -> Fallible<DomRoot<ShadowRoot>> { // Step 1. Run attach a shadow root with this, init["mode"], init["clonable"], init["serializable"], // init["delegatesFocus"], and init["slotAssignment"]. - let shadow_root = self.attach_shadow(IsUserAgentWidget::No, init.mode, init.clonable)?; + let shadow_root = self.attach_shadow( + IsUserAgentWidget::No, + init.mode, + init.clonable, + init.slotAssignment, + )?; // Step 2. Return this’s shadow root. Ok(shadow_root) @@ -3460,6 +3511,16 @@ impl ElementMethods<crate::DomTypeHolder> for Element { fn SetAriaValueText(&self, value: Option<DOMString>, can_gc: CanGc) { self.set_nullable_string_attribute(&local_name!("aria-valuetext"), value, can_gc); } + + /// <https://dom.spec.whatwg.org/#dom-slotable-assignedslot> + fn GetAssignedSlot(&self) -> Option<DomRoot<HTMLSlotElement>> { + let cx = GlobalScope::get_cx(); + + // > The assignedSlot getter steps are to return the result of + // > find a slot given this and with the open flag set. + rooted!(in(*cx) let slottable = Slottable::Element(Dom::from_ref(self))); + slottable.find_a_slot(true) + } } impl VirtualMethods for Element { @@ -3600,6 +3661,12 @@ impl VirtualMethods for Element { } } }, + &local_name!("slot") => { + // Update slottable data + let cx = GlobalScope::get_cx(); + rooted!(in(*cx) let slottable = Slottable::Element(Dom::from_ref(self))); + slottable.update_slot_name(attr, mutation, CanGc::note()) + }, _ => { // FIXME(emilio): This is pretty dubious, and should be done in // the relevant super-classes. diff --git a/components/script/dom/htmlmediaelement.rs b/components/script/dom/htmlmediaelement.rs index b836f414496..c7fd9462b21 100644 --- a/components/script/dom/htmlmediaelement.rs +++ b/components/script/dom/htmlmediaelement.rs @@ -54,7 +54,9 @@ use crate::dom::bindings::codegen::Bindings::MediaErrorBinding::MediaErrorConsta use crate::dom::bindings::codegen::Bindings::MediaErrorBinding::MediaErrorMethods; use crate::dom::bindings::codegen::Bindings::NavigatorBinding::Navigator_Binding::NavigatorMethods; use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods; -use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::ShadowRootMode; +use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::{ + ShadowRootMode, SlotAssignmentMode, +}; use crate::dom::bindings::codegen::Bindings::TextTrackBinding::{TextTrackKind, TextTrackMode}; use crate::dom::bindings::codegen::Bindings::URLBinding::URLMethods; use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods; @@ -1888,7 +1890,12 @@ impl HTMLMediaElement { return; } let shadow_root = element - .attach_shadow(IsUserAgentWidget::Yes, ShadowRootMode::Closed, false) + .attach_shadow( + IsUserAgentWidget::Yes, + ShadowRootMode::Closed, + false, + SlotAssignmentMode::Manual, + ) .unwrap(); let document = self.owner_document(); let script = HTMLScriptElement::new( diff --git a/components/script/dom/htmlslotelement.rs b/components/script/dom/htmlslotelement.rs new file mode 100644 index 00000000000..b5fcdb91c6d --- /dev/null +++ b/components/script/dom/htmlslotelement.rs @@ -0,0 +1,547 @@ +/* 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::RefCell; + +use dom_struct::dom_struct; +use html5ever::{local_name, namespace_url, ns, LocalName, Prefix}; +use js::gc::{RootedGuard, RootedVec}; +use js::rust::HandleObject; +use servo_atoms::Atom; +use style::attr::AttrValue; + +use crate::dom::attr::Attr; +use crate::dom::bindings::codegen::Bindings::HTMLSlotElementBinding::{ + AssignedNodesOptions, HTMLSlotElementMethods, +}; +use crate::dom::bindings::codegen::Bindings::NodeBinding::{GetRootNodeOptions, NodeMethods}; +use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::ShadowRoot_Binding::ShadowRootMethods; +use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::{ + ShadowRootMode, SlotAssignmentMode, +}; +use crate::dom::bindings::codegen::UnionTypes::ElementOrText; +use crate::dom::bindings::inheritance::Castable; +use crate::dom::bindings::root::{Dom, DomRoot}; +use crate::dom::bindings::str::DOMString; +use crate::dom::document::Document; +use crate::dom::element::{AttributeMutation, Element}; +use crate::dom::globalscope::GlobalScope; +use crate::dom::htmlelement::HTMLElement; +use crate::dom::node::{Node, ShadowIncluding}; +use crate::dom::text::Text; +use crate::dom::virtualmethods::VirtualMethods; +use crate::script_runtime::CanGc; + +/// <https://html.spec.whatwg.org/multipage/#the-slot-element> +#[dom_struct] +pub struct HTMLSlotElement { + htmlelement: HTMLElement, + + /// <https://dom.spec.whatwg.org/#slot-assigned-nodes> + assigned_nodes: RefCell<Vec<Slottable>>, + + /// <https://html.spec.whatwg.org/multipage/#manually-assigned-nodes> + manually_assigned_nodes: RefCell<Vec<Slottable>>, +} + +impl HTMLSlotElementMethods<crate::DomTypeHolder> for HTMLSlotElement { + // https://html.spec.whatwg.org/multipage/#dom-slot-name + make_getter!(Name, "name"); + + // https://html.spec.whatwg.org/multipage/#dom-slot-name + make_atomic_setter!(SetName, "name"); + + /// <https://html.spec.whatwg.org/multipage/#dom-slot-assignednodes> + fn AssignedNodes(&self, options: &AssignedNodesOptions) -> Vec<DomRoot<Node>> { + // Step 1. If options["flatten"] is false, then return this's assigned nodes. + if !options.flatten { + return self + .assigned_nodes + .borrow() + .iter() + .map(|slottable| slottable.node()) + .map(DomRoot::from_ref) + .collect(); + } + + // Step 2. Return the result of finding flattened slottables with this. + rooted_vec!(let mut flattened_slottables); + self.find_flattened_slottables(&mut flattened_slottables); + + flattened_slottables + .iter() + .map(|slottable| DomRoot::from_ref(slottable.node())) + .collect() + } + + /// <https://html.spec.whatwg.org/multipage/#dom-slot-assignedelements> + fn AssignedElements(&self, options: &AssignedNodesOptions) -> Vec<DomRoot<Element>> { + self.AssignedNodes(options) + .into_iter() + .flat_map(|node| node.downcast::<Element>().map(DomRoot::from_ref)) + .collect() + } + + /// <https://html.spec.whatwg.org/multipage/#dom-slot-assign> + fn Assign(&self, nodes: Vec<ElementOrText>) { + let cx = GlobalScope::get_cx(); + + // Step 1. For each node of this's manually assigned nodes, set node's manual slot assignment to null. + for slottable in self.manually_assigned_nodes.borrow().iter() { + slottable.set_manual_slot_assignment(None); + } + + // Step 2. Let nodesSet be a new ordered set. + rooted_vec!(let mut nodes_set); + + // Step 3. For each node of nodes: + for element_or_text in nodes.into_iter() { + rooted!(in(*cx) let node = match element_or_text { + ElementOrText::Element(element) => Slottable::Element(Dom::from_ref(&element)), + ElementOrText::Text(text) => Slottable::Text(Dom::from_ref(&text)), + }); + + // Step 3.1 If node's manual slot assignment refers to a slot, + // then remove node from that slot's manually assigned nodes. + if let Some(slot) = node.manual_slot_assignment() { + let mut manually_assigned_nodes = slot.manually_assigned_nodes.borrow_mut(); + if let Some(position) = manually_assigned_nodes + .iter() + .position(|value| *value == *node) + { + manually_assigned_nodes.remove(position); + } + } + + // Step 3.2 Set node's manual slot assignment to this. + node.set_manual_slot_assignment(Some(self)); + + // Step 3.3 Append node to nodesSet. + if !nodes_set.contains(&*node) { + nodes_set.push(node.clone()); + } + } + + // Step 4. Set this's manually assigned nodes to nodesSet. + *self.manually_assigned_nodes.borrow_mut() = nodes_set.iter().cloned().collect(); + + // Step 5. Run assign slottables for a tree for this's root. + self.upcast::<Node>() + .GetRootNode(&GetRootNodeOptions::empty()) + .assign_slottables_for_a_tree(); + } +} + +/// <https://dom.spec.whatwg.org/#concept-slotable> +#[derive(Clone, JSTraceable, MallocSizeOf, PartialEq)] +#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] +pub(crate) enum Slottable { + Element(Dom<Element>), + Text(Dom<Text>), +} + +/// Data shared between all [slottables](https://dom.spec.whatwg.org/#concept-slotable) +/// +/// Note that the [slottable name](https://dom.spec.whatwg.org/#slotable-name) is not +/// part of this. While the spec says that all slottables have a name, only Element's +/// can ever have a non-empty name, so they store it seperately +#[derive(Default, JSTraceable, MallocSizeOf)] +#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] +pub struct SlottableData { + /// <https://dom.spec.whatwg.org/#slotable-assigned-slot> + pub(crate) assigned_slot: Option<Dom<HTMLSlotElement>>, + + /// <https://dom.spec.whatwg.org/#slottable-manual-slot-assignment> + pub(crate) manual_slot_assignment: Option<Dom<HTMLSlotElement>>, +} + +impl HTMLSlotElement { + fn new_inherited( + local_name: LocalName, + prefix: Option<Prefix>, + document: &Document, + ) -> HTMLSlotElement { + HTMLSlotElement { + htmlelement: HTMLElement::new_inherited(local_name, prefix, document), + assigned_nodes: Default::default(), + manually_assigned_nodes: 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<HTMLSlotElement> { + Node::reflect_node_with_proto( + Box::new(HTMLSlotElement::new_inherited(local_name, prefix, document)), + document, + proto, + can_gc, + ) + } + + /// <https://dom.spec.whatwg.org/#find-flattened-slotables> + fn find_flattened_slottables(&self, result: &mut RootedVec<Slottable>) { + // Step 1. Let result be an empty list. + debug_assert!(result.is_empty()); + + // Step 2. If slot’s root is not a shadow root, then return result. + if self.upcast::<Node>().containing_shadow_root().is_none() { + return; + }; + + // Step 3. Let slottables be the result of finding slottables given slot. + rooted_vec!(let mut slottables); + self.find_slottables(&mut slottables); + + // Step 4. If slottables is the empty list, then append each slottable + // child of slot, in tree order, to slottables. + if slottables.is_empty() { + for child in self.upcast::<Node>().children() { + if let Some(element) = child.downcast::<Element>() { + slottables.push(Slottable::Element(Dom::from_ref(element))); + } else if let Some(text) = child.downcast::<Text>() { + slottables.push(Slottable::Text(Dom::from_ref(text))); + } + } + } + + // Step 5. For each node in slottables: + for slottable in slottables.iter() { + // Step 5.1 If node is a slot whose root is a shadow root: + // NOTE: Only elements can be slots + let maybe_slot_element = match &slottable { + Slottable::Element(element) => element.downcast::<HTMLSlotElement>(), + Slottable::Text(_) => None, + }; + match maybe_slot_element { + Some(slot_element) + if slot_element + .upcast::<Node>() + .containing_shadow_root() + .is_some() => + { + // Step 5.1.1 Let temporaryResult be the result of finding flattened slottables given node. + rooted_vec!(let mut temporary_result); + slot_element.find_flattened_slottables(&mut temporary_result); + + // Step 5.1.2 Append each slottable in temporaryResult, in order, to result. + result.extend_from_slice(&temporary_result); + }, + // Step 5.2 Otherwise, append node to result. + _ => { + result.push(slottable.clone()); + }, + }; + } + + // Step 6. Return result. + } + + /// <https://dom.spec.whatwg.org/#find-slotables> + /// + /// To avoid rooting shenanigans, this writes the returned slottables + /// into the `result` argument + fn find_slottables(&self, result: &mut RootedVec<Slottable>) { + let cx = GlobalScope::get_cx(); + + // Step 1. Let result be an empty list. + debug_assert!(result.is_empty()); + + // Step 2. Let root be slot’s root. + // Step 3. If root is not a shadow root, then return result. + let Some(root) = self.upcast::<Node>().containing_shadow_root() else { + return; + }; + + // Step 4. Let host be root’s host. + let host = root.Host(); + + // Step 5. If root’s slot assignment is "manual": + if root.SlotAssignment() == SlotAssignmentMode::Manual { + // Step 5.1 Let result be « ». + // NOTE: redundant. + + // Step 5.2 For each slottable slottable of slot’s manually assigned nodes, + // if slottable’s parent is host, append slottable to result. + for slottable in self.manually_assigned_nodes.borrow().iter() { + if slottable + .node() + .GetParentNode() + .is_some_and(|node| &*node == host.upcast::<Node>()) + { + result.push(slottable.clone()); + } + } + } + // Step 6. Otherwise, for each slottable child slottable of host, in tree order: + else { + let mut for_slottable = |slottable: RootedGuard<Slottable>| { + // Step 6.1 Let foundSlot be the result of finding a slot given slottable. + let found_slot = slottable.find_a_slot(false); + + // Step 6.2 If foundSlot is slot, then append slottable to result. + if found_slot.is_some_and(|found_slot| &*found_slot == self) { + result.push(slottable.clone()); + } + }; + for child in host.upcast::<Node>().children() { + if let Some(element) = child.downcast::<Element>() { + rooted!(in(*cx) let slottable = Slottable::Element(Dom::from_ref(element))); + for_slottable(slottable); + continue; + } + if let Some(text) = child.downcast::<Text>() { + rooted!(in(*cx) let slottable = Slottable::Text(Dom::from_ref(text))); + for_slottable(slottable); + } + } + } + + // Step 7. Return result. + } + + /// <https://dom.spec.whatwg.org/#assign-slotables> + pub(crate) fn assign_slottables(&self) { + // Step 1. Let slottables be the result of finding slottables for slot. + rooted_vec!(let mut slottables); + self.find_slottables(&mut slottables); + + // Step 2. TODO If slottables and slot’s assigned nodes are not identical, + // then run signal a slot change for slot. + + // Step 3. Set slot’s assigned nodes to slottables. + *self.assigned_nodes.borrow_mut() = slottables.iter().cloned().collect(); + + // Step 4. For each slottable in slottables, set slottable’s assigned slot to slot. + for slottable in slottables.iter() { + slottable.set_assigned_slot(DomRoot::from_ref(self)); + } + } +} + +impl Slottable { + /// <https://dom.spec.whatwg.org/#find-a-slot> + pub(crate) fn find_a_slot(&self, open_flag: bool) -> Option<DomRoot<HTMLSlotElement>> { + // Step 1. If slottable’s parent is null, then return null. + let parent = self.node().GetParentNode()?; + + // Step 2. Let shadow be slottable’s parent’s shadow root. + // Step 3. If shadow is null, then return null. + let shadow_root = parent + .downcast::<Element>() + .and_then(Element::shadow_root)?; + + // Step 4. If the open flag is set and shadow’s mode is not "open", then return null. + if open_flag && shadow_root.Mode() != ShadowRootMode::Open { + return None; + } + + // Step 5. If shadow’s slot assignment is "manual", then return the slot in shadow’s descendants whose + // manually assigned nodes contains slottable, if any; otherwise null. + if shadow_root.SlotAssignment() == SlotAssignmentMode::Manual { + for node in shadow_root + .upcast::<Node>() + .traverse_preorder(ShadowIncluding::No) + { + if let Some(slot) = node.downcast::<HTMLSlotElement>() { + if slot.manually_assigned_nodes.borrow().contains(self) { + return Some(DomRoot::from_ref(slot)); + } + } + } + return None; + } + + // Step 6. Return the first slot in tree order in shadow’s descendants whose + // name is slottable’s name, if any; otherwise null. + for node in shadow_root + .upcast::<Node>() + .traverse_preorder(ShadowIncluding::No) + { + if let Some(slot) = node.downcast::<HTMLSlotElement>() { + if slot.Name() == self.name() { + return Some(DomRoot::from_ref(slot)); + } + } + } + None + } + + /// Slottable name change steps from <https://dom.spec.whatwg.org/#light-tree-slotables> + pub(crate) fn update_slot_name(&self, attr: &Attr, mutation: AttributeMutation, can_gc: CanGc) { + debug_assert!(matches!(self, Self::Element(_))); + + // Step 1. If localName is slot and namespace is null: + // NOTE: This is done by the caller + let old_value = if let AttributeMutation::Set(old_name) = mutation { + old_name.and_then(|attr| match attr { + AttrValue::String(s) => Some(s.clone()), + _ => None, + }) + } else { + None + }; + let value = mutation.new_value(attr).and_then(|attr| match &*attr { + AttrValue::String(s) => Some(s.clone()), + _ => None, + }); + + // Step 1.1 If value is oldValue, then return. + if value == old_value { + return; + } + + // Step 1.2 If value is null and oldValue is the empty string, then return. + if value.is_none() && old_value.as_ref().is_some_and(|s| s.is_empty()) { + return; + } + + // Step 1.3 If value is the empty string and oldValue is null, then return. + if old_value.is_none() && value.as_ref().is_some_and(|s| s.is_empty()) { + return; + } + + // Step 1.4 If value is null or the empty string, then set element’s name to the empty string. + if value.as_ref().is_none_or(|s| s.is_empty()) { + self.set_name(DOMString::new(), can_gc); + } + // Step 1.5 Otherwise, set element’s name to value. + else { + self.set_name(DOMString::from(value.unwrap_or_default()), can_gc); + } + + // Step 1.6 If element is assigned, then run assign slottables for element’s assigned slot. + if let Some(assigned_slot) = self.assigned_slot() { + assigned_slot.assign_slottables(); + } + + // Step 1.7 Run assign a slot for element. + self.assign_a_slot(); + } + + /// <https://dom.spec.whatwg.org/#assign-a-slot> + pub(crate) fn assign_a_slot(&self) { + // Step 1. Let slot be the result of finding a slot with slottable. + let slot = self.find_a_slot(false); + + // Step 2. If slot is non-null, then run assign slottables for slot. + if let Some(slot) = slot { + slot.assign_slottables(); + } + } + + fn node(&self) -> &Node { + match self { + Self::Element(element) => element.upcast(), + Self::Text(text) => text.upcast(), + } + } + + fn assigned_slot(&self) -> Option<DomRoot<HTMLSlotElement>> { + match self { + Self::Element(element) => element.assigned_slot(), + Self::Text(text) => { + let assigned_slot = text + .slottable_data() + .borrow() + .assigned_slot + .as_ref()? + .as_rooted(); + Some(assigned_slot) + }, + } + } + + pub(crate) fn set_assigned_slot(&self, assigned_slot: DomRoot<HTMLSlotElement>) { + match self { + Self::Element(element) => element.set_assigned_slot(assigned_slot), + Self::Text(text) => { + text.slottable_data().borrow_mut().assigned_slot = Some(assigned_slot.as_traced()) + }, + } + } + + pub(crate) fn set_manual_slot_assignment( + &self, + manually_assigned_slot: Option<&HTMLSlotElement>, + ) { + match self { + Self::Element(element) => element.set_manual_slot_assignment(manually_assigned_slot), + Self::Text(text) => { + text.slottable_data().borrow_mut().manual_slot_assignment = + manually_assigned_slot.map(Dom::from_ref) + }, + } + } + + pub(crate) fn manual_slot_assignment(&self) -> Option<DomRoot<HTMLSlotElement>> { + match self { + Self::Element(element) => element.manual_slot_assignment(), + Self::Text(text) => text + .slottable_data() + .borrow() + .manual_slot_assignment + .as_ref() + .map(Dom::as_rooted), + } + } + + fn set_name(&self, name: DOMString, can_gc: CanGc) { + // NOTE: Only elements have non-empty names + let Self::Element(element) = self else { + return; + }; + let element = element.as_rooted(); + element.set_attribute( + &local_name!("name"), + AttrValue::Atom(Atom::from(name)), + can_gc, + ); + } + + fn name(&self) -> DOMString { + // NOTE: Only elements have non-empty names + let Self::Element(element) = self else { + return DOMString::new(); + }; + + element + .name_attribute() + .map(|a| DOMString::from(a.as_ref())) + .unwrap_or_default() + .clone() + } +} + +impl VirtualMethods for HTMLSlotElement { + fn super_type(&self) -> Option<&dyn VirtualMethods> { + Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods) + } + + /// <https://dom.spec.whatwg.org/#shadow-tree-slots> + fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation) { + self.super_type().unwrap().attribute_mutated(attr, mutation); + + if attr.local_name() == &local_name!("name") && attr.namespace() == &ns!() { + self.upcast::<Node>() + .GetRootNode(&GetRootNodeOptions::empty()) + .assign_slottables_for_a_tree() + } + } +} + +impl js::gc::Rootable for Slottable {} + +impl js::gc::Initialize for Slottable { + #[allow(unsafe_code)] + #[cfg_attr(crown, allow(crown::unrooted_must_root))] + unsafe fn initial() -> Option<Self> { + None + } +} diff --git a/components/script/dom/mod.rs b/components/script/dom/mod.rs index 2848f5ed1e5..4d31684cfb4 100644 --- a/components/script/dom/mod.rs +++ b/components/script/dom/mod.rs @@ -398,6 +398,7 @@ pub(crate) mod htmlquoteelement; #[allow(dead_code)] pub(crate) mod htmlscriptelement; pub(crate) mod htmlselectelement; +pub(crate) mod htmlslotelement; pub(crate) mod htmlsourceelement; pub(crate) mod htmlspanelement; pub(crate) mod htmlstyleelement; diff --git a/components/script/dom/node.rs b/components/script/dom/node.rs index 3518e3d9b7f..1f991d3b715 100644 --- a/components/script/dom/node.rs +++ b/components/script/dom/node.rs @@ -91,6 +91,7 @@ use crate::dom::htmliframeelement::{HTMLIFrameElement, HTMLIFrameElementLayoutMe use crate::dom::htmlimageelement::{HTMLImageElement, LayoutHTMLImageElementHelpers}; use crate::dom::htmlinputelement::{HTMLInputElement, LayoutHTMLInputElementHelpers}; use crate::dom::htmllinkelement::HTMLLinkElement; +use crate::dom::htmlslotelement::HTMLSlotElement; use crate::dom::htmlstyleelement::HTMLStyleElement; use crate::dom::htmltextareaelement::{HTMLTextAreaElement, LayoutHTMLTextAreaElementHelpers}; use crate::dom::htmlvideoelement::{HTMLVideoElement, LayoutHTMLVideoElementHelpers}; @@ -1316,6 +1317,24 @@ impl Node { .as_ref() .map(|data| data.element_data.borrow().styles.primary().clone()) } + + /// <https://dom.spec.whatwg.org/#assign-slotables-for-a-tree> + pub(crate) fn assign_slottables_for_a_tree(&self) { + // NOTE: This method traverses all descendants of the node and is potentially very + // expensive. If the node is not a shadow root then assigning slottables to it won't + // have any effect, so we take a fast path out. + if !self.is::<ShadowRoot>() { + return; + } + + // > To assign slottables for a tree, given a node root, run assign slottables for each slot + // > slot in root’s inclusive descendants, in tree order. + for node in self.traverse_preorder(ShadowIncluding::No) { + if let Some(slot) = node.downcast::<HTMLSlotElement>() { + slot.assign_slottables(); + } + } + } } /// Iterate through `nodes` until we find a `Node` that is not in `not_in` @@ -2113,6 +2132,11 @@ impl Node { for kid in new_nodes { // Step 7.1. parent.add_child(kid, child); + + // Step 7.6 Run assign slottables for a tree with node’s root. + kid.GetRootNode(&GetRootNodeOptions::empty()) + .assign_slottables_for_a_tree(); + // Step 7.7. for descendant in kid .traverse_preorder(ShadowIncluding::Yes) @@ -2464,7 +2488,12 @@ impl Node { // node’s shadow root’s serializable, node’s shadow root’s delegates focus, // and node’s shadow root’s slot assignment. let copy_shadow_root = - copy_elem.attach_shadow(IsUserAgentWidget::No, shadow_root.Mode(), true) + copy_elem.attach_shadow( + IsUserAgentWidget::No, + shadow_root.Mode(), + true, + shadow_root.SlotAssignment() + ) .expect("placement of attached shadow root must be valid, as this is a copy of an existing one"); // TODO: Step 7.3 Set copy’s shadow root’s declarative to node’s shadow root’s declarative. diff --git a/components/script/dom/raredata.rs b/components/script/dom/raredata.rs index d9b087d3b63..383eaaf70c3 100644 --- a/components/script/dom/raredata.rs +++ b/components/script/dom/raredata.rs @@ -12,6 +12,7 @@ use crate::dom::customelementregistry::{ CustomElementDefinition, CustomElementReaction, CustomElementState, }; use crate::dom::elementinternals::ElementInternals; +use crate::dom::htmlslotelement::SlottableData; use crate::dom::mutationobserver::RegisteredObserver; use crate::dom::node::UniqueId; use crate::dom::shadowroot::ShadowRoot; @@ -57,4 +58,6 @@ pub(crate) struct ElementRareData { pub(crate) client_rect: Option<LayoutValue<Rect<i32>>>, /// <https://html.spec.whatwg.org/multipage#elementinternals> pub(crate) element_internals: Option<Dom<ElementInternals>>, + + pub(crate) slottable_data: SlottableData, } diff --git a/components/script/dom/shadowroot.rs b/components/script/dom/shadowroot.rs index c3911b35642..46fcb38cf1b 100644 --- a/components/script/dom/shadowroot.rs +++ b/components/script/dom/shadowroot.rs @@ -12,8 +12,10 @@ use style::stylesheets::Stylesheet; use style::stylist::{CascadeData, Stylist}; use crate::dom::bindings::cell::DomRefCell; -use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::ShadowRootMode; use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::ShadowRoot_Binding::ShadowRootMethods; +use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::{ + ShadowRootMode, SlotAssignmentMode, +}; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::num::Finite; use crate::dom::bindings::reflector::reflect_dom_object; @@ -57,6 +59,9 @@ pub(crate) struct ShadowRoot { /// <https://dom.spec.whatwg.org/#dom-shadowroot-mode> mode: ShadowRootMode, + /// <https://dom.spec.whatwg.org/#dom-shadowroot-slotassignment> + slot_assignment_mode: SlotAssignmentMode, + /// <https://dom.spec.whatwg.org/#dom-shadowroot-clonable> clonable: bool, } @@ -67,6 +72,7 @@ impl ShadowRoot { host: &Element, document: &Document, mode: ShadowRootMode, + slot_assignment_mode: SlotAssignmentMode, clonable: bool, ) -> ShadowRoot { let document_fragment = DocumentFragment::new_inherited(document); @@ -86,6 +92,7 @@ impl ShadowRoot { stylesheet_list: MutNullableDom::new(None), window: Dom::from_ref(document.window()), mode, + slot_assignment_mode, clonable, } } @@ -94,10 +101,17 @@ impl ShadowRoot { host: &Element, document: &Document, mode: ShadowRootMode, + slot_assignment_mode: SlotAssignmentMode, clonable: bool, ) -> DomRoot<ShadowRoot> { reflect_dom_object( - Box::new(ShadowRoot::new_inherited(host, document, mode, clonable)), + Box::new(ShadowRoot::new_inherited( + host, + document, + mode, + slot_assignment_mode, + clonable, + )), document.window(), CanGc::note(), ) @@ -306,6 +320,11 @@ impl ShadowRootMethods<crate::DomTypeHolder> for ShadowRoot { // Step 4. Replace all with fragment within this. Node::replace_all(Some(frag.upcast()), self.upcast()); } + + /// <https://dom.spec.whatwg.org/#dom-shadowroot-slotassignment> + fn SlotAssignment(&self) -> SlotAssignmentMode { + self.slot_assignment_mode + } } impl VirtualMethods for ShadowRoot { diff --git a/components/script/dom/text.rs b/components/script/dom/text.rs index 2f0e1c66857..3f81f0be6e2 100644 --- a/components/script/dom/text.rs +++ b/components/script/dom/text.rs @@ -2,6 +2,8 @@ * 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::RefCell; + use dom_struct::dom_struct; use js::rust::HandleObject; @@ -12,10 +14,12 @@ use crate::dom::bindings::codegen::Bindings::TextBinding::TextMethods; use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods; use crate::dom::bindings::error::{Error, Fallible}; use crate::dom::bindings::inheritance::Castable; -use crate::dom::bindings::root::DomRoot; +use crate::dom::bindings::root::{Dom, DomRoot}; use crate::dom::bindings::str::DOMString; use crate::dom::characterdata::CharacterData; use crate::dom::document::Document; +use crate::dom::globalscope::GlobalScope; +use crate::dom::htmlslotelement::{HTMLSlotElement, Slottable, SlottableData}; use crate::dom::node::Node; use crate::dom::window::Window; use crate::script_runtime::CanGc; @@ -24,12 +28,14 @@ use crate::script_runtime::CanGc; #[dom_struct] pub(crate) struct Text { characterdata: CharacterData, + slottable_data: RefCell<SlottableData>, } impl Text { pub(crate) fn new_inherited(text: DOMString, document: &Document) -> Text { Text { characterdata: CharacterData::new_inherited(text, document), + slottable_data: Default::default(), } } @@ -50,6 +56,10 @@ impl Text { can_gc, ) } + + pub(crate) fn slottable_data(&self) -> &RefCell<SlottableData> { + &self.slottable_data + } } impl TextMethods<crate::DomTypeHolder> for Text { @@ -119,4 +129,14 @@ impl TextMethods<crate::DomTypeHolder> for Text { } DOMString::from(text) } + + /// <https://dom.spec.whatwg.org/#dom-slotable-assignedslot> + fn GetAssignedSlot(&self) -> Option<DomRoot<HTMLSlotElement>> { + let cx = GlobalScope::get_cx(); + + // > The assignedSlot getter steps are to return the result of + // > find a slot given this and with the open flag set. + rooted!(in(*cx) let slottable = Slottable::Text(Dom::from_ref(self))); + slottable.find_a_slot(true) + } } diff --git a/components/script/dom/virtualmethods.rs b/components/script/dom/virtualmethods.rs index 4f552cefee1..6c2f8b646c6 100644 --- a/components/script/dom/virtualmethods.rs +++ b/components/script/dom/virtualmethods.rs @@ -43,6 +43,7 @@ use crate::dom::htmloutputelement::HTMLOutputElement; use crate::dom::htmlpreelement::HTMLPreElement; use crate::dom::htmlscriptelement::HTMLScriptElement; use crate::dom::htmlselectelement::HTMLSelectElement; +use crate::dom::htmlslotelement::HTMLSlotElement; use crate::dom::htmlsourceelement::HTMLSourceElement; use crate::dom::htmlstyleelement::HTMLStyleElement; use crate::dom::htmltablecellelement::HTMLTableCellElement; @@ -257,6 +258,9 @@ pub(crate) fn vtable_for(node: &Node) -> &dyn VirtualMethods { NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLSourceElement)) => { node.downcast::<HTMLSourceElement>().unwrap() as &dyn VirtualMethods }, + NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLSlotElement)) => { + node.downcast::<HTMLSlotElement>().unwrap() as &dyn VirtualMethods + }, NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLStyleElement)) => { node.downcast::<HTMLStyleElement>().unwrap() as &dyn VirtualMethods }, diff --git a/components/script/dom/webidls/Element.webidl b/components/script/dom/webidls/Element.webidl index c7884925541..60a6db4e4e1 100644 --- a/components/script/dom/webidls/Element.webidl +++ b/components/script/dom/webidls/Element.webidl @@ -90,7 +90,7 @@ interface Element : Node { dictionary ShadowRootInit { required ShadowRootMode mode; // boolean delegatesFocus = false; - // SlotAssignmentMode slotAssignment = "named"; + SlotAssignmentMode slotAssignment = "named"; boolean clonable = false; // boolean serializable = false; }; diff --git a/components/script/dom/webidls/HTMLSlotElement.webidl b/components/script/dom/webidls/HTMLSlotElement.webidl new file mode 100644 index 00000000000..bec872ab1fc --- /dev/null +++ b/components/script/dom/webidls/HTMLSlotElement.webidl @@ -0,0 +1,25 @@ +/* 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/#the-slot-element +[Exposed=Window] +interface HTMLSlotElement : HTMLElement { + [HTMLConstructor] constructor(); + + [CEReactions] attribute DOMString name; + sequence<Node> assignedNodes(optional AssignedNodesOptions options = {}); + sequence<Element> assignedElements(optional AssignedNodesOptions options = {}); + undefined assign((Element or Text)... nodes); +}; + +dictionary AssignedNodesOptions { + boolean flatten = false; +}; + +// https://dom.spec.whatwg.org/#mixin-slotable +interface mixin Slottable { + readonly attribute HTMLSlotElement? assignedSlot; +}; +Element includes Slottable; +Text includes Slottable; diff --git a/components/script/dom/webidls/ShadowRoot.webidl b/components/script/dom/webidls/ShadowRoot.webidl index 40301f48517..444dd53d22c 100644 --- a/components/script/dom/webidls/ShadowRoot.webidl +++ b/components/script/dom/webidls/ShadowRoot.webidl @@ -10,7 +10,7 @@ interface ShadowRoot : DocumentFragment { readonly attribute ShadowRootMode mode; // readonly attribute boolean delegatesFocus; - // readonly attribute SlotAssignmentMode slotAssignment; + readonly attribute SlotAssignmentMode slotAssignment; readonly attribute boolean clonable; // readonly attribute boolean serializable; readonly attribute Element host; @@ -19,7 +19,7 @@ interface ShadowRoot : DocumentFragment { enum ShadowRootMode { "open", "closed"}; -// enum SlotAssignmentMode { "manual", "named" }; +enum SlotAssignmentMode { "manual", "named" }; ShadowRoot includes DocumentOrShadowRoot; |