1 /*jslint onevar:true, undef:true, newcap:true, regexp:true, bitwise:true, maxerr:50, indent:4, white:false, nomen:false, plusplus:false */ 2 /*global window:false, global:false*/ 3 4 /*!! 5 * JS Signals <http://millermedeiros.github.com/js-signals/> 6 * Released under the MIT license <http://www.opensource.org/licenses/mit-license.php> 7 * @author Miller Medeiros <http://millermedeiros.com/> 8 * @version 0.6.1 9 * @build 178 (05/03/2011 01:07 AM) 10 */ 11 (function(global){ 12 13 /** 14 * @namespace Signals Namespace - Custom event/messaging system based on AS3 Signals 15 * @name signals 16 */ 17 var signals = /** @lends signals */{ 18 /** 19 * Signals Version Number 20 * @type String 21 * @const 22 */ 23 VERSION : '0.6.1' 24 }; 25 26 // SignalBinding ------------------------------------------------- 27 //================================================================ 28 29 /** 30 * Object that represents a binding between a Signal and a listener function. 31 * <br />- <strong>This is an internal constructor and shouldn't be called by regular users.</strong> 32 * <br />- inspired by Joa Ebert AS3 SignalBinding and Robert Penner's Slot classes. 33 * @author Miller Medeiros 34 * @constructor 35 * @internal 36 * @name signals.SignalBinding 37 * @param {signals.Signal} signal Reference to Signal object that listener is currently bound to. 38 * @param {Function} listener Handler function bound to the signal. 39 * @param {boolean} isOnce If binding should be executed just once. 40 * @param {Object} [listenerContext] Context on which listener will be executed (object that should represent the `this` variable inside listener function). 41 * @param {Number} [priority] The priority level of the event listener. (default = 0). 42 */ 43 function SignalBinding(signal, listener, isOnce, listenerContext, priority){ 44 45 /** 46 * Handler function bound to the signal. 47 * @type Function 48 * @private 49 */ 50 this._listener = listener; 51 52 /** 53 * If binding should be executed just once. 54 * @type boolean 55 * @private 56 */ 57 this._isOnce = isOnce; 58 59 /** 60 * Context on which listener will be executed (object that should represent the `this` variable inside listener function). 61 * @memberOf signals.SignalBinding.prototype 62 * @name context 63 * @type Object|undefined|null 64 */ 65 this.context = listenerContext; 66 67 /** 68 * Reference to Signal object that listener is currently bound to. 69 * @type signals.Signal 70 * @private 71 */ 72 this._signal = signal; 73 74 /** 75 * Listener priority 76 * @type Number 77 * @private 78 */ 79 this._priority = priority || 0; 80 } 81 82 SignalBinding.prototype = /** @lends signals.SignalBinding.prototype */ { 83 84 /** 85 * If binding is active and should be executed. 86 * @type boolean 87 */ 88 active : true, 89 90 /** 91 * Call listener passing arbitrary parameters. 92 * <p>If binding was added using `Signal.addOnce()` it will be automatically removed from signal dispatch queue, this method is used internally for the signal dispatch.</p> 93 * @param {Array} [paramsArr] Array of parameters that should be passed to the listener 94 * @return {*} Value returned by the listener. 95 */ 96 execute : function(paramsArr){ 97 var r; 98 if(this.active){ 99 r = this._listener.apply(this.context, paramsArr); 100 if(this._isOnce){ 101 this.detach(); 102 } 103 } 104 return r; 105 }, 106 107 /** 108 * Detach binding from signal. 109 * - alias to: mySignal.remove(myBinding.getListener()); 110 * @return {Function} Handler function bound to the signal. 111 */ 112 detach : function(){ 113 return this._signal.remove(this._listener); 114 }, 115 116 /** 117 * @return {Function} Handler function bound to the signal. 118 */ 119 getListener : function(){ 120 return this._listener; 121 }, 122 123 /** 124 * Remove binding from signal and destroy any reference to external Objects (destroy SignalBinding object). 125 * <p><strong>IMPORTANT:</strong> calling methods on the binding instance after calling dispose will throw errors.</p> 126 */ 127 dispose : function(){ 128 this.detach(); 129 this._destroy(); 130 }, 131 132 /** 133 * Delete all instance properties 134 * @private 135 */ 136 _destroy : function(){ 137 delete this._signal; 138 delete this._isOnce; 139 delete this._listener; 140 delete this.context; 141 }, 142 143 /** 144 * @return {boolean} If SignalBinding will only be executed once. 145 */ 146 isOnce : function(){ 147 return this._isOnce; 148 }, 149 150 /** 151 * @return {string} String representation of the object. 152 */ 153 toString : function(){ 154 return '[SignalBinding isOnce: '+ this._isOnce +', active: '+ this.active +']'; 155 } 156 157 }; 158 159 /*global signals:true, SignalBinding:true*/ 160 161 // Signal -------------------------------------------------------- 162 //================================================================ 163 164 /** 165 * Custom event broadcaster 166 * <br />- inspired by Robert Penner's AS3 Signals. 167 * @author Miller Medeiros 168 * @constructor 169 */ 170 signals.Signal = function(){ 171 /** 172 * @type Array.<SignalBinding> 173 * @private 174 */ 175 this._bindings = []; 176 }; 177 178 signals.Signal.prototype = { 179 180 /** 181 * @type boolean 182 * @private 183 */ 184 _shouldPropagate : true, 185 186 /** 187 * If Signal is active and should broadcast events. 188 * <p><strong>IMPORTANT:</strong> Setting this property during a dispatch will only affect the next dispatch, if you want to stop the propagation of a signal use `halt()` instead.</p> 189 * @type boolean 190 */ 191 active : true, 192 193 /** 194 * @param {Function} listener 195 * @param {boolean} isOnce 196 * @param {Object} [scope] 197 * @param {Number} [priority] 198 * @return {SignalBinding} 199 * @private 200 */ 201 _registerListener : function(listener, isOnce, scope, priority){ 202 203 if(typeof listener !== 'function'){ 204 throw new Error('listener is a required param of add() and addOnce() and should be a Function.'); 205 } 206 207 var prevIndex = this._indexOfListener(listener), 208 binding; 209 210 if(prevIndex !== -1){ //avoid creating a new Binding for same listener if already added to list 211 binding = this._bindings[prevIndex]; 212 if(binding.isOnce() !== isOnce){ 213 throw new Error('You cannot add'+ (isOnce? '' : 'Once') +'() then add'+ (!isOnce? '' : 'Once') +'() the same listener without removing the relationship first.'); 214 } 215 } else { 216 binding = new SignalBinding(this, listener, isOnce, scope, priority); 217 this._addBinding(binding); 218 } 219 220 return binding; 221 }, 222 223 /** 224 * @param {Function} binding 225 * @private 226 */ 227 _addBinding : function(binding){ 228 //simplified insertion sort 229 var n = this._bindings.length; 230 do { --n; } while (this._bindings[n] && binding._priority <= this._bindings[n]._priority); 231 this._bindings.splice(n+1, 0, binding); 232 }, 233 234 /** 235 * @param {Function} listener 236 * @return {number} 237 * @private 238 */ 239 _indexOfListener : function(listener){ 240 var n = this._bindings.length; 241 while(n--){ 242 if(this._bindings[n]._listener === listener){ 243 return n; 244 } 245 } 246 return -1; 247 }, 248 249 /** 250 * Add a listener to the signal. 251 * @param {Function} listener Signal handler function. 252 * @param {Object} [scope] Context on which listener will be executed (object that should represent the `this` variable inside listener function). 253 * @param {Number} [priority] The priority level of the event listener. Listeners with higher priority will be executed before listeners with lower priority. Listeners with same priority level will be executed at the same order as they were added. (default = 0) 254 * @return {SignalBinding} An Object representing the binding between the Signal and listener. 255 */ 256 add : function(listener, scope, priority){ 257 return this._registerListener(listener, false, scope, priority); 258 }, 259 260 /** 261 * Add listener to the signal that should be removed after first execution (will be executed only once). 262 * @param {Function} listener Signal handler function. 263 * @param {Object} [scope] Context on which listener will be executed (object that should represent the `this` variable inside listener function). 264 * @param {Number} [priority] The priority level of the event listener. Listeners with higher priority will be executed before listeners with lower priority. Listeners with same priority level will be executed at the same order as they were added. (default = 0) 265 * @return {SignalBinding} An Object representing the binding between the Signal and listener. 266 */ 267 addOnce : function(listener, scope, priority){ 268 return this._registerListener(listener, true, scope, priority); 269 }, 270 271 /** 272 * Remove a single listener from the dispatch queue. 273 * @param {Function} listener Handler function that should be removed. 274 * @return {Function} Listener handler function. 275 */ 276 remove : function(listener){ 277 if(typeof listener !== 'function'){ 278 throw new Error('listener is a required param of remove() and should be a Function.'); 279 } 280 281 var i = this._indexOfListener(listener); 282 if(i !== -1){ 283 this._bindings[i]._destroy(); //no reason to a SignalBinding exist if it isn't attached to a signal 284 this._bindings.splice(i, 1); 285 } 286 return listener; 287 }, 288 289 /** 290 * Remove all listeners from the Signal. 291 */ 292 removeAll : function(){ 293 var n = this._bindings.length; 294 while(n--){ 295 this._bindings[n]._destroy(); 296 } 297 this._bindings.length = 0; 298 }, 299 300 /** 301 * @return {number} Number of listeners attached to the Signal. 302 */ 303 getNumListeners : function(){ 304 return this._bindings.length; 305 }, 306 307 /** 308 * Stop propagation of the event, blocking the dispatch to next listeners on the queue. 309 * <p><strong>IMPORTANT:</strong> should be called only during signal dispatch, calling it before/after dispatch won't affect signal broadcast.</p> 310 * @see signals.Signal.prototype.disable 311 */ 312 halt : function(){ 313 this._shouldPropagate = false; 314 }, 315 316 /** 317 * Dispatch/Broadcast Signal to all listeners added to the queue. 318 * @param {...*} [params] Parameters that should be passed to each handler. 319 */ 320 dispatch : function(params){ 321 if(! this.active){ 322 return; 323 } 324 325 var paramsArr = Array.prototype.slice.call(arguments), 326 bindings = this._bindings.slice(), //clone array in case add/remove items during dispatch 327 n = this._bindings.length; 328 329 this._shouldPropagate = true; //in case `halt` was called before dispatch or during the previous dispatch. 330 331 //execute all callbacks until end of the list or until a callback returns `false` or stops propagation 332 //reverse loop since listeners with higher priority will be added at the end of the list 333 do { n--; } while (bindings[n] && this._shouldPropagate && bindings[n].execute(paramsArr) !== false); 334 }, 335 336 /** 337 * Remove all bindings from signal and destroy any reference to external objects (destroy Signal object). 338 * <p><strong>IMPORTANT:</strong> calling any method on the signal instance after calling dispose will throw errors.</p> 339 */ 340 dispose : function(){ 341 this.removeAll(); 342 delete this._bindings; 343 }, 344 345 /** 346 * @return {string} String representation of the object. 347 */ 348 toString : function(){ 349 return '[Signal active: '+ this.active +' numListeners: '+ this.getNumListeners() +']'; 350 } 351 352 }; 353 354 global.signals = signals; 355 356 }(window || global || this));