UNPKG

@xo-union/event-target-shim

Version:

An implementation of WHATWG EventTarget interface.

885 lines (736 loc) 21.9 kB
/** * @author Toru Nagashima <https://github.com/mysticatea> * @copyright 2015 Toru Nagashima. All rights reserved. * See LICENSE file in root directory for full license. */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function (obj) { return typeof obj; }; } else { _typeof = function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } /** * @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<Event, PrivateData>} * @private */ var privateData = new WeakMap(); /** * Cache for wrapper classes. * @type {WeakMap<Object, Function>} * @private */ var 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) { var 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: eventTarget, event: 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 var keys = Object.keys(event); for (var i = 0; i < keys.length; ++i) { var 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: function composedPath() { var 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: function stopPropagation() { var data = pd(this); data.stopped = true; if (typeof data.event.stopPropagation === "function") { data.event.stopPropagation(); } }, /** * Stop event bubbling. * @returns {void} */ stopImmediatePropagation: function stopImmediatePropagation() { var 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: function 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; } var 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: function 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: function get() { return pd(this).event[key]; }, set: function 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: function value() { var 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) { var 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 (var i = 0; i < keys.length; ++i) { var key = keys[i]; if (!(key in BaseEvent.prototype)) { var descriptor = Object.getOwnPropertyDescriptor(proto, key); var 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; } var 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) { var 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<object, Map<string, ListenerNode>>} * @private */ var listenersMap = new WeakMap(); // Listener types var CAPTURE = 1; var BUBBLE = 2; var 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<string, ListenerNode>} The listeners. * @private */ function getListeners(eventTarget) { var 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: function get() { var listeners = getListeners(this); var node = listeners.get(eventName); while (node != null) { if (node.listenerType === ATTRIBUTE) { return node.listener; } node = node.next; } return null; }, set: function set(listener) { if (typeof listener !== "function" && !isObject(listener)) { listener = null; // eslint-disable-line no-param-reassign } var listeners = getListeners(this); // Traverse to the tail while removing old value. var prev = null; var 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) { var newNode = { listener: 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".concat(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 (var 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) { var types = new Array(arguments.length); for (var 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: function 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."); } var listeners = getListeners(this); var optionsIsObj = isObject(options); var capture = optionsIsObj ? Boolean(options.capture) : Boolean(options); var listenerType = capture ? CAPTURE : BUBBLE; var newNode = { listener: listener, listenerType: 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. }; var node = listeners.get(eventName); if (node === undefined) { listeners.set(eventName, newNode); return; } // Traverse to the tail while checking duplication.. var 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: function removeEventListener(eventName, listener, options) { if (listener == null) { return; } var listeners = getListeners(this); var capture = isObject(options) ? Boolean(options.capture) : Boolean(options); var listenerType = capture ? CAPTURE : BUBBLE; var prev = null; var 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: function dispatchEvent(event) { if (event == null || typeof event.type !== "string") { throw new TypeError('"event.type" should be a string.'); } // If listeners aren't registered, terminate. var listeners = getListeners(this); var eventName = event.type; var node = listeners.get(eventName); if (node == null) { return true; } // Since we cannot rewrite several properties, so wrap object. var wrappedEvent = wrapEvent(this, event); // This doesn't process capturing phase and bubbling phase. // This isn't participating in a tree. var 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); } exports.EventTarget = EventTarget; exports.default = EventTarget; exports.defineEventAttribute = defineEventAttribute; module.exports = EventTarget module.exports.EventTarget = module.exports["default"] = EventTarget module.exports.defineEventAttribute = defineEventAttribute //# sourceMappingURL=event-target-shim.js.map