/*global define*/ !(function (name, definition) { // Check define var hasDefine = typeof define === 'function', // Check exports hasExports = typeof module !== 'undefined' && module.exports; if (hasDefine) { // AMD Module or CMD Module define('eventproxy_debug', function () {return function () {};}); define(['eventproxy_debug'], definition); } else if (hasExports) { // Node.js Module module.exports = definition(require('debug')('eventproxy')); } else { // Assign to common namespaces or simply the global object (window) this[name] = definition(); } })('EventProxy', function (debug) { debug = debug || function () {}; /*! * refs */ var SLICE = Array.prototype.slice; var CONCAT = Array.prototype.concat; var ALL_EVENT = '__all__'; /** * EventProxy. An implementation of task/event based asynchronous pattern. * A module that can be mixed in to *any object* in order to provide it with custom events. * You may `bind` or `unbind` a callback function to an event; * `trigger`-ing an event fires all callbacks in succession. * Examples: * ```js * var render = function (template, resources) {}; * var proxy = new EventProxy(); * proxy.assign("template", "l10n", render); * proxy.trigger("template", template); * proxy.trigger("l10n", resources); * ``` */ var EventProxy = function () { if (!(this instanceof EventProxy)) { return new EventProxy(); } this._callbacks = {}; this._fired = {}; }; /** * Bind an event, specified by a string name, `ev`, to a `callback` function. * Passing __ALL_EVENT__ will bind the callback to all events fired. * Examples: * ```js * var proxy = new EventProxy(); * proxy.addListener("template", function (event) { * // TODO * }); * ``` * @param {String} eventname Event name. * @param {Function} callback Callback. */ EventProxy.prototype.addListener = function (ev, callback) { debug('Add listener for %s', ev); this._callbacks[ev] = this._callbacks[ev] || []; this._callbacks[ev].push(callback); return this; }; /** * `addListener` alias, `bind` */ EventProxy.prototype.bind = EventProxy.prototype.addListener; /** * `addListener` alias, `on` */ EventProxy.prototype.on = EventProxy.prototype.addListener; /** * `addListener` alias, `subscribe` */ EventProxy.prototype.subscribe = EventProxy.prototype.addListener; /** * Bind an event, but put the callback into head of all callbacks. * @param {String} eventname Event name. * @param {Function} callback Callback. */ EventProxy.prototype.headbind = function (ev, callback) { debug('Add listener for %s', ev); this._callbacks[ev] = this._callbacks[ev] || []; this._callbacks[ev].unshift(callback); return this; }; /** * Remove one or many callbacks. * * - If `callback` is null, removes all callbacks for the event. * - If `eventname` is null, removes all bound callbacks for all events. * @param {String} eventname Event name. * @param {Function} callback Callback. */ EventProxy.prototype.removeListener = function (eventname, callback) { var calls = this._callbacks; if (!eventname) { debug('Remove all listeners'); this._callbacks = {}; } else { if (!callback) { debug('Remove all listeners of %s', eventname); calls[eventname] = []; } else { var list = calls[eventname]; if (list) { var l = list.length; for (var i = 0; i < l; i++) { if (callback === list[i]) { debug('Remove a listener of %s', eventname); list[i] = null; } } } } } return this; }; /** * `removeListener` alias, unbind */ EventProxy.prototype.unbind = EventProxy.prototype.removeListener; /** * Remove all listeners. It equals unbind() * Just add this API for as same as Event.Emitter. * @param {String} event Event name. */ EventProxy.prototype.removeAllListeners = function (event) { return this.unbind(event); }; /** * Bind the ALL_EVENT event */ EventProxy.prototype.bindForAll = function (callback) { this.bind(ALL_EVENT, callback); }; /** * Unbind the ALL_EVENT event */ EventProxy.prototype.unbindForAll = function (callback) { this.unbind(ALL_EVENT, callback); }; /** * Trigger an event, firing all bound callbacks. Callbacks are passed the * same arguments as `trigger` is, apart from the event name. * Listening for `"all"` passes the true event name as the first argument. * @param {String} eventname Event name * @param {Mix} data Pass in data */ EventProxy.prototype.trigger = function (eventname, data) { var list, ev, callback, i, l; var both = 2; var calls = this._callbacks; debug('Emit event %s with data %j', eventname, data); while (both--) { ev = both ? eventname : ALL_EVENT; list = calls[ev]; if (list) { for (i = 0, l = list.length; i < l; i++) { if (!(callback = list[i])) { list.splice(i, 1); i--; l--; } else { var args = []; var start = both ? 1 : 0; for (var j = start; j < arguments.length; j++) { args.push(arguments[j]); } callback.apply(this, args); } } } } return this; }; /** * `trigger` alias */ EventProxy.prototype.emit = EventProxy.prototype.trigger; /** * `trigger` alias */ EventProxy.prototype.fire = EventProxy.prototype.trigger; /** * Bind an event like the bind method, but will remove the listener after it was fired. * @param {String} ev Event name * @param {Function} callback Callback */ EventProxy.prototype.once = function (ev, callback) { var self = this; var wrapper = function () { callback.apply(self, arguments); self.unbind(ev, wrapper); }; this.bind(ev, wrapper); return this; }; var later = (typeof setImmediate !== 'undefined' && setImmediate) || (typeof process !== 'undefined' && process.nextTick) || function (fn) { setTimeout(fn, 0); }; /** * emitLater * make emit async */ EventProxy.prototype.emitLater = function () { var self = this; var args = arguments; later(function () { self.trigger.apply(self, args); }); }; /** * Bind an event, and trigger it immediately. * @param {String} ev Event name. * @param {Function} callback Callback. * @param {Mix} data The data that will be passed to calback as arguments. */ EventProxy.prototype.immediate = function (ev, callback, data) { this.bind(ev, callback); this.trigger(ev, data); return this; }; /** * `immediate` alias */ EventProxy.prototype.asap = EventProxy.prototype.immediate; var _assign = function (eventname1, eventname2, cb, once) { var proxy = this; var argsLength = arguments.length; var times = 0; var flag = {}; // Check the arguments length. if (argsLength < 3) { return this; } var events = SLICE.call(arguments, 0, -2); var callback = arguments[argsLength - 2]; var isOnce = arguments[argsLength - 1]; // Check the callback type. if (typeof callback !== "function") { return this; } debug('Assign listener for events %j, once is %s', events, !!isOnce); var bind = function (key) { var method = isOnce ? "once" : "bind"; proxy[method](key, function (data) { proxy._fired[key] = proxy._fired[key] || {}; proxy._fired[key].data = data; if (!flag[key]) { flag[key] = true; times++; } }); }; var length = events.length; for (var index = 0; index < length; index++) { bind(events[index]); } var _all = function (event) { if (times < length) { return; } if (!flag[event]) { return; } var data = []; for (var index = 0; index < length; index++) { data.push(proxy._fired[events[index]].data); } if (isOnce) { proxy.unbindForAll(_all); } debug('Events %j all emited with data %j', events, data); callback.apply(null, data); }; proxy.bindForAll(_all); }; /** * Assign some events, after all events were fired, the callback will be executed once. * * Examples: * ```js * proxy.all(ev1, ev2, callback); * proxy.all([ev1, ev2], callback); * proxy.all(ev1, [ev2, ev3], callback); * ``` * @param {String} eventname1 First event name. * @param {String} eventname2 Second event name. * @param {Function} callback Callback, that will be called after predefined events were fired. */ EventProxy.prototype.all = function (eventname1, eventname2, callback) { var args = CONCAT.apply([], arguments); args.push(true); _assign.apply(this, args); return this; }; /** * `all` alias */ EventProxy.prototype.assign = EventProxy.prototype.all; /** * Assign the only one 'error' event handler. * @param {Function(err)} callback */ EventProxy.prototype.fail = function (callback) { var that = this; that.once('error', function () { that.unbind(); // put all arguments to the error handler // fail(function(err, args1, args2, ...){}) callback.apply(null, arguments); }); return this; }; /** * A shortcut of ep#emit('error', err) */ EventProxy.prototype.throw = function () { var that = this; that.emit.apply(that, ['error'].concat(SLICE.call(arguments))); }; /** * Assign some events, after all events were fired, the callback will be executed first time. * Then any event that predefined be fired again, the callback will executed with the newest data. * Examples: * ```js * proxy.tail(ev1, ev2, callback); * proxy.tail([ev1, ev2], callback); * proxy.tail(ev1, [ev2, ev3], callback); * ``` * @param {String} eventname1 First event name. * @param {String} eventname2 Second event name. * @param {Function} callback Callback, that will be called after predefined events were fired. */ EventProxy.prototype.tail = function () { var args = CONCAT.apply([], arguments); args.push(false); _assign.apply(this, args); return this; }; /** * `tail` alias, assignAll */ EventProxy.prototype.assignAll = EventProxy.prototype.tail; /** * `tail` alias, assignAlways */ EventProxy.prototype.assignAlways = EventProxy.prototype.tail; /** * The callback will be executed after the event be fired N times. * @param {String} eventname Event name. * @param {Number} times N times. * @param {Function} callback Callback, that will be called after event was fired N times. */ EventProxy.prototype.after = function (eventname, times, callback) { if (times === 0) { callback.call(null, []); return this; } var proxy = this, firedData = []; this._after = this._after || {}; var group = eventname + '_group'; this._after[group] = { index: 0, results: [] }; debug('After emit %s times, event %s\'s listenner will execute', times, eventname); var all = function (name, data) { if (name === eventname) { times--; firedData.push(data); if (times < 1) { debug('Event %s was emit %s, and execute the listenner', eventname, times); proxy.unbindForAll(all); callback.apply(null, [firedData]); } } if (name === group) { times--; proxy._after[group].results[data.index] = data.result; if (times < 1) { debug('Event %s was emit %s, and execute the listenner', eventname, times); proxy.unbindForAll(all); callback.call(null, proxy._after[group].results); } } }; proxy.bindForAll(all); return this; }; /** * The `after` method's helper. Use it will return ordered results. * If you need manipulate result, you need callback * Examples: * ```js * var ep = new EventProxy(); * ep.after('file', files.length, function (list) { * // Ordered results * }); * for (var i = 0; i < files.length; i++) { * fs.readFile(files[i], 'utf-8', ep.group('file')); * } * ``` * @param {String} eventname Event name, shoule keep consistent with `after`. * @param {Function} callback Callback function, should return the final result. */ EventProxy.prototype.group = function (eventname, callback) { var that = this; var group = eventname + '_group'; var index = that._after[group].index; that._after[group].index++; return function (err, data) { if (err) { // put all arguments to the error handler return that.emit.apply(that, ['error'].concat(SLICE.call(arguments))); } that.emit(group, { index: index, // callback(err, args1, args2, ...) result: callback ? callback.apply(null, SLICE.call(arguments, 1)) : data }); }; }; /** * The callback will be executed after any registered event was fired. It only executed once. * @param {String} eventname1 Event name. * @param {String} eventname2 Event name. * @param {Function} callback The callback will get a map that has data and eventname attributes. */ EventProxy.prototype.any = function () { var proxy = this, callback = arguments[arguments.length - 1], events = SLICE.call(arguments, 0, -1), _eventname = events.join("_"); debug('Add listenner for Any of events %j emit', events); proxy.once(_eventname, callback); var _bind = function (key) { proxy.bind(key, function (data) { debug('One of events %j emited, execute the listenner'); proxy.trigger(_eventname, {"data": data, eventName: key}); }); }; for (var index = 0; index < events.length; index++) { _bind(events[index]); } }; /** * The callback will be executed when the event name not equals with assigned event. * @param {String} eventname Event name. * @param {Function} callback Callback. */ EventProxy.prototype.not = function (eventname, callback) { var proxy = this; debug('Add listenner for not event %s', eventname); proxy.bindForAll(function (name, data) { if (name !== eventname) { debug('listenner execute of event %s emit, but not event %s.', name, eventname); callback(data); } }); }; /** * Success callback wrapper, will handler err for you. * * ```js * fs.readFile('foo.txt', ep.done('content')); * * // equal to => * * fs.readFile('foo.txt', function (err, content) { * if (err) { * return ep.emit('error', err); * } * ep.emit('content', content); * }); * ``` * * ```js * fs.readFile('foo.txt', ep.done('content', function (content) { * return content.trim(); * })); * * // equal to => * * fs.readFile('foo.txt', function (err, content) { * if (err) { * return ep.emit('error', err); * } * ep.emit('content', content.trim()); * }); * ``` * @param {Function|String} handler, success callback or event name will be emit after callback. * @return {Function} */ EventProxy.prototype.done = function (handler, callback) { var that = this; return function (err, data) { if (err) { // put all arguments to the error handler return that.emit.apply(that, ['error'].concat(SLICE.call(arguments))); } // callback(err, args1, args2, ...) var args = SLICE.call(arguments, 1); if (typeof handler === 'string') { // getAsync(query, ep.done('query')); // or // getAsync(query, ep.done('query', function (data) { // return data.trim(); // })); if (callback) { // only replace the args when it really return a result return that.emit(handler, callback.apply(null, args)); } else { // put all arguments to the done handler //ep.done('some'); //ep.on('some', function(args1, args2, ...){}); return that.emit.apply(that, [handler].concat(args)); } } // speed improve for mostly case: `callback(err, data)` if (arguments.length <= 2) { return handler(data); } // callback(err, args1, args2, ...) handler.apply(null, args); }; }; /** * make done async * @return {Function} delay done */ EventProxy.prototype.doneLater = function (handler, callback) { var _doneHandler = this.done(handler, callback); return function (err, data) { var args = arguments; later(function () { _doneHandler.apply(null, args); }); }; }; /** * Create a new EventProxy * Examples: * ```js * var ep = EventProxy.create(); * ep.assign('user', 'articles', function(user, articles) { * // do something... * }); * // or one line ways: Create EventProxy and Assign * var ep = EventProxy.create('user', 'articles', function(user, articles) { * // do something... * }); * ``` * @return {EventProxy} EventProxy instance */ EventProxy.create = function () { var ep = new EventProxy(); var args = CONCAT.apply([], arguments); if (args.length) { var errorHandler = args[args.length - 1]; var callback = args[args.length - 2]; if (typeof errorHandler === 'function' && typeof callback === 'function') { args.pop(); ep.fail(errorHandler); } ep.assign.apply(ep, args); } return ep; }; // Backwards compatibility EventProxy.EventProxy = EventProxy; return EventProxy; });