@xo-union/event-target-shim
Version:
An implementation of WHATWG EventTarget interface.
885 lines (736 loc) • 21.9 kB
JavaScript
/**
* @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