Introduced a new promise method on deferreds that returns an immutable object (exposi...
[jquery.git] / src / core.js
index 23d40bd..07666d9 100644 (file)
@@ -33,6 +33,9 @@ var jQuery = function( selector, context ) {
        // Check for non-word characters
        rnonword = /\W/,
 
+       // Check for digits
+       rdigit = /\d/,
+
        // Match a standalone tag
        rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/,
 
@@ -57,8 +60,8 @@ var jQuery = function( selector, context ) {
        // Has the ready events already been bound?
        readyBound = false,
        
-       // The functions to execute on DOM ready
-       readyList = [],
+       // The deferred used on DOM ready
+       readyList,
 
        // The ready event handler
        DOMContentLoaded,
@@ -69,7 +72,13 @@ var jQuery = function( selector, context ) {
        push = Array.prototype.push,
        slice = Array.prototype.slice,
        trim = String.prototype.trim,
-       indexOf = Array.prototype.indexOf;
+       indexOf = Array.prototype.indexOf,
+       
+       // [[Class]] -> type pairs
+       class2type = {},
+       
+       // Marker for deferred
+       promiseMarker = [];
 
 jQuery.fn = jQuery.prototype = {
        init: function( selector, context ) {
@@ -88,7 +97,7 @@ jQuery.fn = jQuery.prototype = {
                }
                
                // The body element only exists once, optimize finding it
-               if ( selector === "body" && !context ) {
+               if ( selector === "body" && !context && document.body ) {
                        this.context = document;
                        this[0] = document.body;
                        this.selector = "body";
@@ -209,7 +218,7 @@ jQuery.fn = jQuery.prototype = {
                        this.toArray() :
 
                        // Return just the object
-                       ( num < 0 ? this.slice(num)[ 0 ] : this[ num ] );
+                       ( num < 0 ? this[ this.length + num ] : this[ num ] );
        },
 
        // Take an array of elements and push it onto the stack
@@ -247,22 +256,12 @@ jQuery.fn = jQuery.prototype = {
                return jQuery.each( this, callback, args );
        },
        
-       ready: function( fn ) {
+       ready: function() {
                // Attach the listeners
                jQuery.bindReady();
-
-               // If the DOM is already ready
-               if ( jQuery.isReady ) {
-                       // Execute the function immediately
-                       fn.call( document, jQuery );
-
-               // Otherwise, remember the function for later
-               } else if ( readyList ) {
-                       // Add the function to the wait list
-                       readyList.push( fn );
-               }
-
-               return this;
+               
+               // Change ready & apply
+               return ( jQuery.fn.ready = readyList.then ).apply( this , arguments );
        },
        
        eq: function( i ) {
@@ -305,8 +304,11 @@ jQuery.fn = jQuery.prototype = {
 jQuery.fn.init.prototype = jQuery.fn;
 
 jQuery.extend = jQuery.fn.extend = function() {
-       // copy reference to target object
-       var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options, name, src, copy, copyIsArray;
+        var options, name, src, copy, copyIsArray, clone,
+               target = arguments[0] || {},
+               i = 1,
+               length = arguments.length,
+               deep = false;
 
        // Handle a deep copy situation
        if ( typeof target === "boolean" ) {
@@ -394,7 +396,7 @@ jQuery.extend({
                if ( !jQuery.readyWait || (wait !== true && !jQuery.isReady) ) {
                        // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
                        if ( !document.body ) {
-                               return setTimeout( jQuery.ready, 13 );
+                               return setTimeout( jQuery.ready, 1 );
                        }
 
                        // Remember that the DOM is ready
@@ -406,20 +408,11 @@ jQuery.extend({
                        }
 
                        // If there are functions bound, to execute
-                       if ( readyList ) {
-                               // Execute all of them
-                               var fn, i = 0;
-                               while ( (fn = readyList[ i++ ]) ) {
-                                       fn.call( document, jQuery );
-                               }
-
-                               // Reset the list of functions
-                               readyList = null;
-                       }
-
+                       readyList.fire( document , [ jQuery ] );
+                       
                        // Trigger any bound ready events
-                       if ( jQuery.fn.triggerHandler ) {
-                               jQuery( document ).triggerHandler( "ready" );
+                       if ( jQuery.fn.trigger ) {
+                               jQuery( document ).trigger( "ready" ).unbind( "ready" );
                        }
                }
        },
@@ -434,7 +427,8 @@ jQuery.extend({
                // Catch cases where $(document).ready() is called after the
                // browser event has already occurred.
                if ( document.readyState === "complete" ) {
-                       return jQuery.ready();
+                       // Handle it asynchronously to allow scripts the opportunity to delay ready
+                       return setTimeout( jQuery.ready, 1 );
                }
 
                // Mozilla, Opera and webkit nightlies currently support this event
@@ -484,10 +478,14 @@ jQuery.extend({
                return obj && typeof obj === "object" && "setInterval" in obj;
        },
 
+       isNaN: function( obj ) {
+               return obj == null || !rdigit.test( obj ) || isNaN( obj );
+       },
+
        type: function( obj ) {
                return obj == null ?
                        String( obj ) :
-                       toString.call(obj).slice(8, -1).toLowerCase();
+                       class2type[ toString.call(obj) ] || "object";
        },
 
        isPlainObject: function( obj ) {
@@ -548,6 +546,28 @@ jQuery.extend({
                        jQuery.error( "Invalid JSON: " + data );
                }
        },
+       
+       // Cross-browser xml parsing
+       // (xml & tmp used internally)
+       parseXML: function( data , xml , tmp ) {
+
+               if ( window.DOMParser ) { // Standard
+                       tmp = new DOMParser();
+                       xml = tmp.parseFromString( data , "text/xml" );
+               } else { // IE
+                       xml = new ActiveXObject( "Microsoft.XMLDOM" );
+                       xml.async = "false";
+                       xml.loadXML( data );
+               }
+               
+               tmp = xml.documentElement;
+               
+               if ( ! tmp || ! tmp.nodeName || tmp.nodeName === "parsererror" ) {
+                       jQuery.error( "Invalid XML: " + data );
+               }
+               
+               return xml;
+       },
 
        noop: function() {},
 
@@ -667,7 +687,8 @@ jQuery.extend({
        },
 
        merge: function( first, second ) {
-               var i = first.length, j = 0;
+               var i = first.length,
+                       j = 0;
 
                if ( typeof second.length === "number" ) {
                        for ( var l = second.length; j < l; j++ ) {
@@ -715,6 +736,7 @@ jQuery.extend({
                        }
                }
 
+               // Flatten any nested arrays
                return ret.concat.apply( [], ret );
        },
 
@@ -781,6 +803,154 @@ jQuery.extend({
        now: function() {
                return (new Date()).getTime();
        },
+       
+       // Create a simple deferred (one callbacks list)
+       _Deferred: function() {
+               
+               var // callbacks list
+                       callbacks = [],
+                       // stored [ context , args ]
+                       fired,
+                       // to avoid firing when already doing so
+                       firing,
+                       // flag to know if the deferred has been cancelled
+                       cancelled,
+                       // the deferred itself
+                       deferred  = {
+                               
+                               // then( f1, f2, ...)
+                               then: function () {
+                                       
+                                       if ( ! cancelled ) {
+                                               
+                                               var args = arguments,
+                                                       i,
+                                                       length,
+                                                       elem,
+                                                       type,
+                                                       _fired;
+                                                       
+                                               if ( fired ) {
+                                                       _fired = fired;
+                                                       fired = 0;
+                                               }
+                                               
+                                               for ( i = 0, length = args.length ; i < length ; i++ ) {
+                                                       elem = args[ i ];
+                                                       type = jQuery.type( elem );
+                                                       if ( type === "array" ) {
+                                                               deferred.then.apply( deferred , elem );
+                                                       } else if ( type === "function" ) {
+                                                               callbacks.push( elem );
+                                                       }
+                                               }
+                                               
+                                               if ( _fired ) {
+                                                       deferred.fire( _fired[ 0 ] , _fired[ 1 ] );
+                                               }
+                                       }
+                                       
+                                       return this;
+                               },
+                               
+                               // resolve with given context and args
+                               fire: function( context , args ) {
+                                       if ( ! cancelled && ! fired && ! firing ) {
+                                               
+                                               firing = 1;
+                                               
+                                               try {
+                                                       while( callbacks[ 0 ] ) {
+                                                               callbacks.shift().apply( context , args );
+                                                       }
+                                               }
+                                               finally {
+                                                       fired = [ context , args ];
+                                                       firing = 0;
+                                               }
+                                       }
+                                       return this;
+                               },
+                               
+                               // resolve with this as context and given arguments
+                               resolve: function() {
+                                       deferred.fire( this , arguments );
+                                       return this;
+                               },
+                               
+                               // Has this deferred been resolved?
+                               isResolved: function() {
+                                       return !!( firing || fired );
+                               },
+                               
+                               // Cancel
+                               cancel: function() {
+                                       cancelled = 1;
+                                       callbacks = [];
+                                       return this;
+                               },
+                               
+                               // Has this deferred been cancelled?
+                               isCancelled: function() {
+                                       return !!cancelled;
+                               }
+                       };
+               
+               return deferred;
+       },
+       
+       // Full fledged deferred (two callbacks list)
+       // Typical success/error system
+       Deferred: function( func ) {
+               
+               var errorDeferred = jQuery._Deferred(),
+                       deferred = jQuery._Deferred(),
+                       successCancel = deferred.cancel;
+                       
+               // Add errorDeferred methods and redefine cancel                        
+               jQuery.extend( deferred , {
+
+                               fail: errorDeferred.then,
+                               fireReject: errorDeferred.fire,
+                               reject: errorDeferred.resolve,
+                               isRejected: errorDeferred.isResolved,
+                               // Get a promise for this deferred
+                               // If obj is provided, the promise aspect is added to the object
+                               promise: function( obj ) {
+                                       obj = obj || {};
+                                       for ( var i in { then:1 , fail:1 , isResolved:1 , isRejected:1 , promise:1 } ) {
+                                               obj[ i ] = deferred[ i ];
+                                       }
+                                       return obj;
+                               }
+
+               } );
+               
+               // Remove cancel related
+               delete deferred.cancel;
+               delete deferred.isCancelled;
+               
+               // Add promise marker
+               deferred.promise._ = promiseMarker;
+               
+               // Make sure only one callback list will be used
+               deferred.then( errorDeferred.cancel ).fail( successCancel );
+               
+               // Call given func if any
+               if ( func ) {
+                       func.call( deferred , deferred );
+               }
+               
+               return deferred;
+       },
+
+       // Deferred helper
+       when: function( object ) {
+               object = object && object.promise && object.promise._ === promiseMarker ?
+                       object :
+                       jQuery.Deferred().resolve( object );
+               return object.promise();
+       },
 
        // Use of jQuery.browser is frowned upon.
        // More details: http://docs.jquery.com/Utilities/jQuery.browser
@@ -799,6 +969,14 @@ jQuery.extend({
        browser: {}
 });
 
+// Create readyList deferred
+readyList = jQuery._Deferred();
+
+// Populate the class2type map
+jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) {
+       class2type[ "[object " + name + "]" ] = name.toLowerCase();
+});
+
 browserMatch = jQuery.uaMatch( userAgent );
 if ( browserMatch.browser ) {
        jQuery.browser[ browserMatch.browser ] = true;
@@ -862,6 +1040,11 @@ function doScrollCheck() {
        jQuery.ready();
 }
 
+// Expose jQuery as an Asynchronous Module
+if ( typeof define !== "undefined" ) {
+       define( "jquery", [], function () { return jQuery; } );
+}
+
 // Expose jQuery to the global object
 return (window.jQuery = window.$ = jQuery);