#![deny(missing_docs, missing_debug_implementations)]
#![allow(clippy::type_complexity)]
use std::borrow::Cow;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::{JsCast, UnwrapThrowExt};
use web_sys::{AddEventListenerOptions, Event, EventTarget};
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub enum EventListenerPhase {
#[default]
#[allow(missing_docs)]
Bubble,
#[allow(missing_docs)]
Capture,
}
impl EventListenerPhase {
#[inline]
fn is_capture(&self) -> bool {
match self {
EventListenerPhase::Bubble => false,
EventListenerPhase::Capture => true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EventListenerOptions {
pub phase: EventListenerPhase,
pub passive: bool,
}
impl EventListenerOptions {
#[inline]
pub fn run_in_capture_phase() -> Self {
Self {
phase: EventListenerPhase::Capture,
..Self::default()
}
}
#[inline]
pub fn enable_prevent_default() -> Self {
Self {
passive: false,
..Self::default()
}
}
#[inline]
fn as_js(&self, once: bool) -> AddEventListenerOptions {
let options = AddEventListenerOptions::new();
options.set_capture(self.phase.is_capture());
options.set_once(once);
options.set_passive(self.passive);
options
}
}
impl Default for EventListenerOptions {
#[inline]
fn default() -> Self {
Self {
phase: Default::default(),
passive: true,
}
}
}
thread_local! {
static NEW_OPTIONS: AddEventListenerOptions = EventListenerOptions::default().as_js(false);
static ONCE_OPTIONS: AddEventListenerOptions = EventListenerOptions::default().as_js(true);
}
#[derive(Debug)]
#[must_use = "event listener will never be called after being dropped"]
pub struct EventListener {
target: EventTarget,
event_type: Cow<'static, str>,
callback: Option<Closure<dyn FnMut(&Event)>>,
phase: EventListenerPhase,
}
impl EventListener {
#[inline]
fn raw_new(
target: &EventTarget,
event_type: Cow<'static, str>,
callback: Closure<dyn FnMut(&Event)>,
options: &AddEventListenerOptions,
phase: EventListenerPhase,
) -> Self {
target
.add_event_listener_with_callback_and_add_event_listener_options(
&event_type,
callback.as_ref().unchecked_ref(),
options,
)
.unwrap_throw();
Self {
target: target.clone(),
event_type,
callback: Some(callback),
phase,
}
}
#[inline]
pub fn new<S, F>(target: &EventTarget, event_type: S, callback: F) -> Self
where
S: Into<Cow<'static, str>>,
F: FnMut(&Event) + 'static,
{
let callback = Closure::wrap(Box::new(callback) as Box<dyn FnMut(&Event)>);
NEW_OPTIONS.with(move |options| {
Self::raw_new(
target,
event_type.into(),
callback,
options,
EventListenerPhase::Bubble,
)
})
}
#[inline]
pub fn once<S, F>(target: &EventTarget, event_type: S, callback: F) -> Self
where
S: Into<Cow<'static, str>>,
F: FnOnce(&Event) + 'static,
{
let callback = Closure::once(callback);
ONCE_OPTIONS.with(move |options| {
Self::raw_new(
target,
event_type.into(),
callback,
options,
EventListenerPhase::Bubble,
)
})
}
#[inline]
pub fn new_with_options<S, F>(
target: &EventTarget,
event_type: S,
options: EventListenerOptions,
callback: F,
) -> Self
where
S: Into<Cow<'static, str>>,
F: FnMut(&Event) + 'static,
{
let callback = Closure::wrap(Box::new(callback) as Box<dyn FnMut(&Event)>);
Self::raw_new(
target,
event_type.into(),
callback,
&options.as_js(false),
options.phase,
)
}
#[inline]
pub fn once_with_options<S, F>(
target: &EventTarget,
event_type: S,
options: EventListenerOptions,
callback: F,
) -> Self
where
S: Into<Cow<'static, str>>,
F: FnOnce(&Event) + 'static,
{
let callback = Closure::once(callback);
Self::raw_new(
target,
event_type.into(),
callback,
&options.as_js(true),
options.phase,
)
}
#[inline]
pub fn forget(mut self) {
self.callback.take().unwrap_throw().forget()
}
#[inline]
pub fn target(&self) -> &EventTarget {
&self.target
}
#[inline]
pub fn event_type(&self) -> &str {
&self.event_type
}
#[inline]
pub fn callback(&self) -> &Closure<dyn FnMut(&Event)> {
self.callback.as_ref().unwrap_throw()
}
#[inline]
pub fn phase(&self) -> EventListenerPhase {
self.phase
}
}
impl Drop for EventListener {
#[inline]
fn drop(&mut self) {
if let Some(callback) = &self.callback {
self.target
.remove_event_listener_with_callback_and_bool(
self.event_type(),
callback.as_ref().unchecked_ref(),
self.phase.is_capture(),
)
.unwrap_throw();
}
}
}