1use wasm_bindgen::{JsCast, JsValue, prelude::Closure};
2use web_sys::{Event, History, ScrollRestoration, Window, 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 && let Some([x, y]) = get_current(&h) {
177 ScrollPosition { x, y }.scroll_to(w.clone())
178 }
179 }) as Box<dyn FnMut(Event)>);
180 self.window
181 .add_event_listener_with_callback(
182 "popstate",
183 &function.into_js_value().unchecked_into(),
184 )
185 .unwrap();
186 }
187}
188
189pub struct HashHistory {
192 do_scroll_restoration: bool,
193 history: History,
194 pathname: String,
195 window: Window,
196}
197
198impl Default for HashHistory {
199 fn default() -> Self {
200 Self::new(true)
201 }
202}
203
204impl HashHistory {
205 pub fn new(do_scroll_restoration: bool) -> Self {
210 let myself = Self::new_inner(do_scroll_restoration);
211
212 let current_route = dioxus_history::History::current_route(&myself);
213 let current_route_str = current_route.to_string();
214 let pathname_str = &myself.pathname;
215 let current_url = format!("{pathname_str}#{current_route_str}");
216 let state = myself.create_state();
217 let _ = replace_state_with_url(&myself.history, &state, Some(¤t_url));
218
219 myself
220 }
221
222 fn new_inner(do_scroll_restoration: bool) -> Self {
223 let window = window().expect("access to `window`");
224 let history = window.history().expect("`window` has access to `history`");
225 let pathname = window.location().pathname().unwrap();
226
227 if do_scroll_restoration {
228 history
229 .set_scroll_restoration(ScrollRestoration::Manual)
230 .expect("`history` can set scroll restoration");
231 }
232
233 Self {
234 do_scroll_restoration,
235 history,
236 pathname,
237 window,
238 }
239 }
240
241 fn scroll_pos(&self) -> ScrollPosition {
242 if self.do_scroll_restoration {
243 ScrollPosition::of_window(&self.window)
244 } else {
245 Default::default()
246 }
247 }
248
249 fn create_state(&self) -> [f64; 2] {
250 let scroll = self.scroll_pos();
251 [scroll.x, scroll.y]
252 }
253
254 fn full_path(&self, state: &String) -> String {
255 format!("{}#{state}", self.pathname)
256 }
257
258 fn handle_nav(&self) {
259 if self.do_scroll_restoration {
260 self.window.scroll_to_with_x_and_y(0.0, 0.0)
261 }
262 }
263}
264
265impl dioxus_history::History for HashHistory {
266 fn current_route(&self) -> String {
267 let location = self.window.location();
268
269 let hash = location.hash().unwrap();
270 if hash.is_empty() {
271 "/".to_owned()
273 } else {
274 hash.trim_start_matches("#").to_owned()
275 }
276 }
277
278 fn current_prefix(&self) -> Option<String> {
279 Some(format!("{}#", self.pathname))
280 }
281
282 fn go_back(&self) {
283 let _ = self.history.back();
284 }
285
286 fn go_forward(&self) {
287 let _ = self.history.forward();
288 }
289
290 fn push(&self, state: String) {
291 if state == self.current_route() {
292 return;
294 }
295
296 let w = window().expect("access to `window`");
297 let h = w.history().expect("`window` has access to `history`");
298
299 update_scroll(&w, &h);
301
302 if push_state_and_url(&self.history, &self.create_state(), self.full_path(&state)).is_ok() {
303 self.handle_nav();
304 }
305 }
306
307 fn replace(&self, state: String) {
308 if replace_state_with_url(
309 &self.history,
310 &self.create_state(),
311 Some(&self.full_path(&state)),
312 )
313 .is_ok()
314 {
315 self.handle_nav();
316 }
317 }
318
319 fn external(&self, url: String) -> bool {
320 self.window.location().set_href(&url).is_ok()
321 }
322
323 fn updater(&self, callback: std::sync::Arc<dyn Fn() + Send + Sync>) {
324 let w = self.window.clone();
325 let h = self.history.clone();
326 let d = self.do_scroll_restoration;
327
328 let function = Closure::wrap(Box::new(move |_| {
329 (*callback)();
330 if d && let Some([x, y]) = get_current(&h) {
331 ScrollPosition { x, y }.scroll_to(w.clone())
332 }
333 }) as Box<dyn FnMut(Event)>);
334 self.window
335 .add_event_listener_with_callback(
336 "popstate",
337 &function.into_js_value().unchecked_into(),
338 )
339 .unwrap();
340 }
341}
342
343#[derive(Clone, Copy, Debug, Default)]
344pub(crate) struct ScrollPosition {
345 pub x: f64,
346 pub y: f64,
347}
348
349impl ScrollPosition {
350 pub(crate) fn of_window(window: &Window) -> Self {
351 Self {
352 x: window.scroll_x().unwrap_or_default(),
353 y: window.scroll_y().unwrap_or_default(),
354 }
355 }
356
357 pub(crate) fn scroll_to(&self, window: Window) {
358 let Self { x, y } = *self;
359 let f = Closure::wrap(
360 Box::new(move || window.scroll_to_with_x_and_y(x, y)) as Box<dyn FnMut()>
361 );
362 web_sys::window()
363 .expect("should be run in a context with a `Window` object (dioxus cannot be run from a web worker)")
364 .request_animation_frame(&f.into_js_value().unchecked_into())
365 .expect("should register `requestAnimationFrame` OK");
366 }
367}
368
369pub(crate) fn replace_state_with_url(
370 history: &History,
371 value: &[f64; 2],
372 url: Option<&str>,
373) -> Result<(), JsValue> {
374 let position = js_sys::Array::new();
375 position.push(&JsValue::from(value[0]));
376 position.push(&JsValue::from(value[1]));
377 history.replace_state_with_url(&position, "", url)
378}
379
380pub(crate) fn push_state_and_url(
381 history: &History,
382 value: &[f64; 2],
383 url: String,
384) -> Result<(), JsValue> {
385 let position = js_sys::Array::new();
386 position.push(&JsValue::from(value[0]));
387 position.push(&JsValue::from(value[1]));
388 history.push_state_with_url(&position, "", Some(&url))
389}
390
391pub(crate) fn get_current(history: &History) -> Option<[f64; 2]> {
392 use wasm_bindgen::JsCast;
393 history.state().ok().and_then(|state| {
394 let state = state.dyn_into::<js_sys::Array>().ok()?;
395 let x = state.get(0).as_f64()?;
396 let y = state.get(1).as_f64()?;
397 Some([x, y])
398 })
399}
400
401fn update_scroll(window: &Window, history: &History) {
402 let scroll = ScrollPosition::of_window(window);
403 let _ = replace_state_with_url(history, &[scroll.x, scroll.y], None);
404}