Quellcode durchsuchen

implement scroll restoration

Adrian Wannenmacher vor 2 Jahren
Ursprung
Commit
10c2abf0d8

+ 4 - 3
packages/router-core/Cargo.toml

@@ -13,14 +13,15 @@ futures-util = "0.3.25"
 gloo = { version = "0.8.0", optional = true }
 log = "0.4.17"
 regex = { version = "1.6.0", optional = true }
-serde = { version = "1.0.147", optional = true }
+serde = { version = "1.0.147", optional = true, features = ["derive"] }
+serde-wasm-bindgen = { version = "0.4.5", optional = true }
 serde_urlencoded = { version = "0.7.1", optional = true }
 url = "2.3.1"
 urlencoding = "2.1.2"
 wasm-bindgen = { version = "0.2.83", optional = true }
-web-sys = { version = "0.3.60", optional = true }
+web-sys = { version = "0.3.60", optional = true, features = ["ScrollRestoration"]}
 
 [features]
 regex = ["dep:regex"]
 serde = ["dep:serde", "serde_urlencoded"]
-web = ["gloo", "wasm-bindgen", "web-sys"]
+web = ["gloo", "dep:serde", "serde-wasm-bindgen", "wasm-bindgen", "web-sys"]

+ 8 - 5
packages/router-core/src/history/mod.rs

@@ -8,11 +8,6 @@
 
 use std::sync::Arc;
 
-#[cfg(feature = "web")]
-mod web_hash;
-#[cfg(feature = "web")]
-pub use web_hash::*;
-
 mod memory;
 pub use memory::*;
 
@@ -21,6 +16,14 @@ mod web;
 #[cfg(feature = "web")]
 pub use web::*;
 
+#[cfg(feature = "web")]
+mod web_hash;
+#[cfg(feature = "web")]
+pub use web_hash::*;
+
+#[cfg(feature = "web")]
+pub(crate) mod web_scroll;
+
 /// An integration with some kind of navigation history.
 ///
 /// Depending on your use case, your implementation may deviate from the described procedure. This

+ 113 - 39
packages/router-core/src/history/web.rs

@@ -1,11 +1,13 @@
-use gloo::{
-    history::{BrowserHistory, History, HistoryListener},
-    utils::window,
-};
+use std::sync::{Arc, Mutex};
+
+use gloo::{events::EventListener, render::AnimationFrame};
 use log::error;
-use web_sys::Window;
+use web_sys::{window, History, ScrollRestoration, Window};
 
-use super::HistoryProvider;
+use super::{
+    web_scroll::{top_left, update_history, update_scroll},
+    HistoryProvider,
+};
 
 /// A [`HistoryProvider`] that integrates with a browser via the [History API].
 ///
