1. 1 : /**
  2. 2 : * @file events.js. An Event System (John Resig - Secrets of a JS Ninja http://jsninja.com/)
  3. 3 : * (Original book version wasn't completely usable, so fixed some things and made Closure Compiler compatible)
  4. 4 : * This should work very similarly to jQuery's events, however it's based off the book version which isn't as
  5. 5 : * robust as jquery's, so there's probably some differences.
  6. 6 : *
  7. 7 : * @file events.js
  8. 8 : * @module events
  9. 9 : */
  10. 10 : import DomData from './dom-data';
  11. 11 : import * as Guid from './guid.js';
  12. 12 : import log from './log.js';
  13. 13 : import window from 'global/window';
  14. 14 : import document from 'global/document';
  15. 15 :
  16. 16 : /**
  17. 17 : * Clean up the listener cache and dispatchers
  18. 18 : *
  19. 19 : * @param {Element|Object} elem
  20. 20 : * Element to clean up
  21. 21 : *
  22. 22 : * @param {string} type
  23. 23 : * Type of event to clean up
  24. 24 : */
  25. 25 : function _cleanUpEvents(elem, type) {
  26. 26 : if (!DomData.has(elem)) {
  27. 27 : return;
  28. 28 : }
  29. 29 : const data = DomData.get(elem);
  30. 30 :
  31. 31 : // Remove the events of a particular type if there are none left
  32. 32 : if (data.handlers[type].length === 0) {
  33. 33 : delete data.handlers[type];
  34. 34 : // data.handlers[type] = null;
  35. 35 : // Setting to null was causing an error with data.handlers
  36. 36 :
  37. 37 : // Remove the meta-handler from the element
  38. 38 : if (elem.removeEventListener) {
  39. 39 : elem.removeEventListener(type, data.dispatcher, false);
  40. 40 : } else if (elem.detachEvent) {
  41. 41 : elem.detachEvent('on' + type, data.dispatcher);
  42. 42 : }
  43. 43 : }
  44. 44 :
  45. 45 : // Remove the events object if there are no types left
  46. 46 : if (Object.getOwnPropertyNames(data.handlers).length <= 0) {
  47. 47 : delete data.handlers;
  48. 48 : delete data.dispatcher;
  49. 49 : delete data.disabled;
  50. 50 : }
  51. 51 :
  52. 52 : // Finally remove the element data if there is no data left
  53. 53 : if (Object.getOwnPropertyNames(data).length === 0) {
  54. 54 : DomData.delete(elem);
  55. 55 : }
  56. 56 : }
  57. 57 :
  58. 58 : /**
  59. 59 : * Loops through an array of event types and calls the requested method for each type.
  60. 60 : *
  61. 61 : * @param {Function} fn
  62. 62 : * The event method we want to use.
  63. 63 : *
  64. 64 : * @param {Element|Object} elem
  65. 65 : * Element or object to bind listeners to
  66. 66 : *
  67. 67 : * @param {string} type
  68. 68 : * Type of event to bind to.
  69. 69 : *
  70. 70 : * @param {Function} callback
  71. 71 : * Event listener.
  72. 72 : */
  73. 73 : function _handleMultipleEvents(fn, elem, types, callback) {
  74. 74 : types.forEach(function(type) {
  75. 75 : // Call the event method for each one of the types
  76. 76 : fn(elem, type, callback);
  77. 77 : });
  78. 78 : }
  79. 79 :
  80. 80 : /**
  81. 81 : * Fix a native event to have standard property values
  82. 82 : *
  83. 83 : * @param {Object} event
  84. 84 : * Event object to fix.
  85. 85 : *
  86. 86 : * @return {Object}
  87. 87 : * Fixed event object.
  88. 88 : */
  89. 89 : export function fixEvent(event) {
  90. 90 : if (event.fixed_) {
  91. 91 : return event;
  92. 92 : }
  93. 93 :
  94. 94 : function returnTrue() {
  95. 95 : return true;
  96. 96 : }
  97. 97 :
  98. 98 : function returnFalse() {
  99. 99 : return false;
  100. 100 : }
  101. 101 :
  102. 102 : // Test if fixing up is needed
  103. 103 : // Used to check if !event.stopPropagation instead of isPropagationStopped
  104. 104 : // But native events return true for stopPropagation, but don't have
  105. 105 : // other expected methods like isPropagationStopped. Seems to be a problem
  106. 106 : // with the Javascript Ninja code. So we're just overriding all events now.
  107. 107 : if (!event || !event.isPropagationStopped || !event.isImmediatePropagationStopped) {
  108. 108 : const old = event || window.event;
  109. 109 :
  110. 110 : event = {};
  111. 111 : // Clone the old object so that we can modify the values event = {};
  112. 112 : // IE8 Doesn't like when you mess with native event properties
  113. 113 : // Firefox returns false for event.hasOwnProperty('type') and other props
  114. 114 : // which makes copying more difficult.
  115. 115 : // TODO: Probably best to create a whitelist of event props
  116. 116 : for (const key in old) {
  117. 117 : // Safari 6.0.3 warns you if you try to copy deprecated layerX/Y
  118. 118 : // Chrome warns you if you try to copy deprecated keyboardEvent.keyLocation
  119. 119 : // and webkitMovementX/Y
  120. 120 : // Lighthouse complains if Event.path is copied
  121. 121 : if (key !== 'layerX' && key !== 'layerY' && key !== 'keyLocation' &&
  122. 122 : key !== 'webkitMovementX' && key !== 'webkitMovementY' &&
  123. 123 : key !== 'path') {
  124. 124 : // Chrome 32+ warns if you try to copy deprecated returnValue, but
  125. 125 : // we still want to if preventDefault isn't supported (IE8).
  126. 126 : if (!(key === 'returnValue' && old.preventDefault)) {
  127. 127 : event[key] = old[key];
  128. 128 : }
  129. 129 : }
  130. 130 : }
  131. 131 :
  132. 132 : // The event occurred on this element
  133. 133 : if (!event.target) {
  134. 134 : event.target = event.srcElement || document;
  135. 135 : }
  136. 136 :
  137. 137 : // Handle which other element the event is related to
  138. 138 : if (!event.relatedTarget) {
  139. 139 : event.relatedTarget = event.fromElement === event.target ?
  140. 140 : event.toElement :
  141. 141 : event.fromElement;
  142. 142 : }
  143. 143 :
  144. 144 : // Stop the default browser action
  145. 145 : event.preventDefault = function() {
  146. 146 : if (old.preventDefault) {
  147. 147 : old.preventDefault();
  148. 148 : }
  149. 149 : event.returnValue = false;
  150. 150 : old.returnValue = false;
  151. 151 : event.defaultPrevented = true;
  152. 152 : };
  153. 153 :
  154. 154 : event.defaultPrevented = false;
  155. 155 :
  156. 156 : // Stop the event from bubbling
  157. 157 : event.stopPropagation = function() {
  158. 158 : if (old.stopPropagation) {
  159. 159 : old.stopPropagation();
  160. 160 : }
  161. 161 : event.cancelBubble = true;
  162. 162 : old.cancelBubble = true;
  163. 163 : event.isPropagationStopped = returnTrue;
  164. 164 : };
  165. 165 :
  166. 166 : event.isPropagationStopped = returnFalse;
  167. 167 :
  168. 168 : // Stop the event from bubbling and executing other handlers
  169. 169 : event.stopImmediatePropagation = function() {
  170. 170 : if (old.stopImmediatePropagation) {
  171. 171 : old.stopImmediatePropagation();
  172. 172 : }
  173. 173 : event.isImmediatePropagationStopped = returnTrue;
  174. 174 : event.stopPropagation();
  175. 175 : };
  176. 176 :
  177. 177 : event.isImmediatePropagationStopped = returnFalse;
  178. 178 :
  179. 179 : // Handle mouse position
  180. 180 : if (event.clientX !== null && event.clientX !== undefined) {
  181. 181 : const doc = document.documentElement;
  182. 182 : const body = document.body;
  183. 183 :
  184. 184 : event.pageX = event.clientX +
  185. 185 : (doc && doc.scrollLeft || body && body.scrollLeft || 0) -
  186. 186 : (doc && doc.clientLeft || body && body.clientLeft || 0);
  187. 187 : event.pageY = event.clientY +
  188. 188 : (doc && doc.scrollTop || body && body.scrollTop || 0) -
  189. 189 : (doc && doc.clientTop || body && body.clientTop || 0);
  190. 190 : }
  191. 191 :
  192. 192 : // Handle key presses
  193. 193 : event.which = event.charCode || event.keyCode;
  194. 194 :
  195. 195 : // Fix button for mouse clicks:
  196. 196 : // 0 == left; 1 == middle; 2 == right
  197. 197 : if (event.button !== null && event.button !== undefined) {
  198. 198 :
  199. 199 : // The following is disabled because it does not pass videojs-standard
  200. 200 : // and... yikes.
  201. 201 : /* eslint-disable */
  202. 202 : event.button = (event.button & 1 ? 0 :
  203. 203 : (event.button & 4 ? 1 :
  204. 204 : (event.button & 2 ? 2 : 0)));
  205. 205 : /* eslint-enable */
  206. 206 : }
  207. 207 : }
  208. 208 :
  209. 209 : event.fixed_ = true;
  210. 210 : // Returns fixed-up instance
  211. 211 : return event;
  212. 212 : }
  213. 213 :
  214. 214 : /**
  215. 215 : * Whether passive event listeners are supported
  216. 216 : */
  217. 217 : let _supportsPassive;
  218. 218 :
  219. 219 : const supportsPassive = function() {
  220. 220 : if (typeof _supportsPassive !== 'boolean') {
  221. 221 : _supportsPassive = false;
  222. 222 : try {
  223. 223 : const opts = Object.defineProperty({}, 'passive', {
  224. 224 : get() {
  225. 225 : _supportsPassive = true;
  226. 226 : }
  227. 227 : });
  228. 228 :
  229. 229 : window.addEventListener('test', null, opts);
  230. 230 : window.removeEventListener('test', null, opts);
  231. 231 : } catch (e) {
  232. 232 : // disregard
  233. 233 : }
  234. 234 : }
  235. 235 :
  236. 236 : return _supportsPassive;
  237. 237 : };
  238. 238 :
  239. 239 : /**
  240. 240 : * Touch events Chrome expects to be passive
  241. 241 : */
  242. 242 : const passiveEvents = [
  243. 243 : 'touchstart',
  244. 244 : 'touchmove'
  245. 245 : ];
  246. 246 :
  247. 247 : /**
  248. 248 : * Add an event listener to element
  249. 249 : * It stores the handler function in a separate cache object
  250. 250 : * and adds a generic handler to the element's event,
  251. 251 : * along with a unique id (guid) to the element.
  252. 252 : *
  253. 253 : * @param {Element|Object} elem
  254. 254 : * Element or object to bind listeners to
  255. 255 : *
  256. 256 : * @param {string|string[]} type
  257. 257 : * Type of event to bind to.
  258. 258 : *
  259. 259 : * @param {Function} fn
  260. 260 : * Event listener.
  261. 261 : */
  262. 262 : export function on(elem, type, fn) {
  263. 263 : if (Array.isArray(type)) {
  264. 264 : return _handleMultipleEvents(on, elem, type, fn);
  265. 265 : }
  266. 266 :
  267. 267 : if (!DomData.has(elem)) {
  268. 268 : DomData.set(elem, {});
  269. 269 : }
  270. 270 :
  271. 271 : const data = DomData.get(elem);
  272. 272 :
  273. 273 : // We need a place to store all our handler data
  274. 274 : if (!data.handlers) {
  275. 275 : data.handlers = {};
  276. 276 : }
  277. 277 :
  278. 278 : if (!data.handlers[type]) {
  279. 279 : data.handlers[type] = [];
  280. 280 : }
  281. 281 :
  282. 282 : if (!fn.guid) {
  283. 283 : fn.guid = Guid.newGUID();
  284. 284 : }
  285. 285 :
  286. 286 : data.handlers[type].push(fn);
  287. 287 :
  288. 288 : if (!data.dispatcher) {
  289. 289 : data.disabled = false;
  290. 290 :
  291. 291 : data.dispatcher = function(event, hash) {
  292. 292 :
  293. 293 : if (data.disabled) {
  294. 294 : return;
  295. 295 : }
  296. 296 :
  297. 297 : event = fixEvent(event);
  298. 298 :
  299. 299 : const handlers = data.handlers[event.type];
  300. 300 :
  301. 301 : if (handlers) {
  302. 302 : // Copy handlers so if handlers are added/removed during the process it doesn't throw everything off.
  303. 303 : const handlersCopy = handlers.slice(0);
  304. 304 :
  305. 305 : for (let m = 0, n = handlersCopy.length; m < n; m++) {
  306. 306 : if (event.isImmediatePropagationStopped()) {
  307. 307 : break;
  308. 308 : } else {
  309. 309 : try {
  310. 310 : handlersCopy[m].call(elem, event, hash);
  311. 311 : } catch (e) {
  312. 312 : log.error(e);
  313. 313 : }
  314. 314 : }
  315. 315 : }
  316. 316 : }
  317. 317 : };
  318. 318 : }
  319. 319 :
  320. 320 : if (data.handlers[type].length === 1) {
  321. 321 : if (elem.addEventListener) {
  322. 322 : let options = false;
  323. 323 :
  324. 324 : if (supportsPassive() &&
  325. 325 : passiveEvents.indexOf(type) > -1) {
  326. 326 : options = {passive: true};
  327. 327 : }
  328. 328 : elem.addEventListener(type, data.dispatcher, options);
  329. 329 : } else if (elem.attachEvent) {
  330. 330 : elem.attachEvent('on' + type, data.dispatcher);
  331. 331 : }
  332. 332 : }
  333. 333 : }
  334. 334 :
  335. 335 : /**
  336. 336 : * Removes event listeners from an element
  337. 337 : *
  338. 338 : * @param {Element|Object} elem
  339. 339 : * Object to remove listeners from.
  340. 340 : *
  341. 341 : * @param {string|string[]} [type]
  342. 342 : * Type of listener to remove. Don't include to remove all events from element.
  343. 343 : *
  344. 344 : * @param {Function} [fn]
  345. 345 : * Specific listener to remove. Don't include to remove listeners for an event
  346. 346 : * type.
  347. 347 : */
  348. 348 : export function off(elem, type, fn) {
  349. 349 : // Don't want to add a cache object through getElData if not needed
  350. 350 : if (!DomData.has(elem)) {
  351. 351 : return;
  352. 352 : }
  353. 353 :
  354. 354 : const data = DomData.get(elem);
  355. 355 :
  356. 356 : // If no events exist, nothing to unbind
  357. 357 : if (!data.handlers) {
  358. 358 : return;
  359. 359 : }
  360. 360 :
  361. 361 : if (Array.isArray(type)) {
  362. 362 : return _handleMultipleEvents(off, elem, type, fn);
  363. 363 : }
  364. 364 :
  365. 365 : // Utility function
  366. 366 : const removeType = function(el, t) {
  367. 367 : data.handlers[t] = [];
  368. 368 : _cleanUpEvents(el, t);
  369. 369 : };
  370. 370 :
  371. 371 : // Are we removing all bound events?
  372. 372 : if (type === undefined) {
  373. 373 : for (const t in data.handlers) {
  374. 374 : if (Object.prototype.hasOwnProperty.call(data.handlers || {}, t)) {
  375. 375 : removeType(elem, t);
  376. 376 : }
  377. 377 : }
  378. 378 : return;
  379. 379 : }
  380. 380 :
  381. 381 : const handlers = data.handlers[type];
  382. 382 :
  383. 383 : // If no handlers exist, nothing to unbind
  384. 384 : if (!handlers) {
  385. 385 : return;
  386. 386 : }
  387. 387 :
  388. 388 : // If no listener was provided, remove all listeners for type
  389. 389 : if (!fn) {
  390. 390 : removeType(elem, type);
  391. 391 : return;
  392. 392 : }
  393. 393 :
  394. 394 : // We're only removing a single handler
  395. 395 : if (fn.guid) {
  396. 396 : for (let n = 0; n < handlers.length; n++) {
  397. 397 : if (handlers[n].guid === fn.guid) {
  398. 398 : handlers.splice(n--, 1);
  399. 399 : }
  400. 400 : }
  401. 401 : }
  402. 402 :
  403. 403 : _cleanUpEvents(elem, type);
  404. 404 : }
  405. 405 :
  406. 406 : /**
  407. 407 : * Trigger an event for an element
  408. 408 : *
  409. 409 : * @param {Element|Object} elem
  410. 410 : * Element to trigger an event on
  411. 411 : *
  412. 412 : * @param {EventTarget~Event|string} event
  413. 413 : * A string (the type) or an event object with a type attribute
  414. 414 : *
  415. 415 : * @param {Object} [hash]
  416. 416 : * data hash to pass along with the event
  417. 417 : *
  418. 418 : * @return {boolean|undefined}
  419. 419 : * Returns the opposite of `defaultPrevented` if default was
  420. 420 : * prevented. Otherwise, returns `undefined`
  421. 421 : */
  422. 422 : export function trigger(elem, event, hash) {
  423. 423 : // Fetches element data and a reference to the parent (for bubbling).
  424. 424 : // Don't want to add a data object to cache for every parent,
  425. 425 : // so checking hasElData first.
  426. 426 : const elemData = DomData.has(elem) ? DomData.get(elem) : {};
  427. 427 : const parent = elem.parentNode || elem.ownerDocument;
  428. 428 : // type = event.type || event,
  429. 429 : // handler;
  430. 430 :
  431. 431 : // If an event name was passed as a string, creates an event out of it
  432. 432 : if (typeof event === 'string') {
  433. 433 : event = {type: event, target: elem};
  434. 434 : } else if (!event.target) {
  435. 435 : event.target = elem;
  436. 436 : }
  437. 437 :
  438. 438 : // Normalizes the event properties.
  439. 439 : event = fixEvent(event);
  440. 440 :
  441. 441 : // If the passed element has a dispatcher, executes the established handlers.
  442. 442 : if (elemData.dispatcher) {
  443. 443 : elemData.dispatcher.call(elem, event, hash);
  444. 444 : }
  445. 445 :
  446. 446 : // Unless explicitly stopped or the event does not bubble (e.g. media events)
  447. 447 : // recursively calls this function to bubble the event up the DOM.
  448. 448 : if (parent && !event.isPropagationStopped() && event.bubbles === true) {
  449. 449 : trigger.call(null, parent, event, hash);
  450. 450 :
  451. 451 : // If at the top of the DOM, triggers the default action unless disabled.
  452. 452 : } else if (!parent && !event.defaultPrevented && event.target && event.target[event.type]) {
  453. 453 : if (!DomData.has(event.target)) {
  454. 454 : DomData.set(event.target, {});
  455. 455 : }
  456. 456 : const targetData = DomData.get(event.target);
  457. 457 :
  458. 458 : // Checks if the target has a default action for this event.
  459. 459 : if (event.target[event.type]) {
  460. 460 : // Temporarily disables event dispatching on the target as we have already executed the handler.
  461. 461 : targetData.disabled = true;
  462. 462 : // Executes the default action.
  463. 463 : if (typeof event.target[event.type] === 'function') {
  464. 464 : event.target[event.type]();
  465. 465 : }
  466. 466 : // Re-enables event dispatching.
  467. 467 : targetData.disabled = false;
  468. 468 : }
  469. 469 : }
  470. 470 :
  471. 471 : // Inform the triggerer if the default was prevented by returning false
  472. 472 : return !event.defaultPrevented;
  473. 473 : }
  474. 474 :
  475. 475 : /**
  476. 476 : * Trigger a listener only once for an event.
  477. 477 : *
  478. 478 : * @param {Element|Object} elem
  479. 479 : * Element or object to bind to.
  480. 480 : *
  481. 481 : * @param {string|string[]} type
  482. 482 : * Name/type of event
  483. 483 : *
  484. 484 : * @param {Event~EventListener} fn
  485. 485 : * Event listener function
  486. 486 : */
  487. 487 : export function one(elem, type, fn) {
  488. 488 : if (Array.isArray(type)) {
  489. 489 : return _handleMultipleEvents(one, elem, type, fn);
  490. 490 : }
  491. 491 : const func = function() {
  492. 492 : off(elem, type, func);
  493. 493 : fn.apply(this, arguments);
  494. 494 : };
  495. 495 :
  496. 496 : // copy the guid to the new function so it can removed using the original function's ID
  497. 497 : func.guid = fn.guid = fn.guid || Guid.newGUID();
  498. 498 : on(elem, type, func);
  499. 499 : }
  500. 500 :
  501. 501 : /**
  502. 502 : * Trigger a listener only once and then turn if off for all
  503. 503 : * configured events
  504. 504 : *
  505. 505 : * @param {Element|Object} elem
  506. 506 : * Element or object to bind to.
  507. 507 : *
  508. 508 : * @param {string|string[]} type
  509. 509 : * Name/type of event
  510. 510 : *
  511. 511 : * @param {Event~EventListener} fn
  512. 512 : * Event listener function
  513. 513 : */
  514. 514 : export function any(elem, type, fn) {
  515. 515 : const func = function() {
  516. 516 : off(elem, type, func);
  517. 517 : fn.apply(this, arguments);
  518. 518 : };
  519. 519 :
  520. 520 : // copy the guid to the new function so it can removed using the original function's ID
  521. 521 : func.guid = fn.guid = fn.guid || Guid.newGUID();
  522. 522 :
  523. 523 : // multiple ons, but one off for everything
  524. 524 : on(elem, type, func);
  525. 525 : }