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
use crate::*;
/// Implementation of route configuration construction.
impl EuvRouteConfig {
/// Creates a new route configuration.
///
/// # Arguments
///
/// - `&'static str` - The route path.
/// - `F: Fn() -> VirtualNode + 'static` - The component function.
///
/// # Returns
///
/// - `EuvRouteConfig` - The route configuration.
pub fn new<F>(path: &'static str, component: F) -> Self
where
F: Fn() -> VirtualNode + 'static,
{
Self {
path,
component: Rc::new(component),
}
}
}
/// Implementation of router navigation and viewport utilities.
impl Router {
/// Reads the current hash-based route from the browser URL.
///
/// # Returns
///
/// - `String` - The hash fragment without the leading `#`, or `DEFAULT_ROUTE_PATH` if empty.
pub fn current_route() -> String {
let window: Window = window().expect("no global window exists");
let hash: String = window.location().hash().unwrap_or_default();
let route: String = hash
.strip_prefix(ROUTE_HASH_PREFIX)
.unwrap_or(&hash)
.to_string();
if route.is_empty() {
DEFAULT_ROUTE_PATH.to_string()
} else {
route
}
}
/// Navigates to a new hash-based route.
///
/// Always defers the actual `location.set_hash()` call to `queueMicrotask`
/// to prevent synchronous `hashchange` dispatch while any caller frame is still
/// on the stack. This avoids wasm_bindgen's `"closure invoked recursively
/// or after being dropped"` error which occurs when `set_hash()` fires
/// `hashchange` synchronously and the handler (or the reactive update chain
/// it triggers) calls `navigate()` again before the original dispatch finishes.
///
/// Multiple rapid `navigate()` calls before the microtask fires are coalesced:
/// only the **last** target route wins, as earlier routes were superseded by
///
/// # Arguments
///
/// - `R: AsRef<str>` - The target route path.
pub fn navigate<R>(route: R)
where
R: AsRef<str>,
{
let route_string: String = route.as_ref().to_string();
DEFERRED_NAVIGATION.with(|cell: &Cell<Option<String>>| cell.set(Some(route_string)));
let deferred_closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
let target_route: Option<String> =
DEFERRED_NAVIGATION.with(|cell: &Cell<Option<String>>| cell.take());
if let Some(route_value) = target_route {
let nav_window: Window = web_sys::window().expect("no global window exists");
let nav_location: Location = nav_window.location();
let nav_new_hash: String = format!("{ROUTE_HASH_PREFIX}{route_value}");
let _ = nav_location.set_hash(&nav_new_hash);
}
}));
let window: Window = window().expect("no global window exists");
window.queue_microtask(deferred_closure.as_ref().unchecked_ref::<Function>());
deferred_closure.forget();
}
/// Creates a link click handler that navigates to the given route.
///
/// Calls `event.prevent_default()` to prevent the `<a>` element's
/// default hash navigation, then programmatically navigates via
/// `navigate()`. Without `preventDefault`, both the `<a href>` default
/// behavior and `navigate()` would fire, potentially creating duplicate
/// history entries and causing incorrect browser back/forward behavior.
///
/// # Arguments
///
/// - `R: AsRef<str>` - The target route path.
///
/// # Returns
///
/// - `NativeEventHandler` - An event handler for click events.
pub fn link_handler<R>(route: R) -> NativeEventHandler
where
R: AsRef<str>,
{
let route_string: String = route.as_ref().to_string();
NativeEventHandler::create("click", move |event: Event| {
event.prevent_default();
Self::navigate(&route_string);
})
}
/// Checks whether the current viewport width qualifies as a mobile device.
///
/// Uses `MOBILE_BREAKPOINT` (768px) as the threshold.
///
/// # Returns
///
/// - `bool` - `true` if the viewport width is less than the mobile breakpoint.
pub fn is_mobile() -> bool {
let window: Window = window().expect("no global window exists");
let width: f64 = window
.inner_width()
.ok()
.map(|value: JsValue| Number::from(value).value_of())
.unwrap_or(0.0);
width < MOBILE_BREAKPOINT as f64
}
}