Skip to main content

script/dom/html/
htmlslotelement.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::{Cell, Ref, RefCell};
6
7use dom_struct::dom_struct;
8use html5ever::{LocalName, Prefix, local_name, ns};
9use js::context::JSContext;
10use js::gc::RootedVec;
11use js::rust::HandleObject;
12use script_bindings::codegen::InheritTypes::{CharacterDataTypeId, NodeTypeId};
13
14use crate::ScriptThread;
15use crate::dom::bindings::codegen::Bindings::HTMLSlotElementBinding::{
16    AssignedNodesOptions, HTMLSlotElementMethods,
17};
18use crate::dom::bindings::codegen::Bindings::NodeBinding::{GetRootNodeOptions, NodeMethods};
19use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::ShadowRoot_Binding::ShadowRootMethods;
20use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::{
21    ShadowRootMode, SlotAssignmentMode,
22};
23use crate::dom::bindings::codegen::UnionTypes::ElementOrText;
24use crate::dom::bindings::inheritance::Castable;
25use crate::dom::bindings::root::{Dom, DomRoot};
26use crate::dom::bindings::str::DOMString;
27use crate::dom::document::Document;
28use crate::dom::element::attributes::storage::AttrRef;
29use crate::dom::element::{AttributeMutation, Element};
30use crate::dom::html::htmlelement::HTMLElement;
31use crate::dom::node::virtualmethods::VirtualMethods;
32use crate::dom::node::{
33    BindContext, ForceSlottableNodeReconciliation, IsShadowTree, Node, NodeDamage, NodeTraits,
34    UnbindContext,
35};
36
37/// <https://html.spec.whatwg.org/multipage/#the-slot-element>
38#[dom_struct]
39pub(crate) struct HTMLSlotElement {
40    htmlelement: HTMLElement,
41
42    /// <https://dom.spec.whatwg.org/#slot-assigned-nodes>
43    assigned_nodes: RefCell<Vec<Slottable>>,
44
45    /// <https://html.spec.whatwg.org/multipage/#manually-assigned-nodes>
46    manually_assigned_nodes: RefCell<Vec<Slottable>>,
47
48    /// Whether there is a queued signal change for this element
49    ///
50    /// Necessary to avoid triggering too many slotchange events
51    is_in_agents_signal_slots: Cell<bool>,
52}
53
54impl HTMLSlotElementMethods<crate::DomTypeHolder> for HTMLSlotElement {
55    // https://html.spec.whatwg.org/multipage/#dom-slot-name
56    make_getter!(Name, "name");
57
58    // https://html.spec.whatwg.org/multipage/#dom-slot-name
59    make_atomic_setter!(SetName, "name");
60
61    /// <https://html.spec.whatwg.org/multipage/#dom-slot-assignednodes>
62    fn AssignedNodes(&self, cx: &JSContext, options: &AssignedNodesOptions) -> Vec<DomRoot<Node>> {
63        // Step 1. If options["flatten"] is false, then return this's assigned nodes.
64        if !options.flatten {
65            return self
66                .assigned_nodes
67                .borrow()
68                .iter()
69                .map(|slottable| slottable.node())
70                .map(DomRoot::from_ref)
71                .collect();
72        }
73
74        // Step 2. Return the result of finding flattened slottables with this.
75        rooted_vec!(let mut flattened_slottables);
76        self.find_flattened_slottables(cx, &mut flattened_slottables);
77
78        flattened_slottables
79            .iter()
80            .map(|slottable| DomRoot::from_ref(slottable.node()))
81            .collect()
82    }
83
84    /// <https://html.spec.whatwg.org/multipage/#dom-slot-assignedelements>
85    fn AssignedElements(
86        &self,
87        cx: &JSContext,
88        options: &AssignedNodesOptions,
89    ) -> Vec<DomRoot<Element>> {
90        self.AssignedNodes(cx, options)
91            .into_iter()
92            .flat_map(|node| node.downcast::<Element>().map(DomRoot::from_ref))
93            .collect()
94    }
95
96    /// <https://html.spec.whatwg.org/multipage/#dom-slot-assign>
97    fn Assign(&self, cx: &JSContext, nodes: Vec<ElementOrText>) {
98        // Step 1. For each node of this's manually assigned nodes, set node's manual slot assignment to null.
99        for slottable in self.manually_assigned_nodes.borrow().iter() {
100            slottable.set_manual_slot_assignment(None);
101        }
102
103        // Step 2. Let nodesSet be a new ordered set.
104        rooted_vec!(let mut nodes_set);
105
106        // Step 3. For each node of nodes:
107        for element_or_text in nodes.into_iter() {
108            rooted!(&in(cx) let node = match element_or_text {
109                ElementOrText::Element(element) => Slottable(Dom::from_ref(element.upcast())),
110                ElementOrText::Text(text) => Slottable(Dom::from_ref(text.upcast())),
111            });
112
113            // Step 3.1 If node's manual slot assignment refers to a slot,
114            // then remove node from that slot's manually assigned nodes.
115            if let Some(slot) = node.manual_slot_assignment() {
116                let mut manually_assigned_nodes = slot.manually_assigned_nodes.borrow_mut();
117                if let Some(position) = manually_assigned_nodes
118                    .iter()
119                    .position(|value| *value == *node)
120                {
121                    manually_assigned_nodes.remove(position);
122                }
123            }
124
125            // Step 3.2 Set node's manual slot assignment to this.
126            node.set_manual_slot_assignment(Some(self));
127
128            // Step 3.3 Append node to nodesSet.
129            if !nodes_set.contains(&*node) {
130                nodes_set.push(node.clone());
131            }
132        }
133
134        // Step 4. Set this's manually assigned nodes to nodesSet.
135        *self.manually_assigned_nodes.borrow_mut() = nodes_set.iter().cloned().collect();
136
137        // Step 5. Run assign slottables for a tree for this's root.
138        self.upcast::<Node>()
139            .GetRootNode(&GetRootNodeOptions::empty())
140            .assign_slottables_for_a_tree(cx, ForceSlottableNodeReconciliation::Force);
141    }
142}
143
144/// <https://dom.spec.whatwg.org/#concept-slotable>
145///
146/// The contained node is assumed to be either `Element` or `Text`
147///
148/// This field is public to make it easy to construct slottables.
149/// As such, it is possible to put Nodes that are not slottables
150/// in there. Using a [Slottable] like this will quickly lead to
151/// a panic.
152#[derive(Clone, JSTraceable, MallocSizeOf, PartialEq)]
153#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
154#[repr(transparent)]
155pub(crate) struct Slottable(pub Dom<Node>);
156/// Data shared between all [slottables](https://dom.spec.whatwg.org/#concept-slotable)
157///
158/// Note that the [slottable name](https://dom.spec.whatwg.org/#slotable-name) is not
159/// part of this. While the spec says that all slottables have a name, only Element's
160/// can ever have a non-empty name, so they store it separately
161#[derive(Default, JSTraceable, MallocSizeOf)]
162#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
163pub struct SlottableData {
164    /// <https://dom.spec.whatwg.org/#slotable-assigned-slot>
165    pub(crate) assigned_slot: Option<Dom<HTMLSlotElement>>,
166
167    /// <https://dom.spec.whatwg.org/#slottable-manual-slot-assignment>
168    pub(crate) manual_slot_assignment: Option<Dom<HTMLSlotElement>>,
169}
170
171impl HTMLSlotElement {
172    fn new_inherited(
173        local_name: LocalName,
174        prefix: Option<Prefix>,
175        document: &Document,
176    ) -> HTMLSlotElement {
177        HTMLSlotElement {
178            htmlelement: HTMLElement::new_inherited(local_name, prefix, document),
179            assigned_nodes: Default::default(),
180            manually_assigned_nodes: Default::default(),
181            is_in_agents_signal_slots: Default::default(),
182        }
183    }
184
185    pub(crate) fn new(
186        cx: &mut JSContext,
187        local_name: LocalName,
188        prefix: Option<Prefix>,
189        document: &Document,
190        proto: Option<HandleObject>,
191    ) -> DomRoot<HTMLSlotElement> {
192        Node::reflect_node_with_proto(
193            cx,
194            Box::new(HTMLSlotElement::new_inherited(local_name, prefix, document)),
195            document,
196            proto,
197        )
198    }
199
200    pub(crate) fn has_assigned_nodes(&self) -> bool {
201        !self.assigned_nodes.borrow().is_empty()
202    }
203
204    /// <https://dom.spec.whatwg.org/#find-flattened-slotables>
205    fn find_flattened_slottables(&self, cx: &JSContext, result: &mut RootedVec<Slottable>) {
206        // Step 1. Let result be an empty list.
207        // NOTE: In our case "result" is not necessarily empty, because it contains
208        // the results of multiple successive calls to "find_flattened_slottables". This method
209        // only appends to it, so that doesn't matter.
210
211        // Step 2. If slot’s root is not a shadow root, then return result.
212        if !self.upcast::<Node>().is_in_a_shadow_tree() {
213            return;
214        };
215
216        // Step 3. Let slottables be the result of finding slottables given slot.
217        rooted_vec!(let mut slottables);
218        self.find_slottables(cx, &mut slottables);
219
220        // Step 4. If slottables is the empty list, then append each slottable
221        // child of slot, in tree order, to slottables.
222        if slottables.is_empty() {
223            for child in self.upcast::<Node>().children_unrooted(cx) {
224                let is_slottable = matches!(
225                    child.type_id(),
226                    NodeTypeId::Element(_) |
227                        NodeTypeId::CharacterData(CharacterDataTypeId::Text(_))
228                );
229                if is_slottable {
230                    slottables.push(Slottable(Dom::from_ref(&*child)));
231                }
232            }
233        }
234
235        // Step 5. For each node in slottables:
236        for slottable in slottables.iter() {
237            // Step 5.1 If node is a slot whose root is a shadow root:
238            match slottable.0.downcast::<HTMLSlotElement>() {
239                Some(slot_element) if slot_element.upcast::<Node>().is_in_a_shadow_tree() => {
240                    // Step 5.1.1 Let temporaryResult be the result of finding flattened slottables given node.
241                    // Step 5.1.2 Append each slottable in temporaryResult, in order, to result.
242                    slot_element.find_flattened_slottables(cx, result);
243                },
244                // Step 5.2 Otherwise, append node to result.
245                _ => {
246                    result.push(slottable.clone());
247                },
248            };
249        }
250
251        // Step 6. Return result.
252    }
253
254    /// <https://dom.spec.whatwg.org/#find-slotables>
255    ///
256    /// To avoid rooting shenanigans, this writes the returned slottables
257    /// into the `result` argument
258    fn find_slottables(&self, cx: &JSContext, result: &mut RootedVec<Slottable>) {
259        // Step 1. Let result be an empty list.
260        debug_assert!(result.is_empty());
261
262        // Step 2. Let root be slot’s root.
263        // Step 3. If root is not a shadow root, then return result.
264        let Some(root) = self.upcast::<Node>().containing_shadow_root() else {
265            return;
266        };
267
268        // Step 4. Let host be root’s host.
269        let host = root.Host();
270
271        // Step 5. If root’s slot assignment is "manual":
272        if root.SlotAssignment() == SlotAssignmentMode::Manual {
273            // Step 5.1 Let result be « ».
274            // NOTE: redundant.
275
276            // Step 5.2 For each slottable slottable of slot’s manually assigned nodes,
277            // if slottable’s parent is host, append slottable to result.
278            for slottable in self.manually_assigned_nodes.borrow().iter() {
279                if slottable
280                    .node()
281                    .GetParentNode()
282                    .is_some_and(|node| &*node == host.upcast::<Node>())
283                {
284                    result.push(slottable.clone());
285                }
286            }
287        }
288        // Step 6. Otherwise, for each slottable child slottable of host, in tree order:
289        else {
290            for child in host.upcast::<Node>().children_unrooted(cx) {
291                let is_slottable = matches!(
292                    child.type_id(),
293                    NodeTypeId::Element(_) |
294                        NodeTypeId::CharacterData(CharacterDataTypeId::Text(_))
295                );
296                if is_slottable {
297                    rooted!(&in(cx) let slottable = Slottable(Dom::from_ref(&*child)));
298                    // Step 6.1 Let foundSlot be the result of finding a slot given slottable.
299                    let found_slot = slottable.find_a_slot(false);
300
301                    // Step 6.2 If foundSlot is slot, then append slottable to result.
302                    if found_slot.is_some_and(|found_slot| &*found_slot == self) {
303                        result.push(slottable.clone());
304                    }
305                }
306            }
307        }
308
309        // Step 7. Return result.
310    }
311
312    /// <https://dom.spec.whatwg.org/#assign-slotables>
313    pub(crate) fn assign_slottables(&self, cx: &JSContext) {
314        // Step 1. Let slottables be the result of finding slottables for slot.
315        rooted_vec!(let mut slottables);
316        self.find_slottables(cx, &mut slottables);
317
318        // Step 2. If slottables and slot’s assigned nodes are not identical,
319        // then run signal a slot change for slot.
320        // NOTE: If the slottables *are* identical then the rest of this algorithm
321        // won't have any effect, so we return early.
322        if self.assigned_nodes.borrow().iter().eq(slottables.iter()) {
323            return;
324        }
325
326        // Clear the style and layout data for all previously assigned slottables as well as
327        // marking them as dirty, as they need a full restyle and layout.
328        for slottable in self.assigned_nodes().iter() {
329            slottable
330                .node()
331                .remove_style_and_layout_data_from_subtree(cx);
332            slottable.node().dirty(NodeDamage::Other);
333        }
334
335        self.signal_a_slot_change(cx);
336
337        // If we don't disconnect the old slottables from this slot then they'll stay implicitily
338        // connected, which causes problems later on. Only do this for slottables that have not
339        // already been reassigned to a different slot.
340        //
341        // NOTE: This is not written in the spec, which is likely a bug. See:
342        // <https://github.com/whatwg/dom/issues/1352>.
343        for slottable in self.assigned_nodes().iter() {
344            if slottable
345                .assigned_slot()
346                .is_some_and(|assigned_slot| &*assigned_slot == self)
347            {
348                slottable.set_assigned_slot(None);
349            }
350        }
351
352        // Step 3. Set slot’s assigned nodes to slottables.
353        *self.assigned_nodes.borrow_mut() = slottables.iter().cloned().collect();
354
355        // Step 4. For each slottable in slottables, set slottable’s assigned slot to slot.
356        for slottable in slottables.iter() {
357            slottable.set_assigned_slot(Some(self));
358        }
359
360        // Also clear the style and layout data for all currently assigned slottables
361        // as well as marking them as dirty, as they need a full restyle and layout.
362        for slottable in slottables.iter() {
363            slottable
364                .node()
365                .remove_style_and_layout_data_from_subtree(cx);
366            slottable.node().dirty(NodeDamage::Other);
367        }
368    }
369
370    /// <https://dom.spec.whatwg.org/#signal-a-slot-change>
371    pub(crate) fn signal_a_slot_change(&self, cx: &JSContext) {
372        self.upcast::<Node>().dirty(NodeDamage::ContentOrHeritage);
373
374        if self.is_in_agents_signal_slots.get() {
375            return;
376        }
377        self.is_in_agents_signal_slots.set(true);
378
379        let mutation_observers = ScriptThread::mutation_observers();
380        // Step 1. Append slot to slot’s relevant agent’s signal slots.
381        mutation_observers.add_signal_slot(self);
382
383        // Step 2. Queue a mutation observer microtask.
384        mutation_observers.queue_mutation_observer_microtask(cx, ScriptThread::microtask_queue());
385    }
386
387    pub(crate) fn remove_from_signal_slots(&self) {
388        debug_assert!(self.is_in_agents_signal_slots.get());
389        self.is_in_agents_signal_slots.set(false);
390    }
391
392    /// Returns the slot's assigned nodes if the root's slot assignment mode
393    /// is "named", or the manually assigned nodes otherwise
394    pub(crate) fn assigned_nodes(&self) -> Ref<'_, [Slottable]> {
395        Ref::map(self.assigned_nodes.borrow(), Vec::as_slice)
396    }
397}
398
399impl Slottable {
400    /// <https://dom.spec.whatwg.org/#find-a-slot>
401    pub(crate) fn find_a_slot(&self, open_flag: bool) -> Option<DomRoot<HTMLSlotElement>> {
402        // Step 1. If slottable’s parent is null, then return null.
403        let parent = self.node().GetParentNode()?;
404
405        // Step 2. Let shadow be slottable’s parent’s shadow root.
406        // Step 3. If shadow is null, then return null.
407        let shadow_root = parent
408            .downcast::<Element>()
409            .and_then(Element::shadow_root)?;
410
411        // Step 4. If the open flag is set and shadow’s mode is not "open", then return null.
412        if open_flag && shadow_root.Mode() != ShadowRootMode::Open {
413            return None;
414        }
415
416        // Step 5. If shadow’s slot assignment is "manual", then return the slot in shadow’s descendants whose
417        // manually assigned nodes contains slottable, if any; otherwise null.
418        if shadow_root.SlotAssignment() == SlotAssignmentMode::Manual {
419            return self.assigned_slot();
420        }
421
422        // Step 6. Return the first slot in tree order in shadow’s descendants whose
423        // name is slottable’s name, if any; otherwise null.
424        shadow_root.slot_for_name(&self.name())
425    }
426
427    /// <https://dom.spec.whatwg.org/#assign-a-slot>
428    pub(crate) fn assign_a_slot(&self, cx: &JSContext) {
429        // Step 1. Let slot be the result of finding a slot with slottable.
430        let slot = self.find_a_slot(false);
431
432        // Step 2. If slot is non-null, then run assign slottables for slot.
433        if let Some(slot) = slot {
434            slot.assign_slottables(cx);
435        }
436    }
437
438    pub(crate) fn node(&self) -> &Node {
439        &self.0
440    }
441
442    pub(crate) fn assigned_slot(&self) -> Option<DomRoot<HTMLSlotElement>> {
443        self.node().assigned_slot()
444    }
445
446    pub(crate) fn set_assigned_slot(&self, assigned_slot: Option<&HTMLSlotElement>) {
447        self.node().set_assigned_slot(assigned_slot);
448    }
449
450    pub(crate) fn set_manual_slot_assignment(
451        &self,
452        manually_assigned_slot: Option<&HTMLSlotElement>,
453    ) {
454        self.node()
455            .set_manual_slot_assignment(manually_assigned_slot);
456    }
457
458    pub(crate) fn manual_slot_assignment(&self) -> Option<DomRoot<HTMLSlotElement>> {
459        self.node().manual_slot_assignment()
460    }
461
462    fn name(&self) -> DOMString {
463        // NOTE: Only elements have non-empty names
464        let Some(element) = self.0.downcast::<Element>() else {
465            return DOMString::new();
466        };
467
468        element.get_string_attribute(&local_name!("slot"))
469    }
470}
471
472impl VirtualMethods for HTMLSlotElement {
473    fn super_type(&self) -> Option<&dyn VirtualMethods> {
474        Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
475    }
476
477    /// <https://dom.spec.whatwg.org/#shadow-tree-slots>
478    fn attribute_mutated(
479        &self,
480        cx: &mut JSContext,
481        attr: AttrRef<'_>,
482        mutation: AttributeMutation,
483    ) {
484        self.super_type()
485            .unwrap()
486            .attribute_mutated(cx, attr, mutation);
487
488        if attr.local_name() == &local_name!("name") && attr.namespace() == &ns!() {
489            if let Some(shadow_root) = self.containing_shadow_root() {
490                // Shadow roots keep a list of slot descendants, so we need to tell it
491                // about our name change
492                let old_value = match mutation {
493                    AttributeMutation::Set(old, _) => old
494                        .map(|value| value.to_string().into())
495                        .unwrap_or_default(),
496                    AttributeMutation::Removed => attr.value().to_string().into(),
497                };
498
499                shadow_root.unregister_slot(old_value, self);
500                shadow_root.register_slot(self);
501            }
502
503            // Changing the name might cause slot assignments to change
504            self.upcast::<Node>()
505                .GetRootNode(&GetRootNodeOptions::empty())
506                .assign_slottables_for_a_tree(cx, ForceSlottableNodeReconciliation::Skip);
507        }
508    }
509
510    fn bind_to_tree(&self, cx: &mut JSContext, context: &BindContext) {
511        if let Some(s) = self.super_type() {
512            s.bind_to_tree(cx, context);
513        }
514
515        let was_already_in_shadow_tree = context.is_shadow_tree == IsShadowTree::Yes;
516        if !was_already_in_shadow_tree && let Some(shadow_root) = self.containing_shadow_root() {
517            shadow_root.register_slot(self);
518        }
519    }
520
521    fn unbind_from_tree(&self, cx: &mut JSContext, context: &UnbindContext) {
522        if let Some(s) = self.super_type() {
523            s.unbind_from_tree(cx, context);
524        }
525
526        if !self.upcast::<Node>().is_in_a_shadow_tree() &&
527            let Some(old_shadow_root) = self.containing_shadow_root()
528        {
529            // If we used to be in a shadow root, but aren't anymore, then unregister this slot
530            old_shadow_root.unregister_slot(self.Name(), self);
531        }
532    }
533}
534
535impl js::gc::Rootable for Slottable {}
536
537impl js::gc::Initialize for Slottable {
538    #[expect(unsafe_code)]
539    unsafe fn initial() -> Option<Self> {
540        None
541    }
542}