// inspired by tiny-events:
// https://github.com/ZauberNerd/tiny-events
// MIT License (MIT), Copyright (c) 2014 Björn Brauer
define(['mmirf/util/isArray', 'mmirf/util/toArray'], function(isArray, toArray){
/**
* <blockquote>
* Inspired by <a href="https://github.com/ZauberNerd/tiny-events">tiny-events</a><br>
* MIT License (MIT), Copyright (c) 2014 Björn Brauer
* </blockquote>
*
* Simple Event Emitter / Manager.
*
* Optionally, a list of supported events can be specified:
* if a list of supported events is given, trying the register a listener for an
* unsupported event will cause an error.
*
* @param {any} [thisArg] OPTIONAL
* this context when emitting events:
* if omitted (i.e undefined), the this context will
* be the EventEmitter instance.
* If <code>null</code> the context will be the global
* context.
* Any other value will be taken as-is.
* WARNING: if thisArg is an Array, then events argument
* must also be given, either as an Array, or
* <code>false</code> for disabling the
* supported events restriction
*
* @param {Array<string>} [events] OPTIONAL
* A list of supported event types/names:
* if calling EventEmitter.on() with an unsupported
* type, an error will be thrown.
* If omitted, any event type/names is allowed.
*
* @param {Boolean} [enableHooks] OPTIONAL
* If true, upon emitting events, existing handler functions
* on the <code>thisArg</code> with name "on{lower-case event name}"
* will be triggered, after registered listeners were triggered.
* NOTE: if enabled, the <code>thisArg</code> argument must be undefined or a valid
* context (must not be <code>null</code>).
* NOTE: should not be used in combination with {@link mmir.extensions.EventEmitter#createEventHandlerProperty}
* @constructor
* @memberOf mmir.tools
*/
function EventEmitter(thisArg, events, enableHooks) {
if (typeof events === 'boolean' && typeof enableHooks === 'undefined') {
enableHooks = events;
events = void(0);
}
if (typeof thisArg === 'boolean' && typeof events === 'undefined' && typeof enableHooks === 'undefined') {
enableHooks = thisArg;
thisArg = void(0);
}
if (isArray(thisArg) && typeof events === 'undefined') {
events = thisArg;
thisArg = void(0);
}
this._ctx = typeof thisArg === 'undefined'? this : thisArg;
this._events = events? new Set(events) : null;
this._listeners = new Map();
this._enableHooks = !!enableHooks;
}
/**
* Verifies that an event type can be used with the EventEmitter instance
* (see <code>events</code> argument in constructor).
*
* @param {string} type the event type to check
*
* @throws Error if the event type is not supported by the EventEmitter instance
*/
EventEmitter.prototype.verify = function _on(type) {
if (this._events && !this._events.has(type)) {
throw new Error('unsupported event '+type+', must be one of '+Array.from(this._events));
}
};
/**
* Register a listener for an event type.
*
* Will be ignored, if the listener already is registered for that event type.
*
* If a list of supported event types is specified for the EventEmitter instance,
* and <code>type</code> is not a supported type, an error will be thrown.
*
* @param {string} type the event type
* @param {Function} listener this listener that will be registered
* @return {boolean} <code>true</code> if the listener was registered
*
* @throws Error if the event type is not supported by the EventEmitter instance
*/
EventEmitter.prototype.on = function _on(type, listener) {
this.verify(type);
var l = this._listeners.get(type);
if (!l) {
l = new Set();
this._listeners.set(type, l);
}
if(!l.has(listener)){
l.add(listener);
return true;
}
return false;
};
/**
* Register a listener for an event type to be called one time.
*
* @param {string} type the event type
* @param {Function} listener this listener that will be registered
* @return {boolean} <code>true</code> if the listener was registered
*
* @throws Error if the event type is not supported by the EventEmitter instance
*
* @see #on
*/
EventEmitter.prototype.once = function _once(type, listener) {
var self = this;
function __once() {
var args = toArray(arguments);
self.off(type, __once);
listener.apply(self._ctx, args);
}
__once.listener = listener;
return this.on(type, __once);
};
/**
* Unregister a listener for an event.
*
* @param {string} type the event type
* @param {Function} [listener] OPTIONAL
* The listener to be removed.
* If omitted, all listeners for the event type will be
* removed.
* @return {boolean} <code>true</code> if the listener was removed
*/
EventEmitter.prototype.off = function _off(type, listener) {
var l = this._listeners.get(type);
if (!l) {
return false;
}
if (typeof listener === 'undefined') {
this._listeners.delete(type);
return l.size > 0;
}
if(l.delete(listener)) {
//NOTE remove l, if it becomes empty!
if(l.size === 0) {
this._listeners.delete(type);
}
return true;
}
return false;
};
/**
* Emit an event to registered listeners.
*
* @param {string} type the event type that will be emitted
* @param {...any} [args] OPTIONAL arguments that will be emitted to
* the listeners
* @return {boolean} <code>true</code> if at least one listener was notified
*/
EventEmitter.prototype.emit = function _emit(type) {
var l = this._listeners.get(type);
var hasListeners = l && l.size > 0;
var hook = this._enableHooks && this._ctx['on'+type.toLowerCase()];
if(typeof hook !== 'function'){
hook = null;
}
if(hasListeners || hook){
var size = arguments.length;
var args = new Array(size - 1);
for (var i = 1; i < size; ++i) {
args[i - 1] = arguments[i];
}
if(hasListeners){
l.forEach(function __emit(listener) {
listener.apply(this, args);
}, this._ctx);
}
if(hook){
hook.apply(this._ctx, args);
}
return true;
}
return false;
};
/**
* Check if the EventEmitter instance has any listener registered.
*
* NOTE: only considers hooks, if hooks are enabled and a list of valid events
* was specified (see constructor).
*
* @return {boolean} <code>true</code> if no listener is registered
*/
EventEmitter.prototype.empty = function _empty() {
//ASSERT this._listeners does not contain empty event type collections/Sets
// (that is: no empty Sets will be added & if a Set becomes empty, it will be removed)
if(this._listeners.size === 0){
// -> no listeners are registed via this.on()
//check if any event hooks are present:
if(this._enableHooks && this._ctx && this._events){
var list = toArray(this._events);
for(var i = list.length - 1; i >= 0; --i){
if(typeof this._ctx['on'+list[i].toLowerCase()] === 'function'){
return false; /////////////// EARLY EXIT ////////////////////////
}
}
}
//ASSERT this._listeners does not contain empty event type collections/Sets
// (that is: no empty Sets will be added & if a Set becomes empty, it will be removed)
return true;
} else {
return false;
}
};
/**
* Get a list of event types (if no argument is given) or a list of registered
* listeners for an event type (if event type is given).
*
* @param {string} [type] the event type.
* If omitted returns a list of event types, or
* <code>undefined</code>, if there are no listener
* for any event registered.
* If an event type is specified, returns a list of
* listeners, or <code>undefined</code>, if no
* listeners for the event type or registered.
* @return {Array<Function>|Array<String>} a list of listeners or a list of
* event types or <code>undefined</code>, if there
* are none.
*/
EventEmitter.prototype.get = function _get(type) {
if(typeof type === 'undefined'){
return !this.empty()? toArray(this._listeners.keys()) : void(0);
}
var l = this._listeners.get(type);
//ASSERT if l is present, then l.size > 0
return l? toArray(l) : l;
};
/**
* Check if listener is registered for an event.
*
* @param {string} type the event type
* @param {Function} [listener] OPTIONAL
* The listener to be checked.
* If omitted, it will be checked, if there is any listener
* of the event type.
* @return {boolean} <code>true</code> if the listener (or any if listener
* was not specified) is registered for the event type
*/
EventEmitter.prototype.has = function _has(type, listener) {
var l = this._listeners.get(type);
//ASSERT if l is present, then l.size > 0
return l? (listener? l.has(listener) : true) : false;
};
/**
* Release all resources of EventEmitter instance:
* unregister any listeners and unlink any external references
* (e.g. thisArg if given in constructor).
*
*/
EventEmitter.prototype.destroy = function _destroy() {
this._listeners.forEach(function(l){
l.clear();
});
this._listeners.clear();
if(this._events){
this._events.clear();
this._events = null;
}
this._ctx = null;
};
return EventEmitter;
});