fix(animations): only use the WA-polyfill alongside AnimationBuilder (#22143)

This patch removes the need to include the Web Animations API Polyfill
(web-animations-js) as a dependency. Angular will now fallback to using
CSS Keyframes in the event that `element.animate` is no longer supported
by the browser.

In the event that an application does use `AnimationBuilder` then the
web-animations-js polyfill is required to enable programmatic,
position-based access to an animation.

Closes #17496

PR Close #22143
This commit is contained in:
Matias Niemelä
2018-02-08 15:01:43 -08:00
committed by Victor Berchet
parent 9eecb0b27f
commit b2f366b3b7
23 changed files with 1680 additions and 81 deletions

View File

@ -10,5 +10,7 @@ export {AnimationStyleNormalizer as ɵAnimationStyleNormalizer, NoopAnimationSty
export {WebAnimationsStyleNormalizer as ɵWebAnimationsStyleNormalizer} from './dsl/style_normalization/web_animations_style_normalizer';
export {NoopAnimationDriver as ɵNoopAnimationDriver} from './render/animation_driver';
export {AnimationEngine as ɵAnimationEngine} from './render/animation_engine_next';
export {CssKeyframesDriver as ɵCssKeyframesDriver} from './render/css_keyframes/css_keyframes_driver';
export {CssKeyframesPlayer as ɵCssKeyframesPlayer} from './render/css_keyframes/css_keyframes_player';
export {WebAnimationsDriver as ɵWebAnimationsDriver, supportsWebAnimations as ɵsupportsWebAnimations} from './render/web_animations/web_animations_driver';
export {WebAnimationsPlayer as ɵWebAnimationsPlayer} from './render/web_animations/web_animations_player';

View File

@ -33,7 +33,8 @@ export class NoopAnimationDriver implements AnimationDriver {
animate(
element: any, keyframes: {[key: string]: string | number}[], duration: number, delay: number,
easing: string, previousPlayers: any[] = []): AnimationPlayer {
easing: string, previousPlayers: any[] = [],
scrubberAccessRequested?: boolean): AnimationPlayer {
return new NoopAnimationPlayer(duration, delay);
}
}
@ -56,5 +57,5 @@ export abstract class AnimationDriver {
abstract animate(
element: any, keyframes: {[key: string]: string | number}[], duration: number, delay: number,
easing?: string|null, previousPlayers?: any[]): any;
easing?: string|null, previousPlayers?: any[], scrubberAccessRequested?: boolean): any;
}

View File

@ -0,0 +1,151 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AnimationPlayer, ɵStyleData} from '@angular/animations';
import {allowPreviousPlayerStylesMerge, balancePreviousStylesIntoKeyframes, computeStyle} from '../../util';
import {AnimationDriver} from '../animation_driver';
import {containsElement, invokeQuery, matchesElement, validateStyleProperty} from '../shared';
import {CssKeyframesPlayer} from './css_keyframes_player';
import {DirectStylePlayer} from './direct_style_player';
const KEYFRAMES_NAME_PREFIX = 'gen_css_kf_';
const TAB_SPACE = ' ';
export class CssKeyframesDriver implements AnimationDriver {
private _count = 0;
private readonly _head: any = document.querySelector('head');
private _warningIssued = false;
validateStyleProperty(prop: string): boolean { return validateStyleProperty(prop); }
matchesElement(element: any, selector: string): boolean {
return matchesElement(element, selector);
}
containsElement(elm1: any, elm2: any): boolean { return containsElement(elm1, elm2); }
query(element: any, selector: string, multi: boolean): any[] {
return invokeQuery(element, selector, multi);
}
computeStyle(element: any, prop: string, defaultValue?: string): string {
return (window.getComputedStyle(element) as any)[prop] as string;
}
buildKeyframeElement(element: any, name: string, keyframes: {[key: string]: any}[]): any {
keyframes = keyframes.map(kf => hypenatePropsObject(kf));
let keyframeStr = `@keyframes ${name} {\n`;
let tab = '';
keyframes.forEach(kf => {
tab = TAB_SPACE;
const offset = parseFloat(kf.offset);
keyframeStr += `${tab}${offset * 100}% {\n`;
tab += TAB_SPACE;
Object.keys(kf).forEach(prop => {
const value = kf[prop];
switch (prop) {
case 'offset':
return;
case 'easing':
if (value) {
keyframeStr += `${tab}animation-timing-function: ${value};\n`;
}
return;
default:
keyframeStr += `${tab}${prop}: ${value};\n`;
return;
}
});
keyframeStr += `${tab}}\n`;
});
keyframeStr += `}\n`;
const kfElm = document.createElement('style');
kfElm.innerHTML = keyframeStr;
return kfElm;
}
animate(
element: any, keyframes: ɵStyleData[], duration: number, delay: number, easing: string,
previousPlayers: AnimationPlayer[] = [], scrubberAccessRequested?: boolean): AnimationPlayer {
if (scrubberAccessRequested) {
this._notifyFaultyScrubber();
}
const previousCssKeyframePlayers = <CssKeyframesPlayer[]>previousPlayers.filter(
player => player instanceof CssKeyframesPlayer);
const previousStyles: {[key: string]: any} = {};
if (allowPreviousPlayerStylesMerge(duration, delay)) {
previousCssKeyframePlayers.forEach(player => {
let styles = player.currentSnapshot;
Object.keys(styles).forEach(prop => previousStyles[prop] = styles[prop]);
});
}
keyframes = balancePreviousStylesIntoKeyframes(element, keyframes, previousStyles);
const finalStyles = flattenKeyframesIntoStyles(keyframes);
// if there is no animation then there is no point in applying
// styles and waiting for an event to get fired. This causes lag.
// It's better to just directly apply the styles to the element
// via the direct styling animation player.
if (duration == 0) {
return new DirectStylePlayer(element, finalStyles);
}
const animationName = `${KEYFRAMES_NAME_PREFIX}${this._count++}`;
const kfElm = this.buildKeyframeElement(element, animationName, keyframes);
document.querySelector('head') !.appendChild(kfElm);
const player = new CssKeyframesPlayer(
element, keyframes, animationName, duration, delay, easing, finalStyles);
player.onDestroy(() => removeElement(kfElm));
return player;
}
private _notifyFaultyScrubber() {
if (!this._warningIssued) {
console.warn(
'@angular/animations: please load the web-animations.js polyfill to allow programmatic access...\n',
' visit http://bit.ly/IWukam to learn more about using the web-animation-js polyfill.');
this._warningIssued = true;
}
}
}
function flattenKeyframesIntoStyles(
keyframes: null | {[key: string]: any} | {[key: string]: any}[]): {[key: string]: any} {
let flatKeyframes: {[key: string]: any} = {};
if (keyframes) {
const kfs = Array.isArray(keyframes) ? keyframes : [keyframes];
kfs.forEach(kf => {
Object.keys(kf).forEach(prop => {
if (prop == 'offset' || prop == 'easing') return;
flatKeyframes[prop] = kf[prop];
});
});
}
return flatKeyframes;
}
function hypenatePropsObject(object: {[key: string]: any}): {[key: string]: any} {
const newObj: {[key: string]: any} = {};
Object.keys(object).forEach(prop => {
const newProp = prop.replace(/([a-z])([A-Z])/g, '$1-$2');
newObj[newProp] = object[prop];
});
return newObj;
}
function removeElement(node: any) {
node.parentNode.removeChild(node);
}

