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