web.rs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. use std::sync::{Arc, Mutex};
  2. use gloo::{console::error, events::EventListener, render::AnimationFrame};
  3. use wasm_bindgen::JsValue;
  4. use web_sys::{window, History, ScrollRestoration, Window};
  5. use crate::routable::Routable;
  6. use super::{
  7. web_history::{get_current, push_state_and_url, replace_state_with_url},
  8. web_scroll::ScrollPosition,
  9. HistoryProvider,
  10. };
  11. #[cfg(not(feature = "serde"))]
  12. #[allow(clippy::extra_unused_type_parameters)]
  13. fn update_scroll<R>(window: &Window, history: &History) {
  14. let scroll = ScrollPosition::of_window(window);
  15. if let Err(err) = replace_state_with_url(history, &[scroll.x, scroll.y], None) {
  16. error!(err);
  17. }
  18. }
  19. #[cfg(feature = "serde")]
  20. fn update_scroll<R: serde::Serialize + serde::de::DeserializeOwned + Routable>(
  21. window: &Window,
  22. history: &History,
  23. ) {
  24. if let Some(WebHistoryState { state, .. }) = get_current::<WebHistoryState<R>>(history) {
  25. let scroll = ScrollPosition::of_window(window);
  26. let state = WebHistoryState { state, scroll };
  27. if let Err(err) = replace_state_with_url(history, &state, None) {
  28. error!(err);
  29. }
  30. }
  31. }
  32. #[cfg(feature = "serde")]
  33. #[derive(serde::Deserialize, serde::Serialize)]
  34. struct WebHistoryState<R> {
  35. state: R,
  36. scroll: ScrollPosition,
  37. }
  38. /// A [`HistoryProvider`] that integrates with a browser via the [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API).
  39. ///
  40. /// # Prefix
  41. /// This [`HistoryProvider`] supports a prefix, which can be used for web apps that aren't located
  42. /// at the root of their domain.
  43. ///
  44. /// Application developers are responsible for ensuring that right after the prefix comes a `/`. If
  45. /// that is not the case, this [`HistoryProvider`] will replace the first character after the prefix
  46. /// with one.
  47. ///
  48. /// Application developers are responsible for not rendering the router if the prefix is not present
  49. /// in the URL. Otherwise, if a router navigation is triggered, the prefix will be added.
  50. pub struct WebHistory<R: Routable> {
  51. do_scroll_restoration: bool,
  52. history: History,
  53. listener_navigation: Option<EventListener>,
  54. listener_animation_frame: Arc<Mutex<Option<AnimationFrame>>>,
  55. prefix: Option<String>,
  56. window: Window,
  57. phantom: std::marker::PhantomData<R>,
  58. }
  59. #[cfg(not(feature = "serde"))]
  60. impl<R: Routable> Default for WebHistory<R>
  61. where
  62. <R as std::str::FromStr>::Err: std::fmt::Display,
  63. {
  64. fn default() -> Self {
  65. Self::new(None, true)
  66. }
  67. }
  68. #[cfg(feature = "serde")]
  69. impl<R: Routable> Default for WebHistory<R>
  70. where
  71. <R as std::str::FromStr>::Err: std::fmt::Display,
  72. R: serde::Serialize + serde::de::DeserializeOwned,
  73. {
  74. fn default() -> Self {
  75. Self::new(None, true)
  76. }
  77. }
  78. impl<R: Routable> WebHistory<R> {
  79. #[cfg(not(feature = "serde"))]
  80. /// Create a new [`WebHistory`].
  81. ///
  82. /// If `do_scroll_restoration` is [`true`], [`WebHistory`] will take control of the history
  83. /// state. It'll also set the browsers scroll restoration to `manual`.
  84. pub fn new(prefix: Option<String>, do_scroll_restoration: bool) -> Self
  85. where
  86. <R as std::str::FromStr>::Err: std::fmt::Display,
  87. {
  88. let myself = Self::new_inner(prefix, do_scroll_restoration);
  89. let current_route = myself.current_route();
  90. let current_url = current_route.to_string();
  91. let state = myself.create_state(current_route);
  92. let _ = replace_state_with_url(&myself.history, &state, Some(&current_url));
  93. myself
  94. }
  95. #[cfg(feature = "serde")]
  96. /// Create a new [`WebHistory`].
  97. ///
  98. /// If `do_scroll_restoration` is [`true`], [`WebHistory`] will take control of the history
  99. /// state. It'll also set the browsers scroll restoration to `manual`.
  100. pub fn new(prefix: Option<String>, do_scroll_restoration: bool) -> Self
  101. where
  102. <R as std::str::FromStr>::Err: std::fmt::Display,
  103. R: serde::Serialize + serde::de::DeserializeOwned,
  104. {
  105. let w = window().expect("access to `window`");
  106. let h = w.history().expect("`window` has access to `history`");
  107. let document = w.document().expect("`window` has access to `document`");
  108. let myself = Self::new_inner(
  109. prefix,
  110. do_scroll_restoration,
  111. EventListener::new(&document, "scroll", {
  112. let mut last_updated = 0.0;
  113. move |evt| {
  114. // the time stamp in milliseconds
  115. let time_stamp = evt.time_stamp();
  116. // throttle the scroll event to 100ms
  117. if (time_stamp - last_updated) < 100.0 {
  118. return;
  119. }
  120. update_scroll::<R>(&w, &h);
  121. last_updated = time_stamp;
  122. }
  123. }),
  124. );
  125. let current_route = myself.current_route();
  126. log::trace!("initial route: {:?}", current_route);
  127. let current_url = current_route.to_string();
  128. let state = myself.create_state(current_route);
  129. let _ = replace_state_with_url(&myself.history, &state, Some(&current_url));
  130. myself
  131. }
  132. fn new_inner(prefix: Option<String>, do_scroll_restoration: bool) -> Self
  133. where
  134. <R as std::str::FromStr>::Err: std::fmt::Display,
  135. {
  136. let window = window().expect("access to `window`");
  137. let history = window.history().expect("`window` has access to `history`");
  138. if do_scroll_restoration {
  139. history
  140. .set_scroll_restoration(ScrollRestoration::Manual)
  141. .expect("`history` can set scroll restoration");
  142. }
  143. Self {
  144. do_scroll_restoration,
  145. history,
  146. listener_navigation: None,
  147. listener_animation_frame: Default::default(),
  148. prefix,
  149. window,
  150. phantom: Default::default(),
  151. }
  152. }
  153. fn scroll_pos(&self) -> ScrollPosition {
  154. self.do_scroll_restoration
  155. .then(|| ScrollPosition::of_window(&self.window))
  156. .unwrap_or_default()
  157. }
  158. #[cfg(not(feature = "serde"))]
  159. fn create_state(&self, _state: R) -> [f64; 2] {
  160. let scroll = self.scroll_pos();
  161. [scroll.x, scroll.y]
  162. }
  163. #[cfg(feature = "serde")]
  164. fn create_state(&self, state: R) -> WebHistoryState<R> {
  165. let scroll = self.scroll_pos();
  166. WebHistoryState { state, scroll }
  167. }
  168. }
  169. impl<R: Routable> WebHistory<R>
  170. where
  171. <R as std::str::FromStr>::Err: std::fmt::Display,
  172. {
  173. fn route_from_location(&self) -> R {
  174. R::from_str(
  175. &self
  176. .window
  177. .location()
  178. .pathname()
  179. .unwrap_or_else(|_| String::from("/")),
  180. )
  181. .unwrap_or_else(|err| panic!("{}", err))
  182. }
  183. fn full_path(&self, state: &R) -> String {
  184. match &self.prefix {
  185. None => format!("{state}"),
  186. Some(prefix) => format!("{prefix}{state}"),
  187. }
  188. }
  189. fn handle_nav(&self, result: Result<(), JsValue>) {
  190. match result {
  191. Ok(_) => {
  192. if self.do_scroll_restoration {
  193. self.window.scroll_to_with_x_and_y(0.0, 0.0)
  194. }
  195. }
  196. Err(e) => error!("failed to change state: ", e),
  197. }
  198. }
  199. fn navigate_external(&mut self, url: String) -> bool {
  200. match self.window.location().set_href(&url) {
  201. Ok(_) => true,
  202. Err(e) => {
  203. error!("failed to navigate to external url (", url, "): ", e);
  204. false
  205. }
  206. }
  207. }
  208. }
  209. #[cfg(feature = "serde")]
  210. impl<R: serde::Serialize + serde::de::DeserializeOwned + Routable> HistoryProvider<R>
  211. for WebHistory<R>
  212. where
  213. <R as std::str::FromStr>::Err: std::fmt::Display,
  214. {
  215. fn current_route(&self) -> R {
  216. match get_current::<WebHistoryState<_>>(&self.history) {
  217. // Try to get the route from the history state
  218. Some(route) => route.state,
  219. // If that fails, get the route from the current URL
  220. None => self.route_from_location(),
  221. }
  222. }
  223. fn current_prefix(&self) -> Option<String> {
  224. self.prefix.clone()
  225. }
  226. fn go_back(&mut self) {
  227. if let Err(e) = self.history.back() {
  228. error!("failed to go back: ", e)
  229. }
  230. }
  231. fn go_forward(&mut self) {
  232. if let Err(e) = self.history.forward() {
  233. error!("failed to go forward: ", e)
  234. }
  235. }
  236. fn push(&mut self, state: R) {
  237. use gloo_utils::format::JsValueSerdeExt;
  238. if JsValue::from_serde(&state) != JsValue::from_serde(&self.current_route()) {
  239. // don't push the same state twice
  240. return;
  241. }
  242. let w = window().expect("access to `window`");
  243. let h = w.history().expect("`window` has access to `history`");
  244. // update the scroll position before pushing the new state
  245. update_scroll::<R>(&w, &h);
  246. let path = self.full_path(&state);
  247. let state = self.create_state(state);
  248. self.handle_nav(push_state_and_url(&self.history, &state, path));
  249. }
  250. fn replace(&mut self, state: R) {
  251. let path = match &self.prefix {
  252. None => format!("{state}"),
  253. Some(prefix) => format!("{prefix}{state}"),
  254. };
  255. let state = self.create_state(state);
  256. self.handle_nav(replace_state_with_url(&self.history, &state, Some(&path)));
  257. }
  258. fn external(&mut self, url: String) -> bool {
  259. self.navigate_external(url)
  260. }
  261. fn updater(&mut self, callback: std::sync::Arc<dyn Fn() + Send + Sync>) {
  262. let w = self.window.clone();
  263. let h = self.history.clone();
  264. let s = self.listener_animation_frame.clone();
  265. let d = self.do_scroll_restoration;
  266. self.listener_navigation = Some(EventListener::new(&self.window, "popstate", move |_| {
  267. (*callback)();
  268. if d {
  269. let mut s = s.lock().expect("unpoisoned scroll mutex");
  270. if let Some(current_state) = get_current::<WebHistoryState<R>>(&h) {
  271. *s = Some(current_state.scroll.scroll_to(w.clone()));
  272. }
  273. }
  274. }));
  275. }
  276. }
  277. #[cfg(not(feature = "serde"))]
  278. impl<R: Routable> HistoryProvider<R> for WebHistory<R>
  279. where
  280. <R as std::str::FromStr>::Err: std::fmt::Display,
  281. {
  282. fn current_route(&self) -> R {
  283. self.route_from_location()
  284. }
  285. fn current_prefix(&self) -> Option<String> {
  286. self.prefix.clone()
  287. }
  288. fn go_back(&mut self) {
  289. if let Err(e) = self.history.back() {
  290. error!("failed to go back: ", e)
  291. }
  292. }
  293. fn go_forward(&mut self) {
  294. if let Err(e) = self.history.forward() {
  295. error!("failed to go forward: ", e)
  296. }
  297. }
  298. fn push(&mut self, state: R) {
  299. if state.to_string() == self.current_route().to_string() {
  300. // don't push the same state twice
  301. return;
  302. }
  303. let w = window().expect("access to `window`");
  304. let h = w.history().expect("`window` has access to `history`");
  305. // update the scroll position before pushing the new state
  306. update_scroll::<R>(&w, &h);
  307. let path = self.full_path(&state);
  308. let state: [f64; 2] = self.create_state(state);
  309. self.handle_nav(push_state_and_url(&self.history, &state, path));
  310. }
  311. fn replace(&mut self, state: R) {
  312. let path = match &self.prefix {
  313. None => format!("{state}"),
  314. Some(prefix) => format!("{prefix}{state}"),
  315. };
  316. let state = self.create_state(state);
  317. self.handle_nav(replace_state_with_url(&self.history, &state, Some(&path)));
  318. }
  319. fn external(&mut self, url: String) -> bool {
  320. self.navigate_external(url)
  321. }
  322. fn updater(&mut self, callback: std::sync::Arc<dyn Fn() + Send + Sync>) {
  323. let w = self.window.clone();
  324. let h = self.history.clone();
  325. let s = self.listener_animation_frame.clone();
  326. let d = self.do_scroll_restoration;
  327. self.listener_navigation = Some(EventListener::new(&self.window, "popstate", move |_| {
  328. (*callback)();
  329. if d {
  330. let mut s = s.lock().expect("unpoisoned scroll mutex");
  331. if let Some([x, y]) = get_current(&h) {
  332. *s = Some(ScrollPosition { x, y }.scroll_to(w.clone()));
  333. }
  334. }
  335. }));
  336. }
  337. }