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