Skip to content

Commit

Permalink
Fix #11797. Use Deferred for better animation callbacks. Closes gh-830.
Browse files Browse the repository at this point in the history
In particular, an animation stopped with `gotoEnd` will be rejected.
  • Loading branch information
gnarf authored and dmethvin committed Jun 22, 2012
1 parent 9bb3494 commit 36369ce
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 37 deletions.
77 changes: 44 additions & 33 deletions src/effects.js
Expand Up @@ -56,7 +56,7 @@ function createFxNow() {
return ( fxNow = jQuery.now() ); return ( fxNow = jQuery.now() );
} }


function callTweeners( animation, props ) { function createTweens( animation, props ) {
jQuery.each( props, function( prop, value ) { jQuery.each( props, function( prop, value ) {
var collection = ( tweeners[ prop ] || [] ).concat( tweeners[ "*" ] ), var collection = ( tweeners[ prop ] || [] ).concat( tweeners[ "*" ] ),
index = 0, index = 0,
Expand All @@ -76,16 +76,9 @@ function Animation( elem, properties, options ) {
index = 0, index = 0,
tweenerIndex = 0, tweenerIndex = 0,
length = animationPrefilters.length, length = animationPrefilters.length,
finished = jQuery.Deferred(), deferred = jQuery.Deferred().always( function() {
deferred = jQuery.Deferred().always(function( ended ) {

// don't match elem in the :animated selector // don't match elem in the :animated selector
delete tick.elem; delete tick.elem;
if ( deferred.state() === "resolved" || ended ) {

// fire callbacks
finished.resolveWith( this );
}
}), }),
tick = function() { tick = function() {
var currentTime = fxNow || createFxNow(), var currentTime = fxNow || createFxNow(),
Expand All @@ -101,7 +94,7 @@ function Animation( elem, properties, options ) {
if ( percent < 1 && length ) { if ( percent < 1 && length ) {
return remaining; return remaining;
} else { } else {
deferred.resolveWith( elem, [ currentTime ] ); deferred.resolveWith( elem, [ animation ] );
return false; return false;
} }
}, },
Expand All @@ -113,7 +106,6 @@ function Animation( elem, properties, options ) {
originalOptions: options, originalOptions: options,
startTime: fxNow || createFxNow(), startTime: fxNow || createFxNow(),
duration: options.duration, duration: options.duration,
finish: finished.done,
tweens: [], tweens: [],
createTween: function( prop, end, easing ) { createTween: function( prop, end, easing ) {
var tween = jQuery.Tween( elem, animation.opts, prop, end, var tween = jQuery.Tween( elem, animation.opts, prop, end,
Expand All @@ -130,7 +122,14 @@ function Animation( elem, properties, options ) {
for ( ; index < length ; index++ ) { for ( ; index < length ; index++ ) {
animation.tweens[ index ].run( 1 ); animation.tweens[ index ].run( 1 );
} }
deferred.rejectWith( elem, [ gotoEnd ] );
// resolve when we played the last frame
// otherwise, reject
if ( gotoEnd ) {
deferred.resolveWith( elem, [ animation, gotoEnd ] );
} else {
deferred.rejectWith( elem, [ animation, gotoEnd ] );
}
return this; return this;
} }
}), }),
Expand All @@ -139,14 +138,17 @@ function Animation( elem, properties, options ) {
propFilter( props, animation.opts.specialEasing ); propFilter( props, animation.opts.specialEasing );


for ( ; index < length ; index++ ) { for ( ; index < length ; index++ ) {
result = animationPrefilters[ index ].call( animation, result = animationPrefilters[ index ].call( animation, elem, props, animation.opts );
elem, props, animation.opts );
if ( result ) { if ( result ) {
return result; return result;
} }
} }


callTweeners( animation, props ); createTweens( animation, props );

if ( jQuery.isFunction( animation.opts.start ) ) {
animation.opts.start.call( elem, animation );
}


jQuery.fx.timer( jQuery.fx.timer(
jQuery.extend( tick, { jQuery.extend( tick, {
Expand All @@ -155,7 +157,11 @@ function Animation( elem, properties, options ) {
elem: elem elem: elem
}) })
); );
return animation;
// attach callbacks from options
return animation.done( animation.opts.done, animation.opts.complete )
.fail( animation.opts.fail )
.always( animation.opts.always );
} }


function propFilter( props, specialEasing ) { function propFilter( props, specialEasing ) {
Expand Down Expand Up @@ -246,11 +252,16 @@ function defaultPrefilter( elem, props, opts ) {
}; };
} }
hooks.unqueued++; hooks.unqueued++;

anim.always(function() { anim.always(function() {
hooks.unqueued--; // doing this makes sure that the complete handler will be called
if ( !jQuery.queue( elem, "fx" ).length ) { // before this completes
hooks.empty.fire(); anim.always(function() {
} hooks.unqueued--;
if ( !jQuery.queue( elem, "fx" ).length ) {
hooks.empty.fire();
}
});
}); });
} }


Expand Down Expand Up @@ -281,7 +292,7 @@ function defaultPrefilter( elem, props, opts ) {
if ( opts.overflow ) { if ( opts.overflow ) {
style.overflow = "hidden"; style.overflow = "hidden";
if ( !jQuery.support.shrinkWrapBlocks ) { if ( !jQuery.support.shrinkWrapBlocks ) {
anim.finish(function() { anim.done(function() {
style.overflow = opts.overflow[ 0 ]; style.overflow = opts.overflow[ 0 ];
style.overflowX = opts.overflow[ 1 ]; style.overflowX = opts.overflow[ 1 ];
style.overflowY = opts.overflow[ 2 ]; style.overflowY = opts.overflow[ 2 ];
Expand All @@ -308,11 +319,11 @@ function defaultPrefilter( elem, props, opts ) {
if ( hidden ) { if ( hidden ) {
jQuery( elem ).show(); jQuery( elem ).show();
} else { } else {
anim.finish(function() { anim.done(function() {
jQuery( elem ).hide(); jQuery( elem ).hide();
}); });
} }
anim.finish(function() { anim.done(function() {
var prop; var prop;
jQuery.removeData( elem, "fxshow", true ); jQuery.removeData( elem, "fxshow", true );
for ( prop in orig ) { for ( prop in orig ) {
Expand Down Expand Up @@ -438,19 +449,19 @@ jQuery.fn.extend({
.end().animate({ opacity: to }, speed, easing, callback ); .end().animate({ opacity: to }, speed, easing, callback );
}, },
animate: function( prop, speed, easing, callback ) { animate: function( prop, speed, easing, callback ) {
var optall = jQuery.speed( speed, easing, callback ), var empty = jQuery.isEmptyObject( prop ),
optall = jQuery.speed( speed, easing, callback ),
doAnimation = function() { doAnimation = function() {
Animation( this, prop, optall ).finish( optall.complete ); // Operate on a copy of prop so per-property easing won't be lost
}; var anim = Animation( this, jQuery.extend( {}, prop ), optall );


if ( jQuery.isEmptyObject( prop ) ) { // Empty animations resolve immediately
return this.each( optall.complete, [ false ] ); if ( empty ) {
} anim.stop( true );

}
// Do not change referenced properties as per-property easing will be lost };
prop = jQuery.extend( {}, prop );


return optall.queue === false ? return empty || optall.queue === false ?
this.each( doAnimation ) : this.each( doAnimation ) :
this.queue( optall.queue, doAnimation ); this.queue( optall.queue, doAnimation );
}, },
Expand Down
69 changes: 65 additions & 4 deletions test/unit/effects.js
Expand Up @@ -1369,7 +1369,7 @@ test("Do not append px to 'fill-opacity' #9548", 1, function() {
}); });


// Start 1.8 Animation tests // Start 1.8 Animation tests
asyncTest( "jQuery.Animation( object, props, opts )", 1, function() { asyncTest( "jQuery.Animation( object, props, opts )", 4, function() {
var testObject = { var testObject = {
foo: 0, foo: 0,
bar: 1, bar: 1,
Expand All @@ -1381,11 +1381,16 @@ asyncTest( "jQuery.Animation( object, props, opts )", 1, function() {
width: 200 width: 200
}; };


jQuery.Animation( testObject, testDest, { duration: 1 }) var animation = jQuery.Animation( testObject, testDest, { duration: 1 });
.done( function() { animation.done(function() {
deepEqual( testObject, testDest, "Animated foo and bar" ); for ( var prop in testDest ) {
equal( testObject[ prop ], testDest[ prop ], "Animated: " + prop );
}
animation.done(function() {
deepEqual( testObject, testDest, "No unexpected properties" );
start(); start();
}); });
});
}); });


asyncTest( "Animate Option: step: function( percent, tween )", 1, function() { asyncTest( "Animate Option: step: function( percent, tween )", 1, function() {
Expand Down Expand Up @@ -1660,4 +1665,60 @@ asyncTest( "animate does not change start value for non-px animation (#7109)", 1
}); });
}); });


asyncTest("Animation callbacks (#11797)", 8, function() {
var targets = jQuery("#foo").children(),
done = false;

targets.eq( 0 ).animate( {}, {
duration: 10,
done: function() {
ok( true, "empty: done" );
},
fail: function() {
ok( false, "empty: fail" );
},
always: function() {
ok( true, "empty: always" );
done = true;
}
});

ok( done, "animation done" );

done = false;
targets.eq( 1 ).animate({
opacity: 0
}, {
duration: 10,
done: function() {
ok( false, "stopped: done" );
},
fail: function() {
ok( true, "stopped: fail" );
},
always: function() {
ok( true, "stopped: always" );
done = true;
}
}).stop();

ok( done, "animation stopped" );

targets.eq( 2 ).animate({
opacity: 0
}, {
duration: 10,
done: function() {
ok( true, "async: done" );
},
fail: function() {
ok( false, "async: fail" );
},
always: function() {
ok( true, "async: always" );
start();
}
});
});

} // if ( jQuery.fx ) } // if ( jQuery.fx )

1 comment on commit 36369ce

@gnarf
Copy link
Member Author

@gnarf gnarf commented on 36369ce Jun 22, 2012

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heavy contributions from @gibson042 on this one also.

Please sign in to comment.