1use js_sys::Function;
2use plaster::callback::Callback;
3use route_recognizer::{Params, Router as RecRouter};
4use serde_derive::{Deserialize, Serialize};
5use std::sync::{Arc, Mutex};
6use wasm_bindgen::prelude::*;
7use wasm_bindgen::{JsCast, JsValue};
8use web_sys::{window, CustomEvent, CustomEventInit};
9
10use log::trace;
11pub use plaster_router_macro::Routes;
12
13pub struct Router<T> {
14 routes: Vec<fn(Params) -> T>,
15 index_router: RecRouter<usize>,
16 current_path: Arc<Mutex<String>>,
17 listener: Closure<dyn FnMut(CustomEvent)>,
18 callback: Callback<()>,
19}
20
21impl<T> Router<T> {
22 pub fn new(callback: Callback<()>) -> Router<T> {
23 let win = window().expect("need a window context");
24 let path = if cfg!(not(feature = "mobile")) {
25 win.location().pathname().unwrap_or("/".to_string())
26 } else {
27 "/".to_string()
28 };
29 trace!("initial route: {}", &path);
30 let current_path = Arc::new(Mutex::new(path));
31 let current_path_c = current_path.clone();
32 let callback_c = callback.clone();
33
34 let listener_callback = Closure::wrap(Box::new(move |e: CustomEvent| {
35 let ev: RouteEvent = e
36 .detail()
37 .into_serde()
38 .expect("could not deserialize route event");
39 trace!("route change: {}", &ev.route);
40 *current_path_c.lock().unwrap() = ev.route;
41 callback_c.emit(());
42 }) as Box<dyn FnMut(_)>);
43
44 let listener_function: &Function = listener_callback.as_ref().unchecked_ref();
45
46 win.add_event_listener_with_callback("plasterroutechange", listener_function)
47 .expect("could not attach global event listener");
48
49 if cfg!(not(feature = "mobile")) {
50 win.add_event_listener_with_callback("popstate", listener_function)
51 .expect("could not attach popstate event listener");
52 }
53
54 Router {
55 routes: Vec::new(),
56 index_router: RecRouter::new(),
57 current_path: current_path,
58 listener: listener_callback,
59 callback: callback,
60 }
61 }
62
63 pub fn add_route(&mut self, route: &str, closure: fn(Params) -> T) {
64 trace!("added route: {}", route);
65 let index = self.routes.len();
66 self.routes.push(closure);
67 self.index_router.add(route, index);
68 }
69
70 pub fn navigate(&mut self, path: &str) {
71 *self.current_path.lock().unwrap() = path.to_string();
72 if cfg!(not(feature = "mobile")) {
73 self.push_state();
74 }
75 self.callback.emit(());
76 }
77
78 pub fn resolve(&self) -> Option<T> {
79 let route_match = self
80 .index_router
81 .recognize(&self.current_path.lock().unwrap())
82 .ok();
83 route_match.map(|m| self.routes.get(m.handler.clone()).unwrap()(m.params))
84 }
85
86 pub fn current_route(&self) -> String {
87 self.current_path.lock().unwrap().clone()
88 }
89
90 pub fn set_route(&self, path: &str) {
91 *self.current_path.lock().unwrap() = path.to_string();
92 }
93
94 fn push_state(&self) {
95 match window().expect("need a window context").history() {
96 Ok(history) => {
97 history
98 .push_state_with_url(
99 &JsValue::NULL,
100 "",
101 Some(&self.current_path.lock().unwrap()),
102 )
103 .expect("could not pushState");
104 }
105 Err(_) => (),
106 }
107 }
108}
109
110impl<T> Drop for Router<T> {
111 fn drop(&mut self) {
112 window()
113 .expect("need window context")
114 .remove_event_listener_with_callback(
115 "plasterroutechange",
116 self.listener.as_ref().unchecked_ref(),
117 )
118 .expect("could not remove event listener");
119 }
120}
121
122pub trait Routes<T> {
123 fn router(callback: Callback<()>) -> Router<T>;
124}
125
126pub fn route_to(path: &str) {
127 let win = window().expect("need window context");
128
129 if cfg!(not(feature = "mobile")) {
130 win.history()
131 .expect("history API unavailable")
132 .push_state_with_url(&JsValue::NULL, "", Some(path))
133 .expect("could not pushState");
134 }
135
136 let mut init = CustomEventInit::new();
137 init.detail(
138 &JsValue::from_serde(&RouteEvent {
139 route: path.to_owned(),
140 })
141 .unwrap(),
142 );
143 let event = CustomEvent::new_with_event_init_dict("plasterroutechange", &init)
144 .expect("could not create CustomEvent");
145 win.dispatch_event(&event)
146 .expect("could not dispatch route change");
147}
148
149#[derive(Serialize, Deserialize)]
150struct RouteEvent {
151 route: String,
152}