implement scroll restoration

This commit is contained in:
Adrian Wannenmacher committed 2022-12-15 20:09:53 +01:00
1 parent 9912c32ffd
commit 10c2abf0d8
7 files changed
+261 -75

No files matched your search

+4 -3
View File
@@ -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 -5
View File
@@ -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
+114 -40
View File
@@ -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));
}
}));
}
}
+85 -18
View File
@@ -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))
}
+11 -3
View File
@@ -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))
+1 -6
View File
@@ -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,
}