script/dom/html/interactive/
htmldetailselement.rs1use std::cell::{Cell, Ref};
6use std::collections::HashMap;
7use std::collections::hash_map::Entry;
8
9use dom_struct::dom_struct;
10use html5ever::{LocalName, Prefix, local_name, ns};
11use js::context::JSContext;
12use js::rust::HandleObject;
13use script_bindings::cell::DomRefCell;
14use script_bindings::domstring::DOMString;
15use style::selector_parser::PseudoElement;
16
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::attributes::storage::AttrRef;
28use crate::dom::element::{AttributeMutation, Element};
29use crate::dom::event::{Event, EventBubbles, EventCancelable};
30use crate::dom::eventtarget::EventTarget;
31use crate::dom::html::htmlelement::HTMLElement;
32use crate::dom::html::htmlslotelement::HTMLSlotElement;
33use crate::dom::iterators::ShadowIncluding;
34use crate::dom::node::virtualmethods::VirtualMethods;
35use crate::dom::node::{
36 BindContext, ChildrenMutation, IsShadowTree, Node, NodeDamage, NodeTraits, UnbindContext,
37};
38use crate::dom::shadowroot::ShadowRoot;
39use crate::dom::shadowroot::ua_shadowroot::{CreateUAShadowRootForSelf, UAShadowRoot};
40use crate::dom::text::Text;
41use crate::dom::toggleevent::ToggleEvent;
42
43const DEFAULT_SUMMARY: &str = "Details";
45
46#[derive(Clone, JSTraceable, MallocSizeOf)]
51#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
52struct ShadowTree {
53 summary: Dom<HTMLSlotElement>,
54 details_content: Dom<HTMLSlotElement>,
55 implicit_summary: Dom<HTMLElement>,
57}
58
59impl UAShadowRoot<ShadowTree> for HTMLDetailsElement {
60 fn store_for_shadow_tree(&self, cx: &mut JSContext, shadow_root: DomRoot<ShadowRoot>) {
61 let root = shadow_root.upcast::<Node>();
62
63 let summary = self.create_element_in_ua_shadowroot(cx, local_name!("slot"));
64 let summary = DomRoot::downcast::<HTMLSlotElement>(summary).unwrap();
65 root.AppendChild(cx, summary.upcast::<Node>()).unwrap();
66
67 let fallback_summary = self.create_element_in_ua_shadowroot(cx, local_name!("summary"));
68 let fallback_summary = DomRoot::downcast::<HTMLElement>(fallback_summary).unwrap();
69 fallback_summary
70 .upcast::<Node>()
71 .set_text_content_for_element(cx, Some(DEFAULT_SUMMARY.into()));
72 summary
73 .upcast::<Node>()
74 .AppendChild(cx, fallback_summary.upcast::<Node>())
75 .unwrap();
76
77 let details_content = self.create_element_in_ua_shadowroot(cx, local_name!("slot"));
78 let details_content = DomRoot::downcast::<HTMLSlotElement>(details_content).unwrap();
79 root.AppendChild(cx, details_content.upcast::<Node>())
80 .unwrap();
81 details_content
82 .upcast::<Node>()
83 .set_implemented_pseudo_element(PseudoElement::DetailsContent);
84
85 let _ = self.shadow_tree.borrow_mut().insert(ShadowTree {
86 summary: summary.as_traced(),
87 details_content: details_content.as_traced(),
88 implicit_summary: fallback_summary.as_traced(),
89 });
90 }
91
92 fn borrow_for_shadow_tree(&self) -> Ref<'_, Option<ShadowTree>> {
93 self.shadow_tree.borrow()
94 }
95}
96
97#[dom_struct]
98pub(crate) struct HTMLDetailsElement {
99 htmlelement: HTMLElement,
100 toggle_counter: Cell<u32>,
101
102 shadow_tree: DomRefCell<Option<ShadowTree>>,
104}
105
106#[derive(Clone, Default, JSTraceable, MallocSizeOf)]
109#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
110pub(crate) struct DetailsNameGroups {
111 pub(crate) groups: HashMap<DOMString, Vec<Dom<HTMLDetailsElement>>>,
113}
114
115#[derive(Clone, Copy, Debug, Eq, PartialEq)]
119enum ExclusivityConflictResolution {
120 CloseThisElement,
121 CloseExistingOpenElement,
122}
123
124impl DetailsNameGroups {
125 fn register_details_element(&mut self, details_element: &HTMLDetailsElement) {
126 let name = details_element.Name();
127 if name.is_empty() {
128 return;
129 }
130
131 debug!("Registering details element with name={name:?}");
132 let details_elements_with_the_same_name = self.groups.entry(name).or_default();
133
134 details_elements_with_the_same_name.push(Dom::from_ref(details_element));
136 }
137
138 fn unregister_details_element(
139 &mut self,
140 name: DOMString,
141 details_element: &HTMLDetailsElement,
142 ) {
143 if name.is_empty() {
144 return;
145 }
146
147 debug!("Unregistering details element with name={name:?}");
148 let Entry::Occupied(mut entry) = self.groups.entry(name) else {
149 panic!("details element is not registered");
150 };
151 entry
152 .get_mut()
153 .retain(|group_member| details_element != &**group_member);
154 }
155
156 fn group_members_for(
158 &self,
159 name: &DOMString,
160 details: &HTMLDetailsElement,
161 ) -> impl Iterator<Item = DomRoot<HTMLDetailsElement>> {
162 self.groups
163 .get(name)
164 .map(|members| members.iter())
165 .expect("No details element with the given name was registered for the tree")
166 .filter(move |member| **member != details)
167 .map(|member| member.as_rooted())
168 }
169}
170
171impl HTMLDetailsElement {
172 fn new_inherited(
173 local_name: LocalName,
174 prefix: Option<Prefix>,
175 document: &Document,
176 ) -> HTMLDetailsElement {
177 HTMLDetailsElement {
178 htmlelement: HTMLElement::new_inherited(local_name, prefix, document),
179 toggle_counter: Cell::new(0),
180 shadow_tree: Default::default(),
181 }
182 }
183
184 pub(crate) fn new(
185 cx: &mut js::context::JSContext,
186 local_name: LocalName,
187 prefix: Option<Prefix>,
188 document: &Document,
189 proto: Option<HandleObject>,
190 ) -> DomRoot<HTMLDetailsElement> {
191 Node::reflect_node_with_proto(
192 cx,
193 Box::new(HTMLDetailsElement::new_inherited(
194 local_name, prefix, document,
195 )),
196 document,
197 proto,
198 )
199 }
200
201 pub(crate) fn toggle(&self, cx: &mut JSContext) {
202 self.SetOpen(cx, !self.Open());
203 }
204
205 pub(crate) fn find_corresponding_summary_element(&self) -> Option<DomRoot<HTMLElement>> {
206 self.upcast::<Node>()
207 .children()
208 .filter_map(DomRoot::downcast::<HTMLElement>)
209 .find(|html_element| {
210 html_element.upcast::<Element>().local_name() == &local_name!("summary")
211 })
212 }
213
214 fn update_shadow_tree_contents(&self, cx: &mut JSContext) {
215 let shadow_tree = self.shadow_tree(cx);
216
217 if let Some(summary) = self.find_corresponding_summary_element() {
218 shadow_tree
219 .summary
220 .Assign(cx, vec![ElementOrText::Element(DomRoot::upcast(summary))]);
221 }
222
223 let mut slottable_children = vec![];
224 for child in self.upcast::<Node>().children() {
225 if let Some(element) = child.downcast::<Element>() {
226 if element.local_name() == &local_name!("summary") {
227 continue;
228 }
229
230 slottable_children.push(ElementOrText::Element(DomRoot::from_ref(element)));
231 }
232
233 if let Some(text) = child.downcast::<Text>() {
234 slottable_children.push(ElementOrText::Text(DomRoot::from_ref(text)));
235 }
236 }
237 shadow_tree.details_content.Assign(cx, slottable_children);
238 }
239
240 fn update_shadow_tree_styles(&self, cx: &mut JSContext) {
241 let shadow_tree = self.shadow_tree(cx);
242
243 let implicit_summary_list_item_style = if self.Open() {
247 "disclosure-open"
248 } else {
249 "disclosure-closed"
250 };
251 let implicit_summary_style = format!(
252 "display: list-item;
253 counter-increment: list-item 0;
254 list-style: {implicit_summary_list_item_style} inside;"
255 );
256 shadow_tree
257 .implicit_summary
258 .upcast::<Element>()
259 .set_string_attribute(cx, &local_name!("style"), implicit_summary_style.into());
260 }
261
262 fn ensure_details_exclusivity(
265 &self,
266 cx: &mut js::context::JSContext,
267 conflict_resolution_behaviour: ExclusivityConflictResolution,
268 ) {
269 if !self.Open() {
276 if conflict_resolution_behaviour ==
277 ExclusivityConflictResolution::CloseExistingOpenElement
278 {
279 unreachable!()
280 } else {
281 return;
282 }
283 }
284
285 let name = self.Name();
288 if name.is_empty() {
289 return;
290 }
291
292 let other_open_member = if let Some(shadow_root) = self.containing_shadow_root() {
300 shadow_root
301 .details_name_groups(cx.no_gc())
302 .group_members_for(&name, self)
303 .find(|group_member| group_member.Open())
304 } else if self.upcast::<Node>().is_in_a_document_tree() {
305 self.owner_document()
306 .details_name_groups(cx.no_gc())
307 .group_members_for(&name, self)
308 .find(|group_member| group_member.Open())
309 } else {
310 self.upcast::<Node>()
312 .GetRootNode(&GetRootNodeOptions::empty())
313 .traverse_preorder(ShadowIncluding::No)
314 .flat_map(DomRoot::downcast::<HTMLDetailsElement>)
315 .filter(|details_element| {
316 details_element
317 .upcast::<Element>()
318 .get_string_attribute(&local_name!("name")) ==
319 name
320 })
321 .filter(|group_member| &**group_member != self)
322 .find(|group_member| group_member.Open())
323 };
324
325 if let Some(other_open_member) = other_open_member {
326 match conflict_resolution_behaviour {
334 ExclusivityConflictResolution::CloseThisElement => self.SetOpen(cx, false),
335 ExclusivityConflictResolution::CloseExistingOpenElement => {
336 other_open_member.SetOpen(cx, false)
337 },
338 }
339 }
340 }
341}
342
343impl HTMLDetailsElementMethods<crate::DomTypeHolder> for HTMLDetailsElement {
344 make_getter!(Name, "name");
346
347 make_atomic_setter!(SetName, "name");
349
350 make_bool_getter!(Open, "open");
352
353 make_bool_setter!(SetOpen, "open");
355}
356
357impl VirtualMethods for HTMLDetailsElement {
358 fn super_type(&self) -> Option<&dyn VirtualMethods> {
359 Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
360 }
361
362 fn attribute_mutated(
364 &self,
365 cx: &mut js::context::JSContext,
366 attr: AttrRef<'_>,
367 mutation: AttributeMutation,
368 ) {
369 self.super_type()
370 .unwrap()
371 .attribute_mutated(cx, attr, mutation);
372
373 if *attr.namespace() != ns!() {
375 return;
376 }
377
378 if attr.local_name() == &local_name!("name") {
381 let old_name = mutation.old_value(attr).map(|value| value.into());
382
383 if let Some(shadow_root) = self.containing_shadow_root() {
384 if let Some(old_name) = old_name {
385 shadow_root
386 .details_name_groups(cx.no_gc())
387 .unregister_details_element(old_name, self);
388 }
389 if matches!(mutation, AttributeMutation::Set(_)) {
390 shadow_root
391 .details_name_groups(cx.no_gc())
392 .register_details_element(self);
393 }
394 } else if self.upcast::<Node>().is_in_a_document_tree() {
395 let document = self.owner_document();
396 if let Some(old_name) = old_name {
397 document
398 .details_name_groups(cx.no_gc())
399 .unregister_details_element(old_name, self);
400 }
401 if matches!(mutation, AttributeMutation::Set(_)) {
402 document
403 .details_name_groups(cx.no_gc())
404 .register_details_element(self);
405 }
406 }
407
408 self.ensure_details_exclusivity(cx, ExclusivityConflictResolution::CloseThisElement);
409 }
410 else if attr.local_name() == &local_name!("open") {
412 self.update_shadow_tree_styles(cx);
413
414 let counter = self.toggle_counter.get().wrapping_add(1);
415 self.toggle_counter.set(counter);
416 let (old_state, new_state) = if self.Open() {
417 ("closed", "open")
418 } else {
419 ("open", "closed")
420 };
421
422 let this = Trusted::new(self);
423 self.owner_global()
424 .task_manager()
425 .dom_manipulation_task_source()
426 .queue(task!(details_notification_task_steps: move |cx| {
427 let this = this.root();
428 if counter == this.toggle_counter.get() {
429 let event = ToggleEvent::new(
430 cx,
431 this.global().as_window(),
432 atom!("toggle"),
433 EventBubbles::DoesNotBubble,
434 EventCancelable::NotCancelable,
435 DOMString::from(old_state),
436 DOMString::from(new_state),
437 None,
438 );
439 let event = event.upcast::<Event>();
440 event.fire(cx, this.upcast::<EventTarget>());
441 }
442 }));
443 self.upcast::<Node>().dirty(cx.no_gc(), NodeDamage::Other);
444
445 let was_previously_closed = match mutation {
448 AttributeMutation::Set(old) => old.is_none(),
449 AttributeMutation::Removed => false,
450 };
451 if was_previously_closed && self.Open() {
452 self.ensure_details_exclusivity(
453 cx,
454 ExclusivityConflictResolution::CloseExistingOpenElement,
455 );
456 }
457
458 self.upcast::<Element>().set_open_state(self.Open());
459 }
460 }
461
462 fn children_changed(&self, cx: &mut JSContext, mutation: &ChildrenMutation) {
463 self.super_type().unwrap().children_changed(cx, mutation);
464
465 self.update_shadow_tree_contents(cx);
466 }
467
468 fn bind_to_tree(&self, cx: &mut JSContext, context: &BindContext) {
470 self.super_type().unwrap().bind_to_tree(cx, context);
471
472 self.update_shadow_tree_contents(cx);
473 self.update_shadow_tree_styles(cx);
474
475 if context.tree_is_in_a_document_tree {
476 self.owner_document()
479 .details_name_groups(cx.no_gc())
480 .register_details_element(self);
481 }
482
483 let was_already_in_shadow_tree = context.is_shadow_tree == IsShadowTree::Yes;
484 if !was_already_in_shadow_tree && let Some(shadow_root) = self.containing_shadow_root() {
485 shadow_root
486 .details_name_groups(cx.no_gc())
487 .register_details_element(self);
488 }
489
490 self.ensure_details_exclusivity(cx, ExclusivityConflictResolution::CloseThisElement);
492 }
493
494 fn unbind_from_tree(&self, cx: &mut js::context::JSContext, context: &UnbindContext) {
495 self.super_type().unwrap().unbind_from_tree(cx, context);
496
497 if context.tree_is_in_a_document_tree && !self.upcast::<Node>().is_in_a_document_tree() {
498 self.owner_document()
499 .details_name_groups(cx.no_gc())
500 .unregister_details_element(self.Name(), self);
501 }
502
503 if !self.upcast::<Node>().is_in_a_shadow_tree() &&
504 let Some(old_shadow_root) = self.containing_shadow_root()
505 {
506 old_shadow_root
509 .details_name_groups(cx.no_gc())
510 .unregister_details_element(self.Name(), self);
511 }
512 }
513}