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
33fn render_view(document: &Document, view: View) -> Result<Node, JsValue> {
34 match view {
35 View::Text(text) => Ok(document.create_text_node(&text).into()),
36 View::Element {
37 tag,
38 attrs,
39 children,
40 } => {
41 let el = if tag == "svg"
42 || tag == "path"
43 || tag == "circle"
44 || tag == "rect"
45 || tag == "g"
46 || tag == "line"
47 {
48 document.create_element_ns(Some("http://www.w3.org/2000/svg"), &tag)?
49 } else {
50 document.create_element(&tag)?
51 };
52 for (k, v) in attrs {
53 match v {
54 AttributeValue::String(s) => el.set_attribute(&k, &s)?,
55 AttributeValue::Bool(b) => {
56 if b {
57 el.set_attribute(&k, "")?;
58 }
59 }
60 AttributeValue::Dynamic(f) => {
61 let el_clone = el.clone();
65 let k_clone = k.clone();
66 let val = f();
67 if let AttributeValue::String(s) = &val {
68 let _ = el.set_attribute(&k, s);
69 }
70 create_effect(move || {
72 let val = f();
73 if let AttributeValue::String(s) = val {
74 let _ = el_clone.set_attribute(&k_clone, &s);
75 }
76 });
77 }
78 AttributeValue::Event(cb) => {
79 use wasm_bindgen::JsCast;
80 let closure = wasm_bindgen::closure::Closure::wrap(Box::new(move || {
81 cb();
82 let _ = crate::tick();
83 })
84 as Box<dyn FnMut()>);
85 el.add_event_listener_with_callback(&k, closure.as_ref().unchecked_ref())?;
86 closure.forget();
87 }
88 }
89 }
90 for child in children {
91 let child_node = render_view(document, child)?;
92 el.append_child(&child_node)?;
93 }
94 Ok(el.into())
95 }
96 View::DynamicNode(boundary) => {
97 let view = boundary.id.track(|| {
98 let mut compute = boundary.compute.borrow_mut();
99 compute()
100 });
101 let node = render_view(document, view)?;
102
103 let compute_rc = boundary.compute.clone();
104 BOUNDARIES.with(|b| {
105 b.borrow_mut()
106 .insert(boundary.id, (node.clone(), compute_rc));
107 });
108
109 Ok(node)
110 }
111 View::Fragment(children) => {
112 let el = document.create_element("div")?;
114 for child in children {
115 let child_node = render_view(document, child)?;
116 el.append_child(&child_node)?;
117 }
118 Ok(el.into())
119 }
120 View::KeyedList(children) => {
121 let el = document.create_element("div")?;
122 el.set_attribute("data-th-keyed-list", "true")?;
123 use wasm_bindgen::JsCast;
124 for (key, child) in children {
125 let child_node = render_view(document, child)?;
126 if let Some(child_el) = child_node.dyn_ref::<Element>() {
127 let _ = child_el.set_attribute("data-th-key", &key);
128 }
129 el.append_child(&child_node)?;
130 }
131 Ok(el.into())
132 }
133 View::None => Ok(document.create_text_node("").into()),
134 }
135}
136
137pub fn tick() -> Result<(), JsValue> {
138 let window = web_sys::window().expect("no global `window` exists");
139 let document = window.document().expect("should have a document on window");
140
141 run_effects();
144
145 let pending = take_pending_boundaries();
146
147 let mut boundary_updates = Vec::new();
148
149 for id in pending {
150 let entry = BOUNDARIES.with(|b| b.borrow().get(&id).cloned());
151 if let Some((old_node, compute)) = entry {
152 let view = id.track(|| {
153 let mut comp = compute.borrow_mut();
154 comp()
155 });
156
157 let mut handled = false;
158 if let View::KeyedList(children) = &view {
159 use wasm_bindgen::JsCast;
160 if let Some(old_el) = old_node.dyn_ref::<web_sys::Element>() {
161 if old_el.has_attribute("data-th-keyed-list") {
162 handled = true;
163
164 let mut old_nodes = std::collections::HashMap::new();
165 let mut current_child = old_node.first_child();
166 while let Some(child) = current_child {
167 if let Some(child_el) = child.dyn_ref::<web_sys::Element>() {
168 if let Some(key) = child_el.get_attribute("data-th-key") {
169 old_nodes.insert(key, child.clone());
170 }
171 }
172 current_child = child.next_sibling();
173 }
174
175 let mut new_ordered_children = Vec::new();
177
178 for (key, child_view) in children {
179 if let Some(existing_node) = old_nodes.remove(key) {
180 new_ordered_children.push(existing_node);
181 } else {
182 let new_child = render_view(&document, child_view.clone())?;
183 if let Some(child_el) = new_child.dyn_ref::<web_sys::Element>() {
184 let _ = child_el.set_attribute("data-th-key", key);
185 }
186 new_ordered_children.push(new_child);
187 }
188 }
189
190 old_el.set_inner_html("");
191 for child in new_ordered_children {
192 old_el.append_child(&child)?;
193 }
194
195 boundary_updates.push((id, old_node.clone(), compute.clone()));
196 }
197 }
198 }
199
200 if !handled {
201 let new_node = render_view(&document, view)?;
202 if let Some(parent) = old_node.parent_node() {
203 parent.replace_child(&new_node, &old_node)?;
204 boundary_updates.push((id, new_node, compute));
205 }
206 }
207 }
208 }
209
210 BOUNDARIES.with(|b| {
211 let mut boundaries = b.borrow_mut();
212 for (id, new_node, compute) in boundary_updates {
213 boundaries.insert(id, (new_node, compute));
214 }
215 });
216
217 Ok(())
218}
219
220#[macro_export]
221macro_rules! get_value {
222 ($id:expr) => {{
223 let mut val = String::new();
224 if let Some(w) = $crate::web_sys::window() {
225 if let Some(d) = w.document() {
226 if let Some(el) = d.get_element_by_id($id) {
227 use $crate::wasm_bindgen::JsCast;
228 if let Ok(input_el) = el.clone().dyn_into::<$crate::web_sys::HtmlInputElement>()
229 {
230 val = input_el.value();
231 } else if let Ok(textarea_el) = el
232 .clone()
233 .dyn_into::<$crate::web_sys::HtmlTextAreaElement>()
234 {
235 val = textarea_el.value();
236 } else if let Ok(select_el) =
237 el.dyn_into::<$crate::web_sys::HtmlSelectElement>()
238 {
239 val = select_el.value();
240 }
241 }
242 }
243 }
244 val
245 }};
246}
247
248#[macro_export]
249macro_rules! spawn {
250 ($fut:expr) => {
251 $crate::wasm_bindgen_futures::spawn_local(async move {
252 $fut.await;
253 let _ = $crate::tick();
254 });
255 };
256}
257
258#[macro_export]
259macro_rules! fetch {
260 ($method:ident $url:expr, $body:expr => |$text:ident| $success:block) => {
262 $crate::wasm_bindgen_futures::spawn_local(async move {
263 if let Ok(resp) = $crate::reqwasm::http::Request::$method($url).header("Content-Type", "application/json").body($body).send().await {
264 if let Ok($text) = resp.text().await {
265 $success
266 let _ = $crate::tick();
267 }
268 }
269 });
270 };
271 ($method:ident $url:expr, $body:expr => |$text:ident| $success:block, |$err:ident| $error:block) => {
272 $crate::wasm_bindgen_futures::spawn_local(async move {
273 match $crate::reqwasm::http::Request::$method($url).header("Content-Type", "application/json").body($body).send().await {
274 Ok(resp) => {
275 match resp.text().await {
276 Ok($text) => {
277 $success
278 let _ = $crate::tick();
279 }
280 Err(e) => {
281 let $err = format!("Parse error: {:?}", e);
282 $error
283 let _ = $crate::tick();
284 }
285 }
286 }
287 Err(e) => {
288 let $err = format!("Fetch error: {:?}", e);
289 $error
290 let _ = $crate::tick();
291 }
292 }
293 });
294 };
295
296 ($method:ident $url:expr => |$text:ident| $success:block) => {
298 $crate::wasm_bindgen_futures::spawn_local(async move {
299 if let Ok(resp) = $crate::reqwasm::http::Request::$method($url).send().await {
300 if let Ok($text) = resp.text().await {
301 $success
302 let _ = $crate::tick();
303 }
304 }
305 });
306 };
307 ($method:ident $url:expr => |$text:ident| $success:block, |$err:ident| $error:block) => {
308 $crate::wasm_bindgen_futures::spawn_local(async move {
309 match $crate::reqwasm::http::Request::$method($url).send().await {
310 Ok(resp) => {
311 match resp.text().await {
312 Ok($text) => {
313 $success
314 let _ = $crate::tick();
315 }
316 Err(e) => {
317 let $err = format!("Parse error: {:?}", e);
318 $error
319 let _ = $crate::tick();
320 }
321 }
322 }
323 Err(e) => {
324 let $err = format!("Fetch error: {:?}", e);
325 $error
326 let _ = $crate::tick();
327 }
328 }
329 });
330 };
331
332 ($url:expr => |$text:ident| $success:block) => {
334 $crate::fetch!(get $url => |$text| $success)
335 };
336 ($url:expr => |$text:ident| $success:block, |$err:ident| $error:block) => {
337 $crate::fetch!(get $url => |$text| $success, |$err| $error)
338 };
339}
340
341#[macro_export]
342macro_rules! rpc {
343 ($call:expr => |$ok:ident| $success:block) => {
344 $crate::spawn!(async move {
345 if let Ok($ok) = $call.await {
346 $success
347 }
348 });
349 };
350 ($call:expr => |$ok:ident| $success:block, |$err:ident| $error:block) => {
351 $crate::spawn!(async move {
352 match $call.await {
353 Ok($ok) => $success,
354 Err($err) => $error,
355 }
356 });
357 };
358}
359
360#[macro_export]
361macro_rules! alert {
362 ($msg:expr) => {
363 if let Some(window) = $crate::web_sys::window() {
364 let _ = window.alert_with_message($msg);
365 }
366 };
367}
368
369#[macro_export]
370macro_rules! log {
371 ($($t:tt)*) => {
372 $crate::web_sys::console::log_1(&format!($($t)*).into());
373 }
374}
375
376pub fn toggle_html_class(class: &str, active: bool) {
377 if let Some(window) = web_sys::window() {
378 if let Some(document) = window.document() {
379 if let Some(html) = document.document_element() {
380 if active {
381 let _ = html.set_attribute("class", class);
382 } else {
383 let _ = html.remove_attribute("class");
384 }
385 }
386 }
387 }
388}
389
390#[macro_export]
392macro_rules! get_cookie {
393 () => {{
394 let mut cookie_string = String::new();
395 if let Some(window) = $crate::web_sys::window() {
396 if let Some(document) = window.document() {
397 use $crate::wasm_bindgen::JsCast;
398 if let Ok(html_doc) = document.dyn_into::<$crate::web_sys::HtmlDocument>() {
399 if let Ok(c) = html_doc.cookie() {
400 cookie_string = c;
401 }
402 }
403 }
404 }
405 cookie_string
406 }};
407 ($name:expr) => {{
408 let cookies = $crate::get_cookie!();
409 let name = $name;
410 let mut result = None;
411 for c in cookies.split(';') {
412 let c = c.trim();
413 if c.starts_with(name) && c[name.len()..].starts_with('=') {
414 result = Some(c[name.len() + 1..].to_string());
415 break;
416 }
417 }
418 result
419 }};
420}
421
422#[macro_export]
423macro_rules! set_cookie {
424 ($name:expr, $value:expr) => {
425 if let Some(window) = $crate::web_sys::window() {
426 if let Some(document) = window.document() {
427 use $crate::wasm_bindgen::JsCast;
428 if let Ok(html_doc) = document.dyn_into::<$crate::web_sys::HtmlDocument>() {
429 let cookie_str = format!("{}={}; path=/", $name, $value);
430 let _ = html_doc.set_cookie(&cookie_str);
431 }
432 }
433 }
434 };
435 ($name:expr, $value:expr, $max_age:expr) => {
436 if let Some(window) = $crate::web_sys::window() {
437 if let Some(document) = window.document() {
438 use $crate::wasm_bindgen::JsCast;
439 if let Ok(html_doc) = document.dyn_into::<$crate::web_sys::HtmlDocument>() {
440 let cookie_str = format!("{}={}; max-age={}; path=/", $name, $value, $max_age);
441 let _ = html_doc.set_cookie(&cookie_str);
442 }
443 }
444 }
445 };
446}
447
448#[macro_export]
449macro_rules! navigate {
450 ($path:expr) => {
451 if let Some(window) = $crate::web_sys::window() {
452 let _ = window.history().unwrap().push_state_with_url(
453 &$crate::wasm_bindgen::JsValue::NULL,
454 "",
455 Some($path),
456 );
457 let path_str = $path;
458 let route = path_str.split(['?', '#']).next().unwrap_or(path_str);
459 $crate::ROUTER_SETTER.with(|s| {
460 if let Some(setter) = *s.borrow() {
461 setter.set(route.to_string());
462 }
463 });
464 let _ = $crate::tick();
465 window.scroll_to_with_x_and_y(0.0, 0.0);
466 }
467 };
468}
469
470#[macro_export]
471macro_rules! animate {
472 ($selector:expr, $config:expr) => {
473 if let Some(_) = $crate::web_sys::window() {
474 let script = format!(
475 "if (window.gsap) {{ gsap.to('{}', {}) }}",
476 $selector, $config
477 );
478 if let Err(e) = $crate::js_sys::eval(&script) {
479 $crate::web_sys::console::error_2(&"GSAP animate! error:".into(), &e);
480 }
481 }
482 };
483 (from $selector:expr, $config:expr) => {
484 if let Some(_) = $crate::web_sys::window() {
485 let script = format!(
486 "if (window.gsap) {{ gsap.from('{}', {}) }}",
487 $selector, $config
488 );
489 if let Err(e) = $crate::js_sys::eval(&script) {
490 $crate::web_sys::console::error_2(&"GSAP animate! from error:".into(), &e);
491 }
492 }
493 };
494 (fromTo $selector:expr, $from:expr, $to:expr) => {
495 if let Some(_) = $crate::web_sys::window() {
496 let script = format!(
497 "if (window.gsap) {{ gsap.fromTo('{}', {}, {}) }}",
498 $selector, $from, $to
499 );
500 if let Err(e) = $crate::js_sys::eval(&script) {
501 $crate::web_sys::console::error_2(&"GSAP animate! fromTo error:".into(), &e);
502 }
503 }
504 };
505 (timeline $script:expr) => {
506 if let Some(_) = $crate::web_sys::window() {
507 let script = format!(
508 "if (window.gsap) {{ let tl = gsap.timeline(); {} }}",
509 $script
510 );
511 if let Err(e) = $crate::js_sys::eval(&script) {
512 $crate::web_sys::console::error_2(&"GSAP animate! timeline error:".into(), &e);
513 }
514 }
515 };
516}
517
518#[macro_export]
519macro_rules! redirect {
520 ($url:expr) => {
521 if let Some(w) = $crate::web_sys::window() {
522 let _ = w.location().assign($url);
523 }
524 };
525}
526
527#[macro_export]
528macro_rules! back {
529 () => {
530 if let Some(w) = $crate::web_sys::window() {
531 if let Ok(h) = w.history() {
532 let _ = h.back();
533 }
534 }
535 };
536}