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}