@@ -22,38 +24,60 @@ use super::HistoryProvider;
 ///
 /// [History API]: https://developer.mozilla.org/en-US/docs/Web/API/History_API
 pub struct WebHistory {
-    history: BrowserHistory,
-    listener_navigation: Option<HistoryListener>,
+    do_scroll_restoration: bool,
+    history: History,
+    listener_navigation: Option<EventListener>,
+    #[allow(dead_code)]
+    listener_scroll: Option<EventListener>,
+    listener_animation_frame: Arc<Mutex<Option<AnimationFrame>>>,
     prefix: Option<String>,
     window: Window,
 }
 
 impl WebHistory {
-    /// Create a new [`WebHistory`] with a prefix.
-    #[must_use]
-    pub fn with_prefix(prefix: impl Into<String>) -> Self {
-        Self {
-            prefix: Some(prefix.into()),
-            ..Default::default()
-        }
-    }
-}
+    /// Create a new [`WebHistory`].
+    ///
+    /// If `do_scroll_restoration` is [`true`], [`WebHistory`] will take control of the history
+    /// state. It'll also set the browsers scroll restoration to `manual`.
+    pub fn new(prefix: Option<String>, do_scroll_restoration: bool) -> Self {
+        let window = window().expect("access to `window`");
+        let history = window.history().expect("`window` has access to `history`");
+
+        let listener_scroll = match do_scroll_restoration {
+            true => {
+                history
+                    .set_scroll_restoration(ScrollRestoration::Manual)
+                    .expect("`history` can set scroll restoration");
+                let w = window.clone();
+                let h = history.clone();
+                let document = w.document().expect("`window` has access to `document`");
+
+                Some(EventListener::new(&document, "scroll", move |_| {
+                    update_history(&w, &h);
+                }))
+            }
+            false => None,
+        };
 
-impl Default for WebHistory {
-    fn default() -> Self {
         Self {
-            history: BrowserHistory::new(),
+            do_scroll_restoration,
+            history,
             listener_navigation: None,
-            prefix: None,
-            window: window(),
+            listener_scroll,
+            listener_animation_frame: Default::default(),
+            prefix: prefix,
+            window,
         }
     }
 }
 
 impl HistoryProvider for WebHistory {
     fn current_path(&self) -> String {
-        let location = self.history.location();
-        let path = location.path();
+        let path = self
+            .window
+            .location()
+            .pathname()
+            .unwrap_or_else(|_| String::from("/"));
 
         match &self.prefix {
             None => path.to_string(),
@@ -66,14 +90,17 @@ impl HistoryProvider for WebHistory {
     }
 
     fn current_query(&self) -> Option<String> {
-        let location = self.history.location();
-        let query = location.query_str();
-
-        if query.is_empty() {
-            None
-        } else {
-            Some(query.to_string())
-        }
+        self.window
+            .location()
+            .search()
+            .ok()
+            .map(|mut q| {
+                if q.starts_with('?') {
+                    q.remove(0);
+                }
+                q
+            })
+            .and_then(|q| q.is_empty().then_some(q))
     }
 
     fn current_prefix(&self) -> Option<String> {
@@ -81,25 +108,61 @@ impl HistoryProvider for WebHistory {
     }
 
     fn go_back(&mut self) {
-        self.history.back();
+        if let Err(e) = self.history.back() {
+            error!("failed to go back: {e:?}")
+        }
     }
 
     fn go_forward(&mut self) {
-        self.history.forward();
+        if let Err(e) = self.history.forward() {
+            error!("failed to go forward: {e:?}")
+        }
     }
 
     fn push(&mut self, path: String) {
-        self.history.push(match &self.prefix {
+        let path = match &self.prefix {
             None => path,
             Some(prefix) => format!("{prefix}{path}"),
-        });
+        };
+
+        let state = match self.do_scroll_restoration {
+            true => top_left(),
+            false => self.history.state().unwrap_or_default(),
+        };
+
+        let nav = self.history.push_state_with_url(&state, "", Some(&path));
+
+        match nav {
+            Ok(_) => {
+                if self.do_scroll_restoration {
+                    self.window.scroll_to_with_x_and_y(0.0, 0.0)
+                }
+            }
+            Err(e) => error!("failed to push state: {e:?}"),
+        }
     }
 
     fn replace(&mut self, path: String) {
-        self.history.replace(match &self.prefix {
+        let path = match &self.prefix {
             None => path,
             Some(prefix) => format!("{prefix}{path}"),
-        });
+        };
+
+        let state = match self.do_scroll_restoration {
+            true => top_left(),
+            false => self.history.state().unwrap_or_default(),
+        };
+
+        let nav = self.history.replace_state_with_url(&state, "", Some(&path));
+
+        match nav {
+            Ok(_) => {
+                if self.do_scroll_restoration {
+                    self.window.scroll_to_with_x_and_y(0.0, 0.0)
+                }
+            }
+            Err(e) => error!("failed to replace state: {e:?}"),
+        }
     }
 
     fn external(&mut self, url: String) -> bool {
@@ -113,6 +176,17 @@ impl HistoryProvider for WebHistory {
     }
 
     fn updater(&mut self, callback: std::sync::Arc<dyn Fn() + Send + Sync>) {
-        self.listener_navigation = Some(self.history.listen(move || (*callback)()));
+        let w = self.window.clone();
+        let h = self.history.clone();
+        let s = self.listener_animation_frame.clone();
+        let d = self.do_scroll_restoration;
+
+        self.listener_navigation = Some(EventListener::new(&self.window, "popstate", move |_| {
+            (*callback)();
+            if d {
+                let mut s = s.lock().expect("unpoisoned scroll mutex");
+                *s = Some(update_scroll(&w, &h));
+            }
+        }));
     }
 }

+ 85 - 18
packages/router-core/src/history/web_hash.rs

@@ -1,10 +1,14 @@
-use gloo::{events::EventListener, utils::window};
+use std::sync::{Arc, Mutex};
+
+use gloo::{events::EventListener, render::AnimationFrame, utils::window};
 use log::error;
 use url::Url;
-use wasm_bindgen::JsValue;
-use web_sys::{History, Window};
+use web_sys::{History, ScrollRestoration, Window};
 
-use super::HistoryProvider;
+use super::{
+    web_scroll::{top_left, update_history, update_scroll},
+    HistoryProvider,
+};
 
 const INITIAL_URL: &str = "dioxus-router-core://initial_url.invalid/";
 
@@ -18,16 +22,50 @@ const INITIAL_URL: &str = "dioxus-router-core://initial_url.invalid/";
 ///
 /// [History API]: https://developer.mozilla.org/en-US/docs/Web/API/History_API
 pub struct WebHashHistory {
+    do_scroll_restoration: bool,
     history: History,
+    listener_navigation: Option<EventListener>,
+    #[allow(dead_code)]
+    listener_scroll: Option<EventListener>,
+    listener_animation_frame: Arc<Mutex<Option<AnimationFrame>>>,
     window: Window,
 }
 
-impl Default for WebHashHistory {
-    fn default() -> Self {
+impl WebHashHistory {
+    /// Create a new [`WebHashHistory`].
+    ///
+    /// If `do_scroll_restoration` is [`true`], [`WebHashHistory`] will take control of the history
+    /// state. It'll also set the browsers scroll restoration to `manual`.
+    pub fn new(do_scroll_restoration: bool) -> Self {
         let window = window();
+        let history = window.history().expect("`window` has access to `history`");
+
+        history
+            .set_scroll_restoration(ScrollRestoration::Manual)
+            .expect("`history` can set scroll restoration");
+
+        let listener_scroll = match do_scroll_restoration {
+            true => {
+                history
+                    .set_scroll_restoration(ScrollRestoration::Manual)
+                    .expect("`history` can set scroll restoration");
+                let w = window.clone();
+                let h = history.clone();
+                let document = w.document().expect("`window` has access to `document`");
+
+                Some(EventListener::new(&document, "scroll", move |_| {
+                    update_history(&w, &h);
+                }))
+            }
+            false => None,
+        };
 
         Self {
-            history: window.history().expect("window has history"),
+            do_scroll_restoration,
+            history,
+            listener_navigation: None,
+            listener_scroll,
+            listener_animation_frame: Default::default(),
             window,
         }
     }
@@ -115,11 +153,20 @@ impl HistoryProvider for WebHashHistory {
             None => return,
         };
 
-        if let Err(e) = self
-            .history
-            .push_state_with_url(&JsValue::NULL, "", Some(&hash))
-        {
-            error!("failed to push state: {e:?}");
+        let state = match self.do_scroll_restoration {
+            true => top_left(),
+            false => self.history.state().unwrap_or_default(),
+        };
+
+        let nav = self.history.push_state_with_url(&state, "", Some(&hash));
+
+        match nav {
+            Ok(_) => {
+                if self.do_scroll_restoration {
+                    self.window.scroll_to_with_x_and_y(0.0, 0.0)
+                }
+            }
+            Err(e) => error!("failed to push state: {e:?}"),
         }
     }
 
@@ -129,11 +176,20 @@ impl HistoryProvider for WebHashHistory {
             None => return,
         };
 
-        if let Err(e) = self
-            .history
-            .replace_state_with_url(&JsValue::NULL, "", Some(&hash))
-        {
-            error!("failed to replace state: {e:?}");
+        let state = match self.do_scroll_restoration {
+            true => top_left(),
+            false => self.history.state().unwrap_or_default(),
+        };
+
+        let nav = self.history.replace_state_with_url(&state, "", Some(&hash));
+
+        match nav {
+            Ok(_) => {
+                if self.do_scroll_restoration {
+                    self.window.scroll_to_with_x_and_y(0.0, 0.0)
+                }
+            }
+            Err(e) => error!("failed to replace state: {e:?}"),
         }
     }
 
@@ -148,6 +204,17 @@ impl HistoryProvider for WebHashHistory {
     }
 
     fn updater(&mut self, callback: std::sync::Arc<dyn Fn() + Send + Sync>) {
-        let listener = EventListener::new(&self.window, "popstate", move |_| (*callback)());
+        let w = self.window.clone();
+        let h = self.history.clone();
+        let s = self.listener_animation_frame.clone();
+        let d = self.do_scroll_restoration;
+
+        self.listener_navigation = Some(EventListener::new(&self.window, "popstate", move |_| {
+            (*callback)();
+            if d {
+                let mut s = s.lock().expect("unpoisoned scroll mutex");
+                *s = Some(update_scroll(&w, &h));
+            }
+        }));
     }
 }

+ 38 - 0
packages/router-core/src/history/web_scroll.rs

@@ -0,0 +1,38 @@
+use gloo::render::{request_animation_frame, AnimationFrame};
+use log::error;
+use serde::{Deserialize, Serialize};
+use wasm_bindgen::JsValue;
+use web_sys::{History, Window};
+
+#[derive(Debug, Default, Deserialize, Serialize)]
+pub(crate) struct ScrollPosition {
+    x: f64,
+    y: f64,
+}
+
+pub(crate) fn top_left() -> JsValue {
+    serde_wasm_bindgen::to_value(&ScrollPosition::default()).unwrap()
+}
+
+pub(crate) fn update_history(window: &Window, history: &History) {
+    let position = serde_wasm_bindgen::to_value(&ScrollPosition {
+        x: window.scroll_x().unwrap_or_default(),
+        y: window.scroll_y().unwrap_or_default(),
+    })
+    .unwrap();
+
+    if let Err(e) = history.replace_state(&position, "") {
+        error!("failed to update scroll position: {e:?}");
+    }
+}
+
+pub(crate) fn update_scroll(window: &Window, history: &History) -> AnimationFrame
+{
+    let ScrollPosition { x, y } = history
+        .state()
+        .map(|state| serde_wasm_bindgen::from_value(state).unwrap_or_default())
+        .unwrap_or_default();
+
+    let w = window.clone();
+    request_animation_frame(move |_| w.scroll_to_with_x_and_y(x, y))
+}

+ 11 - 3
packages/router/examples/simple.rs

@@ -1,7 +1,7 @@
 #![allow(non_snake_case)]
 
 use dioxus::prelude::*;
-use dioxus_router::prelude::*;
+use dioxus_router::{history::MemoryHistory, prelude::*};
 
 fn main() {
     #[cfg(not(feature = "web"))]
@@ -13,8 +13,16 @@ fn main() {
 fn App(cx: Scope) -> Element {
     use_router(
         &cx,
-        &|| RouterConfiguration {
-            ..Default::default()
+        &|| {
+            #[cfg(not(feature = "web"))]
+            let history = MemoryHistory::default();
+            #[cfg(feature = "web")]
+            let history = dioxus_router::history::WebHistory::new(None, true);
+
+            RouterConfiguration {
+                history: Box::new(history),
+                ..Default::default()
+            }
         },
         &|| {
             Segment::content(comp(Home))

+ 1 - 6
packages/router/src/hooks/use_router.rs

@@ -188,16 +188,11 @@ pub struct RouterConfiguration {
 
 impl Default for RouterConfiguration {
     fn default() -> Self {
-        #[cfg(not(feature = "web"))]
-        let history = Box::<MemoryHistory>::default();
-        #[cfg(feature = "web")]
-        let history = Box::<crate::history::WebHistory>::default();
-
         Self {
             failure_external_navigation: comp(FailureExternalNavigation),
             failure_named_navigation: comp(FailureNamedNavigation),
             failure_redirection_limit: comp(FailureRedirectionLimit),
-            history: history,
+            history: Box::<MemoryHistory>::default(),
             on_update: None,
             synchronous: false,
         }