src/Events.js
import detectPassiveEvents from 'detect-passive-events';
function createSignature({ target, type, capture, passive }) {
return `target:${target},type:${type},capture:${capture},passive:${passive}`;
}
function createEventOptions({ passive, capture } = {}) {
if (detectPassiveEvents.hasSupport === true) {
return { capture, passive };
}
return capture;
}
function mergeOptions(options, target) {
if (!target) {
target = options ? options.target || window : window;
}
if (typeof options === 'boolean') {
return { target, capture: options };
} else {
return { target, ...options };
}
}
/**
* The Events class.
* Creates a single global event listener per type and target.
*
* Usually, you want to use only one instance of this class.
* (Use the default export which is a singleton instance)
*/
export class Events {
/**
* An object containing "known targets" that are supported by default and for which shortcuts are added to the `on` nad `off` methods.
*
*/
static knownTargets = {
window: window,
// document might not exist
document: window && window['document'],
// document.body might not exist
body: window && window['document'] ? window['document']['body'] : undefined
};
/**
* A `signature<String>: handler<Function>` hashmap
* @private
*/
handlers = {};
/**
* A `signature<String>: listeners:<Array>` hashmap
* @private
*/
listeners = {};
/**
* Creates new instance.
* Adds shortcuts to the `on` and `off` methods for all known targets.
*/
constructor() {
this.on = this.on.bind(this);
this.off = this.off.bind(this);
if (!this.constructor.prototype.__shortcutsInitialized) {
this.constructor.prototype.__shortcutsInitialized = true;
this.initializeShortcuts();
}
}
/**
* Adds an event listener callback.
* Creates a global event listener if necessary.
* Returns an object with a dispose function that can be called without arguments to remove the event listener callback.
*
* @param {String} type - A case-sensitive string representing the [event type](https://developer.mozilla.org/en-US/docs/Web/Events) to listen for.
* @param {Function} listener - The event listener callback
* @param {Object|Boolean} [options] - An [options object](https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md#eventlisteneroptions) or a [boolean `useCapture` flag](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#The_event_listener_callback#Parameters)
* @param {Boolean} [options.capture] - Whether to listen during the capturing phase
* @param {Boolean} [options.passive] - Whether to use a passive event listener
* @param {Object|String} [options.target=window] - The target element on which to add the event listener.
*
* @return {Object} - An object with a `dispose` function.
*/
on(type, listener, options = {}) {
options = mergeOptions(options);
let { target = window, capture = false, passive = false } = options;
if (typeof target === 'string') {
// in case a valid string was provided, e.g. 'window' or 'global'
target = this.constructor.knownTargets[target];
}
if (!target) {
// in case an invalid string was provided, e.g. 'foo', or 'body' in server-side code
if (process.env.NODE_ENV === 'development') {
console.warn(`Invalid target. Using ${window} instead.`);
}
target = window;
}
if (process.env.NODE_ENV === 'development') {
if (Object.values(this.constructor.knownTargets).indexOf(target) === -1) {
console
.warn(
`
[Events] Unknown target ${target}.
Make sure that its toString method returns a unique value.
Set toString.isUnique flag to remove this warning.
`
)
.replace(/\s\s+/g, ' ');
}
}
const signature = createSignature({ target, type, capture, passive });
if (!this.listeners[signature]) {
this.listeners[signature] = [];
}
if (this.listeners[signature].indexOf(listener) === -1) {
this.listeners[signature].push(listener);
}
if (!this.handlers[signature]) {
this.handlers[signature] = event => this.listeners[signature].forEach(cb => cb(event));
target.addEventListener(type, this.handlers[signature], createEventOptions({ passive, capture }));
}
return { dispose: () => this.off(type, listener, { target, capture, passive }) };
}
addEventListener(type, listener, options) {
return this.on(type, listener, options);
}
/**
* Removes an event listener callback.
* Removes the global event listener as well if the callback was the only one.
*
* @param {String} type - A case-sensitive string representing the [event type](https://developer.mozilla.org/en-US/docs/Web/Events) to listen for.
* @param {Function} listener - The event listener callback
* @param {Object|Boolean} [options] - An [options object](https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md#eventlisteneroptions) or a [boolean `capture` flag](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#The_event_listener_callback#Parameters)
* @param {Boolean} [options.capture] - Whether to listen during the capturing phase
* @param {Boolean} [options.passive] - Whether to use a passive event listener
* @param {Object|String} [options.target=window] - The target element on which to add the event listener.
*/
off(type, listener, options = {}) {
options = mergeOptions(options);
let { target = window, capture = false, passive = false } = options;
const signature = createSignature({ target, type, capture, passive });
if (this.listeners[signature]) {
const index = this.listeners[signature].indexOf(listener);
this.listeners[signature].splice(index, 1);
}
if (this.listeners[signature].length === 0 && this.handlers[signature]) {
target.removeEventListener(type, this.handlers[signature], createEventOptions({ passive, capture }));
this.handlers[signature] = undefined;
}
}
removeEventListener(type, listener, options) {
return this.on(type, listener, options);
}
/**
* Adds shortcut functions to the `on` and `off` methods for all known targets.
* Each function is configured with the proper target.
*
* Effectively, these two lines will execute the same:
* - `Events.on.body('click', this.handleBodyClick)`
* - `Events.on('click', this.handleBodyClick, { target: body })`
* @private
*/
initializeShortcuts() {
Object.entries(this.constructor.knownTargets).forEach(([name, target]) => {
const proto = this.constructor.prototype;
proto.on[name] = (type, listener, options) => proto.on(type, listener, mergeOptions(options, target));
proto.off[name] = (type, listener, options) => proto.off(type, listener, mergeOptions(options, target));
});
}
}
export default new Events();