use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{CustomEvent, EventTarget};
use crate::emit::Emit;
use crate::reactive::ScopeId;
use crate::scope::current_scope_id;
pub trait DomEventName: 'static {
type Event: JsCast + 'static;
const NAME: &'static str;
}
type Listener = (&'static str, Closure<dyn FnMut(JsValue)>);
pub struct ListenerHandle {
target: EventTarget,
listeners: Vec<Listener>,
}
impl ListenerHandle {
pub fn cancel(mut self) {
self.cancel_in_place();
}
fn cancel_in_place(&mut self) {
for (name, closure) in self.listeners.drain(..) {
let _ = self
.target
.remove_event_listener_with_callback(name, closure.as_ref().unchecked_ref());
}
}
}
impl Drop for ListenerHandle {
fn drop(&mut self) {
self.cancel_in_place();
}
}
type UnmountCb = Box<dyn FnOnce()>;
type UnmountCbs = HashMap<ScopeId, Vec<UnmountCb>>;
thread_local! {
static UNMOUNT_CBS: RefCell<UnmountCbs> = RefCell::new(HashMap::new());
}
pub fn on_scope_unmount(f: impl FnOnce() + 'static) {
let scope =
current_scope_id().expect("on_scope_unmount called outside a handler / lifecycle context");
on_scope_unmount_for(scope, f);
}
pub fn on_scope_unmount_for(scope: ScopeId, f: impl FnOnce() + 'static) {
if crate::scope::Scope::find(scope).is_none() {
f();
return;
}
UNMOUNT_CBS.with(|m| {
m.borrow_mut().entry(scope).or_default().push(Box::new(f));
});
}
pub fn clear_scope(scope: ScopeId) {
let cbs = UNMOUNT_CBS.with(|m| m.borrow_mut().remove(&scope).unwrap_or_default());
for cb in cbs {
cb();
}
}
#[cfg(test)]
mod tests {
use std::cell::Cell;
use std::rc::Rc;
use super::*;
#[test]
fn on_scope_unmount_for_runs_immediately_when_scope_is_gone() {
let ran = Rc::new(Cell::new(false));
let ran_for_cleanup = ran.clone();
on_scope_unmount_for(ScopeId(u64::MAX), move || ran_for_cleanup.set(true));
assert!(ran.get());
}
}
pub fn on<N, T, F>(target: &T, _name: N, handler: F) -> ListenerHandle
where
N: DomEventName,
T: AsRef<EventTarget>,
F: FnMut(N::Event) + 'static,
{
on_named::<N::Event, _, _>(target, N::NAME, handler)
}
pub fn on_scoped<N, T, F>(target: &T, _name: N, handler: F)
where
N: DomEventName,
T: AsRef<EventTarget>,
F: FnMut(N::Event) + 'static,
{
on_named_scoped::<N::Event, _, _>(target, N::NAME, handler);
}
pub fn on_named<E, T, F>(target: &T, event: &'static str, mut handler: F) -> ListenerHandle
where
E: JsCast + 'static,
T: AsRef<EventTarget>,
F: FnMut(E) + 'static,
{
let closure: Closure<dyn FnMut(JsValue)> = Closure::wrap(Box::new(move |raw: JsValue| {
if let Ok(ev) = raw.dyn_into::<E>() {
handler(ev);
}
}));
let target = target.as_ref();
let _ = target.add_event_listener_with_callback(event, closure.as_ref().unchecked_ref());
ListenerHandle {
target: target.clone(),
listeners: vec![(event, closure)],
}
}
pub fn on_named_scoped<E, T, F>(target: &T, event: &'static str, handler: F)
where
E: JsCast + 'static,
T: AsRef<EventTarget>,
F: FnMut(E) + 'static,
{
let handle = on_named::<E, T, F>(target, event, handler);
on_scope_unmount(move || drop(handle));
}
pub fn on_emit<E, T, F>(target: &T, handler: F) -> ListenerHandle
where
E: Emit + 'static,
T: AsRef<EventTarget>,
F: FnMut(E) + 'static,
{
let target = target.as_ref();
let handler = Rc::new(RefCell::new(handler));
let mut listeners = Vec::with_capacity(E::event_names().len());
for &name in E::event_names() {
let handler = handler.clone();
let closure: Closure<dyn FnMut(JsValue)> = Closure::wrap(Box::new(move |raw: JsValue| {
let detail = raw
.dyn_ref::<CustomEvent>()
.map(|ce| ce.detail())
.unwrap_or(JsValue::UNDEFINED);
if let Some(typed) = E::from_event(name, detail) {
(handler.borrow_mut())(typed);
}
}));
let _ = target.add_event_listener_with_callback(name, closure.as_ref().unchecked_ref());
listeners.push((name, closure));
}
ListenerHandle {
target: target.clone(),
listeners,
}
}
pub fn on_emit_scoped<E, T, F>(target: &T, handler: F)
where
E: Emit + 'static,
T: AsRef<EventTarget>,
F: FnMut(E) + 'static,
{
let handle = on_emit::<E, T, F>(target, handler);
on_scope_unmount(move || drop(handle));
}
pub mod ev {
use super::DomEventName;
use web_sys::{
AnimationEvent, ClipboardEvent, CompositionEvent, DragEvent, ErrorEvent, Event, FocusEvent,
HashChangeEvent, InputEvent, KeyboardEvent, MessageEvent, MouseEvent, PageTransitionEvent,
PointerEvent, PopStateEvent, ProgressEvent, StorageEvent, SubmitEvent, TouchEvent,
TransitionEvent, UiEvent, WheelEvent,
};
macro_rules! event_marker {
($($name:ident => ($lit:literal, $ty:ty)),* $(,)?) => {$(
#[doc = concat!("`", $lit, "` — payload `", stringify!($ty), "`.")]
#[allow(non_camel_case_types)]
pub struct $name;
impl DomEventName for $name {
type Event = $ty;
const NAME: &'static str = $lit;
}
)*};
}
event_marker! {
click => ("click", MouseEvent),
dblclick => ("dblclick", MouseEvent),
contextmenu => ("contextmenu", MouseEvent),
auxclick => ("auxclick", MouseEvent),
mousedown => ("mousedown", MouseEvent),
mouseup => ("mouseup", MouseEvent),
mousemove => ("mousemove", MouseEvent),
mouseenter => ("mouseenter", MouseEvent),
mouseleave => ("mouseleave", MouseEvent),
mouseover => ("mouseover", MouseEvent),
mouseout => ("mouseout", MouseEvent),
wheel => ("wheel", WheelEvent),
pointerdown => ("pointerdown", PointerEvent),
pointerup => ("pointerup", PointerEvent),
pointermove => ("pointermove", PointerEvent),
pointerenter => ("pointerenter", PointerEvent),
pointerleave => ("pointerleave", PointerEvent),
pointerover => ("pointerover", PointerEvent),
pointerout => ("pointerout", PointerEvent),
pointercancel => ("pointercancel", PointerEvent),
gotpointercapture => ("gotpointercapture", PointerEvent),
lostpointercapture => ("lostpointercapture", PointerEvent),
touchstart => ("touchstart", TouchEvent),
touchend => ("touchend", TouchEvent),
touchmove => ("touchmove", TouchEvent),
touchcancel => ("touchcancel", TouchEvent),
drag => ("drag", DragEvent),
dragstart => ("dragstart", DragEvent),
dragend => ("dragend", DragEvent),
dragenter => ("dragenter", DragEvent),
dragleave => ("dragleave", DragEvent),
dragover => ("dragover", DragEvent),
drop => ("drop", DragEvent),
keydown => ("keydown", KeyboardEvent),
keyup => ("keyup", KeyboardEvent),
keypress => ("keypress", KeyboardEvent),
compositionstart => ("compositionstart", CompositionEvent),
compositionupdate => ("compositionupdate", CompositionEvent),
compositionend => ("compositionend", CompositionEvent),
focus => ("focus", FocusEvent),
blur => ("blur", FocusEvent),
focusin => ("focusin", FocusEvent),
focusout => ("focusout", FocusEvent),
input => ("input", InputEvent),
change => ("change", Event),
submit => ("submit", SubmitEvent),
reset => ("reset", Event),
invalid => ("invalid", Event),
search => ("search", Event),
select => ("select", Event),
copy => ("copy", ClipboardEvent),
cut => ("cut", ClipboardEvent),
paste => ("paste", ClipboardEvent),
animationstart => ("animationstart", AnimationEvent),
animationend => ("animationend", AnimationEvent),
animationiteration => ("animationiteration", AnimationEvent),
animationcancel => ("animationcancel", AnimationEvent),
transitionstart => ("transitionstart", TransitionEvent),
transitionrun => ("transitionrun", TransitionEvent),
transitionend => ("transitionend", TransitionEvent),
transitioncancel => ("transitioncancel", TransitionEvent),
load => ("load", Event),
unload => ("unload", Event),
beforeunload => ("beforeunload", Event),
scroll => ("scroll", Event),
scrollend => ("scrollend", Event),
resize => ("resize", UiEvent),
visibilitychange => ("visibilitychange", Event),
fullscreenchange => ("fullscreenchange", Event),
fullscreenerror => ("fullscreenerror", Event),
error => ("error", ErrorEvent),
progress => ("progress", ProgressEvent),
loadstart => ("loadstart", ProgressEvent),
loadend => ("loadend", ProgressEvent),
hashchange => ("hashchange", HashChangeEvent),
popstate => ("popstate", PopStateEvent),
pageshow => ("pageshow", PageTransitionEvent),
pagehide => ("pagehide", PageTransitionEvent),
storage => ("storage", StorageEvent),
online => ("online", Event),
offline => ("offline", Event),
message => ("message", MessageEvent),
toggle => ("toggle", Event),
cancel => ("cancel", Event),
close => ("close", Event),
slotchange => ("slotchange", Event),
}
}