|
@@ -1,11 +1,13 @@
|
|
-use gloo::{
|
|
|
|
- history::{BrowserHistory, History, HistoryListener},
|
|
|
|
- utils::window,
|
|
|
|
-};
|
|
|
|
|
|
+use std::sync::{Arc, Mutex};
|
|
|
|
+
|
|
|
|
+use gloo::{events::EventListener, render::AnimationFrame};
|
|
use log::error;
|
|
use log::error;
|
|
-use web_sys::Window;
|
|
|
|
|
|
+use web_sys::{window, History, ScrollRestoration, Window};
|
|
|
|
|
|
-use super::HistoryProvider;
|
|
|
|
|
|
+use super::{
|
|
|
|
+ web_scroll::{top_left, update_history, update_scroll},
|
|
|
|
+ HistoryProvider,
|
|
|
|
+};
|
|
|
|
|
|
/// A [`HistoryProvider`] that integrates with a browser via the [History API].
|
|
/// A [`HistoryProvider`] that integrates with a browser via the [History API].
|
|
///
|
|
///
|
|
@@ -22,38 +24,60 @@ use super::HistoryProvider;
|
|
///
|
|
///
|
|
/// [History API]: https://developer.mozilla.org/en-US/docs/Web/API/History_API
|
|
/// [History API]: https://developer.mozilla.org/en-US/docs/Web/API/History_API
|
|
pub struct WebHistory {
|
|
pub struct WebHistory {
|
|
- history: BrowserHistory,
|
|
|
|
- listener_navigation: Option<HistoryListener>,
|
|
|
|
|
|
+ do_scroll_restoration: bool,
|
|
|
|
+ history: History,
|
|
|
|
+ listener_navigation: Option<EventListener>,
|
|
|
|
+ #[allow(dead_code)]
|
|
|
|
+ listener_scroll: Option<EventListener>,
|
|
|
|
+ listener_animation_frame: Arc<Mutex<Option<AnimationFrame>>>,
|
|
prefix: Option<String>,
|
|
prefix: Option<String>,
|
|
window: Window,
|
|
window: Window,
|
|
}
|
|
}
|
|
|
|
|
|
impl WebHistory {
|
|
impl WebHistory {
|
|
- /// Create a new [`WebHistory`] with a prefix.
|
|
|
|
- #[must_use]
|
|
|
|
- pub fn with_prefix(prefix: impl Into<String>) -> Self {
|
|
|
|
- Self {
|
|
|
|
- prefix: Some(prefix.into()),
|
|
|
|
- ..Default::default()
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-}
|
|
|
|
|
|
+ /// Create a new [`WebHistory`].
|
|
|
|
+ ///
|
|
|
|
+ /// If `do_scroll_restoration` is [`true`], [`WebHistory`] will take control of the history
|
|
|
|
+ /// state. It'll also set the browsers scroll restoration to `manual`.
|
|
|
|
+ pub fn new(prefix: Option<String>, do_scroll_restoration: bool) -> Self {
|
|
|
|
+ let window = window().expect("access to `window`");
|
|
|
|
+ let history = window.history().expect("`window` has access to `history`");
|
|
|
|
+
|
|
|
|
+ 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,
|
|
|
|
+ };
|
|
|
|
|
|
-impl Default for WebHistory {
|
|
|
|
- fn default() -> Self {
|
|
|
|
Self {
|
|
Self {
|
|
- history: BrowserHistory::new(),
|
|
|
|
|
|
+ do_scroll_restoration,
|
|
|
|
+ history,
|
|
listener_navigation: None,
|
|
listener_navigation: None,
|
|
- prefix: None,
|
|
|
|
- window: window(),
|
|
|
|
|
|
+ listener_scroll,
|
|
|
|
+ listener_animation_frame: Default::default(),
|
|
|
|
+ prefix: prefix,
|
|
|
|
+ window,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
impl HistoryProvider for WebHistory {
|
|
impl HistoryProvider for WebHistory {
|
|
fn current_path(&self) -> String {
|
|
fn current_path(&self) -> String {
|
|
- let location = self.history.location();
|
|
|
|
- let path = location.path();
|
|
|
|
|
|
+ let path = self
|
|
|
|
+ .window
|
|
|
|
+ .location()
|
|
|
|
+ .pathname()
|
|
|
|
+ .unwrap_or_else(|_| String::from("/"));
|
|
|
|
|
|
match &self.prefix {
|
|
match &self.prefix {
|
|
None => path.to_string(),
|
|
None => path.to_string(),
|
|
@@ -66,14 +90,17 @@ impl HistoryProvider for WebHistory {
|
|
}
|
|
}
|
|
|
|
|
|
fn current_query(&self) -> Option<String> {
|
|
fn current_query(&self) -> Option<String> {
|
|
- let location = self.history.location();
|
|
|
|
- let query = location.query_str();
|
|
|
|
-
|
|
|
|
- if query.is_empty() {
|
|
|
|
- None
|
|
|
|
- } else {
|
|
|
|
- Some(query.to_string())
|
|
|
|
- }
|
|
|
|
|
|
+ self.window
|
|
|
|
+ .location()
|
|
|
|
+ .search()
|
|
|
|
+ .ok()
|
|
|
|
+ .map(|mut q| {
|
|
|
|
+ if q.starts_with('?') {
|
|
|
|
+ q.remove(0);
|
|
|
|
+ }
|
|
|
|
+ q
|
|
|
|
+ })
|
|
|
|
+ .and_then(|q| q.is_empty().then_some(q))
|
|
}
|
|
}
|
|
|
|
|
|
fn current_prefix(&self) -> Option<String> {
|
|
fn current_prefix(&self) -> Option<String> {
|
|
@@ -81,25 +108,61 @@ impl HistoryProvider for WebHistory {
|
|
}
|
|
}
|
|
|
|
|
|
fn go_back(&mut self) {
|
|
fn go_back(&mut self) {
|
|
- self.history.back();
|
|
|
|
|
|
+ if let Err(e) = self.history.back() {
|
|
|
|
+ error!("failed to go back: {e:?}")
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
fn go_forward(&mut self) {
|
|
fn go_forward(&mut self) {
|
|
- self.history.forward();
|
|
|
|
|
|
+ if let Err(e) = self.history.forward() {
|
|
|
|
+ error!("failed to go forward: {e:?}")
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
fn push(&mut self, path: String) {
|
|
fn push(&mut self, path: String) {
|
|
- self.history.push(match &self.prefix {
|
|
|
|
|
|
+ let path = match &self.prefix {
|
|
None => path,
|
|
None => path,
|
|
Some(prefix) => format!("{prefix}{path}"),
|
|
Some(prefix) => format!("{prefix}{path}"),
|
|
- });
|
|
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ 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(&path));
|
|
|
|
+
|
|
|
|
+ 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: String) {
|
|
fn replace(&mut self, path: String) {
|
|
- self.history.replace(match &self.prefix {
|
|
|
|
|
|
+ let path = match &self.prefix {
|
|
None => path,
|
|
None => path,
|
|
Some(prefix) => format!("{prefix}{path}"),
|
|
Some(prefix) => format!("{prefix}{path}"),
|
|
- });
|
|
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ 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(&path));
|
|
|
|
+
|
|
|
|
+ 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 {
|
|
fn external(&mut self, url: String) -> bool {
|
|
@@ -113,6 +176,17 @@ impl HistoryProvider for WebHistory {
|
|
}
|
|
}
|
|
|
|
|
|
fn updater(&mut self, callback: std::sync::Arc<dyn Fn() + Send + Sync>) {
|
|
fn updater(&mut self, callback: std::sync::Arc<dyn Fn() + Send + Sync>) {
|
|
- self.listener_navigation = Some(self.history.listen(move || (*callback)()));
|
|
|
|
|
|
+ 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));
|
|
|
|
+ }
|
|
|
|
+ }));
|
|
}
|
|
}
|
|
}
|
|
}
|