diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index 989dea90..2b2de1ab 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -484,12 +484,12 @@ angular.module('ngAnimate', ['ng']) // the matching CSS class. if (status < 0) { //does it have the class or will it have the class - if(hasClass || matchingAnimation.event == 'addClass') { + if (hasClass || matchingAnimation.event == 'addClass') { toRemove.push(className); } } else if (status > 0) { //is the class missing or will it be removed? - if(!hasClass || matchingAnimation.event == 'removeClass') { + if (!hasClass || matchingAnimation.event == 'removeClass') { toAdd.push(className); } } @@ -544,7 +544,7 @@ angular.module('ngAnimate', ['ng']) if (!classNameAdd) { className = classNameRemove; animationEvent = 'removeClass'; - } else if(!classNameRemove) { + } else if (!classNameRemove) { className = classNameAdd; animationEvent = 'addClass'; } else { @@ -1101,6 +1101,7 @@ angular.module('ngAnimate', ['ng']) var totalActiveAnimations = ngAnimateState.totalActive || 0; var lastAnimation = ngAnimateState.last; var skipAnimation = false; + if (totalActiveAnimations > 0) { var animationsToCancel = []; if (!runner.isClassBased) { @@ -1384,6 +1385,7 @@ angular.module('ngAnimate', ['ng']) var PROPERTY_KEY = 'Property'; var DELAY_KEY = 'Delay'; var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount'; + var ANIMATION_PLAYSTATE_KEY = 'PlayState'; var NG_ANIMATE_PARENT_KEY = '$$ngAnimateKey'; var NG_ANIMATE_CSS_DATA_KEY = '$$ngAnimateCSS3Data'; var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3; @@ -1455,47 +1457,33 @@ angular.module('ngAnimate', ['ng']) 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) || {}; - transitionDurationStyle = elementStyles[TRANSITION_PROP + DURATION_KEY]; - + var transitionDurationStyle = elementStyles[TRANSITION_PROP + DURATION_KEY]; transitionDuration = Math.max(parseMaxTime(transitionDurationStyle), transitionDuration); - transitionPropertyStyle = elementStyles[TRANSITION_PROP + PROPERTY_KEY]; - - transitionDelayStyle = elementStyles[TRANSITION_PROP + DELAY_KEY]; - + var transitionDelayStyle = elementStyles[TRANSITION_PROP + DELAY_KEY]; transitionDelay = Math.max(parseMaxTime(transitionDelayStyle), transitionDelay); - animationDelayStyle = elementStyles[ANIMATION_PROP + DELAY_KEY]; - - animationDelay = Math.max(parseMaxTime(animationDelayStyle), animationDelay); + var animationDelayStyle = elementStyles[ANIMATION_PROP + DELAY_KEY]; + animationDelay = Math.max(parseMaxTime(elementStyles[ANIMATION_PROP + DELAY_KEY]), 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 = { total : 0, - transitionPropertyStyle: transitionPropertyStyle, - transitionDurationStyle: transitionDurationStyle, - transitionDelayStyle: transitionDelayStyle, transitionDelay: transitionDelay, transitionDuration: transitionDuration, - animationDelayStyle: animationDelayStyle, animationDelay: animationDelay, animationDuration: animationDuration }; @@ -1571,18 +1559,17 @@ angular.module('ngAnimate', ['ng']) running : formerData.running || 0, itemIndex : itemIndex, blockTransition : blockTransition, - blockAnimation : blockAnimation, closeAnimationFns : closeAnimationFns }); var node = extractElementNode(element); if (blockTransition) { - node.style[TRANSITION_PROP + PROPERTY_KEY] = 'none'; + blockTransitions(node, true); } if (blockAnimation) { - node.style[ANIMATION_PROP] = 'none 0s'; + blockAnimations(node, true); } return true; @@ -1597,22 +1584,43 @@ angular.module('ngAnimate', ['ng']) } if (elementData.blockTransition) { - node.style[TRANSITION_PROP + PROPERTY_KEY] = ''; - } - - if (elementData.blockAnimation) { - node.style[ANIMATION_PROP] = ''; + blockTransitions(node, false); } var activeClassName = ''; + var pendingClassName = ''; forEach(className.split(' '), function(klass, i) { - activeClassName += (i > 0 ? ' ' : '') + klass + '-active'; + var prefix = (i > 0 ? ' ' : '') + klass; + activeClassName += prefix + '-active'; + pendingClassName += prefix + '-pending'; }); - element.addClass(activeClassName); + var style = ''; + var appliedStyles = []; + var itemIndex = elementData.itemIndex; + var stagger = elementData.stagger; + var staggerTime = 0; + if (itemIndex > 0) { + var transitionStaggerDelay = 0; + if (stagger.transitionDelay > 0 && stagger.transitionDuration === 0) { + transitionStaggerDelay = stagger.transitionDelay * itemIndex; + } + + var animationStaggerDelay = 0; + if (stagger.animationDelay > 0 && stagger.animationDuration === 0) { + animationStaggerDelay = stagger.animationDelay * itemIndex; + appliedStyles.push(CSS_PREFIX + 'animation-play-state'); + } + + staggerTime = Math.round(Math.max(transitionStaggerDelay, animationStaggerDelay) * 100) / 100; + } + + if (!staggerTime) { + element.addClass(activeClassName); + } + var eventCacheKey = elementData.cacheKey + ' ' + activeClassName; var timings = getElementAnimationDetails(element, eventCacheKey); - var maxDuration = Math.max(timings.transitionDuration, timings.animationDuration); if (maxDuration === 0) { element.removeClass(activeClassName); @@ -1622,46 +1630,36 @@ angular.module('ngAnimate', ['ng']) } var maxDelay = Math.max(timings.transitionDelay, timings.animationDelay); - var stagger = elementData.stagger; - var itemIndex = elementData.itemIndex; var maxDelayTime = maxDelay * ONE_SECOND; - var style = '', appliedStyles = []; - if (timings.transitionDuration > 0) { - var propertyStyle = timings.transitionPropertyStyle; - if (propertyStyle.indexOf('all') == -1) { - style += CSS_PREFIX + 'transition-property: ' + propertyStyle + ';'; - style += CSS_PREFIX + 'transition-duration: ' + timings.transitionDurationStyle + ';'; - appliedStyles.push(CSS_PREFIX + 'transition-property'); - appliedStyles.push(CSS_PREFIX + 'transition-duration'); - } - } - - if (itemIndex > 0) { - if (stagger.transitionDelay > 0 && stagger.transitionDuration === 0) { - var delayStyle = timings.transitionDelayStyle; - style += CSS_PREFIX + 'transition-delay: ' + - prepareStaggerDelay(delayStyle, stagger.transitionDelay, itemIndex) + '; '; - appliedStyles.push(CSS_PREFIX + 'transition-delay'); - } - - if (stagger.animationDelay > 0 && stagger.animationDuration === 0) { - style += CSS_PREFIX + 'animation-delay: ' + - prepareStaggerDelay(timings.animationDelayStyle, stagger.animationDelay, itemIndex) + '; '; - appliedStyles.push(CSS_PREFIX + 'animation-delay'); - } - } - if (appliedStyles.length > 0) { //the element being animated may sometimes contain comment nodes in //the jqLite object, so we're safe to use a single variable to house //the styles since there is always only one element being animated var oldStyle = node.getAttribute('style') || ''; - node.setAttribute('style', oldStyle + '; ' + style); + if (oldStyle.charAt(oldStyle.length-1) !== ';') { + oldStyle += ';'; + } + node.setAttribute('style', oldStyle + ' ' + style); } var startTime = Date.now(); var css3AnimationEvents = ANIMATIONEND_EVENT + ' ' + TRANSITIONEND_EVENT; + var animationTime = (maxDelay + maxDuration) * CLOSING_TIME_BUFFER; + var totalTime = (staggerTime + animationTime) * ONE_SECOND; + + var staggerTimeout; + if (staggerTime > 0) { + element.addClass(pendingClassName); + staggerTimeout = $timeout(function() { + staggerTimeout = null; + element.addClass(activeClassName); + element.removeClass(pendingClassName); + if (timings.animationDuration > 0) { + blockAnimations(node, false); + } + }, staggerTime * ONE_SECOND, false); + } element.on(css3AnimationEvents, onAnimationProgress); elementData.closeAnimationFns.push(function() { @@ -1669,10 +1667,6 @@ angular.module('ngAnimate', ['ng']) activeAnimationComplete(); }); - var staggerTime = itemIndex * (Math.max(stagger.animationDelay, stagger.transitionDelay) || 0); - var animationTime = (maxDelay + maxDuration) * CLOSING_TIME_BUFFER; - var totalTime = (staggerTime + animationTime) * ONE_SECOND; - elementData.running++; animationCloseHandler(element, totalTime); return onEnd; @@ -1683,6 +1677,10 @@ angular.module('ngAnimate', ['ng']) function onEnd(cancelled) { element.off(css3AnimationEvents, onAnimationProgress); element.removeClass(activeClassName); + element.removeClass(pendingClassName); + if (staggerTimeout) { + $timeout.cancel(staggerTimeout); + } animateClose(element, className); var node = extractElementNode(element); for (var i in appliedStyles) { @@ -1712,13 +1710,12 @@ angular.module('ngAnimate', ['ng']) } } - 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 blockTransitions(node, bool) { + node.style[TRANSITION_PROP + PROPERTY_KEY] = bool ? 'none' : ''; + } + + function blockAnimations(node, bool) { + node.style[ANIMATION_PROP + ANIMATION_PLAYSTATE_KEY] = bool ? 'paused' : ''; } function animateBefore(animationEvent, element, className, calculationDecorator) { diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index 4e4cef9f..a8f67b10 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -5,6 +5,17 @@ describe("ngAnimate", function() { beforeEach(module('ngAnimate')); beforeEach(module('ngAnimateMock')); + function getMaxValue(prop, element, $window) { + var node = element[0]; + var cs = $window.getComputedStyle(node); + var prop0 = 'webkit' + prop.charAt(0).toUpperCase() + prop.substr(1); + var values = (cs[prop0] || cs[prop]).split(/\s*,\s*/); + var maxDelay = 0; + forEach(values, function(value) { + maxDelay = Math.max(parseFloat(value) || 0, maxDelay); + }); + return maxDelay; + } it("should disable animations on bootstrap for structural animations even after the first digest has passed", function() { var hasBeenAnimated = false; @@ -1161,7 +1172,7 @@ describe("ngAnimate", function() { ); - it("should stagger the items when the correct CSS class is provided", + it("should pause the playstate when performing a stagger animation", inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement) { if(!$sniffer.animations) return; @@ -1198,10 +1209,9 @@ describe("ngAnimate", function() { $animate.triggerReflow(); expect(elements[0].attr('style')).toBeFalsy(); - expect(elements[1].attr('style')).toMatch(/animation-delay: 0\.1\d*s/); - expect(elements[2].attr('style')).toMatch(/animation-delay: 0\.2\d*s/); - expect(elements[3].attr('style')).toMatch(/animation-delay: 0\.3\d*s/); - expect(elements[4].attr('style')).toMatch(/animation-delay: 0\.4\d*s/); + for(i = 1; i < 5; i++) { + expect(elements[i].attr('style')).toMatch(/animation-play-state:\s*paused/); + } //final closing timeout $timeout.flush(); @@ -1220,10 +1230,9 @@ describe("ngAnimate", function() { $timeout.verifyNoPendingTasks(); expect(elements[0].attr('style')).toBeFalsy(); - expect(elements[1].attr('style')).not.toMatch(/animation-delay: 0\.1\d*s/); - expect(elements[2].attr('style')).not.toMatch(/animation-delay: 0\.2\d*s/); - expect(elements[3].attr('style')).not.toMatch(/animation-delay: 0\.3\d*s/); - expect(elements[4].attr('style')).not.toMatch(/animation-delay: 0\.4\d*s/); + for(i=1;i<5;i++) { + expect(elements[i].attr('style')).not.toMatch(/animation-play-state:\s*paused/); + } })); @@ -1255,23 +1264,26 @@ describe("ngAnimate", function() { $rootScope.$digest(); expect(elements[0].attr('style')).toBeUndefined(); - expect(elements[1].attr('style')).toMatch(/animation:.*?none/); - expect(elements[2].attr('style')).toMatch(/animation:.*?none/); - expect(elements[3].attr('style')).toMatch(/animation:.*?none/); + for(i = 1; i < 4; i++) { + expect(elements[i].attr('style')).toMatch(/animation-play-state:\s*paused/); + } $animate.triggerReflow(); expect(elements[0].attr('style')).toBeUndefined(); - expect(elements[1].attr('style')).not.toMatch(/animation:.*?none/); - expect(elements[1].attr('style')).toMatch(/animation-delay: 0.2\d*s/); - expect(elements[2].attr('style')).not.toMatch(/animation:.*?none/); - expect(elements[2].attr('style')).toMatch(/animation-delay: 0.4\d*s/); - expect(elements[3].attr('style')).not.toMatch(/animation:.*?none/); - expect(elements[3].attr('style')).toMatch(/animation-delay: 0.6\d*s/); + for(i = 1; i < 4; i++) { + expect(elements[i].attr('style')).toMatch(/animation-play-state:\s*paused/); + } + + $timeout.flush(800); + + for(i = 1; i < 4; i++) { + expect(elements[i].attr('style')).not.toMatch(/animation-play-state/); + } })); it("should stagger items when multiple animation durations/delays are defined", - inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement) { + inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement, $window) { if(!$sniffer.transitions) return; @@ -1298,10 +1310,19 @@ describe("ngAnimate", function() { $rootScope.$digest(); $animate.triggerReflow(); - expect(elements[0].attr('style')).toBeFalsy(); - expect(elements[1].attr('style')).toMatch(/animation-delay: 1\.1\d*s,\s*2\.1\d*s/); - expect(elements[2].attr('style')).toMatch(/animation-delay: 1\.2\d*s,\s*2\.2\d*s/); - expect(elements[3].attr('style')).toMatch(/animation-delay: 1\.3\d*s,\s*2\.3\d*s/); + for(i = 1; i < 4; i++) { + expect(elements[i]).not.toHaveClass('ng-enter-active'); + expect(elements[i]).toHaveClass('ng-enter-pending'); + expect(getMaxValue('animationDelay', elements[i], $window)).toBe(2); + } + + $timeout.flush(300); + + for(i = 1; i < 4; i++) { + expect(elements[i]).toHaveClass('ng-enter-active'); + expect(elements[i]).not.toHaveClass('ng-enter-pending'); + expect(getMaxValue('animationDelay', elements[i], $window)).toBe(2); + } })); }); @@ -1577,7 +1598,7 @@ describe("ngAnimate", function() { })); it("should stagger the items when the correct CSS class is provided", - inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement) { + inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement, $browser) { if(!$sniffer.transitions) return; @@ -1612,11 +1633,8 @@ describe("ngAnimate", function() { $rootScope.$digest(); $animate.triggerReflow(); - expect(elements[0].attr('style')).toBeFalsy(); - expect(elements[1].attr('style')).toMatch(/transition-delay: 0\.1\d*s/); - expect(elements[2].attr('style')).toMatch(/transition-delay: 0\.2\d*s/); - expect(elements[3].attr('style')).toMatch(/transition-delay: 0\.3\d*s/); - expect(elements[4].attr('style')).toMatch(/transition-delay: 0\.4\d*s/); + expect($browser.deferredFns.length).toEqual(5); //4 staggers + 1 combined timeout + $timeout.flush(); for(i = 0; i < 5; i++) { dealoc(elements[i]); @@ -1629,16 +1647,12 @@ describe("ngAnimate", function() { $rootScope.$digest(); $animate.triggerReflow(); - expect(elements[0].attr('style')).toBeFalsy(); - expect(elements[1].attr('style')).not.toMatch(/transition-delay: 0\.1\d*s/); - expect(elements[2].attr('style')).not.toMatch(/transition-delay: 0\.2\d*s/); - expect(elements[3].attr('style')).not.toMatch(/transition-delay: 0\.3\d*s/); - expect(elements[4].attr('style')).not.toMatch(/transition-delay: 0\.4\d*s/); + expect($browser.deferredFns.length).toEqual(0); //no animation was triggered })); it("should stagger items when multiple transition durations/delays are defined", - inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement) { + inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement, $window) { if(!$sniffer.transitions) return; @@ -1665,11 +1679,19 @@ describe("ngAnimate", function() { $rootScope.$digest(); $animate.triggerReflow(); - expect(elements[0].attr('style')).toMatch(/transition-duration: 1\d*s,\s*3\d*s;/); - expect(elements[0].attr('style')).not.toContain('transition-delay'); - expect(elements[1].attr('style')).toMatch(/transition-delay: 2\.1\d*s,\s*4\.1\d*s/); - expect(elements[2].attr('style')).toMatch(/transition-delay: 2\.2\d*s,\s*4\.2\d*s/); - expect(elements[3].attr('style')).toMatch(/transition-delay: 2\.3\d*s,\s*4\.3\d*s/); + for(i = 1; i < 4; i++) { + expect(elements[i]).not.toHaveClass('ng-enter-active'); + expect(elements[i]).toHaveClass('ng-enter-pending'); + expect(getMaxValue('transitionDelay', elements[i], $window)).toBe(4); + } + + $timeout.flush(300); + + for(i = 1; i < 4; i++) { + expect(elements[i]).toHaveClass('ng-enter-active'); + expect(elements[i]).not.toHaveClass('ng-enter-pending'); + expect(getMaxValue('transitionDelay', elements[i], $window)).toBe(4); + } })); @@ -1811,20 +1833,22 @@ describe("ngAnimate", function() { $animate.triggerReflow(); //reflow expect(element.children().length).toBe(5); - for(i = 0; i < 5; i++) { - expect(kids[i].hasClass('ng-enter-active')).toBe(true); + for(i = 1; i < 5; i++) { + expect(kids[i]).not.toHaveClass('ng-enter-active'); + expect(kids[i]).toHaveClass('ng-enter-pending'); } - $timeout.flush(7500); + $timeout.flush(2000); - for(i = 0; i < 5; i++) { - expect(kids[i].hasClass('ng-enter-active')).toBe(true); + for(i = 1; i < 5; i++) { + expect(kids[i]).toHaveClass('ng-enter-active'); + expect(kids[i]).not.toHaveClass('ng-enter-pending'); } //(stagger * index) + (duration + delay) * 150% //0.5 * 4 + 5 * 1.5 = 9500; - //9500 - 7500 = 2000 - $timeout.flush(1999); //remove 1999 more + //9500 - 2000 - 7499 = 1 + $timeout.flush(7499); for(i = 0; i < 5; i++) { expect(kids[i].hasClass('ng-enter-active')).toBe(true); @@ -1837,6 +1861,52 @@ describe("ngAnimate", function() { } })); + it("should cancel all the existing stagger timers when the animation is cancelled", + inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $browser) { + + if (!$sniffer.transitions) return; + + ss.addRule('.entering-element.ng-enter', + '-webkit-transition:5s linear all;' + + 'transition:5s linear all;'); + + ss.addRule('.entering-element.ng-enter-stagger', + '-webkit-transition-delay:1s;' + + 'transition-delay:1s;'); + + var cancellations = []; + element = $compile(html('
'))($rootScope); + var kids = []; + for(var i = 0; i < 5; i++) { + kids.push(angular.element('
')); + cancellations.push($animate.enter(kids[i], element)); + } + $rootScope.$digest(); + + $animate.triggerReflow(); //reflow + expect(element.children().length).toBe(5); + + for(i = 1; i < 5; i++) { + expect(kids[i]).not.toHaveClass('ng-enter-active'); + expect(kids[i]).toHaveClass('ng-enter-pending'); + } + + expect($browser.deferredFns.length).toEqual(5); //4 staggers + 1 combined timeout + + forEach(cancellations, function(promise) { + $animate.cancel(promise); + }); + + for(i = 1; i < 5; i++) { + expect(kids[i]).not.toHaveClass('ng-enter'); + expect(kids[i]).not.toHaveClass('ng-enter-active'); + expect(kids[i]).not.toHaveClass('ng-enter-pending'); + } + + //the staggers are gone, but the global timeout remains + expect($browser.deferredFns.length).toEqual(1); + })); + it("should not allow the closing animation to close off a successive animation midway", inject(function($animate, $rootScope, $compile, $sniffer, $timeout) { @@ -1873,7 +1943,7 @@ describe("ngAnimate", function() { it("should apply staggering to both transitions and keyframe animations when used within the same animation", - inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement) { + inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement, $browser) { if(!$sniffer.transitions) return; @@ -1903,14 +1973,23 @@ describe("ngAnimate", function() { $rootScope.$digest(); $animate.triggerReflow(); + expect($browser.deferredFns.length).toEqual(3); //2 staggers + 1 combined timeout expect(elements[0].attr('style')).toBeFalsy(); + expect(elements[1].attr('style')).toMatch(/animation-play-state:\s*paused/); + expect(elements[2].attr('style')).toMatch(/animation-play-state:\s*paused/); - expect(elements[1].attr('style')).toMatch(/transition-delay:\s+1.1\d*/); - expect(elements[1].attr('style')).toMatch(/animation-delay: 1\.2\d*s,\s*2\.2\d*s/); + for(i = 1; i < 3; i++) { + expect(elements[i]).not.toHaveClass('ng-enter-active'); + expect(elements[i]).toHaveClass('ng-enter-pending'); + } - expect(elements[2].attr('style')).toMatch(/transition-delay:\s+1.2\d*/); - expect(elements[2].attr('style')).toMatch(/animation-delay: 1\.4\d*s,\s*2\.4\d*s/); + $timeout.flush(0.4 * 1000); + + for(i = 1; i < 3; i++) { + expect(elements[i]).toHaveClass('ng-enter-active'); + expect(elements[i]).not.toHaveClass('ng-enter-pending'); + } for(i = 0; i < 3; i++) { browserTrigger(elements[i],'transitionend', { timeStamp: Date.now() + 22000, elapsedTime: 22000 });