#![allow(clippy::type_complexity)]
use std::fmt::Debug;
use dioxus_core::{Attribute, Element, EventHandler, VNode};
use dioxus_core_macro::{rsx, Props};
use dioxus_html::{
self as dioxus_elements, ModifiersInteraction, MountedEvent, MouseEvent, PointerInteraction,
};
use tracing::error;
use crate::navigation::NavigationTarget;
use crate::utils::use_router_internal::use_router_internal;
#[derive(Props, Clone, PartialEq)]
pub struct LinkProps {
pub class: Option<String>,
pub active_class: Option<String>,
pub children: Element,
#[props(default)]
pub new_tab: bool,
pub onclick: Option<EventHandler<MouseEvent>>,
pub onmounted: Option<EventHandler<MountedEvent>>,
#[props(default)]
pub onclick_only: bool,
pub rel: Option<String>,
#[props(into)]
pub to: NavigationTarget,
#[props(extends = GlobalAttributes)]
attributes: Vec<Attribute>,
}
impl Debug for LinkProps {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LinkProps")
.field("active_class", &self.active_class)
.field("children", &self.children)
.field("attributes", &self.attributes)
.field("new_tab", &self.new_tab)
.field("onclick", &self.onclick.as_ref().map(|_| "onclick is set"))
.field("onclick_only", &self.onclick_only)
.field("rel", &self.rel)
.finish()
}
}
#[doc(alias = "<a>")]
#[allow(non_snake_case)]
pub fn Link(props: LinkProps) -> Element {
let LinkProps {
active_class,
children,
attributes,
new_tab,
onclick,
onclick_only,
rel,
to,
class,
..
} = props;
let router = match use_router_internal() {
Some(r) => r,
#[allow(unreachable_code)]
None => {
let msg = "`Link` must have access to a parent router";
error!("{msg}, will be inactive");
#[cfg(debug_assertions)]
panic!("{}", msg);
return VNode::empty();
}
};
let current_url = router.full_route_string();
let href = match &to {
NavigationTarget::Internal(url) => url.clone(),
NavigationTarget::External(route) => route.clone(),
};
let full_href = match &to {
NavigationTarget::Internal(url) => router.prefix().unwrap_or_default() + url,
NavigationTarget::External(route) => route.clone(),
};
let mut class_ = String::new();
if let Some(c) = class {
class_.push_str(&c);
}
if let Some(c) = active_class {
if href == current_url {
if !class_.is_empty() {
class_.push(' ');
}
class_.push_str(&c);
}
}
let class = if class_.is_empty() {
None
} else {
Some(class_)
};
let aria_current = (href == current_url).then_some("page");
let tag_target = new_tab.then_some("_blank");
let is_external = matches!(to, NavigationTarget::External(_));
let is_router_nav = !is_external && !new_tab;
let rel = rel.or_else(|| is_external.then_some("noopener noreferrer".to_string()));
let do_default = onclick.is_none() || !onclick_only;
let action = move |event: MouseEvent| {
if !event.modifiers().is_empty() {
return;
}
if event.trigger_button() != Some(dioxus_elements::input_data::MouseButton::Primary) {
return;
}
if new_tab {
return;
}
if do_default && is_external {
return;
}
event.prevent_default();
if do_default && is_router_nav {
router.push_any(to.clone());
}
if let Some(handler) = onclick {
handler.call(event);
}
};
let onmounted = move |event| {
if let Some(handler) = props.onmounted {
handler.call(event);
}
};
let liveview_prevent_default = {
router.include_prevent_default().then_some(
"if (event.button === 0 && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey) { event.preventDefault() }"
)
};
rsx! {
a {
onclick: action,
"onclick": liveview_prevent_default,
href: full_href,
onmounted: onmounted,
class,
rel,
target: tag_target,
aria_current,
..attributes,
{children}
}
}
}