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