gloo_events/
lib.rs

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