View File

@ -0,0 +1,149 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AnimationPlayer} from '@angular/animations';
import {computeStyle} from '../../util';
import {ElementAnimationStyleHandler} from './element_animation_style_handler';
const DEFAULT_FILL_MODE = 'forwards';
const DEFAULT_EASING = 'linear';
const ANIMATION_END_EVENT = 'animationend';
export enum AnimatorControlState {
INITIALIZED = 1,
STARTED = 2,
FINISHED = 3,
DESTROYED = 4
}
export class CssKeyframesPlayer implements AnimationPlayer {
private _onDoneFns: Function[] = [];
private _onStartFns: Function[] = [];
private _onDestroyFns: Function[] = [];
private _started = false;
private _styler: ElementAnimationStyleHandler;
public parentPlayer: AnimationPlayer;
public readonly totalTime: number;
public readonly easing: string;
public currentSnapshot: {[key: string]: string} = {};
public state = 0;
constructor(
public readonly element: any, public readonly keyframes: {[key: string]: string | number}[],
public readonly animationName: string, private readonly _duration: number,
private readonly _delay: number, easing: string,
private readonly _finalStyles: {[key: string]: any}) {
this.easing = easing || DEFAULT_EASING;
this.totalTime = _duration + _delay;
this._buildStyler();
}
onStart(fn: () => void): void { this._onStartFns.push(fn); }
onDone(fn: () => void): void { this._onDoneFns.push(fn); }
onDestroy(fn: () => void): void { this._onDestroyFns.push(fn); }
destroy() {
this.init();
if (this.state >= AnimatorControlState.DESTROYED) return;
this.state = AnimatorControlState.DESTROYED;
this._styler.destroy();
this._flushStartFns();
this._flushDoneFns();
this._onDestroyFns.forEach(fn => fn());
this._onDestroyFns = [];
}
private _flushDoneFns() {
this._onDoneFns.forEach(fn => fn());
this._onDoneFns = [];
}
private _flushStartFns() {
this._onStartFns.forEach(fn => fn());
this._onStartFns = [];
}
finish() {
this.init();
if (this.state >= AnimatorControlState.FINISHED) return;
this.state = AnimatorControlState.FINISHED;
this._styler.finish();
this._flushStartFns();
this._flushDoneFns();
}
setPosition(value: number) { this._styler.setPosition(value); }
getPosition(): number { return this._styler.getPosition(); }
hasStarted(): boolean { return this.state >= AnimatorControlState.STARTED; }
init(): void {
if (this.state >= AnimatorControlState.INITIALIZED) return;
this.state = AnimatorControlState.INITIALIZED;
const elm = this.element;
this._styler.apply();
if (this._delay) {
this._styler.pause();
}
}
play(): void {
this.init();
if (!this.hasStarted()) {
this._flushStartFns();
this.state = AnimatorControlState.STARTED;
}
this._styler.resume();
}
pause(): void {
this.init();
this._styler.pause();
}
restart(): void {
this.reset();
this.play();
}
reset(): void {
this._styler.destroy();
this._buildStyler();
this._styler.apply();
}
private _buildStyler() {
this._styler = new ElementAnimationStyleHandler(
this.element, this.animationName, this._duration, this._delay, this.easing,
DEFAULT_FILL_MODE, () => this.finish());
}
/* @internal */
triggerCallback(phaseName: string): void {
const methods = phaseName == 'start' ? this._onStartFns : this._onDoneFns;
methods.forEach(fn => fn());
methods.length = 0;
}
beforeDestroy() {
this.init();
const styles: {[key: string]: string} = {};
if (this.hasStarted()) {
const finished = this.state >= AnimatorControlState.FINISHED;
Object.keys(this._finalStyles).forEach(prop => {
if (prop != 'offset') {
styles[prop] = finished ? this._finalStyles[prop] : computeStyle(this.element, prop);
}
});
}
this.currentSnapshot = styles;
}
}

