/** * @author Toru Nagashima * @copyright 2015 Toru Nagashima. All rights reserved. * See LICENSE file in root directory for full license. */ /** * @typedef {object} PrivateData * @property {EventTarget} eventTarget The event target. * @property {{type:string}} event The original event object. * @property {number} eventPhase The current event phase. * @property {EventTarget|null} currentTarget The current event target. * @property {boolean} canceled The flag to prevent default. * @property {boolean} stopped The flag to stop propagation. * @property {boolean} immediateStopped The flag to stop propagation immediately. * @property {Function|null} passiveListener The listener if the current listener is passive. Otherwise this is null. * @property {number} timeStamp The unix time. * @private */ /** * Private data for event wrappers. * @type {WeakMap} * @private */ const privateData = new WeakMap(); /** * Cache for wrapper classes. * @type {WeakMap} * @private */ const wrappers = new WeakMap(); /** * Get private data. * @param {Event} event The event object to get private data. * @returns {PrivateData} The private data of the event. * @private */ function pd(event) { const retv = privateData.get(event); console.assert( retv != null, "'this' is expected an Event object, but got", event ); return retv } /** * https://dom.spec.whatwg.org/#set-the-canceled-flag * @param data {PrivateData} private data. */ function setCancelFlag(data) { if (data.passiveListener != null) { if ( typeof console !== "undefined" && typeof console.error === "function" ) { console.error( "Unable to preventDefault inside passive event listener invocation.", data.passiveListener ); } return } if (!data.event.cancelable) { return } data.canceled = true; if (typeof data.event.preventDefault === "function") { data.event.preventDefault(); } } /** * @see https://dom.spec.whatwg.org/#interface-event * @private */ /** * The event wrapper. * @constructor * @param {EventTarget} eventTarget The event target of this dispatching. * @param {Event|{type:string}} event The original event to wrap. */ function Event(eventTarget, event) { privateData.set(this, { eventTarget, event, eventPhase: 2, currentTarget: eventTarget, canceled: false, stopped: false, immediateStopped: false, passiveListener: null, timeStamp: event.timeStamp || Date.now(), }); // https://heycam.github.io/webidl/#Unforgeable Object.defineProperty(this, "isTrusted", { value: false, enumerable: true }); // Define accessors const keys = Object.keys(event); for (let i = 0; i < keys.length; ++i) { const key = keys[i]; if (!(key in this)) { Object.defineProperty(this, key, defineRedirectDescriptor(key)); } } } // Should be enumerable, but class methods are not enumerable. Event.prototype = { /** * The type of this event. * @type {string} */ get type() { return pd(this).event.type }, /** * The target of this event. * @type {EventTarget} */ get target() { return pd(this).eventTarget }, /** * The target of this event. * @type {EventTarget} */ get currentTarget() { return pd(this).currentTarget }, /** * @returns {EventTarget[]} The composed path of this event. */ composedPath() { const currentTarget = pd(this).currentTarget; if (currentTarget == null) { return [] } return [currentTarget] }, /** * Constant of NONE. * @type {number} */ get NONE() { return 0 }, /** * Constant of CAPTURING_PHASE. * @type {number} */ get CAPTURING_PHASE() { return 1 }, /** * Constant of AT_TARGET. * @type {number} */ get AT_TARGET() { return 2 }, /** * Constant of BUBBLING_PHASE. * @type {number} */ get BUBBLING_PHASE() { return 3 }, /** * The target of this event. * @type {number} */ get eventPhase() { return pd(this).eventPhase }, /** * Stop event bubbling. * @returns {void} */ stopPropagation() { const data = pd(this); data.stopped = true; if (typeof data.event.stopPropagation === "function") { data.event.stopPropagation(); } }, /** * Stop event bubbling. * @returns {void} */ stopImmediatePropagation() { const data = pd(this); data.stopped = true; data.immediateStopped = true; if (typeof data.event.stopImmediatePropagation === "function") { data.event.stopImmediatePropagation(); } }, /** * The flag to be bubbling. * @type {boolean} */ get bubbles() { return Boolean(pd(this).event.bubbles) }, /** * The flag to be cancelable. * @type {boolean} */ get cancelable() { return Boolean(pd(this).event.cancelable) }, /** * Cancel this event. * @returns {void} */ preventDefault() { setCancelFlag(pd(this)); }, /** * The flag to indicate cancellation state. * @type {boolean} */ get defaultPrevented() { return pd(this).canceled }, /** * The flag to be composed. * @type {boolean} */ get composed() { return Boolean(pd(this).event.composed) }, /** * The unix time of this event. * @type {number} */ get timeStamp() { return pd(this).timeStamp }, /** * The target of this event. * @type {EventTarget} * @deprecated */ get srcElement() { return pd(this).eventTarget }, /** * The flag to stop event bubbling. * @type {boolean} * @deprecated */ get cancelBubble() { return pd(this).stopped }, set cancelBubble(value) { if (!value) { return } const data = pd(this); data.stopped = true; if (typeof data.event.cancelBubble === "boolean") { data.event.cancelBubble = true; } }, /** * The flag to indicate cancellation state. * @type {boolean} * @deprecated */ get returnValue() { return !pd(this).canceled }, set returnValue(value) { if (!value) { setCancelFlag(pd(this)); } }, /** * Initialize this event object. But do nothing under event dispatching. * @param {string} type The event type. * @param {boolean} [bubbles=false] The flag to be possible to bubble up. * @param {boolean} [cancelable=false] The flag to be possible to cancel. * @deprecated */ initEvent() { // Do nothing. }, }; // `constructor` is not enumerable. Object.defineProperty(Event.prototype, "constructor", { value: Event, configurable: true, writable: true, }); // Ensure `event instanceof window.Event` is `true`. if (typeof window !== "undefined" && typeof window.Event !== "undefined") { Object.setPrototypeOf(Event.prototype, window.Event.prototype); // Make association for wrappers. wrappers.set(window.Event.prototype, Event); } /** * Get the property descriptor to redirect a given property. * @param {string} key Property name to define property descriptor. * @returns {PropertyDescriptor} The property descriptor to redirect the property. * @private */ function defineRedirectDescriptor(key) { return { get() { return pd(this).event[key] }, set(value) { pd(this).event[key] = value; }, configurable: true, enumerable: true, } } /** * Get the property descriptor to call a given method property. * @param {string} key Property name to define property descriptor. * @returns {PropertyDescriptor} The property descriptor to call the method property. * @private */ function defineCallDescriptor(key) { return { value() { const event = pd(this).event; return event[key].apply(event, arguments) }, configurable: true, enumerable: true, } } /** * Define new wrapper class. * @param {Function} BaseEvent The base wrapper class. * @param {Object} proto The prototype of the original event. * @returns {Function} The defined wrapper class. * @private */ function defineWrapper(BaseEvent, proto) { const keys = Object.keys(proto); if (keys.length === 0) { return BaseEvent } /** CustomEvent */ function CustomEvent(eventTarget, event) { BaseEvent.call(this, eventTarget, event); } CustomEvent.prototype = Object.create(BaseEvent.prototype, { constructor: { value: CustomEvent, configurable: true, writable: true }, }); // Define accessors. for (let i = 0; i < keys.length; ++i) { const key = keys[i]; if (!(key in BaseEvent.prototype)) { const descriptor = Object.getOwnPropertyDescriptor(proto, key); const isFunc = typeof descriptor.value === "function"; Object.defineProperty( CustomEvent.prototype, key, isFunc ? defineCallDescriptor(key) : defineRedirectDescriptor(key) ); } } return CustomEvent } /** * Get the wrapper class of a given prototype. * @param {Object} proto The prototype of the original event to get its wrapper. * @returns {Function} The wrapper class. * @private */ function getWrapper(proto) { if (proto == null || proto === Object.prototype) { return Event } let wrapper = wrappers.get(proto); if (wrapper == null) { wrapper = defineWrapper(getWrapper(Object.getPrototypeOf(proto)), proto); wrappers.set(proto, wrapper); } return wrapper } /** * Wrap a given event to management a dispatching. * @param {EventTarget} eventTarget The event target of this dispatching. * @param {Object} event The event to wrap. * @returns {Event} The wrapper instance. * @private */ function wrapEvent(eventTarget, event) { const Wrapper = getWrapper(Object.getPrototypeOf(event)); return new Wrapper(eventTarget, event) } /** * Get the immediateStopped flag of a given event. * @param {Event} event The event to get. * @returns {boolean} The flag to stop propagation immediately. * @private */ function isStopped(event) { return pd(event).immediateStopped } /** * Set the current event phase of a given event. * @param {Event} event The event to set current target. * @param {number} eventPhase New event phase. * @returns {void} * @private */ function setEventPhase(event, eventPhase) { pd(event).eventPhase = eventPhase; } /** * Set the current target of a given event. * @param {Event} event The event to set current target. * @param {EventTarget|null} currentTarget New current target. * @returns {void} * @private */ function setCurrentTarget(event, currentTarget) { pd(event).currentTarget = currentTarget; } /** * Set a passive listener of a given event. * @param {Event} event The event to set current target. * @param {Function|null} passiveListener New passive listener. * @returns {void} * @private */ function setPassiveListener(event, passiveListener) { pd(event).passiveListener = passiveListener; } /** * @typedef {object} ListenerNode * @property {Function} listener * @property {1|2|3} listenerType * @property {boolean} passive * @property {boolean} once * @property {ListenerNode|null} next * @private */ /** * @type {WeakMap>} * @private */ const listenersMap = new WeakMap(); // Listener types const CAPTURE = 1; const BUBBLE = 2; const ATTRIBUTE = 3; /** * Check whether a given value is an object or not. * @param {any} x The value to check. * @returns {boolean} `true` if the value is an object. */ function isObject(x) { return x !== null && typeof x === "object" //eslint-disable-line no-restricted-syntax } /** * Get listeners. * @param {EventTarget} eventTarget The event target to get. * @returns {Map} The listeners. * @private */ function getListeners(eventTarget) { const listeners = listenersMap.get(eventTarget); if (listeners == null) { throw new TypeError( "'this' is expected an EventTarget object, but got another value." ) } return listeners } /** * Get the property descriptor for the event attribute of a given event. * @param {string} eventName The event name to get property descriptor. * @returns {PropertyDescriptor} The property descriptor. * @private */ function defineEventAttributeDescriptor(eventName) { return { get() { const listeners = getListeners(this); let node = listeners.get(eventName); while (node != null) { if (node.listenerType === ATTRIBUTE) { return node.listener } node = node.next; } return null }, set(listener) { if (typeof listener !== "function" && !isObject(listener)) { listener = null; // eslint-disable-line no-param-reassign } const listeners = getListeners(this); // Traverse to the tail while removing old value. let prev = null; let node = listeners.get(eventName); while (node != null) { if (node.listenerType === ATTRIBUTE) { // Remove old value. if (prev !== null) { prev.next = node.next; } else if (node.next !== null) { listeners.set(eventName, node.next); } else { listeners.delete(eventName); } } else { prev = node; } node = node.next; } // Add new value. if (listener !== null) { const newNode = { listener, listenerType: ATTRIBUTE, passive: false, once: false, next: null, }; if (prev === null) { listeners.set(eventName, newNode); } else { prev.next = newNode; } } }, configurable: true, enumerable: true, } } /** * Define an event attribute (e.g. `eventTarget.onclick`). * @param {Object} eventTargetPrototype The event target prototype to define an event attrbite. * @param {string} eventName The event name to define. * @returns {void} */ function defineEventAttribute(eventTargetPrototype, eventName) { Object.defineProperty( eventTargetPrototype, `on${eventName}`, defineEventAttributeDescriptor(eventName) ); } /** * Define a custom EventTarget with event attributes. * @param {string[]} eventNames Event names for event attributes. * @returns {EventTarget} The custom EventTarget. * @private */ function defineCustomEventTarget(eventNames) { /** CustomEventTarget */ function CustomEventTarget() { EventTarget.call(this); } CustomEventTarget.prototype = Object.create(EventTarget.prototype, { constructor: { value: CustomEventTarget, configurable: true, writable: true, }, }); for (let i = 0; i < eventNames.length; ++i) { defineEventAttribute(CustomEventTarget.prototype, eventNames[i]); } return CustomEventTarget } /** * EventTarget. * * - This is constructor if no arguments. * - This is a function which returns a CustomEventTarget constructor if there are arguments. * * For example: * * class A extends EventTarget {} * class B extends EventTarget("message") {} * class C extends EventTarget("message", "error") {} * class D extends EventTarget(["message", "error"]) {} */ function EventTarget() { /*eslint-disable consistent-return */ if (this instanceof EventTarget) { listenersMap.set(this, new Map()); return } if (arguments.length === 1 && Array.isArray(arguments[0])) { return defineCustomEventTarget(arguments[0]) } if (arguments.length > 0) { const types = new Array(arguments.length); for (let i = 0; i < arguments.length; ++i) { types[i] = arguments[i]; } return defineCustomEventTarget(types) } throw new TypeError("Cannot call a class as a function") /*eslint-enable consistent-return */ } // Should be enumerable, but class methods are not enumerable. EventTarget.prototype = { /** * Add a given listener to this event target. * @param {string} eventName The event name to add. * @param {Function} listener The listener to add. * @param {boolean|{capture?:boolean,passive?:boolean,once?:boolean}} [options] The options for this listener. * @returns {void} */ addEventListener(eventName, listener, options) { if (listener == null) { return } if (typeof listener !== "function" && !isObject(listener)) { throw new TypeError("'listener' should be a function or an object.") } const listeners = getListeners(this); const optionsIsObj = isObject(options); const capture = optionsIsObj ? Boolean(options.capture) : Boolean(options); const listenerType = capture ? CAPTURE : BUBBLE; const newNode = { listener, listenerType, passive: optionsIsObj && Boolean(options.passive), once: optionsIsObj && Boolean(options.once), next: null, }; // Set it as the first node if the first node is null. let node = listeners.get(eventName); if (node === undefined) { listeners.set(eventName, newNode); return } // Traverse to the tail while checking duplication.. let prev = null; while (node != null) { if ( node.listener === listener && node.listenerType === listenerType ) { // Should ignore duplication. return } prev = node; node = node.next; } // Add it. prev.next = newNode; }, /** * Remove a given listener from this event target. * @param {string} eventName The event name to remove. * @param {Function} listener The listener to remove. * @param {boolean|{capture?:boolean,passive?:boolean,once?:boolean}} [options] The options for this listener. * @returns {void} */ removeEventListener(eventName, listener, options) { if (listener == null) { return } const listeners = getListeners(this); const capture = isObject(options) ? Boolean(options.capture) : Boolean(options); const listenerType = capture ? CAPTURE : BUBBLE; let prev = null; let node = listeners.get(eventName); while (node != null) { if ( node.listener === listener && node.listenerType === listenerType ) { if (prev !== null) { prev.next = node.next; } else if (node.next !== null) { listeners.set(eventName, node.next); } else { listeners.delete(eventName); } return } prev = node; node = node.next; } }, /** * Dispatch a given event. * @param {Event|{type:string}} event The event to dispatch. * @returns {boolean} `false` if canceled. */ dispatchEvent(event) { if (event == null || typeof event.type !== "string") { throw new TypeError('"event.type" should be a string.') } // If listeners aren't registered, terminate. const listeners = getListeners(this); const eventName = event.type; let node = listeners.get(eventName); if (node == null) { return true } // Since we cannot rewrite several properties, so wrap object. const wrappedEvent = wrapEvent(this, event); // This doesn't process capturing phase and bubbling phase. // This isn't participating in a tree. let prev = null; while (node != null) { // Remove this listener if it's once if (node.once) { if (prev !== null) { prev.next = node.next; } else if (node.next !== null) { listeners.set(eventName, node.next); } else { listeners.delete(eventName); } } else { prev = node; } // Call this listener setPassiveListener( wrappedEvent, node.passive ? node.listener : null ); if (typeof node.listener === "function") { try { node.listener.call(this, wrappedEvent); } catch (err) { if ( typeof console !== "undefined" && typeof console.error === "function" ) { console.error(err); } } } else if ( node.listenerType !== ATTRIBUTE && typeof node.listener.handleEvent === "function" ) { node.listener.handleEvent(wrappedEvent); } // Break if `event.stopImmediatePropagation` was called. if (isStopped(wrappedEvent)) { break } node = node.next; } setPassiveListener(wrappedEvent, null); setEventPhase(wrappedEvent, 0); setCurrentTarget(wrappedEvent, null); return !wrappedEvent.defaultPrevented }, }; // `constructor` is not enumerable. Object.defineProperty(EventTarget.prototype, "constructor", { value: EventTarget, configurable: true, writable: true, }); // Ensure `eventTarget instanceof window.EventTarget` is `true`. if ( typeof window !== "undefined" && typeof window.EventTarget !== "undefined" ) { Object.setPrototypeOf(EventTarget.prototype, window.EventTarget.prototype); } export default EventTarget; export { defineEventAttribute, EventTarget }; //# sourceMappingURL=event-target-shim.mjs.map