add web hash history integration
This commit is contained in:
4 files changed
+164
-5
No files matched your search
@@ -17,9 +17,10 @@ serde = { version = "1.0.147", 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 }
|
||||
|
||||
[features]
|
||||
regex = ["dep:regex"]
|
||||
serde = ["dep:serde", "serde_urlencoded"]
|
||||
web = ["gloo", "web-sys"]
|
||||
web = ["gloo", "wasm-bindgen", "web-sys"]
|
||||
@@ -8,6 +8,11 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
mod web_hash;
|
||||
#[cfg(feature = "web")]
|
||||
pub use web_hash::*;
|
||||
|
||||
mod memory;
|
||||
pub use memory::*;
|
||||
|
||||
|
||||
@@ -31,9 +31,9 @@ pub struct WebHistory {
|
||||
impl WebHistory {
|
||||
/// Create a new [`WebHistory`] with a prefix.
|
||||
#[must_use]
|
||||
pub fn with_prefix(prefix: String) -> Self {
|
||||
pub fn with_prefix(prefix: impl Into<String>) -> Self {
|
||||
Self {
|
||||
prefix: Some(prefix),
|
||||
prefix: Some(prefix.into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -105,8 +105,8 @@ impl HistoryProvider for WebHistory {
|
||||
fn external(&mut self, url: String) -> bool {
|
||||
match self.window.location().set_href(&url) {
|
||||
Ok(_) => true,
|
||||
Err(_) => {
|
||||
error!("`WebHistory` failed to navigate to external target: {url}");
|
||||
Err(e) => {
|
||||
error!("failed to navigate to external url (`{url}): {e:?}");
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
use gloo::{events::EventListener, utils::window};
|
||||
use log::error;
|
||||
use url::Url;
|
||||
use wasm_bindgen::JsValue;
|
||||
use web_sys::{History, Window};
|
||||
|
||||
use super::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 {
|
||||
history: History,
|
||||
window: Window,
|
||||
}
|
||||
|
||||
impl Default for WebHashHistory {
|
||||
fn default() -> Self {
|
||||
let window = window();
|
||||
|
||||
Self {
|
||||
history: window.history().expect("window has history"),
|
||||
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_path(&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,
|
||||
};
|
||||
|
||||
if let Err(e) = self
|
||||
.history
|
||||
.push_state_with_url(&JsValue::NULL, "", Some(&hash))
|
||||
{
|
||||
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,
|
||||
};
|
||||
|
||||
if let Err(e) = self
|
||||
.history
|
||||
.replace_state_with_url(&JsValue::NULL, "", Some(&hash))
|
||||
{
|
||||
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 listener = EventListener::new(&self.window, "popstate", move |_| (*callback)());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user