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::None => Ok(document.create_text_node("").into()),
121 }
122}
123
124pub fn tick() -> Result<(), JsValue> {
125 let window = web_sys::window().expect("no global `window` exists");
126 let document = window.document().expect("should have a document on window");
127
128 run_effects();
131
132 let pending = take_pending_boundaries();
133
134 let mut boundary_updates = Vec::new();
135
136 for id in pending {
137 let entry = BOUNDARIES.with(|b| b.borrow().get(&id).cloned());
138 if let Some((old_node, compute)) = entry {
139 let view = id.track(|| {
140 let mut comp = compute.borrow_mut();
141 comp()
142 });
143 let new_node = render_view(&document, view)?;
144 if let Some(parent) = old_node.parent_node() {
145 parent.replace_child(&new_node, &old_node)?;
146 boundary_updates.push((id, new_node, compute));
147 }
148 }
149 }
150
151 BOUNDARIES.with(|b| {
152 let mut boundaries = b.borrow_mut();
153 for (id, new_node, compute) in boundary_updates {
154 boundaries.insert(id, (new_node, compute));
155 }
156 });
157
158 Ok(())
159}
160
161#[macro_export]
162macro_rules! get_value {
163 ($id:expr) => {{
164 let mut val = String::new();
165 if let Some(w) = $crate::web_sys::window() {
166 if let Some(d) = w.document() {
167 if let Some(el) = d.get_element_by_id($id) {
168 use $crate::wasm_bindgen::JsCast;
169 if let Ok(input_el) = el.clone().dyn_into::<$crate::web_sys::HtmlInputElement>()
170 {
171 val = input_el.value();
172 } else if let Ok(textarea_el) = el
173 .clone()
174 .dyn_into::<$crate::web_sys::HtmlTextAreaElement>()
175 {
176 val = textarea_el.value();
177 } else if let Ok(select_el) =
178 el.dyn_into::<$crate::web_sys::HtmlSelectElement>()
179 {
180 val = select_el.value();
181 }
182 }
183 }
184 }
185 val
186 }};
187}
188
189#[macro_export]
190macro_rules! spawn {
191 ($fut:expr) => {
192 $crate::wasm_bindgen_futures::spawn_local(async move {
193 $fut.await;
194 let _ = $crate::tick();
195 });
196 };
197}
198
199#[macro_export]
200macro_rules! fetch {
201 ($method:ident $url:expr, $body:expr => |$text:ident| $success:block) => {
203 $crate::wasm_bindgen_futures::spawn_local(async move {
204 if let Ok(resp) = $crate::reqwasm::http::Request::$method($url).header("Content-Type", "application/json").body($body).send().await {
205 if let Ok($text) = resp.text().await {
206 $success
207 let _ = $crate::tick();
208 }
209 }
210 });
211 };
212 ($method:ident $url:expr, $body:expr => |$text:ident| $success:block, |$err:ident| $error:block) => {
213 $crate::wasm_bindgen_futures::spawn_local(async move {
214 match $crate::reqwasm::http::Request::$method($url).header("Content-Type", "application/json").body($body).send().await {
215 Ok(resp) => {
216 match resp.text().await {
217 Ok($text) => {
218 $success
219 let _ = $crate::tick();
220 }
221 Err(e) => {
222 let $err = format!("Parse error: {:?}", e);
223 $error
224 let _ = $crate::tick();
225 }
226 }
227 }
228 Err(e) => {
229 let $err = format!("Fetch error: {:?}", e);
230 $error
231 let _ = $crate::tick();
232 }
233 }
234 });
235 };
236
237 ($method:ident $url:expr => |$text:ident| $success:block) => {
239 $crate::wasm_bindgen_futures::spawn_local(async move {
240 if let Ok(resp) = $crate::reqwasm::http::Request::$method($url).send().await {
241 if let Ok($text) = resp.text().await {
242 $success
243 let _ = $crate::tick();
244 }
245 }
246 });
247 };
248 ($method:ident $url:expr => |$text:ident| $success:block, |$err:ident| $error:block) => {
249 $crate::wasm_bindgen_futures::spawn_local(async move {
250 match $crate::reqwasm::http::Request::$method($url).send().await {
251 Ok(resp) => {
252 match resp.text().await {
253 Ok($text) => {
254 $success
255 let _ = $crate::tick();
256 }
257 Err(e) => {
258 let $err = format!("Parse error: {:?}", e);
259 $error
260 let _ = $crate::tick();
261 }
262 }
263 }
264 Err(e) => {
265 let $err = format!("Fetch error: {:?}", e);
266 $error
267 let _ = $crate::tick();
268 }
269 }
270 });
271 };
272
273 ($url:expr => |$text:ident| $success:block) => {
275 $crate::fetch!(get $url => |$text| $success)
276 };
277 ($url:expr => |$text:ident| $success:block, |$err:ident| $error:block) => {
278 $crate::fetch!(get $url => |$text| $success, |$err| $error)
279 };
280}
281
282#[macro_export]
283macro_rules! rpc {
284 ($call:expr => |$ok:ident| $success:block) => {
285 $crate::spawn!(async move {
286 if let Ok($ok) = $call.await {
287 $success
288 }
289 });
290 };
291 ($call:expr => |$ok:ident| $success:block, |$err:ident| $error:block) => {
292 $crate::spawn!(async move {
293 match $call.await {
294 Ok($ok) => $success,
295 Err($err) => $error,
296 }
297 });
298 };
299}
300
301#[macro_export]
302macro_rules! alert {
303 ($msg:expr) => {
304 if let Some(window) = $crate::web_sys::window() {
305 let _ = window.alert_with_message($msg);
306 }
307 };
308}
309
310#[macro_export]
311macro_rules! log {
312 ($($t:tt)*) => {
313 $crate::web_sys::console::log_1(&format!($($t)*).into());
314 }
315}
316
317pub fn toggle_html_class(class: &str, active: bool) {
318 if let Some(window) = web_sys::window() {
319 if let Some(document) = window.document() {
320 if let Some(html) = document.document_element() {
321 if active {
322 let _ = html.set_attribute("class", class);
323 } else {
324 let _ = html.remove_attribute("class");
325 }
326 }
327 }
328 }
329}
330
331#[macro_export]
333macro_rules! get_cookie {
334 () => {{
335 let mut cookie_string = String::new();
336 if let Some(window) = $crate::web_sys::window() {
337 if let Some(document) = window.document() {
338 use $crate::wasm_bindgen::JsCast;
339 if let Ok(html_doc) = document.dyn_into::<$crate::web_sys::HtmlDocument>() {
340 if let Ok(c) = html_doc.cookie() {
341 cookie_string = c;
342 }
343 }
344 }
345 }
346 cookie_string
347 }};
348 ($name:expr) => {{
349 let cookies = $crate::get_cookie!();
350 let name = $name;
351 let mut result = None;
352 for c in cookies.split(';') {
353 let c = c.trim();
354 if c.starts_with(name) && c[name.len()..].starts_with('=') {
355 result = Some(c[name.len() + 1..].to_string());
356 break;
357 }
358 }
359 result
360 }};
361}
362
363#[macro_export]
364macro_rules! set_cookie {
365 ($name:expr, $value:expr) => {
366 if let Some(window) = $crate::web_sys::window() {
367 if let Some(document) = window.document() {
368 use $crate::wasm_bindgen::JsCast;
369 if let Ok(html_doc) = document.dyn_into::<$crate::web_sys::HtmlDocument>() {
370 let cookie_str = format!("{}={}; path=/", $name, $value);
371 let _ = html_doc.set_cookie(&cookie_str);
372 }
373 }
374 }
375 };
376 ($name:expr, $value:expr, $max_age:expr) => {
377 if let Some(window) = $crate::web_sys::window() {
378 if let Some(document) = window.document() {
379 use $crate::wasm_bindgen::JsCast;
380 if let Ok(html_doc) = document.dyn_into::<$crate::web_sys::HtmlDocument>() {
381 let cookie_str = format!("{}={}; max-age={}; path=/", $name, $value, $max_age);
382 let _ = html_doc.set_cookie(&cookie_str);
383 }
384 }
385 }
386 };
387}
388
389#[macro_export]
390macro_rules! navigate {
391 ($path:expr) => {
392 if let Some(window) = $crate::web_sys::window() {
393 let _ = window.history().unwrap().push_state_with_url(
394 &$crate::wasm_bindgen::JsValue::NULL,
395 "",
396 Some($path),
397 );
398 let path_str = $path;
399 let route = path_str.split(['?', '#']).next().unwrap_or(path_str);
400 $crate::ROUTER_SETTER.with(|s| {
401 if let Some(setter) = *s.borrow() {
402 setter.set(route.to_string());
403 }
404 });
405 let _ = $crate::tick();
406 window.scroll_to_with_x_and_y(0.0, 0.0);
407 }
408 };
409}
410
411#[macro_export]
412macro_rules! animate {
413 ($selector:expr, $config:expr) => {
414 if let Some(_) = $crate::web_sys::window() {
415 let script = format!("if (window.gsap) {{ gsap.to('{}', {}) }}", $selector, $config);
416 if let Err(e) = $crate::js_sys::eval(&script) {
417 $crate::web_sys::console::error_2(&"GSAP animate! error:".into(), &e);
418 }
419 }
420 };
421 (from $selector:expr, $config:expr) => {
422 if let Some(_) = $crate::web_sys::window() {
423 let script = format!("if (window.gsap) {{ gsap.from('{}', {}) }}", $selector, $config);
424 if let Err(e) = $crate::js_sys::eval(&script) {
425 $crate::web_sys::console::error_2(&"GSAP animate! from error:".into(), &e);
426 }
427 }
428 };
429 (fromTo $selector:expr, $from:expr, $to:expr) => {
430 if let Some(_) = $crate::web_sys::window() {
431 let script = format!("if (window.gsap) {{ gsap.fromTo('{}', {}, {}) }}", $selector, $from, $to);
432 if let Err(e) = $crate::js_sys::eval(&script) {
433 $crate::web_sys::console::error_2(&"GSAP animate! fromTo error:".into(), &e);
434 }
435 }
436 };
437 (timeline $script:expr) => {
438 if let Some(_) = $crate::web_sys::window() {
439 let script = format!("if (window.gsap) {{ let tl = gsap.timeline(); {} }}", $script);
440 if let Err(e) = $crate::js_sys::eval(&script) {
441 $crate::web_sys::console::error_2(&"GSAP animate! timeline error:".into(), &e);
442 }
443 }
444 };
445}
446
447#[macro_export]
448macro_rules! redirect {
449 ($url:expr) => {
450 if let Some(w) = $crate::web_sys::window() {
451 let _ = w.location().assign($url);
452 }
453 };
454}
455
456#[macro_export]
457macro_rules! back {
458 () => {
459 if let Some(w) = $crate::web_sys::window() {
460 if let Ok(h) = w.history() {
461 let _ = h.back();
462 }
463 }
464 };
465}