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