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, TracedDOMString};
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
43static 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(DOMString::from_static(DEFAULT_SUMMARY)));
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
86 .shadow_tree
87 .safe_borrow_mut(cx.no_gc())
88 .insert(ShadowTree {
89 summary: summary.as_traced(),
90 details_content: details_content.as_traced(),
91 implicit_summary: fallback_summary.as_traced(),
92 });
93 }
94
95 fn borrow_for_shadow_tree(&self) -> Ref<'_, Option<ShadowTree>> {
96 self.shadow_tree.borrow()
97 }
98}
99
100#[dom_struct]
101pub(crate) struct HTMLDetailsElement {
102 htmlelement: HTMLElement,
103 toggle_counter: Cell<u32>,
104
105 shadow_tree: DomRefCell<Option<ShadowTree>>,
107}
108
109#[derive(Clone, Default, JSTraceable, MallocSizeOf)]
112#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
113pub(crate) struct DetailsNameGroups {
114 pub(crate) groups: HashMap<TracedDOMString, Vec<Dom<HTMLDetailsElement>>>,
116}
117
118#[derive(Clone, Copy, Debug, Eq, PartialEq)]
122enum ExclusivityConflictResolution {
123 CloseThisElement,
124 CloseExistingOpenElement,
125}
126
127impl DetailsNameGroups {
128 fn register_details_element(&mut self, details_element: &HTMLDetailsElement) {
129 let name = details_element.Name();
130 if name.is_empty() {
131 return;
132 }
133
134 debug!("Registering details element with name={name:?}");
135 let details_elements_with_the_same_name = self.groups.entry(name.traced()).or_default();
136
137 details_elements_with_the_same_name.push(Dom::from_ref(details_element));
139 }
140
141 fn unregister_details_element(
142 &mut self,
143 name: DOMString,
144 details_element: &HTMLDetailsElement,
145 ) {
146 if name.is_empty() {
147 return;
148 }
149
150 debug!("Unregistering details element with name={name:?}");
151 let Entry::Occupied(mut entry) = self.groups.entry(name.traced()) else {
152 panic!("details element is not registered");
153 };
154 entry
155 .get_mut()
156 .retain(|group_member| details_element != &**group_member);
157 }
158
159 fn group_members_for(
161 &self,
162 name: &DOMString,
163 details: &HTMLDetailsElement,
164 ) -> impl Iterator<Item = DomRoot<HTMLDetailsElement>> {
165 self.groups
166 .get(name)
167 .map(|members| members.iter())
168 .expect("No details element with the given name was registered for the tree")
169 .filter(move |member| **member != details)
170 .map(|member| member.as_rooted())
171 }
172}
173
174impl HTMLDetailsElement {
175 fn new_inherited(
176 local_name: LocalName,
177 prefix: Option<Prefix>,
178 document: &Document,
179 ) -> HTMLDetailsElement {
180 HTMLDetailsElement {
181 htmlelement: HTMLElement::new_inherited(local_name, prefix, document),
182 toggle_counter: Cell::new(0),
183 shadow_tree: Default::default(),
184 }
185 }
186
187 pub(crate) fn new(
188 cx: &mut js::context::JSContext,
189 local_name: LocalName,
190 prefix: Option<Prefix>,
191 document: &Document,
192 proto: Option<HandleObject>,
193 ) -> DomRoot<HTMLDetailsElement> {
194 Node::reflect_node_with_proto(
195 cx,
196 Box::new(HTMLDetailsElement::new_inherited(
197 local_name, prefix, document,
198 )),
199 document,
200 proto,
201 )
202 }
203
204 pub(crate) fn toggle(&self, cx: &mut JSContext) {
205 self.SetOpen(cx, !self.Open());
206 }
207
208 pub(crate) fn find_corresponding_summary_element(&self) -> Option<DomRoot<HTMLElement>> {
209 self.upcast::<Node>()
210 .children()
211 .filter_map(DomRoot::downcast::<HTMLElement>)
212 .find(|html_element| {
213 html_element.upcast::<Element>().local_name() == &local_name!("summary")
214 })
215 }
216
217 fn update_shadow_tree_contents(&self, cx: &mut JSContext) {
218 let shadow_tree = self.shadow_tree(cx);
219
220 if let Some(summary) = self.find_corresponding_summary_element() {
221 shadow_tree
222 .summary
223 .Assign(cx, vec![ElementOrText::Element(DomRoot::upcast(summary))]);
224 }
225
226 let mut slottable_children = vec![];
227 for child in self.upcast::<Node>().children() {
228 if let Some(element) = child.downcast::<Element>() {
229 if element.local_name() == &local_name!("summary") {
230 continue;
231 }
232
233 slottable_children.push(ElementOrText::Element(DomRoot::from_ref(element)));
234 }
235
236 if let Some(text) = child.downcast::<Text>() {
237 slottable_children.push(ElementOrText::Text(DomRoot::from_ref(text)));
238 }
239 }
240 shadow_tree.details_content.Assign(cx, slottable_children);
241 }
242
243 fn update_shadow_tree_styles(&self, cx: &mut JSContext) {
244 let shadow_tree = self.shadow_tree(cx);
245
246 let implicit_summary_list_item_style = if self.Open() {
250 "disclosure-open"
251 } else {
252 "disclosure-closed"
253 };
254 let implicit_summary_style = format!(
255 "display: list-item;
256 counter-increment: list-item 0;
257 list-style: {implicit_summary_list_item_style} inside;"
258 );
259 shadow_tree
260 .implicit_summary
261 .upcast::<Element>()
262 .set_string_attribute(cx, &local_name!("style"), implicit_summary_style.into());
263 }
264
265 fn ensure_details_exclusivity(
268 &self,
269 cx: &mut js::context::JSContext,
270 conflict_resolution_behaviour: ExclusivityConflictResolution,
271 ) {
272 if !self.Open() {
279 if conflict_resolution_behaviour ==
280 ExclusivityConflictResolution::CloseExistingOpenElement
281 {
282 unreachable!()
283 } else {
284 return;
285 }
286 }
287
288 let name = self.Name();
291 if name.is_empty() {
292 return;
293 }
294
295 let other_open_member = if let Some(shadow_root) = self.containing_shadow_root() {
303 shadow_root
304 .details_name_groups(cx.no_gc())
305 .group_members_for(&name, self)
306 .find(|group_member| group_member.Open())
307 } else if self.upcast::<Node>().is_in_a_document_tree() {
308 self.owner_document()
309 .details_name_groups(cx.no_gc())
310 .group_members_for(&name, self)
311 .find(|group_member| group_member.Open())
312 } else {
313 self.upcast::<Node>()
315 .GetRootNode(&GetRootNodeOptions::empty())
316 .traverse_preorder(ShadowIncluding::No)
317 .flat_map(DomRoot::downcast::<HTMLDetailsElement>)
318 .filter(|details_element| {
319 details_element
320 .upcast::<Element>()
321 .get_string_attribute(&local_name!("name")) ==
322 name
323 })
324 .filter(|group_member| &**group_member != self)
325 .find(|group_member| group_member.Open())
326 };
327
328 if let Some(other_open_member) = other_open_member {
329 match conflict_resolution_behaviour {
337 ExclusivityConflictResolution::CloseThisElement => self.SetOpen(cx, false),
338 ExclusivityConflictResolution::CloseExistingOpenElement => {
339 other_open_member.SetOpen(cx, false)
340 },
341 }
342 }
343 }
344}
345
346impl HTMLDetailsElementMethods<crate::DomTypeHolder> for HTMLDetailsElement {
347 make_getter!(Name, "name");
349
350 make_atomic_setter!(SetName, "name");
352
353 make_bool_getter!(Open, "open");
355
356 make_bool_setter!(SetOpen, "open");
358}
359
360impl VirtualMethods for HTMLDetailsElement {
361 fn super_type(&self) -> Option<&dyn VirtualMethods> {
362 Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
363 }
364
365 fn attribute_mutated(
367 &self,
368 cx: &mut js::context::JSContext,
369 attr: AttrRef<'_>,
370 mutation: AttributeMutation,
371 ) {
372 self.super_type()
373 .unwrap()
374 .attribute_mutated(cx, attr, mutation);
375
376 if *attr.namespace() != ns!() {
378 return;
379 }
380
381 if attr.local_name() == &local_name!("name") {
384 let old_name = mutation.old_value(attr).map(|value| value.into());
385
386 if let Some(shadow_root) = self.containing_shadow_root() {
387 if let Some(old_name) = old_name {
388 shadow_root
389 .details_name_groups(cx.no_gc())
390 .unregister_details_element(old_name, self);
391 }
392 if matches!(mutation, AttributeMutation::Set(_)) {
393 shadow_root
394 .details_name_groups(cx.no_gc())
395 .register_details_element(self);
396 }
397 } else if self.upcast::<Node>().is_in_a_document_tree() {
398 let document = self.owner_document();
399 if let Some(old_name) = old_name {
400 document
401 .details_name_groups(cx.no_gc())
402 .unregister_details_element(old_name, self);
403 }
404 if matches!(mutation, AttributeMutation::Set(_)) {
405 document
406 .details_name_groups(cx.no_gc())
407 .register_details_element(self);
408 }
409 }
410
411 self.ensure_details_exclusivity(cx, ExclusivityConflictResolution::CloseThisElement);
412 }
413 else if attr.local_name() == &local_name!("open") {
415 self.update_shadow_tree_styles(cx);
416
417 let counter = self.toggle_counter.get().wrapping_add(1);
418 self.toggle_counter.set(counter);
419 let (old_state, new_state) = if self.Open() {
420 ("closed", "open")
421 } else {
422 ("open", "closed")
423 };
424
425 let this = Trusted::new(self);
426 self.owner_global()
427 .task_manager()
428 .dom_manipulation_task_source()
429 .queue(task!(details_notification_task_steps: move |cx| {
430 let this = this.root();
431 if counter == this.toggle_counter.get() {
432 let event = ToggleEvent::new(
433 cx,
434 this.global().as_window(),
435 atom!("toggle"),
436 EventBubbles::DoesNotBubble,
437 EventCancelable::NotCancelable,
438 DOMString::from(old_state),
439 DOMString::from(new_state),
440 None,
441 );
442 let event = event.upcast::<Event>();
443 event.fire(cx, this.upcast::<EventTarget>());
444 }
445 }));
446 self.upcast::<Node>().dirty(cx.no_gc(), NodeDamage::Other);
447
448 let was_previously_closed = match mutation {
451 AttributeMutation::Set(old) => old.is_none(),
452 AttributeMutation::Removed => false,
453 };
454 if was_previously_closed && self.Open() {
455 self.ensure_details_exclusivity(
456 cx,
457 ExclusivityConflictResolution::CloseExistingOpenElement,
458 );
459 }
460
461 self.upcast::<Element>()
462 .set_open_state(cx.no_gc(), self.Open());
463 }
464 }
465
466 fn children_changed(&self, cx: &mut JSContext, mutation: &ChildrenMutation) {
467 self.super_type().unwrap().children_changed(cx, mutation);
468
469 self.update_shadow_tree_contents(cx);
470 }
471
472 fn bind_to_tree(&self, cx: &mut JSContext, context: &BindContext) {
474 self.super_type().unwrap().bind_to_tree(cx, context);
475
476 self.update_shadow_tree_contents(cx);
477 self.update_shadow_tree_styles(cx);
478
479 if context.tree_is_in_a_document_tree {
480 self.owner_document()
483 .details_name_groups(cx.no_gc())
484 .register_details_element(self);
485 }
486
487 let was_already_in_shadow_tree = context.is_shadow_tree == IsShadowTree::Yes;
488 if !was_already_in_shadow_tree && let Some(shadow_root) = self.containing_shadow_root() {
489 shadow_root
490 .details_name_groups(cx.no_gc())
491 .register_details_element(self);
492 }
493
494 self.ensure_details_exclusivity(cx, ExclusivityConflictResolution::CloseThisElement);
496 }
497
498 fn unbind_from_tree(&self, cx: &mut js::context::JSContext, context: &UnbindContext) {
499 self.super_type().unwrap().unbind_from_tree(cx, context);
500
501 if context.tree_is_in_a_document_tree && !self.upcast::<Node>().is_in_a_document_tree() {
502 self.owner_document()
503 .details_name_groups(cx.no_gc())
504 .unregister_details_element(self.Name(), self);
505 }
506
507 if !self.upcast::<Node>().is_in_a_shadow_tree() &&
508 let Some(old_shadow_root) = self.containing_shadow_root()
509 {
510 old_shadow_root
513 .details_name_groups(cx.no_gc())
514 .unregister_details_element(self.Name(), self);
515 }
516 }
517}