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 }
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();
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 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 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(¤t));
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 ($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 ($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 ($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#[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}