diff options
Diffstat (limited to 'toolkit/jetpack/sdk/event/core.js')
-rw-r--r-- | toolkit/jetpack/sdk/event/core.js | 193 |
1 files changed, 193 insertions, 0 deletions
diff --git a/toolkit/jetpack/sdk/event/core.js b/toolkit/jetpack/sdk/event/core.js new file mode 100644 index 000000000..c16dd2df5 --- /dev/null +++ b/toolkit/jetpack/sdk/event/core.js @@ -0,0 +1,193 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +module.metadata = { + "stability": "unstable" +}; + +const UNCAUGHT_ERROR = 'An error event was emitted for which there was no listener.'; +const BAD_LISTENER = 'The event listener must be a function.'; + +const { ns } = require('../core/namespace'); + +const event = ns(); + +const EVENT_TYPE_PATTERN = /^on([A-Z]\w+$)/; +exports.EVENT_TYPE_PATTERN = EVENT_TYPE_PATTERN; + +// Utility function to access given event `target` object's event listeners for +// the specific event `type`. If listeners for this type does not exists they +// will be created. +const observers = function observers(target, type) { + if (!target) throw TypeError("Event target must be an object"); + let listeners = event(target); + return type in listeners ? listeners[type] : listeners[type] = []; +}; + +/** + * Registers an event `listener` that is called every time events of + * specified `type` is emitted on the given event `target`. + * @param {Object} target + * Event target object. + * @param {String} type + * The type of event. + * @param {Function} listener + * The listener function that processes the event. + */ +function on(target, type, listener) { + if (typeof(listener) !== 'function') + throw new Error(BAD_LISTENER); + + let listeners = observers(target, type); + if (!~listeners.indexOf(listener)) + listeners.push(listener); +} +exports.on = on; + + +var onceWeakMap = new WeakMap(); + + +/** + * Registers an event `listener` that is called only the next time an event + * of the specified `type` is emitted on the given event `target`. + * @param {Object} target + * Event target object. + * @param {String} type + * The type of the event. + * @param {Function} listener + * The listener function that processes the event. + */ +function once(target, type, listener) { + let replacement = function observer(...args) { + off(target, type, observer); + onceWeakMap.delete(listener); + listener.apply(target, args); + }; + onceWeakMap.set(listener, replacement); + on(target, type, replacement); +} +exports.once = once; + +/** + * Execute each of the listeners in order with the supplied arguments. + * All the exceptions that are thrown by listeners during the emit + * are caught and can be handled by listeners of 'error' event. Thrown + * exceptions are passed as an argument to an 'error' event listener. + * If no 'error' listener is registered exception will be logged into an + * error console. + * @param {Object} target + * Event target object. + * @param {String} type + * The type of event. + * @params {Object|Number|String|Boolean} args + * Arguments that will be passed to listeners. + */ +function emit (target, type, ...args) { + emitOnObject(target, type, target, ...args); +} +exports.emit = emit; + +/** + * A variant of emit that allows setting the this property for event listeners + */ +function emitOnObject(target, type, thisArg, ...args) { + let all = observers(target, '*').length; + let state = observers(target, type); + let listeners = state.slice(); + let count = listeners.length; + let index = 0; + + // If error event and there are no handlers (explicit or catch-all) + // then print error message to the console. + if (count === 0 && type === 'error' && all === 0) + console.exception(args[0]); + while (index < count) { + try { + let listener = listeners[index]; + // Dispatch only if listener is still registered. + if (~state.indexOf(listener)) + listener.apply(thisArg, args); + } + catch (error) { + // If exception is not thrown by a error listener and error listener is + // registered emit `error` event. Otherwise dump exception to the console. + if (type !== 'error') emit(target, 'error', error); + else console.exception(error); + } + index++; + } + // Also emit on `"*"` so that one could listen for all events. + if (type !== '*') emit(target, '*', type, ...args); +} +exports.emitOnObject = emitOnObject; + +/** + * Removes an event `listener` for the given event `type` on the given event + * `target`. If no `listener` is passed removes all listeners of the given + * `type`. If `type` is not passed removes all the listeners of the given + * event `target`. + * @param {Object} target + * The event target object. + * @param {String} type + * The type of event. + * @param {Function} listener + * The listener function that processes the event. + */ +function off(target, type, listener) { + let length = arguments.length; + if (length === 3) { + if (onceWeakMap.has(listener)) { + listener = onceWeakMap.get(listener); + onceWeakMap.delete(listener); + } + + let listeners = observers(target, type); + let index = listeners.indexOf(listener); + if (~index) + listeners.splice(index, 1); + } + else if (length === 2) { + observers(target, type).splice(0); + } + else if (length === 1) { + let listeners = event(target); + Object.keys(listeners).forEach(type => delete listeners[type]); + } +} +exports.off = off; + +/** + * Returns a number of event listeners registered for the given event `type` + * on the given event `target`. + */ +function count(target, type) { + return observers(target, type).length; +} +exports.count = count; + +/** + * Registers listeners on the given event `target` from the given `listeners` + * dictionary. Iterates over the listeners and if property name matches name + * pattern `onEventType` and property is a function, then registers it as + * an `eventType` listener on `target`. + * + * @param {Object} target + * The type of event. + * @param {Object} listeners + * Dictionary of listeners. + */ +function setListeners(target, listeners) { + Object.keys(listeners || {}).forEach(key => { + let match = EVENT_TYPE_PATTERN.exec(key); + let type = match && match[1].toLowerCase(); + if (!type) return; + + let listener = listeners[key]; + if (typeof(listener) === 'function') + on(target, type, listener); + }); +} +exports.setListeners = setListeners; |