script/dom/html/
htmldetailselement.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};
6use std::collections::HashMap;
7use std::collections::hash_map::Entry;
8
9use dom_struct::dom_struct;
10use html5ever::{LocalName, Prefix, QualName, local_name, ns};
11use js::rust::HandleObject;
12use script_bindings::domstring::DOMString;
13use style::selector_parser::PseudoElement;
14
15use crate::dom::attr::Attr;
16use crate::dom::bindings::cell::DomRefCell;
17use crate::dom::bindings::codegen::Bindings::HTMLDetailsElementBinding::HTMLDetailsElementMethods;
18use crate::dom::bindings::codegen::Bindings::HTMLSlotElementBinding::HTMLSlotElement_Binding::HTMLSlotElementMethods;
19use crate::dom::bindings::codegen::Bindings::NodeBinding::GetRootNodeOptions;
20use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods;
21use crate::dom::bindings::codegen::UnionTypes::ElementOrText;
22use crate::dom::bindings::inheritance::Castable;
23use crate::dom::bindings::refcounted::Trusted;
24use crate::dom::bindings::reflector::DomGlobal;
25use crate::dom::bindings::root::{Dom, DomRoot};
26use crate::dom::document::Document;
27use crate::dom::element::{AttributeMutation, CustomElementCreationMode, Element, ElementCreator};
28use crate::dom::event::{Event, EventBubbles, EventCancelable};
29use crate::dom::eventtarget::EventTarget;
30use crate::dom::html::htmlelement::HTMLElement;
31use crate::dom::html::htmlslotelement::HTMLSlotElement;
32use crate::dom::node::{
33    BindContext, ChildrenMutation, IsShadowTree, Node, NodeDamage, NodeTraits, ShadowIncluding,
34    UnbindContext,
35};
36use crate::dom::text::Text;
37use crate::dom::toggleevent::ToggleEvent;
38use crate::dom::virtualmethods::VirtualMethods;
39use crate::script_runtime::CanGc;
40
41/// The summary that should be presented if no `<summary>` element is present
42const DEFAULT_SUMMARY: &str = "Details";
43
44/// Holds handles to all slots in the UA shadow tree
45///
46/// The composition of the tree is described in
47/// <https://html.spec.whatwg.org/multipage/#the-details-and-summary-elements>
48#[derive(Clone, JSTraceable, MallocSizeOf)]
49#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
50struct ShadowTree {
51    summary: Dom<HTMLSlotElement>,
52    details_content: Dom<HTMLSlotElement>,
53    /// The summary that is displayed if no other summary exists
54    implicit_summary: Dom<HTMLElement>,
55}
56
57#[dom_struct]
58pub(crate) struct HTMLDetailsElement {
59    htmlelement: HTMLElement,
60    toggle_counter: Cell<u32>,
61
62    /// Represents the UA widget for the details element
63    shadow_tree: DomRefCell<Option<ShadowTree>>,
64}
65
66/// Tracks all [details name groups](https://html.spec.whatwg.org/multipage/#details-name-group)
67/// within a tree.
68#[derive(Clone, Default, JSTraceable, MallocSizeOf)]
69#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
70pub(crate) struct DetailsNameGroups {
71    /// Map from `name` attribute to a list of details elements.
72    pub(crate) groups: HashMap<DOMString, Vec<Dom<HTMLDetailsElement>>>,
73}
74
75/// Describes how to proceed in case two details elements in the same
76/// [details name groups](https://html.spec.whatwg.org/multipage/#details-name-group) are
77/// open at the same time
78#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79enum ExclusivityConflictResolution {
80    CloseThisElement,
81    CloseExistingOpenElement,
82}
83
84impl DetailsNameGroups {
85    fn register_details_element(&mut self, details_element: &HTMLDetailsElement) {
86        let name = details_element.Name();
87        if name.is_empty() {
88            return;
89        }
90
91        debug!("Registering details element with name={name:?}");
92        let details_elements_with_the_same_name = self.groups.entry(name).or_default();
93
94        // The spec tells us to keep the list in tree order, but that's not actually necessary.
95        details_elements_with_the_same_name.push(Dom::from_ref(details_element));
96    }
97
98    fn unregister_details_element(
99        &mut self,
100        name: DOMString,
101        details_element: &HTMLDetailsElement,
102    ) {
103        if name.is_empty() {
104            return;
105        }
106
107        debug!("Unregistering details element with name={name:?}");
108        let Entry::Occupied(mut entry) = self.groups.entry(name) else {
109            panic!("details element is not registered");
110        };
111        entry
112            .get_mut()
113            .retain(|group_member| details_element != &**group_member);
114    }
115
116    /// Returns an iterator over all members with the given name, except for `details`.
117    fn group_members_for(
118        &self,
119        name: &DOMString,
120        details: &HTMLDetailsElement,
121    ) -> impl Iterator<Item = DomRoot<HTMLDetailsElement>> {
122        self.groups
123            .get(name)
124            .map(|members| members.iter())
125            .expect("No details element with the given name was registered for the tree")
126            .filter(move |member| **member != details)
127            .map(|member| member.as_rooted())
128    }
129}
130
131impl HTMLDetailsElement {
132    fn new_inherited(
133        local_name: LocalName,
134        prefix: Option<Prefix>,
135        document: &Document,
136    ) -> HTMLDetailsElement {
137        HTMLDetailsElement {
138            htmlelement: HTMLElement::new_inherited(local_name, prefix, document),
139            toggle_counter: Cell::new(0),
140            shadow_tree: Default::default(),
141        }
142    }
143
144    pub(crate) fn new(
145        local_name: LocalName,
146        prefix: Option<Prefix>,
147        document: &Document,
148        proto: Option<HandleObject>,
149        can_gc: CanGc,
150    ) -> DomRoot<HTMLDetailsElement> {
151        Node::reflect_node_with_proto(
152            Box::new(HTMLDetailsElement::new_inherited(
153                local_name, prefix, document,
154            )),
155            document,
156            proto,
157            can_gc,
158        )
159    }
160
161    pub(crate) fn toggle(&self) {
162        self.SetOpen(!self.Open());
163    }
164
165    fn shadow_tree(&self, can_gc: CanGc) -> Ref<'_, ShadowTree> {
166        if !self.upcast::<Element>().is_shadow_host() {
167            self.create_shadow_tree(can_gc);
168        }
169
170        Ref::filter_map(self.shadow_tree.borrow(), Option::as_ref)
171            .ok()
172            .expect("UA shadow tree was not created")
173    }
174
175    fn create_shadow_tree(&self, can_gc: CanGc) {
176        let document = self.owner_document();
177        // TODO(stevennovaryo): Reimplement details styling so that it would not
178        //                      mess the cascading and require some reparsing.
179        let root = self.upcast::<Element>().attach_ua_shadow_root(true, can_gc);
180
181        let summary = Element::create(
182            QualName::new(None, ns!(html), local_name!("slot")),
183            None,
184            &document,
185            ElementCreator::ScriptCreated,
186            CustomElementCreationMode::Asynchronous,
187            None,
188            can_gc,
189        );
190        let summary = DomRoot::downcast::<HTMLSlotElement>(summary).unwrap();
191        root.upcast::<Node>()
192            .AppendChild(summary.upcast::<Node>(), can_gc)
193            .unwrap();
194
195        let fallback_summary = Element::create(
196            QualName::new(None, ns!(html), local_name!("summary")),
197            None,
198            &document,
199            ElementCreator::ScriptCreated,
200            CustomElementCreationMode::Asynchronous,
201            None,
202            can_gc,
203        );
204        let fallback_summary = DomRoot::downcast::<HTMLElement>(fallback_summary).unwrap();
205        fallback_summary
206            .upcast::<Node>()
207            .set_text_content_for_element(Some(DEFAULT_SUMMARY.into()), can_gc);
208        summary
209            .upcast::<Node>()
210            .AppendChild(fallback_summary.upcast::<Node>(), can_gc)
211            .unwrap();
212
213        let details_content = Element::create(
214            QualName::new(None, ns!(html), local_name!("slot")),
215            None,
216            &document,
217            ElementCreator::ScriptCreated,
218            CustomElementCreationMode::Asynchronous,
219            None,
220            can_gc,
221        );
222        let details_content = DomRoot::downcast::<HTMLSlotElement>(details_content).unwrap();
223
224        root.upcast::<Node>()
225            .AppendChild(details_content.upcast::<Node>(), can_gc)
226            .unwrap();
227        details_content
228            .upcast::<Node>()
229            .set_implemented_pseudo_element(PseudoElement::DetailsContent);
230
231        let _ = self.shadow_tree.borrow_mut().insert(ShadowTree {
232            summary: summary.as_traced(),
233            details_content: details_content.as_traced(),
234            implicit_summary: fallback_summary.as_traced(),
235        });
236        self.upcast::<Node>()
237            .dirty(crate::dom::node::NodeDamage::Other);
238    }
239
240    pub(crate) fn find_corresponding_summary_element(&self) -> Option<DomRoot<HTMLElement>> {
241        self.upcast::<Node>()
242            .children()
243            .filter_map(DomRoot::downcast::<HTMLElement>)
244            .find(|html_element| {
245                html_element.upcast::<Element>().local_name() == &local_name!("summary")
246            })
247    }
248
249    fn update_shadow_tree_contents(&self, can_gc: CanGc) {
250        let shadow_tree = self.shadow_tree(can_gc);
251
252        if let Some(summary) = self.find_corresponding_summary_element() {
253            shadow_tree
254                .summary
255                .Assign(vec![ElementOrText::Element(DomRoot::upcast(summary))]);
256        }
257
258        let mut slottable_children = vec![];
259        for child in self.upcast::<Node>().children() {
260            if let Some(element) = child.downcast::<Element>() {
261                if element.local_name() == &local_name!("summary") {
262                    continue;
263                }
264
265                slottable_children.push(ElementOrText::Element(DomRoot::from_ref(element)));
266            }
267
268            if let Some(text) = child.downcast::<Text>() {
269                slottable_children.push(ElementOrText::Text(DomRoot::from_ref(text)));
270            }
271        }
272        shadow_tree.details_content.Assign(slottable_children);
273    }
274
275    fn update_shadow_tree_styles(&self, can_gc: CanGc) {
276        let shadow_tree = self.shadow_tree(can_gc);
277
278        // Manually update the list item style of the implicit summary element.
279        // Unlike the other summaries, this summary is in the shadow tree and
280        // can't be styled with UA sheets
281        let implicit_summary_list_item_style = if self.Open() {
282            "disclosure-open"
283        } else {
284            "disclosure-closed"
285        };
286        let implicit_summary_style = format!(
287            "display: list-item;
288            counter-increment: list-item 0;
289            list-style: {implicit_summary_list_item_style} inside;"
290        );
291        shadow_tree
292            .implicit_summary
293            .upcast::<Element>()
294            .set_string_attribute(&local_name!("style"), implicit_summary_style.into(), can_gc);
295    }
296
297    /// <https://html.spec.whatwg.org/multipage/#ensure-details-exclusivity-by-closing-the-given-element-if-needed>
298    /// <https://html.spec.whatwg.org/multipage/#ensure-details-exclusivity-by-closing-other-elements-if-needed>
299    fn ensure_details_exclusivity(
300        &self,
301        conflict_resolution_behaviour: ExclusivityConflictResolution,
302    ) {
303        // NOTE: This method implements two spec algorithms that are very similar to each other, distinguished by the
304        // `conflict_resolution_behaviour` argument. Steps that are different between the two are annotated with two
305        // spec comments.
306
307        // Step 1. Assert: element has an open attribute.
308        // Step 1. If element does not have an open attribute, then return.
309        if !self.Open() {
310            if conflict_resolution_behaviour ==
311                ExclusivityConflictResolution::CloseExistingOpenElement
312            {
313                unreachable!()
314            } else {
315                return;
316            }
317        }
318
319        // Step 2. If element does not have a name attribute, or its name attribute
320        // is the empty string, then return.
321        let name = self.Name();
322        if name.is_empty() {
323            return;
324        }
325
326        // Step 3. Let groupMembers be a list of elements, containing all elements in element's
327        // details name group except for element, in tree order.
328        // Step 4. For each element otherElement of groupMembers:
329        //     Step 4.1 If the open attribute is set on otherElement, then:
330
331        // NOTE: We implement an optimization that allows us to easily find details group members when the
332        // root of the tree is a document or shadow root, which is why this looks a bit more complicated.
333        let other_open_member = if let Some(shadow_root) = self.containing_shadow_root() {
334            shadow_root
335                .details_name_groups()
336                .group_members_for(&name, self)
337                .find(|group_member| group_member.Open())
338        } else if self.upcast::<Node>().is_in_a_document_tree() {
339            self.owner_document()
340                .details_name_groups()
341                .group_members_for(&name, self)
342                .find(|group_member| group_member.Open())
343        } else {
344            // This is the slow case, which is hopefully not too common.
345            self.upcast::<Node>()
346                .GetRootNode(&GetRootNodeOptions::empty())
347                .traverse_preorder(ShadowIncluding::No)
348                .flat_map(DomRoot::downcast::<HTMLDetailsElement>)
349                .filter(|details_element| {
350                    details_element
351                        .upcast::<Element>()
352                        .get_string_attribute(&local_name!("name")) ==
353                        name
354                })
355                .filter(|group_member| &**group_member != self)
356                .find(|group_member| group_member.Open())
357        };
358
359        if let Some(other_open_member) = other_open_member {
360            // Step 4.1.1 Assert: otherElement is the only element in groupMembers that has the open attribute set.
361            // Step 4.1.2 Remove the open attribute on otherElement.
362            // Step 4.1.3 Break.
363            //
364            // Step 4.1.1 Remove the open attribute on element.
365            // Step 4.1.2 Break.
366            // NOTE: We don't bother to assert here and don't need to "break" since we're not in a loop.
367            match conflict_resolution_behaviour {
368                ExclusivityConflictResolution::CloseThisElement => self.SetOpen(false),
369                ExclusivityConflictResolution::CloseExistingOpenElement => {
370                    other_open_member.SetOpen(false)
371                },
372            }
373        }
374    }
375}
376
377impl HTMLDetailsElementMethods<crate::DomTypeHolder> for HTMLDetailsElement {
378    // https://html.spec.whatwg.org/multipage/#dom-details-name
379    make_getter!(Name, "name");
380
381    // https://html.spec.whatwg.org/multipage/#dom-details-name
382    make_atomic_setter!(SetName, "name");
383
384    // https://html.spec.whatwg.org/multipage/#dom-details-open
385    make_bool_getter!(Open, "open");
386
387    // https://html.spec.whatwg.org/multipage/#dom-details-open
388    make_bool_setter!(SetOpen, "open");
389}
390
391impl VirtualMethods for HTMLDetailsElement {
392    fn super_type(&self) -> Option<&dyn VirtualMethods> {
393        Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
394    }
395
396    /// <https://html.spec.whatwg.org/multipage/#the-details-element:concept-element-attributes-change-ext>
397    fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation, can_gc: CanGc) {
398        self.super_type()
399            .unwrap()
400            .attribute_mutated(attr, mutation, can_gc);
401
402        // Step 1. If namespace is not null, then return.
403        if *attr.namespace() != ns!() {
404            return;
405        }
406
407        // Step 2. If localName is name, then ensure details exclusivity by closing the given element if needed
408        // given element.
409        if attr.local_name() == &local_name!("name") {
410            let old_name: Option<DOMString> = match mutation {
411                AttributeMutation::Set(old, _) => old.map(|value| value.to_string().into()),
412                AttributeMutation::Removed => Some(attr.value().to_string().into()),
413            };
414
415            if let Some(shadow_root) = self.containing_shadow_root() {
416                if let Some(old_name) = old_name {
417                    shadow_root
418                        .details_name_groups()
419                        .unregister_details_element(old_name, self);
420                }
421                if matches!(mutation, AttributeMutation::Set(..)) {
422                    shadow_root
423                        .details_name_groups()
424                        .register_details_element(self);
425                }
426            } else if self.upcast::<Node>().is_in_a_document_tree() {
427                let document = self.owner_document();
428                if let Some(old_name) = old_name {
429                    document
430                        .details_name_groups()
431                        .unregister_details_element(old_name, self);
432                }
433                if matches!(mutation, AttributeMutation::Set(..)) {
434                    document
435                        .details_name_groups()
436                        .register_details_element(self);
437                }
438            }
439
440            self.ensure_details_exclusivity(ExclusivityConflictResolution::CloseThisElement);
441        }
442        // Step 3. If localName is open, then:
443        else if attr.local_name() == &local_name!("open") {
444            self.update_shadow_tree_styles(can_gc);
445
446            let counter = self.toggle_counter.get().wrapping_add(1);
447            self.toggle_counter.set(counter);
448            let (old_state, new_state) = if self.Open() {
449                ("closed", "open")
450            } else {
451                ("open", "closed")
452            };
453
454            let this = Trusted::new(self);
455            self.owner_global()
456                .task_manager()
457                .dom_manipulation_task_source()
458                .queue(task!(details_notification_task_steps: move || {
459                    let this = this.root();
460                    if counter == this.toggle_counter.get() {
461                        let event = ToggleEvent::new(
462                            this.global().as_window(),
463                            atom!("toggle"),
464                            EventBubbles::DoesNotBubble,
465                            EventCancelable::NotCancelable,
466                            DOMString::from(old_state),
467                            DOMString::from(new_state),
468                            None,
469                            CanGc::note(),
470                        );
471                        let event = event.upcast::<Event>();
472                        event.fire(this.upcast::<EventTarget>(), CanGc::note());
473                    }
474                }));
475            self.upcast::<Node>().dirty(NodeDamage::Other);
476
477            // Step 3.2. If oldValue is null and value is not null, then ensure details exclusivity
478            // by closing other elements if needed given element.
479            let was_previously_closed = match mutation {
480                AttributeMutation::Set(old, _) => old.is_none(),
481                AttributeMutation::Removed => false,
482            };
483            if was_previously_closed && self.Open() {
484                self.ensure_details_exclusivity(
485                    ExclusivityConflictResolution::CloseExistingOpenElement,
486                );
487            }
488
489            self.upcast::<Element>().set_open_state(self.Open());
490        }
491    }
492
493    fn children_changed(&self, mutation: &ChildrenMutation, can_gc: CanGc) {
494        self.super_type()
495            .unwrap()
496            .children_changed(mutation, can_gc);
497
498        self.update_shadow_tree_contents(can_gc);
499    }
500
501    /// <https://html.spec.whatwg.org/multipage/#the-details-element:html-element-insertion-steps>
502    fn bind_to_tree(&self, context: &BindContext, can_gc: CanGc) {
503        self.super_type().unwrap().bind_to_tree(context, can_gc);
504
505        self.update_shadow_tree_contents(can_gc);
506        self.update_shadow_tree_styles(can_gc);
507
508        if context.tree_is_in_a_document_tree {
509            // If this is true then we can't have been in a document tree previously, so
510            // we register ourselves.
511            self.owner_document()
512                .details_name_groups()
513                .register_details_element(self);
514        }
515
516        let was_already_in_shadow_tree = context.is_shadow_tree == IsShadowTree::Yes;
517        if !was_already_in_shadow_tree {
518            if let Some(shadow_root) = self.containing_shadow_root() {
519                shadow_root
520                    .details_name_groups()
521                    .register_details_element(self);
522            }
523        }
524
525        // Step 1. Ensure details exclusivity by closing the given element if needed given insertedNode.
526        self.ensure_details_exclusivity(ExclusivityConflictResolution::CloseThisElement);
527    }
528
529    fn unbind_from_tree(&self, context: &UnbindContext, can_gc: CanGc) {
530        self.super_type().unwrap().unbind_from_tree(context, can_gc);
531
532        if context.tree_is_in_a_document_tree && !self.upcast::<Node>().is_in_a_document_tree() {
533            self.owner_document()
534                .details_name_groups()
535                .unregister_details_element(self.Name(), self);
536        }
537
538        if !self.upcast::<Node>().is_in_a_shadow_tree() {
539            if let Some(old_shadow_root) = self.containing_shadow_root() {
540                // If we used to be in a shadow root, but aren't anymore, then unregister this details
541                // element.
542                old_shadow_root
543                    .details_name_groups()
544                    .unregister_details_element(self.Name(), self);
545            }
546        }
547    }
548}