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