Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 1475787

Browse files
matskomhevery
authored andcommitted
feat(ngAnimate): Add support for CSS3 Animations with working delays and multiple durations
1 parent 88c3480 commit 1475787

File tree

2 files changed

+265
-43
lines changed

2 files changed

+265
-43
lines changed

src/ng/animator.js

+88-21
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,16 @@
4646
* Keep in mind that if an animation is running, no child element of such animation can also be animated.
4747
*
4848
* <h2>CSS-defined Animations</h2>
49-
* By default, ngAnimate attaches two CSS3 classes per animation event to the DOM element to achieve the animation.
50-
* It is up to you, the developer, to ensure that the animations take place using cross-browser CSS3 transitions.
51-
* All that is required is the following CSS code:
49+
* By default, ngAnimate attaches two CSS classes per animation event to the DOM element to achieve the animation.
50+
* It is up to you, the developer, to ensure that the animations take place using cross-browser CSS3 transitions as
51+
* well as CSS animations.
52+
*
53+
* The following code below demonstrates how to perform animations using **CSS transitions** with ngAnimate:
5254
*
5355
* <pre>
5456
* <style type="text/css">
5557
* /&#42;
56-
* The animate-enter prefix is the event name that you
58+
* The animate-enter CSS class is the event name that you
5759
* have provided within the ngAnimate attribute.
5860
* &#42;/
5961
* .animate-enter-setup {
@@ -81,16 +83,49 @@
8183
* <div ng-directive ng-animate="{enter: 'animate-enter'}"></div>
8284
* </pre>
8385
*
86+
* The following code below demonstrates how to perform animations using **CSS animations** with ngAnimate:
87+
*
88+
* <pre>
89+
* <style type="text/css">
90+
* .animate-enter-setup {
91+
* -webkit-animation: enter_sequence 1s linear;
92+
* -moz-animation: enter_sequence 1s linear;
93+
* -o-animation: enter_sequence 1s linear;
94+
* animation: enter_sequence 1s linear;
95+
* }
96+
* &#64-webkit-keyframes enter_sequence {
97+
* from { opacity:0; }
98+
* to { opacity:1; }
99+
* }
100+
* &#64-moz-keyframes enter_sequence {
101+
* from { opacity:0; }
102+
* to { opacity:1; }
103+
* }
104+
* &#64-o-keyframes enter_sequence {
105+
* from { opacity:0; }
106+
* to { opacity:1; }
107+
* }
108+
* &#64keyframes enter_sequence {
109+
* from { opacity:0; }
110+
* to { opacity:1; }
111+
* }
112+
* </style>
113+
*
114+
* <div ng-directive ng-animate="{enter: 'animate-enter'}"></div>
115+
* </pre>
116+
*
117+
* ngAnimate will first examine any CSS animation code and then fallback to using CSS transitions.
118+
*
84119
* Upon DOM mutation, the setup class is added first, then the browser is allowed to reflow the content and then,
85120
* the start class is added to trigger the animation. The ngAnimate directive will automatically extract the duration
86121
* of the animation to determine when the animation ends. Once the animation is over then both CSS classes will be
87-
* removed from the DOM. If a browser does not support CSS transitions then the animation will start and end
122+
* removed from the DOM. If a browser does not support CSS transitions or CSS animations then the animation will start and end
88123
* immediately resulting in a DOM element that is at it's final state. This final state is when the DOM element
89-
* has no CSS animation classes surrounding it.
124+
* has no CSS transition/animation classes surrounding it.
90125
*
91126
* <h2>JavaScript-defined Animations</h2>
92-
* In the event that you do not want to use CSS3 animations or if you wish to offer animations to browsers that do not
93-
* yet support them, then you can make use of JavaScript animations defined inside ngModule.
127+
* In the event that you do not want to use CSS3 transitions or CSS3 animations or if you wish to offer animations to browsers that do not
128+
* yet support them, then you can make use of JavaScript animations defined inside of your AngularJS module.
94129
*
95130
* <pre>
96131
* var ngModule = angular.module('YourApp', []);
@@ -117,8 +152,8 @@
117152
*
118153
* As you can see, the JavaScript code follows a similar template to the CSS3 animations. Once defined, the animation
119154
* can be used in the same way with the ngAnimate attribute. Keep in mind that, when using JavaScript-enabled
120-
* animations, ngAnimate will also add in the same CSS classes that CSS-enabled animations do (even if you're using
121-
* JavaScript animations) to animated the element, but it will not attempt to find any CSS3 transition duration value.
155+
* animations, ngAnimate will also add in the same CSS classes that CSS-enabled animations do (even if you're not using
156+
* CSS animations) to animated the element, but it will not attempt to find any CSS3 transition or animation duration/delay values.
122157
* It will instead close off the animation once the provided done function is executed. So it's important that you
123158
* make sure your animations remember to fire off the done function once the animations are complete.
124159
*
@@ -258,28 +293,60 @@ var $AnimatorProvider = function() {
258293
// $window.setTimeout(beginAnimation, 0); this was causing the element not to animate
259294
// keep at 1 for animation dom rerender
260295
$window.setTimeout(beginAnimation, 1);
296+
}
297+
298+
function parseMaxTime(str) {
299+
var total = 0, values = isString(str) ? str.split(/\s*,\s*/) : [];
300+
forEach(values, function(value) {
301+
total = Math.max(parseFloat(value) || 0, total);
302+
});
303+
return total;
261304
};
262305

