diff options
Diffstat (limited to 'src')
| -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); } }; |
