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('