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