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