web_hash.rs 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. use std::sync::{Arc, Mutex};
  2. use gloo::{events::EventListener, render::AnimationFrame, utils::window};
  3. use log::error;
  4. use serde::{de::DeserializeOwned, Serialize};
  5. use url::Url;
  6. use web_sys::{History, ScrollRestoration, Window};
  7. use crate::routable::Routable;
  8. use super::HistoryProvider;
  9. const INITIAL_URL: &str = "dioxus-router-core://initial_url.invalid/";
  10. /// A [`HistoryProvider`] that integrates with a browser via the [History API]. It uses the URLs
  11. /// hash instead of its path.
  12. ///
  13. /// Early web applications used the hash to store the current path because there was no other way
  14. /// for them to interact with the history without triggering a browser navigation, as the
  15. /// [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
  16. /// was not, because no browser supports WebAssembly without the [History API].
  17. pub struct WebHashHistory<R: Serialize + DeserializeOwned> {
  18. do_scroll_restoration: bool,
  19. history: History,
  20. listener_navigation: Option<EventListener>,
  21. #[allow(dead_code)]
  22. listener_scroll: Option<EventListener>,
  23. listener_animation_frame: Arc<Mutex<Option<AnimationFrame>>>,
  24. window: Window,
  25. phantom: std::marker::PhantomData<R>,
  26. }
  27. impl<R: Serialize + DeserializeOwned> WebHashHistory<R> {
  28. /// Create a new [`WebHashHistory`].
  29. ///
  30. /// If `do_scroll_restoration` is [`true`], [`WebHashHistory`] will take control of the history
  31. /// state. It'll also set the browsers scroll restoration to `manual`.
  32. pub fn new(do_scroll_restoration: bool) -> Self {
  33. let window = window();
  34. let history = window.history().expect("`window` has access to `history`");
  35. history
  36. .set_scroll_restoration(ScrollRestoration::Manual)
  37. .expect("`history` can set scroll restoration");
  38. let listener_scroll = match do_scroll_restoration {
  39. true => {
  40. history
  41. .set_scroll_restoration(ScrollRestoration::Manual)
  42. .expect("`history` can set scroll restoration");
  43. let w = window.clone();
  44. let h = history.clone();
  45. let document = w.document().expect("`window` has access to `document`");
  46. Some(EventListener::new(&document, "scroll", move |_| {
  47. update_history(&w, &h);
  48. }))
  49. }
  50. false => None,
  51. };
  52. Self {
  53. do_scroll_restoration,
  54. history,
  55. listener_navigation: None,
  56. listener_scroll,
  57. listener_animation_frame: Default::default(),
  58. window,
  59. phantom: Default::default(),
  60. }
  61. }
  62. }
  63. impl<R: Serialize + DeserializeOwned> WebHashHistory<R> {
  64. fn join_url_to_hash(&self, path: R) -> Option<String> {
  65. let url = match self.url() {
  66. Some(c) => match c.join(&path) {
  67. Ok(new) => new,
  68. Err(e) => {
  69. error!("failed to join location with target: {e}");
  70. return None;
  71. }
  72. },
  73. None => {
  74. error!("current location unknown");
  75. return None;
  76. }
  77. };
  78. Some(format!(
  79. "#{path}{query}",
  80. path = url.path(),
  81. query = url.query().map(|q| format!("?{q}")).unwrap_or_default()
  82. ))
  83. }
  84. fn url(&self) -> Option<Url> {
  85. let mut path = self.window.location().hash().ok()?;
  86. if path.starts_with('#') {
  87. path.remove(0);
  88. }
  89. if path.starts_with('/') {
  90. path.remove(0);
  91. }
  92. match Url::parse(&format!("{INITIAL_URL}/{path}")) {
  93. Ok(url) => Some(url),
  94. Err(e) => {
  95. error!("failed to parse hash path: {e}");
  96. None
  97. }
  98. }
  99. }
  100. }
  101. impl<R: Serialize + DeserializeOwned + Routable> HistoryProvider<R> for WebHashHistory<R> {
  102. fn current_route(&self) -> R {
  103. self.url()
  104. .map(|url| url.path().to_string())
  105. .unwrap_or(String::from("/"))
  106. }
  107. fn current_prefix(&self) -> Option<String> {
  108. Some(String::from("#"))
  109. }
  110. fn go_back(&mut self) {
  111. if let Err(e) = self.history.back() {
  112. error!("failed to go back: {e:?}")
  113. }
  114. }
  115. fn go_forward(&mut self) {
  116. if let Err(e) = self.history.forward() {
  117. error!("failed to go forward: {e:?}")
  118. }
  119. }
  120. fn push(&mut self, path: R) {
  121. let hash = match self.join_url_to_hash(path) {
  122. Some(hash) => hash,
  123. None => return,
  124. };
  125. let state = match self.do_scroll_restoration {
  126. true => top_left(),
  127. false => self.history.state().unwrap_or_default(),
  128. };
  129. let nav = self.history.push_state_with_url(&state, "", Some(&hash));
  130. match nav {
  131. Ok(_) => {
  132. if self.do_scroll_restoration {
  133. self.window.scroll_to_with_x_and_y(0.0, 0.0)
  134. }
  135. }
  136. Err(e) => error!("failed to push state: {e:?}"),
  137. }
  138. }
  139. fn replace(&mut self, path: R) {
  140. let hash = match self.join_url_to_hash(path) {
  141. Some(hash) => hash,
  142. None => return,
  143. };
  144. let state = match self.do_scroll_restoration {
  145. true => top_left(),
  146. false => self.history.state().unwrap_or_default(),
  147. };
  148. let nav = self.history.replace_state_with_url(&state, "", Some(&hash));
  149. match nav {
  150. Ok(_) => {
  151. if self.do_scroll_restoration {
  152. self.window.scroll_to_with_x_and_y(0.0, 0.0)
  153. }
  154. }
  155. Err(e) => error!("failed to replace state: {e:?}"),
  156. }
  157. }
  158. fn external(&mut self, url: String) -> bool {
  159. match self.window.location().set_href(&url) {
  160. Ok(_) => true,
  161. Err(e) => {
  162. error!("failed to navigate to external url (`{url}): {e:?}");
  163. false
  164. }
  165. }
  166. }
  167. fn updater(&mut self, callback: std::sync::Arc<dyn Fn() + Send + Sync>) {
  168. let w = self.window.clone();
  169. let h = self.history.clone();
  170. let s = self.listener_animation_frame.clone();
  171. let d = self.do_scroll_restoration;
  172. self.listener_navigation = Some(EventListener::new(&self.window, "popstate", move |_| {
  173. (*callback)();
  174. if d {
  175. let mut s = s.lock().expect("unpoisoned scroll mutex");
  176. *s = Some(update_scroll(&w, &h));
  177. }
  178. }));
  179. }
  180. }