Source: lib/util/event_manager.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.util.EventManager');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.util.IReleasable');
  9. goog.require('shaka.util.MultiMap');
  10. /**
  11. * @summary
  12. * An EventManager maintains a collection of "event
  13. * bindings" between event targets and event listeners.
  14. *
  15. * @implements {shaka.util.IReleasable}
  16. * @export
  17. */
  18. shaka.util.EventManager = class {
  19. /** */
  20. constructor() {
  21. /**
  22. * Maps an event type to an array of event bindings.
  23. * @private {shaka.util.MultiMap<!shaka.util.EventManager.Binding_>}
  24. */
  25. this.bindingMap_ = new shaka.util.MultiMap();
  26. }
  27. /**
  28. * Detaches all event listeners.
  29. * @override
  30. * @export
  31. */
  32. release() {
  33. this.removeAll();
  34. this.bindingMap_ = null;
  35. }
  36. /**
  37. * Attaches an event listener to an event target.
  38. * @param {EventTarget} target The event target.
  39. * @param {string} type The event type.
  40. * @param {shaka.util.EventManager.ListenerType} listener The event listener.
  41. * @param {(boolean|!AddEventListenerOptions)=} options An object that
  42. * specifies characteristics about the event listener.
  43. * The passive option, if true, indicates that this function will never
  44. * call preventDefault(), which improves scrolling performance.
  45. * @export
  46. */
  47. listen(target, type, listener, options) {
  48. if (!this.bindingMap_) {
  49. return;
  50. }
  51. const binding =
  52. new shaka.util.EventManager.Binding_(target, type, listener, options);
  53. this.bindingMap_.push(type, binding);
  54. }
  55. /**
  56. * Attaches an event listener to an event target. The listener will be
  57. * removed when the first instance of the event is fired.
  58. * @param {EventTarget} target The event target.
  59. * @param {string} type The event type.
  60. * @param {shaka.util.EventManager.ListenerType} listener The event listener.
  61. * @param {(boolean|!AddEventListenerOptions)=} options An object that
  62. * specifies characteristics about the event listener.
  63. * The passive option, if true, indicates that this function will never
  64. * call preventDefault(), which improves scrolling performance.
  65. * @export
  66. */
  67. listenOnce(target, type, listener, options) {
  68. // Install a shim listener that will stop listening after the first event.
  69. const shim = (event) => {
  70. // Stop listening to this event.
  71. this.unlisten(target, type, shim);
  72. // Call the original listener.
  73. listener(event);
  74. };
  75. this.listen(target, type, shim, options);
  76. }
  77. /**
  78. * Detaches an event listener from an event target.
  79. * @param {EventTarget} target The event target.
  80. * @param {string} type The event type.
  81. * @param {shaka.util.EventManager.ListenerType=} listener The event listener.
  82. * @export
  83. */
  84. unlisten(target, type, listener) {
  85. if (!this.bindingMap_) {
  86. return;
  87. }
  88. const list = this.bindingMap_.get(type) || [];
  89. for (const binding of list) {
  90. if (binding.target == target) {
  91. if (listener == binding.listener || !listener) {
  92. binding.unlisten();
  93. this.bindingMap_.remove(type, binding);
  94. }
  95. }
  96. }
  97. }
  98. /**
  99. * Detaches all event listeners from all targets.
  100. * @export
  101. */
  102. removeAll() {
  103. if (!this.bindingMap_) {
  104. return;
  105. }
  106. const list = this.bindingMap_.getAll();
  107. for (const binding of list) {
  108. binding.unlisten();
  109. }
  110. this.bindingMap_.clear();
  111. }
  112. };
  113. /**
  114. * @typedef {function(!Event)}
  115. * @export
  116. */
  117. shaka.util.EventManager.ListenerType;
  118. /**
  119. * Creates a new Binding_ and attaches the event listener to the event target.
  120. *
  121. * @private
  122. */
  123. shaka.util.EventManager.Binding_ = class {
  124. /**
  125. * @param {EventTarget} target The event target.
  126. * @param {string} type The event type.
  127. * @param {shaka.util.EventManager.ListenerType} listener The event listener.
  128. * @param {(boolean|!AddEventListenerOptions)=} options An object that
  129. * specifies characteristics about the event listener.
  130. * The passive option, if true, indicates that this function will never
  131. * call preventDefault(), which improves scrolling performance.
  132. */
  133. constructor(target, type, listener, options) {
  134. /** @type {EventTarget} */
  135. this.target = target;
  136. /** @type {string} */
  137. this.type = type;
  138. /** @type {?shaka.util.EventManager.ListenerType} */
  139. this.listener = listener;
  140. /** @type {(boolean|!AddEventListenerOptions)} */
  141. this.options =
  142. shaka.util.EventManager.Binding_.convertOptions_(target, options);
  143. this.target.addEventListener(type, listener, this.options);
  144. }
  145. /**
  146. * Detaches the event listener from the event target. This does nothing if
  147. * the event listener is already detached.
  148. */
  149. unlisten() {
  150. goog.asserts.assert(this.target, 'Missing target');
  151. this.target.removeEventListener(this.type, this.listener, this.options);
  152. this.target = null;
  153. this.listener = null;
  154. this.options = false;
  155. }
  156. /**
  157. * Converts the provided options value into a value accepted by the browser.
  158. * Some browsers (e.g. Tizen) don't support passing options as an
  159. * object. So this detects this case and converts it.
  160. *
  161. * @param {EventTarget} target
  162. * @param {(boolean|!AddEventListenerOptions)=} value
  163. * @return {(boolean|!AddEventListenerOptions)}
  164. * @private
  165. */
  166. static convertOptions_(target, value) {
  167. if (value == undefined) {
  168. return false;
  169. } else if (typeof value == 'boolean') {
  170. return value;
  171. } else {
  172. // Ignore the 'passive' option since it is just an optimization and
  173. // doesn't affect behavior. Assert there aren't any other settings to
  174. // ensure we don't have different behavior on different browsers by
  175. // ignoring an important option.
  176. const ignored = new Set(['passive', 'capture']);
  177. const keys = Object.keys(value).filter((k) => !ignored.has(k));
  178. goog.asserts.assert(
  179. keys.length == 0,
  180. 'Unsupported flag(s) to addEventListener: ' + keys.join(','));
  181. const supports =
  182. shaka.util.EventManager.Binding_.doesSupportObject_(target);
  183. if (supports) {
  184. return value;
  185. } else {
  186. return value['capture'] || false;
  187. }
  188. }
  189. }
  190. /**
  191. * Checks whether the browser supports passing objects as the third argument
  192. * to addEventListener. This caches the result value in a static field to
  193. * avoid a bunch of checks.
  194. *
  195. * @param {EventTarget} target
  196. * @return {boolean}
  197. * @private
  198. */
  199. static doesSupportObject_(target) {
  200. // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Safely_detecting_option_support
  201. let supports = shaka.util.EventManager.Binding_.supportsObject_;
  202. if (supports == undefined) {
  203. supports = false;
  204. try {
  205. const options = {};
  206. // This defines a getter that will set this variable if called. So if
  207. // the browser gets this property, it supports using an object. If the
  208. // browser doesn't get these fields, it won't support objects.
  209. const prop = {
  210. get: () => {
  211. supports = true;
  212. return false;
  213. },
  214. };
  215. Object.defineProperty(options, 'passive', prop);
  216. Object.defineProperty(options, 'capture', prop);
  217. const call = () => {};
  218. target.addEventListener('test', call, options);
  219. target.removeEventListener('test', call, options);
  220. } catch (e) {
  221. supports = false;
  222. }
  223. shaka.util.EventManager.Binding_.supportsObject_ = supports;
  224. }
  225. return supports || false; // "false" fallback needed for compiler.
  226. }
  227. };
  228. /** @private {(boolean|undefined)} */
  229. shaka.util.EventManager.Binding_.supportsObject_ = undefined;