1 /*jslint onevar:true, undef:true, newcap:true, regexp:true, bitwise:true, maxerr:50, indent:4, white:false, nomen:false, plusplus:false */ 2 /*global define:false, require:false, exports:false, module:false, signals:false */ 3 4 /** @license 5 * JS Signals <http://millermedeiros.github.com/js-signals/> 6 * Released under the MIT license 7 * Author: Miller Medeiros 8 * Version: 1.0.0 - Build: 268 (2012/11/29 05:48 PM) 9 */ 10 11 (function(global){ 12 13 // SignalBinding ------------------------------------------------- 14 //================================================================ 15 16 /** 17 * Object that represents a binding between a Signal and a listener function. 18 * <br />- <strong>This is an internal constructor and shouldn't be called by regular users.</strong> 19 * <br />- inspired by Joa Ebert AS3 SignalBinding and Robert Penner's Slot classes. 20 * @author Miller Medeiros 21 * @constructor 22 * @internal 23 * @name SignalBinding 24 * @param {Signal} signal Reference to Signal object that listener is currently bound to. 25 * @param {Function} listener Handler function bound to the signal. 26 * @param {boolean} isOnce If binding should be executed just once. 27 * @param {Object} [listenerContext] Context on which listener will be executed (object that should represent the `this` variable inside listener function). 28 * @param {Number} [priority] The priority level of the event listener. (default = 0). 29 */ 30 function SignalBinding(signal, listener, isOnce, listenerContext, priority) { 31 32 /** 33 * Handler function bound to the signal. 34 * @type Function 35 * @private 36 */ 37 this._listener = listener; 38 39 /** 40 * If binding should be executed just once. 41 * @type boolean 42 * @private 43 */ 44 this._isOnce = isOnce; 45 46 /** 47 * Context on which listener will be executed (object that should represent the `this` variable inside listener function). 48 * @memberOf SignalBinding.prototype 49 * @name context 50 * @type Object|undefined|null 51 */ 52 this.context = listenerContext; 53 54 /** 55 * Reference to Signal object that listener is currently bound to. 56 * @type Signal 57 * @private 58 */ 59 this._signal = signal; 60 61 /** 62 * Listener priority 63 * @type Number 64 * @private 65 */ 66 this._priority = priority || 0; 67 } 68 69 SignalBinding.prototype = { 70 71 /** 72 * If binding is active and should be executed. 73 * @type boolean 74 */ 75 active : true, 76 77 /** 78 * Default parameters passed to listener during `Signal.dispatch` and `SignalBinding.execute`. (curried parameters) 79 * @type Array|null 80 */ 81 params : null, 82 83 /** 84 * Call listener passing arbitrary parameters. 85 * <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> 86 * @param {Array} [paramsArr] Array of parameters that should be passed to the listener 87 * @return {*} Value returned by the listener. 88 */ 89 execute : function (paramsArr) { 90 var handlerReturn, params; 91 if (this.active && !!this._listener) { 92 params = this.params? this.params.concat(paramsArr) : paramsArr; 93 handlerReturn = this._listener.apply(this.context, params); 94 if (this._isOnce) { 95 this.detach(); 96 } 97 } 98 return handlerReturn; 99 }, 100 101 /** 102 * Detach binding from signal. 103 * - alias to: mySignal.remove(myBinding.getListener()); 104 * @return {Function|null} Handler function bound to the signal or `null` if binding was previously detached. 105 */ 106 detach : function () { 107 return this.isBound()? this._signal.remove(this._listener, this.context) : null; 108 }, 109 110 /** 111 * @return {Boolean} `true` if binding is still bound to the signal and have a listener. 112 */ 113 isBound : function () { 114 return (!!this._signal && !!this._listener); 115 }, 116 117 /** 118 * @return {boolean} If SignalBinding will only be executed once. 119 */ 120 isOnce : function () { 121 return this._isOnce; 122 }, 123 124 /** 125 * @return {Function} Handler function bound to the signal. 126 */ 127 getListener : function () { 128 return this._listener; 129 }, 130 131 /** 132 * @return {Signal} Signal that listener is currently bound to. 133 */ 134 getSignal : function () { 135 return this._signal; 136 }, 137 138 /** 139 * Delete instance properties 140 * @private 141 */ 142 _destroy : function () { 143 delete this._signal; 144 delete this._listener; 145 delete this.context; 146 }, 147 148 /** 149 * @return {string} String representation of the object. 150 */ 151 toString : function () { 152 return '[SignalBinding isOnce:' + this._isOnce +', isBound:'+ this.isBound() +', active:' + this.active + ']'; 153 } 154 155 }; 156 157 158 /*global SignalBinding:false*/ 159 160 // Signal -------------------------------------------------------- 161 //================================================================ 162 163 function validateListener(listener, fnName) { 164 if (typeof listener !== 'function') { 165 throw new Error( 'listener is a required param of {fn}() and should be a Function.'.replace('{fn}', fnName) ); 166 } 167 } 168 169 /** 170 * Custom event broadcaster 171 * <br />- inspired by Robert Penner's AS3 Signals. 172 * @name Signal 173 * @author Miller Medeiros 174 * @constructor 175 */ 176 function Signal() { 177 /** 178 * @type Array.<SignalBinding> 179 * @private 180 */ 181 this._bindings = []; 182 this._prevParams = null; 183 184 // enforce dispatch to aways work on same context (#47) 185 var self = this; 186 this.dispatch = function(){ 187 Signal.prototype.dispatch.apply(self, arguments); 188 }; 189 } 190 191 Signal.prototype = { 192 193 /** 194 * Signals Version Number 195 * @type String 196 * @const 197 */ 198 VERSION : '1.0.0', 199 200 /** 201 * If Signal should keep record of previously dispatched parameters and 202 * automatically execute listener during `add()`/`addOnce()` if Signal was 203 * already dispatched before. 204 * @type boolean 205 */ 206 memorize : false, 207 208 /** 209 * @type boolean 210 * @private 211 */ 212 _shouldPropagate : true, 213 214 /** 215 * If Signal is active and should broadcast events. 216 * <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> 217 * @type boolean 218 */ 219 active : true, 220 221 /** 222 * @param {Function} listener 223 * @param {boolean} isOnce 224 * @param {Object} [listenerContext] 225 * @param {Number} [priority] 226 * @return {SignalBinding} 227 * @private 228 */ 229 _registerListener : function (listener, isOnce, listenerContext, priority) { 230 231 var prevIndex = this._indexOfListener(listener, listenerContext), 232 binding; 233 234 if (prevIndex !== -1) { 235 binding = this._bindings[prevIndex]; 236 if (binding.isOnce() !== isOnce) { 237 throw new Error('You cannot add'+ (isOnce? '' : 'Once') +'() then add'+ (!isOnce? '' : 'Once') +'() the same listener without removing the relationship first.'); 238 } 239 } else { 240 binding = new SignalBinding(this, listener, isOnce, listenerContext, priority); 241 this._addBinding(binding); 242 } 243 244 if(this.memorize && this._prevParams){ 245 binding.execute(this._prevParams); 246 } 247 248 return binding; 249 }, 250 251 /** 252 * @param {SignalBinding} binding 253 * @private 254 */ 255 _addBinding : function (binding) { 256 //simplified insertion sort 257 var n = this._bindings.length; 258 do { --n; } while (this._bindings[n] && binding._priority <= this._bindings[n]._priority); 259 this._bindings.splice(n + 1, 0, binding); 260 }, 261 262 /** 263 * @param {Function} listener 264 * @return {number} 265 * @private 266 */ 267 _indexOfListener : function (listener, context) { 268 var n = this._bindings.length, 269 cur; 270 while (n--) { 271 cur = this._bindings[n]; 272 if (cur._listener === listener && cur.context === context) { 273 return n; 274 } 275 } 276 return -1; 277 }, 278 279 /** 280 * Check if listener was attached to Signal. 281 * @param {Function} listener 282 * @param {Object} [context] 283 * @return {boolean} if Signal has the specified listener. 284 */ 285 has : function (listener, context) { 286 return this._indexOfListener(listener, context) !== -1; 287 }, 288 289 /** 290 * Add a listener to the signal. 291 * @param {Function} listener Signal handler function. 292 * @param {Object} [listenerContext] Context on which listener will be executed (object that should represent the `this` variable inside listener function). 293 * @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) 294 * @return {SignalBinding} An Object representing the binding between the Signal and listener. 295 */ 296 add : function (listener, listenerContext, priority) { 297 validateListener(listener, 'add'); 298 return this._registerListener(listener, false, listenerContext, priority); 299 }, 300 301 /** 302 * Add listener to the signal that should be removed after first execution (will be executed only once). 303 * @param {Function} listener Signal handler function. 304 * @param {Object} [listenerContext] Context on which listener will be executed (object that should represent the `this` variable inside listener function). 305 * @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) 306 * @return {SignalBinding} An Object representing the binding between the Signal and listener. 307 */ 308 addOnce : function (listener, listenerContext, priority) { 309 validateListener(listener, 'addOnce'); 310 return this._registerListener(listener, true, listenerContext, priority); 311 }, 312 313 /** 314 * Remove a single listener from the dispatch queue. 315 * @param {Function} listener Handler function that should be removed. 316 * @param {Object} [context] Execution context (since you can add the same handler multiple times if executing in a different context). 317 * @return {Function} Listener handler function. 318 */ 319 remove : function (listener, context) { 320 validateListener(listener, 'remove'); 321 322 var i = this._indexOfListener(listener, context); 323 if (i !== -1) { 324 this._bindings[i]._destroy(); //no reason to a SignalBinding exist if it isn't attached to a signal 325 this._bindings.splice(i, 1); 326 } 327 return listener; 328 }, 329 330 /** 331 * Remove all listeners from the Signal. 332 */ 333 removeAll : function () { 334 var n = this._bindings.length; 335 while (n--) { 336 this._bindings[n]._destroy(); 337 } 338 this._bindings.length = 0; 339 }, 340 341 /** 342 * @return {number} Number of listeners attached to the Signal. 343 */ 344 getNumListeners : function () { 345 return this._bindings.length; 346 }, 347 348 /** 349 * Stop propagation of the event, blocking the dispatch to next listeners on the queue. 350 * <p><strong>IMPORTANT:</strong> should be called only during signal dispatch, calling it before/after dispatch won't affect signal broadcast.</p> 351 * @see Signal.prototype.disable 352 */ 353 halt : function () { 354 this._shouldPropagate = false; 355 }, 356 357 /** 358 * Dispatch/Broadcast Signal to all listeners added to the queue. 359 * @param {...*} [params] Parameters that should be passed to each handler. 360 */ 361 dispatch : function (params) { 362 if (! this.active) { 363 return; 364 } 365 366 var paramsArr = Array.prototype.slice.call(arguments), 367 n = this._bindings.length, 368 bindings; 369 370 if (this.memorize) { 371 this._prevParams = paramsArr; 372 } 373 374 if (! n) { 375 //should come after memorize 376 return; 377 } 378 379 bindings = this._bindings.slice(); //clone array in case add/remove items during dispatch 380 this._shouldPropagate = true; //in case `halt` was called before dispatch or during the previous dispatch. 381 382 //execute all callbacks until end of the list or until a callback returns `false` or stops propagation 383 //reverse loop since listeners with higher priority will be added at the end of the list 384 do { n--; } while (bindings[n] && this._shouldPropagate && bindings[n].execute(paramsArr) !== false); 385 }, 386 387 /** 388 * Forget memorized arguments. 389 * @see Signal.memorize 390 */ 391 forget : function(){ 392 this._prevParams = null; 393 }, 394 395 /** 396 * Remove all bindings from signal and destroy any reference to external objects (destroy Signal object). 397 * <p><strong>IMPORTANT:</strong> calling any method on the signal instance after calling dispose will throw errors.</p> 398 */ 399 dispose : function () { 400 this.removeAll(); 401 delete this._bindings; 402 delete this._prevParams; 403 }, 404 405 /** 406 * @return {string} String representation of the object. 407 */ 408 toString : function () { 409 return '[Signal active:'+ this.active +' numListeners:'+ this.getNumListeners() +']'; 410 } 411 412 }; 413 414 415 // Namespace ----------------------------------------------------- 416 //================================================================ 417 418 /** 419 * Signals namespace 420 * @namespace 421 * @name signals 422 */ 423 var signals = Signal; 424 425 /** 426 * Custom event broadcaster 427 * @see Signal 428 */ 429 // alias for backwards compatibility (see #gh-44) 430 signals.Signal = Signal; 431 432 433 434 //exports to multiple environments 435 if(typeof define === 'function' && define.amd){ //AMD 436 define(function () { return signals; }); 437 } else if (typeof module !== 'undefined' && module.exports){ //node 438 module.exports = signals; 439 } else { //browser 440 //use string because of Google closure compiler ADVANCED_MODE 441 /*jslint sub:true */ 442 global['signals'] = signals; 443 } 444 445 }(this)); 446