diff options
| author | Matias Niemelä | 2013-11-04 16:23:56 -0500 |
|---|---|---|
| committer | Igor Minar | 2013-11-05 21:15:59 -0800 |
| commit | 9d69a0a7c75c937c0a49bb705d31252326b052df (patch) | |
| tree | 154d5a7ae3978d66e6682c72e4e8d9175d74e1e1 /src/ngAnimate | |
| parent | 7d2d2575a24e099cb0ebd9c3b44f75fe37f24298 (diff) | |
| download | angular.js-9d69a0a7c75c937c0a49bb705d31252326b052df.tar.bz2 | |
feat($animate): ensure CSS transitions can work with inherited CSS class definitions
BREAKING CHANGE
ngAnimate addClass / removeClass animations are now applied right away. This means
that as soon as the animation starts the class will be added (addClass) or removed
(removeClass) to the element being animated instead of after the -add-active /
-remove-active animations are completed. This allows for animations outside of
ngAnimate to not conflict with $animate.
This commit introduces beforeAddClass and beforeRemoveClass animation event functions and
executes any addClass and removeClass event functions AFTER the class has been added or
removed (this is opposite functionality of how ngAnimate used to work when performing
JS-enabled animations addClass / removeClass animations). If your animation code relies on
any animations being performed prior to the class change then simply use the new
beforeAddClass and beforeRemoveClass animation event functions.
Finally, when animating show and hide animations using CSS transitions or keyframe animations,
ng-hide-remove doesn't require `display:block!important` for ng-hide-add anymore.
Diffstat (limited to 'src/ngAnimate')
| -rw-r--r-- | src/ngAnimate/animate.js | 642 |
1 files changed, 387 insertions, 255 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); } }; |
