feat(animate): cross-browser compatibility

Closes #4243
This commit is contained in:
Marc Laval 2015-09-18 00:49:56 +02:00
parent 4f56a01b3b
commit bffa2cb59b
6 changed files with 105 additions and 19 deletions

View File

@ -2,7 +2,8 @@ import {
DateWrapper, DateWrapper,
StringWrapper, StringWrapper,
RegExpWrapper, RegExpWrapper,
NumberWrapper NumberWrapper,
isPresent
} from 'angular2/src/core/facade/lang'; } from 'angular2/src/core/facade/lang';
import {Math} from 'angular2/src/core/facade/math'; import {Math} from 'angular2/src/core/facade/math';
import {camelCaseToDashCase} from 'angular2/src/core/render/dom/util'; import {camelCaseToDashCase} from 'angular2/src/core/render/dom/util';
@ -31,6 +32,8 @@ export class Animation {
/** flag used to track whether or not the animation has finished */ /** flag used to track whether or not the animation has finished */
completed: boolean = false; completed: boolean = false;
private _stringPrefix: string = '';
/** total amount of time that the animation should take including delay */ /** total amount of time that the animation should take including delay */
get totalTime(): number { get totalTime(): number {
let delay = this.computedDelay != null ? this.computedDelay : 0; let delay = this.computedDelay != null ? this.computedDelay : 0;
@ -47,6 +50,7 @@ export class Animation {
constructor(public element: HTMLElement, public data: CssAnimationOptions, constructor(public element: HTMLElement, public data: CssAnimationOptions,
public browserDetails: BrowserDetails) { public browserDetails: BrowserDetails) {
this.startTime = DateWrapper.toMillis(DateWrapper.now()); this.startTime = DateWrapper.toMillis(DateWrapper.now());
this._stringPrefix = DOM.getAnimationPrefix();
this.setup(); this.setup();
this.wait(timestamp => this.start()); this.wait(timestamp => this.start());
} }
@ -77,11 +81,14 @@ export class Animation {
if (this.data.toStyles != null) this.applyStyles(this.data.toStyles); if (this.data.toStyles != null) this.applyStyles(this.data.toStyles);
var computedStyles = DOM.getComputedStyle(this.element); var computedStyles = DOM.getComputedStyle(this.element);
this.computedDelay = this.computedDelay =
Math.max(this.parseDurationString(computedStyles.getPropertyValue('transition-delay')), Math.max(this.parseDurationString(
this.parseDurationString(this.element.style.getPropertyValue('transition-delay'))); computedStyles.getPropertyValue(this._stringPrefix + 'transition-delay')),
this.computedDuration = Math.max( this.parseDurationString(
this.parseDurationString(computedStyles.getPropertyValue('transition-duration')), this.element.style.getPropertyValue(this._stringPrefix + 'transition-delay')));
this.parseDurationString(this.element.style.getPropertyValue('transition-duration'))); this.computedDuration = Math.max(this.parseDurationString(computedStyles.getPropertyValue(
this._stringPrefix + 'transition-duration')),
this.parseDurationString(this.element.style.getPropertyValue(
this._stringPrefix + 'transition-duration')));
this.addEvents(); this.addEvents();
} }
@ -91,7 +98,12 @@ export class Animation {
*/ */
applyStyles(styles: StringMap<string, any>): void { applyStyles(styles: StringMap<string, any>): void {
StringMapWrapper.forEach(styles, (value, key) => { StringMapWrapper.forEach(styles, (value, key) => {
DOM.setStyle(this.element, camelCaseToDashCase(key), value.toString()); var dashCaseKey = camelCaseToDashCase(key);
if (isPresent(DOM.getStyle(this.element, dashCaseKey))) {
DOM.setStyle(this.element, dashCaseKey, value.toString());
} else {
DOM.setStyle(this.element, this._stringPrefix + dashCaseKey, value.toString());
}
}); });
} }
@ -117,7 +129,7 @@ export class Animation {
addEvents(): void { addEvents(): void {
if (this.totalTime > 0) { if (this.totalTime > 0) {
this.eventClearFunctions.push(DOM.onAndCancel( this.eventClearFunctions.push(DOM.onAndCancel(
this.element, 'transitionend', (event: any) => this.handleAnimationEvent(event))); this.element, DOM.getTransitionEnd(), (event: any) => this.handleAnimationEvent(event)));
} else { } else {
this.handleAnimationCompleted(); this.handleAnimationCompleted();
} }

View File

@ -139,4 +139,7 @@ export class DomAdapter {
requestAnimationFrame(callback): number { throw _abstract(); } requestAnimationFrame(callback): number { throw _abstract(); }
cancelAnimationFrame(id) { throw _abstract(); } cancelAnimationFrame(id) { throw _abstract(); }
performanceNow(): number { throw _abstract(); } performanceNow(): number { throw _abstract(); }
getAnimationPrefix(): string { throw _abstract(); }
getTransitionEnd(): string { throw _abstract(); }
supportsAnimation(): boolean { throw _abstract(); }
} }

View File

@ -1,11 +1,44 @@
import {ListWrapper} from 'angular2/src/core/facade/collection'; import {ListWrapper, StringMapWrapper} from 'angular2/src/core/facade/collection';
import {isPresent, isFunction} from 'angular2/src/core/facade/lang'; import {isPresent, isFunction, StringWrapper} from 'angular2/src/core/facade/lang';
import {DomAdapter} from './dom_adapter'; import {DomAdapter} from './dom_adapter';
/** /**
* Provides DOM operations in any browser environment. * Provides DOM operations in any browser environment.
*/ */
export class GenericBrowserDomAdapter extends DomAdapter { export class GenericBrowserDomAdapter extends DomAdapter {
private _animationPrefix: string = null;
private _transitionEnd: string = null;
constructor() {
super();
try {
var element = this.createElement('div', this.defaultDoc());
if (isPresent(this.getStyle(element, 'animationName'))) {
this._animationPrefix = '';
} else {
var domPrefixes = ['Webkit', 'Moz', 'O', 'ms'];
for (var i = 0; i < domPrefixes.length; i++) {
if (isPresent(this.getStyle(element, domPrefixes[i] + 'AnimationName'))) {
this._animationPrefix = '-' + StringWrapper.toLowerCase(domPrefixes[i]) + '-';
break;
}
}
}
var transEndEventNames = {
WebkitTransition: 'webkitTransitionEnd',
MozTransition: 'transitionend',
OTransition: 'oTransitionEnd otransitionend',
transition: 'transitionend'
};
StringMapWrapper.forEach(transEndEventNames, (value, key) => {
if (isPresent(this.getStyle(element, key))) {
this._transitionEnd = value;
}
});
} catch (e) {
this._animationPrefix = null;
this._transitionEnd = null;
}
}
getDistributedNodes(el: HTMLElement): Node[] { return (<any>el).getDistributedNodes(); } getDistributedNodes(el: HTMLElement): Node[] { return (<any>el).getDistributedNodes(); }
resolveAndSetHref(el: HTMLAnchorElement, baseUrl: string, href: string) { resolveAndSetHref(el: HTMLAnchorElement, baseUrl: string, href: string) {
el.href = href == null ? baseUrl : baseUrl + '/../' + href; el.href = href == null ? baseUrl : baseUrl + '/../' + href;
@ -37,4 +70,11 @@ export class GenericBrowserDomAdapter extends DomAdapter {
supportsNativeShadowDOM(): boolean { supportsNativeShadowDOM(): boolean {
return isFunction((<any>this.defaultDoc().body).createShadowRoot); return isFunction((<any>this.defaultDoc().body).createShadowRoot);
} }
getAnimationPrefix(): string {
return isPresent(this._animationPrefix) ? this._animationPrefix : "";
}
getTransitionEnd(): string { return isPresent(this._transitionEnd) ? this._transitionEnd : ""; }
supportsAnimation(): boolean {
return isPresent(this._animationPrefix) && isPresent(this._transitionEnd);
}
} }

View File

@ -435,4 +435,16 @@ class Html5LibDomAdapter implements DomAdapter {
performanceNow() { performanceNow() {
throw 'not implemented'; throw 'not implemented';
} }
getAnimationPrefix() {
throw 'not implemented';
}
getTransitionEnd() {
throw 'not implemented';
}
supportsAnimation() {
throw 'not implemented';
}
} }

View File

@ -552,6 +552,9 @@ export class Parse5DomAdapter extends DomAdapter {
requestAnimationFrame(callback): number { return setTimeout(callback, 0); } requestAnimationFrame(callback): number { return setTimeout(callback, 0); }
cancelAnimationFrame(id: number) { clearTimeout(id); } cancelAnimationFrame(id: number) { clearTimeout(id); }
performanceNow(): number { return DateWrapper.toMillis(DateWrapper.now()); } performanceNow(): number { return DateWrapper.toMillis(DateWrapper.now()); }
getAnimationPrefix(): string { return ''; }
getTransitionEnd(): string { return 'transitionend'; }
supportsAnimation(): boolean { return true; }
} }
// TODO: build a proper list, this one is all the keys of a HTMLInputElement // TODO: build a proper list, this one is all the keys of a HTMLInputElement

View File

@ -1,5 +1,6 @@
import {el, describe, it, expect, inject, SpyObject} from 'angular2/test_lib'; import {el, describe, it, iit, expect, inject, SpyObject} from 'angular2/test_lib';
import {AnimationBuilder} from 'angular2/src/animate/animation_builder'; import {AnimationBuilder} from 'angular2/src/animate/animation_builder';
import {DOM} from 'angular2/src/core/dom/dom_adapter';
export function main() { export function main() {
describe("AnimationBuilder", () => { describe("AnimationBuilder", () => {
@ -54,8 +55,13 @@ export function main() {
var runner = animateCss.start(element); var runner = animateCss.start(element);
runner.flush(); runner.flush();
expect(runner.computedDelay).toBe(100); if (DOM.supportsAnimation()) {
expect(runner.computedDuration).toBe(200); expect(runner.computedDelay).toBe(100);
expect(runner.computedDuration).toBe(200);
} else {
expect(runner.computedDelay).toBe(0);
expect(runner.computedDuration).toBe(0);
}
})); }));
it('should support from styles', inject([AnimationBuilder], animate => { it('should support from styles', inject([AnimationBuilder], animate => {
@ -71,12 +77,18 @@ export function main() {
it('should support duration and delay defined in CSS', inject([AnimationBuilder], (animate) => { it('should support duration and delay defined in CSS', inject([AnimationBuilder], (animate) => {
var animateCss = animate.css(); var animateCss = animate.css();
var element = el('<div style="transition: 0.5s ease 250ms;"></div>'); var element =
el(`<div style="${DOM.getAnimationPrefix()}transition: 0.5s ease 250ms;"></div>`);
var runner = animateCss.start(element); var runner = animateCss.start(element);
runner.flush(); runner.flush();
expect(runner.computedDuration).toEqual(500); if (DOM.supportsAnimation()) {
expect(runner.computedDelay).toEqual(250); expect(runner.computedDelay).toBe(250);
expect(runner.computedDuration).toBe(500);
} else {
expect(runner.computedDelay).toEqual(0);
expect(runner.computedDuration).toEqual(0);
}
})); }));
it('should add classes', inject([AnimationBuilder], (animate) => { it('should add classes', inject([AnimationBuilder], (animate) => {
@ -108,11 +120,15 @@ export function main() {
runner.flush(); runner.flush();
expect(callback).not.toHaveBeenCalled(); if (DOM.supportsAnimation()) {
expect(callback).not.toHaveBeenCalled();
runner.handleAnimationCompleted(); runner.handleAnimationCompleted();
expect(callback).toHaveBeenCalled(); expect(callback).toHaveBeenCalled();
} else {
expect(callback).toHaveBeenCalled();
}
})); }));
}); });