1 /** 
  2  * jQuery.enable v0.6.0
  3  * 
  4  * @name jQuery
  5  * @namespace 
  6  *
  7  * @description <p>jQuery.enable.js is a small library of jQuery plugins
  8  *   designed to extend evented behaviors to JavaScript objects and classes.
  9  *   These behaviors include: custom events, Ajax, templating, caching,
 10  *   polling, and more.</p>
 11  *   <p>The cornerstone of the library is jQuery.bindable behavior. All of the
 12  *   other behaviors "inherit" custom event functionality from bindable.</p>
 13  *   
 14  * @author <a href="http://twitter.com/furf">Dave Furfero</a>
 15  */ 
 16 (function (window, document, jQuery) {
 17 
 18   /**
 19    * Regular expression for finding whitespace
 20    * @type {regexp}
 21    */
 22   var rwhite = /\s+/;
 23 
 24   /**
 25    * @description <p>Trims and splits a whitespace-delimited string. A
 26    *   shortcut for splitting "jQuery-style" lists.</p>
 27    *
 28    * @example
 29    *
 30    * jQuery.unwhite('onDoSomething onDoSomethingElse');
 31    * // returns ['onDoSomething', 'onDoSomethingElse']
 32    *
 33    * @param {String} str Whitespace-delimited list
 34    * @return {Array} Array of list times
 35    */
 36   jQuery.unwhite = function (str) {
 37     str = str && jQuery.trim(str);
 38     return str.length ? str.split(rwhite) : [];
 39   };
 40 
 41   /**
 42    * @description <p>Takes a function and returns a new one that will always
 43    * have a particular context, omitting the event argument for improved
 44    * compatibility with external APIs.</p>
 45    * @see The documentation for
 46    *   <a href="http://api.jquery.com/jQuery.proxy/">jQuery.proxy</a>.
 47    *
 48    * @example
 49    *
 50    * // Bind a proxied function to an evented object
 51    * loadableObject.bind('onLoadSuccess', jQuery.eventProxy(function (data) {
 52    *   alert(data.message);
 53    * }));
 54    *
 55    * // Trigger the event
 56    * loadableObject.trigger('onLoadSuccess', [{ message: 'hello, world!' }]);
 57    *
 58    * // The event object normally passed as the first argument to callbacks
 59    * // is ignored and our callback alerts "hello, world!"
 60    *
 61    * @param {Function} fn
 62    * @param {Object} context (optional)
 63    */
 64   jQuery.eventProxy = function (fn, context) {
 65     var proxy = jQuery.proxy.apply(null, arguments);
 66     return function () {
 67       return proxy.apply(null, Array.prototype.slice.call(arguments, 1));
 68     };
 69   };
 70 
 71   /**
 72    * @description <p>Augments a static object or Class prototype with
 73    * custom event functionality.</p>
 74    * 
 75    * @example
 76    * // Usage with a static object
 77    * var dave = {
 78    *   name: 'dave',
 79    *   saySomething: function (text) {
 80    *     alert(this.name + ' says: ' + text);
 81    *     this.trigger('onSaySomething', [text]);
 82    *   }
 83    * };
 84    * 
 85    * // Add bindable behavior
 86    * $.bindable(dave);
 87    * 
 88    * // Add event listener using bind method
 89    * dave.bind('onSaySomething', function (evt, data) {
 90    *   console.log(this.name + ' said: ' + data);
 91    * });
 92    * 
 93    * dave.saySomething('hello, world!');
 94    * // alerts "furf says: hello, world!"
 95    * // logs "furf said: hello, world!"
 96    * 
 97    * @example
 98    * // Usage with a class
 99    * function Person (name) {
100    *   this.name = name
101    * }
102    * 
103    * // Add bindable behavior with custom event method
104    * $.bindable(Person, 'onSaySomething');
105    * 
106    * Person.prototype.saySomething = function (text) {
107    *   alert(this.name + ' says: ' + text);
108    *   this.trigger('onSaySomething', [text]);
109    * };
110    * 
111    * // Create instance
112    * var furf = new Person('furf');
113    * 
114    * // Add event listener using custom event method
115    * furf.onSaySomething(function (evt, data) {
116    *   console.log(this.name + ' said: ' + data);
117    * });
118    * 
119    * furf.saySomething('hello, world!');
120    * // alerts "furf says: hello, world!"
121    * // logs "furf said: hello, world!"
122    * 
123    * @param {Object|Function} obj (optional) Object to be augmented with
124    *   bindable behavior. If none is supplied, a new Object will be created
125    *   and augmented. If a function is supplied, its prototype will be 
126    *   augmented, allowing each instance of the function access to the 
127    *   bindable methods.
128    * @param {String} types (optional) Whitespace-delimited list of custom
129    *   events which will be exposed as convenience bind methods on the
130    *   augmented object
131    * @returns {Object} Augmented object
132    */
133   jQuery.bindable = function (obj, types) {
134 
135     // Allow instantiation without object
136     if (!(obj instanceof Object)) {
137       types = obj;
138       obj   = {};
139     }
140 
141     // Allow use of prototype for shorthanding the augmentation of classes
142     obj = jQuery.isFunction(obj) ? obj.prototype : obj;
143 
144     // Augment the object with jQuery's bind, one, and unbind event methods
145     jQuery.each(['bind', 'one', 'unbind', 'on', 'off'], function (i, method) {
146       obj[method] = function (type, data, fn) {
147         jQuery(this)[method](type, data, fn);
148         return this;
149       };
150     });
151 
152     // The trigger event must be augmented separately because it requires a
153     // new Event to prevent unexpected triggering of a method (and possibly
154     // infinite recursion) when the event type matches the method name
155     obj.trigger = function (type, data) {
156 
157       var event = new jQuery.Event(type),
158           all   = new jQuery.Event(event);
159 
160       event.preventDefault();
161       
162       all.type = '*';
163 
164       if (event.type !== all.type) {
165         jQuery.event.trigger(event, data, this);
166       }
167       
168       jQuery.event.trigger(all, data, this);
169       
170       return this;
171     };
172 
173     // Create convenience methods for event subscription which bind callbacks
174     // to specified events
175     if (typeof types === 'string') {
176       jQuery.each(jQuery.unwhite(types), function (i, type) {
177         obj[type] = function (data, fn) {
178           return arguments.length ? this.bind(type, data, fn) : this.trigger(type);
179         };
180       });
181     }
182 
183     return obj;
184   };
185 
186   /**
187    * @description <p>Augments a static object or Class prototype with
188    * evented Ajax functionality.</p>
189    * 
190    * @param {Object|Function} obj (optional) Object to be augmented with
191    *   loadable behavior
192    * @param {Object|String} defaultCfg Default Ajax settings
193    * @return {Object} Augmented object
194    */
195   jQuery.loadable = function (obj, defaultCfg) {
196 
197     // Allow instantiation without object
198     if (typeof defaultCfg === 'undefined') {
199       defaultCfg = obj;
200       obj = {};
201     }
202 
203     // Implement bindable behavior, adding custom methods for Ajax events
204     obj = jQuery.bindable(obj, 'onLoadBeforeSend onLoadAbort onLoadSuccess onLoadError onLoadComplete');
205 
206     // Allow URL as config (shortcut)
207     if (typeof defaultCfg === 'string') {
208       defaultCfg = {
209         url: defaultCfg
210       };
211     }
212 
213     jQuery.extend(obj, {
214 
215       /**
216        * Merge runtime config with default config
217        * Refactored out of load() for easier integration with everyone's
218        * favorite sequential AJAX library...
219        */
220       loadableConfig: function (cfg) {
221 
222         var beforeSend, dataFilter, success, error, complete;
223 
224         // If one parameter is passed, it's either a config or a callback
225         // @todo take (url, callback)
226         if (typeof cfg === 'string') {
227           cfg = {
228             url: cfg
229           };
230         } else if (jQuery.isFunction(cfg)) {
231           cfg = {
232             success: cfg
233           };
234         }
235 
236         // Extend default config with runtime config
237         cfg = jQuery.extend(true, {}, defaultCfg, cfg);
238 
239         // Cache configured callbacks so they can be called from wrapper
240         // functions below.
241         beforeSend = cfg.beforeSend;
242         dataFilter = cfg.dataFilter;
243         success    = cfg.success;
244         error      = cfg.error;
245         complete   = cfg.complete;
246 
247         // Overload each of the configured jQuery.ajax callback methods with
248         // an evented wrapper function. Each wrapper function executes the
249         // configured callback in the scope of the loadable object and then
250         // fires the corresponding event, passing to it the return value of
251         // the configured callback or the unmodified arguments if no callback
252         // is supplied or the return value is undefined.
253         return jQuery.extend(cfg, {
254 
255           /**
256            * @param {XMLHTTPRequest} xhr
257            * @param {Object} cfg
258            */
259           beforeSend: jQuery.proxy(function (xhr, cfg) {
260 
261             // If defined, execute the beforeSend callback and store its return
262             // value for later return from this proxy function -- used for
263             // aborting the XHR
264             var ret = beforeSend && beforeSend.apply(this, arguments);
265 
266             // Trigger the onLoadBeforeSend event listeners
267             this.trigger('onLoadBeforeSend', arguments);
268 
269             // If the request is explicitly aborted from the beforeSend
270             // callback, trigger the onLoadAbort event listeners
271             if (ret === false) {
272               this.trigger('onLoadAbort', arguments);
273             }
274 
275             return ret;
276 
277           }, this),
278 
279 
280           // just added -- doc it up
281           dataFilter: dataFilter && jQuery.proxy(function (response, type) {
282             return dataFilter.apply(this, arguments);
283           }, this),
284 
285 
286           /**
287            * @param {Object} data
288            * @param {String} status
289            * @param {XMLHTTPRequest} xhr
290            */
291           success: jQuery.proxy(function (data, status, xhr) {
292 
293             var ret;
294 
295             // If defined, execute the success callback
296             if (success) {
297               ret = success.apply(this, arguments);
298             }
299 
300             // Trigger the onLoadSuccess event listeners
301             this.trigger('onLoadSuccess',  arguments);
302 
303             return ret;
304 
305           }, this),
306 
307           /**
308            * @param {XMLHTTPRequest} xhr
309            * @param {String} status
310            * @param {Error} e
311            * @todo correct param type for error?
312            */
313           error: jQuery.proxy(function (xhr, status, e) {
314 
315             var ret;
316 
317             // If defined, execute the error callback
318             if (error) {
319               ret = error.apply(this, arguments);
320             }
321 
322             // Trigger the onLoadError event listeners
323             this.trigger('onLoadError', arguments);
324 
325             return ret;
326 
327           }, this),
328 
329           /**
330            * @param {XMLHTTPRequest} xhr
331            * @param {String} status
332            */
333           complete: jQuery.proxy(function (xhr, status) {
334 
335             var ret;
336 
337             // If defined, execute the complete callback
338             if (complete) {
339               ret = complete.apply(this, arguments);
340             }
341 
342             // Trigger the onLoadComplete event listeners
343             this.trigger('onLoadComplete', arguments);
344 
345             return ret;
346 
347           }, this)
348         });
349       },
350 
351       /**
352        * Execute the XMLHTTPRequest
353        * @param {Object} cfg Overload jQuery.ajax configuration object
354        */
355       load: function (cfg) {
356         return jQuery.ajax(this.loadableConfig(cfg));
357       }
358 
359     });
360 
361     return obj;
362   };
363 
364   /**
365    * jQuery.renderable
366    *
367    * @param {Object|Function} obj (optional) Object to be augmented with renderable behavior
368    * @param {String} tpl Template or URL to template file
369    * @param {String|jQuery} elem (optional) Target DOM element
370    * @return {Object} Augmented object
371    */
372   jQuery.renderable = function (obj, tpl, elem) {
373 
374     // Allow instantiation without object
375     if (!(obj instanceof Object)) {
376       elem = tpl;
377       tpl  = obj;
378       obj  = {};
379     }
380 
381     // Implement bindable behavior, adding custom methods for render events
382     obj = jQuery.bindable(obj, 'onBeforeRender onRender');
383 
384     // Create a jQuery target to handle DOM load
385     if (typeof elem !== 'undefined') {
386       elem = jQuery(elem);
387     }
388 
389     // Create renderer function from supplied template
390     var renderer = jQuery.isFunction(tpl) ? tpl : jQuery.template(tpl);
391 
392     // Augment the object with a render method
393     obj.render = function (data, raw) {
394 
395       if (!(data instanceof Object)) {
396         raw  = data;
397         data = this;
398       } else {
399         data = jQuery.extend(true, {}, this, data);
400       }
401 
402       this.trigger('onBeforeRender', [data]);
403 
404       // Force raw HTML if elem exists (saves effort)
405       var ret = renderer.call(this, data, !!elem || raw);
406 
407       if (elem) {
408         elem.html(ret);
409       }
410 
411       this.trigger('onRender', [ret]);
412 
413       return ret;
414     };
415 
416     return obj;
417   };
418 
419   /**
420    * jQuery.pollable
421    * @todo add passing of anon function to start?
422    * @param {Object|Function} obj (optional) Object to be augmented with pollable behavior
423    * @return {object} Augmented object
424    */
425   jQuery.pollable = function (obj) {
426 
427     // Allow instantiation without object
428     if (typeof obj === 'undefined') {
429       obj = {};
430     }
431 
432     // Implement bindable behavior, adding custom methods for pollable events
433     obj = jQuery.bindable(obj, 'onStart onExecute onStop');
434 
435     // Augment the object with an pollable methods
436     jQuery.extend(obj, {
437 
438       /**
439        * @param {String} method
440        * @return {boolean}
441        */
442       isExecuting: function (method) {
443         var timers = jQuery(this).data('pollable.timers') || {};
444         return method in timers;
445       },
446 
447       /**
448        * @param {String} method
449        * @param {Number} interval
450        * @param {Boolean} immediately
451        */
452       start: function (method, interval, data, immediately) {
453 
454         var self, timers;
455 
456         if (typeof data === 'boolean') {
457           immediately = data;
458           data = null;
459         }
460 
461         data = data || [];
462 
463         if (!this.isExecuting(method) && jQuery.isFunction(this[method]) && interval > 0) {
464 
465           self   = jQuery(this);
466           timers = self.data('pollable.timers') || {};
467 
468           // Store the proxy method as a property of the original method
469           // for later removal
470           this[method].proxy = jQuery.proxy(function () {
471             this.trigger('onExecute', [method, this[method].apply(this, data)]);
472           }, this);
473 
474           // Start timer and add to hash
475           timers[method] = window.setInterval(this[method].proxy, interval);
476 
477           self.data('pollable.timers', timers);
478 
479           // Fire onStart event with method name
480           this.trigger('onStart', [method]);
481 
482           if (immediately) {
483             this[method].proxy();
484           }
485         }
486 
487         return this;
488       },
489 
490       /**
491        * @param {String} method
492        */
493       stop: function (method) {
494 
495         var self, timers;
496 
497         if (this.isExecuting(method)) {
498 
499           self   = jQuery(this);
500           timers = self.data('pollable.timers') || {};
501 
502           // Clear timer
503           window.clearInterval(timers[method]);
504 
505           // Remove timer from hash
506           delete timers[method];
507 
508           // Remove proxy method from original method
509           delete this[method].proxy;
510 
511           self.data('pollable.timers', timers);
512 
513           // Fire onStop event with method name
514           this.trigger('onStop', [method]);
515         }
516         return this;
517       }
518     });
519 
520     return obj;
521   };
522 
523   /**
524    * @description <p>Augments a static object or Class prototype with timed
525    * caching functionality.</p>
526    *
527    * @param {Object|Function} obj (optional) Object to be augmented with
528    *   cacheable behavior
529    * @param {Number} defaultTtl (optional) Default time-to-live for cached
530    *   items
531    * @return {object} Augmented object
532    */
533   jQuery.cacheable = function (obj, defaultTtl) {
534 
535     // Allow instantiation without object
536     if (!(obj instanceof Object)) {
537       defaultTtl = obj;
538       obj        = {};
539     }
540 
541     // Allow use of prototype for shorthanding the augmentation of classes
542     obj = obj.prototype || obj;
543 
544     // I love using Infinity :)
545     defaultTtl = typeof defaultTtl !== 'undefined' ? defaultTtl : Infinity;
546 
547     jQuery.extend(obj, {
548 
549       /**
550        * @param {String} key
551        * @param {*} value
552        * @param {Number} ttl
553        * @return undefined
554        */
555       cacheSet: function(key, value, ttl) {
556 
557         var self    = jQuery(this),
558             cache   = self.data('cacheable.cache') || {},
559             expires = jQuery.now() + (typeof ttl !== 'undefined' ? ttl : defaultTtl);
560 
561         cache[key] = {
562           value:   value,
563           expires: expires
564         };
565 
566         self.data('cacheable.cache', cache);
567       },
568 
569       /**
570        * @param {String} key
571        * @return
572        */
573       cacheGet: function(key) {
574 
575         var cache = jQuery(this).data('cacheable.cache') || {},
576             data,
577             ret;
578 
579         if (key) {
580 
581           if (key in cache) {
582 
583             data = cache[key];
584 
585             if (data.expires < jQuery.now()) {
586               this.cacheUnset(key);
587             } else {
588               ret = data.value;
589             }
590           }
591 
592         } else {
593           ret = cache;
594         }
595 
596         return ret;
597       },
598 
599       /**
600        * @param {String} key
601        * @return {boolean}
602        */
603       cacheHas: function(key) {
604         var cache = jQuery(this).data('cacheable.cache');
605         return (key in cache);
606       },
607 
608       /**
609        * @param {String} key
610        * @return undefined
611        */
612       cacheUnset: function(key) {
613 
614         var self  = jQuery(this),
615             cache = self.data('cacheable.cache');
616 
617         if (cache && key in cache) {
618 
619           cache[key] = null;
620           delete cache[key];
621 
622           self.data('cacheable.cache', cache);
623         }
624       },
625 
626       cacheEmpty: function() {
627         jQuery(this).data('cacheable.cache', {});
628       }
629 
630     });
631 
632     return obj;
633   };
634 
635   /**
636    * jQuery.observable
637    *
638    * @param {Object|Function} obj Object to be augmented with observable behavior
639    * @return {Object} Augmented object
640    */
641   jQuery.observable = function (obj) {
642 
643     // Allow instantiation without object
644     if (typeof obj === 'undefined') {
645       obj = {};
646     }
647 
648     // Implement bindable behavior, adding custom methods for render events
649     obj = jQuery.bindable(obj, 'onObserve');
650 
651     // Augment the object with observe and ignore methods
652     jQuery.extend(obj, {
653 
654       observe: function (obj, namespaces) {
655         obj.bind('*', jQuery.proxy(function (evt) {
656 
657           var orig = evt.originalEvent,
658               type = orig.type,
659               args = Array.prototype.slice.call(arguments, 1);
660 
661           if (namespace) {
662             var self = this;
663             jQuery.each(jQuery.unwhite(namespace), function (i, ns) {
664               orig.type = type + '/' + ns;
665               self.trigger(orig, args);
666             });
667           }
668 
669           orig.type = type + '/*';
670           this.trigger(orig, args);
671 
672         }, this));
673         this.trigger('onObserve', [namespace]);
674         return this;
675       },
676 
677       ignore: function (obj) {
678         // @todo
679         this.trigger('onIgnore', [namespace]);
680       }
681     });
682 
683     return obj;
684   };
685 
686 })(this, this.document, this.jQuery);
687