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