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

- old
+ new

@@ -1,53 +1,54 @@ /** - * @license AngularJS v1.2.0-rc.3 + * @license AngularJS v1.2.0 * (c) 2010-2012 Google, Inc. http://angularjs.org * License: MIT */ (function(window, angular, undefined) {'use strict'; +/* jshint maxlen: false */ + /** * @ngdoc overview * @name ngAnimate * @description * * # ngAnimate * - * `ngAnimate` is an optional module that provides CSS and JavaScript animation hooks. + * 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 * * To see animations in action, all that is required is to define the appropriate CSS classes - * or to register a JavaScript animation via the $animation service. The directives that support animation automatically are: - * `ngRepeat`, `ngInclude`, `ngSwitch`, `ngShow`, `ngHide` and `ngView`. Custom directives can take advantage of animation + * or to register a JavaScript animation via the myModule.animation() function. The directives that support animation automatically are: + * `ngRepeat`, `ngInclude`, `ngIf`, `ngSwitch`, `ngShow`, `ngHide`, `ngView` and `ngClass`. Custom directives can take advantage of animation * by using the `$animate` service. * * Below is a more detailed breakdown of the supported animation events provided by pre-existing ng directives: * * | Directive | Supported Animations | * |---------------------------------------------------------- |----------------------------------------------------| - * | {@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:ngClass#animations ngClass} | add and remove | - * | {@link ng.directive:ngShow#animations ngShow & ngHide} | add and remove (the ng-hide class value) | + * | {@link ng.directive:ngRepeat#usage_animations ngRepeat} | enter, leave and move | + * | {@link ngRoute.directive:ngView#usage_animations ngView} | enter and leave | + * | {@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) | * * 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> * <style type="text/css"> - * .slide.ng-enter > div, - * .slide.ng-leave > div { + * .slide.ng-enter, .slide.ng-leave { * -webkit-transition:0.5s linear all; - * -moz-transition:0.5s linear all; - * -o-transition:0.5s linear all; * transition:0.5s linear all; * } * * .slide.ng-enter { } /&#42; starting animations for enter &#42;/ * .slide.ng-enter-active { } /&#42; terminal animations for enter &#42;/ @@ -78,13 +79,11 @@ * 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;/ * .reveal-animation.ng-enter { * -webkit-transition: 1s linear all; /&#42; Safari/Chrome &#42;/ - * -moz-transition: 1s linear all; /&#42; Firefox &#42;/ - * -o-transition: 1s linear all; /&#42; Opera &#42;/ - * transition: 1s linear all; /&#42; IE10+ and Future Browsers &#42;/ + * transition: 1s linear all; /&#42; All other modern browsers and IE10+ &#42;/ * * /&#42; The animation preparation code &#42;/ * opacity: 0; * } * @@ -108,26 +107,16 @@ * * <pre> * <style type="text/css"> * .reveal-animation.ng-enter { * -webkit-animation: enter_sequence 1s linear; /&#42; Safari/Chrome &#42;/ - * -moz-animation: enter_sequence 1s linear; /&#42; Firefox &#42;/ - * -o-animation: enter_sequence 1s linear; /&#42; Opera &#42;/ * animation: enter_sequence 1s linear; /&#42; IE10+ and Future Browsers &#42;/ * } * &#64-webkit-keyframes enter_sequence { * from { opacity:0; } * to { opacity:1; } * } - * &#64-moz-keyframes enter_sequence { - * from { opacity:0; } - * to { opacity:1; } - * } - * &#64-o-keyframes enter_sequence { - * from { opacity:0; } - * to { opacity:1; } - * } * &#64keyframes enter_sequence { * from { opacity:0; } * to { opacity:1; } * } * </style> @@ -144,41 +133,105 @@ * detect the CSS code to determine when the animation ends. Once the animation is over then both CSS classes will be * removed from the DOM. If a browser does not support CSS transitions or CSS animations then the animation will start and end * immediately resulting in a DOM element that is at its final state. This final state is when the DOM element * has no CSS transition/animation classes applied to it. * + * <h3>CSS Staggering Animations</h3> + * A Staggering animation is a collection of animations that are issued with a slight delay in between each successive operation resulting in a + * 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> + * .my-animation.ng-enter { + * /&#42; standard transition code &#42;/ + * -webkit-transition: 1s linear all; + * transition: 1s linear all; + * opacity:0; + * } + * .my-animation.ng-enter-stagger { + * /&#42; this will have a 100ms delay between each successive leave animation &#42;/ + * -webkit-transition-delay: 0.1s; + * transition-delay: 0.1s; + * + * /&#42; in case the stagger doesn't work then these two values + * must be set to 0 to avoid an accidental CSS inheritance &#42;/ + * -webkit-transition-duration: 0s; + * transition-duration: 0s; + * } + * .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 defiend). 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> + * 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 + * $animate.leave(kids[3]); //stagger index=3 + * $animate.leave(kids[4]); //stagger index=4 + * + * $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> * //!annotate="YourApp" Your AngularJS Module|Replace this or ngModule with the module that you used to define your application. * var ngModule = angular.module('YourApp', []); * ngModule.animation('.my-crazy-animation', function() { * return { * enter: function(element, done) { - * //run the animation - * //!annotate Cancel Animation|This function (if provided) will perform the cancellation of the animation when another is triggered - * return function(element, done) { - * //cancel the animation + * //run the animation here and call done when the animation is complete + * return function(cancelled) { + * //this (optional) function will be called when the animation + * //completes or when the animation is cancelled (the cancelled + * //flag will be set to true if cancelled). * } * } * leave: function(element, done) { }, * move: function(element, done) { }, - * show: function(element, done) { }, - * hide: function(element, done) { }, + * + * //animation that can be triggered before the class is added + * beforeAddClass: function(element, className, done) { }, + * + * //animation that can be triggered after the class is added * addClass: function(element, className, done) { }, - * removeClass: function(element, className, done) { }, + * + * //animation that can be triggered before the class is removed + * beforeRemoveClass: function(element, className, done) { }, + * + * //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 - * be executed. It should be also noted that only simple class selectors are allowed. + * be executed. It should be also noted that only simple, single class selectors are allowed (compound class selectors are not supported). * * Within a JavaScript animation, an object containing various event callback animation functions is expected to be returned. * As explained above, these callbacks are triggered based on the animation event. Therefore if an enter animation is run, * and the JavaScript animation is found, then the enter callback will handle that animation (in addition to the CSS keyframe animation * or transition code that is defined via a stylesheet). @@ -190,13 +243,13 @@ /** * @ngdoc object * @name ngAnimate.$animateProvider * @description * - * The `$AnimationProvider` allows developers to register and access custom JavaScript animations directly inside - * of a module. When an animation is triggered, the $animate service will query the $animation function to find any - * animations that match the provided name value. + * 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. * * 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. * @@ -204,18 +257,25 @@ .config(['$provide', '$animateProvider', function($provide, $animateProvider) { var noop = angular.noop; var forEach = angular.forEach; var selectors = $animateProvider.$$selectors; + var ELEMENT_NODE = 1; 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) { - + var rootAnimateState = {running: true}; + + $provide.decorator('$animate', ['$delegate', '$injector', '$sniffer', '$rootElement', '$timeout', '$rootScope', '$document', + function($delegate, $injector, $sniffer, $rootElement, $timeout, $rootScope, $document) { + $rootElement.data(NG_ANIMATE_STATE, rootAnimateState); + // disable animations during bootstrap, but once we bootstrapped, enable animations + $rootScope.$$postDigest(function() { + rootAnimateState.running = false; + }); + function lookup(name) { if (name) { var matches = [], flagMap = {}, classes = name.substr(1).split('.'); @@ -242,16 +302,15 @@ } /** * @ngdoc object * @name ngAnimate.$animate - * @requires $timeout, $sniffer, $rootElement * @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 + * 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 * will examine any JavaScript-defined animations (which are defined by using the $animateProvider provider object) * as well as any CSS-defined animations against the CSS classes present on the element once the DOM operation is run. * * The `$animate` service is used behind the scenes with pre-existing directives and animation with these directives * will work out of the box without any extra configuration. @@ -267,39 +326,38 @@ * @name ngAnimate.$animate#enter * @methodOf ngAnimate.$animate * @function * * @description - * Appends the element to the parent element that resides in the document and then runs the enter animation. Once + * 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: * * Below is a breakdown of each step that occurs during enter animation: * - * | Animation Step | What the element class attribute looks like | - * |----------------------------------------------------------------------------------------------|-----------------------------------------------| - * | 1. $animate.enter(...) is called | class="my-animation" | - * | 2. element is inserted into the parent element or beside the after element | class="my-animation" | - * | 3. $animate runs any JavaScript-defined animations on the element | class="my-animation" | - * | 4. the .ng-enter class is added to the element | class="my-animation ng-enter" | - * | 5. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-enter" | - * | 6. the .ng-enter-active class is added (this triggers the CSS transition/animation) | class="my-animation ng-enter ng-enter-active" | - * | 7. $animate waits for X milliseconds for the animation to complete | class="my-animation ng-enter ng-enter-active" | - * | 8. The animation ends and both CSS classes are removed from the element | class="my-animation" | - * | 9. The done() callback is fired (if provided) | class="my-animation" | + * | Animation Step | What the element class attribute looks like | + * |----------------------------------------------------------------------------------------------|---------------------------------------------| + * | 1. $animate.enter(...) is called | class="my-animation" | + * | 2. element is inserted into the parentElement element or beside the afterElement element | class="my-animation" | + * | 3. $animate runs any JavaScript-defined animations on the element | class="my-animation ng-animate" | + * | 4. the .ng-enter class is added to the element | class="my-animation ng-animate ng-enter" | + * | 5. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate ng-enter" | + * | 6. $animate waits for 10ms (this performs a reflow) | class="my-animation ng-animate ng-enter" | + * | 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} 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 + * @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 {function()=} doneCallback the callback function that will be called once the animation is complete */ - enter : function(element, parent, after, done) { + enter : function(element, parentElement, afterElement, doneCallback) { this.enabled(false, element); - $delegate.enter(element, parent, after); + $delegate.enter(element, parentElement, afterElement); $rootScope.$$postDigest(function() { - performAnimation('enter', 'ng-enter', element, parent, after, function() { - done && $timeout(done, 0, false); - }); + performAnimation('enter', 'ng-enter', element, parentElement, afterElement, noop, doneCallback); }); }, /** * @ngdoc function @@ -311,73 +369,73 @@ * 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: * * Below is a breakdown of each step that occurs during enter animation: * - * | Animation Step | What the element class attribute looks like | - * |----------------------------------------------------------------------------------------------|----------------------------------------------| - * | 1. $animate.leave(...) is called | class="my-animation" | - * | 2. $animate runs any JavaScript-defined animations on the element | class="my-animation" | - * | 3. the .ng-leave class is added to the element | class="my-animation ng-leave" | - * | 4. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-leave" | - * | 5. the .ng-leave-active class is added (this triggers the CSS transition/animation) | class="my-animation ng-leave ng-leave-active | - * | 6. $animate waits for X milliseconds for the animation to complete | class="my-animation ng-leave ng-leave-active | - * | 7. The animation ends and both CSS classes are removed from the element | class="my-animation" | - * | 8. The element is removed from the DOM | ... | - * | 9. The done() callback is fired (if provided) | ... | + * | Animation Step | What the element class attribute looks like | + * |----------------------------------------------------------------------------------------------|---------------------------------------------| + * | 1. $animate.leave(...) is called | class="my-animation" | + * | 2. $animate runs any JavaScript-defined animations on the element | class="my-animation ng-animate" | + * | 3. the .ng-leave class is added to the element | class="my-animation ng-animate ng-leave" | + * | 4. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate ng-leave" | + * | 5. $animate waits for 10ms (this performs a reflow) | class="my-animation ng-animate ng-leave" | + * | 6. the .ng-leave-active and .ng-animate-active classes is added (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-animate-active ng-leave ng-leave-active" | + * | 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 {function()=} done callback function that will be called once the animation is complete + * @param {function()=} doneCallback the callback function that will be called once the animation is complete */ - leave : function(element, done) { + leave : function(element, doneCallback) { cancelChildAnimations(element); this.enabled(false, element); $rootScope.$$postDigest(function() { performAnimation('leave', 'ng-leave', element, null, null, function() { - $delegate.leave(element, done); - }); + $delegate.leave(element); + }, doneCallback); }); }, /** * @ngdoc function * @name ngAnimate.$animate#move * @methodOf ngAnimate.$animate * @function * * @description - * Fires the move DOM operation. Just before the animation starts, the animate service will either append it into the parent container or - * add the element directly after the after element if present. Then the move animation will be run. Once + * 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 * the animation is started, the following CSS classes will be added for the duration of the animation: * * Below is a breakdown of each step that occurs during move animation: * * | Animation Step | What the element class attribute looks like | * |----------------------------------------------------------------------------------------------|---------------------------------------------| * | 1. $animate.move(...) is called | class="my-animation" | - * | 2. element is moved into the parent element or beside the after element | class="my-animation" | - * | 3. $animate runs any JavaScript-defined animations on the element | class="my-animation" | - * | 4. the .ng-move class is added to the element | class="my-animation ng-move" | - * | 5. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-move" | - * | 6. the .ng-move-active class is added (this triggers the CSS transition/animation) | class="my-animation ng-move ng-move-active" | - * | 7. $animate waits for X milliseconds for the animation to complete | class="my-animation ng-move ng-move-active" | - * | 8. The animation ends and both CSS classes are removed from the element | class="my-animation" | - * | 9. The done() callback is fired (if provided) | class="my-animation" | + * | 2. element is moved into the parentElement element or beside the afterElement element | class="my-animation" | + * | 3. $animate runs any JavaScript-defined animations on the element | class="my-animation ng-animate" | + * | 4. the .ng-move class is added to the element | class="my-animation ng-animate ng-move" | + * | 5. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate ng-move" | + * | 6. $animate waits for 10ms (this performs a reflow) | class="my-animation ng-animate ng-move" | + * | 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} 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 + * @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 {function()=} doneCallback the callback function that will be called once the animation is complete */ - move : function(element, parent, after, done) { + move : function(element, parentElement, afterElement, doneCallback) { cancelChildAnimations(element); this.enabled(false, element); - $delegate.move(element, parent, after); + $delegate.move(element, parentElement, afterElement); $rootScope.$$postDigest(function() { - performAnimation('move', 'ng-move', element, null, null, function() { - done && $timeout(done, 0, false); - }); + performAnimation('move', 'ng-move', element, parentElement, afterElement, noop, doneCallback); }); }, /** * @ngdoc function @@ -386,34 +444,35 @@ * * @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 - * or keyframes are defined on the -add CSS class). + * or keyframes are defined on the -add or base CSS class). * * Below is a breakdown of each step that occurs during addClass animation: * * | Animation Step | What the element class attribute looks like | * |------------------------------------------------------------------------------------------------|---------------------------------------------| - * | 1. $animate.addClass(element, 'super') is called | class="" | - * | 2. $animate runs any JavaScript-defined animations on the element | class="" | - * | 3. the .super-add class is added to the element | class="super-add" | - * | 4. $animate scans the element styles to get the CSS transition/animation duration and delay | class="super-add" | - * | 5. the .super-add-active class is added (this triggers the CSS transition/animation) | class="super-add super-add-active" | - * | 6. $animate waits for X milliseconds for the animation to complete | class="super-add super-add-active" | - * | 7. The animation ends and both CSS classes are removed from the element | class="" | - * | 8. The super class is added to the element | class="super" | - * | 9. The done() callback is fired (if provided) | class="super" | + * | 1. $animate.addClass(element, 'super') is called | class="my-animation" | + * | 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" | + * | 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 {string} className the CSS class that will be animated and then attached to the element - * @param {function()=} done callback function that will be called once the animation is complete + * @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, done) { + addClass : function(element, className, doneCallback) { performAnimation('addClass', className, element, null, null, function() { - $delegate.addClass(element, className, done); - }); + $delegate.addClass(element, className); + }, doneCallback); }, /** * @ngdoc function * @name ngAnimate.$animate#removeClass @@ -421,33 +480,35 @@ * * @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 - * no CSS transitions or keyframes are defined on the -remove CSS class). + * no CSS transitions or keyframes are defined on the -remove or base CSS classes). * * Below is a breakdown of each step that occurs during removeClass animation: * * | Animation Step | What the element class attribute looks like | - * |-----------------------------------------------------------------------------------------------|-------------------------------------------------| - * | 1. $animate.removeClass(element, 'super') is called | class="super" | - * | 2. $animate runs any JavaScript-defined animations on the element | class="super" | - * | 3. the .super-remove class is added to the element | class="super super-remove" | - * | 4. $animate scans the element styles to get the CSS transition/animation duration and delay | class="super super-remove" | - * | 5. the .super-remove-active class is added (this triggers the CSS transition/animation) | class="super super-remove super-remove-active" | - * | 6. $animate waits for X milliseconds for the animation to complete | class="super super-remove super-remove-active" | - * | 7. The animation ends and both CSS all three classes are removed from the element | class="" | - * | 8. The done() callback is fired (if provided) | class="" | + * |-----------------------------------------------------------------------------------------------|---------------------------------------------| + * | 1. $animate.removeClass(element, 'super') is called | class="my-animation super" | + * | 2. $animate runs any JavaScript-defined animations on the element | class="my-animation super ng-animate" | + * | 3. the .super-remove class are added to the element | class="my-animation super ng-animate super-remove"| + * | 4. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation super ng-animate super-remove" | + * | 5. $animate waits for 10ms (this performs a reflow) | class="my-animation super ng-animate super-remove" | + * | 6. the .super-remove-active and .ng-animate-active classes are added and .super is removed (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-animate-active super-remove super-remove-active" | + * | 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 {string} className the CSS class that will be animated and then removed from the element - * @param {function()=} done callback function that will be called once the animation is complete + * @param {function()=} doneCallback the callback function that will be called once the animation is complete */ - removeClass : function(element, className, done) { + removeClass : function(element, className, doneCallback) { performAnimation('removeClass', className, element, null, null, function() { - $delegate.removeClass(element, className, done); - }); + $delegate.removeClass(element, className); + }, doneCallback); }, /** * @ngdoc function * @name ngAnimate.$animate#enabled @@ -464,135 +525,214 @@ enabled : function(value, element) { switch(arguments.length) { case 2: if(value) { cleanup(element); - } - else { + } else { var data = element.data(NG_ANIMATE_STATE) || {}; - data.structural = true; - data.running = true; + data.disabled = true; element.data(NG_ANIMATE_STATE, data); } break; case 1: - rootAnimateState.running = !value; + rootAnimateState.disabled = !value; break; default: - value = !rootAnimateState.running + value = !rootAnimateState.disabled; break; } return !!value; } }; /* all animations call this shared animation triggering function internally. - The event variable refers to the JavaScript animation event that will be triggered + The animationEvent variable refers to the JavaScript animation event that will be triggered and the className value is the name of the animation that will be applied within the - CSS code. Element, parent and after are provided DOM elements for the animation + 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(event, className, element, parent, after, onComplete) { + function performAnimation(animationEvent, className, element, parentElement, afterElement, domOperation, doneCallback) { var classes = (element.attr('class') || '') + ' ' + className; - var animationLookup = (' ' + classes).replace(/\s+/g,'.'), - animations = []; - forEach(lookup(animationLookup), function(animation, index) { - animations.push({ - start : animation[event] - }); - }); - - if (!parent) { - parent = after ? after.parent() : element.parent(); + var animationLookup = (' ' + classes).replace(/\s+/g,'.'); + if (!parentElement) { + parentElement = afterElement ? afterElement.parent() : element.parent(); } - 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 || animations.length == 0) { - done(); + var matches = lookup(animationLookup); + var isClassBased = animationEvent == 'addClass' || animationEvent == 'removeClass'; + var ngAnimateState = element.data(NG_ANIMATE_STATE) || {}; + + //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) { + domOperation(); + closeAnimation(); return; } - var ngAnimateState = element.data(NG_ANIMATE_STATE) || {}; + var animations = []; + //only add animations if the currently running animation is not structural + //or if there is no animation running at all + if(!ngAnimateState.running || !(isClassBased && ngAnimateState.structural)) { + 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]; - var isClassBased = event == 'addClass' || event == 'removeClass'; - if(ngAnimateState.running) { - if(isClassBased && ngAnimateState.structural) { - onComplete && onComplete(); - return; - } + //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) { + domOperation(); + fireDoneCallbackAsync(); + return; + } + + if(ngAnimateState.running) { //if an animation is currently running on the element then lets take the steps //to cancel that animation and fire any required callbacks - $timeout.cancel(ngAnimateState.flagTimer); + $timeout.cancel(ngAnimateState.closeAnimationTimeout); + cleanup(element); cancelAnimations(ngAnimateState.animations); - (ngAnimateState.done || noop)(); + (ngAnimateState.done || noop)(true); } + //There is no point in perform a class-based animation if the element already contains + //(on addClass) or doesn't contain (on removeClass) the className being animated. + //The reason why this is being called after the previous animations are cancelled + //is so that the CSS classes present on the element can be properly examined. + if((animationEvent == 'addClass' && element.hasClass(className)) || + (animationEvent == 'removeClass' && !element.hasClass(className))) { + domOperation(); + fireDoneCallbackAsync(); + return; + } + + //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); + element.data(NG_ANIMATE_STATE, { running:true, structural:!isClassBased, animations:animations, - done:done + done:onBeforeAnimationsComplete }); - //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); + //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); - forEach(animations, function(animation, index) { - var fn = function() { - progress(index); - }; + function onBeforeAnimationsComplete(cancelled) { + domOperation(); + if(cancelled === true) { + closeAnimation(); + return; + } - if(animation.start) { - animation.endFn = isClassBased ? - animation.start(element, className, fn) : - animation.start(element, fn); - } else { - fn(); + //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 data = element.data(NG_ANIMATE_STATE); + if(data) { + data.done = closeAnimation; + element.data(NG_ANIMATE_STATE, data); } - }); + invokeRegisteredAnimationFns(animations, 'after', closeAnimation); + } - function progress(index) { - animations[index].done = true; - (animations[index].endFn || noop)(); - for(var i=0;i<animations.length;i++) { - if(!animations[i].done) return; + function invokeRegisteredAnimationFns(animations, phase, allAnimationFnsComplete) { + 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]) { + 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(); } - done(); } - function done() { - if(!done.hasBeenRun) { - done.hasBeenRun = true; + function fireDoneCallbackAsync() { + doneCallback && $timeout(doneCallback, 0, false); + } + + function closeAnimation() { + if(!closeAnimation.hasBeenRun) { + closeAnimation.hasBeenRun = true; 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() { + data.closeAnimationTimeout = $timeout(function() { cleanup(element); }, 0, false); element.data(NG_ANIMATE_STATE, data); } } - (onComplete || noop)(); + fireDoneCallbackAsync(); } } } function cancelChildAnimations(element) { - angular.forEach(element[0].querySelectorAll('.' + NG_ANIMATE_CLASS_NAME), function(element) { + var node = element[0]; + if(node.nodeType != ELEMENT_NODE) { + return; + } + + forEach(node.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); @@ -601,198 +741,329 @@ } function cancelAnimations(animations) { var isCancelledFlag = true; forEach(animations, function(animation) { - (animation.endFn || noop)(isCancelledFlag); + if(!animations['beforeComplete']) { + (animation.beforeEnd || noop)(isCancelledFlag); + } + if(!animations['afterComplete']) { + (animation.afterEnd || noop)(isCancelledFlag); + } }); } function cleanup(element) { - element.removeClass(NG_ANIMATE_CLASS_NAME); - element.removeData(NG_ANIMATE_STATE); + if(element[0] == $rootElement[0]) { + if(!rootAnimateState.disabled) { + rootAnimateState.running = false; + rootAnimateState.structural = false; + } + } else { + element.removeClass(NG_ANIMATE_CLASS_NAME); + element.removeData(NG_ANIMATE_STATE); + } } + + function animationsDisabled(element, parentElement) { + if (rootAnimateState.disabled) return true; + + if(element[0] == $rootElement[0]) { + return rootAnimateState.disabled || rootAnimateState.running; + } + + 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; + + var isRoot = parentElement[0] == $rootElement[0]; + var state = isRoot ? rootAnimateState : parentElement.data(NG_ANIMATE_STATE); + var result = state && (!!state.disabled || !!state.running); + if(isRoot || result) { + return result; + } + + if(isRoot) return true; + } + while(parentElement = parentElement.parent()); + + return true; + } }]); $animateProvider.register('', ['$window', '$sniffer', '$timeout', function($window, $sniffer, $timeout) { - var forEach = angular.forEach; - // Detect proper transitionend/animationend event names. - var transitionProp, transitionendEvent, animationProp, animationendEvent; + var CSS_PREFIX = '', TRANSITION_PROP, TRANSITIONEND_EVENT, ANIMATION_PROP, ANIMATIONEND_EVENT; // If unprefixed events are not supported but webkit-prefixed are, use the latter. // Otherwise, just use W3C names, browsers not supporting them at all will just ignore them. // 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'; + CSS_PREFIX = '-webkit-'; + TRANSITION_PROP = 'WebkitTransition'; + TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend'; } else { - transitionProp = 'transition'; - transitionendEvent = 'transitionend'; + TRANSITION_PROP = 'transition'; + TRANSITIONEND_EVENT = 'transitionend'; } if (window.onanimationend === undefined && window.onwebkitanimationend !== undefined) { - animationProp = 'WebkitAnimation'; - animationendEvent = 'webkitAnimationEnd animationend'; + CSS_PREFIX = '-webkit-'; + ANIMATION_PROP = 'WebkitAnimation'; + ANIMATIONEND_EVENT = 'webkitAnimationEnd animationend'; } else { - animationProp = 'animation'; - animationendEvent = 'animationend'; + ANIMATION_PROP = 'animation'; + ANIMATIONEND_EVENT = 'animationend'; } - var durationKey = 'Duration', - propertyKey = 'Property', - delayKey = 'Delay', - animationIterationCountKey = 'IterationCount', - ELEMENT_NODE = 1; + var DURATION_KEY = 'Duration'; + var PROPERTY_KEY = 'Property'; + var DELAY_KEY = 'Delay'; + var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount'; + var NG_ANIMATE_PARENT_KEY = '$$ngAnimateKey'; + var NG_ANIMATE_CSS_DATA_KEY = '$$ngAnimateCSS3Data'; + var NG_ANIMATE_FALLBACK_CLASS_NAME = 'ng-animate-start'; + var NG_ANIMATE_FALLBACK_ACTIVE_CLASS_NAME = 'ng-animate-active'; - 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) { + forEach(animationReflowQueue, function(fn) { fn(); }); animationReflowQueue = []; animationTimer = null; lookupCache = {}; - }, 10, false); + }, 10, false); } - function getElementAnimationDetails(element, cacheKey, onlyCheckTransition) { - var data = lookupCache[cacheKey]; + function applyStyle(node, style) { + var oldStyle = node.getAttribute('style') || ''; + var newStyle = (oldStyle.length > 0 ? '; ' : '') + style; + node.setAttribute('style', newStyle); + return oldStyle; + } + + function getElementAnimationDetails(element, cacheKey) { + var data = cacheKey ? lookupCache[cacheKey] : null; if(!data) { - var transitionDuration = 0, transitionDelay = 0, - animationDuration = 0, animationDelay = 0; + var transitionDuration = 0; + var transitionDelay = 0; + var animationDuration = 0; + var animationDelay = 0; + var transitionDelayStyle; + var animationDelayStyle; + var transitionDurationStyle; + var transitionPropertyStyle; //we want all the styles defined before and after forEach(element, function(element) { if (element.nodeType == ELEMENT_NODE) { var elementStyles = $window.getComputedStyle(element) || {}; - transitionDuration = Math.max(parseMaxTime(elementStyles[transitionProp + durationKey]), transitionDuration); + transitionDurationStyle = elementStyles[TRANSITION_PROP + DURATION_KEY]; - if(!onlyCheckTransition) { - transitionDelay = Math.max(parseMaxTime(elementStyles[transitionProp + delayKey]), transitionDelay); + transitionDuration = Math.max(parseMaxTime(transitionDurationStyle), transitionDuration); - animationDelay = Math.max(parseMaxTime(elementStyles[animationProp + delayKey]), animationDelay); + transitionPropertyStyle = elementStyles[TRANSITION_PROP + PROPERTY_KEY]; - var aDuration = parseMaxTime(elementStyles[animationProp + durationKey]); + transitionDelayStyle = elementStyles[TRANSITION_PROP + DELAY_KEY]; - if(aDuration > 0) { - aDuration *= parseInt(elementStyles[animationProp + animationIterationCountKey]) || 1; - } + transitionDelay = Math.max(parseMaxTime(transitionDelayStyle), transitionDelay); - animationDuration = Math.max(aDuration, animationDuration); + animationDelayStyle = elementStyles[ANIMATION_PROP + DELAY_KEY]; + + animationDelay = Math.max(parseMaxTime(animationDelayStyle), animationDelay); + + var aDuration = parseMaxTime(elementStyles[ANIMATION_PROP + DURATION_KEY]); + + if(aDuration > 0) { + aDuration *= parseInt(elementStyles[ANIMATION_PROP + ANIMATION_ITERATION_COUNT_KEY], 10) || 1; } + + animationDuration = Math.max(aDuration, animationDuration); } }); data = { - transitionDelay : transitionDelay, - animationDelay : animationDelay, - transitionDuration : transitionDuration, - animationDuration : animationDuration + total : 0, + transitionPropertyStyle: transitionPropertyStyle, + transitionDurationStyle: transitionDurationStyle, + transitionDelayStyle: transitionDelayStyle, + transitionDelay: transitionDelay, + transitionDuration: transitionDuration, + animationDelayStyle: animationDelayStyle, + animationDelay: animationDelay, + animationDuration: animationDuration }; - lookupCache[cacheKey] = data; + if(cacheKey) { + lookupCache[cacheKey] = data; + } } return data; } function parseMaxTime(str) { - var total = 0, values = angular.isString(str) ? str.split(/\s*,\s*/) : []; + var maxValue = 0; + var values = angular.isString(str) ? + str.split(/\s*,\s*/) : + []; forEach(values, function(value) { - total = Math.max(parseFloat(value) || 0, total); + maxValue = Math.max(parseFloat(value) || 0, maxValue); }); - return total; + return maxValue; } function getCacheKey(element) { - var parent = element.parent(); - var parentID = parent.data(NG_ANIMATE_PARENT_KEY); + var parentElement = element.parent(); + var parentID = parentElement.data(NG_ANIMATE_PARENT_KEY); if(!parentID) { - parent.data(NG_ANIMATE_PARENT_KEY, ++parentCounter); + parentElement.data(NG_ANIMATE_PARENT_KEY, ++parentCounter); parentID = parentCounter; } return parentID + '-' + element[0].className; } - function animate(element, className, done) { - + function animateSetup(element, className) { var cacheKey = getCacheKey(element); - if(getElementAnimationDetails(element, cacheKey, true).transitionDuration > 0) { + var eventCacheKey = cacheKey + ' ' + className; + var stagger = {}; + var ii = lookupCache[eventCacheKey] ? ++lookupCache[eventCacheKey].total : 0; - done(); - return; + if(ii > 0) { + var staggerClassName = className + '-stagger'; + var staggerCacheKey = cacheKey + ' ' + staggerClassName; + var applyClasses = !lookupCache[staggerCacheKey]; + + applyClasses && element.addClass(staggerClassName); + + stagger = getElementAnimationDetails(element, staggerCacheKey); + + applyClasses && element.removeClass(staggerClassName); } element.addClass(className); - var timings = getElementAnimationDetails(element, cacheKey + ' ' + className); + var timings = getElementAnimationDetails(element, eventCacheKey); /* there is no point in performing a reflow if the animation timeout is empty (this would cause a flicker bug normally in the page. There is also no point in performing an animation that only has a delay and no duration */ 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]; + if(maxDuration === 0) { + element.removeClass(className); + return false; + } - //temporarily disable the transition so that the enter styles - //don't animate twice (this is here to avoid a bug in Chrome/FF). - if(timings.transitionDuration > 0) { - node.style[transitionProp + propertyKey] = 'none'; - } + var 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). + var activeClassName = ''; + if(timings.transitionDuration > 0) { + element.addClass(NG_ANIMATE_FALLBACK_CLASS_NAME); + activeClassName += NG_ANIMATE_FALLBACK_ACTIVE_CLASS_NAME + ' '; + node.style[TRANSITION_PROP + PROPERTY_KEY] = 'none'; + } - var activeClassName = ''; - forEach(className.split(' '), function(klass, i) { - activeClassName += (i > 0 ? ' ' : '') + klass + '-active'; - }); + forEach(className.split(' '), function(klass, i) { + activeClassName += (i > 0 ? ' ' : '') + klass + '-active'; + }); - // This triggers a reflow which allows for the transition animation to kick in. - var css3AnimationEvents = animationendEvent + ' ' + transitionendEvent; + element.data(NG_ANIMATE_CSS_DATA_KEY, { + className : className, + activeClassName : activeClassName, + maxDuration : maxDuration, + classes : className + ' ' + activeClassName, + timings : timings, + stagger : stagger, + ii : ii + }); - afterReflow(function() { - if(timings.transitionDuration > 0) { - node.style[transitionProp + propertyKey] = ''; - } - element.addClass(activeClassName); - }); + return true; + } - element.on(css3AnimationEvents, onAnimationProgress); + function animateRun(element, className, activeAnimationComplete) { + var data = element.data(NG_ANIMATE_CSS_DATA_KEY); + if(!element.hasClass(className) || !data) { + activeAnimationComplete(); + return; + } - // 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); + var node = element[0]; + var timings = data.timings; + var stagger = data.stagger; + var maxDuration = data.maxDuration; + var activeClassName = data.activeClassName; + var maxDelayTime = Math.max(timings.transitionDelay, timings.animationDelay) * 1000; + var startTime = Date.now(); + var css3AnimationEvents = ANIMATIONEND_EVENT + ' ' + TRANSITIONEND_EVENT; + var formerStyle; + var ii = data.ii; - // Only when the animation is cancelled is the done() - // function not called for this animation therefore - // this must be also called. - if(cancelled) { - done(); + var applyFallbackStyle, style = ''; + if(timings.transitionDuration > 0) { + node.style[TRANSITION_PROP + PROPERTY_KEY] = ''; + + var propertyStyle = timings.transitionPropertyStyle; + if(propertyStyle.indexOf('all') == -1) { + applyFallbackStyle = true; + var fallbackProperty = $sniffer.msie ? '-ms-zoom' : 'clip'; + style += CSS_PREFIX + 'transition-property: ' + propertyStyle + ', ' + fallbackProperty + '; '; + style += CSS_PREFIX + 'transition-duration: ' + timings.transitionDurationStyle + ', ' + timings.transitionDuration + 's; '; + } + } + + if(ii > 0) { + if(stagger.transitionDelay > 0 && stagger.transitionDuration === 0) { + var delayStyle = timings.transitionDelayStyle; + if(applyFallbackStyle) { + delayStyle += ', ' + timings.transitionDelay + 's'; } + + style += CSS_PREFIX + 'transition-delay: ' + + prepareStaggerDelay(delayStyle, stagger.transitionDelay, ii) + '; '; } + + if(stagger.animationDelay > 0 && stagger.animationDuration === 0) { + style += CSS_PREFIX + 'animation-delay: ' + + prepareStaggerDelay(timings.animationDelayStyle, stagger.animationDelay, ii) + '; '; + } } - else { - element.removeClass(className); - done(); + + if(style.length > 0) { + formerStyle = applyStyle(node, style); } + element.on(css3AnimationEvents, onAnimationProgress); + element.addClass(activeClassName); + + // 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(activeClassName); + animateClose(element, className); + if(formerStyle != null) { + formerStyle.length > 0 ? + node.setAttribute('style', formerStyle) : + node.removeAttribute('style'); + } + }; + 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 @@ -801,30 +1072,141 @@ * 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(); + activeAnimationComplete(); } } + } + function prepareStaggerDelay(delayStyle, staggerDelay, index) { + var style = ''; + forEach(delayStyle.split(','), function(val, i) { + style += (i > 0 ? ',' : '') + + (index * staggerDelay + parseInt(val, 10)) + 's'; + }); + return style; } + function animateBefore(element, className) { + if(animateSetup(element, className)) { + return function(cancelled) { + cancelled && animateClose(element, className); + }; + } + } + + function animateAfter(element, className, afterAnimationComplete) { + if(element.data(NG_ANIMATE_CSS_DATA_KEY)) { + return animateRun(element, className, afterAnimationComplete); + } else { + animateClose(element, className); + afterAnimationComplete(); + } + } + + function animate(element, className, animationComplete) { + //If the animateSetup function doesn't bother returning a + //cancellation function then it means that there is no animation + //to perform at all + var preReflowCancellation = animateBefore(element, className); + if(!preReflowCancellation) { + animationComplete(); + return; + } + + //There are two cancellation functions: one is before the first + //reflow animation and the second is during the active state + //animation. The first function will take care of removing the + //data from the element which will not make the 2nd animation + //happen in the first place + var cancel = preReflowCancellation; + afterReflow(function() { + //once the reflow is complete then we point cancel to + //the new cancellation function which will remove all of the + //animation properties from the active animation + cancel = animateAfter(element, className, animationComplete); + }); + + return function(cancelled) { + (cancel || noop)(cancelled); + }; + } + + function animateClose(element, className) { + element.removeClass(className); + element.removeClass(NG_ANIMATE_FALLBACK_CLASS_NAME); + element.removeData(NG_ANIMATE_CSS_DATA_KEY); + } + return { - enter : function(element, done) { - return animate(element, 'ng-enter', done); + allowCancel : function(element, animationEvent, className) { + //always cancel the current animation if it is a + //structural animation + var oldClasses = (element.data(NG_ANIMATE_CSS_DATA_KEY) || {}).classes; + if(!oldClasses || ['enter','leave','move'].indexOf(animationEvent) >= 0) { + return true; + } + + var parentElement = element.parent(); + var clone = angular.element(element[0].cloneNode()); + + //make the element super hidden and override any CSS style values + clone.attr('style','position:absolute; top:-9999px; left:-9999px'); + clone.removeAttr('id'); + clone.html(''); + + forEach(oldClasses.split(' '), function(klass) { + clone.removeClass(klass); + }); + + var suffix = animationEvent == 'addClass' ? '-add' : '-remove'; + clone.addClass(suffixClasses(className, suffix)); + parentElement.append(clone); + + var timings = getElementAnimationDetails(clone); + clone.remove(); + + return Math.max(timings.transitionDuration, timings.animationDuration) > 0; }, - leave : function(element, done) { - return animate(element, 'ng-leave', done); + + enter : function(element, animationCompleted) { + return animate(element, 'ng-enter', animationCompleted); }, - move : function(element, done) { - return animate(element, 'ng-move', done); + + leave : function(element, animationCompleted) { + return animate(element, 'ng-leave', animationCompleted); }, - addClass : function(element, className, done) { - return animate(element, suffixClasses(className, '-add'), done); + + move : function(element, animationCompleted) { + return animate(element, 'ng-move', animationCompleted); }, - removeClass : function(element, className, done) { - return animate(element, suffixClasses(className, '-remove'), done); + + beforeAddClass : function(element, className, animationCompleted) { + var cancellationMethod = animateBefore(element, suffixClasses(className, '-add')); + if(cancellationMethod) { + afterReflow(animationCompleted); + return cancellationMethod; + } + animationCompleted(); + }, + + addClass : function(element, className, animationCompleted) { + return animateAfter(element, suffixClasses(className, '-add'), animationCompleted); + }, + + beforeRemoveClass : function(element, className, animationCompleted) { + var cancellationMethod = animateBefore(element, suffixClasses(className, '-remove')); + if(cancellationMethod) { + afterReflow(animationCompleted); + return cancellationMethod; + } + animationCompleted(); + }, + + removeClass : function(element, className, animationCompleted) { + return animateAfter(element, suffixClasses(className, '-remove'), animationCompleted); } }; function suffixClasses(classes, suffix) { var className = '';