aboutsummaryrefslogtreecommitdiffstats
path: root/components/script
diff options
context:
space:
mode:
authorSimon Wülker <simon.wuelker@arcor.de>2025-01-19 15:05:05 +0100
committerGitHub <noreply@github.com>2025-01-19 14:05:05 +0000
commitdabe162d44a2b8a47b7a9dc8566e7f51ae678d05 (patch)
tree0b915202184ab6924ecb38cc961bbd4dd8ed830e /components/script
parent8bb50fa3c9d82e2575b02cefb6558467a8187439 (diff)
downloadservo-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.rs2
-rw-r--r--components/script/dom/element.rs73
-rw-r--r--components/script/dom/htmlmediaelement.rs11
-rw-r--r--components/script/dom/htmlslotelement.rs547
-rw-r--r--components/script/dom/mod.rs1
-rw-r--r--components/script/dom/node.rs31
-rw-r--r--components/script/dom/raredata.rs3
-rw-r--r--components/script/dom/shadowroot.rs23
-rw-r--r--components/script/dom/text.rs22
-rw-r--r--components/script/dom/virtualmethods.rs4
-rw-r--r--components/script/dom/webidls/Element.webidl2
-rw-r--r--components/script/dom/webidls/HTMLSlotElement.webidl25
-rw-r--r--components/script/dom/webidls/ShadowRoot.webidl4
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;