diff options
Diffstat (limited to 'libs/angularjs/angular-animate.js')
-rwxr-xr-x | libs/angularjs/angular-animate.js | 688 |
1 files changed, 394 insertions, 294 deletions
diff --git a/libs/angularjs/angular-animate.js b/libs/angularjs/angular-animate.js index d8a7855ae0..61befa2a04 100755 --- a/libs/angularjs/angular-animate.js +++ b/libs/angularjs/angular-animate.js @@ -1,5 +1,5 @@ /** - * @license AngularJS v1.2.13 + * @license AngularJS v1.2.25 * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT */ @@ -8,7 +8,7 @@ /* jshint maxlen: false */ /** - * @ngdoc overview + * @ngdoc module * @name ngAnimate * @description * @@ -16,7 +16,6 @@ * * 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> * @@ -38,12 +37,14 @@ * | {@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; @@ -51,9 +52,9 @@ * } * * .slide.ng-enter { } /* starting animations for enter */ - * .slide.ng-enter-active { } /* terminal animations for enter */ + * .slide.ng-enter.ng-enter-active { } /* terminal animations for enter */ * .slide.ng-leave { } /* starting animations for leave */ - * .slide.ng-leave-active { } /* terminal animations for leave */ + * .slide.ng-leave.ng-leave-active { } /* terminal animations for leave */ * </style> * * <!-- @@ -61,10 +62,24 @@ * 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. + * Keep in mind that, by default, if an animation is running, any child elements cannot be animated + * until the parent element's animation has completed. This blocking feature can be overridden by + * placing the `ng-animate-children` attribute on a parent container tag. + * + * ```html + * <div class="slide-animation" ng-if="on" ng-animate-children> + * <div class="fade-animation" ng-if="on"> + * <div class="explode-animation" ng-if="on"> + * ... + * </div> + * </div> + * </div> + * ``` + * + * When the `on` expression value changes and an animation is triggered then each of the elements within + * will all animate without the block being applied to child elements. * * <h2>CSS-defined Animations</h2> * The animate service will automatically apply two CSS classes to the animated element and these two CSS classes @@ -73,7 +88,7 @@ * * The following code below demonstrates how to perform animations using **CSS transitions** with Angular: * - * <pre> + * ```html * <style type="text/css"> * /* * The animate class is apart of the element and the ng-enter class @@ -101,21 +116,21 @@ * <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; /* Safari/Chrome */ * animation: enter_sequence 1s linear; /* IE10+ and Future Browsers */ * } - * @-webkit-keyframes enter_sequence { + * @-webkit-keyframes enter_sequence { * from { opacity:0; } * to { opacity:1; } * } - * @keyframes enter_sequence { + * @keyframes enter_sequence { * from { opacity:0; } * to { opacity:1; } * } @@ -124,7 +139,7 @@ * <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. * @@ -142,7 +157,7 @@ * 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 { * /* standard transition code */ * -webkit-transition: 1s linear all; @@ -163,7 +178,7 @@ * /* standard transition styles */ * 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 @@ -172,7 +187,7 @@ * * 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 @@ -186,7 +201,7 @@ * $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. * @@ -194,7 +209,7 @@ * 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() { @@ -223,7 +238,7 @@ * 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 @@ -241,8 +256,8 @@ 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. @@ -254,42 +269,37 @@ angular.module('ngAnimate', ['ng']) * 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) { + .directive('ngAnimateChildren', function() { + var NG_ANIMATE_CHILDREN = '$$ngAnimateChildren'; + return function(scope, element, attrs) { + var val = attrs.ngAnimateChildren; + if(angular.isString(val) && val.length === 0) { //empty attribute + element.data(NG_ANIMATE_CHILDREN, true); + } else { + scope.$watch(val, function(value) { + element.data(NG_ANIMATE_CHILDREN, !!value); + }); + } + }; + }) + + //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; - 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(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); }; }]) @@ -300,6 +310,7 @@ angular.module('ngAnimate', ['ng']) var ELEMENT_NODE = 1; var NG_ANIMATE_STATE = '$$ngAnimateState'; + var NG_ANIMATE_CHILDREN = '$$ngAnimateChildren'; var NG_ANIMATE_CLASS_NAME = 'ng-animate'; var rootAnimateState = {running: true}; @@ -312,6 +323,10 @@ angular.module('ngAnimate', ['ng']) } } + function prepareElement(element) { + return element && angular.element(element); + } + function stripCommentsFromElement(element) { return angular.element(extractElementNode(element)); } @@ -320,8 +335,8 @@ angular.module('ngAnimate', ['ng']) 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); @@ -345,6 +360,12 @@ angular.module('ngAnimate', ['ng']) return classNameFilter.test(className); }; + function blockElementAnimations(element) { + var data = element.data(NG_ANIMATE_STATE) || {}; + data.running = true; + element.data(NG_ANIMATE_STATE, data); + } + function lookup(name) { if (name) { var matches = [], @@ -355,9 +376,12 @@ angular.module('ngAnimate', ['ng']) //operation which performs CSS transition and keyframe //animations sniffing. This is always included for each //element animation procedure if the browser supports - //transitions and/or keyframe animations + //transitions and/or keyframe animations. The default + //animation is added to the top of the list to prevent + //any previous animations from affecting the element styling + //prior to the element being animated. if ($sniffer.transitions || $sniffer.animations) { - classes.push(''); + matches.push($injector.get(selectors[''])); } for(var i=0; i < classes.length; i++) { @@ -372,10 +396,152 @@ angular.module('ngAnimate', ['ng']) } } + 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 - * @function + * @ngdoc service + * @name $animate + * @kind 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. @@ -393,10 +559,9 @@ angular.module('ngAnimate', ['ng']) */ return { /** - * @ngdoc function - * @name ngAnimate.$animate#enter - * @methodOf ngAnimate.$animate - * @function + * @ngdoc method + * @name $animate#enter + * @kind function * * @description * Appends the element to the parentElement element that resides in the document and then runs the enter animation. Once @@ -417,13 +582,17 @@ angular.module('ngAnimate', ['ng']) * | 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); + element = angular.element(element); + parentElement = prepareElement(parentElement); + afterElement = prepareElement(afterElement); + + blockElementAnimations(element); $delegate.enter(element, parentElement, afterElement); $rootScope.$$postDigest(function() { element = stripCommentsFromElement(element); @@ -432,10 +601,9 @@ angular.module('ngAnimate', ['ng']) }, /** - * @ngdoc function - * @name ngAnimate.$animate#leave - * @methodOf ngAnimate.$animate - * @function + * @ngdoc method + * @name $animate#leave + * @kind function * * @description * Runs the leave animation operation and, upon completion, removes the element from the DOM. Once @@ -456,25 +624,24 @@ angular.module('ngAnimate', ['ng']) * | 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) { + element = angular.element(element); cancelChildAnimations(element); - this.enabled(false, element); + blockElementAnimations(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 - * @function + * @ngdoc method + * @name $animate#move + * @kind function * * @description * Fires the move DOM operation. Just before the animation starts, the animate service will either append it into the parentElement container or @@ -496,14 +663,18 @@ angular.module('ngAnimate', ['ng']) * | 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) { + element = angular.element(element); + parentElement = prepareElement(parentElement); + afterElement = prepareElement(afterElement); + cancelChildAnimations(element); - this.enabled(false, element); + blockElementAnimations(element); $delegate.move(element, parentElement, afterElement); $rootScope.$$postDigest(function() { element = stripCommentsFromElement(element); @@ -512,9 +683,8 @@ angular.module('ngAnimate', ['ng']) }, /** - * @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. @@ -532,16 +702,17 @@ angular.module('ngAnimate', ['ng']) * | 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 = angular.element(element); element = stripCommentsFromElement(element); performAnimation('addClass', className, element, null, null, function() { $delegate.addClass(element, className); @@ -549,9 +720,8 @@ angular.module('ngAnimate', ['ng']) }, /** - * @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 @@ -574,11 +744,12 @@ angular.module('ngAnimate', ['ng']) * | 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 = angular.element(element); element = stripCommentsFromElement(element); performAnimation('removeClass', className, element, null, null, function() { $delegate.removeClass(element, className); @@ -588,19 +759,19 @@ angular.module('ngAnimate', ['ng']) /** * * @ngdoc function - * @name ng.$animate#setClass - * @methodOf ng.$animate + * @name $animate#setClass * @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 its 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 = angular.element(element); element = stripCommentsFromElement(element); performAnimation('setClass', [add, remove], element, null, null, function() { $delegate.setClass(element, add, remove); @@ -608,13 +779,12 @@ angular.module('ngAnimate', ['ng']) }, /** - * @ngdoc function - * @name ngAnimate.$animate#enabled - * @methodOf ngAnimate.$animate - * @function + * @ngdoc method + * @name $animate#enabled + * @kind 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 @@ -654,52 +824,42 @@ angular.module('ngAnimate', ['ng']) */ 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(); + closeAnimation(); 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; + if (runner.isClassBased) { + skipAnimations = ngAnimateState.running || + ngAnimateState.disabled || + (lastAnimation && !lastAnimation.isClassBased); + } + //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(); @@ -707,50 +867,10 @@ angular.module('ngAnimate', ['ng']) 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 { @@ -777,41 +897,51 @@ angular.module('ngAnimate', ['ng']) } 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) { + fireDOMOperation(); 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 @@ -819,77 +949,26 @@ angular.module('ngAnimate', ['ng']) //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; - } - - //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(); + } else { + fireAfterCallbackAsync(); + runner.after(closeAnimation); } - } + }); 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 @@ -909,13 +988,13 @@ angular.module('ngAnimate', ['ng']) 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) { @@ -933,10 +1012,10 @@ angular.module('ngAnimate', ['ng']) 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 && 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); @@ -952,28 +1031,20 @@ angular.module('ngAnimate', ['ng']) 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); - }); - } - }); - } - - function cancelAnimations(animations) { - var isCancelledFlag = true; - forEach(animations, function(animation) { - if(!animation.beforeComplete) { - (animation.beforeEnd || noop)(isCancelledFlag); - } - if(!animation.afterComplete) { - (animation.afterEnd || noop)(isCancelledFlag); - } - }); + 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 cleanup(element, className) { @@ -986,11 +1057,9 @@ angular.module('ngAnimate', ['ng']) 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) { @@ -1001,30 +1070,49 @@ angular.module('ngAnimate', ['ng']) } function animationsDisabled(element, parentElement) { - if (rootAnimateState.disabled) return true; + if (rootAnimateState.disabled) { + return true; + } - if(isMatchingElement(element, $rootElement)) { - return rootAnimateState.disabled || rootAnimateState.running; + if (isMatchingElement(element, $rootElement)) { + return rootAnimateState.running; } + var allowChildAnimations, parentRunningAnimation, hasParent; do { //the element did not reach the root element which means that it //is not apart of the DOM. Therefore there is no reason to do //any animations on it - if(parentElement.length === 0) break; + 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 || state.totalActive > 0); - if(isRoot || result) { - return result; + var state = isRoot ? rootAnimateState : (parentElement.data(NG_ANIMATE_STATE) || {}); + if (state.disabled) { + return true; } - if(isRoot) return true; + //no matter what, for an animation to work it must reach the root element + //this implies that the element is attached to the DOM when the animation is run + if (isRoot) { + hasParent = true; + } + + //once a flag is found that is strictly false then everything before + //it will be discarded and all child animations will be restricted + if (allowChildAnimations !== false) { + var animateChildrenFlag = parentElement.data(NG_ANIMATE_CHILDREN); + if(angular.isDefined(animateChildrenFlag)) { + allowChildAnimations = animateChildrenFlag; + } + } + + parentRunningAnimation = parentRunningAnimation || + state.running || + (state.last && !state.last.isClassBased); } while(parentElement = parentElement.parent()); - return true; + return !hasParent || (!allowChildAnimations && parentRunningAnimation); } }]); @@ -1094,17 +1182,22 @@ angular.module('ngAnimate', ['ng']) var closingTimestamp = 0; var animationElementQueue = []; function animationCloseHandler(element, totalTime) { - var futureTimestamp = Date.now() + (totalTime * 1000); + 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; 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); @@ -1197,7 +1290,7 @@ angular.module('ngAnimate', ['ng']) parentElement.data(NG_ANIMATE_PARENT_KEY, ++parentCounter); parentID = parentCounter; } - return parentID + '-' + extractElementNode(element).className; + return parentID + '-' + extractElementNode(element).getAttribute('class'); } function animateSetup(animationEvent, element, className, calculationDecorator) { @@ -1243,7 +1336,7 @@ angular.module('ngAnimate', ['ng']) itemIndex : itemIndex, stagger : stagger, timings : timings, - closeAnimationFn : angular.noop + closeAnimationFn : noop }); //temporarily disable the transition so that the enter styles @@ -1252,7 +1345,14 @@ angular.module('ngAnimate', ['ng']) 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); } @@ -1295,7 +1395,7 @@ angular.module('ngAnimate', ['ng']) 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) { + if(node.getAttribute('class').indexOf(className) == -1 || !elementData) { activeAnimationComplete(); return; } @@ -1346,7 +1446,7 @@ angular.module('ngAnimate', ['ng']) //the jqLite object, so we're safe to use a single variable to house //the styles since there is always only one element being animated var oldStyle = node.getAttribute('style') || ''; - node.setAttribute('style', oldStyle + ' ' + style); + node.setAttribute('style', oldStyle + '; ' + style); } element.on(css3AnimationEvents, onAnimationProgress); |