diff --git a/src/ngAnimate/animateCss.js b/src/ngAnimate/animateCss.js index 34eb29e02703..1039db3425df 100644 --- a/src/ngAnimate/animateCss.js +++ b/src/ngAnimate/animateCss.js @@ -474,6 +474,8 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { var maxDelayTime; var maxDuration; var maxDurationTime; + var startTime; + var events = []; if (options.duration === 0 || (!$sniffer.animations && !$sniffer.transitions)) { return closeAndReturnNoopAnimator(); @@ -747,6 +749,11 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { options.onDone(); } + // Remove the transitionend / animationend listener(s) + if (events) { + element.off(events.join(' '), onAnimationProgress); + } + // if the preparation function fails then the promise is not setup if (runner) { runner.complete(!rejected); @@ -782,6 +789,30 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { }; } + function onAnimationProgress(event) { + event.stopPropagation(); + var ev = event.originalEvent || event; + var timeStamp = ev.$manualTimeStamp || ev.timeStamp || Date.now(); + + /* Firefox (or possibly just Gecko) likes to not round values up + * when a ms measurement is used for the animation */ + var elapsedTime = parseFloat(ev.elapsedTime.toFixed(ELAPSED_TIME_MAX_DECIMAL_PLACES)); + + /* $manualTimeStamp is a mocked timeStamp value which is set + * within browserTrigger(). This is only here so that tests can + * mock animations properly. Real events fallback to event.timeStamp, + * or, if they don't, then a timeStamp is automatically created for them. + * We're checking to see if the timeStamp surpasses the expected delay, + * but we're using elapsedTime instead of the timeStamp on the 2nd + * pre-condition since animationPauseds sometimes close off early */ + if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) { + // we set this flag to ensure that if the transition is paused then, when resumed, + // the animation will automatically close itself since transitions cannot be paused. + animationCompleted = true; + close(); + } + } + function start() { if (animationClosed) return; if (!node.parentNode) { @@ -789,8 +820,6 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { return; } - var startTime, events = []; - // even though we only pause keyframe animations here the pause flag // will still happen when transitions are used. Only the transition will // not be paused since that is not possible. If the animation ends when @@ -953,30 +982,6 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { element.removeData(ANIMATE_TIMER_KEY); } } - - function onAnimationProgress(event) { - event.stopPropagation(); - var ev = event.originalEvent || event; - var timeStamp = ev.$manualTimeStamp || ev.timeStamp || Date.now(); - - /* Firefox (or possibly just Gecko) likes to not round values up - * when a ms measurement is used for the animation */ - var elapsedTime = parseFloat(ev.elapsedTime.toFixed(ELAPSED_TIME_MAX_DECIMAL_PLACES)); - - /* $manualTimeStamp is a mocked timeStamp value which is set - * within browserTrigger(). This is only here so that tests can - * mock animations properly. Real events fallback to event.timeStamp, - * or, if they don't, then a timeStamp is automatically created for them. - * We're checking to see if the timeStamp surpasses the expected delay, - * but we're using elapsedTime instead of the timeStamp on the 2nd - * pre-condition since animations sometimes close off early */ - if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) { - // we set this flag to ensure that if the transition is paused then, when resumed, - // the animation will automatically close itself since transitions cannot be paused. - animationCompleted = true; - close(); - } - } } }; }]; diff --git a/test/ngAnimate/.jshintrc b/test/ngAnimate/.jshintrc index b307fb405952..fcaf04058079 100644 --- a/test/ngAnimate/.jshintrc +++ b/test/ngAnimate/.jshintrc @@ -8,6 +8,10 @@ "applyAnimationStyles": false, "applyAnimationFromStyles": false, "applyAnimationToStyles": false, - "applyAnimationClassesFactory": false + "applyAnimationClassesFactory": false, + "TRANSITIONEND_EVENT": false, + "TRANSITION_PROP": false, + "ANIMATION_PROP": false, + "ANIMATIONEND_EVENT": false } } diff --git a/test/ngAnimate/animateCssSpec.js b/test/ngAnimate/animateCssSpec.js index 52182493f61a..4a828ecc372e 100644 --- a/test/ngAnimate/animateCssSpec.js +++ b/test/ngAnimate/animateCssSpec.js @@ -12,6 +12,16 @@ describe("ngAnimate $animateCss", function() { : expect(className).not.toMatch(regex); } + function keyframeProgress(element, duration, delay) { + browserTrigger(element, 'animationend', + { timeStamp: Date.now() + ((delay || 1) * 1000), elapsedTime: duration }); + } + + function transitionProgress(element, duration, delay) { + browserTrigger(element, 'transitionend', + { timeStamp: Date.now() + ((delay || 1) * 1000), elapsedTime: duration }); + } + var fakeStyle = { color: 'blue' }; @@ -355,16 +365,6 @@ describe("ngAnimate $animateCss", function() { assert.toHaveClass('ng-enter-active'); } - function keyframeProgress(element, duration, delay) { - browserTrigger(element, 'animationend', - { timeStamp: Date.now() + ((delay || 1) * 1000), elapsedTime: duration }); - } - - function transitionProgress(element, duration, delay) { - browserTrigger(element, 'transitionend', - { timeStamp: Date.now() + ((delay || 1) * 1000), elapsedTime: duration }); - } - beforeEach(inject(function($rootElement, $document) { element = jqLite('
'); $rootElement.append(element); @@ -1404,6 +1404,105 @@ describe("ngAnimate $animateCss", function() { expect(count.stagger).toBe(2); })); }); + + describe('transitionend/animationend event listeners', function() { + var element, elementOnSpy, elementOffSpy, progress; + + function setStyles(event) { + switch (event) { + case TRANSITIONEND_EVENT: + ss.addRule('.ng-enter', 'transition: 10s linear all;'); + progress = transitionProgress; + break; + case ANIMATIONEND_EVENT: + ss.addRule('.ng-enter', '-webkit-animation: animation 10s;' + + 'animation: animation 10s;'); + progress = keyframeProgress; + break; + } + } + + beforeEach(inject(function($rootElement, $document) { + element = jqLite('
'); + $rootElement.append(element); + jqLite($document[0].body).append($rootElement); + + elementOnSpy = spyOn(element, 'on').andCallThrough(); + elementOffSpy = spyOn(element, 'off').andCallThrough(); + })); + + they('should remove the $prop event listeners on cancel', + [TRANSITIONEND_EVENT, ANIMATIONEND_EVENT], function(event) { + inject(function($animateCss) { + + setStyles(event); + + var animator = $animateCss(element, { + event: 'enter', + structural: true + }); + + var runner = animator.start(); + triggerAnimationStartFrame(); + + expect(elementOnSpy).toHaveBeenCalledOnce(); + expect(elementOnSpy.mostRecentCall.args[0]).toBe(event); + + runner.cancel(); + + expect(elementOffSpy).toHaveBeenCalledOnce(); + expect(elementOffSpy.mostRecentCall.args[0]).toBe(event); + }); + }); + + they("should remove the $prop event listener when the animation is closed", + [TRANSITIONEND_EVENT, ANIMATIONEND_EVENT], function(event) { + inject(function($animateCss) { + + setStyles(event); + + var animator = $animateCss(element, { + event: 'enter', + structural: true + }); + + var runner = animator.start(); + triggerAnimationStartFrame(); + + expect(elementOnSpy).toHaveBeenCalledOnce(); + expect(elementOnSpy.mostRecentCall.args[0]).toBe(event); + + progress(element, 10); + + expect(elementOffSpy).toHaveBeenCalledOnce(); + expect(elementOffSpy.mostRecentCall.args[0]).toBe(event); + }); + }); + + they("should remove the $prop event listener when the closing timeout occurs", + [TRANSITIONEND_EVENT, ANIMATIONEND_EVENT], function(event) { + inject(function($animateCss, $timeout) { + + setStyles(event); + + var animator = $animateCss(element, { + event: 'enter', + structural: true + }); + + animator.start(); + triggerAnimationStartFrame(); + + expect(elementOnSpy).toHaveBeenCalledOnce(); + expect(elementOnSpy.mostRecentCall.args[0]).toBe(event); + + $timeout.flush(15000); + + expect(elementOffSpy).toHaveBeenCalledOnce(); + expect(elementOffSpy.mostRecentCall.args[0]).toBe(event); + }); + }); + }); }); it('should avoid applying the same cache to an element a follow-up animation is run on the same element',