View File

@ -0,0 +1,45 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {NoopAnimationPlayer} from '@angular/animations';
export class DirectStylePlayer extends NoopAnimationPlayer {
private _startingStyles: {[key: string]: any}|null = {};
private __initialized = false;
constructor(public element: any, private _styles: {[key: string]: any}) { super(); }
init() {
if (this.__initialized || !this._startingStyles) return;
this.__initialized = true;
Object.keys(this._styles).forEach(prop => {
this._startingStyles ![prop] = this.element.style[prop];
});
super.init();
}
play() {
if (!this._startingStyles) return;
this.init();
Object.keys(this._styles).forEach(prop => { this.element.style[prop] = this._styles[prop]; });
super.play();
}
destroy() {
if (!this._startingStyles) return;
Object.keys(this._startingStyles).forEach(prop => {
const value = this._startingStyles ![prop];
if (value) {
this.element.style[prop] = value;
} else {
this.element.style.removeProperty(prop);
}
});
this._startingStyles = null;
super.destroy();
}
}

View File

@ -0,0 +1,147 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
const ELAPSED_TIME_MAX_DECIMAL_PLACES = 3;
const ANIMATION_PROP = 'animation';
const ANIMATIONEND_EVENT = 'animationend';
const ONE_SECOND = 1000;
export class ElementAnimationStyleHandler {
private readonly _eventFn: (e: any) => any;
private _finished = false;
private _destroyed = false;
private _startTime = 0;
private _position = 0;
constructor(
private readonly _element: any, private readonly _name: string,
private readonly _duration: number, private readonly _delay: number,
private readonly _easing: string, private readonly _fillMode: ''|'both'|'forwards',
private readonly _onDoneFn: () => any) {
this._eventFn = (e) => this._handleCallback(e);
}
apply() {
applyKeyframeAnimation(
this._element,
`${this._duration}ms ${this._easing} ${this._delay}ms 1 normal ${this._fillMode} ${this._name}`);
addRemoveAnimationEvent(this._element, this._eventFn, false);
this._startTime = Date.now();
}
pause() { playPauseAnimation(this._element, this._name, 'paused'); }
resume() { playPauseAnimation(this._element, this._name, 'running'); }
setPosition(position: number) {
const index = findIndexForAnimation(this._element, this._name);
this._position = position * this._duration;
setAnimationStyle(this._element, 'Delay', `-${this._position}ms`, index);
}
getPosition() { return this._position; }
private _handleCallback(event: any) {
const timestamp = event._ngTestManualTimestamp || Date.now();
const elapsedTime =
parseFloat(event.elapsedTime.toFixed(ELAPSED_TIME_MAX_DECIMAL_PLACES)) * ONE_SECOND;
if (event.animationName == this._name &&
Math.max(timestamp - this._startTime, 0) >= this._delay && elapsedTime >= this._duration) {
this.finish();
}
}
finish() {
if (this._finished) return;
this._finished = true;
this._onDoneFn();
addRemoveAnimationEvent(this._element, this._eventFn, true);
}
destroy() {
if (this._destroyed) return;
this._destroyed = true;
this.finish();
removeKeyframeAnimation(this._element, this._name);
}
}
function playPauseAnimation(element: any, name: string, status: 'running' | 'paused') {
const index = findIndexForAnimation(element, name);
setAnimationStyle(element, 'PlayState', status, index);
}
function applyKeyframeAnimation(element: any, value: string): number {
const anim = getAnimationStyle(element, '').trim();
let index = 0;
if (anim.length) {
index = countChars(anim, ',') + 1;
value = `${anim}, ${value}`;
}
setAnimationStyle(element, '', value);
return index;
}
function removeKeyframeAnimation(element: any, name: string) {
const anim = getAnimationStyle(element, '');
const tokens = anim.split(',');
const index = findMatchingTokenIndex(tokens, name);
if (index >= 0) {
tokens.splice(index, 1);
const newValue = tokens.join(',');
setAnimationStyle(element, '', newValue);
}
}
function findIndexForAnimation(element: any, value: string) {
const anim = getAnimationStyle(element, '');
if (anim.indexOf(',') > 0) {
const tokens = anim.split(',');
return findMatchingTokenIndex(tokens, value);
}
return findMatchingTokenIndex([anim], value);
}
function findMatchingTokenIndex(tokens: string[], searchToken: string): number {
for (let i = 0; i < tokens.length; i++) {
if (tokens[i].indexOf(searchToken) >= 0) {
return i;
}
}
return -1;
}
function addRemoveAnimationEvent(element: any, fn: (e: any) => any, doRemove: boolean) {
doRemove ? element.removeEventListener(ANIMATIONEND_EVENT, fn) :
element.addEventListener(ANIMATIONEND_EVENT, fn);
}
function setAnimationStyle(element: any, name: string, value: string, index?: number) {
const prop = ANIMATION_PROP + name;
if (index != null) {
const oldValue = element.style[prop];
if (oldValue.length) {
const tokens = oldValue.split(',');
tokens[index] = value;
value = tokens.join(',');
}
}
element.style[prop] = value;
}
function getAnimationStyle(element: any, name: string) {
return element.style[ANIMATION_PROP + name];
}
function countChars(value: string, char: string): number {
let count = 0;
for (let i = 0; i < value.length; i++) {
const c = value.charAt(i);
if (c === char) count++;
}
return count;
}

