diff --git a/Makefile b/Makefile
index 06cee5de..3bb56e3d 100644
--- a/Makefile
+++ b/Makefile
@@ -10,6 +10,7 @@ COMPILER = ${JS_ENGINE} ${BUILD_DIR}/uglify.js --unsafe
POST_COMPILER = ${JS_ENGINE} ${BUILD_DIR}/post-compile.js
BASE_FILES = ${SRC_DIR}/core.js\
+ ${SRC_DIR}/callbacks.js\
${SRC_DIR}/deferred.js\
${SRC_DIR}/support.js\
${SRC_DIR}/data.js\
diff --git a/src/ajax.js b/src/ajax.js
index a16717b0..b9be5eb1 100644
--- a/src/ajax.js
+++ b/src/ajax.js
@@ -382,7 +382,7 @@ jQuery.extend({
jQuery( callbackContext ) : jQuery.event,
// Deferreds
deferred = jQuery.Deferred(),
- completeDeferred = jQuery._Deferred(),
+ completeCallbacks = jQuery.Callbacks( "once memory" ),
// Status-dependent callbacks
statusCode = s.statusCode || {},
// ifModified key
@@ -560,7 +560,7 @@ jQuery.extend({
}
// Complete
- completeDeferred.resolveWith( callbackContext, [ jqXHR, statusText ] );
+ completeCallbacks.fireWith( callbackContext, [ jqXHR, statusText ] );
if ( fireGlobals ) {
globalEventContext.trigger( "ajaxComplete", [ jqXHR, s] );
@@ -575,7 +575,7 @@ jQuery.extend({
deferred.promise( jqXHR );
jqXHR.success = jqXHR.done;
jqXHR.error = jqXHR.fail;
- jqXHR.complete = completeDeferred.done;
+ jqXHR.complete = completeCallbacks.add;
// Status-dependent callbacks
jqXHR.statusCode = function( map ) {
diff --git a/src/callbacks.js b/src/callbacks.js
new file mode 100644
index 00000000..5d5eb98e
--- /dev/null
+++ b/src/callbacks.js
@@ -0,0 +1,206 @@
+(function( jQuery ) {
+
+// String to Object flags format cache
+var flagsCache = {};
+
+// Convert String-formatted flags into Object-formatted ones and store in cache
+function createFlags( flags ) {
+ var object = flagsCache[ flags ] = {},
+ i, length;
+ flags = flags.split( /\s+/ );
+ for ( i = 0, length = flags.length; i < length; i++ ) {
+ object[ flags[i] ] = true;
+ }
+ return object;
+}
+
+/*
+ * Create a callback list using the following parameters:
+ *
+ * flags: an optional list of space-separated flags that will change how
+ * the callback list behaves
+ *
+ * filter: an optional function that will be applied to each added callbacks,
+ * what filter returns will then be added provided it is not falsy.
+ *
+ * By default a callback list will act like an event callback list and can be
+ * "fired" multiple times.
+ *
+ * Possible flags:
+ *
+ * once: will ensure the callback list can only be fired once (like a Deferred)
+ *
+ * memory: will keep track of previous values and will call any callback added
+ * after the list has been fired right away with the latest "memorized"
+ * values (like a Deferred)
+ *
+ * unique: will ensure a callback can only be added once (no duplicate in the list)
+ *
+ * relocate: like "unique" but will relocate the callback at the end of the list
+ *
+ */
+jQuery.Callbacks = function( flags, filter ) {
+
+ // flags are optional
+ if ( typeof flags !== "string" ) {
+ filter = flags;
+ flags = undefined;
+ }
+
+ // Convert flags from String-formatted to Object-formatted
+ // (we check in cache first)
+ flags = flags ? ( flagsCache[ flags ] || createFlags( flags ) ) : {};
+
+ var // Actual callback list
+ list = [],
+ // Stack of fire calls for repeatable lists
+ stack = !flags.once && [],
+ // Last fire value (for non-forgettable lists)
+ memory,
+ // Flag to know if list is currently firing
+ firing,
+ // Add a list of callbacks to the list
+ add = function( args ) {
+ var i,
+ length,
+ elem,
+ type,
+ actual;
+ for ( i = 0, length = args.length; i < length; i++ ) {
+ elem = args[ i ];
+ type = jQuery.type( elem );
+ if ( type === "array" ) {
+ // Inspect recursively
+ add( elem );
+ } else if ( type === "function" ) {
+ // If we have to relocate, we remove the callback
+ // if it already exists
+ if ( flags.relocate ) {
+ object.remove( elem );
+ }
+ // Unless we have a list with unicity and the
+ // function is already there, add it
+ if ( !( flags.unique && object.has( elem ) ) ) {
+ // Get the filtered function if needs be
+ actual = filter ? filter(elem) : elem;
+ if ( actual ) {
+ list.push( [ elem, actual ] );
+ }
+ }
+ }
+ }
+ },
+ object = {
+ // Add a callback or a collection of callbacks to the list
+ add: function() {
+ if ( list ) {
+ add( arguments );
+ // If we're not firing and we should call right away
+ if ( !firing && flags.memory && memory ) {
+ // Trick the list into thinking it needs to be fired
+ var tmp = memory;
+ memory = undefined;
+ object.fireWith( tmp[ 0 ], tmp[ 1 ] );
+ }
+ }
+ return this;
+ },
+ // Remove a callback from the list
+ remove: function( fn ) {
+ if ( list ) {
+ var i = 0,
+ length = list.length;
+ for ( ; i < length; i++ ) {
+ if ( fn === list[ i ][ 0 ] ) {
+ list.splice( i, 1 );
+ i--;
+ // If we have some unicity property then
+ // we only need to do this once
+ if ( flags.unique || flags.relocate ) {
+ break;
+ }
+ }
+ }
+ }
+ return this;
+ },
+ // Control if a given callback is in the list
+ has: function( fn ) {
+ if ( list ) {
+ var i = 0,
+ length = list.length;
+ for ( ; i < length; i++ ) {
+ if ( fn === list[ i ][ 0 ] ) {
+ return true;
+ }
+ }
+ }
+ return false;
+ },
+ // Remove all callbacks from the list
+ empty: function() {
+ list = [];
+ return this;
+ },
+ // Have the list do nothing anymore
+ disable: function() {
+ list = stack = memory = undefined;
+ return this;
+ },
+ // Lock the list in its current state
+ lock: function() {
+ stack = undefined;
+ if ( !memory ) {
+ object.disable();
+ }
+ return this;
+ },
+ // Call all callbacks with the given context and arguments
+ fireWith: function( context, args ) {
+ var i;
+ if ( list && ( flags.once ? !memory && !firing : stack ) ) {
+ if ( firing ) {
+ stack.push( [ context, args ] );
+ } else {
+ args = args || [];
+ memory = !flags.memory || [ context, args ];
+ firing = true;
+ try {
+ for ( i = 0; list && i < list.length; i++ ) {
+ if ( list[ i ][ 1 ].apply( context, args ) === false && flags.stopOnFalse ) {
+ break;
+ }
+ }
+ } finally {
+ firing = false;
+ if ( list ) {
+ if ( !flags.once ) {
+ if ( i >= list.length && stack.length ) {
+ object.fire.apply( this, stack.shift() );
+ }
+ } else if ( !flags.memory ) {
+ object.destroy();
+ } else {
+ list = [];
+ }
+ }
+ }
+ }
+ }
+ return this;
+ },
+ // Call all the callbacks with the given arguments
+ fire: function() {
+ object.fireWith( this, arguments );
+ return this;
+ },
+ // To know if the callbacks have already been called at least once
+ fired: function() {
+ return !!memory;
+ }
+ };
+
+ return object;
+};
+
+})( jQuery );
diff --git a/src/core.js b/src/core.js
index 3a32d6f0..f68675ce 100644
--- a/src/core.js
+++ b/src/core.js
@@ -58,7 +58,7 @@ var jQuery = function( selector, context ) {
// For matching the engine and version of the browser
browserMatch,
- // The deferred used on DOM ready
+ // The callback list used on DOM ready
readyList,
// The ready event handler
@@ -257,7 +257,7 @@ jQuery.fn = jQuery.prototype = {
jQuery.bindReady();
// Add the callback
- readyList.done( fn );
+ readyList.add( fn );
return this;
},
@@ -412,7 +412,7 @@ jQuery.extend({
}
// If there are functions bound, to execute
- readyList.resolveWith( document, [ jQuery ] );
+ readyList.fireWith( document, [ jQuery ] );
// Trigger any bound ready events
if ( jQuery.fn.trigger ) {
@@ -426,7 +426,7 @@ jQuery.extend({
return;
}
- readyList = jQuery._Deferred();
+ readyList = jQuery.Callbacks( "once memory" );
// Catch cases where $(document).ready() is called after the
// browser event has already occurred.
diff --git a/src/deferred.js b/src/deferred.js
index 5cc5fb5b..fb12866f 100644
--- a/src/deferred.js
+++ b/src/deferred.js
@@ -1,169 +1,105 @@
(function( jQuery ) {
var // Promise methods
- promiseMethods = "done fail isResolved isRejected promise then always pipe".split( " " ),
+ promiseMethods = "done fail progress isResolved isRejected promise then always pipe".split( " " ),
// Static reference to slice
sliceDeferred = [].slice;
jQuery.extend({
- // 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 = {
- // done( f1, f2, ...)
- done: 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.done.apply( deferred, elem );
- } else if ( type === "function" ) {
- callbacks.push( elem );
+ Deferred: function( func ) {
+ var doneList = jQuery.Callbacks( "once memory" ),
+ failList = jQuery.Callbacks( "once memory" ),
+ progressList = jQuery.Callbacks( "memory" ),
+ promise,
+ deferred = {
+ // Copy existing methods from lists
+ done: doneList.add,
+ removeDone: doneList.remove,
+ fail: failList.add,
+ removeFail: failList.remove,
+ progress: progressList.add,
+ removeProgress: progressList.remove,
+ resolve: doneList.fire,
+ resolveWith: doneList.fireWith,
+ reject: failList.fire,
+ rejectWith: failList.fireWith,
+ ping: progressList.fire,
+ pingWith: progressList.pingWith,
+ isResolved: doneList.fired,
+ isRejected: failList.fired,
+
+ // Create Deferred-specific methods
+ then: function( doneCallbacks, failCallbacks, progressCallbacks ) {
+ deferred.done( doneCallbacks ).fail( failCallbacks ).progress( progressCallbacks );
+ return this;
+ },
+ always: function() {
+ return deferred.done.apply( deferred, arguments ).fail.apply( this, arguments );
+ },
+ pipe: function( fnDone, fnFail, fnProgress ) {
+ return jQuery.Deferred(function( newDefer ) {
+ jQuery.each( {
+ done: [ fnDone, "resolve" ],
+ fail: [ fnFail, "reject" ],
+ progress: [ fnProgress, "ping" ]
+ }, function( handler, data ) {
+ var fn = data[ 0 ],
+ action = data[ 1 ],
+ returned;
+ if ( jQuery.isFunction( fn ) ) {
+ deferred[ handler ](function() {
+ returned = fn.apply( this, arguments );
+ if ( jQuery.isFunction( returned.promise ) ) {
+ returned.promise().then( newDefer.resolve, newDefer.reject, newDefer.ping );
+ } else {
+ newDefer[ action ]( returned );
+ }
+ });
+ } else {
+ deferred[ handler ]( newDefer[ action ] );
}
+ });
+ }).promise();
+ },
+ // Get a promise for this deferred
+ // If obj is provided, the promise aspect is added to the object
+ promise: function( obj ) {
+ if ( obj == null ) {
+ if ( promise ) {
+ return promise;
}
- if ( _fired ) {
- deferred.resolveWith( _fired[ 0 ], _fired[ 1 ] );
- }
+ promise = obj = {};
}
- return this;
- },
-
- // resolve with given context and args
- resolveWith: function( context, args ) {
- if ( !cancelled && !fired && !firing ) {
- // make sure args are available (#8421)
- args = args || [];
- firing = 1;
- try {
- while( callbacks[ 0 ] ) {
- callbacks.shift().apply( context, args );
- }
- }
- finally {
- fired = [ context, args ];
- firing = 0;
- }
+ var i = promiseMethods.length;
+ while( i-- ) {
+ obj[ promiseMethods[i] ] = deferred[ promiseMethods[i] ];
}
- return this;
- },
-
- // resolve with this as context and given arguments
- resolve: function() {
- deferred.resolveWith( this, arguments );
- return this;
- },
-
- // Has this deferred been resolved?
- isResolved: function() {
- return !!( firing || fired );
- },
-
- // Cancel
- cancel: function() {
- cancelled = 1;
- callbacks = [];
- return this;
+ return obj;
}
};
- return deferred;
- },
+ // Handle lists exclusiveness
+ deferred.done( failList.disable, progressList.lock )
+ .fail( doneList.disable, progressList.lock );
- // Full fledged deferred (two callbacks list)
- Deferred: function( func ) {
- var deferred = jQuery._Deferred(),
- failDeferred = jQuery._Deferred(),
- promise;
- // Add errorDeferred methods, then and promise
- jQuery.extend( deferred, {
- then: function( doneCallbacks, failCallbacks ) {
- deferred.done( doneCallbacks ).fail( failCallbacks );
- return this;
- },
- always: function() {
- return deferred.done.apply( deferred, arguments ).fail.apply( this, arguments );
- },
- fail: failDeferred.done,
- rejectWith: failDeferred.resolveWith,
- reject: failDeferred.resolve,
- isRejected: failDeferred.isResolved,
- pipe: function( fnDone, fnFail ) {
- return jQuery.Deferred(function( newDefer ) {
- jQuery.each( {
- done: [ fnDone, "resolve" ],
- fail: [ fnFail, "reject" ]
- }, function( handler, data ) {
- var fn = data[ 0 ],
- action = data[ 1 ],
- returned;
- if ( jQuery.isFunction( fn ) ) {
- deferred[ handler ](function() {
- returned = fn.apply( this, arguments );
- if ( returned && jQuery.isFunction( returned.promise ) ) {
- returned.promise().then( newDefer.resolve, newDefer.reject );
- } else {
- newDefer[ action ]( returned );
- }
- });
- } else {
- deferred[ handler ]( newDefer[ action ] );
- }
- });
- }).promise();
- },
- // Get a promise for this deferred
- // If obj is provided, the promise aspect is added to the object
- promise: function( obj ) {
- if ( obj == null ) {
- if ( promise ) {
- return promise;
- }
- promise = obj = {};
- }
- var i = promiseMethods.length;
- while( i-- ) {
- obj[ promiseMethods[i] ] = deferred[ promiseMethods[i] ];
- }
- return obj;
- }
- });
- // Make sure only one callback list will be used
- deferred.done( failDeferred.cancel ).fail( deferred.cancel );
- // Unexpose cancel
- delete deferred.cancel;
// Call given func if any
if ( func ) {
func.call( deferred, deferred );
}
+
+ // All done!
return deferred;
},
// Deferred helper
when: function( firstParam ) {
- var args = arguments,
+ var args = sliceDeferred.call( arguments, 0 ),
i = 0,
length = args.length,
+ pValues = new Array( length ),
count = length,
+ pCount = length,
deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ?
firstParam :
jQuery.Deferred();
@@ -171,17 +107,22 @@ jQuery.extend({
return function( value ) {
args[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value;
if ( !( --count ) ) {
- // Strange bug in FF4:
- // Values changed onto the arguments object sometimes end up as undefined values
- // outside the $.when method. Cloning the object into a fresh array solves the issue
- deferred.resolveWith( deferred, sliceDeferred.call( args, 0 ) );
+ deferred.resolveWith( deferred, args );
+ }
+ };
+ }
+ function progressFunc( i ) {
+ return function( value ) {
+ pValues[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value;
+ if ( !( --count ) ) {
+ deferred.pingWith( deferred, pValue );
}
};
}
if ( length > 1 ) {
for( ; i < length; i++ ) {
- if ( args[ i ] && jQuery.isFunction( args[ i ].promise ) ) {
- args[ i ].promise().then( resolveFunc(i), deferred.reject );
+ if ( args[ i ] && args[ i ].promise && jQuery.isFunction( args[ i ].promise ) ) {
+ args[ i ].promise().then( resolveFunc(i), deferred.reject, progressFunc(i) );
} else {
--count;
}
diff --git a/src/queue.js b/src/queue.js
index 66383c19..145bfd6f 100644
--- a/src/queue.js
+++ b/src/queue.js
@@ -1,7 +1,7 @@
(function( jQuery ) {
-function handleQueueMarkDefer( elem, type, src ) {
- var deferDataKey = type + "defer",
+function handleCBList( elem, type, src ) {
+ var deferDataKey = type + "cblst",
queueDataKey = type + "queue",
markDataKey = type + "mark",
defer = jQuery.data( elem, deferDataKey, undefined, true );
@@ -14,7 +14,7 @@ function handleQueueMarkDefer( elem, type, src ) {
if ( !jQuery.data( elem, queueDataKey, undefined, true ) &&
!jQuery.data( elem, markDataKey, undefined, true ) ) {
jQuery.removeData( elem, deferDataKey, true );
- defer.resolve();
+ defer.fireWith();
}
}, 0 );
}
@@ -43,7 +43,7 @@ jQuery.extend({
jQuery.data( elem, key, count, true );
} else {
jQuery.removeData( elem, key, true );
- handleQueueMarkDefer( elem, type, "mark" );
+ handleCBList( elem, type, "mark" );
}
}
},
@@ -90,7 +90,7 @@ jQuery.extend({
if ( !queue.length ) {
jQuery.removeData( elem, type + "queue", true );
- handleQueueMarkDefer( elem, type, "queue" );
+ handleCBList( elem, type, "queue" );
}
}
});
@@ -146,7 +146,7 @@ jQuery.fn.extend({
elements = this,
i = elements.length,
count = 1,
- deferDataKey = type + "defer",
+ deferDataKey = type + "cblst",
queueDataKey = type + "queue",
markDataKey = type + "mark",
tmp;
@@ -159,9 +159,9 @@ jQuery.fn.extend({
if (( tmp = jQuery.data( elements[ i ], deferDataKey, undefined, true ) ||
( jQuery.data( elements[ i ], queueDataKey, undefined, true ) ||
jQuery.data( elements[ i ], markDataKey, undefined, true ) ) &&
- jQuery.data( elements[ i ], deferDataKey, jQuery._Deferred(), true ) )) {
+ jQuery.data( elements[ i ], deferDataKey, jQuery.Callbacks( "once memory" ), true ) )) {
count++;
- tmp.done( resolve );
+ tmp.add( resolve );
}
}
resolve();
diff --git a/test/data/offset/absolute.html b/test/data/offset/absolute.html
index 9d7990a3..2002a535 100644
--- a/test/data/offset/absolute.html
+++ b/test/data/offset/absolute.html
@@ -16,7 +16,7 @@
#positionTest { position: absolute; }
-
+
diff --git a/test/data/offset/body.html b/test/data/offset/body.html
index 8dbf282d..a661637f 100644
--- a/test/data/offset/body.html
+++ b/test/data/offset/body.html
@@ -9,7 +9,7 @@
#marker { position: absolute; border: 2px solid #000; width: 50px; height: 50px; background: #ccc; }
-
+
diff --git a/test/data/offset/fixed.html b/test/data/offset/fixed.html
index 81ba4ca7..c6eb9277 100644
--- a/test/data/offset/fixed.html
+++ b/test/data/offset/fixed.html
@@ -13,7 +13,7 @@
#marker { position: absolute; border: 2px solid #000; width: 50px; height: 50px; background: #ccc; }
-
+
diff --git a/test/data/offset/relative.html b/test/data/offset/relative.html
index 280a2fc0..6386eebd 100644
--- a/test/data/offset/relative.html
+++ b/test/data/offset/relative.html
@@ -11,7 +11,7 @@
#marker { position: absolute; border: 2px solid #000; width: 50px; height: 50px; background: #ccc; }
-
+
diff --git a/test/data/offset/scroll.html b/test/data/offset/scroll.html
index a0d1f4d1..0890d0ad 100644
--- a/test/data/offset/scroll.html
+++ b/test/data/offset/scroll.html
@@ -14,7 +14,7 @@
#marker { position: absolute; border: 2px solid #000; width: 50px; height: 50px; background: #ccc; }
-
+
diff --git a/test/data/offset/static.html b/test/data/offset/static.html
index a61b6d10..687e93e8 100644
--- a/test/data/offset/static.html
+++ b/test/data/offset/static.html
@@ -11,7 +11,7 @@
#marker { position: absolute; border: 2px solid #000; width: 50px; height: 50px; background: #ccc; }
-
+
diff --git a/test/data/offset/table.html b/test/data/offset/table.html
index 11fb0e79..06774325 100644
--- a/test/data/offset/table.html
+++ b/test/data/offset/table.html
@@ -11,7 +11,7 @@
#marker { position: absolute; border: 2px solid #000; width: 50px; height: 50px; background: #ccc; }
-
+
diff --git a/test/index.html b/test/index.html
index 4b4c9855..bb563846 100644
--- a/test/index.html
+++ b/test/index.html
@@ -9,6 +9,7 @@
+
@@ -34,8 +35,9 @@
-
+
+
diff --git a/test/unit/deferred.js b/test/unit/deferred.js
index 89c9c612..0d8ef77c 100644
--- a/test/unit/deferred.js
+++ b/test/unit/deferred.js
@@ -1,111 +1,5 @@
module("deferred", { teardown: moduleTeardown });
-jQuery.each( [ "", " - new operator" ], function( _, withNew ) {
-
- function createDeferred() {
- return withNew ? new jQuery._Deferred() : jQuery._Deferred();
- }
-
- test("jQuery._Deferred" + withNew, function() {
-
- expect( 11 );
-
- var deferred,
- object,
- test;
-
- deferred = createDeferred();
-
- test = false;
-
- deferred.done( function( value ) {
- equals( value , "value" , "Test pre-resolve callback" );
- test = true;
- } );
-
- deferred.resolve( "value" );
-
- ok( test , "Test pre-resolve callbacks called right away" );
-
- test = false;
-
- deferred.done( function( value ) {
- equals( value , "value" , "Test post-resolve callback" );
- test = true;
- } );
-
- ok( test , "Test post-resolve callbacks called right away" );
-
- deferred.cancel();
-
- test = true;
-
- deferred.done( function() {
- ok( false , "Cancel was ignored" );
- test = false;
- } );
-
- ok( test , "Test cancel" );
-
- deferred = createDeferred().resolve();
-
- try {
- deferred.done( function() {
- throw "Error";
- } , function() {
- ok( true , "Test deferred do not cancel on exception" );
- } );
- } catch( e ) {
- strictEqual( e , "Error" , "Test deferred propagates exceptions");
- deferred.done();
- }
-
- test = "";
- deferred = createDeferred().done( function() {
-
- test += "A";
-
- }, function() {
-
- test += "B";
-
- } ).resolve();
-
- strictEqual( test , "AB" , "Test multiple done parameters" );
-
- test = "";
-
- deferred.done( function() {
-
- deferred.done( function() {
-
- test += "C";
-
- } );
-
- test += "A";
-
- }, function() {
-
- test += "B";
- } );
-
- strictEqual( test , "ABC" , "Test done callbacks order" );
-
- deferred = createDeferred();
-
- deferred.resolveWith( jQuery , [ document ] ).done( function( doc ) {
- ok( this === jQuery && arguments.length === 1 && doc === document , "Test fire context & args" );
- });
-
- // #8421
- deferred = createDeferred();
- deferred.resolveWith().done(function() {
- ok( true, "Test resolveWith can be called with no argument" );
- });
- });
-} );
-
jQuery.each( [ "", " - new operator" ], function( _, withNew ) {
function createDeferred( fn ) {