From a61f4da0e88d701efa98a5f8ae07c0e669ecd388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Mon, 3 Aug 2015 14:53:17 -0400 Subject: [PATCH] fix($animateCss): properly handle cancellation timeouts for follow-up animations Prior to this fix if `$animateCss` was called multiple on the same element with new animation data then the preceeding fallback timout would cause the animation to cancel midway. This fix ensures that `$animateCss` can be triggered multiple times and only when the final timeout has passed then all animations will be closed. Closes #12359 --- src/ngAnimate/animateCss.js | 38 ++++++++++++++++++---- test/ngAnimate/animateCssSpec.js | 56 ++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 6 deletions(-) diff --git a/src/ngAnimate/animateCss.js b/src/ngAnimate/animateCss.js index dd40039ee62f..f706e13c06a2 100644 --- a/src/ngAnimate/animateCss.js +++ b/src/ngAnimate/animateCss.js @@ -1,5 +1,7 @@ 'use strict'; +var ANIMATE_TIMER_KEY = '$$animateCss'; + /** * @ngdoc service * @name $animateCss @@ -861,17 +863,41 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { } startTime = Date.now(); - element.on(events.join(' '), onAnimationProgress); - $timeout(onAnimationExpired, maxDelayTime + CLOSING_TIME_BUFFER * maxDurationTime, false); + var timerTime = maxDelayTime + CLOSING_TIME_BUFFER * maxDurationTime; + var endTime = startTime + timerTime; + + var animationsData = element.data(ANIMATE_TIMER_KEY) || []; + var setupFallbackTimer = true; + if (animationsData.length) { + var currentTimerData = animationsData[0]; + setupFallbackTimer = endTime > currentTimerData.expectedEndTime; + if (setupFallbackTimer) { + $timeout.cancel(currentTimerData.timer); + } else { + animationsData.push(close); + } + } + if (setupFallbackTimer) { + var timer = $timeout(onAnimationExpired, timerTime, false); + animationsData[0] = { + timer: timer, + expectedEndTime: endTime + }; + animationsData.push(close); + element.data(ANIMATE_TIMER_KEY, animationsData); + } + + element.on(events.join(' '), onAnimationProgress); applyAnimationToStyles(element, options); } function onAnimationExpired() { - // although an expired animation is a failed animation, getting to - // this outcome is very easy if the CSS code screws up. Therefore we - // should still continue normally as if the animation completed correctly. - close(); + var animationsData = element.data(ANIMATE_TIMER_KEY); + for (var i = 1; i < animationsData.length; i++) { + animationsData[i](); + } + element.removeData(ANIMATE_TIMER_KEY); } function onAnimationProgress(event) { diff --git a/test/ngAnimate/animateCssSpec.js b/test/ngAnimate/animateCssSpec.js index d1dd04562a08..854e64a43151 100644 --- a/test/ngAnimate/animateCssSpec.js +++ b/test/ngAnimate/animateCssSpec.js @@ -1135,6 +1135,62 @@ describe("ngAnimate $animateCss", function() { expect(passed).toBe(true); expect(failed).not.toBe(true); })); + + it("should close all stacked animations after the last timeout runs on the same element", + inject(function($animateCss, $$body, $rootElement, $timeout) { + + var now = 0; + spyOn(Date, 'now').andCallFake(function() { + return now; + }); + + var cancelSpy = spyOn($timeout, 'cancel').andCallThrough(); + var doneSpy = jasmine.createSpy(); + + ss.addRule('.elm', 'transition:1s linear all;'); + ss.addRule('.elm.red', 'background:red;'); + ss.addRule('.elm.blue', 'transition:2s linear all; background:blue;'); + ss.addRule('.elm.green', 'background:green;'); + + var element = jqLite('
'); + $rootElement.append(element); + $$body.append($rootElement); + + // timeout will be at 1500s + animate(element, 'red', doneSpy); + expect(doneSpy).not.toHaveBeenCalled(); + + fastForwardClock(500); //1000s left to go + + // timeout will not be at 500 + 3000s = 3500s + animate(element, 'blue', doneSpy); + expect(doneSpy).not.toHaveBeenCalled(); + expect(cancelSpy).toHaveBeenCalled(); + + cancelSpy.reset(); + + // timeout will not be set again since the former animation is longer + animate(element, 'green', doneSpy); + expect(doneSpy).not.toHaveBeenCalled(); + expect(cancelSpy).not.toHaveBeenCalled(); + + // this will close the animations fully + fastForwardClock(3500); + expect(doneSpy).toHaveBeenCalled(); + expect(doneSpy.callCount).toBe(3); + + function fastForwardClock(time) { + now += time; + $timeout.flush(time); + } + + function animate(element, klass, onDone) { + var animator = $animateCss(element, { addClass: klass }).start(); + animator.done(onDone); + triggerAnimationStartFrame(); + return animator; + } + })); }); describe("getComputedStyle", function() {