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;
13
14use crate::dom::attr::Attr;
15use crate::dom::bindings::cell::DomRefCell;
16use crate::dom::bindings::codegen::Bindings::HTMLDetailsElementBinding::HTMLDetailsElementMethods;
17use crate::dom::bindings::codegen::Bindings::HTMLSlotElementBinding::HTMLSlotElement_Binding::HTMLSlotElementMethods;
18use crate::dom::bindings::codegen::Bindings::NodeBinding::GetRootNodeOptions;
19use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods;
20use crate::dom::bindings::codegen::UnionTypes::ElementOrText;
21use crate::dom::bindings::inheritance::Castable;
22use crate::dom::bindings::refcounted::Trusted;
23use crate::dom::bindings::reflector::DomGlobal;
24use crate::dom::bindings::root::{Dom, DomRoot};
25use crate::dom::document::Document;
26use crate::dom::element::{AttributeMutation, CustomElementCreationMode, Element, ElementCreator};
27use crate::dom::event::{Event, EventBubbles, EventCancelable};
28use crate::dom::eventtarget::EventTarget;
29use crate::dom::html::htmlelement::HTMLElement;
30use crate::dom::html::htmlslotelement::HTMLSlotElement;
31use crate::dom::node::{
32 BindContext, ChildrenMutation, IsShadowTree, Node, NodeDamage, NodeTraits, ShadowIncluding,
33 UnbindContext,
34};
35use crate::dom::text::Text;
36use crate::dom::toggleevent::ToggleEvent;
37use crate::dom::virtualmethods::VirtualMethods;
38use crate::script_runtime::CanGc;
39
40const DEFAULT_SUMMARY: &str = "Details";
42
43#[derive(Clone, JSTraceable, MallocSizeOf)]
48#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
49struct ShadowTree {
50 summary: Dom<HTMLSlotElement>,
51 descendants: Dom<HTMLSlotElement>,
52 implicit_summary: Dom<HTMLElement>,
54}
55
56#[dom_struct]
57pub(crate) struct HTMLDetailsElement {
58 htmlelement: HTMLElement,
59 toggle_counter: Cell<u32>,
60
61 shadow_tree: DomRefCell<Option<ShadowTree>>,
63}
64
65#[derive(Clone, Default, JSTraceable, MallocSizeOf)]
68#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
69pub(crate) struct DetailsNameGroups {
70 pub(crate) groups: HashMap<DOMString, Vec<Dom<HTMLDetailsElement>>>,
72}
73
74#[derive(Clone, Copy, Debug, Eq, PartialEq)]
78enum ExclusivityConflictResolution {
79 CloseThisElement,
80 CloseExistingOpenElement,
81}
82
83impl DetailsNameGroups {
84 fn register_details_element(&mut self, details_element: &HTMLDetailsElement) {
85 let name = details_element.Name();
86 if name.is_empty() {
87 return;
88 }
89
90 debug!("Registering details element with name={name:?}");
91 let details_elements_with_the_same_name = self.groups.entry(name).or_default();
92
93 details_elements_with_the_same_name.push(Dom::from_ref(details_element));
95 }
96
97 fn unregister_details_element(
98 &mut self,
99 name: DOMString,
100 details_element: &HTMLDetailsElement,
101 ) {
102 if name.is_empty() {
103 return;
104 }
105
106 debug!("Unregistering details element with name={name:?}");
107 let Entry::Occupied(mut entry) = self.groups.entry(name) else {
108 panic!("details element is not registered");
109 };
110 entry
111 .get_mut()
112 .retain(|group_member| details_element != &**group_member);
113 }
114
115 fn group_members_for(
117 &self,
118 name: &DOMString,
119 details: &HTMLDetailsElement,
120 ) -> impl Iterator<Item = DomRoot<HTMLDetailsElement>> {
121 self.groups
122 .get(name)
123 .map(|members| members.iter())
124 .expect("No details element with the given name was registered for the tree")
125 .filter(move |member| **member != details)
126 .map(|member| member.as_rooted())
127 }
128}
129
130impl HTMLDetailsElement {
131 fn new_inherited(
132 local_name: LocalName,
133 prefix: Option<Prefix>,
134 document: &Document,
135 ) -> HTMLDetailsElement {
136 HTMLDetailsElement {
137 htmlelement: HTMLElement::new_inherited(local_name, prefix, document),
138 toggle_counter: Cell::new(0),
139 shadow_tree: Default::default(),
140 }
141 }
142
143 #[cfg_attr(crown, allow(crown::unrooted_must_root))]
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
180 .upcast::<Element>()
181 .attach_ua_shadow_root(false, can_gc);
182
183 let summary = Element::create(
184 QualName::new(None, ns!(html), local_name!("slot")),
185 None,
186 &document,
187 ElementCreator::ScriptCreated,
188 CustomElementCreationMode::Asynchronous,
189 None,
190 can_gc,
191 );
192 let summary = DomRoot::downcast::<HTMLSlotElement>(summary).unwrap();
193 root.upcast::<Node>()
194 .AppendChild(summary.upcast::<Node>(), can_gc)
195 .unwrap();
196
197 let fallback_summary = Element::create(
198 QualName::new(None, ns!(html), local_name!("summary")),
199 None,
200 &document,
201 ElementCreator::ScriptCreated,
202 CustomElementCreationMode::Asynchronous,
203 None,
204 can_gc,
205 );
206 let fallback_summary = DomRoot::downcast::<HTMLElement>(fallback_summary).unwrap();
207 fallback_summary
208 .upcast::<Node>()
209 .set_text_content_for_element(Some(DEFAULT_SUMMARY.into()), can_gc);
210 summary
211 .upcast::<Node>()
212 .AppendChild(fallback_summary.upcast::<Node>(), can_gc)
213 .unwrap();
214
215 let descendants = Element::create(
216 QualName::new(None, ns!(html), local_name!("slot")),
217 None,
218 &document,
219 ElementCreator::ScriptCreated,
220 CustomElementCreationMode::Asynchronous,
221 None,
222 can_gc,
223 );
224 let descendants = DomRoot::downcast::<HTMLSlotElement>(descendants).unwrap();
225 root.upcast::<Node>()
226 .AppendChild(descendants.upcast::<Node>(), can_gc)
227 .unwrap();
228
229 let _ = self.shadow_tree.borrow_mut().insert(ShadowTree {
230 summary: summary.as_traced(),
231 descendants: descendants.as_traced(),
232 implicit_summary: fallback_summary.as_traced(),
233 });
234 self.upcast::<Node>()
235 .dirty(crate::dom::node::NodeDamage::Other);
236 }
237
238 pub(crate) fn find_corresponding_summary_element(&self) -> Option<DomRoot<HTMLElement>> {
239 self.upcast::<Node>()
240 .children()
241 .filter_map(DomRoot::downcast::<HTMLElement>)
242 .find(|html_element| {
243 html_element.upcast::<Element>().local_name() == &local_name!("summary")
244 })
245 }
246
247 fn update_shadow_tree_contents(&self, can_gc: CanGc) {
248 let shadow_tree = self.shadow_tree(can_gc);
249
250 if let Some(summary) = self.find_corresponding_summary_element() {
251 shadow_tree
252 .summary
253 .Assign(vec![ElementOrText::Element(DomRoot::upcast(summary))]);
254 }
255
256 let mut slottable_children = vec![];
257 for child in self.upcast::<Node>().children() {
258 if let Some(element) = child.downcast::<Element>() {
259 if element.local_name() == &local_name!("summary") {
260 continue;
261 }
262
263 slottable_children.push(ElementOrText::Element(DomRoot::from_ref(element)));
264 }
265
266 if let Some(text) = child.downcast::<Text>() {
267 slottable_children.push(ElementOrText::Text(DomRoot::from_ref(text)));
268 }
269 }
270 shadow_tree.descendants.Assign(slottable_children);
271 }
272
273 fn update_shadow_tree_styles(&self, can_gc: CanGc) {
274 let shadow_tree = self.shadow_tree(can_gc);
275
276 let value = if self.Open() {
277 "display: block;"
278 } else {
279 "display: none;"
282 };
283 shadow_tree
284 .descendants
285 .upcast::<Element>()
286 .set_string_attribute(&local_name!("style"), value.into(), can_gc);
287
288 let implicit_summary_list_item_style = if self.Open() {
292 "disclosure-open"
293 } else {
294 "disclosure-closed"
295 };
296 let implicit_summary_style = format!(
297 "display: list-item;
298 counter-increment: list-item 0;
299 list-style: {implicit_summary_list_item_style} inside;"
300 );
301 shadow_tree
302 .implicit_summary
303 .upcast::<Element>()
304 .set_string_attribute(&local_name!("style"), implicit_summary_style.into(), can_gc);
305 }
306
307 fn ensure_details_exclusivity(
310 &self,
311 conflict_resolution_behaviour: ExclusivityConflictResolution,
312 ) {
313 if !self.Open() {
320 if conflict_resolution_behaviour ==
321 ExclusivityConflictResolution::CloseExistingOpenElement
322 {
323 unreachable!()
324 } else {
325 return;
326 }
327 }
328
329 let name = self.Name();
332 if name.is_empty() {
333 return;
334 }
335
336 let other_open_member = if let Some(shadow_root) = self.containing_shadow_root() {
344 shadow_root
345 .details_name_groups()
346 .group_members_for(&name, self)
347 .find(|group_member| group_member.Open())
348 } else if self.upcast::<Node>().is_in_a_document_tree() {
349 self.owner_document()
350 .details_name_groups()
351 .group_members_for(&name, self)
352 .find(|group_member| group_member.Open())
353 } else {
354 self.upcast::<Node>()
356 .GetRootNode(&GetRootNodeOptions::empty())
357 .traverse_preorder(ShadowIncluding::No)
358 .flat_map(DomRoot::downcast::<HTMLDetailsElement>)
359 .filter(|details_element| {
360 details_element
361 .upcast::<Element>()
362 .get_string_attribute(&local_name!("name")) ==
363 name
364 })
365 .filter(|group_member| &**group_member != self)
366 .find(|group_member| group_member.Open())
367 };
368
369 if let Some(other_open_member) = other_open_member {
370 match conflict_resolution_behaviour {
378 ExclusivityConflictResolution::CloseThisElement => self.SetOpen(false),
379 ExclusivityConflictResolution::CloseExistingOpenElement => {
380 other_open_member.SetOpen(false)
381 },
382 }
383 }
384 }
385}
386
387impl HTMLDetailsElementMethods<crate::DomTypeHolder> for HTMLDetailsElement {
388 make_getter!(Name, "name");
390
391 make_atomic_setter!(SetName, "name");
393
394 make_bool_getter!(Open, "open");
396
397 make_bool_setter!(SetOpen, "open");
399}
400
401impl VirtualMethods for HTMLDetailsElement {
402 fn super_type(&self) -> Option<&dyn VirtualMethods> {
403 Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
404 }
405
406 fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation, can_gc: CanGc) {
408 self.super_type()
409 .unwrap()
410 .attribute_mutated(attr, mutation, can_gc);
411
412 if *attr.namespace() != ns!() {
414 return;
415 }
416
417 if attr.local_name() == &local_name!("name") {
420 let old_name: Option<DOMString> = match mutation {
421 AttributeMutation::Set(old, _) => old.map(|value| value.to_string().into()),
422 AttributeMutation::Removed => Some(attr.value().to_string().into()),
423 };
424
425 if let Some(shadow_root) = self.containing_shadow_root() {
426 if let Some(old_name) = old_name {
427 shadow_root
428 .details_name_groups()
429 .unregister_details_element(old_name, self);
430 }
431 if matches!(mutation, AttributeMutation::Set(..)) {
432 shadow_root
433 .details_name_groups()
434 .register_details_element(self);
435 }
436 } else if self.upcast::<Node>().is_in_a_document_tree() {
437 let document = self.owner_document();
438 if let Some(old_name) = old_name {
439 document
440 .details_name_groups()
441 .unregister_details_element(old_name, self);
442 }
443 if matches!(mutation, AttributeMutation::Set(..)) {
444 document
445 .details_name_groups()
446 .register_details_element(self);
447 }
448 }
449
450 self.ensure_details_exclusivity(ExclusivityConflictResolution::CloseThisElement);
451 }
452 else if attr.local_name() == &local_name!("open") {
454 self.update_shadow_tree_styles(can_gc);
455
456 let counter = self.toggle_counter.get().wrapping_add(1);
457 self.toggle_counter.set(counter);
458 let (old_state, new_state) = if self.Open() {
459 ("closed", "open")
460 } else {
461 ("open", "closed")
462 };
463
464 let this = Trusted::new(self);
465 self.owner_global()
466 .task_manager()
467 .dom_manipulation_task_source()
468 .queue(task!(details_notification_task_steps: move || {
469 let this = this.root();
470 if counter == this.toggle_counter.get() {
471 let event = ToggleEvent::new(
472 this.global().as_window(),
473 atom!("toggle"),
474 EventBubbles::DoesNotBubble,
475 EventCancelable::NotCancelable,
476 DOMString::from(old_state),
477 DOMString::from(new_state),
478 None,
479 CanGc::note(),
480 );
481 let event = event.upcast::<Event>();
482 event.fire(this.upcast::<EventTarget>(), CanGc::note());
483 }
484 }));
485 self.upcast::<Node>().dirty(NodeDamage::Other);
486
487 let was_previously_closed = match mutation {
490 AttributeMutation::Set(old, _) => old.is_none(),
491 AttributeMutation::Removed => false,
492 };
493 if was_previously_closed && self.Open() {
494 self.ensure_details_exclusivity(
495 ExclusivityConflictResolution::CloseExistingOpenElement,
496 );
497 }
498 }
499 }
500
501 fn children_changed(&self, mutation: &ChildrenMutation, can_gc: CanGc) {
502 self.super_type()
503 .unwrap()
504 .children_changed(mutation, can_gc);
505
506 self.update_shadow_tree_contents(can_gc);
507 }
508
509 fn bind_to_tree(&self, context: &BindContext, can_gc: CanGc) {
511 self.super_type().unwrap().bind_to_tree(context, can_gc);
512
513 self.update_shadow_tree_contents(can_gc);
514 self.update_shadow_tree_styles(can_gc);
515
516 if context.tree_is_in_a_document_tree {
517 self.owner_document()
520 .details_name_groups()
521 .register_details_element(self);
522 }
523
524 let was_already_in_shadow_tree = context.is_shadow_tree == IsShadowTree::Yes;
525 if !was_already_in_shadow_tree {
526 if let Some(shadow_root) = self.containing_shadow_root() {
527 shadow_root
528 .details_name_groups()
529 .register_details_element(self);
530 }
531 }
532
533 self.ensure_details_exclusivity(ExclusivityConflictResolution::CloseThisElement);
535 }
536
537 fn unbind_from_tree(&self, context: &UnbindContext, can_gc: CanGc) {
538 self.super_type().unwrap().unbind_from_tree(context, can_gc);
539
540 if context.tree_is_in_a_document_tree && !self.upcast::<Node>().is_in_a_document_tree() {
541 self.owner_document()
542 .details_name_groups()
543 .unregister_details_element(self.Name(), self);
544 }
545
546 if !self.upcast::<Node>().is_in_a_shadow_tree() {
547 if let Some(old_shadow_root) = self.containing_shadow_root() {
548 old_shadow_root
551 .details_name_groups()
552 .unregister_details_element(self.Name(), self);
553 }
554 }
555 }
556}