gloo_events/
lib.rs

1/*!
2Using event listeners with [`web-sys`](http://crates.io/crates/web-sys) is hard! This crate
3provides an [`EventListener`] type which makes it easy!
4
5See the documentation for [`EventListener`] for more information.
6
7[`EventListener`]: struct.EventListener.html
8*/
9#![deny(missing_docs, missing_debug_implementations)]
10// Clippy doesn't like the callback types passed to raw web-sys
11#![allow(clippy::type_complexity)]
12
13use std::borrow::Cow;
14use wasm_bindgen::closure::Closure;
15use wasm_bindgen::{JsCast, UnwrapThrowExt};
16use web_sys::{AddEventListenerOptions, Event, EventTarget};
17
18/// Specifies whether the event listener is run during the capture or bubble phase.
19///
20/// The official specification has [a good explanation](http://www.w3.org/TR/DOM-Level-3-Events/#event-flow)
21/// of capturing vs bubbling.
22///
23/// # Default
24///
25/// ```rust
26/// # use gloo_events::EventListenerPhase;
27/// #
28/// EventListenerPhase::Bubble
29/// # ;
30/// ```
31#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
32pub enum EventListenerPhase {
33    #[default]
34    #[allow(missing_docs)]
35    Bubble,
36
37    #[allow(missing_docs)]
38    Capture,
39}
40
41impl EventListenerPhase {
42    #[inline]
43    fn is_capture(&self) -> bool {
44        match self {
45            EventListenerPhase::Bubble => false,
46            EventListenerPhase::Capture => true,
47        }
48    }
49}
50
51/// Specifies options for [`EventListener::new_with_options`](struct.EventListener.html#method.new_with_options) and
52/// [`EventListener::once_with_options`](struct.EventListener.html#method.once_with_options).
53///
54/// # Default
55///
56/// ```rust
57/// # use gloo_events::{EventListenerOptions, EventListenerPhase};
58/// #
59/// EventListenerOptions {
60///     phase: EventListenerPhase::Bubble,
61///     passive: true,
62/// }
63/// # ;
64/// ```
65///
66/// # Examples
67///
68/// Sets `phase` to `EventListenerPhase::Capture`, using the default for the rest:
69///
70/// ```rust
71/// # use gloo_events::EventListenerOptions;
72/// #
73/// let options = EventListenerOptions::run_in_capture_phase();
74/// ```
75///
76/// Sets `passive` to `false`, using the default for the rest:
77///
78/// ```rust
79/// # use gloo_events::EventListenerOptions;
80/// #
81/// let options = EventListenerOptions::enable_prevent_default();
82/// ```
83///
84/// Specifies all options:
85///
86/// ```rust
87/// # use gloo_events::{EventListenerOptions, EventListenerPhase};
88/// #
89/// let options = EventListenerOptions {
90///     phase: EventListenerPhase::Capture,
91///     passive: false,
92/// };
93/// ```
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub struct EventListenerOptions {
96    /// The phase that the event listener should be run in.
97    pub phase: EventListenerPhase,
98
99    /// If this is `true` then performance is improved, but it is not possible to use
100    /// [`event.prevent_default()`](http://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Event.html#method.prevent_default).
101    ///
102    /// If this is `false` then performance might be reduced, but now it is possible to use
103    /// [`event.prevent_default()`](http://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Event.html#method.prevent_default).
104    ///
105    /// You can read more about the performance costs
106    /// [here](http://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Improving_scrolling_performance_with_passive_listeners).
107    pub passive: bool,
108}
109
110impl EventListenerOptions {
111    /// Returns an `EventListenerOptions` with `phase` set to `EventListenerPhase::Capture`.
112    ///
113    /// This is the same as:
114    ///
115    /// ```rust
116    /// # use gloo_events::{EventListenerOptions, EventListenerPhase};
117    /// #
118    /// EventListenerOptions {
119    ///     phase: EventListenerPhase::Capture,
120    ///     ..Default::default()
121    /// }
122    /// # ;
123    /// ```
124    #[inline]
125    pub fn run_in_capture_phase() -> Self {
126        Self {
127            phase: EventListenerPhase::Capture,
128            ..Self::default()
129        }
130    }
131
132    /// Returns an `EventListenerOptions` with `passive` set to `false`.
133    ///
134    /// This is the same as:
135    ///
136    /// ```rust
137    /// # use gloo_events::EventListenerOptions;
138    /// #
139    /// EventListenerOptions {
140    ///     passive: false,
141    ///     ..Default::default()
142    /// }
143    /// # ;
144    /// ```
145    #[inline]
146    pub fn enable_prevent_default() -> Self {
147        Self {
148            passive: false,
149            ..Self::default()
150        }
151    }
152
153    #[inline]
154    fn as_js(&self, once: bool) -> AddEventListenerOptions {
155        let mut options = AddEventListenerOptions::new();
156
157        options.capture(self.phase.is_capture());
158        options.once(once);
159        options.passive(self.passive);
160
161        options
162    }
163}
164
165impl Default for EventListenerOptions {
166    #[inline]
167    fn default() -> Self {
168        Self {
169            phase: Default::default(),
170            passive: true,
171        }
172    }
173}
174
175// This defaults passive to true to avoid performance issues in browsers:
176// http://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Improving_scrolling_performance_with_passive_listeners
177thread_local! {
178    static NEW_OPTIONS: AddEventListenerOptions = EventListenerOptions::default().as_js(false);
179    static ONCE_OPTIONS: AddEventListenerOptions = EventListenerOptions::default().as_js(true);
180}
181
182/// RAII type which is used to manage DOM event listeners.
183///
184/// When the `EventListener` is dropped, it will automatically deregister the event listener and
185/// clean up the closure's memory.
186///
187/// Normally the `EventListener` is stored inside of another struct, like this:
188///
189/// ```rust
190/// # use gloo_events::EventListener;
191/// # use wasm_bindgen::UnwrapThrowExt;
192/// use std::pin::Pin;
193/// use std::task::{Context, Poll};
194/// use futures::stream::Stream;
195/// use futures::channel::mpsc;
196/// use web_sys::EventTarget;
197///
198/// pub struct OnClick {
199///     receiver: mpsc::UnboundedReceiver<()>,
200///     // Automatically removed from the DOM on drop!
201///     listener: EventListener,
202/// }
203///
204/// impl OnClick {
205///     pub fn new(target: &EventTarget) -> Self {
206///         let (sender, receiver) = mpsc::unbounded();
207///
208///         // Attach an event listener
209///         let listener = EventListener::new(&target, "click", move |_event| {
210///             sender.unbounded_send(()).unwrap_throw();
211///         });
212///
213///         Self {
214///             receiver,
215///             listener,
216///         }
217///     }
218/// }
219///
220/// impl Stream for OnClick {
221///     type Item = ();
222///
223///     fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
224///         Pin::new(&mut self.receiver).poll_next(cx)
225///     }
226/// }
227/// ```
228#[derive(Debug)]
229#[must_use = "event listener will never be called after being dropped"]
230pub struct EventListener {
231    target: EventTarget,
232    event_type: Cow<'static, str>,
233    callback: Option<Closure<dyn FnMut(&Event)>>,
234    phase: EventListenerPhase,
235}
236
237impl EventListener {
238    #[inline]
239    fn raw_new(
240        target: &EventTarget,
241        event_type: Cow<'static, str>,
242        callback: Closure<dyn FnMut(&Event)>,
243        options: &AddEventListenerOptions,
244        phase: EventListenerPhase,
245    ) -> Self {
246        target
247            .add_event_listener_with_callback_and_add_event_listener_options(
248                &event_type,
249                callback.as_ref().unchecked_ref(),
250                options,
251            )
252            .unwrap_throw();
253
254        Self {
255            target: target.clone(),
256            event_type,
257            callback: Some(callback),
258            phase,
259        }
260    }
261
262    /// Registers an event listener on an [`EventTarget`](http://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.EventTarget.html).
263    ///
264    /// For specifying options, there is a corresponding [`EventListener::new_with_options`](#method.new_with_options) method.
265    ///
266    /// If you only need the event to fire once, you can use [`EventListener::once`](#method.once) instead,
267    /// which accepts an `FnOnce` closure.
268    ///
269    /// # Event type
270    ///
271    /// The event type can be either a `&'static str` like `"click"`, or it can be a dynamically constructed `String`.
272    ///
273    /// All event types are supported. Here is a [partial list](http://developer.mozilla.org/en-US/docs/Web/Events) of the available event types.
274    ///
275    /// # Passive
276    ///
277    /// [For performance reasons](http://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Improving_scrolling_performance_with_passive_listeners),
278    /// it is not possible to use [`event.prevent_default()`](http://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Event.html#method.prevent_default).
279    ///
280    /// If you need to use `prevent_default`, you must use [`EventListener::new_with_options`](#method.new_with_options), like this:
281    ///
282    /// ```rust,no_run
283    /// # use gloo_events::{EventListener, EventListenerOptions};
284    /// # let target = unimplemented!();
285    /// # let event_type = "click";
286    /// # fn callback(_: &web_sys::Event) {}
287    /// #
288    /// let options = EventListenerOptions::enable_prevent_default();
289    ///
290    /// EventListener::new_with_options(target, event_type, options, callback)
291    /// # ;
292    /// ```
293    ///
294    /// # Capture
295    ///
296    /// By default, event listeners are run in the bubble phase, *not* the capture phase. The official specification has
297    /// [a good explanation](http://www.w3.org/TR/DOM-Level-3-Events/#event-flow) of capturing vs bubbling.
298    ///
299    /// If you want it to run in the capture phase, you must use [`EventListener::new_with_options`](#method.new_with_options), like this:
300    ///
301    /// ```rust,no_run
302    /// # use gloo_events::{EventListener, EventListenerOptions};
303    /// # let target = unimplemented!();
304    /// # let event_type = "click";
305    /// # fn callback(_: &web_sys::Event) {}
306    /// #
307    /// // This runs the event listener in the capture phase, rather than the bubble phase
308    /// let options = EventListenerOptions::run_in_capture_phase();
309    ///
310    /// EventListener::new_with_options(target, event_type, options, callback)
311    /// # ;
312    /// ```
313    ///
314    /// # Examples
315    ///
316    /// Registers a [`"click"`](http://developer.mozilla.org/en-US/docs/Web/API/Element/click_event) event and downcasts it to the correct `Event` subtype
317    /// (which is [`MouseEvent`](http://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.MouseEvent.html)):
318    ///
319    /// ```rust,no_run
320    /// # use gloo_events::EventListener;
321    /// # use wasm_bindgen::{JsCast, UnwrapThrowExt};
322    /// # let target = unimplemented!();
323    /// #
324    /// let listener = EventListener::new(&target, "click", move |event| {
325    ///     let event = event.dyn_ref::<web_sys::MouseEvent>().unwrap_throw();
326    ///
327    ///     // ...
328    /// });
329    /// ```
330    #[inline]
331    pub fn new<S, F>(target: &EventTarget, event_type: S, callback: F) -> Self
332    where
333        S: Into<Cow<'static, str>>,
334        F: FnMut(&Event) + 'static,
335    {
336        let callback = Closure::wrap(Box::new(callback) as Box<dyn FnMut(&Event)>);
337
338        NEW_OPTIONS.with(move |options| {
339            Self::raw_new(
340                target,
341                event_type.into(),
342                callback,
343                options,
344                EventListenerPhase::Bubble,
345            )
346        })
347    }
348
349    /// This is exactly the same as [`EventListener::new`](#method.new), except the event will only fire once,
350    /// and it accepts `FnOnce` instead of `FnMut`.
351    ///
352    /// For specifying options, there is a corresponding [`EventListener::once_with_options`](#method.once_with_options) method.
353    ///
354    /// # Examples
355    ///
356    /// Registers a [`"load"`](http://developer.mozilla.org/en-US/docs/Web/API/FileReader/load_event) event and casts it to the correct type
357    /// (which is [`ProgressEvent`](http://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.ProgressEvent.html)):
358    ///
359    /// ```rust,no_run
360    /// # use gloo_events::EventListener;
361    /// # use wasm_bindgen::{JsCast, UnwrapThrowExt};
362    /// # let target = unimplemented!();
363    /// #
364    /// let listener = EventListener::once(&target, "load", move |event| {
365    ///     let event = event.dyn_ref::<web_sys::ProgressEvent>().unwrap_throw();
366    ///
367    ///     // ...
368    /// });
369    /// ```
370    #[inline]
371    pub fn once<S, F>(target: &EventTarget, event_type: S, callback: F) -> Self
372    where
373        S: Into<Cow<'static, str>>,
374        F: FnOnce(&Event) + 'static,
375    {
376        let callback = Closure::once(callback);
377
378        ONCE_OPTIONS.with(move |options| {
379            Self::raw_new(
380                target,
381                event_type.into(),
382                callback,
383                options,
384                EventListenerPhase::Bubble,
385            )
386        })
387    }
388
389    /// Registers an event listener on an [`EventTarget`](http://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.EventTarget.html).
390    ///
391    /// It is recommended to use [`EventListener::new`](#method.new) instead, because it has better performance, and it is more convenient.
392    ///
393    /// If you only need the event to fire once, you can use [`EventListener::once_with_options`](#method.once_with_options) instead,
394    /// which accepts an `FnOnce` closure.
395    ///
396    /// # Event type
397    ///
398    /// The event type can be either a `&'static str` like `"click"`, or it can be a dynamically constructed `String`.
399    ///
400    /// All event types are supported. Here is a [partial list](http://developer.mozilla.org/en-US/docs/Web/Events)
401    /// of the available event types.
402    ///
403    /// # Options
404    ///
405    /// See the documentation for [`EventListenerOptions`](struct.EventListenerOptions.html) for more details.
406    ///
407    /// # Examples
408    ///
409    /// Registers a [`"touchstart"`](http://developer.mozilla.org/en-US/docs/Web/API/Element/touchstart_event)
410    /// event and uses
411    /// [`event.prevent_default()`](http://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Event.html#method.prevent_default):
412    ///
413    /// ```rust,no_run
414    /// # use gloo_events::{EventListener, EventListenerOptions};
415    /// # let target = unimplemented!();
416    /// #
417    /// let options = EventListenerOptions::enable_prevent_default();
418    ///
419    /// let listener = EventListener::new_with_options(&target, "touchstart", options, move |event| {
420    ///     event.prevent_default();
421    ///
422    ///     // ...
423    /// });
424    /// ```
425    ///
426    /// Registers a [`"click"`](http://developer.mozilla.org/en-US/docs/Web/API/Element/click_event)
427    /// event in the capturing phase and uses
428    /// [`event.stop_propagation()`](http://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Event.html#method.stop_propagation)
429    /// to stop the event from bubbling:
430    ///
431    /// ```rust,no_run
432    /// # use gloo_events::{EventListener, EventListenerOptions};
433    /// # let target = unimplemented!();
434    /// #
435    /// let options = EventListenerOptions::run_in_capture_phase();
436    ///
437    /// let listener = EventListener::new_with_options(&target, "click", options, move |event| {
438    ///     // Stop the event from bubbling
439    ///     event.stop_propagation();
440    ///
441    ///     // ...
442    /// });
443    /// ```
444    #[inline]
445    pub fn new_with_options<S, F>(
446        target: &EventTarget,
447        event_type: S,
448        options: EventListenerOptions,
449        callback: F,
450    ) -> Self
451    where
452        S: Into<Cow<'static, str>>,
453        F: FnMut(&Event) + 'static,
454    {
455        let callback = Closure::wrap(Box::new(callback) as Box<dyn FnMut(&Event)>);
456
457        Self::raw_new(
458            target,
459            event_type.into(),
460            callback,
461            &options.as_js(false),
462            options.phase,
463        )
464    }
465
466    /// This is exactly the same as [`EventListener::new_with_options`](#method.new_with_options), except the event will only fire once,
467    /// and it accepts `FnOnce` instead of `FnMut`.
468    ///
469    /// It is recommended to use [`EventListener::once`](#method.once) instead, because it has better performance, and it is more convenient.
470    ///
471    /// # Examples
472    ///
473    /// Registers a [`"load"`](http://developer.mozilla.org/en-US/docs/Web/API/FileReader/load_event)
474    /// event and uses
475    /// [`event.prevent_default()`](http://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Event.html#method.prevent_default):
476    ///
477    /// ```rust,no_run
478    /// # use gloo_events::{EventListener, EventListenerOptions};
479    /// # let target = unimplemented!();
480    /// #
481    /// let options = EventListenerOptions::enable_prevent_default();
482    ///
483    /// let listener = EventListener::once_with_options(&target, "load", options, move |event| {
484    ///     event.prevent_default();
485    ///
486    ///     // ...
487    /// });
488    /// ```
489    ///
490    /// Registers a [`"click"`](http://developer.mozilla.org/en-US/docs/Web/API/Element/click_event)
491    /// event in the capturing phase and uses
492    /// [`event.stop_propagation()`](http://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Event.html#method.stop_propagation)
493    /// to stop the event from bubbling:
494    ///
495    /// ```rust,no_run
496    /// # use gloo_events::{EventListener, EventListenerOptions};
497    /// # let target = unimplemented!();
498    /// #
499    /// let options = EventListenerOptions::run_in_capture_phase();
500    ///
501    /// let listener = EventListener::once_with_options(&target, "click", options, move |event| {
502    ///     // Stop the event from bubbling
503    ///     event.stop_propagation();
504    ///
505    ///     // ...
506    /// });
507    /// ```
508    #[inline]
509    pub fn once_with_options<S, F>(
510        target: &EventTarget,
511        event_type: S,
512        options: EventListenerOptions,
513        callback: F,
514    ) -> Self
515    where
516        S: Into<Cow<'static, str>>,
517        F: FnOnce(&Event) + 'static,
518    {
519        let callback = Closure::once(callback);
520
521        Self::raw_new(
522            target,
523            event_type.into(),
524            callback,
525            &options.as_js(true),
526            options.phase,
527        )
528    }
529
530    /// Keeps the `EventListener` alive forever, so it will never be dropped.
531    ///
532    /// This should only be used when you want the `EventListener` to last forever, otherwise it will leak memory!
533    #[inline]
534    pub fn forget(mut self) {
535        // take() is necessary because of Rust's restrictions about Drop
536        // This will never panic, because `callback` is always `Some`
537        self.callback.take().unwrap_throw().forget()
538    }
539
540    /// Returns the `EventTarget`.
541    #[inline]
542    pub fn target(&self) -> &EventTarget {
543        &self.target
544    }
545
546    /// Returns the event type.
547    #[inline]
548    pub fn event_type(&self) -> &str {
549        &self.event_type
550    }
551
552    /// Returns the callback.
553    #[inline]
554    pub fn callback(&self) -> &Closure<dyn FnMut(&Event)> {
555        // This will never panic, because `callback` is always `Some`
556        self.callback.as_ref().unwrap_throw()
557    }
558
559    /// Returns whether the event listener is run during the capture or bubble phase.
560    ///
561    /// The official specification has [a good explanation](http://www.w3.org/TR/DOM-Level-3-Events/#event-flow)
562    /// of capturing vs bubbling.
563    #[inline]
564    pub fn phase(&self) -> EventListenerPhase {
565        self.phase
566    }
567}
568
569impl Drop for EventListener {
570    #[inline]
571    fn drop(&mut self) {
572        // This will only be None if forget() was called
573        if let Some(callback) = &self.callback {
574            self.target
575                .remove_event_listener_with_callback_and_bool(
576                    self.event_type(),
577                    callback.as_ref().unchecked_ref(),
578                    self.phase.is_capture(),
579                )
580                .unwrap_throw();
581        }
582    }
583}