use crate::{components::RouterContext, hooks::use_resolved_path};
use leptos::{children::Children, oco::Oco, prelude::*};
use reactive_graph::{computed::ArcMemo, owner::use_context};
use std::{borrow::Cow, rc::Rc};
pub trait ToHref {
fn to_href(&self) -> Box<dyn Fn() -> String + '_>;
}
impl ToHref for &str {
fn to_href(&self) -> Box<dyn Fn() -> String> {
let s = self.to_string();
Box::new(move || s.clone())
}
}
impl ToHref for String {
fn to_href(&self) -> Box<dyn Fn() -> String> {
let s = self.clone();
Box::new(move || s.clone())
}
}
impl ToHref for Cow<'_, str> {
fn to_href(&self) -> Box<dyn Fn() -> String + '_> {
let s = self.to_string();
Box::new(move || s.clone())
}
}
impl ToHref for Oco<'_, str> {
fn to_href(&self) -> Box<dyn Fn() -> String + '_> {
let s = self.to_string();
Box::new(move || s.clone())
}
}
impl ToHref for Rc<str> {
fn to_href(&self) -> Box<dyn Fn() -> String + '_> {
let s = self.to_string();
Box::new(move || s.clone())
}
}
impl<F> ToHref for F
where
F: Fn() -> String + 'static,
{
fn to_href(&self) -> Box<dyn Fn() -> String + '_> {
Box::new(self)
}
}
#[component]
pub fn A<H>(
href: H,
#[prop(optional, into)]
target: Option<Oco<'static, str>>,
#[prop(optional)]
exact: bool,
#[prop(optional)]
strict_trailing_slash: bool,
#[prop(default = true)]
scroll: bool,
children: Children,
) -> impl IntoView
where
H: ToHref + Send + Sync + 'static,
{
fn inner(
href: ArcMemo<Option<String>>,
target: Option<Oco<'static, str>>,
exact: bool,
children: Children,
strict_trailing_slash: bool,
scroll: bool,
) -> impl IntoView {
let RouterContext { current_url, .. } =
use_context().expect("tried to use <A/> outside a <Router/>.");
let is_active = {
let href = href.clone();
move || {
href.read().as_deref().is_some_and(|to| {
let path = to.split(['?', '#']).next().unwrap_or_default();
current_url.with(|loc| {
let loc = loc.path();
if exact {
loc == path
} else {
is_active_for(path, loc, strict_trailing_slash)
}
})
})
}
};
view! {
<a
href=move || href.get().unwrap_or_default()
target=target
aria-current=move || if is_active() { Some("page") } else { None }
data-noscroll=!scroll
>
{children()}
</a>
}
}
let href = use_resolved_path(move || href.to_href()());
inner(href, target, exact, children, strict_trailing_slash, scroll)
}
fn is_active_for(
href: &str,
location: &str,
strict_trailing_slash: bool,
) -> bool {
let mut href_f = href.split('/');
std::iter::zip(location.split('/'), href_f.by_ref())
.enumerate()
.all(|(c, (loc_p, href_p))| {
loc_p == href_p || href_p.is_empty() && c > 1
})
&& match href_f.next() {
None => true,
Some("") => !strict_trailing_slash,
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::is_active_for;
#[test]
fn is_active_for_matched() {
[false, true].into_iter().for_each(|f| {
assert!(is_active_for("/", "/", f));
assert!(is_active_for("/item", "/item", f));
assert!(is_active_for("/item", "/item/", f));
assert!(is_active_for("/item/", "/item/", f));
assert!(is_active_for("/item", "/item/one", f));
assert!(is_active_for("/item", "/item/one/", f));
assert!(is_active_for("/item/", "/item/one", f));
assert!(is_active_for("/item/", "/item/one/", f));
assert!(is_active_for("/item/1", "/item/1", f));
assert!(is_active_for("/item/1", "/item/1/", f));
assert!(is_active_for("/item/1/", "/item/1/", f));
assert!(is_active_for("/item/1", "/item/1/two", f));
assert!(is_active_for("/item/1", "/item/1/three/four/", f));
assert!(is_active_for("/item/1/", "/item/1/three/four", f));
assert!(is_active_for("/item/1/", "/item/1/two/", f));
assert!(is_active_for("/item/1/two/three", "/item/1/two/three", f));
assert!(is_active_for(
"/item/1/two/three/444",
"/item/1/two/three/444/",
f
));
assert!(is_active_for(
"/item/1/two/three/444/FIVE/final/",
"/item/1/two/three/444/FIVE/final/",
f
));
assert!(is_active_for(
"/item/1/two/three",
"/item/1/two/three/three/two/1/item",
f
));
assert!(is_active_for(
"/item/1/two/three/444",
"/item/1/two/three/444/just_one_more/",
f
));
assert!(is_active_for(
"/item/1/two/three/444/final/",
"/item/1/two/three/444/final/just/kidding",
f
));
assert!(is_active_for(
"/item/////",
"/item/one/two/three/four/",
f
));
assert!(is_active_for(
"/item/////",
"/item/1/two/three/three/two/1/item",
f
));
assert!(is_active_for(
"/item/1///three//1",
"/item/1/two/three/three/two/1/item",
f
));
assert!(is_active_for(
"/item//foo",
"/item/this_is_not_empty/foo/bar/baz",
f
));
});
assert!(is_active_for("/item/", "/item", false));
assert!(is_active_for("/item/1/", "/item/1", false));
assert!(is_active_for(
"/item/1/two/three/444/FIVE/",
"/item/1/two/three/444/FIVE",
false
));
}
#[test]
fn is_active_for_mismatched() {
[false, true].into_iter().for_each(|f| {
assert!(!is_active_for("/", "/item", f));
assert!(!is_active_for("/", "/somewhere/", f));
assert!(!is_active_for("/", "/else/where", f));
assert!(!is_active_for("/", "/no/where/", f));
assert!(!is_active_for("/somewhere", "/", f));
assert!(!is_active_for("/somewhere/", "/", f));
assert!(!is_active_for("/else/where", "/", f));
assert!(!is_active_for("/no/where/", "/", f));
assert!(!is_active_for("/level", "/item", f));
assert!(!is_active_for("/level", "/item/", f));
assert!(!is_active_for("/level/", "/item", f));
assert!(!is_active_for("/level/", "/item/", f));
assert!(!is_active_for("/item/one", "/item", f));
assert!(!is_active_for("/item/one/", "/item", f));
assert!(!is_active_for("/item/one", "/item/", f));
assert!(!is_active_for("/item/one/", "/item/", f));
assert!(!is_active_for("/item/1/two", "/item/1", f));
assert!(!is_active_for("/item/1/three/four/", "/item/1", f));
assert!(!is_active_for("/item/1/three/four", "/item/", f));
assert!(!is_active_for("/item/1/two/", "/item/", f));
assert!(!is_active_for(
"/item/1/two/three/three/two/1/item",
"/item/1/two/three",
f
));
assert!(!is_active_for(
"/item/1/two/three/444/just_one_more/",
"/item/1/two/three/444",
f
));
assert!(!is_active_for(
"/item/1/two/three/444/final/just/kidding",
"/item/1/two/three/444/final/",
f
));
assert!(!is_active_for(
"//////",
"/item/1/two/three/three/two/1/item",
f
));
assert!(!is_active_for(
"/item/1/two/three/three/two/1/item",
"//////",
f
));
assert!(!is_active_for(
"/item/one/two/three/four/",
"/item/////",
f
));
assert!(!is_active_for(
"/item/one/two/three/four/",
"/item////four/",
f
));
});
assert!(!is_active_for("/item/", "/item", true));
assert!(!is_active_for("/item/1/", "/item/1", true));
assert!(!is_active_for(
"/item/1/two/three/444/FIVE/",
"/item/1/two/three/444/FIVE",
true
));
}
}