1use wasm_bindgen::{prelude::Closure, JsCast, JsValue};
2use web_sys::{window, Event, History, ScrollRestoration, Window};
3
4pub struct WebHistory {
17 do_scroll_restoration: bool,
18 history: History,
19 prefix: Option<String>,
20 window: Window,
21}
22
23impl Default for WebHistory {
24 fn default() -> Self {
25 Self::new(None, true)
26 }
27}
28
29impl WebHistory {
30 pub fn new(prefix: Option<String>, do_scroll_restoration: bool) -> Self {
35 let myself = Self::new_inner(prefix, do_scroll_restoration);
36
37 let current_route = dioxus_history::History::current_route(&myself);
38 let current_route_str = current_route.to_string();
39 let prefix_str = myself.prefix.as_deref().unwrap_or("");
40 let current_url = format!("{prefix_str}{current_route_str}");
41 let state = myself.create_state();
42 let _ = replace_state_with_url(&myself.history, &state, Some(¤t_url));
43
44 myself
45 }
46
47 fn new_inner(prefix: Option<String>, do_scroll_restoration: bool) -> Self {
48 let window = window().expect("access to `window`");
49 let history = window.history().expect("`window` has access to `history`");
50
51 if do_scroll_restoration {
52 history
53 .set_scroll_restoration(ScrollRestoration::Manual)
54 .expect("`history` can set scroll restoration");
55 }
56
57 let prefix = prefix
58 .or_else(dioxus_cli_config::web_base_path)
60 .as_ref()
62 .map(|prefix| prefix.trim_matches('/'))
63 .filter(|prefix| !prefix.is_empty())
65 .map(|prefix| format!("/{prefix}"));
67
68 Self {
69 do_scroll_restoration,
70 history,
71 prefix,
72 window,
73 }
74 }
75
76 fn scroll_pos(&self) -> ScrollPosition {
77 if self.do_scroll_restoration {
78 ScrollPosition::of_window(&self.window)
79 } else {
80 Default::default()
81 }
82 }
83
84 fn create_state(&self) -> [f64; 2] {
85 let scroll = self.scroll_pos();
86 [scroll.x, scroll.y]
87 }
88
89 fn handle_nav(&self) {
90 if self.do_scroll_restoration {
91 self.window.scroll_to_with_x_and_y(0.0, 0.0)
92 }
93 }
94
95 fn route_from_location(&self) -> String {
96 let location = self.window.location();
97 let path = location.pathname().unwrap_or_else(|_| "/".into())
98 + &location.search().unwrap_or("".into())
99 + &location.hash().unwrap_or("".into());
100 let mut path = match self.prefix {
101 None => &path,
102 Some(ref prefix) => path.strip_prefix(prefix).unwrap_or(prefix),
103 };
104 if path.is_empty() {
106 path = "/"
107 }
108 path.to_string()
109 }
110
111 fn full_path(&self, state: &String) -> String {
112 match &self.prefix {
113 None => state.to_string(),
114 Some(prefix) => format!("{prefix}{state}"),
115 }
116 }
117}
118
119impl dioxus_history::History for WebHistory {
120 fn current_route(&self) -> String {
121 self.route_from_location()
122 }
123
124 fn current_prefix(&self) -> Option<String> {
125 self.prefix.clone()
126 }
127
128 fn go_back(&self) {
129 let _ = self.history.back();
130 }
131
132 fn go_forward(&self) {
133 let _ = self.history.forward();
134 }
135
136 fn push(&self, state: String) {
137 if state == self.current_route() {
138 return;
140 }
141
142 let w = window().expect("access to `window`");
143 let h = w.history().expect("`window` has access to `history`");
144
145 update_scroll(&w, &h);
147
148 if push_state_and_url(&self.history, &self.create_state(), self.full_path(&state)).is_ok() {
149 self.handle_nav();
150 }
151 }
152
153 fn replace(&self, state: String) {
154 if replace_state_with_url(
155 &self.history,
156 &self.create_state(),
157 Some(&self.full_path(&state)),
158 )
159 .is_ok()
160 {
161 self.handle_nav();
162 }
163 }
164
165 fn external(&self, url: String) -> bool {
166 self.window.location().set_href(&url).is_ok()
167 }
168
169 fn updater(&self, callback: std::sync::Arc<dyn Fn() + Send + Sync>) {
170 let w = self.window.clone();
171 let h = self.history.clone();
172 let d = self.do_scroll_restoration;
173
174 let function = Closure::wrap(Box::new(move |_| {
175 (*callback)();
176 if d {
177 if let Some([x, y]) = get_current(&h) {
178 ScrollPosition { x, y }.scroll_to(w.clone())
179 }
180 }
181 }) as Box<dyn FnMut(Event)>);
182 self.window
183 .add_event_listener_with_callback(
184 "popstate",
185 &function.into_js_value().unchecked_into(),
186 )
187 .unwrap();
188 }
189}
190
191pub struct HashHistory {
194 do_scroll_restoration: bool,
195 history: History,
196 pathname: String,
197 window: Window,
198}
199
200impl Default for HashHistory {
201 fn default() -> Self {
202 Self::new(true)
203 }
204}
205
206impl HashHistory {
207 pub fn new(do_scroll_restoration: bool) -> Self {
212 let myself = Self::new_inner(do_scroll_restoration);
213
214 let current_route = dioxus_history::History::current_route(&myself);
215 let current_route_str = current_route.to_string();
216 let pathname_str = &myself.pathname;
217 let current_url = format!("{pathname_str}#{current_route_str}");
218 let state = myself.create_state();
219 let _ = replace_state_with_url(&myself.history, &state, Some(¤t_url));
220
221 myself
222 }
223
224 fn new_inner(do_scroll_restoration: bool) -> Self {
225 let window = window().expect("access to `window`");
226 let history = window.history().expect("`window` has access to `history`");
227 let pathname = window.location().pathname().unwrap();
228
229 if do_scroll_restoration {
230 history
231 .set_scroll_restoration(ScrollRestoration::Manual)
232 .expect("`history` can set scroll restoration");
233 }
234
235 Self {
236 do_scroll_restoration,
237 history,
238 pathname,
239 window,
240 }
241 }
242
243 fn scroll_pos(&self) -> ScrollPosition {
244 if self.do_scroll_restoration {
245 ScrollPosition::of_window(&self.window)
246 } else {
247 Default::default()
248 }
249 }
250
251 fn create_state(&self) -> [f64; 2] {
252 let scroll = self.scroll_pos();
253 [scroll.x, scroll.y]
254 }
255
256 fn full_path(&self, state: &String) -> String {
257 format!("{}#{state}", self.pathname)
258 }
259
260 fn handle_nav(&self) {
261 if self.do_scroll_restoration {
262 self.window.scroll_to_with_x_and_y(0.0, 0.0)
263 }
264 }
265}
266
267impl dioxus_history::History for HashHistory {
268 fn current_route(&self) -> String {
269 let location = self.window.location();
270
271 let hash = location.hash().unwrap();
272 if hash.is_empty() {
273 "/".to_owned()
275 } else {
276 hash.trim_start_matches("#").to_owned()
277 }
278 }
279
280 fn current_prefix(&self) -> Option<String> {
281 Some(format!("{}#", self.pathname))
282 }
283
284 fn go_back(&self) {
285 let _ = self.history.back();
286 }
287
288 fn go_forward(&self) {
289 let _ = self.history.forward();
290 }
291
292 fn push(&self, state: String) {
293 if state == self.current_route() {
294 return;
296 }
297
298 let w = window().expect("access to `window`");
299 let h = w.history().expect("`window` has access to `history`");
300
301 update_scroll(&w, &h);
303
304 if push_state_and_url(&self.history, &self.create_state(), self.full_path(&state)).is_ok() {
305 self.handle_nav();
306 }
307 }
308
309 fn replace(&self, state: String) {
310 if replace_state_with_url(
311 &self.history,
312 &self.create_state(),
313 Some(&self.full_path(&state)),
314 )
315 .is_ok()
316 {
317 self.handle_nav();
318 }
319 }
320
321 fn external(&self, url: String) -> bool {
322 self.window.location().set_href(&url).is_ok()
323 }
324
325 fn updater(&self, callback: std::sync::Arc<dyn Fn() + Send + Sync>) {
326 let w = self.window.clone();
327 let h = self.history.clone();
328 let d = self.do_scroll_restoration;
329
330 let function = Closure::wrap(Box::new(move |_| {
331 (*callback)();
332 if d {
333 if let Some([x, y]) = get_current(&h) {
334 ScrollPosition { x, y }.scroll_to(w.clone())
335 }
336 }
337 }) as Box<dyn FnMut(Event)>);
338 self.window
339 .add_event_listener_with_callback(
340 "popstate",
341 &function.into_js_value().unchecked_into(),
342 )
343 .unwrap();
344 }
345}
346
347#[derive(Clone, Copy, Debug, Default)]
348pub(crate) struct ScrollPosition {
349 pub x: f64,
350 pub y: f64,
351}
352
353impl ScrollPosition {
354 pub(crate) fn of_window(window: &Window) -> Self {
355 Self {
356 x: window.scroll_x().unwrap_or_default(),
357 y: window.scroll_y().unwrap_or_default(),
358 }
359 }
360
361 pub(crate) fn scroll_to(&self, window: Window) {
362 let Self { x, y } = *self;
363 let f = Closure::wrap(
364 Box::new(move || window.scroll_to_with_x_and_y(x, y)) as Box<dyn FnMut()>
365 );
366 web_sys::window()
367 .expect("should be run in a context with a `Window` object (dioxus cannot be run from a web worker)")
368 .request_animation_frame(&f.into_js_value().unchecked_into())
369 .expect("should register `requestAnimationFrame` OK");
370 }
371}
372
373pub(crate) fn replace_state_with_url(
374 history: &History,
375 value: &[f64; 2],
376 url: Option<&str>,
377) -> Result<(), JsValue> {
378 let position = js_sys::Array::new();
379 position.push(&JsValue::from(value[0]));
380 position.push(&JsValue::from(value[1]));
381 history.replace_state_with_url(&position, "", url)
382}
383
384pub(crate) fn push_state_and_url(
385 history: &History,
386 value: &[f64; 2],
387 url: String,
388) -> Result<(), JsValue> {
389 let position = js_sys::Array::new();
390 position.push(&JsValue::from(value[0]));
391 position.push(&JsValue::from(value[1]));
392 history.push_state_with_url(&position, "", Some(&url))
393}
394
395pub(crate) fn get_current(history: &History) -> Option<[f64; 2]> {
396 use wasm_bindgen::JsCast;
397 history.state().ok().and_then(|state| {
398 let state = state.dyn_into::<js_sys::Array>().ok()?;
399 let x = state.get(0).as_f64()?;
400 let y = state.get(1).as_f64()?;
401 Some([x, y])
402 })
403}
404
405fn update_scroll(window: &Window, history: &History) {
406 let scroll = ScrollPosition::of_window(window);
407 let _ = replace_state_with_url(history, &[scroll.x, scroll.y], None);
408}