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, QualName, local_name, ns};
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, CustomElementCreationMode, Element, ElementCreator};
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 = Element::create(
109            QualName::new(None, ns!(html), local_name!("slot")),
110            None,
111            &document,
112            ElementCreator::ScriptCreated,
113            CustomElementCreationMode::Asynchronous,
114            None,
115            can_gc,
116        );
117        let summary = DomRoot::downcast::<HTMLSlotElement>(summary).unwrap();
118        root.upcast::<Node>()
119            .AppendChild(summary.upcast::<Node>(), can_gc)
120            .unwrap();
121
122        let fallback_summary = Element::create(
123            QualName::new(None, ns!(html), local_name!("summary")),
124            None,
125            &document,
126            ElementCreator::ScriptCreated,
127            CustomElementCreationMode::Asynchronous,
128            None,
129            can_gc,
130        );
131        let fallback_summary = DomRoot::downcast::<HTMLElement>(fallback_summary).unwrap();
132        fallback_summary
133            .upcast::<Node>()
134            .set_text_content_for_element(Some(DEFAULT_SUMMARY.into()), can_gc);
135        summary
136            .upcast::<Node>()
137            .AppendChild(fallback_summary.upcast::<Node>(), can_gc)
138            .unwrap();
139
140        let descendants = Element::create(
141            QualName::new(None, ns!(html), local_name!("slot")),
142            None,
143            &document,
144            ElementCreator::ScriptCreated,
145            CustomElementCreationMode::Asynchronous,
146            None,
147            can_gc,
148        );
149        let descendants = DomRoot::downcast::<HTMLSlotElement>(descendants).unwrap();
150        root.upcast::<Node>()
151            .AppendChild(descendants.upcast::<Node>(), can_gc)
152            .unwrap();
153
154        let _ = self.shadow_tree.borrow_mut().insert(ShadowTree {
155            summary: summary.as_traced(),
156            descendants: descendants.as_traced(),
157            implicit_summary: fallback_summary.as_traced(),
158        });
159        self.upcast::<Node>()
160            .dirty(crate::dom::node::NodeDamage::Other);
161    }
162
163    pub(crate) fn find_corresponding_summary_element(&self) -> Option<DomRoot<HTMLElement>> {
164        self.upcast::<Node>()
165            .children()
166            .filter_map(DomRoot::downcast::<HTMLElement>)
167            .find(|html_element| {
168                html_element.upcast::<Element>().local_name() == &local_name!("summary")
169            })
170    }
171
172    fn update_shadow_tree_contents(&self, can_gc: CanGc) {
173        let shadow_tree = self.shadow_tree(can_gc);
174
175        if let Some(summary) = self.find_corresponding_summary_element() {
176            shadow_tree
177                .summary
178                .Assign(vec![ElementOrText::Element(DomRoot::upcast(summary))]);
179        }
180
181        let mut slottable_children = vec![];
182        for child in self.upcast::<Node>().children() {
183            if let Some(element) = child.downcast::<Element>() {
184                if element.local_name() == &local_name!("summary") {
185                    continue;
186                }
187
188                slottable_children.push(ElementOrText::Element(DomRoot::from_ref(element)));
189            }
190
191            if let Some(text) = child.downcast::<Text>() {
192                slottable_children.push(ElementOrText::Text(DomRoot::from_ref(text)));
193            }
194        }
195        shadow_tree.descendants.Assign(slottable_children);
196    }
197
198    fn update_shadow_tree_styles(&self, can_gc: CanGc) {
199        let shadow_tree = self.shadow_tree(can_gc);
200
201        let value = if self.Open() {
202            "display: block;"
203        } else {
204            // TODO: This should be "display: block; content-visibility: hidden;",
205            // but servo does not support content-visibility yet
206            "display: none;"
207        };
208        shadow_tree
209            .descendants
210            .upcast::<Element>()
211            .set_string_attribute(&local_name!("style"), value.into(), can_gc);
212
213        // Manually update the list item style of the implicit summary element.
214        // Unlike the other summaries, this summary is in the shadow tree and
215        // can't be styled with UA sheets
216        let implicit_summary_list_item_style = if self.Open() {
217            "disclosure-open"
218        } else {
219            "disclosure-closed"
220        };
221        let implicit_summary_style = format!(
222            "display: list-item;
223            counter-increment: list-item 0;
224            list-style: {implicit_summary_list_item_style} inside;"
225        );
226        shadow_tree
227            .implicit_summary
228            .upcast::<Element>()
229            .set_string_attribute(&local_name!("style"), implicit_summary_style.into(), can_gc);
230    }
231}
232
233impl HTMLDetailsElementMethods<crate::DomTypeHolder> for HTMLDetailsElement {
234    // https://html.spec.whatwg.org/multipage/#dom-details-open
235    make_bool_getter!(Open, "open");
236
237    // https://html.spec.whatwg.org/multipage/#dom-details-open
238    make_bool_setter!(SetOpen, "open");
239}
240
241impl VirtualMethods for HTMLDetailsElement {
242    fn super_type(&self) -> Option<&dyn VirtualMethods> {
243        Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
244    }
245
246    fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation, can_gc: CanGc) {
247        self.super_type()
248            .unwrap()
249            .attribute_mutated(attr, mutation, can_gc);
250
251        if attr.local_name() == &local_name!("open") {
252            self.update_shadow_tree_styles(can_gc);
253
254            let counter = self.toggle_counter.get() + 1;
255            self.toggle_counter.set(counter);
256
257            let this = Trusted::new(self);
258            self.owner_global()
259                .task_manager()
260                .dom_manipulation_task_source()
261                .queue(task!(details_notification_task_steps: move || {
262                    let this = this.root();
263                    if counter == this.toggle_counter.get() {
264                        this.upcast::<EventTarget>().fire_event(atom!("toggle"), CanGc::note());
265                    }
266                }));
267            self.upcast::<Node>().dirty(NodeDamage::Other);
268        }
269    }
270
271    fn children_changed(&self, mutation: &ChildrenMutation) {
272        self.super_type().unwrap().children_changed(mutation);
273
274        self.update_shadow_tree_contents(CanGc::note());
275    }
276
277    fn bind_to_tree(&self, context: &BindContext, can_gc: CanGc) {
278        self.super_type().unwrap().bind_to_tree(context, can_gc);
279
280        self.update_shadow_tree_contents(CanGc::note());
281        self.update_shadow_tree_styles(CanGc::note());
282    }
283}