diff --git a/css/angular.css b/css/angular.css index 2566640ebb2f..b88e61e483e2 100644 --- a/css/angular.css +++ b/css/angular.css @@ -9,8 +9,3 @@ ng\:form { display: block; } - -.ng-animate-block-transitions { - transition:0s all!important; - -webkit-transition:0s all!important; -} diff --git a/src/ng/directive/ngShowHide.js b/src/ng/directive/ngShowHide.js index 59312f0f65b5..fd5fa46ae40a 100644 --- a/src/ng/directive/ngShowHide.js +++ b/src/ng/directive/ngShowHide.js @@ -40,10 +40,10 @@ * restating the styles for the .ng-hide class in CSS: * ```css * .ng-hide { - * //!annotate CSS Specificity|Not to worry, this will override the AngularJS default... + * /* Not to worry, this will override the AngularJS default... * display:block!important; * - * //this is just another form of hiding an element + * /* this is just another form of hiding an element */ * position:absolute; * top:-9999px; * left:-9999px; @@ -69,10 +69,20 @@ * //a working example can be found at the bottom of this page * // * .my-element.ng-hide-add, .my-element.ng-hide-remove { - * transition:0.5s linear all; + * /* this is required as of 1.3x to properly + * apply all styling in a show/hide animation */ + * transition:0s linear all; + * + * /* this must be set as block so the animation is visible */ * display:block!important; * } * + * .my-element.ng-hide-add-active, + * .my-element.ng-hide-remove-active { + * /* the transition is defined in the active class */ + * transition:1s linear all; + * } + * * .my-element.ng-hide-add { ... } * .my-element.ng-hide-add.ng-hide-add-active { ... } * .my-element.ng-hide-remove { ... } @@ -109,8 +119,6 @@ .animate-show { - -webkit-transition:all linear 0.5s; - transition:all linear 0.5s; line-height:20px; opacity:1; padding:10px; @@ -123,6 +131,12 @@ display:block!important; } + .animate-show.ng-hide-add.ng-hide-add-active, + .animate-show.ng-hide-remove.ng-hide-remove-active { + -webkit-transition:all linear 0.5s; + transition:all linear 0.5s; + } + .animate-show.ng-hide { line-height:0; opacity:0; diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index 0466cecb1715..919de4cbdd69 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -6,11 +6,8 @@ * @name ngAnimate * @description * - * # ngAnimate - * * The `ngAnimate` module provides support for JavaScript, CSS3 transition and CSS3 keyframe animation hooks within existing core and custom directives. * - * *
* * # Usage @@ -22,17 +19,16 @@ * * Below is a more detailed breakdown of the supported animation events provided by pre-existing ng directives: * - * | Directive | Supported Animations | - * |---------------------------------------------------------- |----------------------------------------------------| - * | {@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) | - * | {@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) | + * | Directive | Supported Animations | + * |-----------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------| + * | {@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 (the CSS class(es) present) | + * | {@link ng.directive:ngShow#usage_animations ngShow} & {@link ng.directive:ngHide#usage_animations ngHide} | add and remove (the ng-hide class value) | + * | {@link ng.directive:form#usage_animations form} & {@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. * @@ -130,7 +126,74 @@ * 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. * - *

CSS Staggering Animations

+ * ### Structural transition animations + * + * Structural transitions (such as enter, leave and move) will always apply a `none 0s` transition + * value to force the browser into rendering the styles defined in the setup (.ng-enter, .ng-leave + * or .ng-move) class. This means that any active transition animations operating on the element + * will be cut off to make way for the enter, leave or move animation. + * + * ### Class-based transition animations + * + * Class-based transitions refer to transition animations that are triggered when a CSS class is + * added to or removed from the element (via `$animate.addClass`, `$animate.removeClass`, + * `$animate.setClass`, or by directives such as `ngClass`, `ngModel` and `form`). + * They are different when compared to structural animations since they **do not cancel existing + * animations** nor do they **block successive transitions** from rendering on the same element. + * This distinction allows for **multiple class-based transitions** to be performed on the same element. + * + * In addition to ngAnimate supporting the default (natural) functionality of class-based transition + * animations, ngAnimate also decorates the element with starting and ending CSS classes to aid the + * developer in further styling the element throughout the transition animation. Earlier versions + * of ngAnimate may have caused natural CSS transitions to break and not render properly due to + * $animate temporarily blocking transitions using `none 0s` in order to allow the setup CSS class + * (the `-add` or `-remove` class) to be applied without triggering an animation. However, as of **1.3**, + * This workaround has been removed with ngAnimate and all non-ngAnimate CSS class transitions + * are compatible with ngAnimate. + * + * There is, however, one special case when dealing with class-based transitions in ngAnimate. + * When rendering class-based transitions that make use of the setup and active CSS classes + * (e.g. `.fade-add` and `.fade-add-active` for when `.fade` is added) be sure to define + * the transition value **on the active CSS class** and not the setup class. + * + * ```css + * .fade-add { + * /* remember to place a 0s transition here + * to ensure that the styles are applied instantly + * even if the element already has a transition style */ + * transition:0s linear all; + * + * /* starting CSS styles */ + * opacity:1; + * } + * .fade-add.fade-add-active { + * /* this will be the length of the animation */ + * transition:1s linear all; + * opacity:0; + * } + * ``` + * + * The setup CSS class (in this case `.fade-add`) also has a transition style property, however, it + * has a duration of zero. This may not be required, however, incase the browser is unable to render + * the styling present in this CSS class instantly then it could be that the browser is attempting + * to perform an unnecessary transition. + * + * This workaround, however, does not apply to standard class-based transitions that are rendered + * when a CSS class containing a transition is applied to an element: + * + * ```css + * .fade { + * /* this works as expected */ + * transition:1s linear all; + * opacity:0; + * } + * ``` + * + * Please keep this in mind when coding the CSS markup that will be used within class-based transitions. + * Also, try not to mix the two class-based animation flavors together since the CSS code may become + * overly complex. + * + * ### CSS Staggering Animations * 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 @@ -522,18 +585,21 @@ angular.module('ngAnimate', ['ng']) * * 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 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" | + * | 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 waits for the next digest to start the animation | class="my-animation ng-animate" | + * | 4. $animate runs the JavaScript-defined animations detected on the element | class="my-animation ng-animate" | + * | 5. the .ng-enter class is added to the element | class="my-animation ng-animate ng-enter" | + * | 6. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate ng-enter" | + * | 7. $animate blocks all CSS transitions on the element to ensure the .ng-enter class styling is applied right away | class="my-animation ng-animate ng-enter" | + * | 8. $animate waits for a single animation frame (this performs a reflow) | class="my-animation ng-animate ng-enter" | + * | 9. $animate removes the CSS transition block placed on the element | class="my-animation ng-animate ng-enter" | + * | 10. the .ng-enter-active class is added (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-enter ng-enter-active" | + * | 11. $animate waits for the animation to complete (via events and timeout) | class="my-animation ng-animate ng-enter ng-enter-active" | + * | 12. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | + * | 13. The doneCallback() callback is fired (if provided) | class="my-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 @@ -560,18 +626,21 @@ angular.module('ngAnimate', ['ng']) * * Below is a breakdown of each step that occurs during leave 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 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) | ... | + * | Animation Step | What the element class attribute looks like | + * |-------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------| + * | 1. $animate.leave(...) is called | class="my-animation" | + * | 2. $animate runs the JavaScript-defined animations detected on the element | class="my-animation ng-animate" | + * | 3. $animate waits for the next digest to start the animation | class="my-animation ng-animate" | + * | 4. the .ng-leave class is added to the element | class="my-animation ng-animate ng-leave" | + * | 5. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate ng-leave" | + * | 6. $animate blocks all CSS transitions on the element to ensure the .ng-leave class styling is applied right away | class="my-animation ng-animate ng-leave” | + * | 7. $animate waits for a single animation frame (this performs a reflow) | class="my-animation ng-animate ng-leave" | + * | 8. $animate removes the CSS transition block placed on the element | class="my-animation ng-animate ng-leave” | + * | 9. the .ng-leave-active class is added (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-leave ng-leave-active" | + * | 10. $animate waits for the animation to complete (via events and timeout) | class="my-animation ng-animate ng-leave ng-leave-active" | + * | 11. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | + * | 12. The element is removed from the DOM | ... | + * | 13. The doneCallback() callback is fired (if provided) | ... | * * @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 @@ -598,18 +667,21 @@ angular.module('ngAnimate', ['ng']) * * 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 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" | + * | Animation Step | What the element class attribute looks like | + * |------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------| + * | 1. $animate.move(...) is called | class="my-animation" | + * | 2. element is moved into the parentElement element or beside the afterElement element | class="my-animation" | + * | 3. $animate waits for the next digest to start the animation | class="my-animation ng-animate" | + * | 4. $animate runs the JavaScript-defined animations detected on the element | class="my-animation ng-animate" | + * | 5. the .ng-move class is added to the element | class="my-animation ng-animate ng-move" | + * | 6. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate ng-move" | + * | 7. $animate blocks all CSS transitions on the element to ensure the .ng-move class styling is applied right away | class="my-animation ng-animate ng-move” | + * | 8. $animate waits for a single animation frame (this performs a reflow) | class="my-animation ng-animate ng-move" | + * | 9. $animate removes the CSS transition block placed on the element | class="my-animation ng-animate ng-move” | + * | 10. the .ng-move-active class is added (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-move ng-move-active" | + * | 11. $animate waits for the animation to complete (via events and timeout) | class="my-animation ng-animate ng-move ng-move-active" | + * | 12. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | + * | 13. The doneCallback() callback is fired (if provided) | class="my-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 @@ -634,22 +706,22 @@ angular.module('ngAnimate', ['ng']) * 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 or base CSS class). + * or keyframes are defined on the -add-active 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="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 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" | + * | Animation Step | What the element class attribute looks like | + * |----------------------------------------------------------------------------------------------------|------------------------------------------------------------------| + * | 1. $animate.addClass(element, 'super') is called | class="my-animation" | + * | 2. $animate runs the JavaScript-defined animations detected on the element | class="my-animation ng-animate" | + * | 3. the .super-add class is added to the element | class="my-animation ng-animate super-add" | + * | 4. $animate waits for a single animation frame (this performs a reflow) | class="my-animation ng-animate super-add" | + * | 5. the .super and .super-add-active classes are added (this triggers the CSS transition/animation) | class="my-animation ng-animate super super-add super-add-active" | + * | 6. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate super-add" | + * | 7. $animate waits for the animation to complete (via events and timeout) | 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 {DOMElement} element the element that will be animated * @param {string} className the CSS class that will be added to the element and then animated @@ -674,17 +746,17 @@ angular.module('ngAnimate', ['ng']) * * 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="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" | + * | Animation Step | What the element class attribute looks like | + * |------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------| + * | 1. $animate.removeClass(element, 'super') is called | class="my-animation super" | + * | 2. $animate runs the JavaScript-defined animations detected on the element | class="my-animation super ng-animate" | + * | 3. the .super-remove class is added to the element | class="my-animation super ng-animate super-remove" | + * | 4. $animate waits for a single animation frame (this performs a reflow) | class="my-animation super ng-animate super-remove" | + * | 5. the .super-remove-active classes are added and .super is removed (this triggers the CSS transition/animation) | class="my-animation ng-animate super-remove super-remove-active" | + * | 6. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation super ng-animate super-remove" | + * | 7. $animate waits for the animation to complete (via events and timeout) | class="my-animation ng-animate 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 {DOMElement} element the element that will be animated @@ -698,20 +770,33 @@ angular.module('ngAnimate', ['ng']) }, doneCallback); }, - /** - * - * @ngdoc function - * @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 {DOMElement} element the element which will it's CSS classes changed - * removed from it - * @param {string} add the CSS classes which will be added to the element - * @param {string} remove the CSS class which will be removed from the element - * @param {Function=} done the callback function (if provided) that will be fired after the - * CSS classes have been set on the element - */ + /** + * + * @ngdoc method + * @name $animate#setClass + * + * @description Adds and/or removes the given CSS classes to and from the element. + * Once complete, the done() callback will be fired (if provided). + * + * | Animation Step | What the element class attribute looks like | + * |--------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------| + * | 1. $animate.removeClass(element, ‘on’, ‘off’) is called | class="my-animation super off” | + * | 2. $animate runs the JavaScript-defined animations detected on the element | class="my-animation super ng-animate off” | + * | 3. the .on-add and .off-remove classes are added to the element | class="my-animation ng-animate on-add off-remove off” | + * | 4. $animate waits for a single animation frame (this performs a reflow) | class="my-animation ng-animate on-add off-remove off” | + * | 5. the .on, .on-add-active and .off-remove-active classes are added and .off is removed (this triggers the CSS transition/animation) | class="my-animation ng-animate on on-add on-add-active off-remove off-remove-active” | + * | 6. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate on on-add on-add-active off-remove off-remove-active" | + * | 7. $animate waits for the animation to complete (via events and timeout) | class="my-animation ng-animate on on-add on-add-active off-remove off-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 {DOMElement} element the element which will it's CSS classes changed + * removed from it + * @param {string} add the CSS classes which will be added to the element + * @param {string} remove the CSS class which will be removed from the element + * @param {Function=} done the callback function (if provided) that will be fired after the + * CSS classes have been set on the element + */ setClass : function(element, add, remove, doneCallback) { element = stripCommentsFromElement(element); performAnimation('setClass', [add, remove], element, null, null, function() { @@ -789,9 +874,9 @@ angular.module('ngAnimate', ['ng']) //only allow animations if the currently running animation is not structural //or if there is no animation running at all - var skipAnimations = runner.isClassBased ? - ngAnimateState.disabled || (lastAnimation && !lastAnimation.isClassBased) : - false; + var skipAnimations = runner.isClassBased + ? ngAnimateState.disabled || (lastAnimation && !lastAnimation.isClassBased) + : false; //skip the animation if animations are disabled, a parent is already being animated, //the element is not currently attached to the document body or then completely close @@ -1020,8 +1105,11 @@ angular.module('ngAnimate', ['ng']) 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); + var state = isRoot ? rootAnimateState : (parentElement.data(NG_ANIMATE_STATE) || {}); + var result = state.disabled || state.running + ? true + : state.last && !state.last.isClassBased; + if(isRoot || result) { return result; } @@ -1071,7 +1159,6 @@ angular.module('ngAnimate', ['ng']) var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount'; var NG_ANIMATE_PARENT_KEY = '$$ngAnimateKey'; var NG_ANIMATE_CSS_DATA_KEY = '$$ngAnimateCSS3Data'; - var NG_ANIMATE_BLOCK_CLASS_NAME = 'ng-animate-block-transitions'; var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3; var CLOSING_TIME_BUFFER = 1.5; var ONE_SECOND = 1000; @@ -1211,7 +1298,9 @@ angular.module('ngAnimate', ['ng']) return parentID + '-' + extractElementNode(element).className; } - function animateSetup(animationEvent, element, className, calculationDecorator) { + function animateSetup(animationEvent, element, className) { + var structural = ['ng-enter','ng-leave','ng-move'].indexOf(className) >= 0; + var cacheKey = getCacheKey(element); var eventCacheKey = cacheKey + ' ' + className; var itemIndex = lookupCache[eventCacheKey] ? ++lookupCache[eventCacheKey].total : 0; @@ -1229,85 +1318,44 @@ angular.module('ngAnimate', ['ng']) applyClasses && element.removeClass(staggerClassName); } - /* the animation itself may need to add/remove special CSS classes - * before calculating the anmation styles */ - calculationDecorator = calculationDecorator || - function(fn) { return fn(); }; - element.addClass(className); var formerData = element.data(NG_ANIMATE_CSS_DATA_KEY) || {}; - - var timings = calculationDecorator(function() { - return getElementAnimationDetails(element, eventCacheKey); - }); - + var timings = getElementAnimationDetails(element, eventCacheKey); var transitionDuration = timings.transitionDuration; var animationDuration = timings.animationDuration; - if(transitionDuration === 0 && animationDuration === 0) { + + if(structural && transitionDuration === 0 && animationDuration === 0) { element.removeClass(className); return false; } + var blockTransition = structural && transitionDuration > 0; + var blockAnimation = animationDuration > 0 && + stagger.animationDelay > 0 && + stagger.animationDuration === 0; + element.data(NG_ANIMATE_CSS_DATA_KEY, { + stagger : stagger, + cacheKey : eventCacheKey, running : formerData.running || 0, itemIndex : itemIndex, - stagger : stagger, - timings : timings, + blockTransition : blockTransition, + blockAnimation : blockAnimation, closeAnimationFn : noop }); - //temporarily disable the transition so that the enter styles - //don't animate twice (this is here to avoid a bug in Chrome/FF). - var isCurrentlyAnimating = formerData.running > 0 || animationEvent == 'setClass'; - if(transitionDuration > 0) { - blockTransitions(element, className, isCurrentlyAnimating); - } - - //staggering keyframe animations work by adjusting the `animation-delay` CSS property - //on the given element, however, the delay value can only calculated after the reflow - //since by that time $animate knows how many elements are being animated. Therefore, - //until the reflow occurs the element needs to be blocked (where the keyframe animation - //is set to `none 0s`). This blocking mechanism should only be set for when a stagger - //animation is detected and when the element item index is greater than 0. - if(animationDuration > 0 && stagger.animationDelay > 0 && stagger.animationDuration === 0) { - blockKeyframeAnimations(element); - } - - return true; - } - - function isStructuralAnimation(className) { - return className == 'ng-enter' || className == 'ng-move' || className == 'ng-leave'; - } + var node = extractElementNode(element); - function blockTransitions(element, className, isAnimating) { - if(isStructuralAnimation(className) || !isAnimating) { - extractElementNode(element).style[TRANSITION_PROP + PROPERTY_KEY] = 'none'; - } else { - element.addClass(NG_ANIMATE_BLOCK_CLASS_NAME); + if(blockTransition) { + node.style[TRANSITION_PROP + PROPERTY_KEY] = 'none'; } - } - function blockKeyframeAnimations(element) { - extractElementNode(element).style[ANIMATION_PROP] = 'none 0s'; - } - - function unblockTransitions(element, className) { - var prop = TRANSITION_PROP + PROPERTY_KEY; - var node = extractElementNode(element); - if(node.style[prop] && node.style[prop].length > 0) { - node.style[prop] = ''; + if(blockAnimation) { + node.style[ANIMATION_PROP] = 'none 0s'; } - element.removeClass(NG_ANIMATE_BLOCK_CLASS_NAME); - } - function unblockKeyframeAnimations(element) { - var prop = ANIMATION_PROP; - var node = extractElementNode(element); - if(node.style[prop] && node.style[prop].length > 0) { - node.style[prop] = ''; - } + return true; } function animateRun(animationEvent, element, className, activeAnimationComplete) { @@ -1318,21 +1366,36 @@ angular.module('ngAnimate', ['ng']) return; } + if(elementData.blockTransition) { + node.style[TRANSITION_PROP + PROPERTY_KEY] = ''; + } + + if(elementData.blockAnimation) { + node.style[ANIMATION_PROP] = ''; + } + var activeClassName = ''; forEach(className.split(' '), function(klass, i) { activeClassName += (i > 0 ? ' ' : '') + klass + '-active'; }); - var stagger = elementData.stagger; - var timings = elementData.timings; - var itemIndex = elementData.itemIndex; + element.addClass(activeClassName); + var eventCacheKey = elementData.eventCacheKey + ' ' + activeClassName; + var timings = getElementAnimationDetails(element, eventCacheKey); + var maxDuration = Math.max(timings.transitionDuration, timings.animationDuration); + if(maxDuration === 0) { + element.removeClass(activeClassName); + animateClose(element, className); + activeAnimationComplete(); + return; + } + var maxDelay = Math.max(timings.transitionDelay, timings.animationDelay); + var stagger = elementData.stagger; + var itemIndex = elementData.itemIndex; var maxDelayTime = maxDelay * ONE_SECOND; - var startTime = Date.now(); - var css3AnimationEvents = ANIMATIONEND_EVENT + ' ' + TRANSITIONEND_EVENT; - var style = '', appliedStyles = []; if(timings.transitionDuration > 0) { var propertyStyle = timings.transitionPropertyStyle; @@ -1367,8 +1430,10 @@ angular.module('ngAnimate', ['ng']) node.setAttribute('style', oldStyle + ' ' + style); } + var startTime = Date.now(); + var css3AnimationEvents = ANIMATIONEND_EVENT + ' ' + TRANSITIONEND_EVENT; + element.on(css3AnimationEvents, onAnimationProgress); - element.addClass(activeClassName); elementData.closeAnimationFn = function() { onEnd(); activeAnimationComplete(); @@ -1460,8 +1525,6 @@ angular.module('ngAnimate', ['ng']) //happen in the first place var cancel = preReflowCancellation; afterReflow(element, function() { - unblockTransitions(element, className); - unblockKeyframeAnimations(element); //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 @@ -1502,49 +1565,27 @@ angular.module('ngAnimate', ['ng']) beforeSetClass : function(element, add, remove, animationCompleted) { var className = suffixClasses(remove, '-remove') + ' ' + suffixClasses(add, '-add'); - var cancellationMethod = animateBefore('setClass', element, className, function(fn) { - /* when classes are removed from an element then the transition style - * that is applied is the transition defined on the element without the - * CSS class being there. This is how CSS3 functions outside of ngAnimate. - * http://plnkr.co/edit/j8OzgTNxHTb4n3zLyjGW?p=preview */ - var klass = element.attr('class'); - element.removeClass(remove); - element.addClass(add); - var timings = fn(); - element.attr('class', klass); - return timings; - }); - + var cancellationMethod = animateBefore('setClass', element, className); if(cancellationMethod) { - afterReflow(element, function() { - unblockTransitions(element, className); - unblockKeyframeAnimations(element); - animationCompleted(); - }); + afterReflow(element, animationCompleted); return cancellationMethod; } animationCompleted(); }, beforeAddClass : function(element, className, animationCompleted) { - var cancellationMethod = animateBefore('addClass', element, suffixClasses(className, '-add'), function(fn) { - - /* when a CSS class is added to an element then the transition style that - * is applied is the transition defined on the element when the CSS class - * is added at the time of the animation. This is how CSS3 functions - * outside of ngAnimate. */ - element.addClass(className); - var timings = fn(); - element.removeClass(className); - return timings; - }); + var cancellationMethod = animateBefore('addClass', element, suffixClasses(className, '-add')); + if(cancellationMethod) { + afterReflow(element, animationCompleted); + return cancellationMethod; + } + animationCompleted(); + }, + beforeRemoveClass : function(element, className, animationCompleted) { + var cancellationMethod = animateBefore('removeClass', element, suffixClasses(className, '-remove')); if(cancellationMethod) { - afterReflow(element, function() { - unblockTransitions(element, className); - unblockKeyframeAnimations(element); - animationCompleted(); - }); + afterReflow(element, animationCompleted); return cancellationMethod; } animationCompleted(); @@ -1561,30 +1602,6 @@ angular.module('ngAnimate', ['ng']) return animateAfter('addClass', element, suffixClasses(className, '-add'), animationCompleted); }, - beforeRemoveClass : function(element, className, animationCompleted) { - var cancellationMethod = animateBefore('removeClass', element, suffixClasses(className, '-remove'), function(fn) { - /* when classes are removed from an element then the transition style - * that is applied is the transition defined on the element without the - * CSS class being there. This is how CSS3 functions outside of ngAnimate. - * http://plnkr.co/edit/j8OzgTNxHTb4n3zLyjGW?p=preview */ - var klass = element.attr('class'); - element.removeClass(className); - var timings = fn(); - element.attr('class', klass); - return timings; - }); - - if(cancellationMethod) { - afterReflow(element, function() { - unblockTransitions(element, className); - unblockKeyframeAnimations(element); - animationCompleted(); - }); - return cancellationMethod; - } - animationCompleted(); - }, - removeClass : function(element, className, animationCompleted) { return animateAfter('removeClass', element, suffixClasses(className, '-remove'), animationCompleted); } diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index 204ca9c32114..98836ee08f26 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -112,11 +112,13 @@ describe("ngAnimate", function() { angular.element(document.body).append($rootElement); $animate.addClass(elm1, 'klass'); + $animate.triggerReflow(); expect(count).toBe(1); $animate.enabled(false); $animate.addClass(elm1, 'klass2'); + $animate.triggerReflow(); expect(count).toBe(1); $animate.enabled(true); @@ -124,16 +126,19 @@ describe("ngAnimate", function() { elm1.append(elm2); $animate.addClass(elm2, 'klass'); + $animate.triggerReflow(); expect(count).toBe(2); $animate.enabled(false, elm1); $animate.addClass(elm2, 'klass2'); + $animate.triggerReflow(); expect(count).toBe(2); var root = angular.element($rootElement[0]); $rootElement.addClass('animated'); $animate.addClass(root, 'klass2'); + $animate.triggerReflow(); expect(count).toBe(3); }); }); @@ -188,12 +193,14 @@ describe("ngAnimate", function() { expect(captured).toBe(false); $animate.addClass(element, 'red'); + $animate.triggerReflow(); expect(captured).toBe(true); captured = false; $animate.enabled(false); $animate.addClass(element, 'blue'); + $animate.triggerReflow(); expect(captured).toBe(false); //clean up the mess @@ -392,6 +399,7 @@ describe("ngAnimate", function() { inject(function($animate, $rootScope, $sniffer, $timeout) { child.attr('class','classify no'); $animate.setClass(child, 'yes', 'no'); + $animate.triggerReflow(); expect(child.hasClass('yes')).toBe(true); expect(child.hasClass('no')).toBe(false); @@ -433,6 +441,7 @@ describe("ngAnimate", function() { inject(function($animate, $rootScope, $sniffer, $timeout) { child.attr('class','classify no'); $animate.setClass(child, 'yes', 'no'); + $animate.triggerReflow(); expect(child.hasClass('yes')).toBe(true); expect(child.hasClass('no')).toBe(false); @@ -647,6 +656,7 @@ describe("ngAnimate", function() { $animate.addClass(child, 'custom-delay'); $animate.addClass(child, 'custom-long-delay'); + $animate.triggerReflow(); expect(child.hasClass('animation-cancelled')).toBe(false); expect(child.hasClass('animation-ended')).toBe(false); @@ -672,6 +682,7 @@ describe("ngAnimate", function() { inject(function($animate, $rootScope, $compile, $sniffer, $timeout) { $animate.addClass(element, 'custom-delay custom-long-delay'); + $animate.triggerReflow(); $timeout.flush(2000); $timeout.flush(20000); expect(element.hasClass('custom-delay')).toBe(true); @@ -1183,6 +1194,53 @@ describe("ngAnimate", function() { } })); + it("should place a hard block when a structural CSS transition is run", + inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement) { + + if(!$sniffer.transitions) return; + + ss.addRule('.leave-animation.ng-leave', + '-webkit-transition:5s linear all;' + + 'transition:5s linear all;' + + 'opacity:1;'); + + ss.addRule('.leave-animation.ng-leave.ng-leave-active', 'opacity:1'); + + element = $compile(html('
1
'))($rootScope); + + $animate.leave(element); + $rootScope.$digest(); + + expect(element.attr('style')).toMatch(/transition.*?:\s*none/); + + $animate.triggerReflow(); + + expect(element.attr('style')).not.toMatch(/transition.*?:\s*none/); + })); + + it("should not place a hard block when a class-based CSS transition is run", + inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement) { + + if(!$sniffer.transitions) return; + + ss.addRule('.my-class', '-webkit-transition:5s linear all;' + + 'transition:5s linear all;'); + + element = $compile(html('
1
'))($rootScope); + + $animate.addClass(element, 'my-class'); + + expect(element.attr('style')).not.toMatch(/transition.*?:\s*none/); + expect(element.hasClass('my-class')).toBe(false); + expect(element.hasClass('my-class-add')).toBe(true); + + $animate.triggerReflow(); + + expect(element.attr('style')).not.toMatch(/transition.*?:\s*none/); + expect(element.hasClass('my-class')).toBe(true); + expect(element.hasClass('my-class-add')).toBe(true); + expect(element.hasClass('my-class-add-active')).toBe(true); + })); it("should stagger the items when the correct CSS class is provided", inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement) { @@ -1650,10 +1708,12 @@ describe("ngAnimate", function() { $animate.addClass(element, 'on', function() { signature += 'A'; }); + $animate.triggerReflow(); $animate.removeClass(element, 'on', function() { signature += 'B'; }); + $animate.triggerReflow(); $animate.triggerCallbacks(); @@ -1676,6 +1736,7 @@ describe("ngAnimate", function() { signature += 'Z'; }); + $animate.triggerReflow(); $animate.triggerCallbacks(); expect(signature).toBe('Z'); @@ -1917,13 +1978,13 @@ describe("ngAnimate", function() { //actual animations captured = 'none'; $animate.removeClass(element, 'some-class'); - $timeout.flush(); + $animate.triggerReflow(); expect(element.hasClass('some-class')).toBe(false); expect(captured).toBe('removeClass-some-class'); captured = 'nothing'; $animate.addClass(element, 'some-class'); - $timeout.flush(); + $animate.triggerReflow(); expect(element.hasClass('some-class')).toBe(true); expect(captured).toBe('addClass-some-class'); })); @@ -1938,10 +1999,12 @@ describe("ngAnimate", function() { var element = jqLite(parent.find('span')); $animate.addClass(element,'klass'); + $animate.triggerReflow(); expect(element.hasClass('klass')).toBe(true); $animate.removeClass(element,'klass'); + $animate.triggerReflow(); expect(element.hasClass('klass')).toBe(false); expect(element.hasClass('klass-remove')).toBe(false); @@ -1962,12 +2025,14 @@ describe("ngAnimate", function() { $animate.addClass(element,'klass', function() { signature += 'A'; }); + $animate.triggerReflow(); expect(element.hasClass('klass')).toBe(true); $animate.removeClass(element,'klass', function() { signature += 'B'; }); + $animate.triggerReflow(); $animate.triggerCallbacks(); expect(element.hasClass('klass')).toBe(false); @@ -2040,6 +2105,7 @@ describe("ngAnimate", function() { $animate.addClass(element,'klassy', function() { signature += 'X'; }); + $animate.triggerReflow(); $timeout.flush(500); @@ -2048,6 +2114,7 @@ describe("ngAnimate", function() { $animate.removeClass(element,'klassy', function() { signature += 'Y'; }); + $animate.triggerReflow(); $timeout.flush(3000); @@ -2546,12 +2613,15 @@ describe("ngAnimate", function() { var element = html($compile('
')($rootScope)); $animate.addClass(element, 'super'); + $animate.triggerReflow(); expect(element.data('classify')).toBe('add-super'); $animate.removeClass(element, 'super'); + $animate.triggerReflow(); expect(element.data('classify')).toBe('remove-super'); $animate.addClass(element, 'superguy'); + $animate.triggerReflow(); expect(element.data('classify')).toBe('add-superguy'); }); }); @@ -2756,12 +2826,14 @@ describe("ngAnimate", function() { $animate.enabled(true, element); $animate.addClass(child, 'awesome'); + $animate.triggerReflow(); expect(childAnimated).toBe(true); childAnimated = false; $animate.enabled(false, element); $animate.addClass(child, 'super'); + $animate.triggerReflow(); expect(childAnimated).toBe(false); $animate.leave(element); @@ -2771,7 +2843,7 @@ describe("ngAnimate", function() { }); - it("should disable all child animations on structural animations until the post animation" + + it("should disable all child animations on structural animations until the post animation " + "timeout has passed as well as all structural animations", function() { var intercepted, continueAnimation; module(function($animateProvider) { @@ -2820,6 +2892,7 @@ describe("ngAnimate", function() { continueAnimation(); $animate.addClass(child1, 'test'); + $animate.triggerReflow(); expect(child1.hasClass('test')).toBe(true); expect(element.children().length).toBe(2); @@ -2877,6 +2950,7 @@ describe("ngAnimate", function() { $rootScope.bool = true; $rootScope.$digest(); + expect(intercepted).toBe(true); }); }); @@ -3014,9 +3088,13 @@ describe("ngAnimate", function() { $rootElement.addClass('animated'); $animate.addClass($rootElement, 'green'); + $animate.triggerReflow(); + expect(count).toBe(1); $animate.addClass($rootElement, 'red'); + $animate.triggerReflow(); + expect(count).toBe(2); }); }); @@ -3045,6 +3123,7 @@ describe("ngAnimate", function() { $rootElement.append(element); $animate.addClass(element, 'red'); + $animate.triggerReflow(); expect(steps).toEqual(['before','after']); }); @@ -3102,20 +3181,18 @@ describe("ngAnimate", function() { $animate.removeClass(element, 'base-class one two'); //still true since we're before the reflow - expect(element.hasClass('base-class')).toBe(false); + expect(element.hasClass('base-class')).toBe(true); //this will cancel the remove animation $animate.addClass(element, 'base-class one two'); - //the cancellation was a success and the class was added right away - //since there was no successive animation for the after animation + //the cancellation was a success and the class was removed right away expect(element.hasClass('base-class')).toBe(false); //the reflow... $animate.triggerReflow(); - //the reflow DOM operation was commenced but it ran before so it - //shouldn't run agaun + //the reflow DOM operation was commenced... expect(element.hasClass('base-class')).toBe(true); })); @@ -3460,6 +3537,7 @@ describe("ngAnimate", function() { ready = true; }); + $animate.triggerReflow(); $animate.triggerCallbacks(); expect(ready).toBe(true); }));