vendor/assets/javascripts/angular-animate.js in angularjs-rails-1.2.12.1 vs vendor/assets/javascripts/angular-animate.js in angularjs-rails-1.2.13

- old
+ new

@@ -1,7 +1,7 @@ /** - * @license AngularJS v1.2.12 + * @license AngularJS v1.2.13 * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT */ (function(window, angular, undefined) {'use strict'; @@ -252,11 +252,13 @@ * Requires the {@link ngAnimate `ngAnimate`} module to be installed. * * 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) { return $timeout(fn, 10, false); }; @@ -265,17 +267,34 @@ $window.webkitCancelAnimationFrame || function(timer) { 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); }; }; }]) + .factory('$$asyncQueueBuffer', ['$timeout', function($timeout) { + var timer, queue = []; + return function(fn) { + $timeout.cancel(timer); + queue.push(fn); + timer = $timeout(function() { + for(var i = 0; i < queue.length; i++) { + queue[i](); + } + queue = []; + }, 0, false); + }; + }]) + .config(['$provide', '$animateProvider', function($provide, $animateProvider) { var noop = angular.noop; var forEach = angular.forEach; var selectors = $animateProvider.$$selectors; @@ -291,17 +310,22 @@ return elm; } } } + function stripCommentsFromElement(element) { + return angular.element(extractElementNode(element)); + } + function isMatchingElement(elm1, elm2) { return extractElementNode(elm1) == extractElementNode(elm2); } - $provide.decorator('$animate', ['$delegate', '$injector', '$sniffer', '$rootElement', '$timeout', '$rootScope', '$document', - function($delegate, $injector, $sniffer, $rootElement, $timeout, $rootScope, $document) { + $provide.decorator('$animate', ['$delegate', '$injector', '$sniffer', '$rootElement', '$$asyncQueueBuffer', '$rootScope', '$document', + function($delegate, $injector, $sniffer, $rootElement, $$asyncQueueBuffer, $rootScope, $document) { + var globalAnimationCounter = 0; $rootElement.data(NG_ANIMATE_STATE, rootAnimateState); // disable animations during bootstrap, but once we bootstrapped, wait again // for another digest until enabling animations. The reason why we digest twice // is because all structural animations (enter, leave and move) all perform a @@ -319,14 +343,10 @@ ? function() { return true; } : function(className) { return classNameFilter.test(className); }; - function async(fn) { - return $timeout(fn, 0, false); - } - function lookup(name) { if (name) { var matches = [], flagMap = {}, classes = name.substr(1).split('.'); @@ -404,10 +424,11 @@ */ enter : function(element, parentElement, afterElement, doneCallback) { this.enabled(false, element); $delegate.enter(element, parentElement, afterElement); $rootScope.$$postDigest(function() { + element = stripCommentsFromElement(element); performAnimation('enter', 'ng-enter', element, parentElement, afterElement, noop, doneCallback); }); }, /** @@ -440,10 +461,11 @@ */ leave : function(element, doneCallback) { cancelChildAnimations(element); this.enabled(false, element); $rootScope.$$postDigest(function() { + element = stripCommentsFromElement(element); performAnimation('leave', 'ng-leave', element, null, null, function() { $delegate.leave(element); }, doneCallback); }); }, @@ -482,10 +504,11 @@ move : function(element, parentElement, afterElement, doneCallback) { cancelChildAnimations(element); this.enabled(false, element); $delegate.move(element, parentElement, afterElement); $rootScope.$$postDigest(function() { + element = stripCommentsFromElement(element); performAnimation('move', 'ng-move', element, parentElement, afterElement, noop, doneCallback); }); }, /** @@ -517,10 +540,11 @@ * @param {jQuery/jqLite element} element the element that will be animated * @param {string} className the CSS class that will be added to the element and then animated * @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); }, @@ -553,15 +577,38 @@ * @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()=} 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 * @methodOf ngAnimate.$animate * @function @@ -604,11 +651,19 @@ and the className value is the name of the animation that will be applied within the CSS code. Element, parentElement and afterElement are provided DOM elements for the animation and the onComplete callback will be fired once the animation is fully complete. */ function performAnimation(animationEvent, className, element, parentElement, afterElement, domOperation, doneCallback) { - var currentClassName, classes, node = extractElementNode(element); + + 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; } @@ -616,23 +671,32 @@ //best to catch this early on to prevent any animation operations from occurring if(!node || !isAnimatableClassName(classes)) { fireDOMOperation(); fireBeforeCallbackAsync(); fireAfterCallbackAsync(); - closeAnimation(); + fireDoneCallbackAsync(); return; } + var elementEvents = angular.element._data(node); + elementEvents = elementEvents && elementEvents.events; + var animationLookup = (' ' + classes).replace(/\s+/g,'.'); if (!parentElement) { 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 //the animation if any matching animations are not found at all. //NOTE: IE8 + IE9 should close properly (run closeAnimation()) in case a NO animation is not found. if (animationsDisabled(element, parentElement) || matches.length === 0) { @@ -646,11 +710,11 @@ var animations = []; //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) { forEach(matches, function(animation) { //add the animation to the queue to if it is allowed to be cancelled @@ -681,97 +745,101 @@ fireAfterCallbackAsync(); fireDoneCallbackAsync(); 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 - $timeout.cancel(ngAnimateState.closeAnimationTimeout); - cleanup(element); - cancelAnimations(ngAnimateState.animations); + 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); + } + } - //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; + 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(); 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, - event:animationEvent, - className:className, - structural:!isClassBased, - animations:animations, + var localAnimationCount = globalAnimationCounter++; + lastAnimation = { + classBased : isClassBased, + event : animationEvent, + animations : animations, done:onBeforeAnimationsComplete + }; + + totalActiveAnimations++; + runningAnimations[className] = lastAnimation; + + element.data(NG_ANIMATE_STATE, { + last : lastAnimation, + active : runningAnimations, + index : localAnimationCount, + totalActive : totalActiveAnimations }); //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); 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(); return; } //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); } function invokeRegisteredAnimationFns(animations, phase, allAnimationFnsComplete) { phase == 'after' ? @@ -790,13 +858,17 @@ animationPhaseCompleted(); return; } 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(); } }); @@ -813,33 +885,36 @@ allAnimationFnsComplete(); } } function fireDOMCallback(animationPhase) { - element.triggerHandler('$animate:' + animationPhase, { - event : animationEvent, - className : className - }); + var eventName = '$animate:' + animationPhase; + if(elementEvents && elementEvents[eventName] && elementEvents[eventName].length > 0) { + $$asyncQueueBuffer(function() { + element.triggerHandler(eventName, { + event : animationEvent, + className : className + }); + }); + } } function fireBeforeCallbackAsync() { - async(function() { - fireDOMCallback('before'); - }); + fireDOMCallback('before'); } function fireAfterCallbackAsync() { - async(function() { - fireDOMCallback('after'); - }); + fireDOMCallback('after'); } function fireDoneCallbackAsync() { - async(function() { - fireDOMCallback('close'); - doneCallback && doneCallback(); - }); + fireDOMCallback('close'); + if(doneCallback) { + $$asyncQueueBuffer(function() { + doneCallback(); + }); + } } //it is less complicated to use a flag than managing and cancelling //timeouts containing multiple callbacks. function fireDOMOperation() { @@ -857,14 +932,17 @@ /* only structural animations wait for reflow before removing an animation, but class-based animations don't. An example of this 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 { - data.closeAnimationTimeout = async(function() { - cleanup(element); + $$asyncQueueBuffer(function() { + var data = element.data(NG_ANIMATE_STATE) || {}; + if(localAnimationCount == data.index) { + cleanup(element, className, animationEvent); + } }); element.data(NG_ANIMATE_STATE, data); } } fireDoneCallbackAsync(); @@ -875,13 +953,15 @@ function cancelChildAnimations(element) { var node = extractElementNode(element); forEach(node.querySelectorAll('.' + NG_ANIMATE_CLASS_NAME), function(element) { element = angular.element(element); var data = element.data(NG_ANIMATE_STATE); - if(data) { - cancelAnimations(data.animations); - cleanup(element); + if(data && data.active) { + angular.forEach(data.active, function(operation) { + (operation.done || noop)(true); + cancelAnimations(operation.animations); + }); } }); } function cancelAnimations(animations) { @@ -894,19 +974,31 @@ (animation.afterEnd || noop)(isCancelledFlag); } }); } - 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); + } } } function animationsDisabled(element, parentElement) { if (rootAnimateState.disabled) return true; @@ -921,11 +1013,11 @@ //any animations on it if(parentElement.length === 0) break; 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; } if(isRoot) return true; @@ -971,78 +1063,61 @@ 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_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)(); } }); } @@ -1123,16 +1198,16 @@ parentID = parentCounter; } 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; var applyClasses = !lookupCache[staggerCacheKey]; @@ -1148,90 +1223,99 @@ calculationDecorator = calculationDecorator || function(fn) { return fn(); }; 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) { var prop = ANIMATION_PROP; var node = extractElementNode(element); if(node.style[prop] && node.style[prop].length > 0) { node.style[prop] = ''; } } - 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) { var propertyStyle = timings.transitionPropertyStyle; if(propertyStyle.indexOf('all') == -1) { @@ -1269,10 +1353,17 @@ element.addClass(activeClassName); elementData.closeAnimationFn = function() { 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 // there is no need to attach this internally to the // timeout done method. @@ -1288,11 +1379,11 @@ function onAnimationProgress(event) { event.stopPropagation(); var ev = event.originalEvent || event; var timeStamp = ev.$manualTimeStamp || ev.timeStamp || Date.now(); - + /* Firefox (or possibly just Gecko) likes to not round values up * when a ms measurement is used for the animation */ var elapsedTime = parseFloat(ev.elapsedTime.toFixed(ELAPSED_TIME_MAX_DECIMAL_PLACES)); /* $manualTimeStamp is a mocked timeStamp value which is set @@ -1315,32 +1406,32 @@ (index * staggerDelay + parseInt(val, 10)) + 's'; }); 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; } @@ -1349,73 +1440,78 @@ //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(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) { (cancel || noop)(cancelled); }; } 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 * is added at the time of the animation. This is how CSS3 functions * outside of ngAnimate. */ @@ -1425,25 +1521,32 @@ return timings; }); if(cancellationMethod) { afterReflow(element, function() { - unblockTransitions(element); + unblockTransitions(element, className); unblockKeyframeAnimations(element); animationCompleted(); }); return cancellationMethod; } 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. * http://plnkr.co/edit/j8OzgTNxHTb4n3zLyjGW?p=preview */ var klass = element.attr('class'); @@ -1453,20 +1556,20 @@ return timings; }); if(cancellationMethod) { afterReflow(element, function() { - unblockTransitions(element); + unblockTransitions(element, className); unblockKeyframeAnimations(element); animationCompleted(); }); return cancellationMethod; } animationCompleted(); }, removeClass : function(element, className, animationCompleted) { - return animateAfter(element, suffixClasses(className, '-remove'), animationCompleted); + return animateAfter('removeClass', element, suffixClasses(className, '-remove'), animationCompleted); } }; function suffixClasses(classes, suffix) { var className = '';