diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index 4f2432200143..2030fd049a02 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -438,7 +438,7 @@ angular.module('ngAnimate', ['ng']) cancelChildAnimations(element); this.enabled(false, element); $rootScope.$$postDigest(function() { - performAnimation('leave', 'ng-leave', element, null, null, function() { + performAnimation('leave', 'ng-leave', element, null, null, function(element) { $delegate.leave(element); }, doneCallback); }); @@ -515,7 +515,7 @@ angular.module('ngAnimate', ['ng']) * @param {function()=} doneCallback the callback function that will be called once the animation is complete */ addClass : function(element, className, doneCallback) { - performAnimation('addClass', className, element, null, null, function() { + performAnimation('addClass', className, element, null, null, function(element) { $delegate.addClass(element, className); }, doneCallback); }, @@ -551,7 +551,7 @@ angular.module('ngAnimate', ['ng']) * @param {function()=} doneCallback the callback function that will be called once the animation is complete */ removeClass : function(element, className, doneCallback) { - performAnimation('removeClass', className, element, null, null, function() { + performAnimation('removeClass', className, element, null, null, function(element) { $delegate.removeClass(element, className); }, doneCallback); }, @@ -570,15 +570,21 @@ angular.module('ngAnimate', ['ng']) * Globally enables/disables animations. * */ - enabled : function(value, element) { + enabled : function(value, contents) { switch(arguments.length) { case 2: - if(value) { - cleanup(element); - } else { - var data = element.data(NG_ANIMATE_STATE) || {}; - data.disabled = true; - element.data(NG_ANIMATE_STATE, data); + var content, data; + for (var i = 0; i < contents.length; i++) { + content = angular.element(contents[i]); + if(content[0].nodeType == ELEMENT_NODE) { + if(value) { + cleanup(content); + } else { + data = content.data(NG_ANIMATE_STATE) || {}; + data.disabled = true; + content.data(NG_ANIMATE_STATE, data); + } + } } break; @@ -601,16 +607,49 @@ angular.module('ngAnimate', ['ng']) CSS code. Element, parentElement and afterElement are provided DOM elements for the animation and the onComplete callback will be fired once the animation is fully complete. */ - function performAnimation(animationEvent, className, element, parentElement, afterElement, domOperation, doneCallback) { - var currentClassName, classes, node = extractElementNode(element); - if(node) { - currentClassName = node.className; - classes = currentClassName + ' ' + className; + function performAnimation(animationEvent, className, contents, parentElement, afterElement, domOperation, doneCallback) { + + var i, elementsCount = 0, content; + + var args = Array.prototype.slice.call(arguments, 0); + + args[6] = subElmDone; + + for (i = 0; i < contents.length; i++) { + content = args[2] = angular.element(contents[i]); + if(content[0].nodeType == ELEMENT_NODE) { + elementsCount++; + // jshint -W040 + performAnimationForElement.apply(this, args); + } else if (domOperation) { + // Fire DOM operation straightaway + domOperation(content); + } + } + + if (!elementsCount) { + doneCallback && $timeout(doneCallback, 0, false); + } + + function subElmDone() { + // This method is always invoked asynchronously + if (++subElmDone.doneCount == elementsCount) { + doneCallback && doneCallback(); + } } + subElmDone.doneCount = 0; + + } + + function performAnimationForElement(animationEvent, className, element, parentElement, afterElement, domOperation, doneCallback) { + var node = element[0], + currentClassName = node.className, + classes = currentClassName + ' ' + className; + //transcluded directives may sometimes fire an animation using only comment nodes //best to catch this early on to prevent any animation operations from occurring - if(!node || !isAnimatableClassName(classes)) { + if(!isAnimatableClassName(classes)) { fireDOMOperation(); fireBeforeCallbackAsync(); fireAfterCallbackAsync(); @@ -838,7 +877,7 @@ angular.module('ngAnimate', ['ng']) function fireDOMOperation() { if(!fireDOMOperation.hasBeenRun) { fireDOMOperation.hasBeenRun = true; - domOperation(); + domOperation(element); } } @@ -865,16 +904,21 @@ angular.module('ngAnimate', ['ng']) } } - function cancelChildAnimations(element) { - var node = extractElementNode(element); - forEach(node.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 cancelChildAnimations(contents) { + var node; + for (var i = 0; i < contents.length; i++) { + node = contents[i]; + if (node.nodeType == ELEMENT_NODE) { + forEach(node.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) { diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index 6d9367bdbab0..f4d2c1fdcd50 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -205,13 +205,14 @@ describe("ngAnimate", function() { describe("with polyfill", function() { - var child, after; + var child, after, elementsAnimated; beforeEach(function() { module(function($animateProvider) { $animateProvider.register('.custom', function() { return { - start: function(element, done) { + enter: function(element, done) { + elementsAnimated.push(element); done(); } } @@ -253,6 +254,7 @@ describe("ngAnimate", function() { } }); return function($animate, $compile, $rootScope, $rootElement) { + elementsAnimated = []; element = $compile('
')($rootScope); forEach(['.ng-hide-add', '.ng-hide-remove', '.ng-enter', '.ng-leave', '.ng-move'], function(selector) { @@ -308,6 +310,82 @@ describe("ngAnimate", function() { })); + it("should run JS animation only on element node", + inject(function($animate, $compile, $rootScope, $timeout, $sniffer) { + + $rootScope.$digest(); + element.empty(); + + // Needs to be wrapped, otherwise $compile wraps text node in span + var wrappedContents = $compile('
' + + '
' + + '' + + 'Some text' + + '
')($rootScope), + contents = wrappedContents.contents(); + + $animate.enter(contents, element); + $rootScope.$digest(); + + if($sniffer.transitions) { + $animate.triggerReflow(); + expect(contents.hasClass('ng-enter')).toBe(true); + expect(contents.hasClass('ng-enter-active')).toBe(true); + browserTrigger(contents, 'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 }); + } + + expect(contents.hasClass('ng-animate')).toBe(true); + + // Only div element gets animated + expect(elementsAnimated.length).toBe(1); + expect(elementsAnimated[0].length).toBe(1); + expect(elementsAnimated[0].hasClass('my-div')).toBe(true); + expect(elementsAnimated[0][0]).toBe(contents[0]); + expect(elementsAnimated[0][0].nodeType).toBe(1); + + $timeout.flush(); + expect(contents.hasClass('ng-animate')).toBe(false); + expect(element.contents().length).toBe(3); + })); + + + it("should animate the addClass animation event per-element", + inject(function($animate, $compile, $rootScope, $timeout, $sniffer) { + + $rootScope.$digest(); + element.empty(); + + var child1 = $compile('
')($rootScope); + var child2 = $compile('
')($rootScope); + element.append(child1); + element.append(child2); + + expect(element.contents().length).toBe(2); + $animate.addClass(element.contents(), 'some-class'); + $rootScope.$digest(); + + // Reflow + $timeout.flush(10); + expect(child1.hasClass('ng-animate')).toBe(true); + expect(child2.hasClass('ng-animate')).toBe(true); + + if($sniffer.transitions) { + browserTrigger(child1,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 }); + browserTrigger(child2,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 }); + } + $timeout.flush(2000); + expect(child1.hasClass('ng-animate')).toBe(false); + expect(child1.hasClass('some-class')).toBe(true); + + // Still not done yet with the other element + expect(child2.hasClass('ng-animate')).toBe(true); + + $timeout.flush(20000); + expect(child2.hasClass('ng-animate')).toBe(false); + expect(child2.hasClass('some-class')).toBe(true); + })); + + it("should animate the move animation event", inject(function($animate, $compile, $rootScope, $timeout, $sniffer) { @@ -1396,12 +1474,16 @@ describe("ngAnimate", function() { module(function($animateProvider) { $animateProvider.register('.custom', function($timeout) { return { - removeClass : function(element, className, done) { - $timeout(done, 2000); - } + enter : animate, + removeClass : animate + }; + + function animate(element, done) { + done = arguments.length == 3 ? arguments[2] : done; + $timeout(done, 2000); } }); - $animateProvider.register('.other', function() { + $animateProvider.register('.other', function($timeout) { return { enter : function(element, done) { $timeout(done, 10000); @@ -1622,6 +1704,31 @@ describe("ngAnimate", function() { })); + it("should fire a done callback when final animation completes for all elements", + inject(function($animate, $rootScope, $compile, $sniffer, $rootElement, $timeout) { + + var parent = jqLite('
' + + '' + + '' + + '
'); + $rootElement.append(parent); + body.append($rootElement); + var element = parent.find('span'); + + var flag = false; + $animate.enter(element, parent, null, function() { + flag = true; + }); + $rootScope.$digest(); + + $timeout.flush(2000); + expect(flag).toBe(false); + + $timeout.flush(10000 - 2000); + expect(flag).toBe(true); + })); + + it("should fire the callback right away if another animation is called right after", inject(function($animate, $rootScope, $compile, $sniffer, $rootElement, $timeout) { @@ -2475,6 +2582,93 @@ describe("ngAnimate", function() { }); + it("should cancel all child animations when a leave or move animation is triggered on multiple parent elements", function() { + + var step, animationState; + module(function($animateProvider) { + $animateProvider.register('.animan', function($timeout) { + return { + enter : function(element, done) { + animationState = 'enter'; + step = done; + return function(cancelled) { + animationState = cancelled ? 'enter-cancel' : animationState; + } + }, + addClass : function(element, className, done) { + animationState = 'addClass'; + step = done; + return function(cancelled) { + animationState = cancelled ? 'addClass-cancel' : animationState; + } + } + }; + }); + }); + + inject(function($animate, $compile, $rootScope, $timeout, $sniffer) { + var element = html($compile('
')($rootScope)); + var container = html($compile('
')($rootScope)); + var child1 = html($compile('
')($rootScope)); + var child2 = 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(child1, element.eq(0)); + $animate.enter(child2, element.eq(1)); + $rootScope.$digest(); + + expect(animationState).toBe('enter'); + if($sniffer.transitions) { + expect(child1.hasClass('ng-enter')).toBe(true); + expect(child2.hasClass('ng-enter')).toBe(true); + $animate.triggerReflow(); + expect(child1.hasClass('ng-enter-active')).toBe(true); + expect(child2.hasClass('ng-enter')).toBe(true); + } + + $animate.move(element, container); + if($sniffer.transitions) { + expect(child1.hasClass('ng-enter')).toBe(false); + expect(child1.hasClass('ng-enter-active')).toBe(false); + expect(child2.hasClass('ng-enter')).toBe(false); + expect(child2.hasClass('ng-enter-active')).toBe(false); + } + + expect(animationState).toBe('enter-cancel'); + + $rootScope.$digest(); + $timeout.flush(); + + $animate.addClass(child1, 'something'); + $animate.addClass(child2, 'something'); + if($sniffer.transitions) { + $animate.triggerReflow(); + } + expect(animationState).toBe('addClass'); + if($sniffer.transitions) { + expect(child1.hasClass('something-add')).toBe(true); + expect(child1.hasClass('something-add')).toBe(true); + expect(child2.hasClass('something-add-active')).toBe(true); + expect(child2.hasClass('something-add-active')).toBe(true); + } + + $animate.leave(container); + expect(animationState).toBe('addClass-cancel'); + if($sniffer.transitions) { + expect(child1.hasClass('something-add')).toBe(false); + expect(child1.hasClass('something-add-active')).toBe(false); + expect(child2.hasClass('something-add')).toBe(false); + expect(child2.hasClass('something-add-active')).toBe(false); + } + }); + }); + + it("should wait until a queue of animations are complete before performing a reflow", inject(function($rootScope, $compile, $timeout, $sniffer, $animate) {