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}