From 7ae8403af701749296542e6679f8a1d24a1529cc Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 23 May 2023 11:22:57 -0500 Subject: [PATCH] update web history --- packages/router/Cargo.toml | 7 +- packages/router/src/history/memory.rs | 4 +- packages/router/src/history/mod.rs | 12 ++- packages/router/src/history/web.rs | 109 +++++++++++---------- packages/router/src/history/web_hash.rs | 28 +++--- packages/router/src/history/web_history.rs | 31 ++++++ packages/router/src/history/web_scroll.rs | 38 +++---- packages/router/src/routable.rs | 2 +- 8 files changed, 127 insertions(+), 104 deletions(-) create mode 100644 packages/router/src/history/web_history.rs diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index c8367c1cc..58bbc034e 100644 --- a/packages/router/Cargo.toml +++ b/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" } diff --git a/packages/router/src/history/memory.rs b/packages/router/src/history/memory.rs index 116b733fa..b8e4dd505 100644 --- a/packages/router/src/history/memory.rs +++ b/packages/router/src/history/memory.rs @@ -41,8 +41,8 @@ impl Default for MemoryHistory { } impl HistoryProvider for MemoryHistory { - 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 { diff --git a/packages/router/src/history/mod.rs b/packages/router/src/history/mod.rs index 4c58457b6..a8cffc405 100644 --- a/packages/router/src/history/mod.rs +++ b/packages/router/src/history/mod.rs @@ -15,11 +15,13 @@ pub use memory::*; mod web; #[cfg(feature = "web")] pub use web::*; +#[cfg(feature = "web")] +pub(crate) mod web_history; -#[cfg(feature = "web")] -mod web_hash; -#[cfg(feature = "web")] -pub use web_hash::*; +// #[cfg(feature = "web")] +// mod web_hash; +// #[cfg(feature = "web")] +// pub use web_hash::*; use crate::routable::Routable; @@ -48,7 +50,7 @@ pub trait HistoryProvider { /// 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. /// diff --git a/packages/router/src/history/web.rs b/packages/router/src/history/web.rs index 1594de0c8..f56d0c2d2 100644 --- a/packages/router/src/history/web.rs +++ b/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 { + state: R, + scroll: ScrollPosition, +} + /// A [`HistoryProvider`] that integrates with a browser via the [History API]. /// /// # Prefix @@ -32,9 +42,10 @@ pub struct WebHistory { listener_animation_frame: Arc>>, prefix: Option, window: Window, + phantom: std::marker::PhantomData, } -impl WebHistory { +impl WebHistory { /// 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,44 +77,41 @@ impl WebHistory { listener_navigation: None, listener_scroll, listener_animation_frame: Default::default(), - prefix: prefix, + prefix, window, + phantom: Default::default(), } } + + fn create_state(&self, state: R) -> WebHistoryState { + let scroll = self + .do_scroll_restoration + .then(|| ScrollPosition::of_window(&self.window)) + .unwrap_or_default(); + WebHistoryState { state, scroll } + } } -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(), +impl HistoryProvider for WebHistory +where + ::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_query(&self) -> Option { - 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 { self.prefix.clone() } @@ -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)); } })); } diff --git a/packages/router/src/history/web_hash.rs b/packages/router/src/history/web_hash.rs index 37af06e9b..a985b003f 100644 --- a/packages/router/src/history/web_hash.rs +++ b/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 { listener_scroll: Option, listener_animation_frame: Arc>>, window: Window, + phantom: std::marker::PhantomData, } -impl WebHashHistory { +impl WebHashHistory { /// 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 { - if path.starts_with("//") { - error!("cannot navigate to paths starting with `//`, got `{path}`"); - return None; - } - +impl WebHashHistory { + fn join_url_to_hash(&self, path: R) -> Option { 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 HistoryProvider for WebHashHistory { + fn current_route(&self) -> R { self.url() .map(|url| url.path().to_string()) .unwrap_or(String::from("/")) } - fn current_query(&self) -> Option { - self.url().and_then(|url| url.query().map(String::from)) - } - fn current_prefix(&self) -> Option { 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, diff --git a/packages/router/src/history/web_history.rs b/packages/router/src/history/web_history.rs new file mode 100644 index 000000000..7106f9f91 --- /dev/null +++ b/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( + 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( + 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(history: &History) -> Option { + history + .state() + .ok() + .and_then(|state| state.into_serde().ok()) +} diff --git a/packages/router/src/history/web_scroll.rs b/packages/router/src/history/web_scroll.rs index 0d6a09624..09a43bbf2 100644 --- a/packages/router/src/history/web_scroll.rs +++ b/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() -} +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_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 scroll_to(&self, window: Window) -> AnimationFrame { + let Self { x, y } = *self; + request_animation_frame(move |_| window.scroll_to_with_x_and_y(x, y)) } } - -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)) -} diff --git a/packages/router/src/routable.rs b/packages/router/src/routable.rs index 3cfcdbcec..a7c5f907d 100644 --- a/packages/router/src/routable.rs +++ b/packages/router/src/routable.rs @@ -94,7 +94,7 @@ impl> 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>; }