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));