1. 1 : /**
  2. 2 : * @file src/js/event-target.js
  3. 3 : */
  4. 4 : import * as Events from './utils/events.js';
  5. 5 : import window from 'global/window';
  6. 6 :
  7. 7 : /**
  8. 8 : * `EventTarget` is a class that can have the same API as the DOM `EventTarget`. It
  9. 9 : * adds shorthand functions that wrap around lengthy functions. For example:
  10. 10 : * the `on` function is a wrapper around `addEventListener`.
  11. 11 : *
  12. 12 : * @see [EventTarget Spec]{@link https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget}
  13. 13 : * @class EventTarget
  14. 14 : */
  15. 15 : const EventTarget = function() {};
  16. 16 :
  17. 17 : /**
  18. 18 : * A Custom DOM event.
  19. 19 : *
  20. 20 : * @typedef {Object} EventTarget~Event
  21. 21 : * @see [Properties]{@link https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent}
  22. 22 : */
  23. 23 :
  24. 24 : /**
  25. 25 : * All event listeners should follow the following format.
  26. 26 : *
  27. 27 : * @callback EventTarget~EventListener
  28. 28 : * @this {EventTarget}
  29. 29 : *
  30. 30 : * @param {EventTarget~Event} event
  31. 31 : * the event that triggered this function
  32. 32 : *
  33. 33 : * @param {Object} [hash]
  34. 34 : * hash of data sent during the event
  35. 35 : */
  36. 36 :
  37. 37 : /**
  38. 38 : * An object containing event names as keys and booleans as values.
  39. 39 : *
  40. 40 : * > NOTE: If an event name is set to a true value here {@link EventTarget#trigger}
  41. 41 : * will have extra functionality. See that function for more information.
  42. 42 : *
  43. 43 : * @property EventTarget.prototype.allowedEvents_
  44. 44 : * @private
  45. 45 : */
  46. 46 : EventTarget.prototype.allowedEvents_ = {};
  47. 47 :
  48. 48 : /**
  49. 49 : * Adds an `event listener` to an instance of an `EventTarget`. An `event listener` is a
  50. 50 : * function that will get called when an event with a certain name gets triggered.
  51. 51 : *
  52. 52 : * @param {string|string[]} type
  53. 53 : * An event name or an array of event names.
  54. 54 : *
  55. 55 : * @param {EventTarget~EventListener} fn
  56. 56 : * The function to call with `EventTarget`s
  57. 57 : */
  58. 58 : EventTarget.prototype.on = function(type, fn) {
  59. 59 : // Remove the addEventListener alias before calling Events.on
  60. 60 : // so we don't get into an infinite type loop
  61. 61 : const ael = this.addEventListener;
  62. 62 :
  63. 63 : this.addEventListener = () => {};
  64. 64 : Events.on(this, type, fn);
  65. 65 : this.addEventListener = ael;
  66. 66 : };
  67. 67 :
  68. 68 : /**
  69. 69 : * An alias of {@link EventTarget#on}. Allows `EventTarget` to mimic
  70. 70 : * the standard DOM API.
  71. 71 : *
  72. 72 : * @function
  73. 73 : * @see {@link EventTarget#on}
  74. 74 : */
  75. 75 : EventTarget.prototype.addEventListener = EventTarget.prototype.on;
  76. 76 :
  77. 77 : /**
  78. 78 : * Removes an `event listener` for a specific event from an instance of `EventTarget`.
  79. 79 : * This makes it so that the `event listener` will no longer get called when the
  80. 80 : * named event happens.
  81. 81 : *
  82. 82 : * @param {string|string[]} type
  83. 83 : * An event name or an array of event names.
  84. 84 : *
  85. 85 : * @param {EventTarget~EventListener} fn
  86. 86 : * The function to remove.
  87. 87 : */
  88. 88 : EventTarget.prototype.off = function(type, fn) {
  89. 89 : Events.off(this, type, fn);
  90. 90 : };
  91. 91 :
  92. 92 : /**
  93. 93 : * An alias of {@link EventTarget#off}. Allows `EventTarget` to mimic
  94. 94 : * the standard DOM API.
  95. 95 : *
  96. 96 : * @function
  97. 97 : * @see {@link EventTarget#off}
  98. 98 : */
  99. 99 : EventTarget.prototype.removeEventListener = EventTarget.prototype.off;
  100. 100 :
  101. 101 : /**
  102. 102 : * This function will add an `event listener` that gets triggered only once. After the
  103. 103 : * first trigger it will get removed. This is like adding an `event listener`
  104. 104 : * with {@link EventTarget#on} that calls {@link EventTarget#off} on itself.
  105. 105 : *
  106. 106 : * @param {string|string[]} type
  107. 107 : * An event name or an array of event names.
  108. 108 : *
  109. 109 : * @param {EventTarget~EventListener} fn
  110. 110 : * The function to be called once for each event name.
  111. 111 : */
  112. 112 : EventTarget.prototype.one = function(type, fn) {
  113. 113 : // Remove the addEventListener aliasing Events.on
  114. 114 : // so we don't get into an infinite type loop
  115. 115 : const ael = this.addEventListener;
  116. 116 :
  117. 117 : this.addEventListener = () => {};
  118. 118 : Events.one(this, type, fn);
  119. 119 : this.addEventListener = ael;
  120. 120 : };
  121. 121 :
  122. 122 : EventTarget.prototype.any = function(type, fn) {
  123. 123 : // Remove the addEventListener aliasing Events.on
  124. 124 : // so we don't get into an infinite type loop
  125. 125 : const ael = this.addEventListener;
  126. 126 :
  127. 127 : this.addEventListener = () => {};
  128. 128 : Events.any(this, type, fn);
  129. 129 : this.addEventListener = ael;
  130. 130 : };
  131. 131 :
  132. 132 : /**
  133. 133 : * This function causes an event to happen. This will then cause any `event listeners`
  134. 134 : * that are waiting for that event, to get called. If there are no `event listeners`
  135. 135 : * for an event then nothing will happen.
  136. 136 : *
  137. 137 : * If the name of the `Event` that is being triggered is in `EventTarget.allowedEvents_`.
  138. 138 : * Trigger will also call the `on` + `uppercaseEventName` function.
  139. 139 : *
  140. 140 : * Example:
  141. 141 : * 'click' is in `EventTarget.allowedEvents_`, so, trigger will attempt to call
  142. 142 : * `onClick` if it exists.
  143. 143 : *
  144. 144 : * @param {string|EventTarget~Event|Object} event
  145. 145 : * The name of the event, an `Event`, or an object with a key of type set to
  146. 146 : * an event name.
  147. 147 : */
  148. 148 : EventTarget.prototype.trigger = function(event) {
  149. 149 : const type = event.type || event;
  150. 150 :
  151. 151 : // deprecation
  152. 152 : // In a future version we should default target to `this`
  153. 153 : // similar to how we default the target to `elem` in
  154. 154 : // `Events.trigger`. Right now the default `target` will be
  155. 155 : // `document` due to the `Event.fixEvent` call.
  156. 156 : if (typeof event === 'string') {
  157. 157 : event = {type};
  158. 158 : }
  159. 159 : event = Events.fixEvent(event);
  160. 160 :
  161. 161 : if (this.allowedEvents_[type] && this['on' + type]) {
  162. 162 : this['on' + type](event);
  163. 163 : }
  164. 164 :
  165. 165 : Events.trigger(this, event);
  166. 166 : };
  167. 167 :
  168. 168 : /**
  169. 169 : * An alias of {@link EventTarget#trigger}. Allows `EventTarget` to mimic
  170. 170 : * the standard DOM API.
  171. 171 : *
  172. 172 : * @function
  173. 173 : * @see {@link EventTarget#trigger}
  174. 174 : */
  175. 175 : EventTarget.prototype.dispatchEvent = EventTarget.prototype.trigger;
  176. 176 :
  177. 177 : let EVENT_MAP;
  178. 178 :
  179. 179 : EventTarget.prototype.queueTrigger = function(event) {
  180. 180 : // only set up EVENT_MAP if it'll be used
  181. 181 : if (!EVENT_MAP) {
  182. 182 : EVENT_MAP = new Map();
  183. 183 : }
  184. 184 :
  185. 185 : const type = event.type || event;
  186. 186 : let map = EVENT_MAP.get(this);
  187. 187 :
  188. 188 : if (!map) {
  189. 189 : map = new Map();
  190. 190 : EVENT_MAP.set(this, map);
  191. 191 : }
  192. 192 :
  193. 193 : const oldTimeout = map.get(type);
  194. 194 :
  195. 195 : map.delete(type);
  196. 196 : window.clearTimeout(oldTimeout);
  197. 197 :
  198. 198 : const timeout = window.setTimeout(() => {
  199. 199 : map.delete(type);
  200. 200 : // if we cleared out all timeouts for the current target, delete its map
  201. 201 : if (map.size === 0) {
  202. 202 : map = null;
  203. 203 : EVENT_MAP.delete(this);
  204. 204 : }
  205. 205 :
  206. 206 : this.trigger(event);
  207. 207 : }, 0);
  208. 208 :
  209. 209 : map.set(type, timeout);
  210. 210 : };
  211. 211 :
  212. 212 : export default EventTarget;