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