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