implement scroll restoration
This commit is contained in:
7 files changed
+261
-75
No files matched your search
@@ -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,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
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use gloo::{
|
||||
history::{BrowserHistory, History, HistoryListener},
|
||||
utils::window,
|
||||
};
|
||||
use log::error;
|
||||
use web_sys::Window;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use super::HistoryProvider;
|
||||
use gloo::{events::EventListener, render::AnimationFrame};
|
||||
use log::error;
|
||||
use web_sys::{window, History, ScrollRestoration, Window};
|
||||
|
||||
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));
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user