123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211 |
- use std::sync::{Arc, Mutex};
- use gloo::{events::EventListener, render::AnimationFrame, utils::window};
- use log::error;
- use serde::{de::DeserializeOwned, Serialize};
- use url::Url;
- use web_sys::{History, ScrollRestoration, Window};
- use crate::routable::Routable;
- 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](https://developer.mozilla.org/en-US/docs/Web/API/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].
- 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,
- phantom: std::marker::PhantomData<R>,
- }
- impl<R: Serialize + DeserializeOwned> WebHashHistory<R> {
- /// 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,
- phantom: Default::default(),
- }
- }
- }
- impl<R: Serialize + DeserializeOwned> WebHashHistory<R> {
- fn join_url_to_hash(&self, path: R) -> Option<String> {
- 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<R: Serialize + DeserializeOwned + Routable> HistoryProvider<R> for WebHashHistory<R> {
- fn current_route(&self) -> R {
- self.url()
- .map(|url| url.path().to_string())
- .unwrap_or(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: R) {
- 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: R) {
- 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));
- }
- }));
- }
- }
|