use std::cell::{Cell, RefCell};
use std::rc::Rc;
use js_sys::Function;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::prelude::*;
use web_sys::{AddEventListenerOptions, Element, Event, EventTarget, KeyboardEvent, Node};
use crate::expr::{self, Expr, Spanned};
use crate::magics::with_current_event;
use crate::reactive::ScopeId;
use crate::scope::with_current_el;
pub fn install(
el: &Element,
scope_id: ScopeId,
proxy: &JsValue,
event: &str,
modifiers: &'static [&'static str],
ast: Rc<Spanned<Expr>>,
) {
let event = event.to_string();
let el = el.clone();
let proxy = proxy.clone();
let prevent = modifiers.contains(&"prevent");
let stop = modifiers.contains(&"stop");
let self_only = modifiers.contains(&"self");
let once = modifiers.contains(&"once");
let on_window = modifiers.contains(&"window");
let on_document = modifiers.contains(&"document");
let outside = modifiers.contains(&"outside");
let capture = modifiers.contains(&"capture");
let debounce_ms: Option<u32> = parse_debounce(modifiers);
type DebouncedInvoke = (Option<Function>, Option<Closure<dyn FnMut()>>);
let last_event: Rc<RefCell<Option<Event>>> = Rc::new(RefCell::new(None));
let (invoke_fn, debounce_closure): DebouncedInvoke = if debounce_ms.is_some() {
let ast = ast.clone();
let last_event = last_event.clone();
let el_for_debounce = el.clone();
let proxy_for_debounce = proxy.clone();
let c = Closure::wrap(Box::new(move || {
let ev = last_event.borrow().clone();
let ev_js: JsValue = match &ev {
Some(e) => {
let r: &JsValue = e.as_ref();
r.clone()
}
None => JsValue::UNDEFINED,
};
with_current_el(&el_for_debounce, || {
crate::scope::with_current_scope_id(scope_id, || {
with_current_event(&ev_js, || {
expr::evaluate(&ast, &proxy_for_debounce);
});
});
});
}) as Box<dyn FnMut()>);
let f: Function = c.as_ref().unchecked_ref::<Function>().clone();
(Some(f), Some(c))
} else {
(None, None)
};
let window = web_sys::window().expect("window");
let timer: Rc<Cell<Option<i32>>> = Rc::new(Cell::new(None));
let key_modifiers = collect_key_modifiers(modifiers);
let el_for_closure = el.clone();
let _debounce_closure_owned = debounce_closure;
let closure = Closure::wrap(Box::new({
let ast = ast.clone();
let proxy = proxy.clone();
let invoke_fn = invoke_fn.clone();
let window = window.clone();
let timer = timer.clone();
let last_event = last_event.clone();
let _debounce_closure_owned = _debounce_closure_owned;
move |ev: Event| {
if outside {
let host_node: &Node = el_for_closure.as_ref();
if !host_node.is_connected() {
return;
}
match ev.target() {
Some(t) => match t.dyn_into::<Node>() {
Ok(node) => {
if el_for_closure.contains(Some(&node)) {
return;
}
if let Some(sel) =
el_for_closure.get_attribute("data-pp-outside-exempt")
{
if let Ok(target_el) = node.clone().dyn_into::<Element>() {
if target_el.closest(&sel).ok().flatten().is_some() {
return;
}
}
}
}
Err(_) => return,
},
None => return,
}
}
if !key_modifiers.is_empty() && !key_filter_matches(&ev, &key_modifiers) {
return;
}
if !outside && prevent {
ev.prevent_default();
}
if stop {
ev.stop_propagation();
}
if self_only {
if outside {
return;
}
if let Some(target) = ev.target() {
if target != *el_for_closure.as_ref() {
return;
}
}
}
if let (Some(ms), Some(invoke_fn)) = (debounce_ms, invoke_fn.as_ref()) {
*last_event.borrow_mut() = Some(ev);
if let Some(prev) = timer.take() {
window.clear_timeout_with_handle(prev);
}
let handle = window
.set_timeout_with_callback_and_timeout_and_arguments_0(invoke_fn, ms as i32)
.unwrap_or(0);
timer.set(Some(handle));
} else {
let ev_js: JsValue = {
let r: &JsValue = ev.as_ref();
r.clone()
};
with_current_el(&el_for_closure, || {
crate::scope::with_current_scope_id(scope_id, || {
with_current_event(&ev_js, || {
expr::evaluate(&ast, &proxy);
});
});
});
}
}
}) as Box<dyn FnMut(Event)>);
let target: EventTarget = if outside || on_document {
web_sys::window()
.and_then(|w| w.document())
.expect("document")
.into()
} else if on_window {
web_sys::window().expect("window").into()
} else {
el.clone().into()
};
let opts = AddEventListenerOptions::new();
opts.set_once(once);
if outside || capture {
opts.set_capture(true);
}
crate::mount::track_listener_on_with_opts(&el, target, &event, &opts, closure);
}
#[doc(hidden)]
pub fn backfill_legacy_call(ast: Spanned<Expr>) -> Spanned<Expr> {
match ast.value {
Expr::Path(ref segs) if segs.len() == 1 => {
let name = segs[0].clone();
let span = ast.span.clone();
let event_arg = Spanned {
value: Expr::Path(vec!["$event".to_string()]),
span: span.clone(),
};
Spanned {
value: Expr::Call(name, vec![event_arg]),
span,
}
}
_ => ast,
}
}
fn parse_debounce(modifiers: &[&str]) -> Option<u32> {
for (i, m) in modifiers.iter().enumerate() {
if *m == "debounce" {
let ms = modifiers
.get(i + 1)
.and_then(|s| s.parse::<u32>().ok())
.unwrap_or(300);
return Some(ms);
}
}
None
}
fn collect_key_modifiers(modifiers: &[&'static str]) -> Vec<&'static str> {
modifiers
.iter()
.enumerate()
.filter_map(|(i, m)| {
if i > 0 && modifiers[i - 1] == "debounce" && m.parse::<u32>().is_ok() {
return None;
}
is_key_modifier(m).then_some(*m)
})
.collect()
}
fn is_key_modifier(m: &str) -> bool {
if matches!(
m,
"prevent"
| "stop"
| "self"
| "once"
| "window"
| "document"
| "debounce"
| "outside"
| "capture"
) {
return false;
}
matches!(m, "ctrl" | "shift" | "alt" | "meta")
|| named_key_for(m).is_some()
|| m.len() == 1 || is_word_key(m)
}
fn named_key_for(m: &str) -> Option<&'static str> {
Some(match m {
"escape" | "esc" => "escape",
"enter" => "enter",
"tab" => "tab",
"space" => " ",
"backspace" => "backspace",
"delete" | "del" => "delete",
"arrow-up" | "up" => "arrowup",
"arrow-down" | "down" => "arrowdown",
"arrow-left" | "left" => "arrowleft",
"arrow-right" | "right" => "arrowright",
"home" => "home",
"end" => "end",
"page-up" => "pageup",
"page-down" => "pagedown",
_ => return None,
})
}
fn is_word_key(m: &str) -> bool {
!m.is_empty() && m.chars().all(|c| c.is_ascii_alphanumeric())
}
fn key_filter_matches(ev: &Event, modifiers: &[&str]) -> bool {
let Ok(ke) = ev.clone().dyn_into::<KeyboardEvent>() else {
return false;
};
let key = ke.key().to_lowercase();
for m in modifiers {
match *m {
"ctrl" if !ke.ctrl_key() => return false,
"shift" if !ke.shift_key() => return false,
"alt" if !ke.alt_key() => return false,
"meta" if !ke.meta_key() => return false,
"ctrl" | "shift" | "alt" | "meta" => continue,
_ => {
let want = named_key_for(m).unwrap_or(m);
if key != want {
return false;
}
}
}
}
true
}
#[cfg(test)]
mod tests {
use super::{collect_key_modifiers, is_key_modifier};
#[test]
fn capture_is_not_a_key_filter() {
assert!(!is_key_modifier("capture"));
}
#[test]
fn debounce_delay_is_not_a_key_filter() {
assert_eq!(
collect_key_modifiers(&["debounce", "500"]),
Vec::<&'static str>::new()
);
}
#[test]
fn numeric_key_shortcuts_still_filter_when_not_debounce_delay() {
assert_eq!(collect_key_modifiers(&["1"]), vec!["1"]);
}
}