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