Skip to main content

fusor/dom/
range.rs

1//! Sibling ranges delimited by two comment anchors, and attaching views to them.
2use super::{JsValue, Scope};
3use wasm_bindgen::JsCast;
4use web_sys::{Element, Node};
5
6/// A handle to a range of sibling nodes between two comment anchors.
7///
8/// A range is either an insertion slot, which a compiled template or
9/// [`Scope::mount_point`] creates and whose views are replaced over time, or a
10/// fragment scope's own content, anchors included. Cloning copies the handle;
11/// whoever created the anchors owns them.
12#[derive(Clone)]
13pub struct MountPoint {
14    pub(super) start: Node,
15    pub(super) end: Node,
16}
17
18/// Owns the anchors of a range from [`MountPoint::append`]. Dropping it removes
19/// them, but not the nodes of views still inside them.
20#[must_use = "dropping the anchors removes the range"]
21pub struct Anchors(MountPoint);
22
23impl Drop for Anchors {
24    fn drop(&mut self) {
25        for node in [&self.0.start, &self.0.end] {
26            if let Some(parent) = node.parent_node() {
27                let _ = parent.remove_child(node);
28            }
29        }
30    }
31}
32
33impl MountPoint {
34    /// Append an empty range to `container`, owned by the returned anchors.
35    pub fn append(container: &Element) -> Result<(Self, Anchors), JsValue> {
36        let document = super::document()?;
37        let start: Node = document.create_comment("fusor:mount").into();
38        let end: Node = document.create_comment("fusor:end").into();
39        container.append_child(&start)?;
40        if let Err(error) = container.append_child(&end) {
41            let _ = container.remove_child(&start);
42            return Err(error);
43        }
44        let point = Self { start, end };
45        Ok((point.clone(), Anchors(point)))
46    }
47
48    /// Native container of this range.
49    pub fn parent_element(&self) -> Result<Element, JsValue> {
50        self.validate()?;
51        self.end
52            .parent_element()
53            .ok_or_else(|| JsValue::from_str("mount range has no element parent"))
54    }
55
56    /// Check that the anchors are siblings in order, and return their parent.
57    pub(super) fn validate(&self) -> Result<Node, JsValue> {
58        self.walk(|_| {})?;
59        Ok(self
60            .start
61            .parent_node()
62            .expect("sibling anchors have a parent"))
63    }
64
65    /// Visit the nodes strictly between the anchors, failing unless the
66    /// anchors are siblings in order.
67    fn walk(&self, mut visit: impl FnMut(Node)) -> Result<(), JsValue> {
68        let mut cursor = self.start.next_sibling();
69        while let Some(node) = cursor {
70            if node.is_same_node(Some(&self.end)) {
71                return Ok(());
72            }
73            cursor = node.next_sibling();
74            visit(node);
75        }
76        let (start, end) = (self.start.parent_node(), self.end.parent_node());
77        Err(JsValue::from_str(match (start, end) {
78            (None, _) => "detached mount range",
79            (Some(start), Some(end)) if start.is_same_node(Some(&end)) => {
80                "mount range anchors are out of order"
81            }
82            _ => "mount range anchors have different parents",
83        }))
84    }
85
86    pub(super) fn is_empty(&self) -> Result<bool, JsValue> {
87        let mut empty = true;
88        self.walk(|_| empty = false)?;
89        Ok(empty)
90    }
91
92    /// The server-rendered root in this slot when hydration begins. There must
93    /// be exactly one element when a child is `present`, and nothing otherwise.
94    pub(super) fn hydrated_root(&self, present: bool) -> Result<Option<Element>, JsValue> {
95        let mut inner = Vec::new();
96        self.walk(|node| inner.push(node))?;
97        match (present, inner.as_slice()) {
98            (false, []) => Ok(None),
99            (true, [root]) if root.node_type() == Node::ELEMENT_NODE => {
100                Ok(Some(root.clone().unchecked_into()))
101            }
102            _ => Err(JsValue::from_str(
103                "server component identity/shape mismatch",
104            )),
105        }
106    }
107
108    /// Both anchors and everything between them.
109    pub(super) fn nodes(&self) -> Result<Vec<Node>, JsValue> {
110        let mut nodes = vec![self.start.clone()];
111        self.walk(|node| nodes.push(node))?;
112        nodes.push(self.end.clone());
113        Ok(nodes)
114    }
115
116    pub(super) fn contains(&self, target: &Node) -> bool {
117        self.nodes()
118            .is_ok_and(|nodes| nodes.iter().any(|node| node.contains(Some(target))))
119    }
120
121    /// Whether `node` is already the last node of the range.
122    pub(super) fn precedes_end(&self, node: &Node) -> bool {
123        node.next_sibling()
124            .is_some_and(|next| next.is_same_node(Some(&self.end)))
125    }
126
127    /// Insert `node` last, without validating the range.
128    pub(super) fn push(&self, node: &Node) -> Result<(), JsValue> {
129        self.end
130            .parent_node()
131            .ok_or_else(|| JsValue::from_str("detached mount range"))?
132            .insert_before(node, Some(&self.end))?;
133        Ok(())
134    }
135
136    /// Remove this fragment's anchors and content from the document.
137    pub(super) fn remove(&self) {
138        let Ok(nodes) = self.nodes() else {
139            return;
140        };
141        for node in nodes {
142            #[cfg(feature = "islands")]
143            if let Some(element) = node.dyn_ref::<Element>() {
144                super::delivery::dispose_tree(element);
145            }
146            if let Some(parent) = node.parent_node() {
147                let _ = parent.remove_child(&node);
148            }
149        }
150    }
151}
152
153impl Scope {
154    /// Append an empty insertion range to a container. Dropping this scope
155    /// removes the anchors, but not the nodes of views still inside them:
156    /// callers retain and dispose any child views they mount there.
157    pub fn mount_point(&mut self, container: &Element) -> Result<MountPoint, JsValue> {
158        let (point, anchors) = MountPoint::append(container)?;
159        self.retain(anchors);
160        Ok(point)
161    }
162
163    /// Insert this prepared view at the end of `target` without activating
164    /// it. A root view is removed when the scope drops; a fragment removes its
165    /// own range. Retain the scope and finish preparation before committing.
166    pub fn attach_at(&mut self, target: &MountPoint) -> Result<(), JsValue> {
167        if self.fragment.is_some() {
168            return self.attach_fragment(target);
169        }
170        target
171            .validate()?
172            .insert_before(&self.root, Some(&target.end))?;
173        self.remove_on_drop = true;
174        Ok(())
175    }
176
177    /// Move this fragment scope's range to the end of `target`.
178    pub(super) fn attach_fragment(&self, target: &MountPoint) -> Result<(), JsValue> {
179        let parent = target.validate()?;
180        let fragment = self
181            .fragment
182            .as_ref()
183            .ok_or_else(|| JsValue::from_str("expected a children fragment"))?;
184        for node in fragment.nodes()? {
185            parent.insert_before(&node, Some(&target.end))?;
186        }
187        Ok(())
188    }
189}