aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/ngAnimate/animate.js642
-rw-r--r--test/ngAnimate/animateSpec.js144
2 files changed, 501 insertions, 285 deletions
diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js
index 78be2143..f0aec2a6 100644
--- a/src/ngAnimate/animate.js
+++ b/src/ngAnimate/animate.js
@@ -205,18 +205,21 @@
* ngModule.animation('.my-crazy-animation', function() {
* return {
* enter: function(element, done) {
- * //run the animation
- * //!annotate Cancel Animation|This function (if provided) will perform the cancellation of the animation when another is triggered
- * return function(element, done) {
- * //cancel the animation
+ * //run the animation here and call done when the animation is complete
+ * return function(cancelled) {
+ * //this (optional) function will be called when the animation
+ * //completes or when the animation is cancelled (the cancelled
+ * //flag will (be set to true if cancelled).
* }
* }
* leave: function(element, done) { },
* move: function(element, done) { },
- * show: function(element, done) { },
- * hide: function(element, done) { },
+ *
+ * beforeAddClass: function(element, className, done) { },
* addClass: function(element, className, done) { },
- * removeClass: function(element, className, done) { },
+ *
+ * beforeRemoveClass: function(element, className, done) { },
+ * removeClass: function(element, className, done) { }
* }
* });
* </pre>
@@ -259,6 +262,7 @@ angular.module('ngAnimate', ['ng'])
var NG_ANIMATE_STATE = '$$ngAnimateState';
var NG_ANIMATE_CLASS_NAME = 'ng-animate';
var rootAnimateState = {disabled:true};
+
$provide.decorator('$animate', ['$delegate', '$injector', '$sniffer', '$rootElement', '$timeout', '$rootScope', '$document',
function($delegate, $injector, $sniffer, $rootElement, $timeout, $rootScope, $document) {
@@ -319,7 +323,7 @@ angular.module('ngAnimate', ['ng'])
* @function
*
* @description
- * Appends the element to the parent element that resides in the document and then runs the enter animation. Once
+ * Appends the element to the parentElement element that resides in the document and then runs the enter animation. Once
* the animation is started, the following CSS classes will be present on the element for the duration of the animation:
*
* Below is a breakdown of each step that occurs during enter animation:
@@ -327,27 +331,25 @@ angular.module('ngAnimate', ['ng'])
* | Animation Step | What the element class attribute looks like |
* |----------------------------------------------------------------------------------------------|-----------------------------------------------|
* | 1. $animate.enter(...) is called | class="my-animation" |
- * | 2. element is inserted into the parent element or beside the after element | class="my-animation" |
+ * | 2. element is inserted into the parentElement element or beside the afterElement element | class="my-animation" |
* | 3. $animate runs any JavaScript-defined animations on the element | class="my-animation" |
* | 4. the .ng-enter class is added to the element | class="my-animation ng-enter" |
* | 5. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-enter" |
* | 6. the .ng-enter-active class is added (this triggers the CSS transition/animation) | class="my-animation ng-enter ng-enter-active" |
* | 7. $animate waits for X milliseconds for the animation to complete | class="my-animation ng-enter ng-enter-active" |
* | 8. The animation ends and both CSS classes are removed from the element | class="my-animation" |
- * | 9. The done() callback is fired (if provided) | class="my-animation" |
+ * | 9. The doneCallback() callback is fired (if provided) | class="my-animation" |
*
* @param {jQuery/jqLite element} element the element that will be the focus of the enter animation
- * @param {jQuery/jqLite element} parent the parent element of the element that will be the focus of the enter animation
- * @param {jQuery/jqLite element} after the sibling element (which is the previous element) of the element that will be the focus of the enter animation
- * @param {function()=} done callback function that will be called once the animation is complete
+ * @param {jQuery/jqLite element} parentElement the parent element of the element that will be the focus of the enter animation
+ * @param {jQuery/jqLite element} afterElement the sibling element (which is the previous element) of the element that will be the focus of the enter animation
+ * @param {function()=} doneCallback callback function that will be called once the animation is complete
*/
- enter : function(element, parent, after, done) {
+ enter : function(element, parentElement, afterElement, doneCallback) {
this.enabled(false, element);
- $delegate.enter(element, parent, after);
+ $delegate.enter(element, parentElement, afterElement);
$rootScope.$$postDigest(function() {
- performAnimation('enter', 'ng-enter', element, parent, after, function() {
- done && $timeout(done, 0, false);
- });
+ performAnimation('enter', 'ng-enter', element, parentElement, afterElement, noop, doneCallback);
});
},
@@ -373,18 +375,18 @@ angular.module('ngAnimate', ['ng'])
* | 6. $animate waits for X milliseconds for the animation to complete | class="my-animation ng-leave ng-leave-active |
* | 7. The animation ends and both CSS classes are removed from the element | class="my-animation" |
* | 8. The element is removed from the DOM | ... |
- * | 9. The done() callback is fired (if provided) | ... |
+ * | 9. The doneCallback() callback is fired (if provided) | ... |
*
* @param {jQuery/jqLite element} element the element that will be the focus of the leave animation
- * @param {function()=} done callback function that will be called once the animation is complete
+ * @param {function()=} doneCallback callback function that will be called once the animation is complete
*/
- leave : function(element, done) {
+ leave : function(element, doneCallback) {
cancelChildAnimations(element);
this.enabled(false, element);
$rootScope.$$postDigest(function() {
performAnimation('leave', 'ng-leave', element, null, null, function() {
- $delegate.leave(element, done);
- });
+ $delegate.leave(element);
+ }, doneCallback);
});
},
@@ -395,8 +397,8 @@ angular.module('ngAnimate', ['ng'])
* @function
*
* @description
- * Fires the move DOM operation. Just before the animation starts, the animate service will either append it into the parent container or
- * add the element directly after the after element if present. Then the move animation will be run. Once
+ * Fires the move DOM operation. Just before the animation starts, the animate service will either append it into the parentElement container or
+ * add the element directly after the afterElement element if present. Then the move animation will be run. Once
* the animation is started, the following CSS classes will be added for the duration of the animation:
*
* Below is a breakdown of each step that occurs during move animation:
@@ -404,28 +406,26 @@ angular.module('ngAnimate', ['ng'])
* | Animation Step | What the element class attribute looks like |
* |----------------------------------------------------------------------------------------------|---------------------------------------------|
* | 1. $animate.move(...) is called | class="my-animation" |
- * | 2. element is moved into the parent element or beside the after element | class="my-animation" |
+ * | 2. element is moved into the parentElement element or beside the afterElement element | class="my-animation" |
* | 3. $animate runs any JavaScript-defined animations on the element | class="my-animation" |
* | 4. the .ng-move class is added to the element | class="my-animation ng-move" |
* | 5. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-move" |
* | 6. the .ng-move-active class is added (this triggers the CSS transition/animation) | class="my-animation ng-move ng-move-active" |
* | 7. $animate waits for X milliseconds for the animation to complete | class="my-animation ng-move ng-move-active" |
* | 8. The animation ends and both CSS classes are removed from the element | class="my-animation" |
- * | 9. The done() callback is fired (if provided) | class="my-animation" |
+ * | 9. The doneCallback() callback is fired (if provided) | class="my-animation" |
*
* @param {jQuery/jqLite element} element the element that will be the focus of the move animation
- * @param {jQuery/jqLite element} parent the parent element of the element that will be the focus of the move animation
- * @param {jQuery/jqLite element} after the sibling element (which is the previous element) of the element that will be the focus of the move animation
- * @param {function()=} done callback function that will be called once the animation is complete
+ * @param {jQuery/jqLite element} parentElement the parentElement element of the element that will be the focus of the move animation
+ * @param {jQuery/jqLite element} afterElement the sibling element (which is the previous element) of the element that will be the focus of the move animation
+ * @param {function()=} doneCallback callback function that will be called once the animation is complete
*/
- move : function(element, parent, after, done) {
+ move : function(element, parentElement, afterElement, doneCallback) {
cancelChildAnimations(element);
this.enabled(false, element);
- $delegate.move(element, parent, after);
+ $delegate.move(element, parentElement, afterElement);
$rootScope.$$postDigest(function() {
- performAnimation('move', 'ng-move', element, null, null, function() {
- done && $timeout(done, 0, false);
- });
+ performAnimation('move', 'ng-move', element, parentElement, afterElement, noop, doneCallback);
});
},
@@ -452,16 +452,16 @@ angular.module('ngAnimate', ['ng'])
* | 6. $animate waits for X milliseconds for the animation to complete | class="super-add super-add-active" |
* | 7. The animation ends and both CSS classes are removed from the element | class="" |
* | 8. The super class is added to the element | class="super" |
- * | 9. The done() callback is fired (if provided) | class="super" |
+ * | 9. The doneCallback() callback is fired (if provided) | class="super" |
*
* @param {jQuery/jqLite element} element the element that will be animated
* @param {string} className the CSS class that will be animated and then attached to the element
* @param {function()=} done callback function that will be called once the animation is complete
*/
- addClass : function(element, className, done) {
+ addClass : function(element, className, doneCallback) {
performAnimation('addClass', className, element, null, null, function() {
- $delegate.addClass(element, className, done);
- });
+ $delegate.addClass(element, className);
+ }, doneCallback);
},
/**
@@ -486,16 +486,16 @@ angular.module('ngAnimate', ['ng'])
* | 5. the .super-remove-active class is added (this triggers the CSS transition/animation) | class="super super-remove super-remove-active" |
* | 6. $animate waits for X milliseconds for the animation to complete | class="super super-remove super-remove-active" |
* | 7. The animation ends and both CSS all three classes are removed from the element | class="" |
- * | 8. The done() callback is fired (if provided) | class="" |
+ * | 8. The doneCallback() callback is fired (if provided) | class="" |
*
* @param {jQuery/jqLite element} element the element that will be animated
* @param {string} className the CSS class that will be animated and then removed from the element
* @param {function()=} done callback function that will be called once the animation is complete
*/
- removeClass : function(element, className, done) {
+ removeClass : function(element, className, doneCallback) {
performAnimation('removeClass', className, element, null, null, function() {
- $delegate.removeClass(element, className, done);
- });
+ $delegate.removeClass(element, className);
+ }, doneCallback);
},
/**
@@ -516,8 +516,7 @@ angular.module('ngAnimate', ['ng'])
case 2:
if(value) {
cleanup(element);
- }
- else {
+ } else {
var data = element.data(NG_ANIMATE_STATE) || {};
data.disabled = true;
element.data(NG_ANIMATE_STATE, data);
@@ -538,28 +537,29 @@ angular.module('ngAnimate', ['ng'])
/*
all animations call this shared animation triggering function internally.
- The event variable refers to the JavaScript animation event that will be triggered
+ The animationEvent variable refers to the JavaScript animation event that will be triggered
and the className value is the name of the animation that will be applied within the
- CSS code. Element, parent and after are provided DOM elements for the animation
+ 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(event, className, element, parent, after, onComplete) {
+ function performAnimation(animationEvent, className, element, parentElement, afterElement, domOperation, doneCallback) {
var classes = (element.attr('class') || '') + ' ' + className;
var animationLookup = (' ' + classes).replace(/\s+/g,'.');
- if (!parent) {
- parent = after ? after.parent() : element.parent();
+ if (!parentElement) {
+ parentElement = afterElement ? afterElement.parent() : element.parent();
}
var matches = lookup(animationLookup);
- var isClassBased = event == 'addClass' || event == 'removeClass';
+ var isClassBased = animationEvent == 'addClass' || animationEvent == 'removeClass';
var ngAnimateState = element.data(NG_ANIMATE_STATE) || {};
//skip the animation if animations are disabled, a parent is already being animated,
//the element is not currently attached to the document body or then completely close
//the animation if any matching animations are not found at all.
- //NOTE: IE8 + IE9 should close properly (run done()) in case a NO animation is not found.
- if (animationsDisabled(element, parent) || matches.length === 0) {
- done();
+ //NOTE: IE8 + IE9 should close properly (run closeAnimation()) in case a NO animation is not found.
+ if (animationsDisabled(element, parentElement) || matches.length === 0) {
+ domOperation();
+ closeAnimation();
return;
}
@@ -569,9 +569,20 @@ angular.module('ngAnimate', ['ng'])
if(!ngAnimateState.running || !(isClassBased && ngAnimateState.structural)) {
forEach(matches, function(animation) {
//add the animation to the queue to if it is allowed to be cancelled
- if(!animation.allowCancel || animation.allowCancel(element, event, className)) {
+ if(!animation.allowCancel || animation.allowCancel(element, animationEvent, className)) {
+ var beforeFn, afterFn = animation[animationEvent];
+
+ //Special case for a leave animation since there is no point in performing an
+ //animation on a element node that has already been removed from the DOM
+ if(animationEvent == 'leave') {
+ beforeFn = afterFn;
+ afterFn = null; //this must be falsy so that the animation is skipped for leave
+ } else {
+ beforeFn = animation['before' + animationEvent.charAt(0).toUpperCase() + animationEvent.substr(1)];
+ }
animations.push({
- start : animation[event]
+ before : beforeFn,
+ after : afterFn
});
}
});
@@ -580,66 +591,108 @@ angular.module('ngAnimate', ['ng'])
//this would mean that an animation was not allowed so let the existing
//animation do it's thing and close this one early
if(animations.length === 0) {
- onComplete && onComplete();
+ domOperation();
+ fireDoneCallbackAsync();
return;
}
if(ngAnimateState.running) {
//if an animation is currently running on the element then lets take the steps
//to cancel that animation and fire any required callbacks
- $timeout.cancel(ngAnimateState.flagTimer);
+ $timeout.cancel(ngAnimateState.closeAnimationTimeout);
cleanup(element);
cancelAnimations(ngAnimateState.animations);
- (ngAnimateState.done || noop)();
+ (ngAnimateState.done || noop)(true);
}
//There is no point in perform a class-based animation if the element already contains
//(on addClass) or doesn't contain (on removeClass) the className being animated.
//The reason why this is being called after the previous animations are cancelled
//is so that the CSS classes present on the element can be properly examined.
- if((event == 'addClass' && element.hasClass(className)) ||
- (event == 'removeClass' && !element.hasClass(className))) {
- onComplete && onComplete();
+ if((animationEvent == 'addClass' && element.hasClass(className)) ||
+ (animationEvent == 'removeClass' && !element.hasClass(className))) {
+ domOperation();
+ fireDoneCallbackAsync();
return;
}
+ //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);
+
element.data(NG_ANIMATE_STATE, {
running:true,
structural:!isClassBased,
animations:animations,
- done:done
+ done:onBeforeAnimationsComplete
});
- //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);
+ //first we run the before animations and when all of those are complete
+ //then we perform the DOM operation and run the next set of animations
+ invokeRegisteredAnimationFns(animations, 'before', onBeforeAnimationsComplete);
- forEach(animations, function(animation, index) {
- var fn = function() {
- progress(index);
- };
+ function onBeforeAnimationsComplete(cancelled) {
+ domOperation();
+ if(cancelled === true) {
+ closeAnimation();
+ return;
+ }
- if(animation.start) {
- animation.endFn = isClassBased ?
- animation.start(element, className, fn) :
- animation.start(element, fn);
- } else {
- fn();
+ //set the done function to the final done function
+ //so that the DOM event won't be executed twice by accident
+ //if the after animation is cancelled as well
+ var data = element.data(NG_ANIMATE_STATE);
+ if(data) {
+ data.done = closeAnimation;
+ element.data(NG_ANIMATE_STATE, data);
}
- });
+ invokeRegisteredAnimationFns(animations, 'after', closeAnimation);
+ }
+
+ function invokeRegisteredAnimationFns(animations, phase, allAnimationFnsComplete) {
+ var endFnName = phase + 'End';
+ forEach(animations, function(animation, index) {
+ var animationPhaseCompleted = function() {
+ progress(index, phase);
+ };
+
+ //there are no before functions for enter + move since the DOM
+ //operations happen before the performAnimation method fires
+ if(phase == 'before' && (animationEvent == 'enter' || animationEvent == 'move')) {
+ animationPhaseCompleted();
+ return;
+ }
- function progress(index) {
- animations[index].done = true;
- (animations[index].endFn || noop)();
- for(var i=0;i<animations.length;i++) {
- if(!animations[i].done) return;
+ if(animation[phase]) {
+ animation[endFnName] = isClassBased ?
+ animation[phase](element, className, animationPhaseCompleted) :
+ animation[phase](element, animationPhaseCompleted);
+ } else {
+ animationPhaseCompleted();
+ }
+ });
+
+ function progress(index, phase) {
+ var phaseCompletionFlag = phase + 'Complete';
+ var currentAnimation = animations[index];
+ currentAnimation[phaseCompletionFlag] = true;
+ (currentAnimation[endFnName] || noop)();
+
+ for(var i=0;i<animations.length;i++) {
+ if(!animations[i][phaseCompletionFlag]) return;
+ }
+
+ allAnimationFnsComplete();
}
- done();
}
- function done() {
- if(!done.hasBeenRun) {
- done.hasBeenRun = true;
+ function fireDoneCallbackAsync() {
+ doneCallback && $timeout(doneCallback, 0, false);
+ }
+
+ function closeAnimation() {
+ if(!closeAnimation.hasBeenRun) {
+ closeAnimation.hasBeenRun = true;
var data = element.data(NG_ANIMATE_STATE);
if(data) {
/* only structural animations wait for reflow before removing an
@@ -649,13 +702,13 @@ angular.module('ngAnimate', ['ng'])
if(isClassBased) {
cleanup(element);
} else {
- data.flagTimer = $timeout(function() {
+ data.closeAnimationTimeout = $timeout(function() {
cleanup(element);
}, 0, false);
element.data(NG_ANIMATE_STATE, data);
}
}
- (onComplete || noop)();
+ fireDoneCallbackAsync();
}
}
}
@@ -666,7 +719,7 @@ angular.module('ngAnimate', ['ng'])
return;
}
- angular.forEach(node.querySelectorAll('.' + NG_ANIMATE_CLASS_NAME), function(element) {
+ forEach(node.querySelectorAll('.' + NG_ANIMATE_CLASS_NAME), function(element) {
element = angular.element(element);
var data = element.data(NG_ANIMATE_STATE);
if(data) {
@@ -679,7 +732,12 @@ angular.module('ngAnimate', ['ng'])
function cancelAnimations(animations) {
var isCancelledFlag = true;
forEach(animations, function(animation) {
- (animation.endFn || noop)(isCancelledFlag);
+ if(!animations['beforeComplete']) {
+ (animation.beforeEnd || noop)(isCancelledFlag);
+ }
+ if(!animations['afterComplete']) {
+ (animation.afterEnd || noop)(isCancelledFlag);
+ }
});
}
@@ -689,14 +747,13 @@ angular.module('ngAnimate', ['ng'])
rootAnimateState.running = false;
rootAnimateState.structural = false;
}
- }
- else {
+ } else {
element.removeClass(NG_ANIMATE_CLASS_NAME);
element.removeData(NG_ANIMATE_STATE);
}
}
- function animationsDisabled(element, parent) {
+ function animationsDisabled(element, parentElement) {
if(element[0] == $rootElement[0]) {
return rootAnimateState.disabled || rootAnimateState.running;
}
@@ -705,10 +762,10 @@ angular.module('ngAnimate', ['ng'])
//the element did not reach the root element which means that it
//is not apart of the DOM. Therefore there is no reason to do
//any animations on it
- if(parent.length === 0) break;
+ if(parentElement.length === 0) break;
- var isRoot = parent[0] == $rootElement[0];
- var state = isRoot ? rootAnimateState : parent.data(NG_ANIMATE_STATE);
+ var isRoot = parentElement[0] == $rootElement[0];
+ var state = isRoot ? rootAnimateState : parentElement.data(NG_ANIMATE_STATE);
var result = state && (!!state.disabled || !!state.running);
if(isRoot || result) {
return result;
@@ -716,17 +773,15 @@ angular.module('ngAnimate', ['ng'])
if(isRoot) return true;
}
- while(parent = parent.parent());
+ while(parentElement = parentElement.parent());
return true;
}
}]);
$animateProvider.register('', ['$window', '$sniffer', '$timeout', function($window, $sniffer, $timeout) {
- var forEach = angular.forEach;
-
// Detect proper transitionend/animationend event names.
- var prefix = '', transitionProp, transitionendEvent, animationProp, animationendEvent;
+ var CSS_PREFIX = '', TRANSITION_PROP, TRANSITIONEND_EVENT, ANIMATION_PROP, ANIMATIONEND_EVENT;
// If unprefixed events are not supported but webkit-prefixed are, use the latter.
// Otherwise, just use W3C names, browsers not supporting them at all will just ignore them.
@@ -737,30 +792,30 @@ angular.module('ngAnimate', ['ng'])
// Also, the only modern browser that uses vendor prefixes for transitions/keyframes is webkit
// therefore there is no reason to test anymore for other vendor prefixes: http://caniuse.com/#search=transition
if (window.ontransitionend === undefined && window.onwebkittransitionend !== undefined) {
- prefix = '-webkit-';
- transitionProp = 'WebkitTransition';
- transitionendEvent = 'webkitTransitionEnd transitionend';
+ CSS_PREFIX = '-webkit-';
+ TRANSITION_PROP = 'WebkitTransition';
+ TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend';
} else {
- transitionProp = 'transition';
- transitionendEvent = 'transitionend';
+ TRANSITION_PROP = 'transition';
+ TRANSITIONEND_EVENT = 'transitionend';
}
if (window.onanimationend === undefined && window.onwebkitanimationend !== undefined) {
- prefix = '-webkit-';
- animationProp = 'WebkitAnimation';
- animationendEvent = 'webkitAnimationEnd animationend';
+ CSS_PREFIX = '-webkit-';
+ ANIMATION_PROP = 'WebkitAnimation';
+ ANIMATIONEND_EVENT = 'webkitAnimationEnd animationend';
} else {
- animationProp = 'animation';
- animationendEvent = 'animationend';
+ ANIMATION_PROP = 'animation';
+ ANIMATIONEND_EVENT = 'animationend';
}
- var durationKey = 'Duration',
- propertyKey = 'Property',
- delayKey = 'Delay',
- animationIterationCountKey = 'IterationCount';
+ var DURATION_KEY = 'Duration';
+ var PROPERTY_KEY = 'Property';
+ var DELAY_KEY = 'Delay';
+ var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount';
+ var NG_ANIMATE_PARENT_KEY = '$$ngAnimateKey';
+ var NG_ANIMATE_CSS_DATA_KEY = '$$ngAnimateCSS3Data';
- var NG_ANIMATE_PARENT_KEY = '$ngAnimateKey';
- var NG_ANIMATE_CLASS_KEY = '$$ngAnimateClasses';
var lookupCache = {};
var parentCounter = 0;
@@ -769,7 +824,7 @@ angular.module('ngAnimate', ['ng'])
animationReflowQueue.push(callback);
$timeout.cancel(animationTimer);
animationTimer = $timeout(function() {
- angular.forEach(animationReflowQueue, function(fn) {
+ forEach(animationReflowQueue, function(fn) {
fn();
});
animationReflowQueue = [];
@@ -785,43 +840,44 @@ angular.module('ngAnimate', ['ng'])
return oldStyle;
}
- function getElementAnimationDetails(element, cacheKey, onlyCheckTransition) {
+ function getElementAnimationDetails(element, cacheKey) {
var data = cacheKey ? lookupCache[cacheKey] : null;
if(!data) {
- var transitionDuration = 0, transitionDelay = 0,
- animationDuration = 0, animationDelay = 0,
- transitionDelayStyle, animationDelayStyle,
- transitionDurationStyle,
- transitionPropertyStyle;
+ var transitionDuration = 0;
+ var transitionDelay = 0;
+ var animationDuration = 0;
+ var animationDelay = 0;
+ var transitionDelayStyle;
+ var animationDelayStyle;
+ var transitionDurationStyle;
+ var transitionPropertyStyle;
//we want all the styles defined before and after
forEach(element, function(element) {
if (element.nodeType == ELEMENT_NODE) {
var elementStyles = $window.getComputedStyle(element) || {};
- transitionDurationStyle = elementStyles[transitionProp + durationKey];
+ transitionDurationStyle = elementStyles[TRANSITION_PROP + DURATION_KEY];
transitionDuration = Math.max(parseMaxTime(transitionDurationStyle), transitionDuration);
- if(!onlyCheckTransition) {
- transitionPropertyStyle = elementStyles[transitionProp + propertyKey];
+ transitionPropertyStyle = elementStyles[TRANSITION_PROP + PROPERTY_KEY];
- transitionDelayStyle = elementStyles[transitionProp + delayKey];
+ transitionDelayStyle = elementStyles[TRANSITION_PROP + DELAY_KEY];
- transitionDelay = Math.max(parseMaxTime(transitionDelayStyle), transitionDelay);
+ transitionDelay = Math.max(parseMaxTime(transitionDelayStyle), transitionDelay);
- animationDelayStyle = elementStyles[animationProp + delayKey];
+ animationDelayStyle = elementStyles[ANIMATION_PROP + DELAY_KEY];
- animationDelay = Math.max(parseMaxTime(animationDelayStyle), animationDelay);
+ animationDelay = Math.max(parseMaxTime(animationDelayStyle), animationDelay);
- var aDuration = parseMaxTime(elementStyles[animationProp + durationKey]);
+ var aDuration = parseMaxTime(elementStyles[ANIMATION_PROP + DURATION_KEY]);
- if(aDuration > 0) {
- aDuration *= parseInt(elementStyles[animationProp + animationIterationCountKey], 10) || 1;
- }
-
- animationDuration = Math.max(aDuration, animationDuration);
+ if(aDuration > 0) {
+ aDuration *= parseInt(elementStyles[ANIMATION_PROP + ANIMATION_ITERATION_COUNT_KEY], 10) || 1;
}
+
+ animationDuration = Math.max(aDuration, animationDuration);
}
});
data = {
@@ -843,35 +899,32 @@ angular.module('ngAnimate', ['ng'])
}
function parseMaxTime(str) {
- var total = 0, values = angular.isString(str) ? str.split(/\s*,\s*/) : [];
+ var maxValue = 0;
+ var values = angular.isString(str) ?
+ str.split(/\s*,\s*/) :
+ [];
forEach(values, function(value) {
- total = Math.max(parseFloat(value) || 0, total);
+ maxValue = Math.max(parseFloat(value) || 0, maxValue);
});
- return total;
+ return maxValue;
}
function getCacheKey(element) {
- var parent = element.parent();
- var parentID = parent.data(NG_ANIMATE_PARENT_KEY);
+ var parentElement = element.parent();
+ var parentID = parentElement.data(NG_ANIMATE_PARENT_KEY);
if(!parentID) {
- parent.data(NG_ANIMATE_PARENT_KEY, ++parentCounter);
+ parentElement.data(NG_ANIMATE_PARENT_KEY, ++parentCounter);
parentID = parentCounter;
}
return parentID + '-' + element[0].className;
}
- function animate(element, className, done) {
+ function animateSetup(element, className) {
var cacheKey = getCacheKey(element);
- if(getElementAnimationDetails(element, cacheKey, true).transitionDuration > 0) {
-
- done();
- return;
- }
-
var eventCacheKey = cacheKey + ' ' + className;
+ var stagger = {};
var ii = lookupCache[eventCacheKey] ? ++lookupCache[eventCacheKey].total : 0;
- var stagger = {};
if(ii > 0) {
var staggerClassName = className + '-stagger';
var staggerCacheKey = cacheKey + ' ' + staggerClassName;
@@ -893,107 +946,105 @@ angular.module('ngAnimate', ['ng'])
in the page. There is also no point in performing an animation
that only has a delay and no duration */
var maxDuration = Math.max(timings.transitionDuration, timings.animationDuration);
- if(maxDuration > 0) {
- var maxDelayTime = Math.max(timings.transitionDelay, timings.animationDelay) * 1000,
- startTime = Date.now(),
- node = element[0];
-
- //temporarily disable the transition so that the enter styles
- //don't animate twice (this is here to avoid a bug in Chrome/FF).
- if(timings.transitionDuration > 0) {
- node.style[transitionProp + propertyKey] = 'none';
- }
-
- var activeClassName = 'ng-animate-active ';
- forEach(className.split(' '), function(klass, i) {
- activeClassName += (i > 0 ? ' ' : '') + klass + '-active';
- });
-
- var formerStyle, css3AnimationEvents = animationendEvent + ' ' + transitionendEvent;
+ if(maxDuration === 0) {
+ element.removeClass(className);
+ return false;
+ }
- // This triggers a reflow which allows for the transition animation to kick in.
- afterReflow(function() {
- if(!element.hasClass(className)) {
- done();
- return;
- }
+ var node = element[0];
+ //temporarily disable the transition so that the enter styles
+ //don't animate twice (this is here to avoid a bug in Chrome/FF).
+ if(timings.transitionDuration > 0) {
+ node.style[TRANSITION_PROP + PROPERTY_KEY] = 'none';
+ }
- var applyFallbackStyle, style = '';
- if(timings.transitionDuration > 0) {
- node.style[transitionProp + propertyKey] = '';
+ var activeClassName = 'ng-animate-active ';
+ forEach(className.split(' '), function(klass, i) {
+ activeClassName += (i > 0 ? ' ' : '') + klass + '-active';
+ });
- var propertyStyle = timings.transitionPropertyStyle;
- if(propertyStyle.indexOf('all') == -1) {
- applyFallbackStyle = true;
- var fallbackProperty = $sniffer.msie ? '-ms-zoom' : 'clip';
- style += prefix + 'transition-property: ' + propertyStyle + ', ' + fallbackProperty + '; ';
- style += prefix + 'transition-duration: ' + timings.transitionDurationStyle + ', ' + timings.transitionDuration + 's; ';
- }
- }
+ element.data(NG_ANIMATE_CSS_DATA_KEY, {
+ className : className,
+ activeClassName : activeClassName,
+ maxDuration : maxDuration,
+ classes : className + ' ' + activeClassName,
+ timings : timings,
+ stagger : stagger,
+ ii : ii
+ });
- if(ii > 0) {
- if(stagger.transitionDelay > 0 && stagger.transitionDuration === 0) {
- var delayStyle = timings.transitionDelayStyle;
- if(applyFallbackStyle) {
- delayStyle += ', ' + timings.transitionDelay + 's';
- }
+ return true;
+ }
- style += prefix + 'transition-delay: ' +
- prepareStaggerDelay(delayStyle, stagger.transitionDelay, ii) + '; ';
- }
+ function animateRun(element, className, activeAnimationComplete) {
+ var data = element.data(NG_ANIMATE_CSS_DATA_KEY);
+ if(!element.hasClass(className) || !data) {
+ activeAnimationComplete();
+ return;
+ }
- if(stagger.animationDelay > 0 && stagger.animationDuration === 0) {
- style += prefix + 'animation-delay: ' +
- prepareStaggerDelay(timings.animationDelayStyle, stagger.animationDelay, ii) + '; ';
- }
- }
+ var node = element[0];
+ var timings = data.timings;
+ var stagger = data.stagger;
+ var maxDuration = data.maxDuration;
+ var activeClassName = data.activeClassName;
+ var maxDelayTime = Math.max(timings.transitionDelay, timings.animationDelay) * 1000;
+ var startTime = Date.now();
+ var css3AnimationEvents = ANIMATIONEND_EVENT + ' ' + TRANSITIONEND_EVENT;
+ var formerStyle;
+ var ii = data.ii;
+
+ var applyFallbackStyle, style = '';
+ if(timings.transitionDuration > 0) {
+ node.style[TRANSITION_PROP + PROPERTY_KEY] = '';
+
+ var propertyStyle = timings.transitionPropertyStyle;
+ if(propertyStyle.indexOf('all') == -1) {
+ applyFallbackStyle = true;
+ var fallbackProperty = $sniffer.msie ? '-ms-zoom' : 'clip';
+ style += CSS_PREFIX + 'transition-property: ' + propertyStyle + ', ' + fallbackProperty + '; ';
+ style += CSS_PREFIX + 'transition-duration: ' + timings.transitionDurationStyle + ', ' + timings.transitionDuration + 's; ';
+ }
+ }
- if(style.length > 0) {
- formerStyle = applyStyle(node, style);
+ if(ii > 0) {
+ if(stagger.transitionDelay > 0 && stagger.transitionDuration === 0) {
+ var delayStyle = timings.transitionDelayStyle;
+ if(applyFallbackStyle) {
+ delayStyle += ', ' + timings.transitionDelay + 's';
}
- element.addClass(activeClassName);
- });
-
- element.data(NG_ANIMATE_CLASS_KEY, className + ' ' + activeClassName);
- element.on(css3AnimationEvents, onAnimationProgress);
-
- // This will automatically be called by $animate so
- // there is no need to attach this internally to the
- // timeout done method.
- return function onEnd(cancelled) {
- element.off(css3AnimationEvents, onAnimationProgress);
- element.removeClass(className);
- element.removeClass(activeClassName);
- element.removeData(NG_ANIMATE_CLASS_KEY);
- if(formerStyle != null) {
- formerStyle.length > 0 ?
- node.setAttribute('style', formerStyle) :
- node.removeAttribute('style');
- }
+ style += CSS_PREFIX + 'transition-delay: ' +
+ prepareStaggerDelay(delayStyle, stagger.transitionDelay, ii) + '; ';
+ }
- // Only when the animation is cancelled is the done()
- // function not called for this animation therefore
- // this must be also called.
- if(cancelled) {
- done();
- }
- };
- }
- else {
- element.removeClass(className);
- done();
+ if(stagger.animationDelay > 0 && stagger.animationDuration === 0) {
+ style += CSS_PREFIX + 'animation-delay: ' +
+ prepareStaggerDelay(timings.animationDelayStyle, stagger.animationDelay, ii) + '; ';
+ }
}
- function prepareStaggerDelay(delayStyle, staggerDelay, index) {
- var style = '';
- angular.forEach(delayStyle.split(','), function(val, i) {
- style += (i > 0 ? ',' : '') +
- (index * staggerDelay + parseInt(val, 10)) + 's';
- });
- return style;
+ if(style.length > 0) {
+ formerStyle = applyStyle(node, style);
}
+ element.on(css3AnimationEvents, onAnimationProgress);
+ element.addClass(activeClassName);
+
+ // This will automatically be called by $animate so
+ // there is no need to attach this internally to the
+ // timeout done method.
+ return function onEnd(cancelled) {
+ element.off(css3AnimationEvents, onAnimationProgress);
+ element.removeClass(activeClassName);
+ animateClose(element, className);
+ if(formerStyle != null) {
+ formerStyle.length > 0 ?
+ node.setAttribute('style', formerStyle) :
+ node.removeAttribute('style');
+ }
+ };
+
function onAnimationProgress(event) {
event.stopPropagation();
var ev = event.originalEvent || event;
@@ -1006,22 +1057,80 @@ angular.module('ngAnimate', ['ng'])
* 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 && ev.elapsedTime >= maxDuration) {
- done();
+ activeAnimationComplete();
}
}
+ }
+ function prepareStaggerDelay(delayStyle, staggerDelay, index) {
+ var style = '';
+ forEach(delayStyle.split(','), function(val, i) {
+ style += (i > 0 ? ',' : '') +
+ (index * staggerDelay + parseInt(val, 10)) + 's';
+ });
+ return style;
+ }
+
+ function animateBefore(element, className) {
+ if(animateSetup(element, className)) {
+ return function(cancelled) {
+ cancelled && animateClose(element, className);
+ };
+ }
+ }
+
+ function animateAfter(element, className, afterAnimationComplete) {
+ if(element.data(NG_ANIMATE_CSS_DATA_KEY)) {
+ return animateRun(element, className, afterAnimationComplete);
+ } else {
+ animateClose(element, className);
+ afterAnimationComplete();
+ }
+ }
+
+ function animate(element, className, animationComplete) {
+ //If the animateSetup function doesn't bother returning a
+ //cancellation function then it means that there is no animation
+ //to perform at all
+ var preReflowCancellation = animateBefore(element, className);
+ if(!preReflowCancellation) {
+ animationComplete();
+ return;
+ }
+
+ //There are two cancellation functions: one is before the first
+ //reflow animation and the second is during the active state
+ //animation. The first function will take care of removing the
+ //data from the element which will not make the 2nd animation
+ //happen in the first place
+ var cancel = preReflowCancellation;
+ afterReflow(function() {
+ //once the reflow is complete then we point cancel to
+ //the new cancellation function which will remove all of the
+ //animation properties from the active animation
+ cancel = animateAfter(element, className, animationComplete);
+ });
+
+ return function(cancelled) {
+ (cancel || noop)(cancelled);
+ };
+ }
+
+ function animateClose(element, className) {
+ element.removeClass(className);
+ element.removeData(NG_ANIMATE_CSS_DATA_KEY);
}
return {
- allowCancel : function(element, event, className) {
+ allowCancel : function(element, animationEvent, className) {
//always cancel the current animation if it is a
//structural animation
- var oldClasses = element.data(NG_ANIMATE_CLASS_KEY);
- if(!oldClasses || ['enter','leave','move'].indexOf(event) >= 0) {
+ var oldClasses = (element.data(NG_ANIMATE_CSS_DATA_KEY) || {}).classes;
+ if(!oldClasses || ['enter','leave','move'].indexOf(animationEvent) >= 0) {
return true;
}
- var parent = element.parent();
+ var parentElement = element.parent();
var clone = angular.element(element[0].cloneNode());
//make the element super hidden and override any CSS style values
@@ -1029,33 +1138,56 @@ angular.module('ngAnimate', ['ng'])
clone.removeAttr('id');
clone.html('');
- angular.forEach(oldClasses.split(' '), function(klass) {
+ forEach(oldClasses.split(' '), function(klass) {
clone.removeClass(klass);
});
- var suffix = event == 'addClass' ? '-add' : '-remove';
+ var suffix = animationEvent == 'addClass' ? '-add' : '-remove';
clone.addClass(suffixClasses(className, suffix));
- parent.append(clone);
+ parentElement.append(clone);
var timings = getElementAnimationDetails(clone);
clone.remove();
return Math.max(timings.transitionDuration, timings.animationDuration) > 0;
},
- enter : function(element, done) {
- return animate(element, 'ng-enter', done);
+
+ enter : function(element, animationCompleted) {
+ return animate(element, 'ng-enter', animationCompleted);
+ },
+
+ leave : function(element, animationCompleted) {
+ return animate(element, 'ng-leave', animationCompleted);
+ },
+
+ move : function(element, animationCompleted) {
+ return animate(element, 'ng-move', animationCompleted);
},
- leave : function(element, done) {
- return animate(element, 'ng-leave', done);
+
+ beforeAddClass : function(element, className, animationCompleted) {
+ var cancellationMethod = animateBefore(element, suffixClasses(className, '-add'));
+ if(cancellationMethod) {
+ afterReflow(animationCompleted);
+ return cancellationMethod;
+ }
+ animationCompleted();
},
- move : function(element, done) {
- return animate(element, 'ng-move', done);
+
+ addClass : function(element, className, animationCompleted) {
+ return animateAfter(element, suffixClasses(className, '-add'), animationCompleted);
},
- addClass : function(element, className, done) {
- return animate(element, suffixClasses(className, '-add'), done);
+
+ beforeRemoveClass : function(element, className, animationCompleted) {
+ var cancellationMethod = animateBefore(element, suffixClasses(className, '-remove'));
+ if(cancellationMethod) {
+ afterReflow(animationCompleted);
+ return cancellationMethod;
+ }
+ animationCompleted();
},
- removeClass : function(element, className, done) {
- return animate(element, suffixClasses(className, '-remove'), done);
+
+ removeClass : function(element, className, animationCompleted) {
+ return animateAfter(element, suffixClasses(className, '-remove'), animationCompleted);
}
};
diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js
index 66e81008..dee9bcba 100644
--- a/test/ngAnimate/animateSpec.js
+++ b/test/ngAnimate/animateSpec.js
@@ -219,7 +219,7 @@ describe("ngAnimate", function() {
return function($animate, $compile, $rootScope, $rootElement) {
element = $compile('<div></div>')($rootScope);
- angular.forEach(['.ng-hide-add', '.ng-hide-remove', '.ng-enter', '.ng-leave', '.ng-move'], function(selector) {
+ forEach(['.ng-hide-add', '.ng-hide-remove', '.ng-enter', '.ng-leave', '.ng-move'], function(selector) {
ss.addRule(selector, '-webkit-transition:1s linear all;' +
'transition:1s linear all;');
});
@@ -371,11 +371,10 @@ describe("ngAnimate", function() {
expect(child.attr('class')).toContain('ng-leave');
expect(child.attr('class')).toContain('ng-leave-active');
browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- $timeout.flush();
}));
it("should not run if animations are disabled",
- inject(function($animate, $rootScope) {
+ inject(function($animate, $rootScope, $timeout, $sniffer) {
$animate.enabled(false);
@@ -392,6 +391,9 @@ describe("ngAnimate", function() {
element.addClass('ng-hide');
$animate.removeClass(element, 'ng-hide');
+ if($sniffer.transitions) {
+ $timeout.flush();
+ }
expect(element.text()).toBe('memento');
}));
@@ -403,7 +405,9 @@ describe("ngAnimate", function() {
expect(element).toBeShown();
$animate.addClass(child, 'ng-hide');
- expect(child).toBeShown();
+ if($sniffer.transitions) {
+ expect(child).toBeShown();
+ }
$animate.leave(child);
$rootScope.$digest();
@@ -546,7 +550,7 @@ describe("ngAnimate", function() {
describe("Animations", function() {
it("should properly detect and make use of CSS Animations",
- inject(function($animate, $rootScope, $compile, $sniffer) {
+ inject(function($animate, $rootScope, $compile, $sniffer, $timeout) {
ss.addRule('.ng-hide-add',
'-webkit-animation: some_animation 4s linear 0s 1 alternate;' +
@@ -562,13 +566,14 @@ describe("ngAnimate", function() {
$animate.removeClass(element, 'ng-hide');
if ($sniffer.animations) {
+ $timeout.flush();
browserTrigger(element,'animationend', { timeStamp: Date.now() + 4000, elapsedTime: 4 });
}
expect(element).toBeShown();
}));
it("should properly detect and make use of CSS Animations with multiple iterations",
- inject(function($animate, $rootScope, $compile, $sniffer) {
+ inject(function($animate, $rootScope, $compile, $sniffer, $timeout) {
var style = '-webkit-animation-duration: 2s;' +
'-webkit-animation-iteration-count: 3;' +
@@ -585,13 +590,14 @@ describe("ngAnimate", function() {
$animate.removeClass(element, 'ng-hide');
if ($sniffer.animations) {
+ $timeout.flush();
browserTrigger(element,'animationend', { timeStamp: Date.now() + 6000, elapsedTime: 6 });
}
expect(element).toBeShown();
}));
it("should fallback to the animation duration if an infinite iteration is provided",
- inject(function($animate, $rootScope, $compile, $sniffer) {
+ inject(function($animate, $rootScope, $compile, $sniffer, $timeout) {
var style = '-webkit-animation-duration: 2s;' +
'-webkit-animation-iteration-count: infinite;' +
@@ -608,13 +614,14 @@ describe("ngAnimate", function() {
$animate.removeClass(element, 'ng-hide');
if ($sniffer.animations) {
+ $timeout.flush();
browserTrigger(element,'animationend', { timeStamp: Date.now() + 2000, elapsedTime: 2 });
}
expect(element).toBeShown();
}));
it("should not consider the animation delay is provided",
- inject(function($animate, $rootScope, $compile, $sniffer) {
+ inject(function($animate, $rootScope, $compile, $sniffer, $timeout) {
var style = '-webkit-animation-duration: 2s;' +
'-webkit-animation-delay: 10s;' +
@@ -633,6 +640,7 @@ describe("ngAnimate", function() {
$animate.removeClass(element, 'ng-hide');
if ($sniffer.transitions) {
+ $timeout.flush();
browserTrigger(element,'animationend', { timeStamp : Date.now() + 20000, elapsedTime: 10 });
}
expect(element).toBeShown();
@@ -861,13 +869,14 @@ describe("ngAnimate", function() {
$animate.removeClass(element, 'ng-hide');
if ($sniffer.transitions) {
+ $timeout.flush();
browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
}
expect(element).toBeShown();
}));
it("should skip animations if disabled and run when enabled picking the longest specified duration",
- inject(function($animate, $rootScope, $compile, $sniffer) {
+ inject(function($animate, $rootScope, $compile, $sniffer, $timeout) {
var style = '-webkit-transition-duration: 1s, 2000ms, 1s;' +
'-webkit-transition-property: height, left, opacity;' +
@@ -879,13 +888,16 @@ describe("ngAnimate", function() {
element = $compile(html('<div>foo</div>'))($rootScope);
element.addClass('ng-hide');
+
$animate.removeClass(element, 'ng-hide');
+
if ($sniffer.transitions) {
- expect(element).toBeHidden();
+ $timeout.flush();
var now = Date.now();
browserTrigger(element,'transitionend', { timeStamp: now + 1000, elapsedTime: 1 });
browserTrigger(element,'transitionend', { timeStamp: now + 1000, elapsedTime: 1 });
browserTrigger(element,'transitionend', { timeStamp: now + 2000, elapsedTime: 2 });
+ expect(element.hasClass('ng-animate')).toBe(false);
}
expect(element).toBeShown();
}));
@@ -916,6 +928,7 @@ describe("ngAnimate", function() {
$animate.removeClass(element, 'ng-hide');
if ($sniffer.transitions) {
+ $timeout.flush();
var now = Date.now();
browserTrigger(element,'transitionend', { timeStamp: now + 1000, elapsedTime: 1 });
browserTrigger(element,'transitionend', { timeStamp: now + 3000, elapsedTime: 3 });
@@ -925,7 +938,7 @@ describe("ngAnimate", function() {
}));
it("should animate for the highest duration",
- inject(function($animate, $rootScope, $compile, $sniffer) {
+ inject(function($animate, $rootScope, $compile, $sniffer, $timeout) {
var style = '-webkit-transition:1s linear all 2s;' +
'transition:1s linear all 2s;' +
'-webkit-animation:my_ani 10s 1s;' +
@@ -941,9 +954,14 @@ describe("ngAnimate", function() {
$animate.removeClass(element, 'ng-hide');
if ($sniffer.transitions) {
- browserTrigger(element,'animationend', { timeStamp: Date.now() + 11000, elapsedTime: 11 });
+ $timeout.flush();
}
expect(element).toBeShown();
+ if ($sniffer.transitions) {
+ expect(element.hasClass('ng-animate-active')).toBe(true);
+ browserTrigger(element,'animationend', { timeStamp: Date.now() + 11000, elapsedTime: 11 });
+ expect(element.hasClass('ng-animate-active')).toBe(false);
+ }
}));
it("should finish the previous transition when a new animation is started",
@@ -1503,12 +1521,13 @@ describe("ngAnimate", function() {
});
if($sniffer.transitions) {
- $timeout.flush();
- expect(element.hasClass('klass')).toBe(false);
expect(element.hasClass('klass-add')).toBe(true);
+ $timeout.flush();
+ expect(element.hasClass('klass')).toBe(true);
expect(element.hasClass('klass-add-active')).toBe(true);
browserTrigger(element,'transitionend', { timeStamp: Date.now() + 3000, elapsedTime: 3 });
}
+ $timeout.flush();
//this cancels out the older animation
$animate.removeClass(element,'klass', function() {
@@ -1516,12 +1535,13 @@ describe("ngAnimate", function() {
});
if($sniffer.transitions) {
+ expect(element.hasClass('klass-remove')).toBe(true);
+
$timeout.flush();
- expect(element.hasClass('klass')).toBe(true);
+ expect(element.hasClass('klass')).toBe(false);
expect(element.hasClass('klass-add')).toBe(false);
expect(element.hasClass('klass-add-active')).toBe(false);
- expect(element.hasClass('klass-remove')).toBe(true);
browserTrigger(element,'transitionend', { timeStamp: Date.now() + 3000, elapsedTime: 3 });
}
$timeout.flush();
@@ -2079,7 +2099,7 @@ describe("ngAnimate", function() {
});
});
- it("should skip ngAnimate animations when any pre-existing CSS transitions are present on the element", function() {
+ it("should not skip ngAnimate animations when any pre-existing CSS transitions are present on the element", function() {
inject(function($compile, $rootScope, $animate, $timeout, $sniffer) {
if(!$sniffer.transitions) return;
@@ -2103,7 +2123,7 @@ describe("ngAnimate", function() {
}
catch(e) {}
- expect(empty).toBe(true);
+ expect(empty).toBe(false);
});
});
@@ -2143,22 +2163,22 @@ describe("ngAnimate", function() {
it("should cancel all child animations when a leave or move animation is triggered on a parent element", function() {
- var animationState;
+ var step, 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';
+ step = done;
+ return function(cancelled) {
+ animationState = cancelled ? 'enter-cancel' : animationState;
}
},
addClass : function(element, className, done) {
animationState = 'addClass';
- $timeout(done, 0, false);
- return function() {
- animationState = 'addClass-cancel';
+ step = done;
+ return function(cancelled) {
+ animationState = cancelled ? 'addClass-cancel' : animationState;
}
}
};
@@ -2193,14 +2213,17 @@ describe("ngAnimate", function() {
}
expect(animationState).toBe('enter-cancel');
+
$rootScope.$digest();
$timeout.flush();
$animate.addClass(child, 'something');
+ if($sniffer.transitions) {
+ $timeout.flush();
+ }
expect(animationState).toBe('addClass');
if($sniffer.transitions) {
expect(child.hasClass('something-add')).toBe(true);
- $timeout.flush();
expect(child.hasClass('something-add-active')).toBe(true);
}
@@ -2229,7 +2252,7 @@ describe("ngAnimate", function() {
$timeout.flush();
expect(element[0].querySelectorAll('.ng-enter-active').length).toBe(5);
- angular.forEach(element.children(), function(kid) {
+ forEach(element.children(), function(kid) {
browserTrigger(kid, 'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
});
@@ -2401,7 +2424,7 @@ describe("ngAnimate", function() {
$timeout.flush();
//called three times since the classname is the same
- expect(count).toBe(3);
+ expect(count).toBe(2);
dealoc(element);
count = 0;
@@ -2414,12 +2437,12 @@ describe("ngAnimate", function() {
$rootScope.$digest();
$timeout.flush();
- expect(count).toBe(40);
+ expect(count).toBe(20);
});
});
it("should cancel an ongoing class-based animation only if the new class contains transition/animation CSS code",
- inject(function($compile, $rootScope, $animate, $sniffer) {
+ inject(function($compile, $rootScope, $animate, $sniffer, $timeout) {
if (!$sniffer.transitions) return;
@@ -2477,4 +2500,65 @@ describe("ngAnimate", function() {
});
});
+ it('should perform pre and post animations', function() {
+ var steps = [];
+ module(function($animateProvider) {
+ $animateProvider.register('.class-animate', function() {
+ return {
+ beforeAddClass : function(element, className, done) {
+ steps.push('before');
+ done();
+ },
+ addClass : function(element, className, done) {
+ steps.push('after');
+ done();
+ }
+ };
+ });
+ });
+ inject(function($animate, $rootScope, $compile, $rootElement, $timeout) {
+ $animate.enabled(true);
+
+ var element = $compile('<div class="class-animate"></div>')($rootScope);
+ $rootElement.append(element);
+
+ $animate.addClass(element, 'red');
+
+ expect(steps).toEqual(['before','after']);
+ });
+ });
+
+ it('should treat the leave event always as a before event and discard the beforeLeave function', function() {
+ var parentID, steps = [];
+ module(function($animateProvider) {
+ $animateProvider.register('.animate', function() {
+ return {
+ beforeLeave : function(element, done) {
+ steps.push('before');
+ done();
+ },
+ leave : function(element, done) {
+ parentID = element.parent().attr('id');
+ steps.push('after');
+ done();
+ }
+ };
+ });
+ });
+ inject(function($animate, $rootScope, $compile, $rootElement) {
+ $animate.enabled(true);
+
+ var element = $compile('<div id="parentGuy"></div>')($rootScope);
+ var child = $compile('<div class="animate"></div>')($rootScope);
+ $rootElement.append(element);
+ element.append(child);
+
+ $animate.leave(child);
+ $rootScope.$digest();
+
+ expect(steps).toEqual(['after']);
+ expect(parentID).toEqual('parentGuy');
+ });
+ });
+
});