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 }
|
gloo = { version = "0.8.0", optional = true }
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
regex = { version = "1.6.0", optional = true }
|
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 }
|
serde_urlencoded = { version = "0.7.1", optional = true }
|
||||||
url = "2.3.1"
|
url = "2.3.1"
|
||||||
urlencoding = "2.1.2"
|
urlencoding = "2.1.2"
|
||||||
wasm-bindgen = { version = "0.2.83", optional = true }
|
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]
|
[features]
|
||||||
regex = ["dep:regex"]
|
regex = ["dep:regex"]
|
||||||
serde = ["dep:serde", "serde_urlencoded"]
|
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;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[cfg(feature = "web")]
|
|
||||||
mod web_hash;
|
|
||||||
#[cfg(feature = "web")]
|
|
||||||
pub use web_hash::*;
|
|
||||||
|
|
||||||
mod memory;
|
mod memory;
|
||||||
pub use memory::*;
|
pub use memory::*;
|
||||||
|
|
||||||
@@ -21,6 +16,14 @@ mod web;
|
|||||||
#[cfg(feature = "web")]
|
#[cfg(feature = "web")]
|
||||||
pub use 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.
|
/// An integration with some kind of navigation history.
|
||||||
///
|
///
|
||||||
/// Depending on your use case, your implementation may deviate from the described procedure. This
|
/// Depending on your use case, your implementation may deviate from the described procedure. This
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
use gloo::{
|
use std::sync::{Arc, Mutex};
|
||||||
history::{BrowserHistory, History, HistoryListener},
|
|
||||||
utils::window,
|
|
||||||
};
|
|
||||||
use log::error;
|
|
||||||
use web_sys::Window;
|
|
||||||
|
|
||||||
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].
|
/// 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
|
/// [History API]: https://developer.mozilla.org/en-US/docs/Web/API/History_API
|
||||||
pub struct WebHistory {
|
pub struct WebHistory {
|
||||||
history: BrowserHistory,
|
do_scroll_restoration: bool,
|
||||||
listener_navigation: Option<HistoryListener>,
|
history: History,
|
||||||
|
listener_navigation: Option<EventListener>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
listener_scroll: Option<EventListener>,
|
||||||
|
listener_animation_frame: Arc<Mutex<Option<AnimationFrame>>>,
|
||||||
prefix: Option<String>,
|
prefix: Option<String>,
|
||||||
window: Window,
|
window: Window,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebHistory {
|
impl WebHistory {
|
||||||
/// Create a new [`WebHistory`] with a prefix.
|
/// Create a new [`WebHistory`].
|
||||||
#[must_use]
|
///
|
||||||
pub fn with_prefix(prefix: impl Into<String>) -> Self {
|
/// If `do_scroll_restoration` is [`true`], [`WebHistory`] will take control of the history
|
||||||
Self {
|
/// state. It'll also set the browsers scroll restoration to `manual`.
|
||||||
prefix: Some(prefix.into()),
|
pub fn new(prefix: Option<String>, do_scroll_restoration: bool) -> Self {
|
||||||
..Default::default()
|
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 {
|
Self {
|
||||||
history: BrowserHistory::new(),
|
do_scroll_restoration,
|
||||||
|
history,
|
||||||
listener_navigation: None,
|
listener_navigation: None,
|
||||||
prefix: None,
|
listener_scroll,
|
||||||
window: window(),
|
listener_animation_frame: Default::default(),
|
||||||
|
prefix: prefix,
|
||||||
|
window,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HistoryProvider for WebHistory {
|
impl HistoryProvider for WebHistory {
|
||||||
fn current_path(&self) -> String {
|
fn current_path(&self) -> String {
|
||||||
let location = self.history.location();
|
let path = self
|
||||||
let path = location.path();
|
.window
|
||||||
|
.location()
|
||||||
|
.pathname()
|
||||||
|
.unwrap_or_else(|_| String::from("/"));
|
||||||
|
|
||||||
match &self.prefix {
|
match &self.prefix {
|
||||||
None => path.to_string(),
|
None => path.to_string(),
|
||||||
@@ -66,14 +90,17 @@ impl HistoryProvider for WebHistory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn current_query(&self) -> Option<String> {
|
fn current_query(&self) -> Option<String> {
|
||||||
let location = self.history.location();
|
self.window
|
||||||
let query = location.query_str();
|
.location()
|
||||||
|
.search()
|
||||||
if query.is_empty() {
|
.ok()
|
||||||
None
|
.map(|mut q| {
|
||||||
} else {
|
if q.starts_with('?') {
|
||||||
Some(query.to_string())
|
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> {
|
||||||
@@ -81,25 +108,61 @@ impl HistoryProvider for WebHistory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn go_back(&mut self) {
|
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) {
|
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) {
|
fn push(&mut self, path: String) {
|
||||||
self.history.push(match &self.prefix {
|
let path = match &self.prefix {
|
||||||
None => path,
|
None => path,
|
||||||
Some(prefix) => format!("{prefix}{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) {
|
fn replace(&mut self, path: String) {
|
||||||
self.history.replace(match &self.prefix {
|
let path = match &self.prefix {
|
||||||
None => path,
|
None => path,
|
||||||
Some(prefix) => format!("{prefix}{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 {
|
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>) {
|
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 log::error;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use wasm_bindgen::JsValue;
|
use web_sys::{History, ScrollRestoration, Window};
|
||||||
use web_sys::{History, 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/";
|
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
|
/// [History API]: https://developer.mozilla.org/en-US/docs/Web/API/History_API
|
||||||
pub struct WebHashHistory {
|
pub struct WebHashHistory {
|
||||||
|
do_scroll_restoration: bool,
|
||||||
history: History,
|
history: History,
|
||||||
|
listener_navigation: Option<EventListener>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
listener_scroll: Option<EventListener>,
|
||||||
|
listener_animation_frame: Arc<Mutex<Option<AnimationFrame>>>,
|
||||||
window: Window,
|
window: Window,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for WebHashHistory {
|
impl WebHashHistory {
|
||||||
fn default() -> Self {
|
/// 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 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 {
|
Self {
|
||||||
history: window.history().expect("window has history"),
|
do_scroll_restoration,
|
||||||
|
history,
|
||||||
|
listener_navigation: None,
|
||||||
|
listener_scroll,
|
||||||
|
listener_animation_frame: Default::default(),
|
||||||
window,
|
window,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,11 +153,20 @@ impl HistoryProvider for WebHashHistory {
|
|||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = self
|
let state = match self.do_scroll_restoration {
|
||||||
.history
|
true => top_left(),
|
||||||
.push_state_with_url(&JsValue::NULL, "", Some(&hash))
|
false => self.history.state().unwrap_or_default(),
|
||||||
{
|
};
|
||||||
error!("failed to push state: {e:?}");
|
|
||||||
|
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,
|
None => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = self
|
let state = match self.do_scroll_restoration {
|
||||||
.history
|
true => top_left(),
|
||||||
.replace_state_with_url(&JsValue::NULL, "", Some(&hash))
|
false => self.history.state().unwrap_or_default(),
|
||||||
{
|
};
|
||||||
error!("failed to replace state: {e:?}");
|
|
||||||
|
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>) {
|
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)]
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use dioxus_router::prelude::*;
|
use dioxus_router::{history::MemoryHistory, prelude::*};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
#[cfg(not(feature = "web"))]
|
#[cfg(not(feature = "web"))]
|
||||||
@@ -13,8 +13,16 @@ fn main() {
|
|||||||
fn App(cx: Scope) -> Element {
|
fn App(cx: Scope) -> Element {
|
||||||
use_router(
|
use_router(
|
||||||
&cx,
|
&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))
|
Segment::content(comp(Home))
|
||||||
|
|||||||
@@ -188,16 +188,11 @@ pub struct RouterConfiguration {
|
|||||||
|
|
||||||
impl Default for RouterConfiguration {
|
impl Default for RouterConfiguration {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
#[cfg(not(feature = "web"))]
|
|
||||||
let history = Box::<MemoryHistory>::default();
|
|
||||||
#[cfg(feature = "web")]
|
|
||||||
let history = Box::<crate::history::WebHistory>::default();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
failure_external_navigation: comp(FailureExternalNavigation),
|
failure_external_navigation: comp(FailureExternalNavigation),
|
||||||
failure_named_navigation: comp(FailureNamedNavigation),
|
failure_named_navigation: comp(FailureNamedNavigation),
|
||||||
failure_redirection_limit: comp(FailureRedirectionLimit),
|
failure_redirection_limit: comp(FailureRedirectionLimit),
|
||||||
history: history,
|
history: Box::<MemoryHistory>::default(),
|
||||||
on_update: None,
|
on_update: None,
|
||||||
synchronous: false,
|
synchronous: false,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user