diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index ee637b3ba4a1..9b09cad23d12 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -200,6 +200,7 @@ angular.module('ngAnimate', ['ng']) var selectors = $animateProvider.$$selectors; var NG_ANIMATE_STATE = '$$ngAnimateState'; + var NG_ANIMATE_CLASS_NAME = 'ng-animate'; var rootAnimateState = {running:true}; $provide.decorator('$animate', ['$delegate', '$injector', '$sniffer', '$rootElement', '$timeout', '$rootScope', function($delegate, $injector, $sniffer, $rootElement, $timeout, $rootScope) { @@ -320,6 +321,7 @@ angular.module('ngAnimate', ['ng']) * @param {function()=} done callback function that will be called once the animation is complete */ leave : function(element, done) { + cancelChildAnimations(element); $rootScope.$$postDigest(function() { performAnimation('leave', 'ng-leave', element, null, null, function() { $delegate.leave(element, done); @@ -358,6 +360,7 @@ angular.module('ngAnimate', ['ng']) * @param {function()=} done callback function that will be called once the animation is complete */ move : function(element, parent, after, done) { + cancelChildAnimations(element); $delegate.move(element, parent, after); $rootScope.$$postDigest(function() { performAnimation('move', 'ng-move', element, null, null, function() { @@ -503,6 +506,10 @@ angular.module('ngAnimate', ['ng']) done:done }); + //the ng-animate class does nothing, but it's here to allow for + //parent animations to find and cancel child animations when needed + element.addClass(NG_ANIMATE_CLASS_NAME); + forEach(animations, function(animation, index) { var fn = function() { progress(index); @@ -519,12 +526,6 @@ angular.module('ngAnimate', ['ng']) } }); - function cancelAnimations(animations) { - var isCancelledFlag = true; - forEach(animations, function(animation) { - (animation.endFn || noop)(isCancelledFlag); - }); - } function progress(index) { animations[index].done = true; @@ -538,11 +539,34 @@ angular.module('ngAnimate', ['ng']) function done() { if(!done.hasBeenRun) { done.hasBeenRun = true; - element.removeData(NG_ANIMATE_STATE); + cleanup(element); (onComplete || noop)(); } } } + + function cancelChildAnimations(element) { + angular.forEach(element[0].querySelectorAll('.' + NG_ANIMATE_CLASS_NAME), function(element) { + element = angular.element(element); + var data = element.data(NG_ANIMATE_STATE); + if(data) { + cancelAnimations(data.animations); + cleanup(element); + } + }); + } + + function cancelAnimations(animations) { + var isCancelledFlag = true; + forEach(animations, function(animation) { + (animation.endFn || noop)(isCancelledFlag); + }); + } + + function cleanup(element) { + element.removeClass(NG_ANIMATE_CLASS_NAME); + element.removeData(NG_ANIMATE_STATE); + } }]); $animateProvider.register('', ['$window', '$sniffer', function($window, $sniffer) { diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index 90868ef6abc4..5e53c5850f03 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -696,8 +696,9 @@ describe("ngAnimate", function() { $rootScope.$digest(); if ($sniffer.transitions) { - expect(element.hasClass('abc ng-enter')).toBe(true); - expect(element.hasClass('abc ng-enter ng-enter-active')).toBe(true); + expect(element.hasClass('abc')).toBe(true); + expect(element.hasClass('ng-enter')).toBe(true); + expect(element.hasClass('ng-enter-active')).toBe(true); browserTrigger(element,'transitionend', { timeStamp: Date.now() + 22000, elapsedTime: 22000 }); } expect(element.hasClass('abc')).toBe(true); @@ -708,7 +709,8 @@ describe("ngAnimate", function() { if ($sniffer.transitions) { expect(element.hasClass('xyz')).toBe(true); - expect(element.hasClass('xyz ng-enter ng-enter-active')).toBe(true); + expect(element.hasClass('ng-enter')).toBe(true); + expect(element.hasClass('ng-enter-active')).toBe(true); browserTrigger(element,'transitionend', { timeStamp: Date.now() + 11000, elapsedTime: 11000 }); } expect(element.hasClass('xyz')).toBe(true); @@ -732,8 +734,10 @@ describe("ngAnimate", function() { $animate.enter(element, parent); $rootScope.$digest(); if($sniffer.transitions) { - expect(element.hasClass('one two ng-enter')).toBe(true); - expect(element.hasClass('one two ng-enter ng-enter-active')).toBe(true); + expect(element.hasClass('one')).toBe(true); + expect(element.hasClass('two')).toBe(true); + expect(element.hasClass('ng-enter')).toBe(true); + expect(element.hasClass('ng-enter-active')).toBe(true); expect(element.hasClass('one-active')).toBe(false); expect(element.hasClass('two-active')).toBe(false); browserTrigger(element,'transitionend', { timeStamp: Date.now() + 3000, elapsedTime: 3000 }); @@ -1574,4 +1578,74 @@ describe("ngAnimate", function() { expect(element.contents().length).toBe(1); })); + it("should cancel all child animations when a leave or move animation is triggered on a parent element", function() { + + var animationState; + module(function($animateProvider) { + $animateProvider.register('.animan', function($timeout) { + return { + enter : function(element, done) { + animationState = 'enter'; + $timeout(done, 0, false); + return function() { + animationState = 'enter-cancel'; + } + }, + addClass : function(element, className, done) { + animationState = 'addClass'; + $timeout(done, 0, false); + return function() { + animationState = 'addClass-cancel'; + } + } + }; + }); + }); + + inject(function($animate, $compile, $rootScope, $timeout, $sniffer) { + var element = html($compile('
')($rootScope)); + var container = html($compile('')($rootScope)); + var child = html($compile('')($rootScope)); + + ss.addRule('.animan.ng-enter, .animan.something-add', '-webkit-transition: width 1s, background 1s 1s;' + + 'transition: width 1s, background 1s 1s;'); + + $rootElement.append(element); + jqLite(document.body).append($rootElement); + + $animate.enter(child, element); + $rootScope.$digest(); + + expect(animationState).toBe('enter'); + if($sniffer.transitions) { + expect(child.hasClass('ng-enter')).toBe(true); + expect(child.hasClass('ng-enter-active')).toBe(true); + } + + $animate.move(element, container); + if($sniffer.transitions) { + expect(child.hasClass('ng-enter')).toBe(false); + expect(child.hasClass('ng-enter-active')).toBe(false); + } + + expect(animationState).toBe('enter-cancel'); + $rootScope.$digest(); + $timeout.flush(); + + $animate.addClass(child, 'something'); + expect(animationState).toBe('addClass'); + if($sniffer.transitions) { + expect(child.hasClass('something-add')).toBe(true); + expect(child.hasClass('something-add-active')).toBe(true); + } + + $animate.leave(container); + expect(animationState).toBe('addClass-cancel'); + if($sniffer.transitions) { + expect(child.hasClass('something-add')).toBe(false); + expect(child.hasClass('something-add-active')).toBe(false); + } + }); + }); + });