1use leptos::html;
21use leptos::prelude::*;
22use leptos_router::hooks::use_location;
23use std::sync::atomic::AtomicBool;
24#[cfg(target_arch = "wasm32")]
25use std::sync::atomic::Ordering;
26
27#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
29static CONNECTED: AtomicBool = AtomicBool::new(false);
30
31pub fn push(route: &str) {
33 #[cfg(target_arch = "wasm32")]
34 {
35 if CONNECTED.load(Ordering::Acquire) && crate::bridges::navigation::push(route) {
36 return;
37 }
38 if let Some(window) = web_sys::window() {
39 let _ = window.location().assign(route);
40 }
41 }
42 #[cfg(not(target_arch = "wasm32"))]
43 let _ = route;
44}
45
46pub fn replace(route: &str) {
48 #[cfg(target_arch = "wasm32")]
49 {
50 if CONNECTED.load(Ordering::Acquire) && crate::bridges::navigation::replace(route) {
51 return;
52 }
53 if let Some(window) = web_sys::window() {
54 let _ = window.location().replace(route);
55 }
56 }
57 #[cfg(not(target_arch = "wasm32"))]
58 let _ = route;
59}
60
61pub fn back() {
63 #[cfg(target_arch = "wasm32")]
64 {
65 if CONNECTED.load(Ordering::Acquire) && crate::bridges::navigation::back() {
66 return;
67 }
68 if let Some(history) = web_sys::window().and_then(|window| window.history().ok()) {
69 let _ = history.back();
70 }
71 }
72}
73
74pub fn forward() {
76 #[cfg(target_arch = "wasm32")]
77 {
78 if CONNECTED.load(Ordering::Acquire) && crate::bridges::navigation::forward() {
79 return;
80 }
81 if let Some(history) = web_sys::window().and_then(|window| window.history().ok()) {
82 let _ = history.forward();
83 }
84 }
85}
86
87pub fn external(url: &str) {
89 #[cfg(target_arch = "wasm32")]
90 {
91 if CONNECTED.load(Ordering::Acquire) && crate::bridges::navigation::external(url) {
92 return;
93 }
94 if let Some(window) = web_sys::window() {
95 let _ = window.location().assign(url);
96 }
97 }
98 #[cfg(not(target_arch = "wasm32"))]
99 let _ = url;
100}
101
102#[component]
112pub fn NavigationBridge() -> impl IntoView {
113 #[cfg(feature = "hydrate")]
114 {
115 use crate::context::UserContext;
116 use crate::models::user::UserRole;
117 use leptos_router::location::{BrowserUrl, LocationProvider};
118 use wasm_bindgen::prelude::Closure;
119
120 let browser_url = use_context::<BrowserUrl>();
121 let shared = Owner::current_shared_context();
122 let notify = StoredValue::new_local(None::<Closure<dyn FnMut()>>);
124 let prefetch = StoredValue::new_local(None::<Closure<dyn FnMut(String, bool)>>);
125 let user = use_context::<UserContext>();
126 Effect::new(move |_| {
127 let browser_url = browser_url.clone();
128 let shared = shared.clone();
129 leptos::task::spawn_local(async move {
130 while shared.as_ref().is_some_and(|sc| sc.during_hydration()) {
132 crate::utils::time::sleep_ms(10).await;
133 }
134 if notify.try_with_value(|_| ()).is_none() {
136 return;
137 }
138 if let Some(browser_url) = browser_url {
139 let closure = Closure::wrap(Box::new(move || {
140 if let Ok(url) = BrowserUrl::current() {
141 browser_url.as_url().set(url);
142 }
143 }) as Box<dyn FnMut()>);
144 let prefetch_closure =
145 Closure::wrap(Box::new(move |route: String, with_data: bool| {
146 let is_admin = user.is_some_and(|ctx| {
147 ctx.user
148 .try_with_untracked(|u| {
149 u.as_ref().is_some_and(|u| u.role == UserRole::Admin)
150 })
151 .unwrap_or(false)
152 });
153 crate::route_prefetch::prefetch(&route, with_data, is_admin);
154 }) as Box<dyn FnMut(String, bool)>);
155 if crate::bridges::navigation::connect(&closure, &prefetch_closure) {
156 CONNECTED.store(true, Ordering::Release);
157 notify.set_value(Some(closure));
158 prefetch.set_value(Some(prefetch_closure));
159 }
160 }
161 if let Some(root) = document().document_element() {
162 let _ = root.set_attribute("data-hydrated", "true");
163 }
164 });
165 });
166 on_cleanup(move || {
167 if CONNECTED.swap(false, Ordering::AcqRel) {
168 crate::bridges::navigation::disconnect();
169 }
170 });
171 }
172}
173
174fn route_key(pathname: &str, search: &str) -> String {
176 if search.is_empty() {
177 pathname.to_owned()
178 } else {
179 format!("{pathname}?{search}")
180 }
181}
182
183#[derive(Clone, Debug, Default, PartialEq, Eq)]
185pub struct RenderedRoute {
186 pub pathname: String,
187 pub search: String,
188}
189
190impl RenderedRoute {
191 pub fn key(&self) -> String {
193 route_key(&self.pathname, &self.search)
194 }
195}
196
197#[derive(Clone, Copy)]
199struct CommittedRoute(ReadSignal<RenderedRoute>);
200
201pub fn use_rendered_route() -> Memo<RenderedRoute> {
210 let location = use_location();
211 let CommittedRoute(committed) = expect_context::<CommittedRoute>();
212 Memo::new(move |previous: Option<&RenderedRoute>| {
213 let committed = committed.get();
214 if previous.is_some() {
215 committed
216 } else {
217 RenderedRoute {
218 pathname: location.pathname.get_untracked(),
219 search: location.search.get_untracked(),
220 }
221 }
222 })
223}
224
225#[component]
231pub fn RouteMarker(is_routing: ReadSignal<bool>, children: Children) -> impl IntoView {
232 let location = use_location();
233 let pathname = location.pathname;
234 let search = location.search;
235 let first_route = RenderedRoute {
236 pathname: pathname.get_untracked(),
237 search: search.get_untracked(),
238 };
239 let first_key = first_route.key();
240 let committed = RwSignal::new(first_route);
241 provide_context(CommittedRoute(committed.read_only()));
242 let marker = NodeRef::<html::Div>::new();
243
244 #[cfg(feature = "hydrate")]
245 Effect::new(move |was_routing: Option<bool>| {
246 let route = RenderedRoute {
247 pathname: pathname.get(),
248 search: search.get(),
249 };
250 if is_routing.get() {
251 return true;
252 }
253 let commit = move |route: &RenderedRoute| {
254 if committed.with_untracked(|current| current != route) {
255 committed.set(route.clone());
256 }
257 };
258 if was_routing == Some(true) {
261 commit(&route);
262 }
263 set_timeout(
266 move || {
267 if is_routing.get_untracked() {
269 return;
270 }
271 commit(&route);
273 let Some(element) = marker.get_untracked() else {
274 return;
275 };
276 let key = route.key();
277 let id = crate::bridges::navigation::navigation_id();
278 let _ = element.set_attribute("data-vt-route", &key);
279 let _ = element.set_attribute("data-vt-navigation", &id.to_string());
280 crate::bridges::navigation::rendered(id, &key);
281 },
282 std::time::Duration::ZERO,
283 );
284 false
285 });
286 #[cfg(not(feature = "hydrate"))]
287 let _ = is_routing;
288
289 view! {
290 <div
291 style="display: contents"
292 data-vt-route=first_key
293 data-vt-navigation="0"
294 node_ref=marker
295 >
296 {children()}
297 </div>
298 }
299}
300
301#[cfg(feature = "hydrate")]
304const ROUTE_PROGRESS_DELAY: std::time::Duration = std::time::Duration::from_millis(300);
305
306#[component]
309pub fn RouteProgress(is_routing: ReadSignal<bool>) -> impl IntoView {
310 let active = RwSignal::new(false);
311
312 #[cfg(feature = "hydrate")]
313 {
314 let generation = StoredValue::new(0_u64);
316 Effect::new(move |_| {
317 let routing = is_routing.get();
318 generation.update_value(|g| *g += 1);
319 if !routing {
320 active.set(false);
321 return;
322 }
323 let id = generation.get_value();
324 set_timeout(
325 move || {
326 if generation.get_value() == id && is_routing.get_untracked() {
327 active.set(true);
328 }
329 },
330 ROUTE_PROGRESS_DELAY,
331 );
332 });
333 }
334 #[cfg(not(feature = "hydrate"))]
335 let _ = is_routing;
336
337 view! {
338 <div class="route-progress" data-active=move || active.get().then_some("") aria-hidden="true">
339 <span></span>
340 </div>
341 }
342}