event-target-shim.js (23692B)
1 /** 2 * @author Toru Nagashima <https://github.com/mysticatea> 3 * @copyright 2015 Toru Nagashima. All rights reserved. 4 * See LICENSE file in root directory for full license. 5 */ 6 'use strict'; 7 8 Object.defineProperty(exports, '__esModule', { value: true }); 9 10 /** 11 * @typedef {object} PrivateData 12 * @property {EventTarget} eventTarget The event target. 13 * @property {{type:string}} event The original event object. 14 * @property {number} eventPhase The current event phase. 15 * @property {EventTarget|null} currentTarget The current event target. 16 * @property {boolean} canceled The flag to prevent default. 17 * @property {boolean} stopped The flag to stop propagation. 18 * @property {boolean} immediateStopped The flag to stop propagation immediately. 19 * @property {Function|null} passiveListener The listener if the current listener is passive. Otherwise this is null. 20 * @property {number} timeStamp The unix time. 21 * @private 22 */ 23 24 /** 25 * Private data for event wrappers. 26 * @type {WeakMap<Event, PrivateData>} 27 * @private 28 */ 29 const privateData = new WeakMap(); 30 31 /** 32 * Cache for wrapper classes. 33 * @type {WeakMap<Object, Function>} 34 * @private 35 */ 36 const wrappers = new WeakMap(); 37 38 /** 39 * Get private data. 40 * @param {Event} event The event object to get private data. 41 * @returns {PrivateData} The private data of the event. 42 * @private 43 */ 44 function pd(event) { 45 const retv = privateData.get(event); 46 console.assert( 47 retv != null, 48 "'this' is expected an Event object, but got", 49 event 50 ); 51 return retv 52 } 53 54 /** 55 * https://dom.spec.whatwg.org/#set-the-canceled-flag 56 * @param data {PrivateData} private data. 57 */ 58 function setCancelFlag(data) { 59 if (data.passiveListener != null) { 60 if ( 61 typeof console !== "undefined" && 62 typeof console.error === "function" 63 ) { 64 console.error( 65 "Unable to preventDefault inside passive event listener invocation.", 66 data.passiveListener 67 ); 68 } 69 return 70 } 71 if (!data.event.cancelable) { 72 return 73 } 74 75 data.canceled = true; 76 if (typeof data.event.preventDefault === "function") { 77 data.event.preventDefault(); 78 } 79 } 80 81 /** 82 * @see https://dom.spec.whatwg.org/#interface-event 83 * @private 84 */ 85 /** 86 * The event wrapper. 87 * @constructor 88 * @param {EventTarget} eventTarget The event target of this dispatching. 89 * @param {Event|{type:string}} event The original event to wrap. 90 */ 91 function Event(eventTarget, event) { 92 privateData.set(this, { 93 eventTarget, 94 event, 95 eventPhase: 2, 96 currentTarget: eventTarget, 97 canceled: false, 98 stopped: false, 99 immediateStopped: false, 100 passiveListener: null, 101 timeStamp: event.timeStamp || Date.now(), 102 }); 103 104 // https://heycam.github.io/webidl/#Unforgeable 105 Object.defineProperty(this, "isTrusted", { value: false, enumerable: true }); 106 107 // Define accessors 108 const keys = Object.keys(event); 109 for (let i = 0; i < keys.length; ++i) { 110 const key = keys[i]; 111 if (!(key in this)) { 112 Object.defineProperty(this, key, defineRedirectDescriptor(key)); 113 } 114 } 115 } 116 117 // Should be enumerable, but class methods are not enumerable. 118 Event.prototype = { 119 /** 120 * The type of this event. 121 * @type {string} 122 */ 123 get type() { 124 return pd(this).event.type 125 }, 126 127 /** 128 * The target of this event. 129 * @type {EventTarget} 130 */ 131 get target() { 132 return pd(this).eventTarget 133 }, 134 135 /** 136 * The target of this event. 137 * @type {EventTarget} 138 */ 139 get currentTarget() { 140 return pd(this).currentTarget 141 }, 142 143 /** 144 * @returns {EventTarget[]} The composed path of this event. 145 */ 146 composedPath() { 147 const currentTarget = pd(this).currentTarget; 148 if (currentTarget == null) { 149 return [] 150 } 151 return [currentTarget] 152 }, 153 154 /** 155 * Constant of NONE. 156 * @type {number} 157 */ 158 get NONE() { 159 return 0 160 }, 161 162 /** 163 * Constant of CAPTURING_PHASE. 164 * @type {number} 165 */ 166 get CAPTURING_PHASE() { 167 return 1 168 }, 169 170 /** 171 * Constant of AT_TARGET. 172 * @type {number} 173 */ 174 get AT_TARGET() { 175 return 2 176 }, 177 178 /** 179 * Constant of BUBBLING_PHASE. 180 * @type {number} 181 */ 182 get BUBBLING_PHASE() { 183 return 3 184 }, 185 186 /** 187 * The target of this event. 188 * @type {number} 189 */ 190 get eventPhase() { 191 return pd(this).eventPhase 192 }, 193 194 /** 195 * Stop event bubbling. 196 * @returns {void} 197 */ 198 stopPropagation() { 199 const data = pd(this); 200 201 data.stopped = true; 202 if (typeof data.event.stopPropagation === "function") { 203 data.event.stopPropagation(); 204 } 205 }, 206 207 /** 208 * Stop event bubbling. 209 * @returns {void} 210 */ 211 stopImmediatePropagation() { 212 const data = pd(this); 213 214 data.stopped = true; 215 data.immediateStopped = true; 216 if (typeof data.event.stopImmediatePropagation === "function") { 217 data.event.stopImmediatePropagation(); 218 } 219 }, 220 221 /** 222 * The flag to be bubbling. 223 * @type {boolean} 224 */ 225 get bubbles() { 226 return Boolean(pd(this).event.bubbles) 227 }, 228 229 /** 230 * The flag to be cancelable. 231 * @type {boolean} 232 */ 233 get cancelable() { 234 return Boolean(pd(this).event.cancelable) 235 }, 236 237 /** 238 * Cancel this event. 239 * @returns {void} 240 */ 241 preventDefault() { 242 setCancelFlag(pd(this)); 243 }, 244 245 /** 246 * The flag to indicate cancellation state. 247 * @type {boolean} 248 */ 249 get defaultPrevented() { 250 return pd(this).canceled 251 }, 252 253 /** 254 * The flag to be composed. 255 * @type {boolean} 256 */ 257 get composed() { 258 return Boolean(pd(this).event.composed) 259 }, 260 261 /** 262 * The unix time of this event. 263 * @type {number} 264 */ 265 get timeStamp() { 266 return pd(this).timeStamp 267 }, 268 269 /** 270 * The target of this event. 271 * @type {EventTarget} 272 * @deprecated 273 */ 274 get srcElement() { 275 return pd(this).eventTarget 276 }, 277 278 /** 279 * The flag to stop event bubbling. 280 * @type {boolean} 281 * @deprecated 282 */ 283 get cancelBubble() { 284 return pd(this).stopped 285 }, 286 set cancelBubble(value) { 287 if (!value) { 288 return 289 } 290 const data = pd(this); 291 292 data.stopped = true; 293 if (typeof data.event.cancelBubble === "boolean") { 294 data.event.cancelBubble = true; 295 } 296 }, 297 298 /** 299 * The flag to indicate cancellation state. 300 * @type {boolean} 301 * @deprecated 302 */ 303 get returnValue() { 304 return !pd(this).canceled 305 }, 306 set returnValue(value) { 307 if (!value) { 308 setCancelFlag(pd(this)); 309 } 310 }, 311 312 /** 313 * Initialize this event object. But do nothing under event dispatching. 314 * @param {string} type The event type. 315 * @param {boolean} [bubbles=false] The flag to be possible to bubble up. 316 * @param {boolean} [cancelable=false] The flag to be possible to cancel. 317 * @deprecated 318 */ 319 initEvent() { 320 // Do nothing. 321 }, 322 }; 323 324 // `constructor` is not enumerable. 325 Object.defineProperty(Event.prototype, "constructor", { 326 value: Event, 327 configurable: true, 328 writable: true, 329 }); 330 331 // Ensure `event instanceof window.Event` is `true`. 332 if (typeof window !== "undefined" && typeof window.Event !== "undefined") { 333 Object.setPrototypeOf(Event.prototype, window.Event.prototype); 334 335 // Make association for wrappers. 336 wrappers.set(window.Event.prototype, Event); 337 } 338 339 /** 340 * Get the property descriptor to redirect a given property. 341 * @param {string} key Property name to define property descriptor. 342 * @returns {PropertyDescriptor} The property descriptor to redirect the property. 343 * @private 344 */ 345 function defineRedirectDescriptor(key) { 346 return { 347 get() { 348 return pd(this).event[key] 349 }, 350 set(value) { 351 pd(this).event[key] = value; 352 }, 353 configurable: true, 354 enumerable: true, 355 } 356 } 357 358 /** 359 * Get the property descriptor to call a given method property. 360 * @param {string} key Property name to define property descriptor. 361 * @returns {PropertyDescriptor} The property descriptor to call the method property. 362 * @private 363 */ 364 function defineCallDescriptor(key) { 365 return { 366 value() { 367 const event = pd(this).event; 368 return event[key].apply(event, arguments) 369 }, 370 configurable: true, 371 enumerable: true, 372 } 373 } 374 375 /** 376 * Define new wrapper class. 377 * @param {Function} BaseEvent The base wrapper class. 378 * @param {Object} proto The prototype of the original event. 379 * @returns {Function} The defined wrapper class. 380 * @private 381 */ 382 function defineWrapper(BaseEvent, proto) { 383 const keys = Object.keys(proto); 384 if (keys.length === 0) { 385 return BaseEvent 386 } 387 388 /** CustomEvent */ 389 function CustomEvent(eventTarget, event) { 390 BaseEvent.call(this, eventTarget, event); 391 } 392 393 CustomEvent.prototype = Object.create(BaseEvent.prototype, { 394 constructor: { value: CustomEvent, configurable: true, writable: true }, 395 }); 396 397 // Define accessors. 398 for (let i = 0; i < keys.length; ++i) { 399 const key = keys[i]; 400 if (!(key in BaseEvent.prototype)) { 401 const descriptor = Object.getOwnPropertyDescriptor(proto, key); 402 const isFunc = typeof descriptor.value === "function"; 403 Object.defineProperty( 404 CustomEvent.prototype, 405 key, 406 isFunc 407 ? defineCallDescriptor(key) 408 : defineRedirectDescriptor(key) 409 ); 410 } 411 } 412 413 return CustomEvent 414 } 415 416 /** 417 * Get the wrapper class of a given prototype. 418 * @param {Object} proto The prototype of the original event to get its wrapper. 419 * @returns {Function} The wrapper class. 420 * @private 421 */ 422 function getWrapper(proto) { 423 if (proto == null || proto === Object.prototype) { 424 return Event 425 } 426 427 let wrapper = wrappers.get(proto); 428 if (wrapper == null) { 429 wrapper = defineWrapper(getWrapper(Object.getPrototypeOf(proto)), proto); 430 wrappers.set(proto, wrapper); 431 } 432 return wrapper 433 } 434 435 /** 436 * Wrap a given event to management a dispatching. 437 * @param {EventTarget} eventTarget The event target of this dispatching. 438 * @param {Object} event The event to wrap. 439 * @returns {Event} The wrapper instance. 440 * @private 441 */ 442 function wrapEvent(eventTarget, event) { 443 const Wrapper = getWrapper(Object.getPrototypeOf(event)); 444 return new Wrapper(eventTarget, event) 445 } 446 447 /** 448 * Get the immediateStopped flag of a given event. 449 * @param {Event} event The event to get. 450 * @returns {boolean} The flag to stop propagation immediately. 451 * @private 452 */ 453 function isStopped(event) { 454 return pd(event).immediateStopped 455 } 456 457 /** 458 * Set the current event phase of a given event. 459 * @param {Event} event The event to set current target. 460 * @param {number} eventPhase New event phase. 461 * @returns {void} 462 * @private 463 */ 464 function setEventPhase(event, eventPhase) { 465 pd(event).eventPhase = eventPhase; 466 } 467 468 /** 469 * Set the current target of a given event. 470 * @param {Event} event The event to set current target. 471 * @param {EventTarget|null} currentTarget New current target. 472 * @returns {void} 473 * @private 474 */ 475 function setCurrentTarget(event, currentTarget) { 476 pd(event).currentTarget = currentTarget; 477 } 478 479 /** 480 * Set a passive listener of a given event. 481 * @param {Event} event The event to set current target. 482 * @param {Function|null} passiveListener New passive listener. 483 * @returns {void} 484 * @private 485 */ 486 function setPassiveListener(event, passiveListener) { 487 pd(event).passiveListener = passiveListener; 488 } 489 490 /** 491 * @typedef {object} ListenerNode 492 * @property {Function} listener 493 * @property {1|2|3} listenerType 494 * @property {boolean} passive 495 * @property {boolean} once 496 * @property {ListenerNode|null} next 497 * @private 498 */ 499 500 /** 501 * @type {WeakMap<object, Map<string, ListenerNode>>} 502 * @private 503 */ 504 const listenersMap = new WeakMap(); 505 506 // Listener types 507 const CAPTURE = 1; 508 const BUBBLE = 2; 509 const ATTRIBUTE = 3; 510 511 /** 512 * Check whether a given value is an object or not. 513 * @param {any} x The value to check. 514 * @returns {boolean} `true` if the value is an object. 515 */ 516 function isObject(x) { 517 return x !== null && typeof x === "object" //eslint-disable-line no-restricted-syntax 518 } 519 520 /** 521 * Get listeners. 522 * @param {EventTarget} eventTarget The event target to get. 523 * @returns {Map<string, ListenerNode>} The listeners. 524 * @private 525 */ 526 function getListeners(eventTarget) { 527 const listeners = listenersMap.get(eventTarget); 528 if (listeners == null) { 529 throw new TypeError( 530 "'this' is expected an EventTarget object, but got another value." 531 ) 532 } 533 return listeners 534 } 535 536 /** 537 * Get the property descriptor for the event attribute of a given event. 538 * @param {string} eventName The event name to get property descriptor. 539 * @returns {PropertyDescriptor} The property descriptor. 540 * @private 541 */ 542 function defineEventAttributeDescriptor(eventName) { 543 return { 544 get() { 545 const listeners = getListeners(this); 546 let node = listeners.get(eventName); 547 while (node != null) { 548 if (node.listenerType === ATTRIBUTE) { 549 return node.listener 550 } 551 node = node.next; 552 } 553 return null 554 }, 555 556 set(listener) { 557 if (typeof listener !== "function" && !isObject(listener)) { 558 listener = null; // eslint-disable-line no-param-reassign 559 } 560 const listeners = getListeners(this); 561 562 // Traverse to the tail while removing old value. 563 let prev = null; 564 let node = listeners.get(eventName); 565 while (node != null) { 566 if (node.listenerType === ATTRIBUTE) { 567 // Remove old value. 568 if (prev !== null) { 569 prev.next = node.next; 570 } else if (node.next !== null) { 571 listeners.set(eventName, node.next); 572 } else { 573 listeners.delete(eventName); 574 } 575 } else { 576 prev = node; 577 } 578 579 node = node.next; 580 } 581 582 // Add new value. 583 if (listener !== null) { 584 const newNode = { 585 listener, 586 listenerType: ATTRIBUTE, 587 passive: false, 588 once: false, 589 next: null, 590 }; 591 if (prev === null) { 592 listeners.set(eventName, newNode); 593 } else { 594 prev.next = newNode; 595 } 596 } 597 }, 598 configurable: true, 599 enumerable: true, 600 } 601 } 602 603 /** 604 * Define an event attribute (e.g. `eventTarget.onclick`). 605 * @param {Object} eventTargetPrototype The event target prototype to define an event attrbite. 606 * @param {string} eventName The event name to define. 607 * @returns {void} 608 */ 609 function defineEventAttribute(eventTargetPrototype, eventName) { 610 Object.defineProperty( 611 eventTargetPrototype, 612 `on${eventName}`, 613 defineEventAttributeDescriptor(eventName) 614 ); 615 } 616 617 /** 618 * Define a custom EventTarget with event attributes. 619 * @param {string[]} eventNames Event names for event attributes. 620 * @returns {EventTarget} The custom EventTarget. 621 * @private 622 */ 623 function defineCustomEventTarget(eventNames) { 624 /** CustomEventTarget */ 625 function CustomEventTarget() { 626 EventTarget.call(this); 627 } 628 629 CustomEventTarget.prototype = Object.create(EventTarget.prototype, { 630 constructor: { 631 value: CustomEventTarget, 632 configurable: true, 633 writable: true, 634 }, 635 }); 636 637 for (let i = 0; i < eventNames.length; ++i) { 638 defineEventAttribute(CustomEventTarget.prototype, eventNames[i]); 639 } 640 641 return CustomEventTarget 642 } 643 644 /** 645 * EventTarget. 646 * 647 * - This is constructor if no arguments. 648 * - This is a function which returns a CustomEventTarget constructor if there are arguments. 649 * 650 * For example: 651 * 652 * class A extends EventTarget {} 653 * class B extends EventTarget("message") {} 654 * class C extends EventTarget("message", "error") {} 655 * class D extends EventTarget(["message", "error"]) {} 656 */ 657 function EventTarget() { 658 /*eslint-disable consistent-return */ 659 if (this instanceof EventTarget) { 660 listenersMap.set(this, new Map()); 661 return 662 } 663 if (arguments.length === 1 && Array.isArray(arguments[0])) { 664 return defineCustomEventTarget(arguments[0]) 665 } 666 if (arguments.length > 0) { 667 const types = new Array(arguments.length); 668 for (let i = 0; i < arguments.length; ++i) { 669 types[i] = arguments[i]; 670 } 671 return defineCustomEventTarget(types) 672 } 673 throw new TypeError("Cannot call a class as a function") 674 /*eslint-enable consistent-return */ 675 } 676 677 // Should be enumerable, but class methods are not enumerable. 678 EventTarget.prototype = { 679 /** 680 * Add a given listener to this event target. 681 * @param {string} eventName The event name to add. 682 * @param {Function} listener The listener to add. 683 * @param {boolean|{capture?:boolean,passive?:boolean,once?:boolean}} [options] The options for this listener. 684 * @returns {void} 685 */ 686 addEventListener(eventName, listener, options) { 687 if (listener == null) { 688 return 689 } 690 if (typeof listener !== "function" && !isObject(listener)) { 691 throw new TypeError("'listener' should be a function or an object.") 692 } 693 694 const listeners = getListeners(this); 695 const optionsIsObj = isObject(options); 696 const capture = optionsIsObj 697 ? Boolean(options.capture) 698 : Boolean(options); 699 const listenerType = capture ? CAPTURE : BUBBLE; 700 const newNode = { 701 listener, 702 listenerType, 703 passive: optionsIsObj && Boolean(options.passive), 704 once: optionsIsObj && Boolean(options.once), 705 next: null, 706 }; 707 708 // Set it as the first node if the first node is null. 709 let node = listeners.get(eventName); 710 if (node === undefined) { 711 listeners.set(eventName, newNode); 712 return 713 } 714 715 // Traverse to the tail while checking duplication.. 716 let prev = null; 717 while (node != null) { 718 if ( 719 node.listener === listener && 720 node.listenerType === listenerType 721 ) { 722 // Should ignore duplication. 723 return 724 } 725 prev = node; 726 node = node.next; 727 } 728 729 // Add it. 730 prev.next = newNode; 731 }, 732 733 /** 734 * Remove a given listener from this event target. 735 * @param {string} eventName The event name to remove. 736 * @param {Function} listener The listener to remove. 737 * @param {boolean|{capture?:boolean,passive?:boolean,once?:boolean}} [options] The options for this listener. 738 * @returns {void} 739 */ 740 removeEventListener(eventName, listener, options) { 741 if (listener == null) { 742 return 743 } 744 745 const listeners = getListeners(this); 746 const capture = isObject(options) 747 ? Boolean(options.capture) 748 : Boolean(options); 749 const listenerType = capture ? CAPTURE : BUBBLE; 750 751 let prev = null; 752 let node = listeners.get(eventName); 753 while (node != null) { 754 if ( 755 node.listener === listener && 756 node.listenerType === listenerType 757 ) { 758 if (prev !== null) { 759 prev.next = node.next; 760 } else if (node.next !== null) { 761 listeners.set(eventName, node.next); 762 } else { 763 listeners.delete(eventName); 764 } 765 return 766 } 767 768 prev = node; 769 node = node.next; 770 } 771 }, 772 773 /** 774 * Dispatch a given event. 775 * @param {Event|{type:string}} event The event to dispatch. 776 * @returns {boolean} `false` if canceled. 777 */ 778 dispatchEvent(event) { 779 if (event == null || typeof event.type !== "string") { 780 throw new TypeError('"event.type" should be a string.') 781 } 782 783 // If listeners aren't registered, terminate. 784 const listeners = getListeners(this); 785 const eventName = event.type; 786 let node = listeners.get(eventName); 787 if (node == null) { 788 return true 789 } 790 791 // Since we cannot rewrite several properties, so wrap object. 792 const wrappedEvent = wrapEvent(this, event); 793 794 // This doesn't process capturing phase and bubbling phase. 795 // This isn't participating in a tree. 796 let prev = null; 797 while (node != null) { 798 // Remove this listener if it's once 799 if (node.once) { 800 if (prev !== null) { 801 prev.next = node.next; 802 } else if (node.next !== null) { 803 listeners.set(eventName, node.next); 804 } else { 805 listeners.delete(eventName); 806 } 807 } else { 808 prev = node; 809 } 810 811 // Call this listener 812 setPassiveListener( 813 wrappedEvent, 814 node.passive ? node.listener : null 815 ); 816 if (typeof node.listener === "function") { 817 try { 818 node.listener.call(this, wrappedEvent); 819 } catch (err) { 820 if ( 821 typeof console !== "undefined" && 822 typeof console.error === "function" 823 ) { 824 console.error(err); 825 } 826 } 827 } else if ( 828 node.listenerType !== ATTRIBUTE && 829 typeof node.listener.handleEvent === "function" 830 ) { 831 node.listener.handleEvent(wrappedEvent); 832 } 833 834 // Break if `event.stopImmediatePropagation` was called. 835 if (isStopped(wrappedEvent)) { 836 break 837 } 838 839 node = node.next; 840 } 841 setPassiveListener(wrappedEvent, null); 842 setEventPhase(wrappedEvent, 0); 843 setCurrentTarget(wrappedEvent, null); 844 845 return !wrappedEvent.defaultPrevented 846 }, 847 }; 848 849 // `constructor` is not enumerable. 850 Object.defineProperty(EventTarget.prototype, "constructor", { 851 value: EventTarget, 852 configurable: true, 853 writable: true, 854 }); 855 856 // Ensure `eventTarget instanceof window.EventTarget` is `true`. 857 if ( 858 typeof window !== "undefined" && 859 typeof window.EventTarget !== "undefined" 860 ) { 861 Object.setPrototypeOf(EventTarget.prototype, window.EventTarget.prototype); 862 } 863 864 exports.defineEventAttribute = defineEventAttribute; 865 exports.EventTarget = EventTarget; 866 exports.default = EventTarget; 867 868 module.exports = EventTarget 869 module.exports.EventTarget = module.exports["default"] = EventTarget 870 module.exports.defineEventAttribute = defineEventAttribute 871 //# sourceMappingURL=event-target-shim.js.map