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();