Skip to main content

threadloom_dom/
lib.rs

1#![allow(warnings)]
2pub use js_sys;
3pub use reqwasm;
4use std::cell::RefCell;
5use std::collections::HashMap;
6use std::rc::Rc;
7use threadloom_core::{
8    create_effect, run_effects, take_pending_boundaries, AttributeValue, Boundary, NodeId, View,
9};
10pub use wasm_bindgen;
11use wasm_bindgen::prelude::*;
12pub use wasm_bindgen_futures;
13pub use web_sys;
14use web_sys::{Document, Element, Node};
15
16thread_local! {
17    static BOUNDARIES: RefCell<HashMap<NodeId, (Node, Rc<RefCell<dyn FnMut() -> View>>)>> = RefCell::new(HashMap::new());
18    pub static ROUTER_SETTER: std::cell::RefCell<Option<threadloom_core::WriteSignal<String>>> = std::cell::RefCell::new(None);
19}
20
21pub mod storage;
22pub mod ws;
23
24pub fn mount(view: View, container: &Element) -> Result<(), JsValue> {
25    let window = web_sys::window().expect("no global `window` exists");
26    let document = window.document().expect("should have a document on window");
27
28    let node = render_view(&document, view)?;
29    container.append_child(&node)?;
30    Ok(())
31}
32
33pub fn mount_to_body(view: View) {
34    let window = web_sys::window().unwrap();
35    let document = window.document().unwrap();
36    let body = document.body().unwrap();
37    
38    setup_global_listeners(&document);
39
40    let node = render_view(&document, view).unwrap();
41    body.append_child(&node).unwrap();
42}
43
44thread_local! {
45    static ELEMENT_CACHE: std::cell::RefCell<std::collections::HashMap<String, web_sys::Element>> = std::cell::RefCell::new(std::collections::HashMap::new());
46    static STRING_CACHE: std::cell::RefCell<std::collections::HashMap<String, wasm_bindgen::JsValue>> = std::cell::RefCell::new(std::collections::HashMap::new());
47    
48    static GLOBAL_EVENTS: std::cell::RefCell<std::collections::HashMap<String, std::collections::HashMap<u32, std::rc::Rc<dyn Fn(web_sys::Event)>>>> = std::cell::RefCell::new(std::collections::HashMap::new());
49    static NEXT_EVENT_ID: std::cell::Cell<u32> = std::cell::Cell::new(1);
50    static GLOBAL_LISTENERS_SETUP: std::cell::Cell<bool> = std::cell::Cell::new(false);
51}
52
53fn setup_global_listeners(document: &Document) {
54    if GLOBAL_LISTENERS_SETUP.with(|s| s.get()) { return; }
55    GLOBAL_LISTENERS_SETUP.with(|s| s.set(true));
56
57    let window = web_sys::window().unwrap();
58    let events = ["click", "input", "change", "keydown"];
59    for event_name in events {
60        let event_name_str = event_name.to_string();
61        let closure = wasm_bindgen::closure::Closure::wrap(Box::new(move |e: web_sys::Event| {
62            use wasm_bindgen::JsCast;
63            if let Some(target) = e.target() {
64                if let Ok(el) = target.dyn_into::<web_sys::Element>() {
65                    let attr_name = format!("data-th-evt-{}", event_name_str);
66                    let mut current = Some(el);
67                    while let Some(node) = current {
68                        if let Some(id_str) = node.get_attribute(&attr_name) {
69                            if let Ok(id) = id_str.parse::<u32>() {
70                                let cb = GLOBAL_EVENTS.with(|e| {
71                                    e.borrow().get(&event_name_str).and_then(|m| m.get(&id).cloned())
72                                });
73                                if let Some(cb) = cb {
74                                    cb(e.clone());
75                                    let _ = crate::tick();
76                                }
77                            }
78                        }
79                        current = node.parent_element();
80                    }
81                }
82            }
83        }) as Box<dyn FnMut(web_sys::Event)>);
84        window.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref()).unwrap();
85        closure.forget();
86    }
87}
88
89fn get_interned_string(s: &str) -> wasm_bindgen::JsValue {
90    STRING_CACHE.with(|c| {
91        let mut cache = c.borrow_mut();
92        if let Some(val) = cache.get(s) {
93            val.clone()
94        } else {
95            let val = wasm_bindgen::JsValue::from_str(s);
96            cache.insert(s.to_string(), val.clone());
97            val
98        }
99    })
100}
101
102fn render_view(document: &Document, view: View) -> Result<Node, JsValue> {
103    match view {
104        View::Text(text) => Ok(document.create_text_node(&text).into()),
105        View::RcText(text) => Ok(document.create_text_node(&text).into()),
106        View::DynamicText(f) => {
107            let text = f();
108            let node = document.create_text_node(&text);
109            let node_clone = node.clone();
110            create_effect(move || {
111                let new_text = f();
112                node_clone.set_data(&new_text);
113            });
114            Ok(node.into())
115        }
116        View::DynamicRcText(f) => {
117            let text = f();
118            let node = document.create_text_node(&text);
119            let node_clone = node.clone();
120            create_effect(move || {
121                let new_text = f();
122                node_clone.set_data(&new_text);
123            });
124            Ok(node.into())
125        }
126        View::Element {
127            tag,
128            attrs,
129            children,
130        } => {
131            let tag_str: &str = tag.as_ref();
132            let is_svg = tag == "svg" || tag == "path" || tag == "circle" || tag == "rect" || tag == "g" || tag == "line";
133            
134            let el = ELEMENT_CACHE.with(|c| {
135                let mut cache = c.borrow_mut();
136                if let Some(template) = cache.get(tag_str) {
137                    template.clone_node().unwrap().unchecked_into::<web_sys::Element>()
138                } else {
139                    let el = if is_svg {
140                        document.create_element_ns(Some("http://www.w3.org/2000/svg"), tag.as_ref()).unwrap()
141                    } else {
142                        document.create_element(tag.as_ref()).unwrap()
143                    };
144                    cache.insert(tag.to_string(), el.clone());
145                    el
146                }
147            });
148
149            for (k, v) in attrs {
150                let k_interned = wasm_bindgen::intern(&k);
151                match v {
152                    AttributeValue::String(s) => {
153                        if k == "class" {
154                            let s_interned = wasm_bindgen::intern(&s);
155                            el.set_class_name(s_interned);
156                        } else if k == "id" {
157                            el.set_id(&s);
158                        } else {
159                            let _ = el.set_attribute(k_interned, &s);
160                        }
161                    }
162                    AttributeValue::RcString(s) => {
163                        if k == "class" {
164                            let s_interned = wasm_bindgen::intern(&s);
165                            el.set_class_name(s_interned);
166                        } else if k == "id" {
167                            el.set_id(&s);
168                        } else {
169                            let _ = el.set_attribute(k_interned, &s);
170                        }
171                    }
172                    AttributeValue::Bool(b) => {
173                        if b {
174                            el.set_attribute(k_interned, "")?;
175                        }
176                    }
177                    AttributeValue::Dynamic(f) => {
178                        let el_clone = el.clone();
179                        let k_clone = k.clone();
180                        let f_rc = f.clone();
181                        let val = f_rc();
182                        if let AttributeValue::String(s) = &val {
183                            let _ = el.set_attribute(k_interned, s);
184                        } else if let AttributeValue::RcString(s) = &val {
185                            let _ = el.set_attribute(k_interned, s);
186                        }
187                        create_effect(move || {
188                            let val = f_rc();
189                            if let AttributeValue::String(s) = val {
190                                let _ = el_clone.set_attribute(&k_clone, &s);
191                            } else if let AttributeValue::RcString(s) = val {
192                                let _ = el_clone.set_attribute(&k_clone, &s);
193                            }
194                        });
195                    }
196                    AttributeValue::Event(cb) => {
197                        use wasm_bindgen::JsCast;
198                        let attr_key = format!("data-th-evt-{}", k);
199                        let js_key = wasm_bindgen::JsValue::from_str(&attr_key);
200                        if js_sys::Reflect::has(&el, &js_key).unwrap_or(false) == false {
201                            let _ =
202                                js_sys::Reflect::set(&el, &js_key, &wasm_bindgen::JsValue::TRUE);
203                            let cb_rc = cb.clone();
204                            let closure = wasm_bindgen::closure::Closure::wrap(Box::new(move || {
205                                cb_rc();
206                                let _ = crate::tick();
207                            })
208                                as Box<dyn FnMut()>);
209                            el.add_event_listener_with_callback(
210                                k_interned,
211                                closure.as_ref().unchecked_ref(),
212                            )?;
213                            closure.forget();
214                        }
215                    }
216                    AttributeValue::EventObj(cb) => {
217                        let id = NEXT_EVENT_ID.with(|id| {
218                            let val = id.get();
219                            id.set(val + 1);
220                            val
221                        });
222                        GLOBAL_EVENTS.with(|e| {
223                            let mut map = e.borrow_mut();
224                            map.entry(k.to_string()).or_default().insert(id, cb.clone());
225                        });
226                        let attr_name = format!("data-th-evt-{}", k);
227                        let attr_name_interned = get_interned_string(&attr_name);
228                        let id_str = get_interned_string(&id.to_string());
229                        
230                        let _ = js_sys::Reflect::set(&el, &attr_name_interned, &id_str);
231                        let _ = el.set_attribute(&attr_name, &id.to_string());
232                    }
233                }
234            }
235            for child in children {
236                let child_node = render_view(document, child)?;
237                el.append_child(&child_node)?;
238            }
239            Ok(el.into())
240        }
241        View::DynamicNode(boundary) => {
242            let view = boundary.id.track(|| {
243                let mut compute = boundary.compute.borrow_mut();
244                compute()
245            });
246            let node = render_view(document, view)?;
247
248            let compute_rc = boundary.compute.clone();
249            BOUNDARIES.with(|b| {
250                b.borrow_mut()
251                    .insert(boundary.id, (node.clone(), compute_rc));
252            });
253
254            Ok(node)
255        }
256        View::Fragment(children) => {
257            let el = document.create_element("div")?;
258            for child in children {
259                let child_node = render_view(document, child)?;
260                el.append_child(&child_node)?;
261            }
262            Ok(el.into())
263        }
264        View::KeyedList(children) => {
265            let el = document.create_element("div")?;
266            el.set_attribute("data-th-keyed-list", "true")?;
267            use wasm_bindgen::JsCast;
268            for (key, child) in children {
269                let child_node = render_view(document, child)?;
270                let child_el = child_node.unchecked_ref::<Element>();
271                let _ = child_el.set_attribute("data-th-key", &key);
272                el.append_child(&child_node)?;
273            }
274            Ok(el.into())
275        }
276        View::None => Ok(document.create_text_node("").into()),
277    }
278}
279
280fn patch_node(document: &Document, dom_node: &Node, new_view: View) -> Result<Node, JsValue> {
281    match new_view {
282        View::Text(text) => {
283            if dom_node.node_type() == Node::TEXT_NODE {
284                if let Some(text_content) = dom_node.text_content() {
285                    if text_content != text {
286                        let _ = dom_node.set_text_content(Some(&text));
287                    }
288                }
289                Ok(dom_node.clone())
290            } else {
291                let new_node: Node = document.create_text_node(&text).into();
292                if let Some(parent) = dom_node.parent_node() {
293                    let _ = parent.replace_child(&new_node, dom_node);
294                }
295                Ok(new_node)
296            }
297        }
298        View::RcText(text) => {
299            if dom_node.node_type() == Node::TEXT_NODE {
300                if let Some(text_content) = dom_node.text_content() {
301                    if text_content != *text {
302                        let _ = dom_node.set_text_content(Some(&text));
303                    }
304                }
305                Ok(dom_node.clone())
306            } else {
307                let new_node: Node = document.create_text_node(&text).into();
308                if let Some(parent) = dom_node.parent_node() {
309                    let _ = parent.replace_child(&new_node, dom_node);
310                }
311                Ok(new_node)
312            }
313        }
314        View::DynamicText(_) => {
315            if dom_node.node_type() == Node::TEXT_NODE {
316                Ok(dom_node.clone())
317            } else {
318                let new_node = render_view(document, new_view)?;
319                if let Some(parent) = dom_node.parent_node() {
320                    let _ = parent.replace_child(&new_node, dom_node);
321                }
322                Ok(new_node)
323            }
324        }
325        View::DynamicRcText(_) => {
326            if dom_node.node_type() == Node::TEXT_NODE {
327                Ok(dom_node.clone())
328            } else {
329                let new_node = render_view(document, new_view)?;
330                if let Some(parent) = dom_node.parent_node() {
331                    let _ = parent.replace_child(&new_node, dom_node);
332                }
333                Ok(new_node)
334            }
335        }
336        View::DynamicNode(_) => {
337            Ok(dom_node.clone())
338        }
339        View::Element {
340            tag,
341            attrs,
342            children,
343        } => {
344            if dom_node.node_type() == Node::ELEMENT_NODE {
345                if let Some(el) = dom_node.dyn_ref::<Element>() {
346                    if el.tag_name().eq_ignore_ascii_case(&tag) {
347                        for (k, v) in attrs {
348                            let k_interned = wasm_bindgen::intern(&k);
349                            match v {
350                                AttributeValue::String(s) => {
351                                    if k == "class" {
352                                        let s_interned = wasm_bindgen::intern(&s);
353                                        let _ = el.set_class_name(s_interned);
354                                    } else if k == "id" {
355                                        let _ = el.set_id(&s);
356                                    } else {
357                                        if el.get_attribute(k_interned).as_deref() != Some(&*s) {
358                                            let _ = el.set_attribute(k_interned, &s);
359                                        }
360                                    }
361                                }
362                                AttributeValue::RcString(s) => {
363                                    if k == "class" {
364                                        let s_interned = wasm_bindgen::intern(&s);
365                                        let _ = el.set_class_name(s_interned);
366                                    } else if k == "id" {
367                                        let _ = el.set_id(&s);
368                                    } else {
369                                        if el.get_attribute(k_interned).as_deref() != Some(&**s) {
370                                            let _ = el.set_attribute(k_interned, &s);
371                                        }
372                                    }
373                                }
374                                AttributeValue::Bool(b) => {
375                                    if b {
376                                        if !el.has_attribute(&k) {
377                                            let _ = el.set_attribute(&k, "");
378                                        }
379                                    } else {
380                                        if el.has_attribute(&k) {
381                                            let _ = el.remove_attribute(&k);
382                                        }
383                                    }
384                                }
385                                AttributeValue::Dynamic(f) => {
386                                    let attr_key = format!("data-th-dyn-{}", k);
387                                    if !el.has_attribute(&attr_key) {
388                                        let _ = el.set_attribute(&attr_key, "");
389                                        let el_clone = el.clone();
390                                        let k_clone = k.clone();
391                                        let f_rc = f.clone();
392                                        let val = f_rc();
393                                        if let AttributeValue::String(s) = &val {
394                                            let _ = el.set_attribute(&k, s);
395                                        }
396                                        create_effect(move || {
397                                            let val = f_rc();
398                                            if let AttributeValue::String(s) = val {
399                                                let _ = el_clone.set_attribute(&k_clone, &s);
400                                            }
401                                        });
402                                    }
403                                }
404                                AttributeValue::Event(cb) => {
405                                    let attr_key = format!("data-th-evt-{}", k);
406                                    let js_key = wasm_bindgen::JsValue::from_str(&attr_key);
407                                    if js_sys::Reflect::has(&el, &js_key).unwrap_or(false) == false
408                                    {
409                                        let _ = js_sys::Reflect::set(
410                                            &el,
411                                            &js_key,
412                                            &wasm_bindgen::JsValue::TRUE,
413                                        );
414                                        let cb_rc = cb.clone();
415                                        let closure = wasm_bindgen::closure::Closure::wrap(
416                                            Box::new(move || {
417                                                cb_rc();
418                                                let _ = crate::tick();
419                                            })
420                                                as Box<dyn FnMut()>,
421                                        );
422                                        let _ = el.add_event_listener_with_callback(
423                                            k_interned,
424                                            closure.as_ref().unchecked_ref(),
425                                        );
426                                        closure.forget();
427                                    }
428                                }
429                                AttributeValue::EventObj(_) => {
430                                    // Handled globally via attributes
431                                }
432                            }
433                        }
434
435                        let mut current_child = el.first_child();
436                        for child_view in children {
437                            if let Some(child_node) = current_child.clone() {
438                                let _ = patch_node(document, &child_node, child_view)?;
439                                current_child = child_node.next_sibling();
440                            } else {
441                                let new_child = render_view(document, child_view)?;
442                                el.append_child(&new_child)?;
443                            }
444                        }
445
446                        while let Some(child_node) = current_child {
447                            let next = child_node.next_sibling();
448                            let _ = el.remove_child(&child_node);
449                            current_child = next;
450                        }
451
452                        return Ok(dom_node.clone());
453                    }
454                }
455            }
456
457            let new_node = render_view(
458                document,
459                View::Element {
460                    tag,
461                    attrs,
462                    children,
463                },
464            )?;
465            if let Some(parent) = dom_node.parent_node() {
466                parent.replace_child(&new_node, dom_node)?;
467            }
468            Ok(new_node)
469        }
470        _ => {
471            let new_node = render_view(document, new_view)?;
472            if let Some(parent) = dom_node.parent_node() {
473                parent.replace_child(&new_node, dom_node)?;
474            }
475            Ok(new_node)
476        }
477    }
478}
479
480pub fn tick() -> Result<(), JsValue> {
481    let window = web_sys::window().expect("no global `window` exists");
482    let document = window.document().expect("should have a document on window");
483
484    // run_effects() re-runs any create_effect closures whose signals changed,
485    // including dynamic attribute effects registered during render.
486    run_effects();
487
488    let pending = take_pending_boundaries();
489
490    let mut boundary_updates = Vec::new();
491
492    for id in pending {
493        let entry = BOUNDARIES.with(|b| b.borrow().get(&id).cloned());
494        if let Some((old_node, compute)) = entry {
495            let view = id.track(|| {
496                let mut comp = compute.borrow_mut();
497                comp()
498            });
499
500            let mut handled = false;
501            if let View::KeyedList(children) = &view {
502                use wasm_bindgen::JsCast;
503                if let Some(old_el) = old_node.dyn_ref::<web_sys::Element>() {
504                    if old_el.has_attribute("data-th-keyed-list") {
505                        handled = true;
506
507                        let mut old_nodes = std::collections::HashMap::new();
508                        let mut old_keys_in_order = Vec::new();
509                        let mut current_child = old_el.first_child();
510                        while let Some(child) = current_child {
511                            if let Some(child_el) = child.dyn_ref::<web_sys::Element>() {
512                                if let Some(key) = child_el.get_attribute("data-th-key") {
513                                    old_keys_in_order.push(key.clone());
514                                    old_nodes.insert(key, child.clone());
515                                }
516                            }
517                            current_child = child.next_sibling();
518                        }
519
520                        if children.is_empty() {
521                            let _ = old_el.set_text_content(Some(""));
522                            boundary_updates.push((id, old_node.clone(), compute.clone()));
523                            continue;
524                        }
525
526                        let mut is_append_only = false;
527                        if children.len() >= old_keys_in_order.len() {
528                            is_append_only = true;
529                            for (i, old_key) in old_keys_in_order.iter().enumerate() {
530                                if &children[i].0 != old_key {
531                                    is_append_only = false;
532                                    break;
533                                }
534                            }
535                        }
536
537                        if is_append_only {
538                            // Fast path: skip prefix diffing entirely, just append new nodes
539                            let fragment = document.create_document_fragment();
540                            for (key, child_view) in
541                                children.into_iter().skip(old_keys_in_order.len())
542                            {
543                                let new_child = render_view(&document, child_view.clone())?;
544                                let child_el = new_child.unchecked_ref::<web_sys::Element>();
545                                let _ = child_el.set_attribute("data-th-key", &key);
546                                let _ = fragment.append_child(&new_child);
547                            }
548                            let _ = old_el.append_child(&fragment);
549                        } else {
550                            // Slow path: full keyed reconciliation
551                            let mut current_dom_node = old_el.first_child();
552                            for (key, child_view) in children {
553                                let node_to_place = if let Some(existing_node) =
554                                    old_nodes.remove(key)
555                                {
556                                    patch_node(&document, &existing_node, child_view.clone())?
557                                } else {
558                                    let new_child = render_view(&document, child_view.clone())?;
559                                    let child_el = new_child.unchecked_ref::<web_sys::Element>();
560                                    let _ = child_el.set_attribute("data-th-key", key);
561                                    new_child
562                                };
563
564                                if let Some(current) = current_dom_node.clone() {
565                                    if current != node_to_place {
566                                        if current.next_sibling().as_ref() == Some(&node_to_place) {
567                                            current_dom_node = node_to_place.next_sibling();
568                                        } else {
569                                            let _ = old_el
570                                                .insert_before(&node_to_place, Some(&current));
571                                        }
572                                    } else {
573                                        current_dom_node = current.next_sibling();
574                                    }
575                                } else {
576                                    let _ = old_el.append_child(&node_to_place);
577                                }
578                            }
579
580                            for (_, old_child) in old_nodes {
581                                let _ = old_el.remove_child(&old_child);
582                            }
583                        }
584
585                        boundary_updates.push((id, old_node.clone(), compute.clone()));
586                    }
587                }
588            }
589
590            if !handled {
591                let new_node = render_view(&document, view)?;
592                if let Some(parent) = old_node.parent_node() {
593                    parent.replace_child(&new_node, &old_node)?;
594                    boundary_updates.push((id, new_node, compute));
595                }
596            }
597        }
598    }
599
600    BOUNDARIES.with(|b| {
601        let mut boundaries = b.borrow_mut();
602        for (id, new_node, compute) in boundary_updates {
603            boundaries.insert(id, (new_node, compute));
604        }
605    });
606
607    Ok(())
608}
609
610#[macro_export]
611macro_rules! get_value {
612    ($id:expr) => {{
613        let mut val = String::new();
614        if let Some(w) = $crate::web_sys::window() {
615            if let Some(d) = w.document() {
616                if let Some(el) = d.get_element_by_id($id) {
617                    use $crate::wasm_bindgen::JsCast;
618                    if let Ok(input_el) = el.clone().dyn_into::<$crate::web_sys::HtmlInputElement>()
619                    {
620                        val = input_el.value();
621                    } else if let Ok(textarea_el) = el
622                        .clone()
623                        .dyn_into::<$crate::web_sys::HtmlTextAreaElement>()
624                    {
625                        val = textarea_el.value();
626                    } else if let Ok(select_el) =
627                        el.dyn_into::<$crate::web_sys::HtmlSelectElement>()
628                    {
629                        val = select_el.value();
630                    }
631                }
632            }
633        }
634        val
635    }};
636}
637
638#[macro_export]
639macro_rules! spawn {
640    ($fut:expr) => {
641        $crate::wasm_bindgen_futures::spawn_local(async move {
642            $fut.await;
643            let _ = $crate::tick();
644        });
645    };
646}
647
648#[macro_export]
649macro_rules! fetch {
650    // With body
651    ($method:ident $url:expr, $body:expr => |$text:ident| $success:block) => {
652        $crate::wasm_bindgen_futures::spawn_local(async move {
653            if let Ok(resp) = $crate::reqwasm::http::Request::$method($url).header("Content-Type", "application/json").body($body).send().await {
654                if let Ok($text) = resp.text().await {
655                    $success
656                    let _ = $crate::tick();
657                }
658            }
659        });
660    };
661    ($method:ident $url:expr, $body:expr => |$text:ident| $success:block, |$err:ident| $error:block) => {
662        $crate::wasm_bindgen_futures::spawn_local(async move {
663            match $crate::reqwasm::http::Request::$method($url).header("Content-Type", "application/json").body($body).send().await {
664                Ok(resp) => {
665                    match resp.text().await {
666                        Ok($text) => {
667                            $success
668                            let _ = $crate::tick();
669                        }
670                        Err(e) => {
671                            let $err = format!("Parse error: {:?}", e);
672                            $error
673                            let _ = $crate::tick();
674                        }
675                    }
676                }
677                Err(e) => {
678                    let $err = format!("Fetch error: {:?}", e);
679                    $error
680                    let _ = $crate::tick();
681                }
682            }
683        });
684    };
685
686    // Without body
687    ($method:ident $url:expr => |$text:ident| $success:block) => {
688        $crate::wasm_bindgen_futures::spawn_local(async move {
689            if let Ok(resp) = $crate::reqwasm::http::Request::$method($url).send().await {
690                if let Ok($text) = resp.text().await {
691                    $success
692                    let _ = $crate::tick();
693                }
694            }
695        });
696    };
697    ($method:ident $url:expr => |$text:ident| $success:block, |$err:ident| $error:block) => {
698        $crate::wasm_bindgen_futures::spawn_local(async move {
699            match $crate::reqwasm::http::Request::$method($url).send().await {
700                Ok(resp) => {
701                    match resp.text().await {
702                        Ok($text) => {
703                            $success
704                            let _ = $crate::tick();
705                        }
706                        Err(e) => {
707                            let $err = format!("Parse error: {:?}", e);
708                            $error
709                            let _ = $crate::tick();
710                        }
711                    }
712                }
713                Err(e) => {
714                    let $err = format!("Fetch error: {:?}", e);
715                    $error
716                    let _ = $crate::tick();
717                }
718            }
719        });
720    };
721
722    // Default GET
723    ($url:expr => |$text:ident| $success:block) => {
724        $crate::fetch!(get $url => |$text| $success)
725    };
726    ($url:expr => |$text:ident| $success:block, |$err:ident| $error:block) => {
727        $crate::fetch!(get $url => |$text| $success, |$err| $error)
728    };
729}
730
731#[macro_export]
732macro_rules! rpc {
733    ($call:expr => |$ok:ident| $success:block) => {
734        $crate::spawn!(async move {
735            if let Ok($ok) = $call.await {
736                $success
737            }
738        });
739    };
740    ($call:expr => |$ok:ident| $success:block, |$err:ident| $error:block) => {
741        $crate::spawn!(async move {
742            match $call.await {
743                Ok($ok) => $success,
744                Err($err) => $error,
745            }
746        });
747    };
748}
749
750#[macro_export]
751macro_rules! alert {
752    ($msg:expr) => {
753        if let Some(window) = $crate::web_sys::window() {
754            let _ = window.alert_with_message($msg);
755        }
756    };
757}
758
759#[macro_export]
760macro_rules! log {
761    ($($t:tt)*) => {
762        $crate::web_sys::console::log_1(&format!($($t)*).into());
763    }
764}
765
766pub fn toggle_html_class(class: &str, active: bool) {
767    if let Some(window) = web_sys::window() {
768        if let Some(document) = window.document() {
769            if let Some(html) = document.document_element() {
770                if active {
771                    let _ = html.set_attribute("class", class);
772                } else {
773                    let _ = html.remove_attribute("class");
774                }
775            }
776        }
777    }
778}
779
780// ponytail: keep it simple. use max-age for expiration, no complex date parsing.
781#[macro_export]
782macro_rules! get_cookie {
783    () => {{
784        let mut cookie_string = String::new();
785        if let Some(window) = $crate::web_sys::window() {
786            if let Some(document) = window.document() {
787                use $crate::wasm_bindgen::JsCast;
788                if let Ok(html_doc) = document.dyn_into::<$crate::web_sys::HtmlDocument>() {
789                    if let Ok(c) = html_doc.cookie() {
790                        cookie_string = c;
791                    }
792                }
793            }
794        }
795        cookie_string
796    }};
797    ($name:expr) => {{
798        let cookies = $crate::get_cookie!();
799        let name = $name;
800        let mut result = None;
801        for c in cookies.split(';') {
802            let c = c.trim();
803            if c.starts_with(name) && c[name.len()..].starts_with('=') {
804                result = Some(c[name.len() + 1..].to_string());
805                break;
806            }
807        }
808        result
809    }};
810}
811
812#[macro_export]
813macro_rules! set_cookie {
814    ($name:expr, $value:expr) => {
815        if let Some(window) = $crate::web_sys::window() {
816            if let Some(document) = window.document() {
817                use $crate::wasm_bindgen::JsCast;
818                if let Ok(html_doc) = document.dyn_into::<$crate::web_sys::HtmlDocument>() {
819                    let cookie_str = format!("{}={}; path=/", $name, $value);
820                    let _ = html_doc.set_cookie(&cookie_str);
821                }
822            }
823        }
824    };
825    ($name:expr, $value:expr, $max_age:expr) => {
826        if let Some(window) = $crate::web_sys::window() {
827            if let Some(document) = window.document() {
828                use $crate::wasm_bindgen::JsCast;
829                if let Ok(html_doc) = document.dyn_into::<$crate::web_sys::HtmlDocument>() {
830                    let cookie_str = format!("{}={}; max-age={}; path=/", $name, $value, $max_age);
831                    let _ = html_doc.set_cookie(&cookie_str);
832                }
833            }
834        }
835    };
836}
837
838#[macro_export]
839macro_rules! navigate {
840    ($path:expr) => {
841        if let Some(window) = $crate::web_sys::window() {
842            let _ = window.history().unwrap().push_state_with_url(
843                &$crate::wasm_bindgen::JsValue::NULL,
844                "",
845                Some($path),
846            );
847            let path_str = $path;
848            let route = path_str.split(['?', '#']).next().unwrap_or(path_str);
849            $crate::ROUTER_SETTER.with(|s| {
850                if let Some(setter) = *s.borrow() {
851                    setter.set(route.to_string());
852                }
853            });
854            let _ = $crate::tick();
855            window.scroll_to_with_x_and_y(0.0, 0.0);
856        }
857    };
858}
859
860#[macro_export]
861macro_rules! animate {
862    ($selector:expr, $config:expr) => {
863        if let Some(_) = $crate::web_sys::window() {
864            let script = format!(
865                "if (window.gsap) {{ gsap.to('{}', {}) }}",
866                $selector, $config
867            );
868            if let Err(e) = $crate::js_sys::eval(&script) {
869                $crate::web_sys::console::error_2(&"GSAP animate! error:".into(), &e);
870            }
871        }
872    };
873    (from $selector:expr, $config:expr) => {
874        if let Some(_) = $crate::web_sys::window() {
875            let script = format!(
876                "if (window.gsap) {{ gsap.from('{}', {}) }}",
877                $selector, $config
878            );
879            if let Err(e) = $crate::js_sys::eval(&script) {
880                $crate::web_sys::console::error_2(&"GSAP animate! from error:".into(), &e);
881            }
882        }
883    };
884    (fromTo $selector:expr, $from:expr, $to:expr) => {
885        if let Some(_) = $crate::web_sys::window() {
886            let script = format!(
887                "if (window.gsap) {{ gsap.fromTo('{}', {}, {}) }}",
888                $selector, $from, $to
889            );
890            if let Err(e) = $crate::js_sys::eval(&script) {
891                $crate::web_sys::console::error_2(&"GSAP animate! fromTo error:".into(), &e);
892            }
893        }
894    };
895    (timeline $script:expr) => {
896        if let Some(_) = $crate::web_sys::window() {
897            let script = format!(
898                "if (window.gsap) {{ let tl = gsap.timeline(); {} }}",
899                $script
900            );
901            if let Err(e) = $crate::js_sys::eval(&script) {
902                $crate::web_sys::console::error_2(&"GSAP animate! timeline error:".into(), &e);
903            }
904        }
905    };
906}
907
908#[macro_export]
909macro_rules! redirect {
910    ($url:expr) => {
911        if let Some(w) = $crate::web_sys::window() {
912            let _ = w.location().assign($url);
913        }
914    };
915}
916
917#[macro_export]
918macro_rules! back {
919    () => {
920        if let Some(w) = $crate::web_sys::window() {
921            if let Ok(h) = w.history() {
922                let _ = h.back();
923            }
924        }
925    };
926}