web.rs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  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. #[allow(dead_code)]
  55. listener_scroll: Option<EventListener>,
  56. listener_animation_frame: Arc<Mutex<Option<AnimationFrame>>>,
  57. prefix: Option<String>,
  58. window: Window,
  59. phantom: std::marker::PhantomData<R>,
  60. }
  61. #[cfg(not(feature = "serde"))]
  62. impl<R: Routable> Default for WebHistory<R>
  63. where
  64. <R as std::str::FromStr>::Err: std::fmt::Display,
  65. {
  66. fn default() -> Self {
  67. Self::new(None, true)
  68. }
  69. }
  70. #[cfg(feature = "serde")]
  71. impl<R: Routable> Default for WebHistory<R>
  72. where
  73. <R as std::str::FromStr>::Err: std::fmt::Display,
  74. R: serde::Serialize + serde::de::DeserializeOwned,
  75. {
  76. fn default() -> Self {
  77. Self::new(None, true)
  78. }
  79. }
  80. impl<R: Routable> WebHistory<R> {
  81. #[cfg(not(feature = "serde"))]
  82. /// Create a new [`WebHistory`].
  83. ///
  84. /// If `do_scroll_restoration` is [`true`], [`WebHistory`] will take control of the history
  85. /// state. It'll also set the browsers scroll restoration to `manual`.
  86. fn new(prefix: Option<String>, do_scroll_restoration: bool) -> Self
  87. where
  88. <R as std::str::FromStr>::Err: std::fmt::Display,
  89. {
  90. let w = window().expect("access to `window`");
  91. let h = w.history().expect("`window` has access to `history`");
  92. let document = w.document().expect("`window` has access to `document`");
  93. let myself = Self::new_inner(
  94. prefix,
  95. do_scroll_restoration,
  96. EventListener::new(&document, "scroll", {
  97. let mut last_updated = 0.0;
  98. move |evt| {
  99. // the time stamp in milliseconds
  100. let time_stamp = evt.time_stamp();
  101. // throttle the scroll event to 100ms
  102. if (time_stamp - last_updated) < 100.0 {
  103. return;
  104. }
  105. update_scroll::<R>(&w, &h);
  106. last_updated = time_stamp;
  107. }
  108. }),
  109. );
  110. let current_route = myself.current_route();
  111. let current_url = current_route.to_string();
  112. let state = myself.create_state(current_route);
  113. let _ = replace_state_with_url(&myself.history, &state, Some(&current_url));
  114. myself
  115. }
  116. #[cfg(feature = "serde")]
  117. /// Create a new [`WebHistory`].
  118. ///
  119. /// If `do_scroll_restoration` is [`true`], [`WebHistory`] will take control of the history
  120. /// state. It'll also set the browsers scroll restoration to `manual`.
  121. fn new(prefix: Option<String>, do_scroll_restoration: bool) -> Self
  122. where
  123. <R as std::str::FromStr>::Err: std::fmt::Display,
  124. R: serde::Serialize + serde::de::DeserializeOwned,
  125. {
  126. let w = window().expect("access to `window`");
  127. let h = w.history().expect("`window` has access to `history`");
  128. let document = w.document().expect("`window` has access to `document`");
  129. let myself = Self::new_inner(
  130. prefix,
  131. do_scroll_restoration,
  132. EventListener::new(&document, "scroll", {
  133. let mut last_updated = 0.0;
  134. move |evt| {
  135. // the time stamp in milliseconds
  136. let time_stamp = evt.time_stamp();
  137. // throttle the scroll event to 100ms
  138. if (time_stamp - last_updated) < 100.0 {
  139. return;
  140. }
  141. update_scroll::<R>(&w, &h);
  142. last_updated = time_stamp;
  143. }
  144. }),
  145. );
  146. let current_route = myself.current_route();
  147. let current_url = current_route.to_string();
  148. let state = myself.create_state(current_route);
  149. let _ = replace_state_with_url(&myself.history, &state, Some(&current_url));
  150. myself
  151. }
  152. fn new_inner(
  153. prefix: Option<String>,
  154. do_scroll_restoration: bool,
  155. event_listener: EventListener,
  156. ) -> Self
  157. where
  158. <R as std::str::FromStr>::Err: std::fmt::Display,
  159. {
  160. let window = window().expect("access to `window`");
  161. let history = window.history().expect("`window` has access to `history`");
  162. let listener_scroll = match do_scroll_restoration {
  163. true => {
  164. history
  165. .set_scroll_restoration(ScrollRestoration::Manual)
  166. .expect("`history` can set scroll restoration");
  167. Some(event_listener)
  168. }
  169. false => None,
  170. };
  171. Self {
  172. do_scroll_restoration,
  173. history,
  174. listener_navigation: None,
  175. listener_scroll,
  176. listener_animation_frame: Default::default(),
  177. prefix,
  178. window,
  179. phantom: Default::default(),
  180. }
  181. }
  182. fn scroll_pos(&self) -> ScrollPosition {
  183. self.do_scroll_restoration
  184. .then(|| ScrollPosition::of_window(&self.window))
  185. .unwrap_or_default()
  186. }
  187. #[cfg(not(feature = "serde"))]
  188. fn create_state(&self, _state: R) -> [f64; 2] {
  189. let scroll = self.scroll_pos();
  190. [scroll.x, scroll.y]
  191. }
  192. #[cfg(feature = "serde")]
  193. fn create_state(&self, state: R) -> WebHistoryState<R> {
  194. let scroll = self.scroll_pos();
  195. WebHistoryState { state, scroll }
  196. }
  197. }
  198. impl<R: Routable> WebHistory<R>
  199. where
  200. <R as std::str::FromStr>::Err: std::fmt::Display,
  201. {
  202. fn route_from_location(&self) -> R {
  203. R::from_str(
  204. &self
  205. .window
  206. .location()
  207. .pathname()
  208. .unwrap_or_else(|_| String::from("/")),
  209. )
  210. .unwrap_or_else(|err| panic!("{}", err))
  211. }
  212. fn full_path(&self, state: &R) -> String {
  213. match &self.prefix {
  214. None => format!("{state}"),
  215. Some(prefix) => format!("{prefix}{state}"),
  216. }
  217. }
  218. fn handle_nav(&self, result: Result<(), JsValue>) {
  219. match result {
  220. Ok(_) => {
  221. if self.do_scroll_restoration {
  222. self.window.scroll_to_with_x_and_y(0.0, 0.0)
  223. }
  224. }
  225. Err(e) => error!("failed to change state: ", e),
  226. }
  227. }
  228. fn navigate_external(&mut self, url: String) -> bool {
  229. match self.window.location().set_href(&url) {
  230. Ok(_) => true,
  231. Err(e) => {
  232. error!("failed to navigate to external url (", url, "): ", e);
  233. false
  234. }
  235. }
  236. }
  237. }
  238. #[cfg(feature = "serde")]
  239. impl<R: serde::Serialize + serde::de::DeserializeOwned + Routable> HistoryProvider<R>
  240. for WebHistory<R>
  241. where
  242. <R as std::str::FromStr>::Err: std::fmt::Display,
  243. {
  244. fn current_route(&self) -> R {
  245. match get_current::<WebHistoryState<_>>(&self.history) {
  246. // Try to get the route from the history state
  247. Some(route) => route.state,
  248. // If that fails, get the route from the current URL
  249. None => self.route_from_location(),
  250. }
  251. }
  252. fn current_prefix(&self) -> Option<String> {
  253. self.prefix.clone()
  254. }
  255. fn go_back(&mut self) {
  256. if let Err(e) = self.history.back() {
  257. error!("failed to go back: ", e)
  258. }
  259. }
  260. fn go_forward(&mut self) {
  261. if let Err(e) = self.history.forward() {
  262. error!("failed to go forward: ", e)
  263. }
  264. }
  265. fn push(&mut self, state: R) {
  266. let path = self.full_path(&state);
  267. let state = self.create_state(state);
  268. self.handle_nav(push_state_and_url(&self.history, &state, path));
  269. }
  270. fn replace(&mut self, state: R) {
  271. let path = match &self.prefix {
  272. None => format!("{state}"),
  273. Some(prefix) => format!("{prefix}{state}"),
  274. };
  275. let state = self.create_state(state);
  276. self.handle_nav(replace_state_with_url(&self.history, &state, Some(&path)));
  277. }
  278. fn external(&mut self, url: String) -> bool {
  279. self.navigate_external(url)
  280. }
  281. fn updater(&mut self, callback: std::sync::Arc<dyn Fn() + Send + Sync>) {
  282. let w = self.window.clone();
  283. let h = self.history.clone();
  284. let s = self.listener_animation_frame.clone();
  285. let d = self.do_scroll_restoration;
  286. self.listener_navigation = Some(EventListener::new(&self.window, "popstate", move |_| {
  287. (*callback)();
  288. if d {
  289. let mut s = s.lock().expect("unpoisoned scroll mutex");
  290. if let Some(current_state) = get_current::<WebHistoryState<R>>(&h) {
  291. *s = Some(current_state.scroll.scroll_to(w.clone()));
  292. }
  293. }
  294. }));
  295. }
  296. }
  297. #[cfg(not(feature = "serde"))]
  298. impl<R: Routable> HistoryProvider<R> for WebHistory<R>
  299. where
  300. <R as std::str::FromStr>::Err: std::fmt::Display,
  301. {
  302. fn current_route(&self) -> R {
  303. self.route_from_location()
  304. }
  305. fn current_prefix(&self) -> Option<String> {
  306. self.prefix.clone()
  307. }
  308. fn go_back(&mut self) {
  309. if let Err(e) = self.history.back() {
  310. error!("failed to go back: ", e)
  311. }
  312. }
  313. fn go_forward(&mut self) {
  314. if let Err(e) = self.history.forward() {
  315. error!("failed to go forward: ", e)
  316. }
  317. }
  318. fn push(&mut self, state: R) {
  319. let path = self.full_path(&state);
  320. let state: [f64; 2] = self.create_state(state);
  321. self.handle_nav(push_state_and_url(&self.history, &state, path));
  322. }
  323. fn replace(&mut self, state: R) {
  324. let path = match &self.prefix {
  325. None => format!("{state}"),
  326. Some(prefix) => format!("{prefix}{state}"),
  327. };
  328. let state = self.create_state(state);
  329. self.handle_nav(replace_state_with_url(&self.history, &state, Some(&path)));
  330. }
  331. fn external(&mut self, url: String) -> bool {
  332. self.navigate_external(url)
  333. }
  334. fn updater(&mut self, callback: std::sync::Arc<dyn Fn() + Send + Sync>) {
  335. let w = self.window.clone();
  336. let h = self.history.clone();
  337. let s = self.listener_animation_frame.clone();
  338. let d = self.do_scroll_restoration;
  339. self.listener_navigation = Some(EventListener::new(&self.window, "popstate", move |_| {
  340. (*callback)();
  341. if d {
  342. let mut s = s.lock().expect("unpoisoned scroll mutex");
  343. if let Some([x, y]) = get_current(&h) {
  344. *s = Some(ScrollPosition { x, y }.scroll_to(w.clone()));
  345. }
  346. }
  347. }));
  348. }
  349. }