stardom_web/
lib.rs

1use std::{
2    cell::RefCell,
3    rc::{Rc, Weak},
4    thread_local,
5};
6
7use stardom_nodes::{EventKey, Node, NodeType};
8use wasm_bindgen::{intern, prelude::*};
9
10thread_local! {
11    static DOCUMENT: web_sys::Document = web_sys::window()
12        .unwrap()
13        .document()
14        .unwrap();
15}
16
17pub fn document() -> web_sys::Document {
18    DOCUMENT.with(Clone::clone)
19}
20
21#[derive(Clone, Debug)]
22pub struct DomNode(Rc<Inner>);
23
24type WeakNode = Weak<Inner>;
25type EventClosure = Closure<dyn Fn(web_sys::Event)>;
26
27#[derive(Debug)]
28struct Inner {
29    native: web_sys::Node,
30    ty: NodeType,
31
32    parent: RefCell<Option<WeakNode>>,
33    children: RefCell<Vec<DomNode>>,
34    events: RefCell<Vec<EventClosure>>,
35}
36
37impl DomNode {
38    fn new(native: web_sys::Node, ty: NodeType) -> Self {
39        Self(Rc::new(Inner {
40            native,
41            ty,
42            parent: RefCell::default(),
43            children: RefCell::default(),
44            events: RefCell::default(),
45        }))
46    }
47
48    pub fn native(&self) -> &web_sys::Node {
49        &self.0.native
50    }
51
52    fn is_virtual(&self) -> bool {
53        matches!(self.ty(), NodeType::Fragment | NodeType::Raw)
54    }
55
56    fn from_native(native: web_sys::Node) -> Option<Self> {
57        let ty = if native.has_type::<web_sys::Element>() {
58            NodeType::Element
59        } else if native.has_type::<web_sys::Text>() {
60            NodeType::Text
61        } else if native.has_type::<web_sys::Comment>() {
62            NodeType::Fragment
63        } else {
64            return None;
65        };
66
67        Some(Self::new(native, ty))
68    }
69
70    fn native_parent(&self) -> Option<web_sys::Node> {
71        self.0.native.parent_node()
72    }
73
74    fn native_target(&self) -> Option<web_sys::Node> {
75        if self.is_virtual() {
76            self.native_parent()
77        } else {
78            Some(self.0.native.clone())
79        }
80    }
81
82    fn first_node(&self) -> web_sys::Node {
83        if self.is_virtual() {
84            let children = self.0.children.borrow();
85            if let Some(first) = children.first() {
86                return first.0.native.clone();
87            }
88        }
89
90        self.0.native.clone()
91    }
92
93    pub fn mount_to_native(&self, target: &web_sys::Node, before: Option<&web_sys::Node>) {
94        if self.is_virtual() {
95            let children = self.0.children.borrow();
96            for child in &*children {
97                child.mount_to_native(target, before);
98            }
99        }
100
101        target.insert_before(&self.0.native, before).unwrap();
102    }
103
104    pub fn remove_from_native(&self, target: &web_sys::Node) {
105        if self.is_virtual() {
106            let children = self.0.children.borrow();
107            for child in &*children {
108                child.remove_from_native(target);
109            }
110        }
111
112        target.remove_child(&self.0.native).unwrap();
113    }
114}
115
116impl Node for DomNode {
117    fn element(namespace: Option<&str>, name: &str) -> Self {
118        let native = DOCUMENT
119            .with(|document| {
120                if namespace.is_some() {
121                    document.create_element_ns(namespace, name)
122                } else {
123                    document.create_element(name)
124                }
125            })
126            .unwrap();
127
128        Self::new(native.unchecked_into(), NodeType::Element)
129    }
130
131    fn text() -> Self {
132        let native = web_sys::Text::new().unwrap();
133
134        Self::new(native.unchecked_into(), NodeType::Text)
135    }
136
137    fn fragment() -> Self {
138        let native = web_sys::Comment::new().unwrap();
139
140        Self::new(native.unchecked_into(), NodeType::Fragment)
141    }
142
143    fn raw() -> Self {
144        let native = web_sys::Comment::new().unwrap();
145
146        Self::new(native.unchecked_into(), NodeType::Raw)
147    }
148
149    fn ty(&self) -> NodeType {
150        self.0.ty
151    }
152
153    fn parent(&self) -> Option<Self> {
154        self.0
155            .parent
156            .borrow()
157            .as_ref()
158            .and_then(Weak::upgrade)
159            .map(DomNode)
160    }
161
162    fn children(&self) -> Vec<Self> {
163        self.0.children.borrow().clone()
164    }
165
166    fn next_sibling(&self) -> Option<Self> {
167        let parent = self.parent()?;
168        let children = parent.0.children.borrow();
169        children
170            .iter()
171            .position(|node| node == self)
172            .and_then(|idx| children.get(idx + 1).cloned())
173    }
174
175    fn insert(&self, child: &Self, before: Option<&Self>) {
176        let mut children = self.0.children.borrow_mut();
177        let idx = if let Some(before) = before {
178            children
179                .iter()
180                .position(|node| node == before)
181                .expect("not a parent of insertion point node")
182        } else {
183            children.len()
184        };
185        children.insert(idx, child.clone());
186
187        child.0.parent.borrow_mut().replace(Rc::downgrade(&self.0));
188
189        if let Some(target) = self.native_target() {
190            let before = before.map(|node| node.first_node()).or_else(|| {
191                if self.is_virtual() {
192                    self.native().next_sibling()
193                } else {
194                    None
195                }
196            });
197
198            child.mount_to_native(&target, before.as_ref());
199        }
200    }
201
202    fn remove(&self, child: &Self) {
203        let mut children = self.0.children.borrow_mut();
204        let idx = children
205            .iter()
206            .position(|node| node == child)
207            .expect("not a parent of child node");
208        children.remove(idx);
209
210        child.0.parent.borrow_mut().take();
211
212        if let Some(target) = self.native_target() {
213            child.remove_from_native(&target);
214        }
215    }
216
217    fn set_text(&self, content: &str) {
218        match self.ty() {
219            NodeType::Text => {
220                self.0.native.set_text_content(Some(content));
221            }
222            NodeType::Raw => {
223                for child in self.0.children.borrow().clone() {
224                    self.remove(&child);
225                }
226
227                let range = web_sys::Range::new().unwrap();
228                let doc = range.create_contextual_fragment(content).unwrap();
229                let native_nodes = doc.child_nodes();
230
231                for i in 0..native_nodes.length() {
232                    let native = native_nodes.get(i).unwrap();
233                    if let Some(node) = Self::from_native(native) {
234                        self.insert(&node, None);
235                    }
236                }
237            }
238            _ => panic!("can only set text content of text or raw nodes"),
239        }
240    }
241
242    fn attr(&self, name: &str) -> Option<String> {
243        if self.ty() == NodeType::Element {
244            self.0
245                .native
246                .unchecked_ref::<web_sys::Element>()
247                .get_attribute(name)
248        } else {
249            panic!("attributes only exist on element nodes");
250        }
251    }
252
253    fn set_attr(&self, name: &str, value: &str) {
254        if self.ty() == NodeType::Element {
255            self.0
256                .native
257                .unchecked_ref::<web_sys::Element>()
258                .set_attribute(intern(name), value)
259                .unwrap();
260        } else {
261            panic!("attributes only exist on element nodes");
262        }
263    }
264
265    fn remove_attr(&self, name: &str) {
266        if self.ty() == NodeType::Element {
267            self.0
268                .native
269                .unchecked_ref::<web_sys::Element>()
270                .remove_attribute(intern(name))
271                .unwrap();
272        } else {
273            panic!("attributes only exist on element nodes");
274        }
275    }
276
277    fn event<E, F>(&self, event: &E, f: F)
278    where
279        E: EventKey,
280        F: Fn(E::Event) + 'static,
281    {
282        if self.ty() != NodeType::Element {
283            panic!("can only set events on element nodes");
284        }
285
286        let closure = EventClosure::new(move |value: web_sys::Event| {
287            let value = value
288                .dyn_into::<E::Event>()
289                .expect("invalid event type cast");
290            f(value);
291        });
292
293        self.0
294            .native
295            .add_event_listener_with_callback(
296                intern(event.name()),
297                closure.as_ref().unchecked_ref(),
298            )
299            .unwrap();
300
301        self.0.events.borrow_mut().push(closure);
302    }
303}
304
305impl PartialEq for DomNode {
306    fn eq(&self, other: &Self) -> bool {
307        Rc::ptr_eq(&self.0, &other.0)
308    }
309}
310
311impl Eq for DomNode {}
312
313#[cfg(all(test, target_family = "wasm"))]
314mod tests {
315    use crate::{DomNode as N, Node};
316    use wasm_bindgen_test::*;
317
318    wasm_bindgen_test_configure!(run_in_browser);
319
320    #[wasm_bindgen_test]
321    fn fragment_insertion() {
322        let root = N::element(None, "div");
323
324        let last = N::text();
325        root.insert(&last, None);
326
327        let outer = N::fragment();
328        let inner = N::text();
329
330        root.insert(&outer, Some(&last));
331        outer.insert(&inner, None);
332
333        assert_eq!(inner.native().next_sibling(), Some(last.native().clone()));
334    }
335}