Quellcode durchsuchen

update web history

Evan Almloff vor 2 Jahren
Ursprung
Commit
7ae8403af7

+ 6 - 1
packages/router/Cargo.toml

@@ -14,14 +14,19 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
 anyhow = "1.0.66"
 dioxus = { path="../dioxus" }
 dioxus-router-macro = { path="../router-macro" }
+gloo = { version = "0.8.0", optional = true }
 log = "0.4.17"
 serde = { version = "1.0.163", features = ["derive"] }
 thiserror = "1.0.37"
 url = "2.3.1"
+wasm-bindgen = { version = "0.2.86", optional = true }
+web-sys = { version = "0.3.60", optional = true, features = ["ScrollRestoration"] }
+gloo-utils = { version = "0.1.6", optional = true, features = ["serde"] }
 
 [features]
+default = ["web"]
 wasm_test = []
-web = []
+web = ["gloo", "web-sys", "wasm-bindgen", "gloo-utils"]
 
 [dev-dependencies]
 dioxus = { path = "../dioxus" }

+ 2 - 2
packages/router/src/history/memory.rs

@@ -41,8 +41,8 @@ impl<R: Routable> Default for MemoryHistory<R> {
 }
 
 impl<R: Routable> HistoryProvider<R> for MemoryHistory<R> {
-    fn current_route(&self) -> &R {
-        self.current.as_ref().expect("current route is not set")
+    fn current_route(&self) -> R {
+        self.current.clone().expect("current route is not set")
     }
 
     fn can_go_back(&self) -> bool {

+ 7 - 5
packages/router/src/history/mod.rs

@@ -15,11 +15,13 @@ pub use memory::*;
 mod web;
 #[cfg(feature = "web")]
 pub use web::*;
-
-#[cfg(feature = "web")]
-mod web_hash;
 #[cfg(feature = "web")]
-pub use web_hash::*;
+pub(crate) mod web_history;
+
+// #[cfg(feature = "web")]
+// mod web_hash;
+// #[cfg(feature = "web")]
+// pub use web_hash::*;
 
 use crate::routable::Routable;
 
@@ -48,7 +50,7 @@ pub trait HistoryProvider<R: Routable> {
     /// assert_eq!(history.current_path(), "/path");
     /// ```
     #[must_use]
-    fn current_route(&self) -> &R;
+    fn current_route(&self) -> R;
 
     /// Get the current path prefix of the URL.
     ///

+ 56 - 53
packages/router/src/history/web.rs

@@ -2,13 +2,23 @@ use std::sync::{Arc, Mutex};
 
 use gloo::{events::EventListener, render::AnimationFrame};
 use log::error;
+use serde::{de::DeserializeOwned, Deserialize, Serialize};
 use web_sys::{window, History, ScrollRestoration, Window};
 
+use crate::routable::Routable;
+
 use super::{
-    web_scroll::{top_left, update_history, update_scroll},
+    web_history::{get_current, push_state_and_url, replace_state_with_url},
+    web_scroll::ScrollPosition,
     HistoryProvider,
 };
 
+#[derive(Deserialize, Serialize)]
+struct WebHistoryState<R> {
+    state: R,
+    scroll: ScrollPosition,
+}
+
 /// A [`HistoryProvider`] that integrates with a browser via the [History API].
 ///
 /// # Prefix
@@ -32,9 +42,10 @@ pub struct WebHistory<R: Serialize + DeserializeOwned> {
     listener_animation_frame: Arc<Mutex<Option<AnimationFrame>>>,
     prefix: Option<String>,
     window: Window,
+    phantom: std::marker::PhantomData<R>,
 }
 
-impl WebHistory {
+impl<R: Serialize + DeserializeOwned> WebHistory<R> {
     /// Create a new [`WebHistory`].
     ///
     /// If `do_scroll_restoration` is [`true`], [`WebHistory`] will take control of the history
@@ -52,9 +63,10 @@ impl WebHistory {
                 let h = history.clone();
                 let document = w.document().expect("`window` has access to `document`");
 
-                Some(EventListener::new(&document, "scroll", move |_| {
-                    update_history(&w, &h);
-                }))
+                // Some(EventListener::new(&document, "scroll", move |_| {
+                //     set_state(&w, &h);
+                // }))
+                None
             }
             false => None,
         };
@@ -65,42 +77,39 @@ impl WebHistory {
             listener_navigation: None,
             listener_scroll,
             listener_animation_frame: Default::default(),
-            prefix: prefix,
+            prefix,
             window,
+            phantom: Default::default(),
         }
     }
-}
 
-impl HistoryProvider for WebHistory {
-    fn current_route(&self) -> String {
-        let path = self
-            .window
-            .location()
-            .pathname()
-            .unwrap_or_else(|_| String::from("/"));
-
-        match &self.prefix {
-            None => path.to_string(),
-            Some(prefix) => path
-                .starts_with(prefix)
-                .then(|| path.split_at(prefix.len()).1)
-                .unwrap_or("/")
-                .to_string(),
-        }
+    fn create_state(&self, state: R) -> WebHistoryState<R> {
+        let scroll = self
+            .do_scroll_restoration
+            .then(|| ScrollPosition::of_window(&self.window))
+            .unwrap_or_default();
+        WebHistoryState { state, scroll }
     }
+}
 
-    fn current_query(&self) -> Option<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))
+impl<R: Serialize + DeserializeOwned + Routable> HistoryProvider<R> for WebHistory<R>
+where
+    <R as std::str::FromStr>::Err: std::fmt::Display,
+{
+    fn current_route(&self) -> R {
+        match get_current(&self.history) {
+            // Try to get the route from the history state
+            Some(route) => route,
+            // If that fails, get the route from the current URL
+            None => R::from_str(
+                &self
+                    .window
+                    .location()
+                    .pathname()
+                    .unwrap_or_else(|_| String::from("/")),
+            )
+            .unwrap_or_else(|err| panic!("{}", err)),
+        }
     }
 
     fn current_prefix(&self) -> Option<String> {
@@ -119,18 +128,15 @@ impl HistoryProvider for WebHistory {
         }
     }
 
-    fn push(&mut self, path: String) {
+    fn push(&mut self, state: R) {
         let path = match &self.prefix {
-            None => path,
-            Some(prefix) => format!("{prefix}{path}"),
+            None => format!("{state}"),
+            Some(prefix) => format!("{prefix}{state}"),
         };
 
-        let state = match self.do_scroll_restoration {
-            true => top_left(),
-            false => self.history.state().unwrap_or_default(),
-        };
+        let state = self.create_state(state);
 
-        let nav = self.history.push_state_with_url(&state, "", Some(&path));
+        let nav = push_state_and_url(&self.history, &state, path);
 
         match nav {
             Ok(_) => {
@@ -142,18 +148,15 @@ impl HistoryProvider for WebHistory {
         }
     }
 
-    fn replace(&mut self, path: String) {
+    fn replace(&mut self, state: R) {
         let path = match &self.prefix {
-            None => path,
-            Some(prefix) => format!("{prefix}{path}"),
+            None => format!("{state}"),
+            Some(prefix) => format!("{prefix}{state}"),
         };
 
-        let state = match self.do_scroll_restoration {
-            true => top_left(),
-            false => self.history.state().unwrap_or_default(),
-        };
+        let state = self.create_state(state);
 
-        let nav = self.history.replace_state_with_url(&state, "", Some(&path));
+        let nav = replace_state_with_url(&self.history, &state, Some(&path));
 
         match nav {
             Ok(_) => {
@@ -184,8 +187,8 @@ impl HistoryProvider for WebHistory {
         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));
+                // let mut s = s.lock().expect("unpoisoned scroll mutex");
+                // *s = Some(update_scroll(&w, &h));
             }
         }));
     }

+ 12 - 16
packages/router/src/history/web_hash.rs

@@ -2,9 +2,12 @@ use std::sync::{Arc, Mutex};
 
 use gloo::{events::EventListener, render::AnimationFrame, utils::window};
 use log::error;
+use serde::{de::DeserializeOwned, Serialize};
 use url::Url;
 use web_sys::{History, ScrollRestoration, Window};
 
+use crate::routable::Routable;
+
 use super::{
     web_scroll::{top_left, update_history, update_scroll},
     HistoryProvider,
@@ -29,9 +32,10 @@ pub struct WebHashHistory<R: Serialize + DeserializeOwned> {
     listener_scroll: Option<EventListener>,
     listener_animation_frame: Arc<Mutex<Option<AnimationFrame>>>,
     window: Window,
+    phantom: std::marker::PhantomData<R>,
 }
 
-impl WebHashHistory {
+impl<R: Serialize + DeserializeOwned> WebHashHistory<R> {
     /// Create a new [`WebHashHistory`].
     ///
     /// If `do_scroll_restoration` is [`true`], [`WebHashHistory`] will take control of the history
@@ -67,17 +71,13 @@ impl WebHashHistory {
             listener_scroll,
             listener_animation_frame: Default::default(),
             window,
+            phantom: Default::default(),
         }
     }
 }
 
-impl WebHashHistory {
-    fn join_url_to_hash(&self, path: String) -> Option<String> {
-        if path.starts_with("//") {
-            error!("cannot navigate to paths starting with `//`, got `{path}`");
-            return None;
-        }
-
+impl<R: Serialize + DeserializeOwned> WebHashHistory<R> {
+    fn join_url_to_hash(&self, path: R) -> Option<String> {
         let url = match self.url() {
             Some(c) => match c.join(&path) {
                 Ok(new) => new,
@@ -120,17 +120,13 @@ impl WebHashHistory {
     }
 }
 
-impl HistoryProvider for WebHashHistory {
-    fn current_route(&self) -> String {
+impl<R: Serialize + DeserializeOwned + Routable> HistoryProvider<R> for WebHashHistory<R> {
+    fn current_route(&self) -> R {
         self.url()
             .map(|url| url.path().to_string())
             .unwrap_or(String::from("/"))
     }
 
-    fn current_query(&self) -> Option<String> {
-        self.url().and_then(|url| url.query().map(String::from))
-    }
-
     fn current_prefix(&self) -> Option<String> {
         Some(String::from("#"))
     }
@@ -147,7 +143,7 @@ impl HistoryProvider for WebHashHistory {
         }
     }
 
-    fn push(&mut self, path: String) {
+    fn push(&mut self, path: R) {
         let hash = match self.join_url_to_hash(path) {
             Some(hash) => hash,
             None => return,
@@ -170,7 +166,7 @@ impl HistoryProvider for WebHashHistory {
         }
     }
 
-    fn replace(&mut self, path: String) {
+    fn replace(&mut self, path: R) {
         let hash = match self.join_url_to_hash(path) {
             Some(hash) => hash,
             None => return,

+ 31 - 0
packages/router/src/history/web_history.rs

@@ -0,0 +1,31 @@
+use gloo_utils::format::JsValueSerdeExt;
+use serde::{de::DeserializeOwned, Serialize};
+use wasm_bindgen::JsValue;
+use web_sys::History;
+
+pub(crate) fn replace_state_with_url<V: Serialize>(
+    history: &History,
+    value: &V,
+    url: Option<&str>,
+) -> Result<(), JsValue> {
+    let position = JsValue::from_serde(value).unwrap();
+
+    history.replace_state_with_url(&position, "", url)
+}
+
+pub(crate) fn push_state_and_url<V: Serialize>(
+    history: &History,
+    value: &V,
+    url: String,
+) -> Result<(), JsValue> {
+    let position = JsValue::from_serde(value).unwrap();
+
+    history.push_state_with_url(&position, "", Some(&url))
+}
+
+pub(crate) fn get_current<V: DeserializeOwned>(history: &History) -> Option<V> {
+    history
+        .state()
+        .ok()
+        .and_then(|state| state.into_serde().ok())
+}

+ 12 - 26
packages/router/src/history/web_scroll.rs

@@ -1,37 +1,23 @@
 use gloo::render::{request_animation_frame, AnimationFrame};
-use log::error;
 use serde::{Deserialize, Serialize};
-use wasm_bindgen::JsValue;
-use web_sys::{History, Window};
+use web_sys::Window;
 
-#[derive(Debug, Default, Deserialize, Serialize)]
+#[derive(Clone, Copy, 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:?}");
+impl ScrollPosition {
+    pub(crate) fn of_window(window: &Window) -> Self {
+        Self {
+            x: window.scroll_x().unwrap_or_default(),
+            y: window.scroll_y().unwrap_or_default(),
+        }
     }
-}
-
-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))
+    pub(crate) fn scroll_to(&self, window: Window) -> AnimationFrame {
+        let Self { x, y } = *self;
+        request_animation_frame(move |_| window.scroll_to_with_x_and_y(x, y))
+    }
 }

+ 1 - 1
packages/router/src/routable.rs

@@ -94,7 +94,7 @@ impl<I: std::iter::FromIterator<String>> FromRouteSegments for I {
 }
 
 /// Something that can be routed to
-pub trait Routable: std::fmt::Display + std::str::FromStr + 'static {
+pub trait Routable: std::fmt::Display + std::str::FromStr + Clone + 'static {
     /// Render the route at the given level
     fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a>;
 }