View File

@ -44,7 +44,7 @@ export class TimelineAnimationEngine {
const element = i.element;
const keyframes = normalizeKeyframes(
this._driver, this._normalizer, element, i.keyframes, preStyles, postStyles);
return this._driver.animate(element, keyframes, i.duration, i.delay, i.easing, []);
return this._driver.animate(element, keyframes, i.duration, i.delay, i.easing, [], true);
}
create(id: string, element: any, options: AnimationOptions = {}): AnimationPlayer {

View File

@ -7,12 +7,17 @@
*/
import {AnimationPlayer, ɵStyleData} from '@angular/animations';
import {allowPreviousPlayerStylesMerge, balancePreviousStylesIntoKeyframes, copyStyles} from '../../util';
import {AnimationDriver} from '../animation_driver';
import {CssKeyframesDriver} from '../css_keyframes/css_keyframes_driver';
import {containsElement, invokeQuery, matchesElement, validateStyleProperty} from '../shared';
import {WebAnimationsPlayer} from './web_animations_player';
export class WebAnimationsDriver implements AnimationDriver {
private _isNativeImpl = /\{\s*\[native\s+code\]\s*\}/.test(getElementAnimateFn().toString());
private _cssKeyframesDriver = new CssKeyframesDriver();
validateStyleProperty(prop: string): boolean { return validateStyleProperty(prop); }
matchesElement(element: any, selector: string): boolean {
@ -29,24 +34,46 @@ export class WebAnimationsDriver implements AnimationDriver {
return (window.getComputedStyle(element) as any)[prop] as string;
}
overrideWebAnimationsSupport(supported: boolean) { this._isNativeImpl = supported; }
animate(
element: any, keyframes: ɵStyleData[], duration: number, delay: number, easing: string,
previousPlayers: AnimationPlayer[] = []): WebAnimationsPlayer {
previousPlayers: AnimationPlayer[] = [], scrubberAccessRequested?: boolean): AnimationPlayer {
const useKeyframes = !scrubberAccessRequested && !this._isNativeImpl;
if (useKeyframes) {
return this._cssKeyframesDriver.animate(
element, keyframes, duration, delay, easing, previousPlayers);
}
const fill = delay == 0 ? 'both' : 'forwards';
const playerOptions: {[key: string]: string | number} = {duration, delay, fill};
// we check for this to avoid having a null|undefined value be present
// for the easing (which results in an error for certain browsers #9752)
if (easing) {
playerOptions['easing'] = easing;
}
const previousStyles: {[key: string]: any} = {};
const previousWebAnimationPlayers = <WebAnimationsPlayer[]>previousPlayers.filter(
player => { return player instanceof WebAnimationsPlayer; });
return new WebAnimationsPlayer(element, keyframes, playerOptions, previousWebAnimationPlayers);
player => player instanceof WebAnimationsPlayer);
if (allowPreviousPlayerStylesMerge(duration, delay)) {
previousWebAnimationPlayers.forEach(player => {
let styles = player.currentSnapshot;
Object.keys(styles).forEach(prop => previousStyles[prop] = styles[prop]);
});
}
keyframes = keyframes.map(styles => copyStyles(styles, false));
keyframes = balancePreviousStylesIntoKeyframes(element, keyframes, previousStyles);
return new WebAnimationsPlayer(element, keyframes, playerOptions);
}
}
export function supportsWebAnimations() {
return typeof Element !== 'undefined' && typeof(<any>Element).prototype['animate'] === 'function';
return typeof getElementAnimateFn() === 'function';
}
function getElementAnimateFn(): any {
return (typeof Element !== 'undefined' && (<any>Element).prototype['animate']) || {};
}

View File

@ -7,7 +7,7 @@
*/
import {AnimationPlayer} from '@angular/animations';
import {allowPreviousPlayerStylesMerge, copyStyles} from '../../util';
import {allowPreviousPlayerStylesMerge, balancePreviousStylesIntoKeyframes, computeStyle, copyStyles} from '../../util';
import {DOMAnimation} from './dom_animation';
@ -27,23 +27,14 @@ export class WebAnimationsPlayer implements AnimationPlayer {
public time = 0;
public parentPlayer: AnimationPlayer|null = null;
public previousStyles: {[styleName: string]: string | number} = {};
public currentSnapshot: {[styleName: string]: string | number} = {};
constructor(
public element: any, public keyframes: {[key: string]: string | number}[],
public options: {[key: string]: string | number},
private previousPlayers: WebAnimationsPlayer[] = []) {
public options: {[key: string]: string | number}) {
this._duration = <number>options['duration'];
this._delay = <number>options['delay'] || 0;
this.time = this._duration + this._delay;
if (allowPreviousPlayerStylesMerge(this._duration, this._delay)) {
previousPlayers.forEach(player => {
let styles = player.currentSnapshot;
Object.keys(styles).forEach(prop => this.previousStyles[prop] = styles[prop]);
});
}
}
private _onFinish() {
@ -63,30 +54,7 @@ export class WebAnimationsPlayer implements AnimationPlayer {
if (this._initialized) return;
this._initialized = true;
const keyframes = this.keyframes.map(styles => copyStyles(styles, false));
const previousStyleProps = Object.keys(this.previousStyles);
if (previousStyleProps.length && keyframes.length) {
let startingKeyframe = keyframes[0];
let missingStyleProps: string[] = [];
previousStyleProps.forEach(prop => {
if (!startingKeyframe.hasOwnProperty(prop)) {
missingStyleProps.push(prop);
}
startingKeyframe[prop] = this.previousStyles[prop];
});
if (missingStyleProps.length) {
const self = this;
// tslint:disable-next-line
for (var i = 1; i < keyframes.length; i++) {
let kf = keyframes[i];
missingStyleProps.forEach(function(prop) {
kf[prop] = _computeStyle(self.element, prop);
});
}
}
}
const keyframes = this.keyframes;
(this as{domPlayer: DOMAnimation}).domPlayer =
this._triggerWebAnimation(this.element, keyframes, this.options);
this._finalKeyframe = keyframes.length ? keyframes[keyframes.length - 1] : {};
@ -178,7 +146,7 @@ export class WebAnimationsPlayer implements AnimationPlayer {
Object.keys(this._finalKeyframe).forEach(prop => {
if (prop != 'offset') {
styles[prop] =
this._finished ? this._finalKeyframe[prop] : _computeStyle(this.element, prop);
this._finished ? this._finalKeyframe[prop] : computeStyle(this.element, prop);
}
});
}
@ -192,7 +160,3 @@ export class WebAnimationsPlayer implements AnimationPlayer {
methods.length = 0;
}
}
function _computeStyle(element: any, prop: string): string {
return (<any>window.getComputedStyle(element))[prop];
}

View File

@ -235,6 +235,30 @@ export function allowPreviousPlayerStylesMerge(duration: number, delay: number)
return duration === 0 || delay === 0;
}
export function balancePreviousStylesIntoKeyframes(
element: any, keyframes: {[key: string]: any}[], previousStyles: {[key: string]: any}) {
const previousStyleProps = Object.keys(previousStyles);
if (previousStyleProps.length && keyframes.length) {
let startingKeyframe = keyframes[0];
let missingStyleProps: string[] = [];
previousStyleProps.forEach(prop => {
if (!startingKeyframe.hasOwnProperty(prop)) {
missingStyleProps.push(prop);
}
startingKeyframe[prop] = previousStyles[prop];
});
if (missingStyleProps.length) {
// tslint:disable-next-line
for (var i = 1; i < keyframes.length; i++) {
let kf = keyframes[i];
missingStyleProps.forEach(function(prop) { kf[prop] = computeStyle(element, prop); });
}
}
}
return keyframes;
}
export function visitDslNode(
visitor: AnimationDslVisitor, node: AnimationMetadata, context: any): any;
export function visitDslNode(
@ -271,3 +295,7 @@ export function visitDslNode(visitor: any, node: any, context: any): any {
throw new Error(`Unable to resolve animation metadata node #${node.type}`);
}
}
export function computeStyle(element: any, prop: string): string {
return (<any>window.getComputedStyle(element))[prop];
}