| /* 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::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::html::htmlelement::HTMLElement; |
| use crate::dom::html::htmlslotelement::HTMLSlotElement; |
| use crate::dom::node::{BindContext, ChildrenMutation, Node, NodeDamage, NodeTraits}; |
| use crate::dom::text::Text; |
| use crate::dom::virtualmethods::VirtualMethods; |
| use crate::script_runtime::CanGc; |
| |
| /// The summary that should be presented if no `<summary>` 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 |
| /// <https://html.spec.whatwg.org/multipage/#the-details-and-summary-elements> |
| #[derive(Clone, JSTraceable, MallocSizeOf)] |
| #[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] |
| struct ShadowTree { |
| summary: Dom<HTMLSlotElement>, |
| descendants: Dom<HTMLSlotElement>, |
| /// The summary that is displayed if no other summary exists |
| implicit_summary: Dom<HTMLElement>, |
| } |
| |
| #[dom_struct] |
| pub(crate) struct HTMLDetailsElement { |
| htmlelement: HTMLElement, |
| toggle_counter: Cell<u32>, |
| |
| /// Represents the UA widget for the details element |
| shadow_tree: DomRefCell<Option<ShadowTree>>, |
| } |
| |
| impl HTMLDetailsElement { |
| fn new_inherited( |
| local_name: LocalName, |
| prefix: Option<Prefix>, |
| 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<Prefix>, |
| document: &Document, |
| proto: Option<HandleObject>, |
| can_gc: CanGc, |
| ) -> DomRoot<HTMLDetailsElement> { |
| 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::<Element>().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(); |
| // TODO(stevennovaryo): Reimplement details styling so that it would not |
| // mess the cascading and require some reparsing. |
| let root = self |
| .upcast::<Element>() |
| .attach_ua_shadow_root(false, can_gc); |
| |
| let summary = HTMLSlotElement::new(local_name!("slot"), None, &document, None, can_gc); |
| root.upcast::<Node>() |
| .AppendChild(summary.upcast::<Node>(), can_gc) |
| .unwrap(); |
| |
| let fallback_summary = |
| HTMLElement::new(local_name!("summary"), None, &document, None, can_gc); |
| fallback_summary |
| .upcast::<Node>() |
| .set_text_content_for_element(Some(DEFAULT_SUMMARY.into()), can_gc); |
| summary |
| .upcast::<Node>() |
| .AppendChild(fallback_summary.upcast::<Node>(), can_gc) |
| .unwrap(); |
| |
| let descendants = HTMLSlotElement::new(local_name!("slot"), None, &document, None, can_gc); |
| root.upcast::<Node>() |
| .AppendChild(descendants.upcast::<Node>(), 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::<Node>() |
| .dirty(crate::dom::node::NodeDamage::Other); |
| } |
| |
| pub(crate) fn find_corresponding_summary_element(&self) -> Option<DomRoot<HTMLElement>> { |
| self.upcast::<Node>() |
| .children() |
| .filter_map(DomRoot::downcast::<HTMLElement>) |
| .find(|html_element| { |
| html_element.upcast::<Element>().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::<Node>().children() { |
| if let Some(element) = child.downcast::<Element>() { |
| if element.local_name() == &local_name!("summary") { |
| continue; |
| } |
| |
| slottable_children.push(ElementOrText::Element(DomRoot::from_ref(element))); |
| } |
| |
| if let Some(text) = child.downcast::<Text>() { |
| slottable_children.push(ElementOrText::Text(DomRoot::from_ref(text))); |
| } |
| } |
| shadow_tree.descendants.Assign(slottable_children); |
| } |
| |
| 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::<Element>() |
| .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::<Element>() |
| .set_string_attribute(&local_name!("style"), implicit_summary_style.into(), can_gc); |
| } |
| } |
| |
| impl HTMLDetailsElementMethods<crate::DomTypeHolder> 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::<HTMLElement>() 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(can_gc); |
| |
| 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::<EventTarget>().fire_event(atom!("toggle"), CanGc::note()); |
| } |
| })); |
| self.upcast::<Node>().dirty(NodeDamage::Other); |
| } |
| } |
| |
| 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()); |
| } |
| } |