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