vendor/assets/javascripts/angular-animate.js in angular-gem-1.2.8 vs vendor/assets/javascripts/angular-animate.js in angular-gem-1.2.9

- old
+ new

@@ -1,7 +1,7 @@ /** - * @license AngularJS v1.2.8 + * @license AngularJS v1.2.9 * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT */ (function(window, angular, undefined) {'use strict'; @@ -252,10 +252,30 @@ * 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) { + var requestAnimationFrame = $window.requestAnimationFrame || + $window.webkitRequestAnimationFrame || + function(fn) { + return $timeout(fn, 10, false); + }; + + var cancelAnimationFrame = $window.cancelAnimationFrame || + $window.webkitCancelAnimationFrame || + function(timer) { + return $timeout.cancel(timer); + }; + return function(fn) { + var id = requestAnimationFrame(fn); + return function() { + cancelAnimationFrame(id); + }; + }; + }]) + .config(['$provide', '$animateProvider', function($provide, $animateProvider) { var noop = angular.noop; var forEach = angular.forEach; var selectors = $animateProvider.$$selectors; @@ -299,10 +319,14 @@ ? 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('.'); @@ -590,10 +614,12 @@ //transcluded directives may sometimes fire an animation using only comment nodes //best to catch this early on to prevent any animation operations from occurring if(!node || !isAnimatableClassName(classes)) { fireDOMOperation(); + fireBeforeCallbackAsync(); + fireAfterCallbackAsync(); closeAnimation(); return; } var animationLookup = (' ' + classes).replace(/\s+/g,'.'); @@ -609,10 +635,12 @@ //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) { fireDOMOperation(); + fireBeforeCallbackAsync(); + fireAfterCallbackAsync(); closeAnimation(); return; } var animations = []; @@ -647,51 +675,67 @@ //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) { fireDOMOperation(); + fireBeforeCallbackAsync(); + 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 = ' ' + currentClassName + ' '; + 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); + //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) { + if(ngAnimateState.beforeComplete || isRevertingClassAnimation) { (ngAnimateState.done || noop)(true); - } else if(isClassBased && !ngAnimateState.structural) { + } 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(ngAnimateState.className, '') : - futureClassName + ngAnimateState.className + ' '; + futureClassName.replace(ONE_SPACE + ngAnimateState.className + ONE_SPACE, ONE_SPACE) : + futureClassName + ngAnimateState.className + ONE_SPACE; } } //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 = ' ' + className + ' '; + var classNameToken = ONE_SPACE + className + ONE_SPACE; if((animationEvent == 'addClass' && futureClassName.indexOf(classNameToken) >= 0) || (animationEvent == 'removeClass' && futureClassName.indexOf(classNameToken) == -1)) { fireDOMOperation(); + fireBeforeCallbackAsync(); + fireAfterCallbackAsync(); fireDoneCallbackAsync(); return; } //the ng-animate class does nothing, but it's here to allow for @@ -728,10 +772,14 @@ } invokeRegisteredAnimationFns(animations, 'after', closeAnimation); } function invokeRegisteredAnimationFns(animations, phase, allAnimationFnsComplete) { + phase == 'after' ? + fireAfterCallbackAsync() : + fireBeforeCallbackAsync(); + var endFnName = phase + 'End'; forEach(animations, function(animation, index) { var animationPhaseCompleted = function() { progress(index, phase); }; @@ -764,12 +812,31 @@ allAnimationFnsComplete(); } } + function fireDOMCallback(animationPhase) { + element.triggerHandler('$animate:' + animationPhase, { + event : animationEvent, + className : className + }); + } + + function fireBeforeCallbackAsync() { + async(function() { + fireDOMCallback('before'); + }); + } + + function fireAfterCallbackAsync() { + async(function() { + fireDOMCallback('after'); + }); + } + function fireDoneCallbackAsync() { - doneCallback && $timeout(doneCallback, 0, false); + doneCallback && async(doneCallback); } //it is less complicated to use a flag than managing and cancelling //timeouts containing multiple callbacks. function fireDOMOperation() { @@ -789,13 +856,13 @@ 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); } else { - data.closeAnimationTimeout = $timeout(function() { + data.closeAnimationTimeout = async(function() { cleanup(element); - }, 0, false); + }); element.data(NG_ANIMATE_STATE, data); } } fireDoneCallbackAsync(); } @@ -815,14 +882,14 @@ } function cancelAnimations(animations) { var isCancelledFlag = true; forEach(animations, function(animation) { - if(!animations.beforeComplete) { + if(!animation.beforeComplete) { (animation.beforeEnd || noop)(isCancelledFlag); } - if(!animations.afterComplete) { + if(!animation.afterComplete) { (animation.afterEnd || noop)(isCancelledFlag); } }); } @@ -864,11 +931,12 @@ return true; } }]); - $animateProvider.register('', ['$window', '$sniffer', '$timeout', function($window, $sniffer, $timeout) { + $animateProvider.register('', ['$window', '$sniffer', '$timeout', '$$animateReflow', + function($window, $sniffer, $timeout, $$animateReflow) { // Detect proper transitionend/animationend event names. 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. @@ -909,32 +977,38 @@ var animationCounter = 0; var lookupCache = {}; var parentCounter = 0; var animationReflowQueue = []; var animationElementQueue = []; - var animationTimer; + var cancelAnimationReflow; var closingAnimationTime = 0; var timeOut = false; function afterReflow(element, callback) { - $timeout.cancel(animationTimer); + 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); - closingAnimationTime = Math.max(closingAnimationTime, - (elementData.maxDelay + elementData.maxDuration) * CLOSING_TIME_BUFFER * ONE_SECOND); + 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; - animationTimer = $timeout(function() { + cancelAnimationReflow = $$animateReflow(function() { forEach(animationReflowQueue, function(fn) { fn(); }); //copy the list of elements so that successive @@ -951,15 +1025,15 @@ elementQueueSnapshot = null; }, closingAnimationTime, false); animationReflowQueue = []; animationElementQueue = []; - animationTimer = null; + cancelAnimationReflow = null; lookupCache = {}; closingAnimationTime = 0; animationCounter++; - }, 10, false); + }); } function closeAllAnimations(elements, count) { forEach(elements, function(element) { var elementData = element.data(NG_ANIMATE_CSS_DATA_KEY); @@ -1046,17 +1120,17 @@ parentID = parentCounter; } return parentID + '-' + extractElementNode(element).className; } - function animateSetup(element, className) { + function animateSetup(element, className, calculationDecorator) { var cacheKey = getCacheKey(element); var eventCacheKey = cacheKey + ' ' + className; var stagger = {}; - var ii = lookupCache[eventCacheKey] ? ++lookupCache[eventCacheKey].total : 0; + var itemIndex = lookupCache[eventCacheKey] ? ++lookupCache[eventCacheKey].total : 0; - if(ii > 0) { + if(itemIndex > 0) { var staggerClassName = className + '-stagger'; var staggerCacheKey = cacheKey + ' ' + staggerClassName; var applyClasses = !lookupCache[staggerCacheKey]; applyClasses && element.addClass(staggerClassName); @@ -1064,13 +1138,20 @@ stagger = getElementAnimationDetails(element, staggerCacheKey); applyClasses && element.removeClass(staggerClassName); } + /* the animation itself may need to add/remove special CSS classes + * before calculating the anmation styles */ + calculationDecorator = calculationDecorator || + function(fn) { return fn(); }; + element.addClass(className); - var timings = getElementAnimationDetails(element, eventCacheKey); + 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 */ @@ -1098,11 +1179,11 @@ maxDuration : maxDuration, maxDelay : maxDelay, classes : className + ' ' + activeClassName, timings : timings, stagger : stagger, - ii : ii + itemIndex : itemIndex }); return true; } @@ -1143,11 +1224,11 @@ var maxDuration = elementData.maxDuration; var activeClassName = elementData.activeClassName; var maxDelayTime = Math.max(timings.transitionDelay, timings.animationDelay) * ONE_SECOND; var startTime = Date.now(); var css3AnimationEvents = ANIMATIONEND_EVENT + ' ' + TRANSITIONEND_EVENT; - var ii = elementData.ii; + var itemIndex = elementData.itemIndex; var style = '', appliedStyles = []; if(timings.transitionDuration > 0) { var propertyStyle = timings.transitionPropertyStyle; if(propertyStyle.indexOf('all') == -1) { @@ -1156,21 +1237,21 @@ appliedStyles.push(CSS_PREFIX + 'transition-property'); appliedStyles.push(CSS_PREFIX + 'transition-duration'); } } - if(ii > 0) { + if(itemIndex > 0) { if(stagger.transitionDelay > 0 && stagger.transitionDuration === 0) { var delayStyle = timings.transitionDelayStyle; style += CSS_PREFIX + 'transition-delay: ' + - prepareStaggerDelay(delayStyle, stagger.transitionDelay, ii) + '; '; + prepareStaggerDelay(delayStyle, stagger.transitionDelay, itemIndex) + '; '; appliedStyles.push(CSS_PREFIX + 'transition-delay'); } if(stagger.animationDelay > 0 && stagger.animationDuration === 0) { style += CSS_PREFIX + 'animation-delay: ' + - prepareStaggerDelay(timings.animationDelayStyle, stagger.animationDelay, ii) + '; '; + prepareStaggerDelay(timings.animationDelayStyle, stagger.animationDelay, itemIndex) + '; '; appliedStyles.push(CSS_PREFIX + 'animation-delay'); } } if(appliedStyles.length > 0) { @@ -1231,12 +1312,12 @@ (index * staggerDelay + parseInt(val, 10)) + 's'; }); return style; } - function animateBefore(element, className) { - if(animateSetup(element, className)) { + function animateBefore(element, className, calculationDecorator) { + if(animateSetup(element, className, calculationDecorator)) { return function(cancelled) { cancelled && animateClose(element, className); }; } } @@ -1327,11 +1408,22 @@ move : function(element, animationCompleted) { return animate(element, 'ng-move', animationCompleted); }, beforeAddClass : function(element, className, animationCompleted) { - var cancellationMethod = animateBefore(element, suffixClasses(className, '-add')); + var cancellationMethod = animateBefore(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. */ + element.addClass(className); + var timings = fn(); + element.removeClass(className); + return timings; + }); + if(cancellationMethod) { afterReflow(element, function() { unblockTransitions(element); unblockKeyframeAnimations(element); animationCompleted(); @@ -1344,10 +1436,21 @@ addClass : function(element, className, animationCompleted) { return animateAfter(element, suffixClasses(className, '-add'), animationCompleted); }, beforeRemoveClass : function(element, className, animationCompleted) { - var cancellationMethod = animateBefore(element, suffixClasses(className, '-remove')); + var cancellationMethod = animateBefore(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'); + element.removeClass(className); + var timings = fn(); + element.attr('class', klass); + return timings; + }); + if(cancellationMethod) { afterReflow(element, function() { unblockTransitions(element); unblockKeyframeAnimations(element); animationCompleted();