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