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};
6
7use dom_struct::dom_struct;
8use html5ever::{LocalName, Prefix, local_name};
9use js::rust::HandleObject;
10
11use crate::dom::attr::Attr;
12use crate::dom::bindings::cell::DomRefCell;
13use crate::dom::bindings::codegen::Bindings::HTMLDetailsElementBinding::HTMLDetailsElementMethods;
14use crate::dom::bindings::codegen::Bindings::HTMLSlotElementBinding::HTMLSlotElement_Binding::HTMLSlotElementMethods;
15use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods;
16use crate::dom::bindings::codegen::UnionTypes::ElementOrText;
17use crate::dom::bindings::inheritance::Castable;
18use crate::dom::bindings::refcounted::Trusted;
19use crate::dom::bindings::root::{Dom, DomRoot};
20use crate::dom::document::Document;
21use crate::dom::element::{AttributeMutation, Element};
22use crate::dom::eventtarget::EventTarget;
23use crate::dom::html::htmlelement::HTMLElement;
24use crate::dom::html::htmlslotelement::HTMLSlotElement;
25use crate::dom::node::{BindContext, ChildrenMutation, Node, NodeDamage, NodeTraits};
26use crate::dom::text::Text;
27use crate::dom::virtualmethods::VirtualMethods;
28use crate::script_runtime::CanGc;
29
30/// The summary that should be presented if no `<summary>` element is present
31const DEFAULT_SUMMARY: &str = "Details";
32
33/// Holds handles to all slots in the UA shadow tree
34///
35/// The composition of the tree is described in
36/// <https://html.spec.whatwg.org/multipage/#the-details-and-summary-elements>
37#[derive(Clone, JSTraceable, MallocSizeOf)]
38#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
39struct ShadowTree {
40    summary: Dom<HTMLSlotElement>,
41    descendants: Dom<HTMLSlotElement>,
42    /// The summary that is displayed if no other summary exists
43    implicit_summary: Dom<HTMLElement>,
44}
45
46#[dom_struct]
47pub(crate) struct HTMLDetailsElement {
48    htmlelement: HTMLElement,
49    toggle_counter: Cell<u32>,
50
51    /// Represents the UA widget for the details element
52    shadow_tree: DomRefCell<Option<ShadowTree>>,
53}
54
55impl HTMLDetailsElement {
56    fn new_inherited(
57        local_name: LocalName,
58        prefix: Option<Prefix>,
59        document: &Document,
60    ) -> HTMLDetailsElement {
61        HTMLDetailsElement {
62            htmlelement: HTMLElement::new_inherited(local_name, prefix, document),
63            toggle_counter: Cell::new(0),
64            shadow_tree: Default::default(),
65        }
66    }
67
68    #[cfg_attr(crown, allow(crown::unrooted_must_root))]
69    pub(crate) fn new(
70        local_name: LocalName,
71        prefix: Option<Prefix>,
72        document: &Document,
73        proto: Option<HandleObject>,
74        can_gc: CanGc,
75    ) -> DomRoot<HTMLDetailsElement> {
76        Node::reflect_node_with_proto(
77            Box::new(HTMLDetailsElement::new_inherited(
78                local_name, prefix, document,
79            )),
80            document,
81            proto,
82            can_gc,
83        )
84    }
85
86    pub(crate) fn toggle(&self) {
87        self.SetOpen(!self.Open());
88    }
89
90    fn shadow_tree(&self, can_gc: CanGc) -> Ref<'_, ShadowTree> {
91        if !self.upcast::<Element>().is_shadow_host() {
92            self.create_shadow_tree(can_gc);
93        }
94
95        Ref::filter_map(self.shadow_tree.borrow(), Option::as_ref)
96            .ok()
97            .expect("UA shadow tree was not created")
98    }
99
100    fn create_shadow_tree(&self, can_gc: CanGc) {
101        let document = self.owner_document();
102        // TODO(stevennovaryo): Reimplement details styling so that it would not
103        //                      mess the cascading and require some reparsing.
104        let root = self
105            .upcast::<Element>()
106            .attach_ua_shadow_root(false, can_gc);
107
108        let summary = HTMLSlotElement::new(local_name!("slot"), None, &document, None, can_gc);
109        root.upcast::<Node>()
110            .AppendChild(summary.upcast::<Node>(), can_gc)
111            .unwrap();
112
113        let fallback_summary =
114            HTMLElement::new(local_name!("summary"), None, &document, None, can_gc);
115        fallback_summary
116            .upcast::<Node>()
117            .set_text_content_for_element(Some(DEFAULT_SUMMARY.into()), can_gc);
118        summary
119            .upcast::<Node>()
120            .AppendChild(fallback_summary.upcast::<Node>(), can_gc)
121            .unwrap();
122
123        let descendants = HTMLSlotElement::new(local_name!("slot"), None, &document, None, can_gc);
124        root.upcast::<Node>()
125            .AppendChild(descendants.upcast::<Node>(), can_gc)
126            .unwrap();
127
128        let _ = self.shadow_tree.borrow_mut().insert(ShadowTree {
129            summary: summary.as_traced(),
130            descendants: descendants.as_traced(),
131            implicit_summary: fallback_summary.as_traced(),
132        });
133        self.upcast::<Node>()
134            .dirty(crate::dom::node::NodeDamage::Other);
135    }
136
137    pub(crate) fn find_corresponding_summary_element(&self) -> Option<DomRoot<HTMLElement>> {
138        self.upcast::<Node>()
139            .children()
140            .filter_map(DomRoot::downcast::<HTMLElement>)
141            .find(|html_element| {
142                html_element.upcast::<Element>().local_name() == &local_name!("summary")
143            })
144    }
145
146    fn update_shadow_tree_contents(&self, can_gc: CanGc) {
147        let shadow_tree = self.shadow_tree(can_gc);
148
149        if let Some(summary) = self.find_corresponding_summary_element() {
150            shadow_tree
151                .summary
152                .Assign(vec![ElementOrText::Element(DomRoot::upcast(summary))]);
153        }
154
155        let mut slottable_children = vec![];
156        for child in self.upcast::<Node>().children() {
157            if let Some(element) = child.downcast::<Element>() {
158                if element.local_name() == &local_name!("summary") {
159                    continue;
160                }
161
162                slottable_children.push(ElementOrText::Element(DomRoot::from_ref(element)));
163            }
164
165            if let Some(text) = child.downcast::<Text>() {
166                slottable_children.push(ElementOrText::Text(DomRoot::from_ref(text)));
167            }
168        }
169        shadow_tree.descendants.Assign(slottable_children);
170    }
171
172    fn update_shadow_tree_styles(&self, can_gc: CanGc) {
173        let shadow_tree = self.shadow_tree(can_gc);
174
175        let value = if self.Open() {
176            "display: block;"
177        } else {
178            // TODO: This should be "display: block; content-visibility: hidden;",
179            // but servo does not support content-visibility yet
180            "display: none;"
181        };
182        shadow_tree
183            .descendants
184            .upcast::<Element>()
185            .set_string_attribute(&local_name!("style"), value.into(), can_gc);
186
187        // Manually update the list item style of the implicit summary element.
188        // Unlike the other summaries, this summary is in the shadow tree and
189        // can't be styled with UA sheets
190        let implicit_summary_list_item_style = if self.Open() {
191            "disclosure-open"
192        } else {
193            "disclosure-closed"
194        };
195        let implicit_summary_style = format!(
196            "display: list-item;
197            counter-increment: list-item 0;
198            list-style: {implicit_summary_list_item_style} inside;"
199        );
200        shadow_tree
201            .implicit_summary
202            .upcast::<Element>()
203            .set_string_attribute(&local_name!("style"), implicit_summary_style.into(), can_gc);
204    }
205}
206
207impl HTMLDetailsElementMethods<crate::DomTypeHolder> for HTMLDetailsElement {
208    // https://html.spec.whatwg.org/multipage/#dom-details-open
209    make_bool_getter!(Open, "open");
210
211    // https://html.spec.whatwg.org/multipage/#dom-details-open
212    make_bool_setter!(SetOpen, "open");
213}
214
215impl VirtualMethods for HTMLDetailsElement {
216    fn super_type(&self) -> Option<&dyn VirtualMethods> {
217        Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
218    }
219
220    fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation, can_gc: CanGc) {
221        self.super_type()
222            .unwrap()
223            .attribute_mutated(attr, mutation, can_gc);
224
225        if attr.local_name() == &local_name!("open") {
226            self.update_shadow_tree_styles(can_gc);
227
228            let counter = self.toggle_counter.get() + 1;
229            self.toggle_counter.set(counter);
230
231            let this = Trusted::new(self);
232            self.owner_global()
233                .task_manager()
234                .dom_manipulation_task_source()
235                .queue(task!(details_notification_task_steps: move || {
236                    let this = this.root();
237                    if counter == this.toggle_counter.get() {
238                        this.upcast::<EventTarget>().fire_event(atom!("toggle"), CanGc::note());
239                    }
240                }));
241            self.upcast::<Node>().dirty(NodeDamage::Other);
242        }
243    }
244
245    fn children_changed(&self, mutation: &ChildrenMutation) {
246        self.super_type().unwrap().children_changed(mutation);
247
248        self.update_shadow_tree_contents(CanGc::note());
249    }
250
251    fn bind_to_tree(&self, context: &BindContext, can_gc: CanGc) {
252        self.super_type().unwrap().bind_to_tree(context, can_gc);
253
254        self.update_shadow_tree_contents(CanGc::note());
255        self.update_shadow_tree_styles(CanGc::note());
256    }
257}