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