update web history
This commit is contained in:
1 parent
e4b8fbfafe
commit
7ae8403af7
8 files changed
+127
-104
No files matched your search
@@ -14,14 +14,19 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
|||||||
anyhow = "1.0.66"
|
anyhow = "1.0.66"
|
||||||
dioxus = { path="../dioxus" }
|
dioxus = { path="../dioxus" }
|
||||||
dioxus-router-macro = { path="../router-macro" }
|
dioxus-router-macro = { path="../router-macro" }
|
||||||
|
gloo = { version = "0.8.0", optional = true }
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
serde = { version = "1.0.163", features = ["derive"] }
|
serde = { version = "1.0.163", features = ["derive"] }
|
||||||
thiserror = "1.0.37"
|
thiserror = "1.0.37"
|
||||||
url = "2.3.1"
|
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]
|
[features]
|
||||||
|
default = ["web"]
|
||||||
wasm_test = []
|
wasm_test = []
|
||||||
web = []
|
web = ["gloo", "web-sys", "wasm-bindgen", "gloo-utils"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
dioxus = { path = "../dioxus" }
|
dioxus = { path = "../dioxus" }
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ impl<R: Routable> Default for MemoryHistory<R> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<R: Routable> HistoryProvider<R> for MemoryHistory<R> {
|
impl<R: Routable> HistoryProvider<R> for MemoryHistory<R> {
|
||||||
fn current_route(&self) -> &R {
|
fn current_route(&self) -> R {
|
||||||
self.current.as_ref().expect("current route is not set")
|
self.current.clone().expect("current route is not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn can_go_back(&self) -> bool {
|
fn can_go_back(&self) -> bool {
|
||||||
|
|||||||
@@ -15,11 +15,13 @@ pub use memory::*;
|
|||||||
mod web;
|
mod web;
|
||||||
#[cfg(feature = "web")]
|
#[cfg(feature = "web")]
|
||||||
pub use web::*;
|
pub use web::*;
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
pub(crate) mod web_history;
|
||||||
|
|
||||||
#[cfg(feature = "web")]
|
// #[cfg(feature = "web")]
|
||||||
mod web_hash;
|
// mod web_hash;
|
||||||
#[cfg(feature = "web")]
|
// #[cfg(feature = "web")]
|
||||||
pub use web_hash::*;
|
// pub use web_hash::*;
|
||||||
|
|
||||||
use crate::routable::Routable;
|
use crate::routable::Routable;
|
||||||
|
|
||||||
@@ -48,7 +50,7 @@ pub trait HistoryProvider<R: Routable> {
|
|||||||
/// assert_eq!(history.current_path(), "/path");
|
/// assert_eq!(history.current_path(), "/path");
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
fn current_route(&self) -> &R;
|
fn current_route(&self) -> R;
|
||||||
|
|
||||||
/// Get the current path prefix of the URL.
|
/// Get the current path prefix of the URL.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -2,13 +2,23 @@ use std::sync::{Arc, Mutex};
|
|||||||
|
|
||||||
use gloo::{events::EventListener, render::AnimationFrame};
|
use gloo::{events::EventListener, render::AnimationFrame};
|
||||||
use log::error;
|
use log::error;
|
||||||
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
use web_sys::{window, History, ScrollRestoration, Window};
|
use web_sys::{window, History, ScrollRestoration, Window};
|
||||||
|
|
||||||
|
use crate::routable::Routable;
|
||||||
|
|
||||||
use super::{
|
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,
|
HistoryProvider,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
struct WebHistoryState<R> {
|
||||||
|
state: R,
|
||||||
|
scroll: ScrollPosition,
|
||||||
|
}
|
||||||
|
|
||||||
/// A [`HistoryProvider`] that integrates with a browser via the [History API].
|
/// A [`HistoryProvider`] that integrates with a browser via the [History API].
|
||||||
///
|
///
|
||||||
/// # Prefix
|
/// # Prefix
|
||||||
@@ -32,9 +42,10 @@ pub struct WebHistory<R: Serialize + DeserializeOwned> {
|
|||||||
listener_animation_frame: Arc<Mutex<Option<AnimationFrame>>>,
|
listener_animation_frame: Arc<Mutex<Option<AnimationFrame>>>,
|
||||||
prefix: Option<String>,
|
prefix: Option<String>,
|
||||||
window: Window,
|
window: Window,
|
||||||
|
phantom: std::marker::PhantomData<R>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebHistory {
|
impl<R: Serialize + DeserializeOwned> WebHistory<R> {
|
||||||
/// Create a new [`WebHistory`].
|
/// Create a new [`WebHistory`].
|
||||||
///
|
///
|
||||||
/// If `do_scroll_restoration` is [`true`], [`WebHistory`] will take control of the history
|
/// If `do_scroll_restoration` is [`true`], [`WebHistory`] will take control of the history
|
||||||
@@ -52,9 +63,10 @@ impl WebHistory {
|
|||||||
let h = history.clone();
|
let h = history.clone();
|
||||||
let document = w.document().expect("`window` has access to `document`");
|
let document = w.document().expect("`window` has access to `document`");
|
||||||
|
|
||||||
Some(EventListener::new(&document, "scroll", move |_| {
|
// Some(EventListener::new(&document, "scroll", move |_| {
|
||||||
update_history(&w, &h);
|
// set_state(&w, &h);
|
||||||
}))
|
// }))
|
||||||
|
None
|
||||||
}
|
}
|
||||||
false => None,
|
false => None,
|
||||||
};
|
};
|
||||||
@@ -65,44 +77,41 @@ impl WebHistory {
|
|||||||
listener_navigation: None,
|
listener_navigation: None,
|
||||||
listener_scroll,
|
listener_scroll,
|
||||||
listener_animation_frame: Default::default(),
|
listener_animation_frame: Default::default(),
|
||||||
prefix: prefix,
|
prefix,
|
||||||
window,
|
window,
|
||||||
|
phantom: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HistoryProvider for WebHistory {
|
impl<R: Serialize + DeserializeOwned + Routable> HistoryProvider<R> for WebHistory<R>
|
||||||
fn current_route(&self) -> String {
|
where
|
||||||
let path = self
|
<R as std::str::FromStr>::Err: std::fmt::Display,
|
||||||
.window
|
{
|
||||||
.location()
|
fn current_route(&self) -> R {
|
||||||
.pathname()
|
match get_current(&self.history) {
|
||||||
.unwrap_or_else(|_| String::from("/"));
|
// Try to get the route from the history state
|
||||||
|
Some(route) => route,
|
||||||
match &self.prefix {
|
// If that fails, get the route from the current URL
|
||||||
None => path.to_string(),
|
None => R::from_str(
|
||||||
Some(prefix) => path
|
&self
|
||||||
.starts_with(prefix)
|
.window
|
||||||
.then(|| path.split_at(prefix.len()).1)
|
.location()
|
||||||
.unwrap_or("/")
|
.pathname()
|
||||||
.to_string(),
|
.unwrap_or_else(|_| String::from("/")),
|
||||||
|
)
|
||||||
|
.unwrap_or_else(|err| panic!("{}", err)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn current_prefix(&self) -> Option<String> {
|
fn current_prefix(&self) -> Option<String> {
|
||||||
self.prefix.clone()
|
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 {
|
let path = match &self.prefix {
|
||||||
None => path,
|
None => format!("{state}"),
|
||||||
Some(prefix) => format!("{prefix}{path}"),
|
Some(prefix) => format!("{prefix}{state}"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let state = match self.do_scroll_restoration {
|
let state = self.create_state(state);
|
||||||
true => top_left(),
|
|
||||||
false => self.history.state().unwrap_or_default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let nav = self.history.push_state_with_url(&state, "", Some(&path));
|
let nav = push_state_and_url(&self.history, &state, path);
|
||||||
|
|
||||||
match nav {
|
match nav {
|
||||||
Ok(_) => {
|
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 {
|
let path = match &self.prefix {
|
||||||
None => path,
|
None => format!("{state}"),
|
||||||
Some(prefix) => format!("{prefix}{path}"),
|
Some(prefix) => format!("{prefix}{state}"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let state = match self.do_scroll_restoration {
|
let state = self.create_state(state);
|
||||||
true => top_left(),
|
|
||||||
false => self.history.state().unwrap_or_default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let nav = self.history.replace_state_with_url(&state, "", Some(&path));
|
let nav = replace_state_with_url(&self.history, &state, Some(&path));
|
||||||
|
|
||||||
match nav {
|
match nav {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
@@ -184,8 +187,8 @@ impl HistoryProvider for WebHistory {
|
|||||||
self.listener_navigation = Some(EventListener::new(&self.window, "popstate", move |_| {
|
self.listener_navigation = Some(EventListener::new(&self.window, "popstate", move |_| {
|
||||||
(*callback)();
|
(*callback)();
|
||||||
if d {
|
if d {
|
||||||
let mut s = s.lock().expect("unpoisoned scroll mutex");
|
// let mut s = s.lock().expect("unpoisoned scroll mutex");
|
||||||
*s = Some(update_scroll(&w, &h));
|
// *s = Some(update_scroll(&w, &h));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ use std::sync::{Arc, Mutex};
|
|||||||
|
|
||||||
use gloo::{events::EventListener, render::AnimationFrame, utils::window};
|
use gloo::{events::EventListener, render::AnimationFrame, utils::window};
|
||||||
use log::error;
|
use log::error;
|
||||||
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use web_sys::{History, ScrollRestoration, Window};
|
use web_sys::{History, ScrollRestoration, Window};
|
||||||
|
|
||||||
|
use crate::routable::Routable;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
web_scroll::{top_left, update_history, update_scroll},
|
web_scroll::{top_left, update_history, update_scroll},
|
||||||
HistoryProvider,
|
HistoryProvider,
|
||||||
@@ -29,9 +32,10 @@ pub struct WebHashHistory<R: Serialize + DeserializeOwned> {
|
|||||||
listener_scroll: Option<EventListener>,
|
listener_scroll: Option<EventListener>,
|
||||||
listener_animation_frame: Arc<Mutex<Option<AnimationFrame>>>,
|
listener_animation_frame: Arc<Mutex<Option<AnimationFrame>>>,
|
||||||
window: Window,
|
window: Window,
|
||||||
|
phantom: std::marker::PhantomData<R>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebHashHistory {
|
impl<R: Serialize + DeserializeOwned> WebHashHistory<R> {
|
||||||
/// Create a new [`WebHashHistory`].
|
/// Create a new [`WebHashHistory`].
|
||||||
///
|
///
|
||||||
/// If `do_scroll_restoration` is [`true`], [`WebHashHistory`] will take control of the history
|
/// If `do_scroll_restoration` is [`true`], [`WebHashHistory`] will take control of the history
|
||||||
@@ -67,17 +71,13 @@ impl WebHashHistory {
|
|||||||
listener_scroll,
|
listener_scroll,
|
||||||
listener_animation_frame: Default::default(),
|
listener_animation_frame: Default::default(),
|
||||||
window,
|
window,
|
||||||
|
phantom: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebHashHistory {
|
impl<R: Serialize + DeserializeOwned> WebHashHistory<R> {
|
||||||
fn join_url_to_hash(&self, path: String) -> Option<String> {
|
fn join_url_to_hash(&self, path: R) -> Option<String> {
|
||||||
if path.starts_with("//") {
|
|
||||||
error!("cannot navigate to paths starting with `//`, got `{path}`");
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = match self.url() {
|
let url = match self.url() {
|
||||||
Some(c) => match c.join(&path) {
|
Some(c) => match c.join(&path) {
|
||||||
Ok(new) => new,
|
Ok(new) => new,
|
||||||
@@ -120,17 +120,13 @@ impl WebHashHistory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HistoryProvider for WebHashHistory {
|
impl<R: Serialize + DeserializeOwned + Routable> HistoryProvider<R> for WebHashHistory<R> {
|
||||||
fn current_route(&self) -> String {
|
fn current_route(&self) -> R {
|
||||||
self.url()
|
self.url()
|
||||||
.map(|url| url.path().to_string())
|
.map(|url| url.path().to_string())
|
||||||
.unwrap_or(String::from("/"))
|
.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> {
|
fn current_prefix(&self) -> Option<String> {
|
||||||
Some(String::from("#"))
|
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) {
|
let hash = match self.join_url_to_hash(path) {
|
||||||
Some(hash) => hash,
|
Some(hash) => hash,
|
||||||
None => return,
|
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) {
|
let hash = match self.join_url_to_hash(path) {
|
||||||
Some(hash) => hash,
|
Some(hash) => hash,
|
||||||
None => return,
|
None => return,
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
@@ -1,37 +1,23 @@
|
|||||||
use gloo::render::{request_animation_frame, AnimationFrame};
|
use gloo::render::{request_animation_frame, AnimationFrame};
|
||||||
use log::error;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use wasm_bindgen::JsValue;
|
use web_sys::Window;
|
||||||
use web_sys::{History, Window};
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
|
||||||
pub(crate) struct ScrollPosition {
|
pub(crate) struct ScrollPosition {
|
||||||
x: f64,
|
x: f64,
|
||||||
y: f64,
|
y: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn top_left() -> JsValue {
|
impl ScrollPosition {
|
||||||
serde_wasm_bindgen::to_value(&ScrollPosition::default()).unwrap()
|
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) {
|
pub(crate) fn scroll_to(&self, window: Window) -> AnimationFrame {
|
||||||
let position = serde_wasm_bindgen::to_value(&ScrollPosition {
|
let Self { x, y } = *self;
|
||||||
x: window.scroll_x().unwrap_or_default(),
|
request_animation_frame(move |_| window.scroll_to_with_x_and_y(x, y))
|
||||||
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))
|
|
||||||
}
|
|
||||||
@@ -94,7 +94,7 @@ impl<I: std::iter::FromIterator<String>> FromRouteSegments for I {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Something that can be routed to
|
/// 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
|
/// Render the route at the given level
|
||||||
fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a>;
|
fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user