1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
use js_sys::Function;
use plaster::callback::Callback;
use route_recognizer::{Params, Router as RecRouter};
use std::sync::{Arc, Mutex};
use wasm_bindgen::prelude::*;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{window, Event};

use log::trace;
pub use plaster_router_macro::Routes;

pub struct Router<T> {
    routes: Vec<fn(Params) -> T>,
    index_router: RecRouter<usize>,
    current_path: Arc<Mutex<String>>,
    listener: Closure<dyn FnMut(JsValue)>,
    callback: Callback<()>,
}

impl<T> Router<T> {
    pub fn new(callback: Callback<()>) -> Router<T> {
        let win = window().expect("need a window context");
        let path = win.location().pathname().unwrap_or("/".to_string());
        trace!("initial route: {}", &path);
        let current_path = Arc::new(Mutex::new(path));
        let current_path_c = current_path.clone();
        let callback_c = callback.clone();

        let listener_callback = Closure::wrap(Box::new(move |_: JsValue| {
            let path = window()
                .expect("need a window context")
                .location()
                .pathname()
                .unwrap_or("/".to_string());
            trace!("route change: {}", &path);
            *current_path_c.lock().unwrap() = path;
            callback_c.emit(());
        }) as Box<dyn FnMut(_)>);

        let listener_function: &Function = listener_callback.as_ref().unchecked_ref();

        win.add_event_listener_with_callback("plasterroutechange", listener_function)
            .expect("could not attach global event listener");
        win.add_event_listener_with_callback("popstate", listener_function)
            .expect("could not attach popstate event listener");

        Router {
            routes: Vec::new(),
            index_router: RecRouter::new(),
            current_path: current_path,
            listener: listener_callback,
            callback: callback,
        }
    }

    pub fn add_route(&mut self, route: &str, closure: fn(Params) -> T) {
        trace!("added route: {}", route);
        let index = self.routes.len();
        self.routes.push(closure);
        self.index_router.add(route, index);
    }

    pub fn navigate(&mut self, path: &str) {
        *self.current_path.lock().unwrap() = path.to_string();
        self.push_state();
        self.callback.emit(());
    }

    pub fn resolve(&self) -> Option<T> {
        let route_match = self
            .index_router
            .recognize(&self.current_path.lock().unwrap())
            .ok();
        route_match.map(|m| self.routes.get(m.handler.clone()).unwrap()(m.params))
    }

    fn push_state(&self) {
        match window().expect("need a window context").history() {
            Ok(history) => {
                history
                    .push_state_with_url(
                        &JsValue::NULL,
                        "",
                        Some(&self.current_path.lock().unwrap()),
                    )
                    .expect("could not pushState");
            }
            Err(_) => (),
        }
    }
}

impl<T> Drop for Router<T> {
    fn drop(&mut self) {
        window()
            .expect("need window context")
            .remove_event_listener_with_callback(
                "plasterroutechange",
                self.listener.as_ref().unchecked_ref(),
            )
            .expect("could not remove event listener");
    }
}

pub trait Routes<T> {
    fn router(callback: Callback<()>) -> Router<T>;
}

pub fn route_to(path: &str) {
    let win = window().expect("need window context");
    win.history()
        .expect("history API unavailable")
        .push_state_with_url(&JsValue::NULL, "", Some(path))
        .expect("could not pushState");
    win.dispatch_event(&Event::new("plasterroutechange").expect("could not create event"))
        .expect("could not dispatch event");
}