/**
* @name pc.events
* @namespace
* @description Namespace for event functions. Use these functions to attach events to an object.
* @example
* var obj = { };
* pc.events.attach(obj);
*
* // subscribe to an event
* obj.on('hello', function(str) {
* console.log('event hello is fired', str);
* });
*
* // fire event
* obj.fire('hello', 'world');
*/
pc.events = {
/**
* @function
* @name pc.events.attach
* @description Attach event methods 'on', 'off', 'fire', 'once' and 'hasEvent' to the target object
* @param {Object} target The object to add events to.
* @returns {Object} The target object
* @example
* var obj = { };
* pc.events.attach(obj);
*/
attach: function (target) {
var ev = pc.events;
target.on = ev.on;
target.off = ev.off;
target.fire = ev.fire;
target.once = ev.once;
target.hasEvent = ev.hasEvent;
target._callbacks = { };
target._callbackActive = { };
return target;
},
/**
* @function
* @name pc.events.on
* @description Attach an event handler to an event
* @param {String} name Name of the event to bind the callback to
* @param {Function} callback Function that is called when event is fired. Note the callback is limited to 8 arguments.
* @param {Object} [scope] Object to use as 'this' when the event is fired, defaults to current this
* @returns {*} 'this' for chaining
* @example
* obj.on('test', function (a, b) {
* console.log(a + b);
* });
* obj.fire('test', 1, 2); // prints 3 to the console
*/
on: function (name, callback, scope) {
if (!name || typeof name !== 'string' || !callback)
return this;
if (!this._callbacks[name])
this._callbacks[name] = [];
if (this._callbackActive[name] && this._callbackActive[name] === this._callbacks[name])
this._callbackActive[name] = this._callbackActive[name].slice();
this._callbacks[name].push({
callback: callback,
scope: scope || this
});
return this;
},
/**
* @function
* @name pc.events.off
* @description Detach an event handler from an event. If callback is not provided then all callbacks are unbound from the event,
* if scope is not provided then all events with the callback will be unbound.
* @param {String} [name] Name of the event to unbind
* @param {Function} [callback] Function to be unbound
* @param {Object} [scope] Scope that was used as the this when the event is fired
* @returns {*} 'this' for chaining
* @example
* var handler = function () {
* };
* obj.on('test', handler);
*
* obj.off(); // Removes all events
* obj.off('test'); // Removes all events called 'test'
* obj.off('test', handler); // Removes all handler functions, called 'test'
* obj.off('test', handler, this); // Removes all hander functions, called 'test' with scope this
*/
off: function (name, callback, scope) {
if (name) {
if (this._callbackActive[name] && this._callbackActive[name] === this._callbacks[name])
this._callbackActive[name] = this._callbackActive[name].slice();
} else {
for (var key in this._callbackActive) {
if (!this._callbacks[key])
continue;
if (this._callbacks[key] !== this._callbackActive[key])
continue;
this._callbackActive[key] = this._callbackActive[key].slice();
}
}
if (!name) {
this._callbacks = { };
} else if (!callback) {
if (this._callbacks[name])
this._callbacks[name] = [];
} else {
var events = this._callbacks[name];
if (!events)
return this;
var count = events.length;
for (var i = 0; i < count; i++) {
if (events[i].callback !== callback)
continue;
if (scope && events[i].scope !== scope)
continue;
events[i--] = events[--count];
}
events.length = count;
}
return this;
},
// ESLint rule disabled here as documenting arg1, arg2...argN as [...] rest
// arguments is preferable to documenting each one individually.
/* eslint-disable valid-jsdoc */
/**
* @function
* @name pc.events.fire
* @description Fire an event, all additional arguments are passed on to the event listener
* @param {Object} name Name of event to fire
* @param {*} [...] Arguments that are passed to the event handler
* @returns {*} 'this' for chaining
* @example
* obj.fire('test', 'This is the message');
*/
/* eslint-enable valid-jsdoc */
fire: function (name, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) {
if (!name || !this._callbacks[name])
return this;
var callbacks;
if (!this._callbackActive[name]) {
this._callbackActive[name] = this._callbacks[name];
} else {
if (this._callbackActive[name] === this._callbacks[name])
this._callbackActive[name] = this._callbackActive[name].slice();
callbacks = this._callbacks[name].slice();
}
// TODO: What does callbacks do here?
// In particular this condition check looks wrong: (i < (callbacks || this._callbackActive[name]).length)
// Because callbacks is not an integer
// eslint-disable-next-line no-unmodified-loop-condition
for (var i = 0; (callbacks || this._callbackActive[name]) && (i < (callbacks || this._callbackActive[name]).length); i++) {
var evt = (callbacks || this._callbackActive[name])[i];
evt.callback.call(evt.scope, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8);
if (evt.callback.once) {
var ind = this._callbacks[name].indexOf(evt);
if (ind !== -1) {
if (this._callbackActive[name] === this._callbacks[name])
this._callbackActive[name] = this._callbackActive[name].slice();
this._callbacks[name].splice(ind, 1);
}
}
}
if (!callbacks)
this._callbackActive[name] = null;
return this;
},
/**
* @function
* @name pc.events.once
* @description Attach an event handler to an event. This handler will be removed after being fired once.
* @param {String} name Name of the event to bind the callback to
* @param {Function} callback Function that is called when event is fired. Note the callback is limited to 8 arguments.
* @param {Object} [scope] Object to use as 'this' when the event is fired, defaults to current this
* @returns {*} 'this' for chaining
* @example
* obj.once('test', function (a, b) {
* console.log(a + b);
* });
* obj.fire('test', 1, 2); // prints 3 to the console
* obj.fire('test', 1, 2); // not going to get handled
*/
once: function (name, callback, scope) {
callback.once = true;
this.on(name, callback, scope);
return this;
},
/**
* @function
* @name pc.events.hasEvent
* @description Test if there are any handlers bound to an event name
* @param {String} name The name of the event to test
* @returns {Boolean} true if the object has handlers bound to the specified event name.
* @example
* obj.on('test', function () { }); // bind an event to 'test'
* obj.hasEvent('test'); // returns true
* obj.hasEvent('hello'); // returns false
*/
hasEvent: function (name) {
return (this._callbacks[name] && this._callbacks[name].length !== 0) || false;
}
};