vendor/assets/javascripts/angular-animate.js in angular-gem-1.2.13 vs vendor/assets/javascripts/angular-animate.js in angular-gem-1.2.14

- old
+ new

@@ -1,24 +1,23 @@ /** - * @license AngularJS v1.2.13 + * @license AngularJS v1.2.14 * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT */ (function(window, angular, undefined) {'use strict'; /* jshint maxlen: false */ /** - * @ngdoc overview + * @ngdoc module * @name ngAnimate * @description * * # ngAnimate * * The `ngAnimate` module provides support for JavaScript, CSS3 transition and CSS3 keyframe animation hooks within existing core and custom directives. * - * {@installModule animate} * * <div doc-module-components="ngAnimate"></div> * * # Usage * @@ -36,16 +35,18 @@ * | {@link ng.directive:ngInclude#usage_animations ngInclude} | enter and leave | * | {@link ng.directive:ngSwitch#usage_animations ngSwitch} | enter and leave | * | {@link ng.directive:ngIf#usage_animations ngIf} | enter and leave | * | {@link ng.directive:ngClass#usage_animations ngClass} | add and remove | * | {@link ng.directive:ngShow#usage_animations ngShow & ngHide} | add and remove (the ng-hide class value) | + * | {@link ng.directive:form#usage_animations form} | add and remove (dirty, pristine, valid, invalid & all other validations) | + * | {@link ng.directive:ngModel#usage_animations ngModel} | add and remove (dirty, pristine, valid, invalid & all other validations) | * * 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: * - * <pre> + * ```html * <style type="text/css"> * .slide.ng-enter, .slide.ng-leave { * -webkit-transition:0.5s linear all; * transition:0.5s linear all; * } @@ -59,11 +60,11 @@ * <!-- * the animate service will automatically add .ng-enter and .ng-leave to the element * to trigger the CSS transition/animations * --> * <ANY class="slide" ng-include="..."></ANY> - * </pre> + * ``` * * Keep in mind that if an animation is running, any child elements cannot be animated until the parent element's * animation has completed. * * <h2>CSS-defined Animations</h2> @@ -71,11 +72,11 @@ * are designed to contain the start and end CSS styling. Both CSS transitions and keyframe animations are supported * and can be used to play along with this naming structure. * * The following code below demonstrates how to perform animations using **CSS transitions** with Angular: * - * <pre> + * ```html * <style type="text/css"> * /&#42; * The animate class is apart of the element and the ng-enter class * is attached to the element once the enter animation event is triggered * &#42;/ @@ -99,15 +100,15 @@ * </style> * * <div class="view-container"> * <div ng-view class="reveal-animation"></div> * </div> - * </pre> + * ``` * * The following code below demonstrates how to perform animations using **CSS animations** with Angular: * - * <pre> + * ```html * <style type="text/css"> * .reveal-animation.ng-enter { * -webkit-animation: enter_sequence 1s linear; /&#42; Safari/Chrome &#42;/ * animation: enter_sequence 1s linear; /&#42; IE10+ and Future Browsers &#42;/ * } @@ -122,11 +123,11 @@ * </style> * * <div class="view-container"> * <div ng-view class="reveal-animation"></div> * </div> - * </pre> + * ``` * * Both CSS3 animations and transitions can be used together and the animate service will figure out the correct duration and delay timing. * * Upon DOM mutation, the event class is added first (something like `ng-enter`), then the browser prepares itself to add * the active class (in this case `ng-enter-active`) which then triggers the animation. The animation module will automatically @@ -140,11 +141,11 @@ * curtain-like effect. The ngAnimate module, as of 1.2.0, supports staggering animations and the stagger effect can be * performed by creating a **ng-EVENT-stagger** CSS class and attaching that class to the base CSS class used for * the animation. The style property expected within the stagger class can either be a **transition-delay** or an * **animation-delay** property (or both if your animation contains both transitions and keyframe animations). * - * <pre> + * ```css * .my-animation.ng-enter { * /&#42; standard transition code &#42;/ * -webkit-transition: 1s linear all; * transition: 1s linear all; * opacity:0; @@ -161,20 +162,20 @@ * } * .my-animation.ng-enter.ng-enter-active { * /&#42; standard transition styles &#42;/ * opacity:1; * } - * </pre> + * ``` * * Staggering animations work by default in ngRepeat (so long as the CSS class is defined). Outside of ngRepeat, to use staggering animations * on your own, they can be triggered by firing multiple calls to the same event on $animate. However, the restrictions surrounding this * are that each of the elements must have the same CSS className value as well as the same parent element. A stagger operation * will also be reset if more than 10ms has passed after the last animation has been fired. * * The following code will issue the **ng-leave-stagger** event on the element provided: * - * <pre> + * ```js * var kids = parent.children(); * * $animate.leave(kids[0]); //stagger index=0 * $animate.leave(kids[1]); //stagger index=1 * $animate.leave(kids[2]); //stagger index=2 @@ -184,19 +185,19 @@ * $timeout(function() { * //stagger has reset itself * $animate.leave(kids[5]); //stagger index=0 * $animate.leave(kids[6]); //stagger index=1 * }, 100, false); - * </pre> + * ``` * * Stagger animations are currently only supported within CSS-defined animations. * * <h2>JavaScript-defined Animations</h2> * In the event that you do not want to use CSS3 transitions or CSS3 animations or if you wish to offer animations on browsers that do not * yet support CSS transitions/animations, then you can make use of JavaScript animations defined inside of your AngularJS module. * - * <pre> + * ```js * //!annotate="YourApp" Your AngularJS Module|Replace this or ngModule with the module that you used to define your application. * var ngModule = angular.module('YourApp', ['ngAnimate']); * ngModule.animation('.my-crazy-animation', function() { * return { * enter: function(element, done) { @@ -221,11 +222,11 @@ * * //animation that can be triggered after the class is removed * removeClass: function(element, className, done) { } * }; * }); - * </pre> + * ``` * * JavaScript-defined animations are created with a CSS-like class selector and a collection of events which are set to run * a javascript callback function. When an animation is triggered, $animate will look for a matching animation which fits * the element's CSS class attribute value and then run the matching animation event function (if found). * In other words, if the CSS classes present on the animated element match any of the JavaScript animations then the callback function will @@ -239,12 +240,12 @@ */ angular.module('ngAnimate', ['ng']) /** - * @ngdoc object - * @name ngAnimate.$animateProvider + * @ngdoc provider + * @name $animateProvider * @description * * The `$animateProvider` allows developers to register JavaScript animation event handlers directly inside of a module. * When an animation is triggered, the $animate service will query the $animate service to find any animations that match * the provided name value. @@ -252,49 +253,31 @@ * 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', '$document', - function($window, $timeout, $document) { - var bod = $document[0].body; - 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); - }; + //this private service is only used within CSS-enabled animations + //IE8 + IE9 do not support rAF natively, but that is fine since they + //also don't support transitions and keyframes which means that the code + //below will never be used by the two browsers. + .factory('$$animateReflow', ['$$rAF', '$document', function($$rAF, $document) { + var bod = $document[0].body; return function(fn) { - var id = requestAnimationFrame(function() { + //the returned function acts as the cancellation function + return $$rAF(function() { + //the line below will force the browser to perform a repaint + //so that all the animated elements within the animation frame + //will be properly updated and drawn on screen. This is + //required to perform multi-class CSS based animations with + //Firefox. DO NOT REMOVE THIS LINE. 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; @@ -318,12 +301,12 @@ function isMatchingElement(elm1, elm2) { return extractElementNode(elm1) == extractElementNode(elm2); } - $provide.decorator('$animate', ['$delegate', '$injector', '$sniffer', '$rootElement', '$$asyncQueueBuffer', '$rootScope', '$document', - function($delegate, $injector, $sniffer, $rootElement, $$asyncQueueBuffer, $rootScope, $document) { + $provide.decorator('$animate', ['$delegate', '$injector', '$sniffer', '$rootElement', '$$asyncCallback', '$rootScope', '$document', + function($delegate, $injector, $sniffer, $rootElement, $$asyncCallback, $rootScope, $document) { var globalAnimationCounter = 0; $rootElement.data(NG_ANIMATE_STATE, rootAnimateState); // disable animations during bootstrap, but once we bootstrapped, wait again @@ -370,13 +353,155 @@ } return matches; } } + function animationRunner(element, animationEvent, className) { + //transcluded directives may sometimes fire an animation using only comment nodes + //best to catch this early on to prevent any animation operations from occurring + var node = element[0]; + if(!node) { + return; + } + + var isSetClassOperation = animationEvent == 'setClass'; + var isClassBased = isSetClassOperation || + animationEvent == 'addClass' || + animationEvent == 'removeClass'; + + var classNameAdd, classNameRemove; + if(angular.isArray(className)) { + classNameAdd = className[0]; + classNameRemove = className[1]; + className = classNameAdd + ' ' + classNameRemove; + } + + var currentClassName = element.attr('class'); + var classes = currentClassName + ' ' + className; + if(!isAnimatableClassName(classes)) { + return; + } + + var beforeComplete = noop, + beforeCancel = [], + before = [], + afterComplete = noop, + afterCancel = [], + after = []; + + var animationLookup = (' ' + classes).replace(/\s+/g,'.'); + forEach(lookup(animationLookup), function(animationFactory) { + var created = registerAnimation(animationFactory, animationEvent); + if(!created && isSetClassOperation) { + registerAnimation(animationFactory, 'addClass'); + registerAnimation(animationFactory, 'removeClass'); + } + }); + + function registerAnimation(animationFactory, event) { + var afterFn = animationFactory[event]; + var beforeFn = animationFactory['before' + event.charAt(0).toUpperCase() + event.substr(1)]; + if(afterFn || beforeFn) { + if(event == 'leave') { + beforeFn = afterFn; + //when set as null then animation knows to skip this phase + afterFn = null; + } + after.push({ + event : event, fn : afterFn + }); + before.push({ + event : event, fn : beforeFn + }); + return true; + } + } + + function run(fns, cancellations, allCompleteFn) { + var animations = []; + forEach(fns, function(animation) { + animation.fn && animations.push(animation); + }); + + var count = 0; + function afterAnimationComplete(index) { + if(cancellations) { + (cancellations[index] || noop)(); + if(++count < animations.length) return; + cancellations = null; + } + allCompleteFn(); + } + + //The code below adds directly to the array in order to work with + //both sync and async animations. Sync animations are when the done() + //operation is called right away. DO NOT REFACTOR! + forEach(animations, function(animation, index) { + var progress = function() { + afterAnimationComplete(index); + }; + switch(animation.event) { + case 'setClass': + cancellations.push(animation.fn(element, classNameAdd, classNameRemove, progress)); + break; + case 'addClass': + cancellations.push(animation.fn(element, classNameAdd || className, progress)); + break; + case 'removeClass': + cancellations.push(animation.fn(element, classNameRemove || className, progress)); + break; + default: + cancellations.push(animation.fn(element, progress)); + break; + } + }); + + if(cancellations && cancellations.length === 0) { + allCompleteFn(); + } + } + + return { + node : node, + event : animationEvent, + className : className, + isClassBased : isClassBased, + isSetClassOperation : isSetClassOperation, + before : function(allCompleteFn) { + beforeComplete = allCompleteFn; + run(before, beforeCancel, function() { + beforeComplete = noop; + allCompleteFn(); + }); + }, + after : function(allCompleteFn) { + afterComplete = allCompleteFn; + run(after, afterCancel, function() { + afterComplete = noop; + allCompleteFn(); + }); + }, + cancel : function() { + if(beforeCancel) { + forEach(beforeCancel, function(cancelFn) { + (cancelFn || noop)(true); + }); + beforeComplete(true); + } + if(afterCancel) { + forEach(afterCancel, function(cancelFn) { + (cancelFn || noop)(true); + }); + afterComplete(true); + } + } + }; + } + /** - * @ngdoc object - * @name ngAnimate.$animate + * @ngdoc service + * @name $animate * @function * * @description * The `$animate` service provides animation detection support while performing DOM operations (enter, leave and move) as well as during addClass and removeClass operations. * When any of these operations are run, the $animate service @@ -391,13 +516,12 @@ * Please visit the {@link ngAnimate `ngAnimate`} module overview page learn more about how to use animations in your application. * */ return { /** - * @ngdoc function - * @name ngAnimate.$animate#enter - * @methodOf ngAnimate.$animate + * @ngdoc method + * @name $animate#enter * @function * * @description * 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: @@ -415,13 +539,13 @@ * | 7. the .ng-enter-active and .ng-animate-active classes are added (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-animate-active ng-enter ng-enter-active" | * | 8. $animate waits for X milliseconds for the animation to complete | class="my-animation ng-animate ng-animate-active ng-enter ng-enter-active" | * | 9. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | * | 10. 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} 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 {DOMElement} element the element that will be the focus of the enter animation + * @param {DOMElement} parentElement the parent element of the element that will be the focus of the enter animation + * @param {DOMElement} afterElement the sibling element (which is the previous element) of the element that will be the focus of the enter animation * @param {function()=} doneCallback the callback function that will be called once the animation is complete */ enter : function(element, parentElement, afterElement, doneCallback) { this.enabled(false, element); $delegate.enter(element, parentElement, afterElement); @@ -430,13 +554,12 @@ performAnimation('enter', 'ng-enter', element, parentElement, afterElement, noop, doneCallback); }); }, /** - * @ngdoc function - * @name ngAnimate.$animate#leave - * @methodOf ngAnimate.$animate + * @ngdoc method + * @name $animate#leave * @function * * @description * Runs the leave animation operation and, upon completion, removes the element from the DOM. Once * the animation is started, the following CSS classes will be added for the duration of the animation: @@ -454,28 +577,26 @@ * | 7. $animate waits for X milliseconds for the animation to complete | class="my-animation ng-animate ng-animate-active ng-leave ng-leave-active" | * | 8. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | * | 9. The element is removed from the DOM | ... | * | 10. The doneCallback() callback is fired (if provided) | ... | * - * @param {jQuery/jqLite element} element the element that will be the focus of the leave animation + * @param {DOMElement} element the element that will be the focus of the leave animation * @param {function()=} doneCallback the callback function that will be called once the animation is complete */ leave : function(element, doneCallback) { cancelChildAnimations(element); this.enabled(false, element); $rootScope.$$postDigest(function() { - element = stripCommentsFromElement(element); - performAnimation('leave', 'ng-leave', element, null, null, function() { + performAnimation('leave', 'ng-leave', stripCommentsFromElement(element), null, null, function() { $delegate.leave(element); }, doneCallback); }); }, /** - * @ngdoc function - * @name ngAnimate.$animate#move - * @methodOf ngAnimate.$animate + * @ngdoc method + * @name $animate#move * @function * * @description * 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 @@ -494,13 +615,13 @@ * | 7. the .ng-move-active and .ng-animate-active classes is added (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-animate-active ng-move ng-move-active" | * | 8. $animate waits for X milliseconds for the animation to complete | class="my-animation ng-animate ng-animate-active ng-move ng-move-active" | * | 9. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | * | 10. 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} 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 {DOMElement} element the element that will be the focus of the move animation + * @param {DOMElement} parentElement the parentElement element of the element that will be the focus of the move animation + * @param {DOMElement} afterElement the sibling element (which is the previous element) of the element that will be the focus of the move animation * @param {function()=} doneCallback the callback function that will be called once the animation is complete */ move : function(element, parentElement, afterElement, doneCallback) { cancelChildAnimations(element); this.enabled(false, element); @@ -510,13 +631,12 @@ performAnimation('move', 'ng-move', element, parentElement, afterElement, noop, doneCallback); }); }, /** - * @ngdoc function - * @name ngAnimate.$animate#addClass - * @methodOf ngAnimate.$animate + * @ngdoc method + * @name $animate#addClass * * @description * Triggers a custom animation event based off the className variable and then attaches the className value to the element as a CSS class. * Unlike the other animation methods, the animate service will suffix the className value with {@type -add} in order to provide * the animate service the setup and active CSS classes in order to trigger the animation (this will be skipped if no CSS transitions @@ -530,16 +650,16 @@ * | 2. $animate runs any JavaScript-defined animations on the element | class="my-animation ng-animate" | * | 3. the .super-add class are added to the element | class="my-animation ng-animate super-add" | * | 4. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate super-add" | * | 5. $animate waits for 10ms (this performs a reflow) | class="my-animation ng-animate super-add" | * | 6. the .super, .super-add-active and .ng-animate-active classes are added (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-animate-active super super-add super-add-active" | - * | 7. $animate waits for X milliseconds for the animation to complete | class="my-animation super-add super-add-active" | + * | 7. $animate waits for X milliseconds for the animation to complete | class="my-animation super super-add super-add-active" | * | 8. The animation ends and all generated CSS classes are removed from the element | class="my-animation super" | * | 9. The super class is kept on the element | class="my-animation super" | * | 10. The doneCallback() callback is fired (if provided) | class="my-animation super" | * - * @param {jQuery/jqLite element} element the element that will be animated + * @param {DOMElement} 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); @@ -547,13 +667,12 @@ $delegate.addClass(element, className); }, doneCallback); }, /** - * @ngdoc function - * @name ngAnimate.$animate#removeClass - * @methodOf ngAnimate.$animate + * @ngdoc method + * @name $animate#removeClass * * @description * Triggers a custom animation event based off the className variable and then removes the CSS class provided by the className value * from the element. Unlike the other animation methods, the animate service will suffix the className value with {@type -remove} in * order to provide the animate service the setup and active CSS classes in order to trigger the animation (this will be skipped if @@ -572,11 +691,11 @@ * | 7. $animate waits for X milliseconds for the animation to complete | class="my-animation ng-animate ng-animate-active super-remove super-remove-active" | * | 8. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | * | 9. The doneCallback() callback is fired (if provided) | class="my-animation" | * * - * @param {jQuery/jqLite element} element the element that will be animated + * @param {DOMElement} 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); @@ -591,32 +710,31 @@ * @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 + * @param {DOMElement} 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 + * @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 + * @ngdoc method + * @name $animate#enabled * @function * * @param {boolean=} value If provided then set the animation on or off. - * @param {jQuery/jqLite element=} element If provided then the element will be used to represent the enable/disable operation + * @param {DOMElement=} element If provided then the element will be used to represent the enable/disable operation * @return {boolean} Current animation state. * * @description * Globally enables/disables animations. * @@ -652,107 +770,54 @@ 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 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; - } - - //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)) { + var runner = animationRunner(element, animationEvent, className); + if(!runner) { fireDOMOperation(); fireBeforeCallbackAsync(); fireAfterCallbackAsync(); fireDoneCallbackAsync(); return; } - var elementEvents = angular.element._data(node); + className = runner.className; + var elementEvents = angular.element._data(runner.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' || - setClassOperation; var ngAnimateState = element.data(NG_ANIMATE_STATE) || {}; - var runningAnimations = ngAnimateState.active || {}; var totalActiveAnimations = ngAnimateState.totalActive || 0; var lastAnimation = ngAnimateState.last; + //only allow animations if the currently running animation is not structural + //or if there is no animation running at all + var skipAnimations = runner.isClassBased ? + ngAnimateState.disabled || (lastAnimation && !lastAnimation.isClassBased) : + false; + //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) { + //NOTE: IE8 + IE9 should close properly (run closeAnimation()) in case an animation was found. + if (skipAnimations || animationsDisabled(element, parentElement)) { fireDOMOperation(); fireBeforeCallbackAsync(); fireAfterCallbackAsync(); closeAnimation(); return; } - 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 && (!lastAnimation || lastAnimation.classBased) : - true; - - if(allowAnimations) { - forEach(matches, function(animation) { - //add the animation to the queue to if it is allowed to be cancelled - 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({ - before : beforeFn, - after : afterFn - }); - } - }); - } - - //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 skipAnimation = false; if(totalActiveAnimations > 0) { var animationsToCancel = []; - if(!isClassBased) { + if(!runner.isClassBased) { if(animationEvent == 'leave' && runningAnimations['ng-leave']) { skipAnimation = true; } else { //cancel all animations when a structural animation takes place for(var klass in runningAnimations) { @@ -775,123 +840,81 @@ cleanup(element, className); } } if(animationsToCancel.length > 0) { - angular.forEach(animationsToCancel, function(operation) { - (operation.done || noop)(true); - cancelAnimations(operation.animations); + forEach(animationsToCancel, function(operation) { + operation.cancel(); }); } } - if(isClassBased && !setClassOperation && !skipAnimation) { + if(runner.isClassBased && !runner.isSetClassOperation && !skipAnimation) { skipAnimation = (animationEvent == 'addClass') == element.hasClass(className); //opposite of XOR } if(skipAnimation) { fireBeforeCallbackAsync(); fireAfterCallbackAsync(); fireDoneCallbackAsync(); return; } + if(animationEvent == 'leave') { + //there's no need to ever remove the listener since the element + //will be removed (destroyed) after the leave animation ends or + //is cancelled midway + element.one('$destroy', function(e) { + var element = angular.element(this); + var state = element.data(NG_ANIMATE_STATE); + if(state) { + var activeLeaveAnimation = state.active['ng-leave']; + if(activeLeaveAnimation) { + activeLeaveAnimation.cancel(); + cleanup(element, 'ng-leave'); + } + } + }); + } + //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); var localAnimationCount = globalAnimationCounter++; - lastAnimation = { - classBased : isClassBased, - event : animationEvent, - animations : animations, - done:onBeforeAnimationsComplete - }; - totalActiveAnimations++; - runningAnimations[className] = lastAnimation; + runningAnimations[className] = runner; element.data(NG_ANIMATE_STATE, { - last : lastAnimation, + last : runner, 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) { + fireBeforeCallbackAsync(); + runner.before(function(cancelled) { var data = element.data(NG_ANIMATE_STATE); cancelled = cancelled || - !data || !data.active[className] || - (isClassBased && data.active[className].event != animationEvent); + !data || !data.active[className] || + (runner.isClassBased && data.active[className].event != animationEvent); fireDOMOperation(); if(cancelled === true) { closeAnimation(); - return; + } else { + fireAfterCallbackAsync(); + runner.after(closeAnimation); } + }); - //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 currentAnimation = data.active[className]; - currentAnimation.done = closeAnimation; - 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); - }; - - //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; - } - - if(animation[phase]) { - 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(); - } - }); - - 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(); - } - } - function fireDOMCallback(animationPhase) { var eventName = '$animate:' + animationPhase; if(elementEvents && elementEvents[eventName] && elementEvents[eventName].length > 0) { - $$asyncQueueBuffer(function() { + $$asyncCallback(function() { element.triggerHandler(eventName, { event : animationEvent, className : className }); }); @@ -907,17 +930,17 @@ } function fireDoneCallbackAsync() { fireDOMCallback('close'); if(doneCallback) { - $$asyncQueueBuffer(function() { + $$asyncCallback(function() { doneCallback(); }); } } - //it is less complicated to use a flag than managing and cancelling + //it is less complicated to use a flag than managing and canceling //timeouts containing multiple callbacks. function fireDOMOperation() { if(!fireDOMOperation.hasBeenRun) { fireDOMOperation.hasBeenRun = true; domOperation(); @@ -931,14 +954,14 @@ 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) { + if(runner.isClassBased) { cleanup(element, className); } else { - $$asyncQueueBuffer(function() { + $$asyncCallback(function() { var data = element.data(NG_ANIMATE_STATE) || {}; if(localAnimationCount == data.index) { cleanup(element, className, animationEvent); } }); @@ -950,49 +973,39 @@ } } 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 && data.active) { - angular.forEach(data.active, function(operation) { - (operation.done || noop)(true); - cancelAnimations(operation.animations); - }); - } - }); + if (node) { + var nodes = angular.isFunction(node.getElementsByClassName) ? + node.getElementsByClassName(NG_ANIMATE_CLASS_NAME) : + node.querySelectorAll('.' + NG_ANIMATE_CLASS_NAME); + forEach(nodes, function(element) { + element = angular.element(element); + var data = element.data(NG_ANIMATE_STATE); + if(data && data.active) { + forEach(data.active, function(runner) { + runner.cancel(); + }); + } + }); + } } - function cancelAnimations(animations) { - var isCancelledFlag = true; - forEach(animations, function(animation) { - if(!animation.beforeComplete) { - (animation.beforeEnd || noop)(isCancelledFlag); - } - if(!animation.afterComplete) { - (animation.afterEnd || noop)(isCancelledFlag); - } - }); - } - function cleanup(element, className) { if(isMatchingElement(element, $rootElement)) { if(!rootAnimateState.disabled) { rootAnimateState.running = false; rootAnimateState.structural = false; } } 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.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); @@ -1092,21 +1105,26 @@ var closingTimer = null; var closingTimestamp = 0; var animationElementQueue = []; function animationCloseHandler(element, totalTime) { + var node = extractElementNode(element); + element = angular.element(node); + + //this item will be garbage collected by the closing + //animation timeout + animationElementQueue.push(element); + + //but it may not need to cancel out the existing timeout + //if the timestamp is less than the previous one 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); @@ -1241,19 +1259,26 @@ element.data(NG_ANIMATE_CSS_DATA_KEY, { running : formerData.running || 0, itemIndex : itemIndex, stagger : stagger, timings : timings, - closeAnimationFn : angular.noop + closeAnimationFn : 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) { + + //staggering keyframe animations work by adjusting the `animation-delay` CSS property + //on the given element, however, the delay value can only calculated after the reflow + //since by that time $animate knows how many elements are being animated. Therefore, + //until the reflow occurs the element needs to be blocked (where the keyframe animation + //is set to `none 0s`). This blocking mechanism should only be set for when a stagger + //animation is detected and when the element item index is greater than 0. + if(animationDuration > 0 && stagger.animationDelay > 0 && stagger.animationDuration === 0) { blockKeyframeAnimations(element); } return true; }