use super::{handle_anchor_click, LocationChange, LocationProvider, Url};
use crate::{hooks::use_navigate, params::ParamsMap};
use core::fmt;
use futures::channel::oneshot;
use js_sys::{try_iter, Array, JsString};
use leptos::{ev, prelude::*};
use or_poisoned::OrPoisoned;
use reactive_graph::{
signal::ArcRwSignal,
traits::{ReadUntracked, Set},
};
use std::{
borrow::Cow,
string::String,
sync::{Arc, Mutex},
};
use tachys::dom::{document, window};
use wasm_bindgen::{JsCast, JsValue};
use web_sys::UrlSearchParams;
#[derive(Clone)]
pub struct BrowserUrl {
url: ArcRwSignal<Url>,
pub(crate) pending_navigation: Arc<Mutex<Option<oneshot::Sender<()>>>>,
pub(crate) path_stack: ArcStoredValue<Vec<Url>>,
pub(crate) is_back: ArcRwSignal<bool>,
}
impl fmt::Debug for BrowserUrl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BrowserUrl").finish_non_exhaustive()
}
}
impl BrowserUrl {
fn scroll_to_el(loc_scroll: bool) {
if let Ok(hash) = window().location().hash() {
if !hash.is_empty() {
let hash = js_sys::decode_uri(&hash[1..])
.ok()
.and_then(|decoded| decoded.as_string())
.unwrap_or(hash);
let el = document().get_element_by_id(&hash);
if let Some(el) = el {
el.scroll_into_view();
return;
}
}
}
if loc_scroll {
window().scroll_to_with_x_and_y(0.0, 0.0);
}
}
}
impl LocationProvider for BrowserUrl {
type Error = JsValue;
fn new() -> Result<Self, JsValue> {
let url = ArcRwSignal::new(Self::current()?);
let path_stack = ArcStoredValue::new(
Self::current().map(|n| vec![n]).unwrap_or_default(),
);
Ok(Self {
url,
pending_navigation: Default::default(),
path_stack,
is_back: Default::default(),
})
}
fn as_url(&self) -> &ArcRwSignal<Url> {
&self.url
}
fn current() -> Result<Url, Self::Error> {
let location = window().location();
Ok(Url {
origin: location.origin()?,
path: location.pathname()?,
search: location
.search()?
.strip_prefix('?')
.map(String::from)
.unwrap_or_default(),
search_params: search_params_from_web_url(
&UrlSearchParams::new_with_str(&location.search()?)?,
)?,
hash: location.hash()?,
})
}
fn parse(url: &str) -> Result<Url, Self::Error> {
let base = window().location().origin()?;
Self::parse_with_base(url, &base)
}
fn parse_with_base(url: &str, base: &str) -> Result<Url, Self::Error> {
let location = web_sys::Url::new_with_base(url, base)?;
Ok(Url {
origin: location.origin(),
path: location.pathname(),
search: location
.search()
.strip_prefix('?')
.map(String::from)
.unwrap_or_default(),
search_params: search_params_from_web_url(
&location.search_params(),
)?,
hash: location.hash(),
})
}
fn init(&self, base: Option<Cow<'static, str>>) {
let navigate = {
let url = self.url.clone();
let pending = Arc::clone(&self.pending_navigation);
let this = self.clone();
move |new_url: Url, loc| {
let same_path = {
let curr = url.read_untracked();
curr.origin() == new_url.origin()
&& curr.path() == new_url.path()
};
url.set(new_url.clone());
if same_path {
this.complete_navigation(&loc);
}
let pending = Arc::clone(&pending);
let (tx, rx) = oneshot::channel::<()>();
if !same_path {
*pending.lock().or_poisoned() = Some(tx);
}
let url = url.clone();
let this = this.clone();
async move {
if !same_path {
if rx.await.is_ok() {
let curr = url.read_untracked();
if curr == new_url {
this.complete_navigation(&loc);
}
}
}
}
}
};
let handle_anchor_click =
handle_anchor_click(base, Self::parse_with_base, navigate);
let click_handle = window_event_listener(ev::click, move |ev| {
if let Err(e) = handle_anchor_click(ev) {
#[cfg(feature = "tracing")]
tracing::error!("{e:?}");
#[cfg(not(feature = "tracing"))]
web_sys::console::error_1(&e);
}
});
let popstate_cb = {
let url = self.url.clone();
let path_stack = self.path_stack.clone();
let is_back = self.is_back.clone();
move || match Self::current() {
Ok(new_url) => {
let mut stack = path_stack.write_value();
let is_navigating_back = stack.len() == 1
|| (stack.len() >= 2
&& stack.get(stack.len() - 2) == Some(&new_url));
if is_navigating_back {
stack.pop();
}
is_back.set(is_navigating_back);
url.set(new_url);
}
Err(e) => {
#[cfg(feature = "tracing")]
tracing::error!("{e:?}");
#[cfg(not(feature = "tracing"))]
web_sys::console::error_1(&e);
}
}
};
let popstate_handle =
window_event_listener(ev::popstate, move |_| popstate_cb());
on_cleanup(|| {
click_handle.remove();
popstate_handle.remove();
});
}
fn ready_to_complete(&self) {
if let Some(tx) = self.pending_navigation.lock().or_poisoned().take() {
_ = tx.send(());
}
}
fn complete_navigation(&self, loc: &LocationChange) {
let history = window().history().unwrap();
let current_path = self
.path_stack
.read_value()
.last()
.map(|url| url.to_full_path());
let add_to_stack = current_path.as_ref() != Some(&loc.value);
if loc.replace {
history
.replace_state_with_url(
&loc.state.to_js_value(),
"",
Some(&loc.value),
)
.unwrap();
} else if add_to_stack {
let state = &loc.state.to_js_value();
history
.push_state_with_url(state, "", Some(&loc.value))
.unwrap();
}
if let Ok(url) = Self::current() {
if add_to_stack {
self.path_stack.write_value().push(url);
}
self.is_back.set(false);
}
Self::scroll_to_el(loc.scroll);
}
fn redirect(loc: &str) {
let navigate = use_navigate();
let Some(url) = resolve_redirect_url(loc) else {
return; };
let current_origin = location().origin().unwrap();
if url.origin() == current_origin {
let navigate = navigate.clone();
request_animation_frame(move || {
navigate(&url.href(), Default::default());
});
} else if let Err(e) = location().set_href(&url.href()) {
leptos::logging::error!("Failed to redirect: {e:#?}");
}
}
fn is_back(&self) -> ReadSignal<bool> {
self.is_back.read_only().into()
}
}
fn search_params_from_web_url(
params: &web_sys::UrlSearchParams,
) -> Result<ParamsMap, JsValue> {
try_iter(params)?
.into_iter()
.flatten()
.map(|pair| {
pair.and_then(|pair| {
let row = pair.dyn_into::<Array>()?;
Ok((
String::from(row.get(0).dyn_into::<JsString>()?),
String::from(row.get(1).dyn_into::<JsString>()?),
))
})
})
.collect()
}
pub(crate) fn resolve_redirect_url(loc: &str) -> Option<web_sys::Url> {
let origin = match window().location().origin() {
Ok(origin) => origin,
Err(e) => {
leptos::logging::error!("Failed to get origin: {:#?}", e);
return None;
}
};
let base = origin;
match web_sys::Url::new_with_base(loc, &base) {
Ok(url) => Some(url),
Err(e) => {
leptos::logging::error!(
"Invalid redirect location: {}",
e.as_string().unwrap_or_default(),
);
None
}
}
}