Skip to main content

euv_ui/component/router/view/
impl.rs

1use crate::*;
2
3/// Implementation of route configuration construction.
4impl EuvRouteConfig {
5    /// Creates a new route configuration.
6    ///
7    /// # Arguments
8    ///
9    /// - `&'static str` - The route path.
10    /// - `F: Fn() -> VirtualNode + 'static` - The component function.
11    ///
12    /// # Returns
13    ///
14    /// - `EuvRouteConfig` - The route configuration.
15    pub fn new<F>(path: &'static str, component: F) -> Self
16    where
17        F: Fn() -> VirtualNode + 'static,
18    {
19        Self {
20            path,
21            component: Rc::new(component),
22        }
23    }
24}
25
26/// Implementation of router navigation and viewport utilities.
27impl Router {
28    /// Reads the current hash-based route from the browser URL.
29    ///
30    /// # Returns
31    ///
32    /// - `String` - The hash fragment without the leading `#`, or `DEFAULT_ROUTE_PATH` if empty.
33    pub fn current_route() -> String {
34        let window: Window = window().expect("no global window exists");
35        let hash: String = window.location().hash().unwrap_or_default();
36        let route: String = hash
37            .strip_prefix(ROUTE_HASH_PREFIX)
38            .unwrap_or(&hash)
39            .to_string();
40        if route.is_empty() {
41            DEFAULT_ROUTE_PATH.to_string()
42        } else {
43            route
44        }
45    }
46
47    /// Navigates to a new hash-based route.
48    ///
49    /// Always defers the actual `location.set_hash()` call to `queueMicrotask`
50    /// to prevent synchronous `hashchange` dispatch while any caller frame is still
51    /// on the stack. This avoids wasm_bindgen's `"closure invoked recursively
52    /// or after being dropped"` error which occurs when `set_hash()` fires
53    /// `hashchange` synchronously and the handler (or the reactive update chain
54    /// it triggers) calls `navigate()` again before the original dispatch finishes.
55    ///
56    /// Multiple rapid `navigate()` calls before the microtask fires are coalesced:
57    /// only the **last** target route wins, as earlier routes were superseded by
58    ///
59    /// # Arguments
60    ///
61    /// - `R: AsRef<str>` - The target route path.
62    pub fn navigate<R>(route: R)
63    where
64        R: AsRef<str>,
65    {
66        let route_string: String = route.as_ref().to_string();
67        DEFERRED_NAVIGATION.with(|cell: &Cell<Option<String>>| cell.set(Some(route_string)));
68        let deferred_closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
69            let target_route: Option<String> =
70                DEFERRED_NAVIGATION.with(|cell: &Cell<Option<String>>| cell.take());
71            if let Some(route_value) = target_route {
72                let nav_window: Window = web_sys::window().expect("no global window exists");
73                let nav_location: Location = nav_window.location();
74                let nav_new_hash: String = format!("{ROUTE_HASH_PREFIX}{route_value}");
75                let _ = nav_location.set_hash(&nav_new_hash);
76            }
77        }));
78        let window: Window = window().expect("no global window exists");
79        window.queue_microtask(deferred_closure.as_ref().unchecked_ref::<Function>());
80        deferred_closure.forget();
81    }
82
83    /// Creates a link click handler that navigates to the given route.
84    ///
85    /// Calls `event.prevent_default()` to prevent the `<a>` element's
86    /// default hash navigation, then programmatically navigates via
87    /// `navigate()`. Without `preventDefault`, both the `<a href>` default
88    /// behavior and `navigate()` would fire, potentially creating duplicate
89    /// history entries and causing incorrect browser back/forward behavior.
90    ///
91    /// # Arguments
92    ///
93    /// - `R: AsRef<str>` - The target route path.
94    ///
95    /// # Returns
96    ///
97    /// - `NativeEventHandler` - An event handler for click events.
98    pub fn link_handler<R>(route: R) -> NativeEventHandler
99    where
100        R: AsRef<str>,
101    {
102        let route_string: String = route.as_ref().to_string();
103        NativeEventHandler::create("click", move |event: Event| {
104            event.prevent_default();
105            Self::navigate(&route_string);
106        })
107    }
108
109    /// Checks whether the current viewport width qualifies as a mobile device.
110    ///
111    /// Uses `MOBILE_BREAKPOINT` (768px) as the threshold.
112    ///
113    /// # Returns
114    ///
115    /// - `bool` - `true` if the viewport width is less than the mobile breakpoint.
116    pub fn is_mobile() -> bool {
117        let window: Window = window().expect("no global window exists");
118        let width: f64 = window
119            .inner_width()
120            .ok()
121            .map(|value: JsValue| Number::from(value).value_of())
122            .unwrap_or(0.0);
123        width < MOBILE_BREAKPOINT as f64
124    }
125}