263306
function beginAnimation() {
264307
element.addClass(startClass);
265308
if (polyfillStart) {
266309
polyfillStart(element, done, memento);
267310
} else if (isFunction($window.getComputedStyle)) {
268-
var vendorTransitionProp = $sniffer.vendorPrefix + 'Transition';
269-
var w3cTransitionProp = 'transition'; //one day all browsers will have this
311+
//one day all browsers will have these properties
312+
var w3cAnimationProp = 'animation';
313+
var w3cTransitionProp = 'transition';
270314

271-
var durationKey = 'Duration';
272-
var duration = 0;
315+
//but some still use vendor-prefixed styles
316+
var vendorAnimationProp = $sniffer.vendorPrefix + 'Animation';
317+
var vendorTransitionProp = $sniffer.vendorPrefix + 'Transition';
273318

319+
var durationKey = 'Duration',
320+
delayKey = 'Delay',
321+
animationIterationCountKey = 'IterationCount',
322+
duration = 0;
323+
274324
//we want all the styles defined before and after
325+
var ELEMENT_NODE = 1;
275326
forEach(element, function(element) {
276-
if (element.nodeType == 1) {
277-
var globalStyles = $window.getComputedStyle(element) || {};
278-
duration = Math.max(
279-
parseFloat(globalStyles[w3cTransitionProp + durationKey]) ||
280-
parseFloat(globalStyles[vendorTransitionProp + durationKey]) ||
281-
0,
282-
duration);
327+
if (element.nodeType == ELEMENT_NODE) {
328+
var w3cProp = w3cTransitionProp,
329+
vendorProp = vendorTransitionProp,
330+
iterations = 1,
331+
elementStyles = $window.getComputedStyle(element) || {};
332+
333+
//use CSS Animations over CSS Transitions
334+
if(parseFloat(elementStyles[w3cAnimationProp + durationKey]) > 0 ||
335+
parseFloat(elementStyles[vendorAnimationProp + durationKey]) > 0) {
336+
w3cProp = w3cAnimationProp;
337+
vendorProp = vendorAnimationProp;
338+
iterations = Math.max(parseInt(elementStyles[w3cProp + animationIterationCountKey]) || 0,
339+
parseInt(elementStyles[vendorProp + animationIterationCountKey]) || 0,
340+
iterations);
341+
}
342+
343+
var parsedDelay = Math.max(parseMaxTime(elementStyles[w3cProp + delayKey]),
344+
parseMaxTime(elementStyles[vendorProp + delayKey]));
345+
346+
var parsedDuration = Math.max(parseMaxTime(elementStyles[w3cProp + durationKey]),
347+
parseMaxTime(elementStyles[vendorProp + durationKey]));
348+
349+
duration = Math.max(parsedDelay + (iterations * parsedDuration), duration);
283350
}
284351
});
285352
$window.setTimeout(done, duration * 1000);

test/ng/animatorSpec.js

+177-22
Original file line numberDiff line numberDiff line change
@@ -295,45 +295,200 @@ describe("$animator", function() {
295295
}));
296296
});
297297

