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/ngAnimate/animate.js b/src/ngAnimate/animate.js index 542b678f8316..ffb4c4d3f8db 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -1020,8 +1020,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 +1074,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 +1213,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 +1233,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 +1281,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 +1345,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 +1440,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 +1480,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 +1517,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..ed4766211624 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); }));