use std::sync::{Arc, Mutex};
use gloo::{console::error, events::EventListener, render::AnimationFrame};
use wasm_bindgen::JsValue;
use web_sys::{window, History, ScrollRestoration, Window};
use crate::routable::Routable;
use super::{
web_history::{get_current, push_state_and_url, replace_state_with_url},
web_scroll::ScrollPosition,
HistoryProvider,
};
#[cfg(not(feature = "serde"))]
#[allow(clippy::extra_unused_type_parameters)]
fn update_scroll<R>(window: &Window, history: &History) {
let scroll = ScrollPosition::of_window(window);
if let Err(err) = replace_state_with_url(history, &[scroll.x, scroll.y], None) {
error!(err);
}
}
#[cfg(feature = "serde")]
fn update_scroll<R: serde::Serialize + serde::de::DeserializeOwned + Routable>(
window: &Window,
history: &History,
) {
if let Some(WebHistoryState { state, .. }) = get_current::<WebHistoryState<R>>(history) {
let scroll = ScrollPosition::of_window(window);
let state = WebHistoryState { state, scroll };
if let Err(err) = replace_state_with_url(history, &state, None) {
error!(err);
}
}
}
#[cfg(feature = "serde")]
#[derive(serde::Deserialize, serde::Serialize)]
struct WebHistoryState<R> {
state: R,
scroll: ScrollPosition,
}
pub struct WebHistory<R: Routable> {
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>,
window: Window,
phantom: std::marker::PhantomData<R>,
}
#[cfg(not(feature = "serde"))]
impl<R: Routable> Default for WebHistory<R>
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
fn default() -> Self {
Self::new(None, true)
}
}
#[cfg(feature = "serde")]
impl<R: Routable> Default for WebHistory<R>
where
<R as std::str::FromStr>::Err: std::fmt::Display,
R: serde::Serialize + serde::de::DeserializeOwned,
{
fn default() -> Self {
Self::new(None, true)
}
}
impl<R: Routable> WebHistory<R> {
#[cfg(not(feature = "serde"))]
pub fn new(prefix: Option<String>, do_scroll_restoration: bool) -> Self
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
let w = window().expect("access to `window`");
let h = w.history().expect("`window` has access to `history`");
let document = w.document().expect("`window` has access to `document`");
let myself = Self::new_inner(
prefix,
do_scroll_restoration,
EventListener::new(&document, "scroll", {
let mut last_updated = 0.0;
move |evt| {
let time_stamp = evt.time_stamp();
if (time_stamp - last_updated) < 100.0 {
return;
}
update_scroll::<R>(&w, &h);
last_updated = time_stamp;
}
}),
);
let current_route = myself.current_route();
let current_url = current_route.to_string();
let state = myself.create_state(current_route);
let _ = replace_state_with_url(&myself.history, &state, Some(¤t_url));
myself
}
#[cfg(feature = "serde")]
pub fn new(prefix: Option<String>, do_scroll_restoration: bool) -> Self
where
<R as std::str::FromStr>::Err: std::fmt::Display,
R: serde::Serialize + serde::de::DeserializeOwned,
{
let w = window().expect("access to `window`");
let h = w.history().expect("`window` has access to `history`");
let document = w.document().expect("`window` has access to `document`");
let myself = Self::new_inner(
prefix,
do_scroll_restoration,
EventListener::new(&document, "scroll", {
let mut last_updated = 0.0;
move |evt| {
let time_stamp = evt.time_stamp();
if (time_stamp - last_updated) < 100.0 {
return;
}
update_scroll::<R>(&w, &h);
last_updated = time_stamp;
}
}),
);
let current_route = myself.current_route();
log::trace!("initial route: {:?}", current_route);
let current_url = current_route.to_string();
let state = myself.create_state(current_route);
let _ = replace_state_with_url(&myself.history, &state, Some(¤t_url));
myself
}
fn new_inner(
prefix: Option<String>,
do_scroll_restoration: bool,
event_listener: EventListener,
) -> Self
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
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");
Some(event_listener)
}
false => None,
};
Self {
do_scroll_restoration,
history,
listener_navigation: None,
listener_scroll,
listener_animation_frame: Default::default(),
prefix,
window,
phantom: Default::default(),
}
}
fn scroll_pos(&self) -> ScrollPosition {
self.do_scroll_restoration
.then(|| ScrollPosition::of_window(&self.window))
.unwrap_or_default()
}
#[cfg(not(feature = "serde"))]
fn create_state(&self, _state: R) -> [f64; 2] {
let scroll = self.scroll_pos();
[scroll.x, scroll.y]
}
#[cfg(feature = "serde")]
fn create_state(&self, state: R) -> WebHistoryState<R> {
let scroll = self.scroll_pos();
WebHistoryState { state, scroll }
}
}
impl<R: Routable> WebHistory<R>
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
fn route_from_location(&self) -> R {
R::from_str(
&self
.window
.location()
.pathname()
.unwrap_or_else(|_| String::from("/")),
)
.unwrap_or_else(|err| panic!("{}", err))
}
fn full_path(&self, state: &R) -> String {
match &self.prefix {
None => format!("{state}"),
Some(prefix) => format!("{prefix}{state}"),
}
}
fn handle_nav(&self, result: Result<(), JsValue>) {
match result {
Ok(_) => {
if self.do_scroll_restoration {
self.window.scroll_to_with_x_and_y(0.0, 0.0)
}
}
Err(e) => error!("failed to change state: ", e),
}
}
fn navigate_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
}
}
}
}
#[cfg(feature = "serde")]
impl<R: serde::Serialize + serde::de::DeserializeOwned + Routable> HistoryProvider<R>
for WebHistory<R>
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
fn current_route(&self) -> R {
match get_current::<WebHistoryState<_>>(&self.history) {
Some(route) => route.state,
None => self.route_from_location(),
}
}
fn current_prefix(&self) -> Option<String> {
self.prefix.clone()
}
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, state: R) {
let path = self.full_path(&state);
let state = self.create_state(state);
self.handle_nav(push_state_and_url(&self.history, &state, path));
}
fn replace(&mut self, state: R) {
let path = match &self.prefix {
None => format!("{state}"),
Some(prefix) => format!("{prefix}{state}"),
};
let state = self.create_state(state);
self.handle_nav(replace_state_with_url(&self.history, &state, Some(&path)));
}
fn external(&mut self, url: String) -> bool {
self.navigate_external(url)
}
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");
if let Some(current_state) = get_current::<WebHistoryState<R>>(&h) {
*s = Some(current_state.scroll.scroll_to(w.clone()));
}
}
}));
}
}
#[cfg(not(feature = "serde"))]
impl<R: Routable> HistoryProvider<R> for WebHistory<R>
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
fn current_route(&self) -> R {
self.route_from_location()
}
fn current_prefix(&self) -> Option<String> {
self.prefix.clone()
}
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, state: R) {
let path = self.full_path(&state);
let state: [f64; 2] = self.create_state(state);
self.handle_nav(push_state_and_url(&self.history, &state, path));
}
fn replace(&mut self, state: R) {
let path = match &self.prefix {
None => format!("{state}"),
Some(prefix) => format!("{prefix}{state}"),
};
let state = self.create_state(state);
self.handle_nav(replace_state_with_url(&self.history, &state, Some(&path)));
}
fn external(&mut self, url: String) -> bool {
self.navigate_external(url)
}
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");
if let Some([x, y]) = get_current(&h) {
*s = Some(ScrollPosition { x, y }.scroll_to(w.clone()));
}
}
}));
}
}