298-
describe("with css3", function() {
298+
describe("with CSS3", function() {
299299
var window, animator, prefix, vendorPrefix;
300300

301301
beforeEach(function() {
302302
module(function($animationProvider, $provide) {
303303
$provide.value('$window', window = angular.mock.createMockWindow());
304304
return function($sniffer, _$rootElement_, $animator) {
305-
vendorPrefix = '-' + $sniffer.vendorPrefix + '-';
305+
vendorPrefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-';
306306
$rootElement = _$rootElement_;
307307
$animator.enabled(true);
308308
};
309309
})
310310
});
311311

312-
it("should skip animations if disabled and run when enabled",
312+
describe("Animations", function() {
313+
it("should properly detect and make use of CSS Animations",
314+
inject(function($animator, $rootScope, $compile, $sniffer) {
315+
var style = 'animation: some_animation 4s linear 0s 1 alternate;' +
316+
vendorPrefix + 'animation: some_animation 4s linear 0s 1 alternate;';
317+
element = $compile(html('<div style="' + style + '">1</div>'))($rootScope);
318+
var animator = $animator($rootScope, {
319+
ngAnimate : '{show: \'inline-show\'}'
320+
});
321+
322+
element.css('display','none');
323+
expect(element.css('display')).toBe('none');
324+
325+
animator.show(element);
326+
if ($sniffer.supportsAnimations) {
327+
window.setTimeout.expect(1).process();
328+
window.setTimeout.expect(4000).process();
329+
}
330+
expect(element[0].style.display).toBe('');
331+
}));
332+
333+
it("should properly detect and make use of CSS Animations with multiple iterations",
334+
inject(function($animator, $rootScope, $compile, $sniffer) {
335+
var style = 'animation-duration: 2s;' +
336+
'animation-iteration-count: 3;' +
337+
vendorPrefix + 'animation-duration: 2s;' +
338+
vendorPrefix + 'animation-iteration-count: 3;';
339+
element = $compile(html('<div style="' + style + '">1</div>'))($rootScope);
340+
var animator = $animator($rootScope, {
341+
ngAnimate : '{show: \'inline-show\'}'
342+
});
343+
344+
element.css('display','none');
345+
expect(element.css('display')).toBe('none');
346+
347+
animator.show(element);
348+
if ($sniffer.supportsAnimations) {
349+
window.setTimeout.expect(1).process();
350+
window.setTimeout.expect(6000).process();
351+
}
352+
expect(element[0].style.display).toBe('');
353+
}));
354+
355+
it("should fallback to the animation duration if an infinite iteration is provided",
356+
inject(function($animator, $rootScope, $compile, $sniffer) {
357+
var style = 'animation-duration: 2s;' +
358+
'animation-iteration-count: infinite;' +
359+
vendorPrefix + 'animation-duration: 2s;' +
360+
vendorPrefix + 'animation-iteration-count: infinite;';
361+
element = $compile(html('<div style="' + style + '">1</div>'))($rootScope);
362+
var animator = $animator($rootScope, {
363+
ngAnimate : '{show: \'inline-show\'}'
364+
});
365+
366+
element.css('display','none');
367+
expect(element.css('display')).toBe('none');
368+
369+
animator.show(element);
370+
if ($sniffer.supportsAnimations) {
371+
window.setTimeout.expect(1).process();
372+
window.setTimeout.expect(2000).process();
373+
}
374+
expect(element[0].style.display).toBe('');
375+
}));
376+
377+
it("should consider the animation delay is provided",
378+
inject(function($animator, $rootScope, $compile, $sniffer) {
379+
var style = 'animation-duration: 2s;' +
380+
'animation-delay: 10s;' +
381+
'animation-iteration-count: 5;' +
382+
vendorPrefix + 'animation-duration: 2s;' +
383+
vendorPrefix + 'animation-delay: 10s;' +
384+
vendorPrefix + 'animation-iteration-count: 5;';
385+
element = $compile(html('<div style="' + style + '">1</div>'))($rootScope);
386+
var animator = $animator($rootScope, {
387+
ngAnimate : '{show: \'inline-show\'}'
388+
});
389+
390+
element.css('display','none');
391+
expect(element.css('display')).toBe('none');
392+
393+
animator.show(element);
394+
if ($sniffer.supportsTransitions) {
395+
window.setTimeout.expect(1).process();
396+
window.setTimeout.expect(20000).process();
397+
}
398+
expect(element[0].style.display).toBe('');
399+
}));
400+
401+
it("should skip animations if disabled and run when enabled",
402+
inject(function($animator, $rootScope, $compile, $sniffer) {
403+
$animator.enabled(false);
404+
var style = 'animation: some_animation 2s linear 0s 1 alternate;' +
405+
vendorPrefix + 'animation: some_animation 2s linear 0s 1 alternate;'
406+
407+
element = $compile(html('<div style="' + style + '">1</div>'))($rootScope);
408+
var animator = $animator($rootScope, {
409+
ngAnimate : '{show: \'inline-show\'}'
410+
});
411+
element.css('display','none');
412+
expect(element.css('display')).toBe('none');
413+
animator.show(element);
414+
expect(element[0].style.display).toBe('');
415+
}));
416+
});
417+
418+
describe("Transitions", function() {
419+
it("should skip transitions if disabled and run when enabled",
420+
inject(function($animator, $rootScope, $compile, $sniffer) {
421+
$animator.enabled(false);
422+
element = $compile(html('<div style="' + vendorPrefix + 'transition: 1s linear all">1</div>'))($rootScope);
423+
var animator = $animator($rootScope, {
424+
ngAnimate : '{show: \'inline-show\'}'
425+
});
426+
427+
element.css('display','none');
428+
expect(element.css('display')).toBe('none');
429+
animator.show(element);
430+
expect(element[0].style.display).toBe('');
431+
432+
$animator.enabled(true);
433+
434+
element.css('display','none');
435+
expect(element.css('display')).toBe('none');
436+
437+
animator.show(element);
438+
if ($sniffer.supportsTransitions) {
439+
window.setTimeout.expect(1).process();
440+
window.setTimeout.expect(1000).process();
441+
}
442+
expect(element[0].style.display).toBe('');
443+
}));
444+
445+
it("should skip animations if disabled and run when enabled picking the longest specified duration",
313446
inject(function($animator, $rootScope, $compile, $sniffer) {
314-
$animator.enabled(false);
315-
element = $compile(html('<div style="' + vendorPrefix + 'transition: 1s linear all">1</div>'))($rootScope);
316-
var animator = $animator($rootScope, {
317-
ngAnimate : '{show: \'inline-show\'}'
318-
});
447+
$animator.enabled(true);
448+
element = $compile(html('<div style="' + vendorPrefix + 'transition-duration: 1s, 2000ms, 1s; ' + vendorPrefix + 'transition-property: height, left, opacity">foo</div>'))($rootScope);
449+
var animator = $animator($rootScope, {
450+
ngAnimate : '{show: \'inline-show\'}'
451+
});
452+
element.css('display','none');
453+
animator.show(element);
454+
if ($sniffer.supportsTransitions) {
455+
window.setTimeout.expect(1).process();
456+
window.setTimeout.expect(2000).process();
457+
}
458+
expect(element[0].style.display).toBe('');
459+
}));
319460

320-
element.css('display','none');
321-
expect(element.css('display')).toBe('none');
322-
animator.show(element);
323-
expect(element[0].style.display).toBe('');
461+
it("should skip animations if disabled and run when enabled picking the longest specified duration/delay combination",
462+
inject(function($animator, $rootScope, $compile, $sniffer) {
463+
$animator.enabled(false);
464+
element = $compile(html('<div style="' + vendorPrefix +
465+
'transition-duration: 1s, 0s, 1s; ' + vendorPrefix +
466+
'transition-delay: 2s, 1000ms, 2s; ' + vendorPrefix +
467+
'transition-property: height, left, opacity">foo</div>'))($rootScope);
324468

325-
$animator.enabled(true);
469+
var animator = $animator($rootScope, {
470+
ngAnimate : '{show: \'inline-show\'}'
471+
});
326472

327-
element.css('display','none');
328-
expect(element.css('display')).toBe('none');
473+
element.css('display','none');
474+
expect(element.css('display')).toBe('none');
475+
animator.show(element);
476+
expect(element[0].style.display).toBe('');
329477

330-
animator.show(element);
331-
if ($sniffer.transitions) {
332-
window.setTimeout.expect(1).process();
333-
window.setTimeout.expect(1000).process();
334-
}
335-
expect(element[0].style.display).toBe('');
336-
}));
478+
$animator.enabled(true);
479+
480+
element.css('display','none');
481+
expect(element.css('display')).toBe('none');
482+
483+
animator.show(element);
484+
if ($sniffer.transitions) {
485+
window.setTimeout.expect(1).process();
486+
window.setTimeout.expect(3000).process();
487+
return;
488+
}
489+
expect(element[0].style.display).toBe('');
490+
}));
491+
});
337492
});
338493

339494
describe('anmation evaluation', function () {

0 commit comments

Comments
 (0)