vendor/assets/javascripts/angular-animate.js in angularjs-rails-1.2.0.rc2 vs vendor/assets/javascripts/angular-animate.js in angularjs-rails-1.2.0.rc3

- old
+ new

@@ -1,7 +1,7 @@ /** - * @license AngularJS v1.2.0-rc.2 + * @license AngularJS v1.2.0-rc.3 * (c) 2010-2012 Google, Inc. http://angularjs.org * License: MIT */ (function(window, angular, undefined) {'use strict'; @@ -30,11 +30,11 @@ * | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave and move | * | {@link ngRoute.directive:ngView#animations ngView} | enter and leave | * | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave | * | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave | * | {@link ng.directive:ngIf#animations ngIf} | enter and leave | - * | {@link ng.directive:ngShow#animations ngClass} | add and remove | + * | {@link ng.directive:ngClass#animations ngClass} | add and remove | * | {@link ng.directive:ngShow#animations ngShow & ngHide} | add and remove (the ng-hide class value) | * * You can find out more information about animations upon visiting each directive page. * * Below is an example of how to apply animations to a directive that supports animation hooks: @@ -205,10 +205,11 @@ var noop = angular.noop; var forEach = angular.forEach; var selectors = $animateProvider.$$selectors; var NG_ANIMATE_STATE = '$$ngAnimateState'; + var NG_ANIMATE_CLASS_NAME = 'ng-animate'; var rootAnimateState = {running:true}; $provide.decorator('$animate', ['$delegate', '$injector', '$sniffer', '$rootElement', '$timeout', '$rootScope', function($delegate, $injector, $sniffer, $rootElement, $timeout, $rootScope) { $rootElement.data(NG_ANIMATE_STATE, rootAnimateState); @@ -220,12 +221,15 @@ classes = name.substr(1).split('.'); //the empty string value is the default animation //operation which performs CSS transition and keyframe //animations sniffing. This is always included for each - //element animation procedure - classes.push(''); + //element animation procedure if the browser supports + //transitions and/or keyframe animations + if ($sniffer.transitions || $sniffer.animations) { + classes.push(''); + } for(var i=0; i < classes.length; i++) { var klass = classes[i], selectorFactoryName = selectors[klass]; if(selectorFactoryName && !flagMap[klass]) { @@ -286,10 +290,11 @@ * @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 */ enter : function(element, parent, after, done) { + this.enabled(false, element); $delegate.enter(element, parent, after); $rootScope.$$postDigest(function() { performAnimation('enter', 'ng-enter', element, parent, after, function() { done && $timeout(done, 0, false); }); @@ -322,10 +327,12 @@ * * @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 */ leave : function(element, done) { + cancelChildAnimations(element); + this.enabled(false, element); $rootScope.$$postDigest(function() { performAnimation('leave', 'ng-leave', element, null, null, function() { $delegate.leave(element, done); }); }); @@ -360,10 +367,12 @@ * @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 */ move : function(element, parent, after, done) { + cancelChildAnimations(element); + this.enabled(false, element); $delegate.move(element, parent, after); $rootScope.$$postDigest(function() { performAnimation('move', 'ng-move', element, null, null, function() { done && $timeout(done, 0, false); }); @@ -450,16 +459,34 @@ * * @description * Globally enables/disables animations. * */ - enabled : function(value) { - if (arguments.length) { - rootAnimateState.running = !value; + enabled : function(value, element) { + switch(arguments.length) { + case 2: + if(value) { + cleanup(element); + } + else { + var data = element.data(NG_ANIMATE_STATE) || {}; + data.structural = true; + data.running = true; + element.data(NG_ANIMATE_STATE, data); + } + break; + + case 1: + rootAnimateState.running = !value; + break; + + default: + value = !rootAnimateState.running + break; } - return !rootAnimateState.running; - } + return !!value; + } }; /* all animations call this shared animation triggering function internally. The event variable refers to the JavaScript animation event that will be triggered @@ -482,56 +509,56 @@ } var disabledAnimation = { running : true }; //skip the animation if animations are disabled, a parent is already being animated //or the element is not currently attached to the document body. - if ((parent.inheritedData(NG_ANIMATE_STATE) || disabledAnimation).running) { - //avoid calling done() since there is no need to remove any - //data or className values since this happens earlier than that - //and also use a timeout so that it won't be asynchronous - $timeout(onComplete || noop, 0, false); + if ((parent.inheritedData(NG_ANIMATE_STATE) || disabledAnimation).running || animations.length == 0) { + done(); return; } var ngAnimateState = element.data(NG_ANIMATE_STATE) || {}; - //if an animation is currently running on the element then lets take the steps - //to cancel that animation and fire any required callbacks + var isClassBased = event == 'addClass' || event == 'removeClass'; if(ngAnimateState.running) { + if(isClassBased && ngAnimateState.structural) { + onComplete && onComplete(); + return; + } + + //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); cancelAnimations(ngAnimateState.animations); - ngAnimateState.done(); + (ngAnimateState.done || noop)(); } element.data(NG_ANIMATE_STATE, { running:true, + structural:!isClassBased, animations:animations, done:done }); + //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); + forEach(animations, function(animation, index) { var fn = function() { progress(index); }; if(animation.start) { - if(event == 'addClass' || event == 'removeClass') { - animation.endFn = animation.start(element, className, fn); - } else { - animation.endFn = animation.start(element, fn); - } + animation.endFn = isClassBased ? + animation.start(element, className, fn) : + animation.start(element, fn); } else { fn(); } }); - function cancelAnimations(animations) { - var isCancelledFlag = true; - forEach(animations, function(animation) { - (animation.endFn || noop)(isCancelledFlag); - }); - } - function progress(index) { animations[index].done = true; (animations[index].endFn || noop)(); for(var i=0;i<animations.length;i++) { if(!animations[i].done) return; @@ -540,138 +567,247 @@ } function done() { if(!done.hasBeenRun) { done.hasBeenRun = true; - element.removeData(NG_ANIMATE_STATE); + var data = element.data(NG_ANIMATE_STATE); + if(data) { + /* 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); + } else { + data.flagTimer = $timeout(function() { + cleanup(element); + }, 0, false); + element.data(NG_ANIMATE_STATE, data); + } + } (onComplete || noop)(); } } } + + function cancelChildAnimations(element) { + angular.forEach(element[0].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); + } + }); + } + + function cancelAnimations(animations) { + var isCancelledFlag = true; + forEach(animations, function(animation) { + (animation.endFn || noop)(isCancelledFlag); + }); + } + + function cleanup(element) { + element.removeClass(NG_ANIMATE_CLASS_NAME); + element.removeData(NG_ANIMATE_STATE); + } }]); - $animateProvider.register('', ['$window','$sniffer', '$timeout', function($window, $sniffer, $timeout) { - var noop = angular.noop; + $animateProvider.register('', ['$window', '$sniffer', '$timeout', function($window, $sniffer, $timeout) { var forEach = angular.forEach; - //one day all browsers will have these properties - var w3cAnimationProp = 'animation'; - var w3cTransitionProp = 'transition'; + // Detect proper transitionend/animationend event names. + var transitionProp, transitionendEvent, animationProp, animationendEvent; - //but some still use vendor-prefixed styles - var vendorAnimationProp = $sniffer.vendorPrefix + 'Animation'; - var vendorTransitionProp = $sniffer.vendorPrefix + 'Transition'; + // 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. + // Note: Chrome implements `window.onwebkitanimationend` and doesn't implement `window.onanimationend` + // but at the same time dispatches the `animationend` event and not `webkitAnimationEnd`. + // Register both events in case `window.onanimationend` is not supported because of that, + // do the same for `transitionend` as Safari is likely to exhibit similar behavior. + // 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) { + transitionProp = 'WebkitTransition'; + transitionendEvent = 'webkitTransitionEnd transitionend'; + } else { + transitionProp = 'transition'; + transitionendEvent = 'transitionend'; + } + if (window.onanimationend === undefined && window.onwebkitanimationend !== undefined) { + animationProp = 'WebkitAnimation'; + animationendEvent = 'webkitAnimationEnd animationend'; + } else { + animationProp = 'animation'; + animationendEvent = 'animationend'; + } + var durationKey = 'Duration', - delayKey = 'Delay', propertyKey = 'Property', + delayKey = 'Delay', animationIterationCountKey = 'IterationCount', ELEMENT_NODE = 1; - function animate(element, className, done) { - if (!($sniffer.transitions || $sniffer.animations)) { - done(); - return; - } - else if(['ng-enter','ng-leave','ng-move'].indexOf(className) == -1) { - var existingDuration = 0; + var NG_ANIMATE_PARENT_KEY = '$ngAnimateKey'; + var lookupCache = {}; + var parentCounter = 0; + + var animationReflowQueue = [], animationTimer, timeOut = false; + function afterReflow(callback) { + animationReflowQueue.push(callback); + $timeout.cancel(animationTimer); + animationTimer = $timeout(function() { + angular.forEach(animationReflowQueue, function(fn) { + fn(); + }); + animationReflowQueue = []; + animationTimer = null; + lookupCache = {}; + }, 10, false); + } + + function getElementAnimationDetails(element, cacheKey, onlyCheckTransition) { + var data = lookupCache[cacheKey]; + if(!data) { + var transitionDuration = 0, transitionDelay = 0, + animationDuration = 0, animationDelay = 0; + + //we want all the styles defined before and after forEach(element, function(element) { if (element.nodeType == ELEMENT_NODE) { var elementStyles = $window.getComputedStyle(element) || {}; - existingDuration = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + durationKey]), - parseMaxTime(elementStyles[vendorTransitionProp + durationKey]), - existingDuration); + + transitionDuration = Math.max(parseMaxTime(elementStyles[transitionProp + durationKey]), transitionDuration); + + if(!onlyCheckTransition) { + transitionDelay = Math.max(parseMaxTime(elementStyles[transitionProp + delayKey]), transitionDelay); + + animationDelay = Math.max(parseMaxTime(elementStyles[animationProp + delayKey]), animationDelay); + + var aDuration = parseMaxTime(elementStyles[animationProp + durationKey]); + + if(aDuration > 0) { + aDuration *= parseInt(elementStyles[animationProp + animationIterationCountKey]) || 1; + } + + animationDuration = Math.max(aDuration, animationDuration); + } } }); - if(existingDuration > 0) { - done(); - return; - } + data = { + transitionDelay : transitionDelay, + animationDelay : animationDelay, + transitionDuration : transitionDuration, + animationDuration : animationDuration + }; + lookupCache[cacheKey] = data; } + return data; + } - element.addClass(className); + function parseMaxTime(str) { + var total = 0, values = angular.isString(str) ? str.split(/\s*,\s*/) : []; + forEach(values, function(value) { + total = Math.max(parseFloat(value) || 0, total); + }); + return total; + } - //we want all the styles defined before and after - var duration = 0; - forEach(element, function(element) { - if (element.nodeType == ELEMENT_NODE) { - var elementStyles = $window.getComputedStyle(element) || {}; + function getCacheKey(element) { + var parent = element.parent(); + var parentID = parent.data(NG_ANIMATE_PARENT_KEY); + if(!parentID) { + parent.data(NG_ANIMATE_PARENT_KEY, ++parentCounter); + parentID = parentCounter; + } + return parentID + '-' + element[0].className; + } - var transitionDelay = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + delayKey]), - parseMaxTime(elementStyles[vendorTransitionProp + delayKey])); + function animate(element, className, done) { - var animationDelay = Math.max(parseMaxTime(elementStyles[w3cAnimationProp + delayKey]), - parseMaxTime(elementStyles[vendorAnimationProp + delayKey])); + var cacheKey = getCacheKey(element); + if(getElementAnimationDetails(element, cacheKey, true).transitionDuration > 0) { - var transitionDuration = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + durationKey]), - parseMaxTime(elementStyles[vendorTransitionProp + durationKey])); + done(); + return; + } - var animationDuration = Math.max(parseMaxTime(elementStyles[w3cAnimationProp + durationKey]), - parseMaxTime(elementStyles[vendorAnimationProp + durationKey])); + element.addClass(className); - if(animationDuration > 0) { - animationDuration *= Math.max(parseInt(elementStyles[w3cAnimationProp + animationIterationCountKey]) || 0, - parseInt(elementStyles[vendorAnimationProp + animationIterationCountKey]) || 0, - 1); - } + var timings = getElementAnimationDetails(element, cacheKey + ' ' + className); - duration = Math.max(animationDelay + animationDuration, - transitionDelay + transitionDuration, - duration); - } - }); - /* there is no point in performing a reflow if the animation timeout is empty (this would cause a flicker bug normally - in the page */ - if(duration > 0) { - var node = element[0]; + 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). - node.style[w3cTransitionProp + propertyKey] = 'none'; - node.style[vendorTransitionProp + propertyKey] = 'none'; + if(timings.transitionDuration > 0) { + node.style[transitionProp + propertyKey] = 'none'; + } var activeClassName = ''; forEach(className.split(' '), function(klass, i) { activeClassName += (i > 0 ? ' ' : '') + klass + '-active'; }); - //this triggers a reflow which allows for the transition animation to kick in - element.prop('clientWidth'); - node.style[w3cTransitionProp + propertyKey] = ''; - node.style[vendorTransitionProp + propertyKey] = ''; - element.addClass(activeClassName); + // This triggers a reflow which allows for the transition animation to kick in. + var css3AnimationEvents = animationendEvent + ' ' + transitionendEvent; - $timeout(done, duration * 1000, false); + afterReflow(function() { + if(timings.transitionDuration > 0) { + node.style[transitionProp + propertyKey] = ''; + } + element.addClass(activeClassName); + }); - //this will automatically be called by $animate so - //there is no need to attach this internally to the - //timeout done method + 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); - //only when the animation is cancelled is the done() - //function not called for this animation therefore - //this must be also called + // 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(); } - function parseMaxTime(str) { - var total = 0, values = angular.isString(str) ? str.split(/\s*,\s*/) : []; - forEach(values, function(value) { - total = Math.max(parseFloat(value) || 0, total); - }); - return total; + function onAnimationProgress(event) { + event.stopPropagation(); + var ev = event.originalEvent || event; + var timeStamp = ev.$manualTimeStamp || ev.timeStamp || Date.now(); + /* $manualTimeStamp is a mocked timeStamp value which is set + * within browserTrigger(). This is only here so that tests can + * mock animations properly. Real events fallback to event.timeStamp, + * or, if they don't, then a timeStamp is automatically created for them. + * We're checking to see if the timeStamp surpasses the expected delay, + * 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(); + } } + } return { enter : function(element, done) { return animate(element, 'ng-enter', done);