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}