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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
//! See readme for details.

#![allow(unused_macros)]

use std::collections::HashMap;
use std::panic;

use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;

pub mod dom_types;
pub mod fetch;
#[macro_use]
pub mod shortcuts;
pub mod storage;
mod vdom;
mod websys_bridge;

// For fetch:
#[macro_use]
extern crate serde_derive;

//// todos:
// Passing values to enums that have arguments without lifetime issues.
// todo router
// todo local storage
// todo maybe?? High-level css-grid and flex api?
// todo Async conflicts with events stepping on each other ?
// todo keyed elements??
// todo composable styles and attrs. Perhaps it's adding multiple ones, or maybe merge methods.


/// Convenience function used in event handling: Convert an event target
/// to an input element; eg so you can take its value.
pub fn to_input(target: &web_sys::EventTarget ) -> &web_sys::HtmlInputElement {
    // This might be more appropriate for web_sys::bridge, but I'd
    // like to expose it without making websys_bridge public.
    target.dyn_ref::<web_sys::HtmlInputElement>().expect("Unable to cast as an input element")
}

/// See to_input
pub fn to_textarea(target: &web_sys::EventTarget ) -> &web_sys::HtmlTextAreaElement {
    // This might be more appropriate for web_sys::bridge, but I'd
    // like to expose it without making websys_bridge public.
    target.dyn_ref::<web_sys::HtmlTextAreaElement>().expect("Unable to cast as a textarea element")
}

/// See to_input
pub fn to_select(target: &web_sys::EventTarget ) -> &web_sys::HtmlSelectElement {
    // This might be more appropriate for web_sys::bridge, but I'd
    // like to expose it without making websys_bridge public.
    target.dyn_ref::<web_sys::HtmlSelectElement>().expect("Unable to cast as a select element")
}

/// See to_input
pub fn to_html_el(target: &web_sys::EventTarget ) -> &web_sys::HtmlElement {
    // This might be more appropriate for web_sys::bridge, but I'd
    // like to expose it without making websys_bridge public.
    target.dyn_ref::<web_sys::HtmlElement>().expect("Unable to cast as an HTML element")
}

/// Convert a web_sys::Event to a web_sys::KeyboardEvent. Useful for extracting
/// info like which key has been pressed, which is not available with normal Events.
pub fn to_kbevent(event: &web_sys::Event ) -> &web_sys::KeyboardEvent {
    // This might be more appropriate for web_sys::bridge, but I'd
    // like to expose it without making websys_bridge public.
    event.dyn_ref::<web_sys::KeyboardEvent>().expect("Unable to cast as a keyboard event")
}

/// Convenience function to access the web_sys DOM document.
pub fn document() -> web_sys::Document {
    web_sys::window()
        .expect("Can't find window")
        .document()
        .expect("Can't find document")
}

/// App initialization: Collect its fundamental components, setup, and perform
/// an initial render.
pub fn run<Ms, Mdl>(model: Mdl, update: fn(Ms, Mdl) -> Mdl,
          view: fn(Mdl) -> dom_types::El<Ms>, mount_point_id: &str, route_map: Option<HashMap<&str, Ms>>)
    where Ms: Clone + Sized + 'static, Mdl: Clone + Sized + 'static
{

    let app = vdom::App::new(model.clone(), update, view, mount_point_id);

    // Our initial render. Can't initialize in new due to mailbox() requiring self.
    let mut topel_vdom = (app.data.view)(model);
    app.setup_vdom(&mut topel_vdom, 0, 0);

    vdom::attach_listeners(&mut topel_vdom, app.mailbox());

    // Attach all children: This is where our initial render occurs.
    websys_bridge::attach_els(&mut topel_vdom, &app.data.mount_point);

    app.data.main_el_vdom.replace(topel_vdom);

    // If a route map is inlcluded, update the state on page load, based
    // on the starting URL. Must be set up on the server as well.
    if let Some(r_map) = route_map {
        // todo switch back to path name.
        let window = web_sys::window().expect("no global `window` exists");
        let path = window.location().pathname().expect("Can't find pathname");
        for (route, route_message) in r_map.clone().into_iter() {
            if route == &path {
                app.update_dom(route_message);
                break;
            }
        }

        // todo: Potentially split this routing hanlign to a sep func.
        // todo: Probably in its own module.

            // Convert to a map over owned strings, to prevent lifetime problems in the closure.
            let mut r_map2 = HashMap::new();
            for (route, msg) in r_map {
                r_map2.insert(route.to_string(), msg);
            }

            let history_closure = Closure::wrap(
                Box::new(move |event: web_sys::Event| {
                    let event = event.dyn_into::<web_sys::PopStateEvent>()
                        .expect("Unable to cast as a PopStateEvent");

                    // We recreate window and document here since they're captured in a closure.
                    let window = web_sys::window().expect("no global `window` exists");
                    let path = window.location().pathname().expect("Can't find pathname");
                    let path_trimmed = &path[1..path.len()].to_string();

                    if let Some(route_message) = r_map2.get(path_trimmed) {
                        app.update_dom(route_message.clone());
                    }

                    // todo: It looks like we could use either the event, or path name.
                    // todo path name might be easier, since
//                    if let Some(state) = event.state().as_string() {
//                        crate::log("state: ".to_string() + &state);
//                    }
                })
                    as Box<FnMut(web_sys::Event) + 'static>,
            );

            (window.as_ref() as &web_sys::EventTarget)
                .add_event_listener_with_callback("popstate", history_closure.as_ref().unchecked_ref())
                .expect("Problem adding popstate listener");

        history_closure.forget();  // todo: Is this leaking memory?

    }

    // Allows panic messages to output to the browser console.error.
    panic::set_hook(Box::new(console_error_panic_hook::hook));
}

/// Push a new state to the window's history, and append a custom path to the url. This
/// is an improtant client-side routing feature.
/// https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.History.html#method.push_state_with_url
/// https://developer.mozilla.org/en-US/docs/Web/API/History_API
pub fn push_route(path: &str) {
    let window = web_sys::window().expect("No global window exists");
    let history = window.history().expect("Can't find history");
    // The second parameter, title, is currently unused by Firefox at least.
    // The first, an arbitrary state object, we could possibly use.
    // todo: Look into using state
    history.push_state_with_url(&JsValue::from_str(""), "", Some(path));
}

/// Create an element flagged in a way that it will not be rendered. Useful
/// in ternary operations.
pub fn empty<Ms: Clone>() -> dom_types::El<Ms> {
    // The tag doesn't matter here, but this seems semantically appropriate.
    let mut el = dom_types::El::empty(dom_types::Tag::Del);
    el.add_attr("dummy-element".into(), "true".into());
    el
}

/// A convenience function for logging to the web browser's console.  See also
/// the log! macro, which is more flexible.
pub fn log<S: ToString>(text: S) {
    web_sys::console::log_1(&text.to_string().into());
}


/// Introduce El into the global namespace for convenience (It will be repeated
/// often in the output type of components), and UpdateEl, which is required
/// for element-creation macros, input event constructors, and the History struct.
pub mod prelude {
    pub use crate::dom_types::{El, UpdateEl, simple_ev, input_ev, keyboard_ev, raw_ev};
    pub use std::collections::HashMap;
}