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