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,
StringWrapper,
RegExpWrapper,
NumberWrapper
NumberWrapper,
isPresent
} from 'angular2/src/core/facade/lang';
import {Math} from 'angular2/src/core/facade/math';
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 */
completed: boolean = false;
private _stringPrefix: string = '';
/** total amount of time that the animation should take including delay */
get totalTime(): number {
let delay = this.computedDelay != null ? this.computedDelay : 0;
@ -47,6 +50,7 @@ export class Animation {
constructor(public element: HTMLElement, public data: CssAnimationOptions,
public browserDetails: BrowserDetails) {
this.startTime = DateWrapper.toMillis(DateWrapper.now());
this._stringPrefix = DOM.getAnimationPrefix();
this.setup();
this.wait(timestamp => this.start());
}
@ -77,11 +81,14 @@ export class Animation {
if (this.data.toStyles != null) this.applyStyles(this.data.toStyles);
var computedStyles = DOM.getComputedStyle(this.element);
this.computedDelay =
Math.max(this.parseDurationString(computedStyles.getPropertyValue('transition-delay')),
this.parseDurationString(this.element.style.getPropertyValue('transition-delay')));
this.computedDuration = Math.max(
this.parseDurationString(computedStyles.getPropertyValue('transition-duration')),
this.parseDurationString(this.element.style.getPropertyValue('transition-duration')));
Math.max(this.parseDurationString(
computedStyles.getPropertyValue(this._stringPrefix + 'transition-delay')),
this.parseDurationString(
this.element.style.getPropertyValue(this._stringPrefix + 'transition-delay')));
this.computedDuration = Math.max(this.parseDurationString(computedStyles.getPropertyValue(
this._stringPrefix + 'transition-duration')),
this.parseDurationString(this.element.style.getPropertyValue(
this._stringPrefix + 'transition-duration')));
this.addEvents();
}
@ -91,7 +98,12 @@ export class Animation {
*/
applyStyles(styles: StringMap<string, any>): void {
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 {
if (this.totalTime > 0) {
this.eventClearFunctions.push(DOM.onAndCancel(
this.element, 'transitionend', (event: any) => this.handleAnimationEvent(event)));
this.element, DOM.getTransitionEnd(), (event: any) => this.handleAnimationEvent(event)));
} else {
this.handleAnimationCompleted();
}

View File

@ -139,4 +139,7 @@ export class DomAdapter {
requestAnimationFrame(callback): number { throw _abstract(); }
cancelAnimationFrame(id) { 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 {isPresent, isFunction} from 'angular2/src/core/facade/lang';
import {ListWrapper, StringMapWrapper} from 'angular2/src/core/facade/collection';
import {isPresent, isFunction, StringWrapper} from 'angular2/src/core/facade/lang';
import {DomAdapter} from './dom_adapter';
/**
* Provides DOM operations in any browser environment.
*/
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(); }
resolveAndSetHref(el: HTMLAnchorElement, baseUrl: string, href: string) {
el.href = href == null ? baseUrl : baseUrl + '/../' + href;
@ -37,4 +70,11 @@ export class GenericBrowserDomAdapter extends DomAdapter {
supportsNativeShadowDOM(): boolean {
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() {
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); }
cancelAnimationFrame(id: number) { clearTimeout(id); }
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

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 {DOM} from 'angular2/src/core/dom/dom_adapter';
export function main() {
describe("AnimationBuilder", () => {
@ -54,8 +55,13 @@ export function main() {
var runner = animateCss.start(element);
runner.flush();
if (DOM.supportsAnimation()) {
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 => {
@ -71,12 +77,18 @@ export function main() {
it('should support duration and delay defined in CSS', inject([AnimationBuilder], (animate) => {
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);
runner.flush();
expect(runner.computedDuration).toEqual(500);
expect(runner.computedDelay).toEqual(250);
if (DOM.supportsAnimation()) {
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) => {
@ -108,11 +120,15 @@ export function main() {
runner.flush();
if (DOM.supportsAnimation()) {
expect(callback).not.toHaveBeenCalled();
runner.handleAnimationCompleted();
expect(callback).toHaveBeenCalled();
} else {
expect(callback).toHaveBeenCalled();
}
}));
});