use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue;
use super::opt_out::LinkClick;
pub(crate) fn window() -> Result<web_sys::Window, JsValue> {
web_sys::window().ok_or_else(|| JsValue::from_str("no window"))
}
pub(crate) fn document() -> Result<web_sys::Document, JsValue> {
window()?
.document()
.ok_or_else(|| JsValue::from_str("no document"))
}
pub(crate) fn document_origin() -> Result<String, JsValue> {
window()?.location().origin()
}
pub(crate) fn closest_anchor(target: &web_sys::EventTarget) -> Option<web_sys::HtmlAnchorElement> {
let element = target.dyn_ref::<web_sys::Element>()?;
let matched = element.closest("a").ok().flatten()?;
matched.dyn_into::<web_sys::HtmlAnchorElement>().ok()
}
pub(crate) fn link_click_from(
event: &web_sys::MouseEvent,
anchor: &web_sys::HtmlAnchorElement,
document_origin: String,
) -> LinkClick {
let has_modifier_key =
event.meta_key() || event.ctrl_key() || event.shift_key() || event.alt_key();
let target_attribute = anchor.target();
let has_foreign_target = !target_attribute.is_empty() && target_attribute != "_self";
let has_download_attribute = anchor.has_attribute("download");
let has_no_morph_attribute = anchor.has_attribute("data-no-morph");
let rel_tokens = anchor
.rel()
.split_whitespace()
.map(|token| token.to_ascii_lowercase())
.collect::<Vec<_>>();
let raw_href = anchor.get_attribute("href");
let has_no_href = raw_href.as_deref().map(str::is_empty).unwrap_or(true);
let href_scheme = scheme_of(anchor);
let href_origin = origin_of(anchor);
let is_pure_fragment = is_pure_fragment(anchor, raw_href.as_deref());
LinkClick {
button: event.button(),
has_modifier_key,
has_foreign_target,
has_download_attribute,
has_no_morph_attribute,
rel_tokens,
href_origin,
document_origin,
href_scheme,
is_pure_fragment,
has_no_href,
}
}
pub(crate) fn link_click_for_prefetch(
anchor: &web_sys::HtmlAnchorElement,
document_origin: String,
) -> LinkClick {
let target_attribute = anchor.target();
let has_foreign_target = !target_attribute.is_empty() && target_attribute != "_self";
let rel_tokens = anchor
.rel()
.split_whitespace()
.map(|token| token.to_ascii_lowercase())
.collect::<Vec<_>>();
let raw_href = anchor.get_attribute("href");
let has_no_href = raw_href.as_deref().map(str::is_empty).unwrap_or(true);
LinkClick {
button: 0,
has_modifier_key: false,
has_foreign_target,
has_download_attribute: anchor.has_attribute("download"),
has_no_morph_attribute: anchor.has_attribute("data-no-morph"),
rel_tokens,
href_origin: origin_of(anchor),
document_origin,
href_scheme: scheme_of(anchor),
is_pure_fragment: is_pure_fragment(anchor, raw_href.as_deref()),
has_no_href,
}
}
fn scheme_of(anchor: &web_sys::HtmlAnchorElement) -> Option<String> {
let protocol = anchor.protocol();
let trimmed = protocol.strip_suffix(':').unwrap_or(&protocol);
if trimmed.is_empty() {
return None;
}
Some(trimmed.to_ascii_lowercase())
}
fn origin_of(anchor: &web_sys::HtmlAnchorElement) -> Option<String> {
let origin = anchor.origin();
if origin.is_empty() || origin == "null" {
return None;
}
Some(origin)
}
fn is_pure_fragment(anchor: &web_sys::HtmlAnchorElement, raw_href: Option<&str>) -> bool {
if let Some(raw) = raw_href {
if raw.starts_with('#') {
return true;
}
}
let location = match window().ok().map(|window| window.location()) {
Some(location) => location,
None => return false,
};
if anchor.hash().is_empty() {
return false;
}
let same_path = location.pathname().map(|path| path == anchor.pathname()).unwrap_or(false);
let same_search = location
.search()
.map(|search| search == anchor.search())
.unwrap_or(false);
same_path && same_search
}