diff --git a/src/ng/animator.js b/src/ng/animator.js index a8064f5e29ff..97c9e0ed2a4e 100644 --- a/src/ng/animator.js +++ b/src/ng/animator.js @@ -46,14 +46,16 @@ * Keep in mind that if an animation is running, no child element of such animation can also be animated. * *
* + * + * + *+ * + * ngAnimate will first examine any CSS animation code and then fallback to using CSS transitions. + * * Upon DOM mutation, the setup class is added first, then the browser is allowed to reflow the content and then, * the start class is added to trigger the animation. The ngAnimate directive will automatically extract the duration * of the animation to determine when the animation ends. Once the animation is over then both CSS classes will be - * removed from the DOM. If a browser does not support CSS transitions then the animation will start and end + * removed from the DOM. If a browser does not support CSS transitions or CSS animations then the animation will start and end * immediately resulting in a DOM element that is at it's final state. This final state is when the DOM element - * has no CSS animation classes surrounding it. + * has no CSS transition/animation classes surrounding it. * *
* var ngModule = angular.module('YourApp', []); @@ -117,8 +152,8 @@ * * As you can see, the JavaScript code follows a similar template to the CSS3 animations. Once defined, the animation * can be used in the same way with the ngAnimate attribute. Keep in mind that, when using JavaScript-enabled - * animations, ngAnimate will also add in the same CSS classes that CSS-enabled animations do (even if you're using - * JavaScript animations) to animated the element, but it will not attempt to find any CSS3 transition duration value. + * animations, ngAnimate will also add in the same CSS classes that CSS-enabled animations do (even if you're not using + * CSS animations) to animated the element, but it will not attempt to find any CSS3 transition or animation duration/delay values. * It will instead close off the animation once the provided done function is executed. So it's important that you * make sure your animations remember to fire off the done function once the animations are complete. * @@ -258,6 +293,14 @@ var $AnimatorProvider = function() { // $window.setTimeout(beginAnimation, 0); this was causing the element not to animate // keep at 1 for animation dom rerender $window.setTimeout(beginAnimation, 1); + } + + function parseMaxTime(str) { + var total = 0, values = isString(str) ? str.split(/\s*,\s*/) : []; + forEach(values, function(value) { + total = Math.max(parseFloat(value) || 0, total); + }); + return total; }; function beginAnimation() { @@ -265,21 +308,45 @@ var $AnimatorProvider = function() { if (polyfillStart) { polyfillStart(element, done, memento); } else if (isFunction($window.getComputedStyle)) { - var vendorTransitionProp = $sniffer.vendorPrefix + 'Transition'; - var w3cTransitionProp = 'transition'; //one day all browsers will have this + //one day all browsers will have these properties + var w3cAnimationProp = 'animation'; + var w3cTransitionProp = 'transition'; - var durationKey = 'Duration'; - var duration = 0; + //but some still use vendor-prefixed styles + var vendorAnimationProp = $sniffer.vendorPrefix + 'Animation'; + var vendorTransitionProp = $sniffer.vendorPrefix + 'Transition'; + var durationKey = 'Duration', + delayKey = 'Delay', + animationIterationCountKey = 'IterationCount', + duration = 0; + //we want all the styles defined before and after + var ELEMENT_NODE = 1; forEach(element, function(element) { - if (element.nodeType == 1) { - var globalStyles = $window.getComputedStyle(element) || {}; - duration = Math.max( - parseFloat(globalStyles[w3cTransitionProp + durationKey]) || - parseFloat(globalStyles[vendorTransitionProp + durationKey]) || - 0, - duration); + if (element.nodeType == ELEMENT_NODE) { + var w3cProp = w3cTransitionProp, + vendorProp = vendorTransitionProp, + iterations = 1, + elementStyles = $window.getComputedStyle(element) || {}; + + //use CSS Animations over CSS Transitions + if(parseFloat(elementStyles[w3cAnimationProp + durationKey]) > 0 || + parseFloat(elementStyles[vendorAnimationProp + durationKey]) > 0) { + w3cProp = w3cAnimationProp; + vendorProp = vendorAnimationProp; + iterations = Math.max(parseInt(elementStyles[w3cProp + animationIterationCountKey]) || 0, + parseInt(elementStyles[vendorProp + animationIterationCountKey]) || 0, + iterations); + } + + var parsedDelay = Math.max(parseMaxTime(elementStyles[w3cProp + delayKey]), + parseMaxTime(elementStyles[vendorProp + delayKey])); + + var parsedDuration = Math.max(parseMaxTime(elementStyles[w3cProp + durationKey]), + parseMaxTime(elementStyles[vendorProp + durationKey])); + + duration = Math.max(parsedDelay + (iterations * parsedDuration), duration); } }); $window.setTimeout(done, duration * 1000); diff --git a/test/ng/animatorSpec.js b/test/ng/animatorSpec.js index 89677852ef2e..5a8ac1a07866 100644 --- a/test/ng/animatorSpec.js +++ b/test/ng/animatorSpec.js @@ -295,45 +295,200 @@ describe("$animator", function() { })); }); - describe("with css3", function() { + describe("with CSS3", function() { var window, animator, prefix, vendorPrefix; beforeEach(function() { module(function($animationProvider, $provide) { $provide.value('$window', window = angular.mock.createMockWindow()); return function($sniffer, _$rootElement_, $animator) { - vendorPrefix = '-' + $sniffer.vendorPrefix + '-'; + vendorPrefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-'; $rootElement = _$rootElement_; $animator.enabled(true); }; }) }); - it("should skip animations if disabled and run when enabled", + describe("Animations", function() { + it("should properly detect and make use of CSS Animations", + inject(function($animator, $rootScope, $compile, $sniffer) { + var style = 'animation: some_animation 4s linear 0s 1 alternate;' + + vendorPrefix + 'animation: some_animation 4s linear 0s 1 alternate;'; + element = $compile(html('1'))($rootScope); + var animator = $animator($rootScope, { + ngAnimate : '{show: \'inline-show\'}' + }); + + element.css('display','none'); + expect(element.css('display')).toBe('none'); + + animator.show(element); + if ($sniffer.supportsAnimations) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(4000).process(); + } + expect(element[0].style.display).toBe(''); + })); + + it("should properly detect and make use of CSS Animations with multiple iterations", + inject(function($animator, $rootScope, $compile, $sniffer) { + var style = 'animation-duration: 2s;' + + 'animation-iteration-count: 3;' + + vendorPrefix + 'animation-duration: 2s;' + + vendorPrefix + 'animation-iteration-count: 3;'; + element = $compile(html('1'))($rootScope); + var animator = $animator($rootScope, { + ngAnimate : '{show: \'inline-show\'}' + }); + + element.css('display','none'); + expect(element.css('display')).toBe('none'); + + animator.show(element); + if ($sniffer.supportsAnimations) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(6000).process(); + } + expect(element[0].style.display).toBe(''); + })); + + it("should fallback to the animation duration if an infinite iteration is provided", + inject(function($animator, $rootScope, $compile, $sniffer) { + var style = 'animation-duration: 2s;' + + 'animation-iteration-count: infinite;' + + vendorPrefix + 'animation-duration: 2s;' + + vendorPrefix + 'animation-iteration-count: infinite;'; + element = $compile(html('1'))($rootScope); + var animator = $animator($rootScope, { + ngAnimate : '{show: \'inline-show\'}' + }); + + element.css('display','none'); + expect(element.css('display')).toBe('none'); + + animator.show(element); + if ($sniffer.supportsAnimations) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(2000).process(); + } + expect(element[0].style.display).toBe(''); + })); + + it("should consider the animation delay is provided", + inject(function($animator, $rootScope, $compile, $sniffer) { + var style = 'animation-duration: 2s;' + + 'animation-delay: 10s;' + + 'animation-iteration-count: 5;' + + vendorPrefix + 'animation-duration: 2s;' + + vendorPrefix + 'animation-delay: 10s;' + + vendorPrefix + 'animation-iteration-count: 5;'; + element = $compile(html('1'))($rootScope); + var animator = $animator($rootScope, { + ngAnimate : '{show: \'inline-show\'}' + }); + + element.css('display','none'); + expect(element.css('display')).toBe('none'); + + animator.show(element); + if ($sniffer.supportsTransitions) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(20000).process(); + } + expect(element[0].style.display).toBe(''); + })); + + it("should skip animations if disabled and run when enabled", + inject(function($animator, $rootScope, $compile, $sniffer) { + $animator.enabled(false); + var style = 'animation: some_animation 2s linear 0s 1 alternate;' + + vendorPrefix + 'animation: some_animation 2s linear 0s 1 alternate;' + + element = $compile(html('1'))($rootScope); + var animator = $animator($rootScope, { + ngAnimate : '{show: \'inline-show\'}' + }); + element.css('display','none'); + expect(element.css('display')).toBe('none'); + animator.show(element); + expect(element[0].style.display).toBe(''); + })); + }); + + describe("Transitions", function() { + it("should skip transitions if disabled and run when enabled", + inject(function($animator, $rootScope, $compile, $sniffer) { + $animator.enabled(false); + element = $compile(html('1'))($rootScope); + var animator = $animator($rootScope, { + ngAnimate : '{show: \'inline-show\'}' + }); + + element.css('display','none'); + expect(element.css('display')).toBe('none'); + animator.show(element); + expect(element[0].style.display).toBe(''); + + $animator.enabled(true); + + element.css('display','none'); + expect(element.css('display')).toBe('none'); + + animator.show(element); + if ($sniffer.supportsTransitions) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(1000).process(); + } + expect(element[0].style.display).toBe(''); + })); + + it("should skip animations if disabled and run when enabled picking the longest specified duration", inject(function($animator, $rootScope, $compile, $sniffer) { - $animator.enabled(false); - element = $compile(html('1'))($rootScope); - var animator = $animator($rootScope, { - ngAnimate : '{show: \'inline-show\'}' - }); + $animator.enabled(true); + element = $compile(html('foo'))($rootScope); + var animator = $animator($rootScope, { + ngAnimate : '{show: \'inline-show\'}' + }); + element.css('display','none'); + animator.show(element); + if ($sniffer.supportsTransitions) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(2000).process(); + } + expect(element[0].style.display).toBe(''); + })); - element.css('display','none'); - expect(element.css('display')).toBe('none'); - animator.show(element); - expect(element[0].style.display).toBe(''); + it("should skip animations if disabled and run when enabled picking the longest specified duration/delay combination", + inject(function($animator, $rootScope, $compile, $sniffer) { + $animator.enabled(false); + element = $compile(html('foo'))($rootScope); - $animator.enabled(true); + var animator = $animator($rootScope, { + ngAnimate : '{show: \'inline-show\'}' + }); - element.css('display','none'); - expect(element.css('display')).toBe('none'); + element.css('display','none'); + expect(element.css('display')).toBe('none'); + animator.show(element); + expect(element[0].style.display).toBe(''); - animator.show(element); - if ($sniffer.transitions) { - window.setTimeout.expect(1).process(); - window.setTimeout.expect(1000).process(); - } - expect(element[0].style.display).toBe(''); - })); + $animator.enabled(true); + + element.css('display','none'); + expect(element.css('display')).toBe('none'); + + animator.show(element); + if ($sniffer.transitions) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(3000).process(); + return; + } + expect(element[0].style.display).toBe(''); + })); + }); }); describe('anmation evaluation', function () {