use std::{borrow::Cow, error::Error};
pub use url::Url;
use crate::prelude::*;
pub struct Location {
url: Url,
#[cfg(target_arch = "wasm32")]
closure: Option<LocationClosure>,
}
#[cfg(target_arch = "wasm32")]
type LocationClosure = web_sys::wasm_bindgen::prelude::Closure<dyn FnMut()>;
impl Location {
pub fn init(url: &str, ctx: &impl AsRef<ProviderCtx>) -> Result<(), Box<dyn Error>> {
let url = Url::parse(url).map_err(|e| format!("Invalid URL '{}': {}", url, e))?;
let mut loc = Self::write_of(ctx);
#[cfg(target_arch = "wasm32")]
{
#[cfg(target_arch = "wasm32")]
loc.same_origin_check(&url)?;
Self::update_browser_history(url.as_str())?;
}
#[cfg(not(target_arch = "wasm32"))]
if loc.url != Self::fallback_url() {
return Err(format!("The location is already initialized to '{}'", loc.url).into());
}
loc.url = url;
Ok(())
}
pub fn of(ctx: &impl AsRef<ProviderCtx>) -> QueryRef<'_, Self> {
Provider::of(ctx).expect("Location provider not found")
}
pub fn state_of(ctx: &impl AsRef<ProviderCtx>) -> Stateful<Self> {
Provider::state_of::<Stateful<Self>>(ctx)
.expect("Location provider not found")
.clone_writer()
}
pub fn get_query(&self, name: &str) -> Option<Cow<'_, str>> {
self
.url
.query_pairs()
.find_map(|(key, value)| (key == name).then_some(value))
}
pub fn goto(url: &str, ctx: &impl AsRef<ProviderCtx>) -> Result<(), Box<dyn Error>> {
Self::write_of(ctx).resolve_relative(url)
}
pub fn resolve_relative(&mut self, url: &str) -> Result<(), Box<dyn Error>> {
let new_url = self
.join(url)
.map_err(|e| format!("Invalid path '{}': {}", url, e))?;
#[cfg(target_arch = "wasm32")]
Self::update_browser_history(new_url.as_str())?;
self.url = new_url;
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn stateful() -> Stateful<Location> {
Stateful::new(Location { url: Self::fallback_url() })
}
#[cfg(target_arch = "wasm32")]
pub(crate) fn stateful() -> Stateful<Location> {
use web_sys::wasm_bindgen::JsCast;
let initial_url = Self::current_browser_url().unwrap_or_else(Self::fallback_url);
let location = Stateful::new(Location { url: initial_url, closure: None });
let popstate_handler = Self::create_popstate_handler(location.clone_writer());
web_sys::window()
.expect("Browser window unavailable")
.add_event_listener_with_callback("popstate", popstate_handler.as_ref().unchecked_ref())
.expect("Failed to register popstate listener");
location.write().closure = Some(popstate_handler);
location
}
pub(crate) fn release(&mut self) {
#[cfg(target_arch = "wasm32")]
{
use web_sys::wasm_bindgen::JsCast;
if let Some(closure) = self.closure.take() {
let _ = web_sys::window().and_then(|w| {
w.remove_event_listener_with_callback("popstate", closure.as_ref().unchecked_ref())
.ok()
});
}
}
}
#[cfg(target_arch = "wasm32")]
fn same_origin_check(&self, url: &Url) -> Result<(), String> {
let self_origin = self.url.origin();
let other_origin = url.origin();
if self_origin == other_origin {
return Ok(());
}
let self_display = self_origin.unicode_serialization();
let other_display = other_origin.unicode_serialization();
Err(format!("Cross-origin navigation blocked: {self_display} → {other_display}"))
}
fn write_of(ctx: &impl AsRef<ProviderCtx>) -> WriteRef<'_, Location> {
Provider::write_of(ctx).expect("Location write provider not found")
}
#[cfg(target_arch = "wasm32")]
fn update_browser_history(url: &str) -> Result<(), Box<dyn Error>> {
let history = web_sys::window()
.and_then(|w| w.history().ok())
.ok_or("Browser history unavailable")?;
if Self::current_browser_url().is_some_and(|u| u.as_str() != url) {
history
.push_state_with_url(&web_sys::wasm_bindgen::JsValue::NULL, "", Some(url))
.map_err(|e| format!("History API error: {:?}", e))?;
}
Ok(())
}
#[cfg(target_arch = "wasm32")]
fn create_popstate_handler(location: Stateful<Location>) -> LocationClosure {
web_sys::wasm_bindgen::prelude::Closure::<dyn FnMut()>::new(move || {
if let Some(new_url) = Self::current_browser_url() {
let mut loc = location.write();
if loc.url != new_url {
loc.url = new_url;
}
}
})
}
#[cfg(target_arch = "wasm32")]
fn current_browser_url() -> Option<Url> {
web_sys::window()
.and_then(|w| w.location().href().ok())
.and_then(|href| Url::parse(&href).ok())
}
fn fallback_url() -> Url { Url::parse("https://ribir.org").unwrap() }
}
impl std::ops::Deref for Location {
type Target = Url;
fn deref(&self) -> &Self::Target { &self.url }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn goto() {
reset_test_env!();
let location = Location::stateful();
let mut w = location.write();
w.resolve_relative("a/b/c").unwrap();
assert_eq!(w.url.as_str(), "https://ribir.org/a/b/c");
w.resolve_relative("/a/b/d").unwrap();
assert_eq!(w.url.as_str(), "https://ribir.org/a/b/d");
w.resolve_relative("../e").unwrap();
assert_eq!(w.url.as_str(), "https://ribir.org/a/e");
w.resolve_relative("./f").unwrap();
assert_eq!(w.url.as_str(), "https://ribir.org/a/f");
}
}