/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::cell::{Cell, Ref}; use dom_struct::dom_struct; use html5ever::{LocalName, Prefix, local_name}; use js::rust::HandleObject; use crate::dom::attr::Attr; use crate::dom::bindings::cell::DomRefCell; use crate::dom::bindings::codegen::Bindings::HTMLDetailsElementBinding::HTMLDetailsElementMethods; use crate::dom::bindings::codegen::Bindings::HTMLSlotElementBinding::HTMLSlotElement_Binding::HTMLSlotElementMethods; use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods; use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::{ ShadowRootMode, SlotAssignmentMode, }; use crate::dom::bindings::codegen::UnionTypes::ElementOrText; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::refcounted::Trusted; use crate::dom::bindings::root::{Dom, DomRoot}; use crate::dom::document::Document; use crate::dom::element::{AttributeMutation, Element}; use crate::dom::eventtarget::EventTarget; use crate::dom::htmlelement::HTMLElement; use crate::dom::htmlslotelement::HTMLSlotElement; use crate::dom::node::{BindContext, ChildrenMutation, Node, NodeDamage, NodeTraits}; use crate::dom::shadowroot::IsUserAgentWidget; use crate::dom::text::Text; use crate::dom::virtualmethods::VirtualMethods; use crate::script_runtime::CanGc; /// The summary that should be presented if no `` element is present const DEFAULT_SUMMARY: &str = "Details"; /// Holds handles to all slots in the UA shadow tree /// /// The composition of the tree is described in /// #[derive(Clone, JSTraceable, MallocSizeOf)] #[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] struct ShadowTree { summary: Dom, descendants: Dom, /// The summary that is displayed if no other summary exists implicit_summary: Dom, } #[dom_struct] pub(crate) struct HTMLDetailsElement { htmlelement: HTMLElement, toggle_counter: Cell, /// Represents the UA widget for the details element shadow_tree: DomRefCell>, } impl HTMLDetailsElement { fn new_inherited( local_name: LocalName, prefix: Option, document: &Document, ) -> HTMLDetailsElement { HTMLDetailsElement { htmlelement: HTMLElement::new_inherited(local_name, prefix, document), toggle_counter: Cell::new(0), shadow_tree: Default::default(), } } #[cfg_attr(crown, allow(crown::unrooted_must_root))] pub(crate) fn new( local_name: LocalName, prefix: Option, document: &Document, proto: Option, can_gc: CanGc, ) -> DomRoot { Node::reflect_node_with_proto( Box::new(HTMLDetailsElement::new_inherited( local_name, prefix, document, )), document, proto, can_gc, ) } pub(crate) fn toggle(&self) { self.SetOpen(!self.Open()); } fn shadow_tree(&self, can_gc: CanGc) -> Ref<'_, ShadowTree> { if !self.upcast::().is_shadow_host() { self.create_shadow_tree(can_gc); } Ref::filter_map(self.shadow_tree.borrow(), Option::as_ref) .ok() .expect("UA shadow tree was not created") } fn create_shadow_tree(&self, can_gc: CanGc) { let document = self.owner_document(); let root = self .upcast::() .attach_shadow( IsUserAgentWidget::Yes, ShadowRootMode::Closed, false, false, false, SlotAssignmentMode::Manual, can_gc, ) .expect("Attaching UA shadow root failed"); let summary = HTMLSlotElement::new(local_name!("slot"), None, &document, None, can_gc); root.upcast::() .AppendChild(summary.upcast::(), can_gc) .unwrap(); let fallback_summary = HTMLElement::new(local_name!("summary"), None, &document, None, CanGc::note()); fallback_summary .upcast::() .SetTextContent(Some(DEFAULT_SUMMARY.into()), can_gc); summary .upcast::() .AppendChild(fallback_summary.upcast::(), can_gc) .unwrap(); let descendants = HTMLSlotElement::new(local_name!("slot"), None, &document, None, can_gc); root.upcast::() .AppendChild(descendants.upcast::(), can_gc) .unwrap(); let _ = self.shadow_tree.borrow_mut().insert(ShadowTree { summary: summary.as_traced(), descendants: descendants.as_traced(), implicit_summary: fallback_summary.as_traced(), }); self.upcast::() .dirty(crate::dom::node::NodeDamage::OtherNodeDamage); } pub(crate) fn find_corresponding_summary_element(&self) -> Option> { self.upcast::() .children() .filter_map(DomRoot::downcast::) .find(|html_element| { html_element.upcast::().local_name() == &local_name!("summary") }) } fn update_shadow_tree_contents(&self, can_gc: CanGc) { let shadow_tree = self.shadow_tree(can_gc); if let Some(summary) = self.find_corresponding_summary_element() { shadow_tree .summary .Assign(vec![ElementOrText::Element(DomRoot::upcast(summary))]); } let mut slottable_children = vec![]; for child in self.upcast::().children() { if let Some(element) = child.downcast::() { if element.local_name() == &local_name!("summary") { continue; } slottable_children.push(ElementOrText::Element(DomRoot::from_ref(element))); } if let Some(text) = child.downcast::() { slottable_children.push(ElementOrText::Text(DomRoot::from_ref(text))); } } shadow_tree.descendants.Assign(slottable_children); self.upcast::().dirty(NodeDamage::OtherNodeDamage); } fn update_shadow_tree_styles(&self, can_gc: CanGc) { let shadow_tree = self.shadow_tree(can_gc); let value = if self.Open() { "display: block;" } else { // TODO: This should be "display: block; content-visibility: hidden;", // but servo does not support content-visibility yet "display: none;" }; shadow_tree .descendants .upcast::() .set_string_attribute(&local_name!("style"), value.into(), can_gc); // Manually update the list item style of the implicit summary element. // Unlike the other summaries, this summary is in the shadow tree and // can't be styled with UA sheets let implicit_summary_list_item_style = if self.Open() { "disclosure-open" } else { "disclosure-closed" }; let implicit_summary_style = format!( "display: list-item; counter-increment: list-item 0; list-style: {implicit_summary_list_item_style} inside;" ); shadow_tree .implicit_summary .upcast::() .set_string_attribute(&local_name!("style"), implicit_summary_style.into(), can_gc); self.upcast::().dirty(NodeDamage::OtherNodeDamage); } } impl HTMLDetailsElementMethods for HTMLDetailsElement { // https://html.spec.whatwg.org/multipage/#dom-details-open make_bool_getter!(Open, "open"); // https://html.spec.whatwg.org/multipage/#dom-details-open make_bool_setter!(SetOpen, "open"); } impl VirtualMethods for HTMLDetailsElement { fn super_type(&self) -> Option<&dyn VirtualMethods> { Some(self.upcast::() as &dyn VirtualMethods) } fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation, can_gc: CanGc) { self.super_type() .unwrap() .attribute_mutated(attr, mutation, can_gc); if attr.local_name() == &local_name!("open") { self.update_shadow_tree_styles(CanGc::note()); let counter = self.toggle_counter.get() + 1; self.toggle_counter.set(counter); let this = Trusted::new(self); self.owner_global() .task_manager() .dom_manipulation_task_source() .queue(task!(details_notification_task_steps: move || { let this = this.root(); if counter == this.toggle_counter.get() { this.upcast::().fire_event(atom!("toggle"), CanGc::note()); } })); self.upcast::().dirty(NodeDamage::OtherNodeDamage); } } fn children_changed(&self, mutation: &ChildrenMutation) { self.super_type().unwrap().children_changed(mutation); self.update_shadow_tree_contents(CanGc::note()); } fn bind_to_tree(&self, context: &BindContext, can_gc: CanGc) { self.super_type().unwrap().bind_to_tree(context, can_gc); self.update_shadow_tree_contents(CanGc::note()); self.update_shadow_tree_styles(CanGc::note()); } }