Skip to main content

euv_ui/component/router/view/
impl.rs

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