1use 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
41const DEFAULT_SUMMARY: &str = "Details";
43
44#[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 implicit_summary: Dom<HTMLElement>,
55}
56
57#[dom_struct]
58pub(crate) struct HTMLDetailsElement {
59 htmlelement: HTMLElement,
60 toggle_counter: Cell<u32>,
61
62 shadow_tree: DomRefCell<Option<ShadowTree>>,
64}
65
66#[derive(Clone, Default, JSTraceable, MallocSizeOf)]
69#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
70pub(crate) struct DetailsNameGroups {
71 pub(crate) groups: HashMap<DOMString, Vec<Dom<HTMLDetailsElement>>>,
73}
74
75#[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 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 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 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 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 fn ensure_details_exclusivity(
300 &self,
301 conflict_resolution_behaviour: ExclusivityConflictResolution,
302 ) {
303 if !self.Open() {
310 if conflict_resolution_behaviour ==
311 ExclusivityConflictResolution::CloseExistingOpenElement
312 {
313 unreachable!()
314 } else {
315 return;
316 }
317 }
318
319 let name = self.Name();
322 if name.is_empty() {
323 return;
324 }
325
326 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 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 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 make_getter!(Name, "name");
380
381 make_atomic_setter!(SetName, "name");
383
384 make_bool_getter!(Open, "open");
386
387 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 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 if *attr.namespace() != ns!() {
404 return;
405 }
406
407 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 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 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 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 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 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 old_shadow_root
543 .details_name_groups()
544 .unregister_details_element(self.Name(), self);
545 }
546 }
547 }
548}