From 3e72faedea7b6a7a30156ddf8293873c8f40906e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Mon, 4 May 2015 18:00:54 -0700 Subject: [PATCH] fix($animateCss): ensure that detection cache is flushed after animation is closed In the post-quiet animation detection phase of $animateCss there is a call to getComputedStyle which is cached, however, the cache is not cleared until the next animation is run. This causes a caching issue since the next animation's cache data may be read and provided incorrectly. This patch clears the cache after each animation has closed itself. Closes #11723 --- src/ngAnimate/animateCss.js | 29 +++++++++++++++++++++-------- test/ngAnimate/animateCssSpec.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/src/ngAnimate/animateCss.js b/src/ngAnimate/animateCss.js index b432ee652a44..fa83a7191edb 100644 --- a/src/ngAnimate/animateCss.js +++ b/src/ngAnimate/animateCss.js @@ -424,7 +424,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { var KEY = "$$ngAnimateParentKey"; var parentNode = node.parentNode; var parentID = parentNode[KEY] || (parentNode[KEY] = ++parentCounter); - return parentID + '-' + node.getAttribute('class') + '-' + extraClasses; + return parentID + '-' + (node.getAttribute('class') || '') + '-' + (extraClasses || ''); } function computeCachedCssStyles(node, className, cacheKey, properties) { @@ -472,6 +472,11 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { return stagger || {}; } + function flushGCSCache() { + gcsLookup.flush(); + gcsStaggerLookup.flush(); + } + var bod = $document[0].body; var cancelLastRAFRequest; var rafWaitQueue = []; @@ -482,8 +487,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { rafWaitQueue.push(callback); cancelLastRAFRequest = $$rAF(function() { cancelLastRAFRequest = null; - gcsLookup.flush(); - gcsStaggerLookup.flush(); + flushGCSCache(); //the line below will force the browser to perform a repaint so //that all the animated elements within the animation frame will @@ -514,7 +518,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { ? Math.max(aD, tD) : (aD || tD); timings.maxDuration = Math.max( - timings.animationDuration * timings.animationIterationCount, + (timings.animationDuration * timings.animationIterationCount) || 0, timings.transitionDuration); return timings; @@ -525,7 +529,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { options = prepareAnimationOptions(options); var temporaryStyles = []; - var classes = element.attr('class'); + var classes = element.attr('class') || ''; var styles = packageStyles(options); var animationClosed; var animationPaused; @@ -649,7 +653,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { var timings = computeTimings(node, fullClassName, cacheKey); var relativeDelay = timings.maxDelay; - maxDelay = Math.max(relativeDelay, 0); + maxDelay = Math.max(relativeDelay || 0, 0); maxDuration = timings.maxDuration; var flags = {}; @@ -715,6 +719,11 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { start: function() { if (animationClosed) return; + // the code below will wait until the first RAF has passed. By waiting + // we allow multiple similar animations to be grouped together to allow + // for stagger calculations. This waiting phase is known as the quiet + // phase and any code that runs after is known as "post-quiet" code. + runnerHost = { end: endFn, cancel: cancelFn, @@ -765,6 +774,10 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { applyAnimationClasses(element, options); applyAnimationStyles(element, options); + // we need to clear the cache since the post-quiet state may add and remove CSS + // classes which contain follow-up animation data which will be cached. + $$rAF(flushGCSCache); + // the reason why we have this option is to allow a synchronous closing callback // that is fired as SOON as the animation ends (when the CSS is removed) or if // the animation never takes off at all. A good example is a leave animation since @@ -860,7 +873,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { timings = computeTimings(node, fullClassName, cacheKey); relativeDelay = timings.maxDelay; - maxDelay = Math.max(relativeDelay, 0); + maxDelay = Math.max(relativeDelay || 0, 0); maxDuration = timings.maxDuration; if (maxDuration === 0) { @@ -877,7 +890,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { ? parseFloat(options.delay) : relativeDelay; - maxDelay = Math.max(relativeDelay, 0); + maxDelay = Math.max(relativeDelay || 0, 0); var delayStyle; if (flags.applyTransitionDelay) { diff --git a/test/ngAnimate/animateCssSpec.js b/test/ngAnimate/animateCssSpec.js index 5ab7e116221c..75d75111dfd7 100644 --- a/test/ngAnimate/animateCssSpec.js +++ b/test/ngAnimate/animateCssSpec.js @@ -1069,6 +1069,36 @@ describe("ngAnimate $animateCss", function() { expect(count.normal).toBe(3); })); + it("should cache the post-quiet state detection and flush one frame after the animation is complete", + inject(function($animateCss, $document, $rootElement, $$rAF) { + + var i, animator, elms = []; + for (i = 0; i < 5; i++) { + var elm = jqLite('
' + i + '
'); + $rootElement.append(elm); + animator = $animateCss(elm, { to: {color:'green'}, duration: 0.5 }); + var runner = animator.start(); + elms.push(elm); + } + + expect(count.normal).toBe(2); //first + stagger + triggerAnimationStartFrame(); + + expect(count.normal).toBe(3); //first + stagger + post-quiet + for (i = 0; i < elms.length; i++) { + browserTrigger(elms[i], 'transitionend', + { timeStamp: Date.now() + 1000, elapsedTime: 1 }); + } + + $$rAF.flush(); + + for (i = 0; i < elms.length; i++) { + animator = $animateCss(elms[i], { to: {color:'red'}, duration: 0.8 }); + } + + expect(count.normal).toBe(5); //first + stagger + post-quiet + first + stagger + })); + it("should cache frequent calls to getComputedStyle for stagger animations before the next animation frame kicks in", inject(function($animateCss, $document, $rootElement, $$rAF) {