Intigrate macro with router hooks and components
This commit is contained in:
1 parent
ef6551a6cd
commit
e4b8fbfafe
37 files changed
+1442
-3009
No files matched your search
@@ -0,0 +1,230 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::routable::Routable;
|
||||
|
||||
use super::HistoryProvider;
|
||||
|
||||
/// A [`HistoryProvider`] that stores all navigation information in memory.
|
||||
pub struct MemoryHistory<R: Routable> {
|
||||
current: Option<R>,
|
||||
history: Vec<R>,
|
||||
future: Vec<R>,
|
||||
}
|
||||
|
||||
impl<R: Routable> MemoryHistory<R> {
|
||||
/// Create a [`MemoryHistory`] starting at `path`.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
|
||||
/// let mut history = MemoryHistory::with_initial_path("/some/path").unwrap();
|
||||
/// assert_eq!(history.current_path(), "/some/path");
|
||||
/// assert_eq!(history.can_go_back(), false);
|
||||
/// ```
|
||||
pub fn with_initial_path(path: impl Into<String>) -> Result<Self, <R as FromStr>::Err> {
|
||||
let path = path.into();
|
||||
|
||||
Ok(Self {
|
||||
current: Some(R::from_str(&path)?),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Routable> Default for MemoryHistory<R> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
current: None,
|
||||
history: Vec::new(),
|
||||
future: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Routable> HistoryProvider<R> for MemoryHistory<R> {
|
||||
fn current_route(&self) -> &R {
|
||||
self.current.as_ref().expect("current route is not set")
|
||||
}
|
||||
|
||||
fn can_go_back(&self) -> bool {
|
||||
!self.history.is_empty()
|
||||
}
|
||||
|
||||
fn go_back(&mut self) {
|
||||
if let Some(last) = self.history.pop() {
|
||||
let new = self.current.replace(last);
|
||||
if let Some(new) = new {
|
||||
self.future.push(new);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn can_go_forward(&self) -> bool {
|
||||
!self.future.is_empty()
|
||||
}
|
||||
|
||||
fn go_forward(&mut self) {
|
||||
if let Some(next) = self.future.pop() {
|
||||
let old = self.current.replace(next);
|
||||
if let Some(old) = old {
|
||||
self.history.push(old);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, new: R) {
|
||||
if let Some(current) = self.current.take() {
|
||||
self.history.push(current);
|
||||
}
|
||||
self.current = Some(new);
|
||||
self.future.clear();
|
||||
}
|
||||
|
||||
fn replace(&mut self, path: R) {
|
||||
self.current = Some(path);
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use super::*;
|
||||
|
||||
// #[test]
|
||||
// fn default() {
|
||||
// let mem = MemoryHistory::default();
|
||||
// assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
|
||||
// assert_eq!(mem.history, Vec::<String>::new());
|
||||
// assert_eq!(mem.future, Vec::<String>::new());
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn with_initial_path() {
|
||||
// let mem = MemoryHistory::with_initial_path("something").unwrap();
|
||||
// assert_eq!(
|
||||
// mem.current,
|
||||
// Url::parse(&format!("{INITIAL_URL}something")).unwrap()
|
||||
// );
|
||||
// assert_eq!(mem.history, Vec::<String>::new());
|
||||
// assert_eq!(mem.future, Vec::<String>::new());
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn with_initial_path_with_leading_slash() {
|
||||
// let mem = MemoryHistory::with_initial_path("/something").unwrap();
|
||||
// assert_eq!(
|
||||
// mem.current,
|
||||
// Url::parse(&format!("{INITIAL_URL}something")).unwrap()
|
||||
// );
|
||||
// assert_eq!(mem.history, Vec::<String>::new());
|
||||
// assert_eq!(mem.future, Vec::<String>::new());
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn can_go_back() {
|
||||
// let mut mem = MemoryHistory::default();
|
||||
// assert!(!mem.can_go_back());
|
||||
|
||||
// mem.push(String::from("/test"));
|
||||
// assert!(mem.can_go_back());
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn go_back() {
|
||||
// let mut mem = MemoryHistory::default();
|
||||
// mem.push(String::from("/test"));
|
||||
// mem.go_back();
|
||||
|
||||
// assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
|
||||
// assert!(mem.history.is_empty());
|
||||
// assert_eq!(mem.future, vec![format!("{INITIAL_URL}test")]);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn can_go_forward() {
|
||||
// let mut mem = MemoryHistory::default();
|
||||
// assert!(!mem.can_go_forward());
|
||||
|
||||
// mem.push(String::from("/test"));
|
||||
// mem.go_back();
|
||||
|
||||
// assert!(mem.can_go_forward());
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn go_forward() {
|
||||
// let mut mem = MemoryHistory::default();
|
||||
// mem.push(String::from("/test"));
|
||||
// mem.go_back();
|
||||
// mem.go_forward();
|
||||
|
||||
// assert_eq!(
|
||||
// mem.current,
|
||||
// Url::parse(&format!("{INITIAL_URL}test")).unwrap()
|
||||
// );
|
||||
// assert_eq!(mem.history, vec![INITIAL_URL.to_string()]);
|
||||
// assert!(mem.future.is_empty());
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn push() {
|
||||
// let mut mem = MemoryHistory::default();
|
||||
// mem.push(String::from("/test"));
|
||||
|
||||
// assert_eq!(
|
||||
// mem.current,
|
||||
// Url::parse(&format!("{INITIAL_URL}test")).unwrap()
|
||||
// );
|
||||
// assert_eq!(mem.history, vec![INITIAL_URL.to_string()]);
|
||||
// assert!(mem.future.is_empty());
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// #[should_panic = r#"cannot navigate to paths starting with "//": //test"#]
|
||||
// #[cfg(debug_assertions)]
|
||||
// fn push_debug() {
|
||||
// let mut mem = MemoryHistory::default();
|
||||
// mem.push(String::from("//test"));
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// #[cfg(not(debug_assertions))]
|
||||
// fn push_release() {
|
||||
// let mut mem = MemoryHistory::default();
|
||||
// mem.push(String::from("//test"));
|
||||
|
||||
// assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
|
||||
// assert!(mem.history.is_empty())
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn replace() {
|
||||
// let mut mem = MemoryHistory::default();
|
||||
// mem.push(String::from("/test"));
|
||||
// mem.push(String::from("/other"));
|
||||
// mem.go_back();
|
||||
// mem.replace(String::from("/replace"));
|
||||
|
||||
// assert_eq!(
|
||||
// mem.current,
|
||||
// Url::parse(&format!("{INITIAL_URL}replace")).unwrap()
|
||||
// );
|
||||
// assert_eq!(mem.history, vec![INITIAL_URL.to_string()]);
|
||||
// assert_eq!(mem.future, vec![format!("{INITIAL_URL}other")]);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// #[should_panic = r#"cannot navigate to paths starting with "//": //test"#]
|
||||
// #[cfg(debug_assertions)]
|
||||
// fn replace_debug() {
|
||||
// let mut mem = MemoryHistory::default();
|
||||
// mem.replace(String::from("//test"));
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// #[cfg(not(debug_assertions))]
|
||||
// fn replace_release() {
|
||||
// let mut mem = MemoryHistory::default();
|
||||
// mem.replace(String::from("//test"));
|
||||
|
||||
// assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,193 @@
|
||||
//! History Integration
|
||||
//!
|
||||
//! dioxus-router-core relies on so-called [`HistoryProvider`]s to store the current URL, and possibly a
|
||||
//! history (i.e. a browsers back button) and future (i.e. a browsers forward button).
|
||||
//!
|
||||
//! To integrate dioxus-router-core with a any type of history, all you have to do is implement the
|
||||
//! [`HistoryProvider`] trait. dioxus-router-core also comes with some (for now one) default implementations.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
mod memory;
|
||||
pub use memory::*;
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
mod web;
|
||||
#[cfg(feature = "web")]
|
||||
pub use web::*;
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
mod web_hash;
|
||||
#[cfg(feature = "web")]
|
||||
pub use web_hash::*;
|
||||
|
||||
use crate::routable::Routable;
|
||||
|
||||
#[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
|
||||
/// is fine, as long as both `current_path` and `current_query` match the described format.
|
||||
///
|
||||
/// However, you should document all deviations. Also, make sure the navigation is user-friendly.
|
||||
/// The described behaviors are designed to mimic a web browser, which most users should already
|
||||
/// know. Deviations might confuse them.
|
||||
pub trait HistoryProvider<R: Routable> {
|
||||
/// Get the path of the current URL.
|
||||
///
|
||||
/// **Must start** with `/`. **Must _not_ contain** the prefix.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
|
||||
/// let mut history = MemoryHistory::default();
|
||||
/// assert_eq!(history.current_path(), "/");
|
||||
///
|
||||
/// history.push(String::from("/path"));
|
||||
/// assert_eq!(history.current_path(), "/path");
|
||||
/// ```
|
||||
#[must_use]
|
||||
fn current_route(&self) -> &R;
|
||||
|
||||
/// Get the current path prefix of the URL.
|
||||
///
|
||||
/// Not all [`HistoryProvider`]s need a prefix feature. It is meant for environments where a
|
||||
/// dioxus-router-core-routed application is not running on `/`. The [`HistoryProvider`] is responsible
|
||||
/// for removing the prefix from the dioxus-router-core-internal path, and also for adding it back in
|
||||
/// during navigation. This functions value is only used for creating `href`s (e.g. for SSR or
|
||||
/// display (but not navigation) in a web app).
|
||||
fn current_prefix(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Check whether there is a previous page to navigate back to.
|
||||
///
|
||||
/// If a [`HistoryProvider`] cannot know this, it should return [`true`].
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
|
||||
/// let mut history = MemoryHistory::default();
|
||||
/// assert_eq!(history.can_go_back(), false);
|
||||
///
|
||||
/// history.push(String::from("/some-other-page"));
|
||||
/// assert_eq!(history.can_go_back(), true);
|
||||
/// ```
|
||||
#[must_use]
|
||||
fn can_go_back(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Go back to a previous page.
|
||||
///
|
||||
/// If a [`HistoryProvider`] cannot go to a previous page, it should do nothing. This method
|
||||
/// might be called, even if `can_go_back` returns [`false`].
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
|
||||
/// let mut history = MemoryHistory::default();
|
||||
/// assert_eq!(history.current_path(), "/");
|
||||
///
|
||||
/// history.go_back();
|
||||
/// assert_eq!(history.current_path(), "/");
|
||||
///
|
||||
/// history.push(String::from("/some-other-page"));
|
||||
/// assert_eq!(history.current_path(), "/some-other-page");
|
||||
///
|
||||
/// history.go_back();
|
||||
/// assert_eq!(history.current_path(), "/");
|
||||
/// ```
|
||||
fn go_back(&mut self);
|
||||
|
||||
/// Check whether there is a future page to navigate forward to.
|
||||
///
|
||||
/// If a [`HistoryProvider`] cannot know this, it should return [`true`].
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
|
||||
/// let mut history = MemoryHistory::default();
|
||||
/// assert_eq!(history.can_go_forward(), false);
|
||||
///
|
||||
/// history.push(String::from("/some-other-page"));
|
||||
/// assert_eq!(history.can_go_forward(), false);
|
||||
///
|
||||
/// history.go_back();
|
||||
/// assert_eq!(history.can_go_forward(), true);
|
||||
/// ```
|
||||
#[must_use]
|
||||
fn can_go_forward(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Go forward to a future page.
|
||||
///
|
||||
/// If a [`HistoryProvider`] cannot go to a previous page, it should do nothing. This method
|
||||
/// might be called, even if `can_go_forward` returns [`false`].
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
|
||||
/// let mut history = MemoryHistory::default();
|
||||
/// history.push(String::from("/some-other-page"));
|
||||
/// assert_eq!(history.current_path(), "/some-other-page");
|
||||
///
|
||||
/// history.go_back();
|
||||
/// assert_eq!(history.current_path(), "/");
|
||||
///
|
||||
/// history.go_forward();
|
||||
/// assert_eq!(history.current_path(), "/some-other-page");
|
||||
/// ```
|
||||
fn go_forward(&mut self);
|
||||
|
||||
/// Go to another page.
|
||||
///
|
||||
/// This should do three things:
|
||||
/// 1. Merge the current URL with the `path` parameter (which may also include a query part).
|
||||
/// 2. Remove the previous URL to the navigation history.
|
||||
/// 3. Clear the navigation future.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
|
||||
/// let mut history = MemoryHistory::default();
|
||||
/// assert_eq!(history.current_path(), "/");
|
||||
///
|
||||
/// history.push(String::from("/some-other-page"));
|
||||
/// assert_eq!(history.current_path(), "/some-other-page");
|
||||
/// assert!(history.can_go_back());
|
||||
/// ```
|
||||
fn push(&mut self, route: R);
|
||||
|
||||
/// Replace the current page with another one.
|
||||
///
|
||||
/// This should merge the current URL with the `path` parameter (which may also include a query
|
||||
/// part). In contrast to the `push` function, the navigation history and future should stay
|
||||
/// untouched.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
|
||||
/// let mut history = MemoryHistory::default();
|
||||
/// assert_eq!(history.current_path(), "/");
|
||||
///
|
||||
/// history.replace(String::from("/some-other-page"));
|
||||
/// assert_eq!(history.current_path(), "/some-other-page");
|
||||
/// assert!(!history.can_go_back());
|
||||
/// ```
|
||||
fn replace(&mut self, path: R);
|
||||
|
||||
/// Navigate to an external URL.
|
||||
///
|
||||
/// This should navigate to an external URL, which isn't controlled by the router. If a
|
||||
/// [`HistoryProvider`] cannot do that, it should return [`false`], otherwise [`true`].
|
||||
///
|
||||
/// Returning [`false`] will cause the router to handle the external navigation failure.
|
||||
#[allow(unused_variables)]
|
||||
fn external(&mut self, url: String) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Provide the [`HistoryProvider`] with an update callback.
|
||||
///
|
||||
/// Some [`HistoryProvider`]s may receive URL updates from outside the router. When such
|
||||
/// updates are received, they should call `callback`, which will cause the router to update.
|
||||
#[allow(unused_variables)]
|
||||
fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
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].
|
||||
///
|
||||
/// # Prefix
|
||||
/// This [`HistoryProvider`] supports a prefix, which can be used for web apps that aren't located
|
||||
/// at the root of their domain.
|
||||
///
|
||||
/// Application developers are responsible for ensuring that right after the prefix comes a `/`. If
|
||||
/// that is not the case, this [`HistoryProvider`] will replace the first character after the prefix
|
||||
/// with one.
|
||||
///
|
||||
/// Application developers are responsible for not rendering the router if the prefix is not present
|
||||
/// in the URL. Otherwise, if a router navigation is triggered, the prefix will be added.
|
||||
///
|
||||
/// [History API]: https://developer.mozilla.org/en-US/docs/Web/API/History_API
|
||||
pub struct WebHistory<R: Serialize + DeserializeOwned> {
|
||||
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`].
|
||||
///
|
||||
/// 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,
|
||||
};
|
||||
|
||||
Self {
|
||||
do_scroll_restoration,
|
||||
history,
|
||||
listener_navigation: None,
|
||||
listener_scroll,
|
||||
listener_animation_frame: Default::default(),
|
||||
prefix: prefix,
|
||||
window,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
self.prefix.clone()
|
||||
}
|
||||
|
||||
fn go_back(&mut self) {
|
||||
if let Err(e) = self.history.back() {
|
||||
error!("failed to go back: {e:?}")
|
||||
}
|
||||
}
|
||||
|
||||
fn go_forward(&mut self) {
|
||||
if let Err(e) = self.history.forward() {
|
||||
error!("failed to go forward: {e:?}")
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, path: String) {
|
||||
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) {
|
||||
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 {
|
||||
match self.window.location().set_href(&url) {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
error!("failed to navigate to external url (`{url}): {e:?}");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn updater(&mut self, callback: std::sync::Arc<dyn Fn() + Send + Sync>) {
|
||||
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,220 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use gloo::{events::EventListener, render::AnimationFrame, utils::window};
|
||||
use log::error;
|
||||
use url::Url;
|
||||
use web_sys::{History, ScrollRestoration, Window};
|
||||
|
||||
use super::{
|
||||
web_scroll::{top_left, update_history, update_scroll},
|
||||
HistoryProvider,
|
||||
};
|
||||
|
||||
const INITIAL_URL: &str = "dioxus-router-core://initial_url.invalid/";
|
||||
|
||||
/// A [`HistoryProvider`] that integrates with a browser via the [History API]. It uses the URLs
|
||||
/// hash instead of its path.
|
||||
///
|
||||
/// Early web applications used the hash to store the current path because there was no other way
|
||||
/// for them to interact with the history without triggering a browser navigation, as the
|
||||
/// [History API] did not yet exist. While this implementation could have been written that way, it
|
||||
/// was not, because no browser supports WebAssembly without the [History API].
|
||||
///
|
||||
/// [History API]: https://developer.mozilla.org/en-US/docs/Web/API/History_API
|
||||
pub struct WebHashHistory<R: Serialize + DeserializeOwned> {
|
||||
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 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 {
|
||||
do_scroll_restoration,
|
||||
history,
|
||||
listener_navigation: None,
|
||||
listener_scroll,
|
||||
listener_animation_frame: Default::default(),
|
||||
window,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WebHashHistory {
|
||||
fn join_url_to_hash(&self, path: String) -> Option<String> {
|
||||
if path.starts_with("//") {
|
||||
error!("cannot navigate to paths starting with `//`, got `{path}`");
|
||||
return None;
|
||||
}
|
||||
|
||||
let url = match self.url() {
|
||||
Some(c) => match c.join(&path) {
|
||||
Ok(new) => new,
|
||||
Err(e) => {
|
||||
error!("failed to join location with target: {e}");
|
||||
return None;
|
||||
}
|
||||
},
|
||||
None => {
|
||||
error!("current location unknown");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
Some(format!(
|
||||
"#{path}{query}",
|
||||
path = url.path(),
|
||||
query = url.query().map(|q| format!("?{q}")).unwrap_or_default()
|
||||
))
|
||||
}
|
||||
|
||||
fn url(&self) -> Option<Url> {
|
||||
let mut path = self.window.location().hash().ok()?;
|
||||
|
||||
if path.starts_with('#') {
|
||||
path.remove(0);
|
||||
}
|
||||
|
||||
if path.starts_with('/') {
|
||||
path.remove(0);
|
||||
}
|
||||
|
||||
match Url::parse(&format!("{INITIAL_URL}/{path}")) {
|
||||
Ok(url) => Some(url),
|
||||
Err(e) => {
|
||||
error!("failed to parse hash path: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HistoryProvider for WebHashHistory {
|
||||
fn current_route(&self) -> String {
|
||||
self.url()
|
||||
.map(|url| url.path().to_string())
|
||||
.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> {
|
||||
Some(String::from("#"))
|
||||
}
|
||||
|
||||
fn go_back(&mut self) {
|
||||
if let Err(e) = self.history.back() {
|
||||
error!("failed to go back: {e:?}")
|
||||
}
|
||||
}
|
||||
|
||||
fn go_forward(&mut self) {
|
||||
if let Err(e) = self.history.forward() {
|
||||
error!("failed to go forward: {e:?}")
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, path: String) {
|
||||
let hash = match self.join_url_to_hash(path) {
|
||||
Some(hash) => hash,
|
||||
None => return,
|
||||
};
|
||||
|
||||
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:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn replace(&mut self, path: String) {
|
||||
let hash = match self.join_url_to_hash(path) {
|
||||
Some(hash) => hash,
|
||||
None => return,
|
||||
};
|
||||
|
||||
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:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn external(&mut self, url: String) -> bool {
|
||||
match self.window.location().set_href(&url) {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
error!("failed to navigate to external url (`{url}): {e:?}");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn updater(&mut self, callback: std::sync::Arc<dyn Fn() + Send + Sync>) {
|
||||
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,37 @@
|
||||
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))
|
||||
}
|
||||
Reference in new issue
Block a user