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) {