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 ) {