aboutsummaryrefslogtreecommitdiffstats
path: root/src/ngAnimate/animate.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/ngAnimate/animate.js')
-rw-r--r--src/ngAnimate/animate.js491
1 files changed, 286 insertions, 205 deletions
diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js
index dba8d3fa..28a1510a 100644
--- a/src/ngAnimate/animate.js
+++ b/src/ngAnimate/animate.js
@@ -248,7 +248,9 @@ angular.module('ngAnimate', ['ng'])
* Please visit the {@link ngAnimate `ngAnimate`} module overview page learn more about how to use animations in your application.
*
*/
- .factory('$$animateReflow', ['$window', '$timeout', function($window, $timeout) {
+ .factory('$$animateReflow', ['$window', '$timeout', '$document',
+ function($window, $timeout, $document) {
+ var bod = $document[0].body;
var requestAnimationFrame = $window.requestAnimationFrame ||
$window.webkitRequestAnimationFrame ||
function(fn) {
@@ -261,7 +263,10 @@ angular.module('ngAnimate', ['ng'])
return $timeout.cancel(timer);
};
return function(fn) {
- var id = requestAnimationFrame(fn);
+ var id = requestAnimationFrame(function() {
+ var a = bod.offsetWidth + 1;
+ fn();
+ });
return function() {
cancelAnimationFrame(id);
};
@@ -301,6 +306,10 @@ angular.module('ngAnimate', ['ng'])
}
}
+ function stripCommentsFromElement(element) {
+ return angular.element(extractElementNode(element));
+ }
+
function isMatchingElement(elm1, elm2) {
return extractElementNode(elm1) == extractElementNode(elm2);
}
@@ -411,6 +420,7 @@ angular.module('ngAnimate', ['ng'])
this.enabled(false, element);
$delegate.enter(element, parentElement, afterElement);
$rootScope.$$postDigest(function() {
+ element = stripCommentsFromElement(element);
performAnimation('enter', 'ng-enter', element, parentElement, afterElement, noop, doneCallback);
});
},
@@ -447,6 +457,7 @@ angular.module('ngAnimate', ['ng'])
cancelChildAnimations(element);
this.enabled(false, element);
$rootScope.$$postDigest(function() {
+ element = stripCommentsFromElement(element);
performAnimation('leave', 'ng-leave', element, null, null, function() {
$delegate.leave(element);
}, doneCallback);
@@ -489,6 +500,7 @@ angular.module('ngAnimate', ['ng'])
this.enabled(false, element);
$delegate.move(element, parentElement, afterElement);
$rootScope.$$postDigest(function() {
+ element = stripCommentsFromElement(element);
performAnimation('move', 'ng-move', element, parentElement, afterElement, noop, doneCallback);
});
},
@@ -524,6 +536,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) {
+ element = stripCommentsFromElement(element);
performAnimation('addClass', className, element, null, null, function() {
$delegate.addClass(element, className);
}, doneCallback);
@@ -560,11 +573,34 @@ angular.module('ngAnimate', ['ng'])
* @param {function()=} doneCallback the callback function that will be called once the animation is complete
*/
removeClass : function(element, className, doneCallback) {
+ element = stripCommentsFromElement(element);
performAnimation('removeClass', className, element, null, null, function() {
$delegate.removeClass(element, className);
}, doneCallback);
},
+ /**
+ *
+ * @ngdoc function
+ * @name ng.$animate#setClass
+ * @methodOf ng.$animate
+ * @function
+ * @description Adds and/or removes the given CSS classes to and from the element.
+ * Once complete, the done() callback will be fired (if provided).
+ * @param {jQuery/jqLite element} element the element which will it's CSS classes changed
+ * removed from it
+ * @param {string} add the CSS classes which will be added to the element
+ * @param {string} remove the CSS class which will be removed from the element
+ * @param {function=} done the callback function (if provided) that will be fired after the
+ * CSS classes have been set on the element
+ */
+ setClass : function(element, add, remove, doneCallback) {
+ element = stripCommentsFromElement(element);
+ performAnimation('setClass', [add, remove], element, null, null, function() {
+ $delegate.setClass(element, add, remove);
+ }, doneCallback);
+ },
+
/**
* @ngdoc function
* @name ngAnimate.$animate#enabled
@@ -611,7 +647,15 @@ angular.module('ngAnimate', ['ng'])
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);
+
+ var classNameAdd, classNameRemove, setClassOperation = animationEvent == 'setClass';
+ if(setClassOperation) {
+ classNameAdd = className[0];
+ classNameRemove = className[1];
+ className = classNameAdd + ' ' + classNameRemove;
+ }
+
+ var currentClassName, classes, node = element[0];
if(node) {
currentClassName = node.className;
classes = currentClassName + ' ' + className;
@@ -623,7 +667,7 @@ angular.module('ngAnimate', ['ng'])
fireDOMOperation();
fireBeforeCallbackAsync();
fireAfterCallbackAsync();
- closeAnimation();
+ fireDoneCallbackAsync();
return;
}
@@ -635,9 +679,15 @@ angular.module('ngAnimate', ['ng'])
parentElement = afterElement ? afterElement.parent() : element.parent();
}
- var matches = lookup(animationLookup);
- var isClassBased = animationEvent == 'addClass' || animationEvent == 'removeClass';
- var ngAnimateState = element.data(NG_ANIMATE_STATE) || {};
+ var matches = lookup(animationLookup);
+ var isClassBased = animationEvent == 'addClass' ||
+ animationEvent == 'removeClass' ||
+ setClassOperation;
+ var ngAnimateState = element.data(NG_ANIMATE_STATE) || {};
+
+ var runningAnimations = ngAnimateState.active || {};
+ var totalActiveAnimations = ngAnimateState.totalActive || 0;
+ var lastAnimation = ngAnimateState.last;
//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
@@ -656,7 +706,7 @@ angular.module('ngAnimate', ['ng'])
//only add animations if the currently running animation is not structural
//or if there is no animation running at all
var allowAnimations = isClassBased ?
- !ngAnimateState.disabled && (!ngAnimateState.running || !ngAnimateState.structural) :
+ !ngAnimateState.disabled && (!lastAnimation || lastAnimation.classBased) :
true;
if(allowAnimations) {
@@ -691,54 +741,48 @@ angular.module('ngAnimate', ['ng'])
return;
}
- var ONE_SPACE = ' ';
- //this value will be searched for class-based CSS className lookup. Therefore,
- //we prefix and suffix the current className value with spaces to avoid substring
- //lookups of className tokens
- var futureClassName = ONE_SPACE + currentClassName + ONE_SPACE;
- 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
- cleanup(element);
- cancelAnimations(ngAnimateState.animations);
-
- //in the event that the CSS is class is quickly added and removed back
- //then we don't want to wait until after the reflow to add/remove the CSS
- //class since both class animations may run into a race condition.
- //The code below will check to see if that is occurring and will
- //immediately remove the former class before the reflow so that the
- //animation can snap back to the original animation smoothly
- var isFullyClassBasedAnimation = isClassBased && !ngAnimateState.structural;
- var isRevertingClassAnimation = isFullyClassBasedAnimation &&
- ngAnimateState.className == className &&
- animationEvent != ngAnimateState.event;
-
- //if the class is removed during the reflow then it will revert the styles temporarily
- //back to the base class CSS styling causing a jump-like effect to occur. This check
- //here ensures that the domOperation is only performed after the reflow has commenced
- if(ngAnimateState.beforeComplete || isRevertingClassAnimation) {
- (ngAnimateState.done || noop)(true);
- } else if(isFullyClassBasedAnimation) {
- //class-based animations will compare element className values after cancelling the
- //previous animation to see if the element properties already contain the final CSS
- //class and if so then the animation will be skipped. Since the domOperation will
- //be performed only after the reflow is complete then our element's className value
- //will be invalid. Therefore the same string manipulation that would occur within the
- //DOM operation will be performed below so that the class comparison is valid...
- futureClassName = ngAnimateState.event == 'removeClass' ?
- futureClassName.replace(ONE_SPACE + ngAnimateState.className + ONE_SPACE, ONE_SPACE) :
- futureClassName + ngAnimateState.className + ONE_SPACE;
+ var skipAnimation = false;
+ if(totalActiveAnimations > 0) {
+ var animationsToCancel = [];
+ if(!isClassBased) {
+ if(animationEvent == 'leave' && runningAnimations['ng-leave']) {
+ skipAnimation = true;
+ } else {
+ //cancel all animations when a structural animation takes place
+ for(var klass in runningAnimations) {
+ animationsToCancel.push(runningAnimations[klass]);
+ cleanup(element, klass);
+ }
+ runningAnimations = {};
+ totalActiveAnimations = 0;
+ }
+ } else if(lastAnimation.event == 'setClass') {
+ animationsToCancel.push(lastAnimation);
+ cleanup(element, className);
+ }
+ else if(runningAnimations[className]) {
+ var current = runningAnimations[className];
+ if(current.event == animationEvent) {
+ skipAnimation = true;
+ } else {
+ animationsToCancel.push(current);
+ cleanup(element, className);
+ }
+ }
+
+ if(animationsToCancel.length > 0) {
+ angular.forEach(animationsToCancel, function(operation) {
+ (operation.done || noop)(true);
+ cancelAnimations(operation.animations);
+ });
}
}
- //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.
- var classNameToken = ONE_SPACE + className + ONE_SPACE;
- if((animationEvent == 'addClass' && futureClassName.indexOf(classNameToken) >= 0) ||
- (animationEvent == 'removeClass' && futureClassName.indexOf(classNameToken) == -1)) {
- fireDOMOperation();
+ if(isClassBased && !setClassOperation && !skipAnimation) {
+ skipAnimation = (animationEvent == 'addClass') == element.hasClass(className); //opposite of XOR
+ }
+
+ if(skipAnimation) {
fireBeforeCallbackAsync();
fireAfterCallbackAsync();
fireDoneCallbackAsync();
@@ -750,15 +794,21 @@ angular.module('ngAnimate', ['ng'])
element.addClass(NG_ANIMATE_CLASS_NAME);
var localAnimationCount = globalAnimationCounter++;
+ lastAnimation = {
+ classBased : isClassBased,
+ event : animationEvent,
+ animations : animations,
+ done:onBeforeAnimationsComplete
+ };
+
+ totalActiveAnimations++;
+ runningAnimations[className] = lastAnimation;
element.data(NG_ANIMATE_STATE, {
- running:true,
- event:animationEvent,
- className:className,
- structural:!isClassBased,
- animations:animations,
- index:localAnimationCount,
- done:onBeforeAnimationsComplete
+ last : lastAnimation,
+ active : runningAnimations,
+ index : localAnimationCount,
+ totalActive : totalActiveAnimations
});
//first we run the before animations and when all of those are complete
@@ -766,6 +816,11 @@ angular.module('ngAnimate', ['ng'])
invokeRegisteredAnimationFns(animations, 'before', onBeforeAnimationsComplete);
function onBeforeAnimationsComplete(cancelled) {
+ var data = element.data(NG_ANIMATE_STATE);
+ cancelled = cancelled ||
+ !data || !data.active[className] ||
+ (isClassBased && data.active[className].event != animationEvent);
+
fireDOMOperation();
if(cancelled === true) {
closeAnimation();
@@ -775,11 +830,8 @@ angular.module('ngAnimate', ['ng'])
//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);
- }
+ var currentAnimation = data.active[className];
+ currentAnimation.done = closeAnimation;
invokeRegisteredAnimationFns(animations, 'after', closeAnimation);
}
@@ -802,9 +854,13 @@ angular.module('ngAnimate', ['ng'])
}
if(animation[phase]) {
- animation[endFnName] = isClassBased ?
- animation[phase](element, className, animationPhaseCompleted) :
- animation[phase](element, animationPhaseCompleted);
+ if(setClassOperation) {
+ animation[endFnName] = animation[phase](element, classNameAdd, classNameRemove, animationPhaseCompleted);
+ } else {
+ animation[endFnName] = isClassBased ?
+ animation[phase](element, className, animationPhaseCompleted) :
+ animation[phase](element, animationPhaseCompleted);
+ }
} else {
animationPhaseCompleted();
}
@@ -872,12 +928,12 @@ angular.module('ngAnimate', ['ng'])
failing would be when a parent HTML tag has a ng-class attribute
causing ALL directives below to skip animations during the digest */
if(isClassBased) {
- cleanup(element);
+ cleanup(element, className);
} else {
$$asyncQueueBuffer(function() {
var data = element.data(NG_ANIMATE_STATE) || {};
if(localAnimationCount == data.index) {
- cleanup(element);
+ cleanup(element, className, animationEvent);
}
});
element.data(NG_ANIMATE_STATE, data);
@@ -893,9 +949,11 @@ angular.module('ngAnimate', ['ng'])
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);
+ if(data && data.active) {
+ angular.forEach(data.active, function(operation) {
+ (operation.done || noop)(true);
+ cancelAnimations(operation.animations);
+ });
}
});
}
@@ -912,15 +970,27 @@ angular.module('ngAnimate', ['ng'])
});
}
- function cleanup(element) {
+ function cleanup(element, className) {
if(isMatchingElement(element, $rootElement)) {
if(!rootAnimateState.disabled) {
rootAnimateState.running = false;
rootAnimateState.structural = false;
}
- } else {
- element.removeClass(NG_ANIMATE_CLASS_NAME);
- element.removeData(NG_ANIMATE_STATE);
+ } else if(className) {
+ var data = element.data(NG_ANIMATE_STATE) || {};
+
+ var removeAnimations = className === true;
+ if(!removeAnimations) {
+ if(data.active && data.active[className]) {
+ data.totalActive--;
+ delete data.active[className];
+ }
+ }
+
+ if(removeAnimations || !data.totalActive) {
+ element.removeClass(NG_ANIMATE_CLASS_NAME);
+ element.removeData(NG_ANIMATE_STATE);
+ }
}
}
@@ -939,7 +1009,7 @@ angular.module('ngAnimate', ['ng'])
var isRoot = isMatchingElement(parentElement, $rootElement);
var state = isRoot ? rootAnimateState : parentElement.data(NG_ANIMATE_STATE);
- var result = state && (!!state.disabled || !!state.running);
+ var result = state && (!!state.disabled || state.running || state.totalActive > 0);
if(isRoot || result) {
return result;
}
@@ -989,74 +1059,57 @@ angular.module('ngAnimate', ['ng'])
var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount';
var NG_ANIMATE_PARENT_KEY = '$$ngAnimateKey';
var NG_ANIMATE_CSS_DATA_KEY = '$$ngAnimateCSS3Data';
+ var NG_ANIMATE_BLOCK_CLASS_NAME = 'ng-animate-block-transitions';
var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3;
var CLOSING_TIME_BUFFER = 1.5;
var ONE_SECOND = 1000;
- var animationCounter = 0;
var lookupCache = {};
var parentCounter = 0;
var animationReflowQueue = [];
- var animationElementQueue = [];
var cancelAnimationReflow;
- var closingAnimationTime = 0;
- var timeOut = false;
function afterReflow(element, callback) {
if(cancelAnimationReflow) {
cancelAnimationReflow();
}
-
animationReflowQueue.push(callback);
-
- var node = extractElementNode(element);
- element = angular.element(node);
- animationElementQueue.push(element);
-
- var elementData = element.data(NG_ANIMATE_CSS_DATA_KEY);
-
- var stagger = elementData.stagger;
- var staggerTime = elementData.itemIndex * (Math.max(stagger.animationDelay, stagger.transitionDelay) || 0);
-
- var animationTime = (elementData.maxDelay + elementData.maxDuration) * CLOSING_TIME_BUFFER;
- closingAnimationTime = Math.max(closingAnimationTime, (staggerTime + animationTime) * ONE_SECOND);
-
- //by placing a counter we can avoid an accidental
- //race condition which may close an animation when
- //a follow-up animation is midway in its animation
- elementData.animationCount = animationCounter;
-
cancelAnimationReflow = $$animateReflow(function() {
forEach(animationReflowQueue, function(fn) {
fn();
});
- //copy the list of elements so that successive
- //animations won't conflict if they're added before
- //the closing animation timeout has run
- var elementQueueSnapshot = [];
- var animationCounterSnapshot = animationCounter;
- forEach(animationElementQueue, function(elm) {
- elementQueueSnapshot.push(elm);
- });
-
- $timeout(function() {
- closeAllAnimations(elementQueueSnapshot, animationCounterSnapshot);
- elementQueueSnapshot = null;
- }, closingAnimationTime, false);
-
animationReflowQueue = [];
- animationElementQueue = [];
cancelAnimationReflow = null;
lookupCache = {};
- closingAnimationTime = 0;
- animationCounter++;
});
}
- function closeAllAnimations(elements, count) {
+ var closingTimer = null;
+ var closingTimestamp = 0;
+ var animationElementQueue = [];
+ function animationCloseHandler(element, totalTime) {
+ var futureTimestamp = Date.now() + (totalTime * 1000);
+ if(futureTimestamp <= closingTimestamp) {
+ return;
+ }
+
+ $timeout.cancel(closingTimer);
+
+ var node = extractElementNode(element);
+ element = angular.element(node);
+ animationElementQueue.push(element);
+
+ closingTimestamp = futureTimestamp;
+ closingTimer = $timeout(function() {
+ closeAllAnimations(animationElementQueue);
+ animationElementQueue = [];
+ }, totalTime, false);
+ }
+
+ function closeAllAnimations(elements) {
forEach(elements, function(element) {
var elementData = element.data(NG_ANIMATE_CSS_DATA_KEY);
- if(elementData && elementData.animationCount == count) {
+ if(elementData) {
(elementData.closeAnimationFn || noop)();
}
});
@@ -1141,12 +1194,12 @@ angular.module('ngAnimate', ['ng'])
return parentID + '-' + extractElementNode(element).className;
}
- function animateSetup(element, className, calculationDecorator) {
+ function animateSetup(animationEvent, element, className, calculationDecorator) {
var cacheKey = getCacheKey(element);
var eventCacheKey = cacheKey + ' ' + className;
- var stagger = {};
var itemIndex = lookupCache[eventCacheKey] ? ++lookupCache[eventCacheKey].total : 0;
+ var stagger = {};
if(itemIndex > 0) {
var staggerClassName = className + '-stagger';
var staggerCacheKey = cacheKey + ' ' + staggerClassName;
@@ -1166,60 +1219,63 @@ angular.module('ngAnimate', ['ng'])
element.addClass(className);
+ var formerData = element.data(NG_ANIMATE_CSS_DATA_KEY) || {};
+
var timings = calculationDecorator(function() {
return getElementAnimationDetails(element, eventCacheKey);
});
- /* there is no point in performing a reflow if the animation
- timeout is empty (this would cause a flicker bug normally
- in the page. There is also no point in performing an animation
- that only has a delay and no duration */
- var maxDelay = Math.max(timings.transitionDelay, timings.animationDelay);
- var maxDuration = Math.max(timings.transitionDuration, timings.animationDuration);
- if(maxDuration === 0) {
+ var transitionDuration = timings.transitionDuration;
+ var animationDuration = timings.animationDuration;
+ if(transitionDuration === 0 && animationDuration === 0) {
element.removeClass(className);
return false;
}
- //temporarily disable the transition so that the enter styles
- //don't animate twice (this is here to avoid a bug in Chrome/FF).
- var activeClassName = '';
- timings.transitionDuration > 0 ?
- blockTransitions(element) :
- blockKeyframeAnimations(element);
-
- forEach(className.split(' '), function(klass, i) {
- activeClassName += (i > 0 ? ' ' : '') + klass + '-active';
- });
-
element.data(NG_ANIMATE_CSS_DATA_KEY, {
- className : className,
- activeClassName : activeClassName,
- maxDuration : maxDuration,
- maxDelay : maxDelay,
- classes : className + ' ' + activeClassName,
- timings : timings,
+ running : formerData.running || 0,
+ itemIndex : itemIndex,
stagger : stagger,
- itemIndex : itemIndex
+ timings : timings,
+ closeAnimationFn : angular.noop
});
+ //temporarily disable the transition so that the enter styles
+ //don't animate twice (this is here to avoid a bug in Chrome/FF).
+ var isCurrentlyAnimating = formerData.running > 0 || animationEvent == 'setClass';
+ if(transitionDuration > 0) {
+ blockTransitions(element, className, isCurrentlyAnimating);
+ }
+ if(animationDuration > 0) {
+ blockKeyframeAnimations(element);
+ }
+
return true;
}
- function blockTransitions(element) {
- extractElementNode(element).style[TRANSITION_PROP + PROPERTY_KEY] = 'none';
+ function isStructuralAnimation(className) {
+ return className == 'ng-enter' || className == 'ng-move' || className == 'ng-leave';
+ }
+
+ function blockTransitions(element, className, isAnimating) {
+ if(isStructuralAnimation(className) || !isAnimating) {
+ extractElementNode(element).style[TRANSITION_PROP + PROPERTY_KEY] = 'none';
+ } else {
+ element.addClass(NG_ANIMATE_BLOCK_CLASS_NAME);
+ }
}
function blockKeyframeAnimations(element) {
extractElementNode(element).style[ANIMATION_PROP] = 'none 0s';
}
- function unblockTransitions(element) {
+ function unblockTransitions(element, className) {
var prop = TRANSITION_PROP + PROPERTY_KEY;
var node = extractElementNode(element);
if(node.style[prop] && node.style[prop].length > 0) {
node.style[prop] = '';
}
+ element.removeClass(NG_ANIMATE_BLOCK_CLASS_NAME);
}
function unblockKeyframeAnimations(element) {
@@ -1230,22 +1286,28 @@ angular.module('ngAnimate', ['ng'])
}
}
- function animateRun(element, className, activeAnimationComplete) {
- var elementData = element.data(NG_ANIMATE_CSS_DATA_KEY);
+ function animateRun(animationEvent, element, className, activeAnimationComplete) {
var node = extractElementNode(element);
+ var elementData = element.data(NG_ANIMATE_CSS_DATA_KEY);
if(node.className.indexOf(className) == -1 || !elementData) {
activeAnimationComplete();
return;
}
- var timings = elementData.timings;
+ var activeClassName = '';
+ forEach(className.split(' '), function(klass, i) {
+ activeClassName += (i > 0 ? ' ' : '') + klass + '-active';
+ });
+
var stagger = elementData.stagger;
- var maxDuration = elementData.maxDuration;
- var activeClassName = elementData.activeClassName;
- var maxDelayTime = Math.max(timings.transitionDelay, timings.animationDelay) * ONE_SECOND;
+ var timings = elementData.timings;
+ var itemIndex = elementData.itemIndex;
+ var maxDuration = Math.max(timings.transitionDuration, timings.animationDuration);
+ var maxDelay = Math.max(timings.transitionDelay, timings.animationDelay);
+ var maxDelayTime = maxDelay * ONE_SECOND;
+
var startTime = Date.now();
var css3AnimationEvents = ANIMATIONEND_EVENT + ' ' + TRANSITIONEND_EVENT;
- var itemIndex = elementData.itemIndex;
var style = '', appliedStyles = [];
if(timings.transitionDuration > 0) {
@@ -1287,6 +1349,13 @@ angular.module('ngAnimate', ['ng'])
onEnd();
activeAnimationComplete();
};
+
+ var staggerTime = itemIndex * (Math.max(stagger.animationDelay, stagger.transitionDelay) || 0);
+ var animationTime = (maxDelay + maxDuration) * CLOSING_TIME_BUFFER;
+ var totalTime = (staggerTime + animationTime) * ONE_SECOND;
+
+ elementData.running++;
+ animationCloseHandler(element, totalTime);
return onEnd;
// This will automatically be called by $animate so
@@ -1333,28 +1402,28 @@ angular.module('ngAnimate', ['ng'])
return style;
}
- function animateBefore(element, className, calculationDecorator) {
- if(animateSetup(element, className, calculationDecorator)) {
+ function animateBefore(animationEvent, element, className, calculationDecorator) {
+ if(animateSetup(animationEvent, element, className, calculationDecorator)) {
return function(cancelled) {
cancelled && animateClose(element, className);
};
}
}
- function animateAfter(element, className, afterAnimationComplete) {
+ function animateAfter(animationEvent, element, className, afterAnimationComplete) {
if(element.data(NG_ANIMATE_CSS_DATA_KEY)) {
- return animateRun(element, className, afterAnimationComplete);
+ return animateRun(animationEvent, element, className, afterAnimationComplete);
} else {
animateClose(element, className);
afterAnimationComplete();
}
}
- function animate(element, className, animationComplete) {
+ function animate(animationEvent, 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);
+ var preReflowCancellation = animateBefore(animationEvent, element, className);
if(!preReflowCancellation) {
animationComplete();
return;
@@ -1367,12 +1436,12 @@ angular.module('ngAnimate', ['ng'])
//happen in the first place
var cancel = preReflowCancellation;
afterReflow(element, function() {
- unblockTransitions(element);
+ unblockTransitions(element, className);
unblockKeyframeAnimations(element);
//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);
+ cancel = animateAfter(animationEvent, element, className, animationComplete);
});
return function(cancelled) {
@@ -1382,54 +1451,59 @@ angular.module('ngAnimate', ['ng'])
function animateClose(element, className) {
element.removeClass(className);
- element.removeData(NG_ANIMATE_CSS_DATA_KEY);
+ var data = element.data(NG_ANIMATE_CSS_DATA_KEY);
+ if(data) {
+ if(data.running) {
+ data.running--;
+ }
+ if(!data.running || data.running === 0) {
+ element.removeData(NG_ANIMATE_CSS_DATA_KEY);
+ }
+ }
}
return {
- allowCancel : function(element, animationEvent, className) {
- //always cancel the current animation if it is a
- //structural animation
- var oldClasses = (element.data(NG_ANIMATE_CSS_DATA_KEY) || {}).classes;
- if(!oldClasses || ['enter','leave','move'].indexOf(animationEvent) >= 0) {
- return true;
- }
-
- var parentElement = element.parent();
- var clone = angular.element(extractElementNode(element).cloneNode());
-
- //make the element super hidden and override any CSS style values
- clone.attr('style','position:absolute; top:-9999px; left:-9999px');
- clone.removeAttr('id');
- clone.empty();
-
- forEach(oldClasses.split(' '), function(klass) {
- clone.removeClass(klass);
- });
-
- var suffix = animationEvent == 'addClass' ? '-add' : '-remove';
- clone.addClass(suffixClasses(className, suffix));
- parentElement.append(clone);
-
- var timings = getElementAnimationDetails(clone);
- clone.remove();
-
- return Math.max(timings.transitionDuration, timings.animationDuration) > 0;
- },
-
enter : function(element, animationCompleted) {
- return animate(element, 'ng-enter', animationCompleted);
+ return animate('enter', element, 'ng-enter', animationCompleted);
},
leave : function(element, animationCompleted) {
- return animate(element, 'ng-leave', animationCompleted);
+ return animate('leave', element, 'ng-leave', animationCompleted);
},
move : function(element, animationCompleted) {
- return animate(element, 'ng-move', animationCompleted);
+ return animate('move', element, 'ng-move', animationCompleted);
+ },
+
+ beforeSetClass : function(element, add, remove, animationCompleted) {
+ var className = suffixClasses(remove, '-remove') + ' ' +
+ suffixClasses(add, '-add');
+ var cancellationMethod = animateBefore('setClass', element, className, function(fn) {
+ /* when classes are removed from an element then the transition style
+ * that is applied is the transition defined on the element without the
+ * CSS class being there. This is how CSS3 functions outside of ngAnimate.
+ * http://plnkr.co/edit/j8OzgTNxHTb4n3zLyjGW?p=preview */
+ var klass = element.attr('class');
+ element.removeClass(remove);
+ element.addClass(add);
+ var timings = fn();
+ element.attr('class', klass);
+ return timings;
+ });
+
+ if(cancellationMethod) {
+ afterReflow(element, function() {
+ unblockTransitions(element, className);
+ unblockKeyframeAnimations(element);
+ animationCompleted();
+ });
+ return cancellationMethod;
+ }
+ animationCompleted();
},
beforeAddClass : function(element, className, animationCompleted) {
- var cancellationMethod = animateBefore(element, suffixClasses(className, '-add'), function(fn) {
+ var cancellationMethod = animateBefore('addClass', element, suffixClasses(className, '-add'), function(fn) {
/* when a CSS class is added to an element then the transition style that
* is applied is the transition defined on the element when the CSS class
@@ -1443,7 +1517,7 @@ angular.module('ngAnimate', ['ng'])
if(cancellationMethod) {
afterReflow(element, function() {
- unblockTransitions(element);
+ unblockTransitions(element, className);
unblockKeyframeAnimations(element);
animationCompleted();
});
@@ -1452,12 +1526,19 @@ angular.module('ngAnimate', ['ng'])
animationCompleted();
},
+ setClass : function(element, add, remove, animationCompleted) {
+ remove = suffixClasses(remove, '-remove');
+ add = suffixClasses(add, '-add');
+ var className = remove + ' ' + add;
+ return animateAfter('setClass', element, className, animationCompleted);
+ },
+
addClass : function(element, className, animationCompleted) {
- return animateAfter(element, suffixClasses(className, '-add'), animationCompleted);
+ return animateAfter('addClass', element, suffixClasses(className, '-add'), animationCompleted);
},
beforeRemoveClass : function(element, className, animationCompleted) {
- var cancellationMethod = animateBefore(element, suffixClasses(className, '-remove'), function(fn) {
+ var cancellationMethod = animateBefore('removeClass', element, suffixClasses(className, '-remove'), function(fn) {
/* when classes are removed from an element then the transition style
* that is applied is the transition defined on the element without the
* CSS class being there. This is how CSS3 functions outside of ngAnimate.
@@ -1471,7 +1552,7 @@ angular.module('ngAnimate', ['ng'])
if(cancellationMethod) {
afterReflow(element, function() {
- unblockTransitions(element);
+ unblockTransitions(element, className);
unblockKeyframeAnimations(element);
animationCompleted();
});
@@ -1481,7 +1562,7 @@ angular.module('ngAnimate', ['ng'])
},
removeClass : function(element, className, animationCompleted) {
- return animateAfter(element, suffixClasses(className, '-remove'), animationCompleted);
+ return animateAfter('removeClass', element, suffixClasses(className, '-remove'), animationCompleted);
}
};