/* Event constructor for pub/sub, eventEmitter, whateve. dandavis 2012-2013. v2.0 [CCBY3] based on node.js's EventEmiter (http://nodejs.org/api/events.html), but smaller, faster and with several enhancements: // better argument passing: -pass an object of many events, named by keys, values by functions or arrays of functions to on, off, once, etc to toggle a whole group of functionality -pass a RegExp to emit() to raise many events at once -pass an array or comma-seperated string list of event names to on, off, once, many, until, and while, instead of just one string name. -pass an array of methods to bind/unbind many handlers under one key, or under many keys each when using an array of names. -pass a string instead of a function to on/off/once/many/until/while, and it will be automatically converted into a Function with 3 formal parameters named a, b, and c. -pass a bundle of data when you call on/once/many/until/while, and fetch that data inside the event as this.$data; -pass many arguments to emit(), and they are all passed in-order to the handler function // smarter returns -return false to cancel further events by that event name -return this.STOP to cancel further events by that name or aliases or the rest of a RegExp-defined sub-pool of all names -return this.OFF to unbind the particular event in-place, further events will fire as queued. you can also use .off(this.$name, arguments.callee) and return something else. // more than on and off: -many() extends the idea of once(), arguments[1] is the # of time to fire -until() is like many(), but accept a function that returns true or false (ish) to unsubscribe the handler -while() is like until(), but instead of removing the event, it fires the event after the conditional has been evaluated true-ish // magic events and monitoring: -subscribe to "*" to subscribe to all emits in one handler -subscribe to "newListener" to get a notification when adding further subscriptions -subscribe to "removeListener" to get a notification when removing further subscriptions -use this.$name to get the event name, and this.$data to get any data passed at bind time */ function Events() { this.pool = {}; this.SEP = sep = /\s*,\s*/; //unique 'constant' flags: this.STOP = ["STOP"]; this.OFF = ["OFF"]; this.whitelist = null; this.extend = function() { return Events.extend.apply(this, [this].concat([].slice.call(arguments))); }; this.extend.apply(this, arguments); return this; } //static methods for internal and utility usage: Events.extend = function extend(base) { [].slice.call(arguments, 1).forEach(function(updated) { if(!updated || Object(updated)!==updated){return;} Object.keys(updated).map(function(key) { if (Events.isObject(updated[key]) && base[key]) { base[key] = extend(base[key], updated[key]); } else { base[key] = updated[key]; } }, this); }, this); return base; }; Events.listenerCount=function(emitter, event){ return Event.keys(emmiter.pool[event]||{}).length || false; }; Events.isArray=Array.isArray||function(value){return value.join && {}.toString.call(value)==="[object Array]";}; Events.isObject=function(value){return typeof value === 'object' && !value.splice && !value.toGMTString; }; Events.toFunction = function F(f) { return f.split ? Function("a,b,c", f.replace(/^===?/g, "return ")) : f; }; //fewer holes to plug for old browsers, still need to cover array prototypes. Events.keys = Object.keys || function(ob) { var r = [], i = 0; for (var z in ob) { if ({}.hasOwnProperty.call(ob, z)) { r[i++] = z; } } return r; }; //prototype methods (function() { var F = Events.toFunction, O=Events.prototype = { on: function _on(events, handler, _data) { return this.until(events, isNaN, handler, false, _data); }, onObject: function _onObject(bundle, _data) { Events.keys(bundle).forEach(function(name) { this.until(name, isNaN, bundle[name], false, _data); }, this); return this; }, offObject: function _offObject(bundle) { Events.keys(bundle).forEach(function(name) { this.off(name, bundle[name]); }, this); return this; }, once: function _once(events, handler, _data) { return this.until(events, isNaN, function(){ handler.apply(this, arguments); return this.OFF; }, false, _data); }, many: function _many(events, numTimesToFire, handler, _data) { return this.until(events, function _many() { return --numTimesToFire < 1; }, handler, true, _data); }, until: function _until(events, condition, handlers, _blnWhileMode, _data) { if (!handlers && Events.isObject(events)) { return this.onObject(events); } var that = this, eventGroups= String(events).split(this.SEP), all = this.pool.newListener; if (!Events.isArray(handlers)) { handlers = [handlers]; } eventGroups.forEach(function(event) { var cat = that.pool[event] || (that.pool[event] = []); handlers.forEach(function(handler) { if (all) { this.emit("newListener", event, handler); } if (this.whitelist && this.whitelist[event] && this.whitelist[event]-- < 1) { console.error(new RangeError("channel " + event + " has reached limit")); return; } var executor = _blnWhileMode ? function executor_whileMode() { if (F(condition).apply(that, arguments)) { return F(handler).apply(that, arguments); } } : (condition === isNaN ? F(handler) : function executor() { var rez = F(handler).apply(that, arguments); if (F(condition).apply(that, arguments)) { that.off(event, _until); } return rez; }); executor.$orig = handler; executor.$name = event; executor.$data = _data; cat.push(executor); }, this); //next handler }, this); //next event return this; }, 'while': function _while(events, condition, handler, _data) { return this.until(events, condition, handler, true, _data); }, emit: function _emit(events) { var emitThis = this, args = [].slice.call(arguments, 1), stop = false, eventGroups = events.test ? Events.keys(this.pool).filter(/./.test, events) : String(events).split(this.SEP); if( this.pool["*"] ) { eventGroups.push("*"); } //also emit on all channel (if needed) eventGroups.some(function(strEventName, x) { x=this.pool[strEventName]; if(x) x.some(function _emitInvoker(fn) { this.$data=fn.$data; this.$name=strEventName || fn.$name; var rez = fn.apply( emitThis, args ); delete this.$data; delete this.$name; if(!rez){return;} switch(rez){ case false : return true; case this.STOP: return stop = true; case this.OFF : return this.off(strEventName, fn); } }, this ); //end event runner (some) return stop; }, this ); //end event pool splitter (some) return this; }, off: function _off(events, handlers) { if (!handlers && Events.isObject(events) ) { return this.offObject(events); } String(events).split(this.SEP).map(function(event) { if (!event || !handlers) { return; } if (!Events.isArray(handlers)) { handlers = [handlers]; } handlers.forEach(function(handler) { var n = event, hit = this.pool[event] || [], pos = -1; pos = hit.map(function(a) { return a.$orig == this; }, handler).indexOf(true); if (pos > -1) { hit.splice(pos, 1); if (hit.length === 0) { delete this.pool[event]; } } if (this.pool.removeListener && event !== "removeListener") { this.emit("removeListener", event, handler); } }, this); //next handler }, this); //next event return this; }, bind: function(){ var args=[].slice.call(arguments); return function(){ return E.emit.apply(E, args.concat([].slice.call(arguments)) ); } }, getEvents: function() { return Events.keys(this.pool); }, removeAllListeners: function(name){ if(!name){ Event.keys(this.pool).forEach(function(k){delete this[k]}, this.pool); return this; } String([].concat.apply([],arguments)).split(this.SEP).filter(Boolean).map(function(k){delete this[k];}, this.pool); return this; }, getListeners: function(name) { if (!name) { return Events.keys(this.pool).map(function(a) { return this[a]; }, this.pool).reduce(function(a, b) { return a.concat(b); }); } if (name.test) { return Events.keys(this.pool).filter(/./.test, name).map(function(k) { return this[k]; }, this.pool).filter(Boolean); } return name.join ? name.map(function(k) { return this[k]; }, this.pool).filter(Boolean) : this.pool[name]; }, hasEventListener: function(name){ return !! this.getListeners(name).length ; } }; // aliases for node and popular lib compat and dev-familiarity: O.addListener= O.addEventListener=O.on; O.detach= O.removeListener= O.removeEventListener=O.off; O.invoke= O.fire= O.raise= O.trigger= O.dispatch= O.dispatchEvent=O.emit; }()); /* //unit tests Events._runTests = function testEvents(constructor) { ee = new Events(); return [{ title: "add event by string name, adds to pool", test: function() { var total = 0; function counta() { total++; }; this.on("a", counta); return this.pool.a.length }, expects: 1 }, { title: "add event by string name and string of code, adds to and remove from pool", test: function() { self.total = 0; this.on("g", "self.total++"); this.off("g", "self.total++"); return !this.pool.g; }, expects: true }, { title: "add events via Object of named methods, adds all to pool", test: function() { var total = 0, bundle = { a1: function counta() { total++; }, b1: function countb() { total++; } }; this.on(bundle); return this.pool.a1.length === 1 && this.pool.b1.length === 1; }, expects: true }, { title: "add events by array with two string names, adds both to pool", test: function() { var total = 0; function counta() { total++; }; this.on(["a", "b"], counta); return this.pool.a.length === 2 && this.pool.b.length === 1; }, expects: true }, { title: "add events by string CSV with two string names, adds both to pool", test: function() { var total = 0; function counta() { total++; }; this.on("a,b", counta); return this.pool.a.length === 3 && this.pool.b.length === 2; }, expects: true }, { title: "getting the event name in the handler", test: function() { var s = "unknown"; function counta() { s = this.$name; }; this.on("somename", counta); this.emit("somename"); return s; }, expects: "somename" }, { title: "getting the event early data in the handler", test: function() { var s = "unknown"; function demo(arg1) { s = this.$data.whatever+arg1; }; this.on("myevent", demo, { whatever: "yay " }); this.emit("myevent", "me"); return s; }, expects: "yay me" }, { title: "add two events under two string names, and two different handlers with an array of handlers", test: function() { var total = 0; function counta() { total++; }; function countb() { total++; }; this.on("aa, bb", [counta, countb]); return this.pool.aa.length === 2 && this.pool.bb.length === 2; }, expects: true }, { title: "using while() to fire an event sometimes", test: function() { self.total = 0; function condition() { return total < 3; }; function action() { total++; }; this. while ("chk", condition, action); this.emit("chk"); this.emit("chk"); this.emit("chk"); this.emit("chk"); this.emit("chk"); this.emit("chk"); return total; }, expects: 3 }, { title: "subscribing to newListener() raises an event upon adding a new event", test: function() { var total = 0; function spy(evt, fn) { if (evt == "c" && fn.name === "specific") { total++; this.off("newListener", spy); } } this.on("newListener", spy); this.on("c", function specific() {}); this.off("c", spy) return total === 1; }, expects: true }, { title: "subscribing to removeListener() raises an event upon removing an event", test: function() { var that = this; var total = 0; function spy(evt, fn) { if (evt == "c" && fn.name === "specific") { total++; that.off("newListener", spy); } } this.on("removeListener", spy); this.on("c", function specific() {}); this.off("c", function specific() {}); return total === 1; }, expects: true }, { title: "raising a single event by name does what it's supposed to do", test: function() { var totals = 4; function capture(data) { totals += data; } this.on("x", capture); this.emit("x", 6); this.off("x", capture); return totals; }, expects: 10 }, { title: "returning false when raising a single event by name stops further events from firing", test: function() { var total = 0; function capture(data) { total += 1; } function captureStop(data) { total += 1; return false; } this.on("y", capture); //+1 this.on("y", captureStop); // +1 this.on("y", capture); // +0 this.emit("y", 0); //this.off("y", capture); //this.off("y", captureStop); //this.off("y", capture); return total }, expects: 2 }, { title: "raising two events by regexp does what it's supposed to do", test: function() { var total = 4; function capture(data) { total += data; } this.on("a,b", capture); this.emit(/^\w{1}$/, 6); this.off("a", capture); this.off("b", capture); return total === 16; }, expects: true }, { title: "running two off()s as csv removes the events", test: function() { var total = 4; function capture(data) { total += data; } this.on("a,b", capture); this.emit(/^\w{1}$/, 6); this.off("a,b", capture); return this.pool.a.indexOf(capture) === -1 && this.pool.b.indexOf(capture) === -1; }, expects: true }, { title: "running two off()s from an array of strings removes the events", test: function() { var total = 4; function capture(data) { total += data; } this.on(["a", "b"], capture); this.emit(/^\w{1}$/, 6); this.off(["a", "b"], capture); return this.pool.a.indexOf(capture) === -1 && this.pool.b.indexOf(capture) === -1; }, expects: true }, { title: "done", test: function(i, r) { document.title = "done running " + i + " unit tests, " + r.filter(function(a) { return a.status === true; }).length + " were good" }, expects: 0 } ].map(function assert(o, x, i) { console.log("testing", o.title) return o.status = ((x = o.test.call(this, x, i)) === o.expects) || x, o; }, ee); } // run unit tests: var r = Events._runTests(); HTML.innerHTML = r.map(function(a) { return a.title.big().fontcolor(a.status === true ? "green" : "red") + " \t " + a.status + " / " + a.expects; }).join("