use silex_core::reactivity::{Memo, ReadSignal, Signal, WriteSignal, provide_context, use_context};
use silex_core::traits::{Get, GetUntracked, Set};
use silex_dom::view::{AnyView, View};
use std::collections::HashMap;
use std::rc::Rc;
use web_sys::Node;
#[derive(Clone)]
pub struct ViewFactory(pub Rc<dyn Fn() -> AnyView>);
impl PartialEq for ViewFactory {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.0, &other.0)
}
}
impl View for ViewFactory {
fn mount(self, parent: &Node) {
let factory = self.0.clone();
let closure = move || (factory)();
closure.mount(parent);
}
}
#[derive(Clone)]
pub struct RouterContext {
pub base_path: String,
pub path: ReadSignal<String>,
pub search: ReadSignal<String>,
pub navigator: Navigator,
}
#[derive(Clone)]
pub struct Navigator {
pub(crate) base_path: String,
pub(crate) path: ReadSignal<String>,
pub(crate) search: ReadSignal<String>,
pub(crate) set_path: WriteSignal<String>,
pub(crate) set_search: WriteSignal<String>,
}
impl Navigator {
fn handle_navigation(&self, url: &str, replace: bool) {
let window = web_sys::window().unwrap();
let full_url = if url.starts_with('/') {
if self.base_path == "/" || self.base_path.is_empty() {
url.to_string()
} else {
let base = self.base_path.trim_end_matches('/');
format!("{}{}", base, url)
}
} else {
url.to_string()
};
if let Ok(history) = window.history() {
if replace {
let _ = history.replace_state_with_url(
&wasm_bindgen::JsValue::NULL,
"",
Some(&full_url),
);
} else {
let _ =
history.push_state_with_url(&wasm_bindgen::JsValue::NULL, "", Some(&full_url));
}
}
let location = window.location();
let raw_path = location.pathname().unwrap_or_else(|_| "/".to_string());
let logical_path = if !self.base_path.is_empty()
&& self.base_path != "/"
&& raw_path.starts_with(&self.base_path)
{
let p = &raw_path[self.base_path.len()..];
if p.is_empty() { "/" } else { p }
} else {
&raw_path
};
let search = location.search().unwrap_or_default();
if self.path.get_untracked() != logical_path {
self.set_path.set(logical_path.to_string());
}
if self.search.get_untracked() != search {
self.set_search.set(search);
}
}
pub fn push<T: crate::router::ToRoute>(&self, to: T) {
self.handle_navigation(&to.to_route(), false);
}
pub fn replace<T: crate::router::ToRoute>(&self, to: T) {
self.handle_navigation(&to.to_route(), true);
}
}
#[derive(Clone)]
pub(crate) struct RouterContextProps {
pub base_path: String,
pub path: ReadSignal<String>,
pub search: ReadSignal<String>,
pub set_path: WriteSignal<String>,
pub set_search: WriteSignal<String>,
}
pub(crate) fn provide_router_context(props: RouterContextProps) {
let navigator = Navigator {
base_path: props.base_path.clone(),
path: props.path,
search: props.search,
set_path: props.set_path,
set_search: props.set_search,
};
let ctx = RouterContext {
base_path: props.base_path,
path: props.path,
search: props.search,
navigator,
};
let _ = provide_context(ctx);
}
pub fn use_router() -> Option<RouterContext> {
use_context::<RouterContext>()
}
pub fn use_navigate() -> Navigator {
use_router()
.expect("use_navigate called outside of <Router>")
.navigator
}
pub fn use_location_path() -> Signal<String> {
use_router()
.map(|ctx| ctx.path.into())
.expect("use_location_path called outside of <Router>")
}
pub fn use_location_search() -> Signal<String> {
use_router()
.map(|ctx| ctx.search.into())
.expect("use_location called outside of <Router>")
}
pub fn use_query_map() -> silex_core::reactivity::Memo<HashMap<String, String>> {
let search_signal = use_location_search();
Memo::new(move |_| {
let s = search_signal.get();
let mut map = HashMap::new();
let clean = s.trim_start_matches('?');
if clean.is_empty() {
return map;
}
for pair in clean.split('&') {
if let Some((key, value)) = pair.split_once('=') {
let k = js_sys::decode_uri_component(key)
.ok()
.and_then(|x| x.as_string())
.unwrap_or(key.to_string());
let v = js_sys::decode_uri_component(value)
.ok()
.and_then(|x| x.as_string())
.unwrap_or(value.to_string());
map.insert(k, v);
}
}
map
})
}
pub fn use_query_signal(key: impl Into<String>) -> silex_core::reactivity::RwSignal<String> {
use silex_core::reactivity::{Effect, RwSignal};
let key = key.into();
let query_map = use_query_map();
let navigator = use_navigate();
let initial_value = query_map
.get_untracked()
.get(&key)
.cloned()
.unwrap_or_default();
let signal = RwSignal::new(initial_value);
Effect::new({
let key = key.clone();
let signal = signal;
move |_| {
let map = query_map.get();
let url_val = map.get(&key).map(|s| s.as_str()).unwrap_or("");
if signal.get_untracked() != url_val {
signal.set(url_val.to_string());
}
}
});
Effect::new(move |_| {
let val = signal.get();
let current_map = query_map.get_untracked();
let current_url_val = current_map.get(&key).map(|s| s.as_str()).unwrap_or("");
if val != current_url_val {
let window = web_sys::window().unwrap();
let location = window.location();
let pathname = location.pathname().unwrap_or_else(|_| "/".into());
let search = location.search().unwrap_or_default();
if let Ok(params) = web_sys::UrlSearchParams::new_with_str(&search) {
if val.is_empty() {
params.delete(&key);
} else {
params.set(&key, &val);
}
let new_search = params.to_string().as_string().unwrap_or_default();
let new_url = if new_search.is_empty() {
pathname
} else {
format!("{}?{}", pathname, new_search)
};
navigator.push(&new_url);
}
}
});
signal
}