Skip to main content

rdom_core/accessor/
node_mut.rs

1//! `NodeMut<'a, Ext>` — the mutable accessor. Split out of `accessor/mod.rs`
2//! so the read surface and the write surface each stay a single-screen
3//! module; the `Dom -> accessor` constructors live in the parent.
4
5use super::*;
6
7// ─────────────────────────────────────────────────────────────────────
8//  NodeMut
9// ─────────────────────────────────────────────────────────────────────
10
11/// Mutable handle. All mutations go through this wrapper so future index
12/// maintenance hooks (Phase 4) have a single chokepoint.
13pub struct NodeMut<'a, Ext: 'static = ()> {
14    pub(crate) dom: &'a mut Dom<Ext>,
15    pub(crate) id: NodeId,
16}
17
18impl<'a, Ext> NodeMut<'a, Ext> {
19    pub fn id(&self) -> NodeId {
20        self.id
21    }
22
23    pub fn as_ref(&self) -> NodeRef<'_, Ext> {
24        NodeRef {
25            dom: self.dom,
26            id: self.id,
27        }
28    }
29
30    /// Reborrow the inner `&mut Dom<Ext>`. Lets downstream crates
31    /// (notably `rdom-parser`'s `NodeMutHtml` extension trait)
32    /// reach Dom-level operations that aren't surfaced on
33    /// `NodeMut` directly. The borrow shares the receiver's
34    /// lifetime.
35    pub fn dom_mut(&mut self) -> &mut Dom<Ext> {
36        self.dom
37    }
38
39    /// Consume this `NodeMut` and return the inner `&'a mut
40    /// Dom<Ext>`. Used by extension traits whose methods need to
41    /// operate on the Dom past the receiver's logical lifetime
42    /// (e.g. `set_outer_html`, which destroys the receiver).
43    pub fn into_dom_mut(self) -> &'a mut Dom<Ext> {
44        self.dom
45    }
46
47    /// Mutable borrow of the per-element extension data. `None` for
48    /// Text / Comment / Fragment. Pair of `NodeRef::ext()`.
49    pub fn ext_mut(&mut self) -> Option<&mut Ext> {
50        match &mut self.dom.get_node_mut(self.id)?.data {
51            NodeData::Element { ext, .. } => Some(ext),
52            _ => None,
53        }
54    }
55}
56
57impl<'a, Ext: 'static> NodeMut<'a, Ext> {
58    // ── Attributes ────────────────────────────────────────────────
59
60    pub fn set_attribute(&mut self, key: &str, value: &str) -> Result<()> {
61        self.dom.set_attribute(self.id, key, value)
62    }
63
64    pub fn remove_attribute(&mut self, key: &str) -> Result<bool> {
65        self.dom.remove_attribute(self.id, key)
66    }
67
68    pub fn toggle_attribute(&mut self, key: &str) -> Result<bool> {
69        self.dom.toggle_attribute(self.id, key)
70    }
71
72    pub fn set_id(&mut self, value: &str) -> Result<()> {
73        self.dom.set_id(self.id, value)
74    }
75
76    pub fn add_class(&mut self, class: &str) -> Result<()> {
77        self.dom.add_class(self.id, class)
78    }
79
80    pub fn remove_class(&mut self, class: &str) -> Result<bool> {
81        self.dom.remove_class(self.id, class)
82    }
83
84    pub fn toggle_class(&mut self, class: &str) -> Result<bool> {
85        self.dom.toggle_class(self.id, class)
86    }
87
88    pub fn replace_class(&mut self, old: &str, new: &str) -> Result<bool> {
89        self.dom.replace_class(self.id, old, new)
90    }
91
92    /// Replace the entire `class` attribute. DOM
93    /// `Element.className` setter.
94    ///
95    /// Writes the raw attribute string AND rebuilds the canonical
96    /// classList from whitespace-separated tokens — both reads
97    /// ([`NodeRef::class_name`] and [`NodeRef::class_list`]) reflect
98    /// the new value after this call. Empty `value` clears the
99    /// classList entirely.
100    pub fn set_class_name(&mut self, value: &str) -> Result<()> {
101        // `set_attribute("class", …)` is the WHATWG-canonical entry point
102        // and owns the attribute ↔ classList ↔ index sync; one call, one
103        // `AttributeChanged` record.
104        self.dom.set_attribute(self.id, "class", value)
105    }
106
107    /// Mutating handle for the element's class tokens. DOM
108    /// `Element.classList`.
109    ///
110    /// The returned wrapper holds a reborrowed `NodeMut`; drop the
111    /// wrapper to release the borrow before mutating other fields
112    /// on this element.
113    pub fn class_list_mut(&mut self) -> DomTokenListMut<'_, Ext> {
114        DomTokenListMut::new(NodeMut {
115            dom: &mut *self.dom,
116            id: self.id,
117        })
118    }
119
120    /// DOM `HTMLElement.dataset` mutator — write-side handle for
121    /// `data-*` attributes keyed by their camelCase form. The
122    /// returned wrapper holds a reborrowed `NodeMut`; drop it
123    /// before mutating other fields on this element.
124    pub fn dataset_mut(&mut self) -> DomStringMapMut<'_, Ext> {
125        DomStringMapMut::new(NodeMut {
126            dom: &mut *self.dom,
127            id: self.id,
128        })
129    }
130
131    /// DOM `HTMLElement.tabIndex` setter. Writes the integer value
132    /// to the `tabindex` attribute as its decimal string form.
133    pub fn set_tab_index(&mut self, value: i32) -> Result<()> {
134        self.set_attribute("tabindex", &value.to_string())
135    }
136
137    /// DOM `HTMLElement.hidden` setter. `true` writes the boolean
138    /// attribute (empty value); `false` removes it.
139    pub fn set_hidden(&mut self, value: bool) -> Result<()> {
140        if value {
141            self.set_attribute("hidden", "")
142        } else {
143            self.remove_attribute("hidden").map(|_| ())
144        }
145    }
146
147    /// DOM `HTMLElement.contentEditable` setter (HTML §6.8.1), matched
148    /// ASCII case-insensitively: `"inherit"` removes the attribute;
149    /// `"true"`, `"false"` and `"plaintext-only"` write that keyword
150    /// in lowercase; anything else is a [`DomError::Syntax`] and leaves
151    /// the attribute untouched.
152    pub fn set_content_editable(&mut self, value: &str) -> Result<()> {
153        if value.eq_ignore_ascii_case("inherit") {
154            return self.remove_attribute("contenteditable").map(|_| ());
155        }
156        match crate::ContentEditableState::from_attribute(value) {
157            Some(state) if !value.is_empty() => {
158                self.set_attribute("contenteditable", state.keyword())
159            }
160            _ => Err(DomError::Syntax(
161                "contentEditable must be true, false, plaintext-only or inherit",
162            )),
163        }
164    }
165
166    /// Toggle an attribute with optional force. DOM
167    /// `Element.toggleAttribute(qualifiedName, force?)`.
168    ///
169    /// - `force = Some(true)` → ensure present (empty string value
170    ///   if newly added); returns `true`.
171    /// - `force = Some(false)` → ensure absent; returns `false`.
172    /// - `force = None` → flip; returns the post-flip presence.
173    pub fn toggle_attribute_force(&mut self, name: &str, force: Option<bool>) -> Result<bool> {
174        match force {
175            Some(true) => {
176                if !self.as_ref().has_attribute(name) {
177                    self.set_attribute(name, "")?;
178                }
179                Ok(true)
180            }
181            Some(false) => {
182                self.remove_attribute(name)?;
183                Ok(false)
184            }
185            None => self.toggle_attribute(name),
186        }
187    }
188
189    // ── Tree mutation ─────────────────────────────────────────────
190
191    pub fn append_child(&mut self, child: NodeId) -> Result<()> {
192        self.dom.append_child(self.id, child)
193    }
194
195    pub fn prepend_child(&mut self, child: NodeId) -> Result<()> {
196        self.dom.prepend_child(self.id, child)
197    }
198
199    pub fn remove_child(&mut self, child: NodeId) -> Result<()> {
200        self.dom.remove_child(self.id, child)
201    }
202
203    pub fn replace_child(&mut self, old: NodeId, new: NodeId) -> Result<()> {
204        self.dom.replace_child(self.id, old, new)
205    }
206
207    pub fn insert_before(&mut self, new: NodeId, reference: Option<NodeId>) -> Result<()> {
208        self.dom.insert_before(self.id, new, reference)
209    }
210
211    pub fn insert_adjacent(&mut self, position: AdjacentPosition, new: NodeId) -> Result<()> {
212        self.dom.insert_adjacent(self.id, position, new)
213    }
214
215    pub fn clear_children(&mut self) -> Result<()> {
216        self.dom.clear_children(self.id)
217    }
218
219    // ── Variadic tree helpers (DOM `ChildNode` / `ParentNode`) ────
220
221    /// Append each item to the end of this node's child list, in
222    /// order. Text items create fresh text nodes. DOM
223    /// `ParentNode.append`.
224    pub fn append(&mut self, children: impl IntoIterator<Item = NodeOrString>) -> Result<()> {
225        let parent = self.id;
226        for item in children {
227            let new_id = match item {
228                NodeOrString::Node(n) => n,
229                NodeOrString::Text(s) => self.dom.create_text_node(&s),
230            };
231            self.dom.append_child(parent, new_id)?;
232        }
233        Ok(())
234    }
235
236    /// Insert each item at the start of this node's child list, in
237    /// order — the first item of `children` becomes the new first
238    /// child. DOM `ParentNode.prepend`.
239    pub fn prepend(&mut self, children: impl IntoIterator<Item = NodeOrString>) -> Result<()> {
240        let parent = self.id;
241        let reference = self.dom.get_node(parent).and_then(|n| n.first_child);
242        for item in children {
243            let new_id = match item {
244                NodeOrString::Node(n) => n,
245                NodeOrString::Text(s) => self.dom.create_text_node(&s),
246            };
247            self.dom.insert_before(parent, new_id, reference)?;
248        }
249        Ok(())
250    }
251
252    /// Insert each item as a sibling immediately before this node,
253    /// in order. DOM `ChildNode.before`.
254    ///
255    /// Silently no-ops when this node has no parent (browser-
256    /// faithful). Text items create fresh text nodes only when
257    /// insertion actually happens.
258    pub fn before(&mut self, siblings: impl IntoIterator<Item = NodeOrString>) -> Result<()> {
259        let id = self.id;
260        let parent = match self.dom.get_node(id).and_then(|n| n.parent) {
261            Some(p) => p,
262            None => return Ok(()),
263        };
264        for item in siblings {
265            let new_id = match item {
266                NodeOrString::Node(n) => n,
267                NodeOrString::Text(s) => self.dom.create_text_node(&s),
268            };
269            self.dom.insert_before(parent, new_id, Some(id))?;
270        }
271        Ok(())
272    }
273
274    /// Insert each item as a sibling immediately after this node,
275    /// in order. DOM `ChildNode.after`.
276    ///
277    /// Silently no-ops when this node has no parent (browser-
278    /// faithful).
279    pub fn after(&mut self, siblings: impl IntoIterator<Item = NodeOrString>) -> Result<()> {
280        let id = self.id;
281        if self.dom.get_node(id).and_then(|n| n.parent).is_none() {
282            return Ok(());
283        }
284        let mut cursor = id;
285        for item in siblings {
286            let new_id = match item {
287                NodeOrString::Node(n) => n,
288                NodeOrString::Text(s) => self.dom.create_text_node(&s),
289            };
290            self.dom
291                .insert_adjacent(cursor, AdjacentPosition::AfterEnd, new_id)?;
292            cursor = new_id;
293        }
294        Ok(())
295    }
296
297    /// Clear this node's children and append the new ones. DOM
298    /// `ParentNode.replaceChildren`.
299    pub fn replace_children(
300        &mut self,
301        children: impl IntoIterator<Item = NodeOrString>,
302    ) -> Result<()> {
303        let parent = self.id;
304        self.dom.clear_children(parent)?;
305        for item in children {
306            let new_id = match item {
307                NodeOrString::Node(n) => n,
308                NodeOrString::Text(s) => self.dom.create_text_node(&s),
309            };
310            self.dom.append_child(parent, new_id)?;
311        }
312        Ok(())
313    }
314
315    /// Replace this node with `siblings`, inserted at its position
316    /// in the parent, then detach this node. DOM
317    /// `ChildNode.replaceWith`.
318    ///
319    /// **Consumes `self`** — the receiver is detached from the
320    /// tree, so the handle is no longer usable. Silently no-ops
321    /// when this node has no parent.
322    ///
323    /// ```compile_fail
324    /// use rdom_core::Dom;
325    /// let mut dom: Dom = Dom::new();
326    /// let parent = dom.create_element("div");
327    /// let el = dom.create_element("span");
328    /// dom.append_child(parent, el).unwrap();
329    /// let nm = dom.node_mut(el);
330    /// nm.replace_with([]).unwrap();
331    /// let _ = nm.id();  // ERROR: nm was consumed
332    /// ```
333    pub fn replace_with(self, siblings: impl IntoIterator<Item = NodeOrString>) -> Result<()> {
334        let NodeMut { dom, id } = self;
335        let parent = match dom.get_node(id).and_then(|n| n.parent) {
336            Some(p) => p,
337            None => return Ok(()),
338        };
339        for item in siblings {
340            let new_id = match item {
341                NodeOrString::Node(n) => n,
342                NodeOrString::Text(s) => dom.create_text_node(&s),
343            };
344            dom.insert_before(parent, new_id, Some(id))?;
345        }
346        dom.remove_child(parent, id)?;
347        Ok(())
348    }
349
350    /// Detach this node from its parent. DOM `ChildNode.remove`.
351    ///
352    /// **Consumes `self`**. Silently no-ops on parentless nodes.
353    /// The node remains in the arena — it's just orphaned.
354    ///
355    /// ```compile_fail
356    /// use rdom_core::Dom;
357    /// let mut dom: Dom = Dom::new();
358    /// let parent = dom.create_element("div");
359    /// let el = dom.create_element("span");
360    /// dom.append_child(parent, el).unwrap();
361    /// let nm = dom.node_mut(el);
362    /// nm.remove_self().unwrap();
363    /// let _ = nm.id();  // ERROR: nm was consumed
364    /// ```
365    pub fn remove_self(self) -> Result<()> {
366        let NodeMut { dom, id } = self;
367        if let Some(parent) = dom.get_node(id).and_then(|n| n.parent) {
368            dom.remove_child(parent, id)?;
369        }
370        Ok(())
371    }
372
373    /// Set Text/Comment node's own data. Errors on Element/Fragment.
374    /// Fires `Mutation::CharacterDataChanged`.
375    pub fn set_node_value(&mut self, data: &str) -> Result<()> {
376        let id = self.id;
377        let old = match &self.dom.node_or_err(id)?.data {
378            NodeData::Text { data: d } | NodeData::Comment { data: d } => d.clone(),
379            NodeData::Element { .. } => {
380                return Err(DomError::WrongNodeType {
381                    expected: "Text or Comment",
382                    got: NodeType::Element,
383                });
384            }
385            NodeData::Fragment => {
386                return Err(DomError::WrongNodeType {
387                    expected: "Text or Comment",
388                    got: NodeType::Fragment,
389                });
390            }
391        };
392        if old == data {
393            return Ok(());
394        }
395        match &mut self.dom.node_mut_or_err(id)?.data {
396            NodeData::Text { data: d } | NodeData::Comment { data: d } => {
397                *d = data.to_string();
398            }
399            _ => unreachable!("type-checked above"),
400        }
401        self.dom
402            .fire_mutation(crate::Mutation::CharacterDataChanged {
403                id,
404                old,
405                new: data.to_string(),
406            });
407        Ok(())
408    }
409
410    /// `CharacterData.data` setter — alias for `set_node_value` on Text/
411    /// Comment.
412    pub fn set_data(&mut self, data: &str) -> Result<()> {
413        self.set_node_value(data)
414    }
415
416    /// Replace the byte range `[start..end)` of a Text/Comment node's
417    /// data with `replacement`. Convenience over `set_node_value` for
418    /// editors that want byte-precise mutations (insert, delete,
419    /// replace a range) without assembling the full new string.
420    ///
421    /// Errors:
422    /// - `WrongNodeType` — node isn't Text/Comment.
423    /// - `InvalidOffset` — `start` or `end` overshoot the data length
424    ///   or land mid-UTF-8-codepoint. Editors that derive offsets
425    ///   from `Position` / grapheme walks won't hit this.
426    ///
427    /// Fires `Mutation::CharacterDataChanged` (via `set_node_value`).
428    pub fn edit_text(&mut self, start: usize, end: usize, replacement: &str) -> Result<()> {
429        let id = self.id;
430        let data = match &self.dom.node_or_err(id)?.data {
431            NodeData::Text { data: d } | NodeData::Comment { data: d } => d.clone(),
432            NodeData::Element { .. } => {
433                return Err(DomError::WrongNodeType {
434                    expected: "Text or Comment",
435                    got: NodeType::Element,
436                });
437            }
438            NodeData::Fragment => {
439                return Err(DomError::WrongNodeType {
440                    expected: "Text or Comment",
441                    got: NodeType::Fragment,
442                });
443            }
444        };
445        if start > data.len() || !data.is_char_boundary(start) {
446            return Err(DomError::InvalidOffset {
447                node: id,
448                offset: start,
449            });
450        }
451        let end = end.max(start);
452        if end > data.len() || !data.is_char_boundary(end) {
453            return Err(DomError::InvalidOffset {
454                node: id,
455                offset: end,
456            });
457        }
458        let mut new_data = String::with_capacity(data.len() - (end - start) + replacement.len());
459        new_data.push_str(&data[..start]);
460        new_data.push_str(replacement);
461        new_data.push_str(&data[end..]);
462        self.set_node_value(&new_data)
463    }
464}
465
466impl<'a, Ext: Default> NodeMut<'a, Ext> {
467    /// `textContent` setter — replace all children of this Element/Fragment
468    /// with a single Text node. Errors on Text/Comment (use `set_data`).
469    pub fn set_text_content(&mut self, text: &str) -> Result<()> {
470        self.dom.set_text_content(self.id, text)
471    }
472}