feat(ivy): enhance [style] and [class] bindings to be animation aware (#26096)
PR Close #26096
This commit is contained in:

committed by
Misko Hevery

parent
be337a2e52
commit
fa8e633be5
@ -5,13 +5,20 @@
|
||||
* 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 {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
|
||||
import {InitialStylingFlags} from '../interfaces/definition';
|
||||
import {BindingStore, BindingType, Player, PlayerBuilder, PlayerFactory, PlayerIndex} from '../interfaces/player';
|
||||
import {Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer';
|
||||
import {InitialStyles, StylingContext, StylingFlags, StylingIndex} from '../interfaces/styling';
|
||||
import {LViewData, RootContext} from '../interfaces/view';
|
||||
import {getRootContext} from '../util';
|
||||
|
||||
import {BoundPlayerFactory} from './player_factory';
|
||||
import {addPlayerInternal, allocPlayerContext, createEmptyStylingContext, getPlayerContext} from './util';
|
||||
|
||||
const EMPTY_ARR: any[] = [];
|
||||
const EMPTY_OBJ: {[key: string]: any} = {};
|
||||
|
||||
import {EMPTY_ARR, EMPTY_OBJ, createEmptyStylingContext} from './util';
|
||||
|
||||
/**
|
||||
* Creates a styling context template where styling information is stored.
|
||||
@ -124,12 +131,14 @@ export function createStylingContextTemplate(
|
||||
setFlag(context, indexForSingle, pointers(initialFlag, indexForInitial, indexForMulti));
|
||||
setProp(context, indexForSingle, prop);
|
||||
setValue(context, indexForSingle, null);
|
||||
setPlayerBuilderIndex(context, indexForSingle, 0);
|
||||
|
||||
const flagForMulti =
|
||||
initialFlag | (initialValue !== null ? StylingFlags.Dirty : StylingFlags.None);
|
||||
setFlag(context, indexForMulti, pointers(flagForMulti, indexForInitial, indexForSingle));
|
||||
setProp(context, indexForMulti, prop);
|
||||
setValue(context, indexForMulti, null);
|
||||
setPlayerBuilderIndex(context, indexForMulti, 0);
|
||||
}
|
||||
|
||||
// there is no initial value flag for the master index since it doesn't
|
||||
@ -142,7 +151,7 @@ export function createStylingContextTemplate(
|
||||
|
||||
/**
|
||||
* Sets and resolves all `multi` styling on an `StylingContext` so that they can be
|
||||
* applied to the element once `renderStyling` is called.
|
||||
* applied to the element once `renderStyleAndClassBindings` is called.
|
||||
*
|
||||
* All missing styles/class (any values that are not provided in the new `styles`
|
||||
* or `classes` params) will resolve to `null` within their respective positions
|
||||
@ -150,43 +159,73 @@ export function createStylingContextTemplate(
|
||||
*
|
||||
* @param context The styling context that will be updated with the
|
||||
* newly provided style values.
|
||||
* @param classes The key/value map of CSS class names that will be used for the update.
|
||||
* @param styles The key/value map of CSS styles that will be used for the update.
|
||||
* @param classesInput The key/value map of CSS class names that will be used for the update.
|
||||
* @param stylesInput The key/value map of CSS styles that will be used for the update.
|
||||
*/
|
||||
export function updateStylingMap(
|
||||
context: StylingContext, classes: {[key: string]: any} | string | null,
|
||||
styles?: {[key: string]: any} | null): void {
|
||||
styles = styles || null;
|
||||
context: StylingContext, classesInput: {[key: string]: any} | string |
|
||||
BoundPlayerFactory<null|string|{[key: string]: any}>| null,
|
||||
stylesInput?: {[key: string]: any} | BoundPlayerFactory<null|{[key: string]: any}>|
|
||||
null): void {
|
||||
stylesInput = stylesInput || null;
|
||||
|
||||
const element = context[StylingIndex.ElementPosition] !as HTMLElement;
|
||||
const classesPlayerBuilder = classesInput instanceof BoundPlayerFactory ?
|
||||
new ClassAndStylePlayerBuilder(classesInput as any, element, BindingType.Class) :
|
||||
null;
|
||||
const stylesPlayerBuilder = stylesInput instanceof BoundPlayerFactory ?
|
||||
new ClassAndStylePlayerBuilder(stylesInput as any, element, BindingType.Style) :
|
||||
null;
|
||||
|
||||
const classesValue = classesPlayerBuilder ?
|
||||
(classesInput as BoundPlayerFactory<{[key: string]: any}|string>) !.value :
|
||||
classesInput;
|
||||
const stylesValue = stylesPlayerBuilder ? stylesInput !.value : stylesInput;
|
||||
|
||||
// early exit (this is what's done to avoid using ctx.bind() to cache the value)
|
||||
const ignoreAllClassUpdates = classes === context[StylingIndex.PreviousMultiClassValue];
|
||||
const ignoreAllStyleUpdates = styles === context[StylingIndex.PreviousMultiStyleValue];
|
||||
const ignoreAllClassUpdates = classesValue === context[StylingIndex.PreviousMultiClassValue];
|
||||
const ignoreAllStyleUpdates = stylesValue === context[StylingIndex.PreviousMultiStyleValue];
|
||||
if (ignoreAllClassUpdates && ignoreAllStyleUpdates) return;
|
||||
|
||||
context[StylingIndex.PreviousMultiClassValue] = classesValue;
|
||||
context[StylingIndex.PreviousMultiStyleValue] = stylesValue;
|
||||
|
||||
let classNames: string[] = EMPTY_ARR;
|
||||
let applyAllClasses = false;
|
||||
let playerBuildersAreDirty = false;
|
||||
|
||||
const classesPlayerBuilderIndex =
|
||||
classesPlayerBuilder ? PlayerIndex.ClassMapPlayerBuilderPosition : 0;
|
||||
if (hasPlayerBuilderChanged(
|
||||
context, classesPlayerBuilder, PlayerIndex.ClassMapPlayerBuilderPosition)) {
|
||||
setPlayerBuilder(context, classesPlayerBuilder, PlayerIndex.ClassMapPlayerBuilderPosition);
|
||||
playerBuildersAreDirty = true;
|
||||
}
|
||||
|
||||
const stylesPlayerBuilderIndex =
|
||||
stylesPlayerBuilder ? PlayerIndex.StyleMapPlayerBuilderPosition : 0;
|
||||
if (hasPlayerBuilderChanged(
|
||||
context, stylesPlayerBuilder, PlayerIndex.StyleMapPlayerBuilderPosition)) {
|
||||
setPlayerBuilder(context, stylesPlayerBuilder, PlayerIndex.StyleMapPlayerBuilderPosition);
|
||||
playerBuildersAreDirty = true;
|
||||
}
|
||||
|
||||
// each time a string-based value pops up then it shouldn't require a deep
|
||||
// check of what's changed.
|
||||
if (!ignoreAllClassUpdates) {
|
||||
context[StylingIndex.PreviousMultiClassValue] = classes;
|
||||
if (typeof classes == 'string') {
|
||||
classNames = classes.split(/\s+/);
|
||||
if (typeof classesValue == 'string') {
|
||||
classNames = classesValue.split(/\s+/);
|
||||
// this boolean is used to avoid having to create a key/value map of `true` values
|
||||
// since a classname string implies that all those classes are added
|
||||
applyAllClasses = true;
|
||||
} else {
|
||||
classNames = classes ? Object.keys(classes) : EMPTY_ARR;
|
||||
classNames = classesValue ? Object.keys(classesValue) : EMPTY_ARR;
|
||||
}
|
||||
}
|
||||
|
||||
classes = (classes || EMPTY_OBJ) as{[key: string]: any};
|
||||
|
||||
if (!ignoreAllStyleUpdates) {
|
||||
context[StylingIndex.PreviousMultiStyleValue] = styles;
|
||||
}
|
||||
|
||||
const styleProps = styles ? Object.keys(styles) : EMPTY_ARR;
|
||||
styles = styles || EMPTY_OBJ;
|
||||
const classes = (classesValue || EMPTY_OBJ) as{[key: string]: any};
|
||||
const styleProps = stylesValue ? Object.keys(stylesValue) : EMPTY_ARR;
|
||||
const styles = stylesValue || EMPTY_OBJ;
|
||||
|
||||
const classesStartIndex = styleProps.length;
|
||||
const multiStartIndex = getMultiStartIndex(context);
|
||||
@ -213,13 +252,18 @@ export function updateStylingMap(
|
||||
isClassBased ? classNames[adjustedPropIndex] : styleProps[adjustedPropIndex];
|
||||
const newValue: string|boolean =
|
||||
isClassBased ? (applyAllClasses ? true : classes[newProp]) : styles[newProp];
|
||||
const playerBuilderIndex =
|
||||
isClassBased ? classesPlayerBuilderIndex : stylesPlayerBuilderIndex;
|
||||
|
||||
const prop = getProp(context, ctxIndex);
|
||||
if (prop === newProp) {
|
||||
const value = getValue(context, ctxIndex);
|
||||
const flag = getPointers(context, ctxIndex);
|
||||
setPlayerBuilderIndex(context, ctxIndex, playerBuilderIndex);
|
||||
|
||||
if (hasValueChanged(flag, value, newValue)) {
|
||||
setValue(context, ctxIndex, newValue);
|
||||
playerBuildersAreDirty = playerBuildersAreDirty || !!playerBuilderIndex;
|
||||
|
||||
const initialValue = getInitialValue(context, flag);
|
||||
|
||||
@ -242,13 +286,16 @@ export function updateStylingMap(
|
||||
setValue(context, ctxIndex, newValue);
|
||||
if (hasValueChanged(flagToCompare, initialValue, newValue)) {
|
||||
setDirty(context, ctxIndex, true);
|
||||
playerBuildersAreDirty = playerBuildersAreDirty || !!playerBuilderIndex;
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// we only care to do this if the insertion is in the middle
|
||||
const newFlag = prepareInitialFlag(newProp, isClassBased, getStyleSanitizer(context));
|
||||
insertNewMultiProperty(context, ctxIndex, isClassBased, newProp, newFlag, newValue);
|
||||
playerBuildersAreDirty = playerBuildersAreDirty || !!playerBuilderIndex;
|
||||
insertNewMultiProperty(
|
||||
context, ctxIndex, isClassBased, newProp, newFlag, newValue, playerBuilderIndex);
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
@ -272,6 +319,13 @@ export function updateStylingMap(
|
||||
if (doRemoveValue) {
|
||||
setDirty(context, ctxIndex, true);
|
||||
setValue(context, ctxIndex, null);
|
||||
|
||||
// we keep the player factory the same so that the `nulled` value can
|
||||
// be instructed into the player because removing a style and/or a class
|
||||
// is a valid animation player instruction.
|
||||
const playerBuilderIndex =
|
||||
isClassBased ? classesPlayerBuilderIndex : stylesPlayerBuilderIndex;
|
||||
setPlayerBuilderIndex(context, ctxIndex, playerBuilderIndex);
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
@ -292,7 +346,9 @@ export function updateStylingMap(
|
||||
const value: string|boolean =
|
||||
isClassBased ? (applyAllClasses ? true : classes[prop]) : styles[prop];
|
||||
const flag = prepareInitialFlag(prop, isClassBased, sanitizer) | StylingFlags.Dirty;
|
||||
context.push(flag, prop, value);
|
||||
const playerBuilderIndex =
|
||||
isClassBased ? classesPlayerBuilderIndex : stylesPlayerBuilderIndex;
|
||||
context.push(flag, prop, value, playerBuilderIndex);
|
||||
dirty = true;
|
||||
}
|
||||
propIndex++;
|
||||
@ -301,11 +357,15 @@ export function updateStylingMap(
|
||||
if (dirty) {
|
||||
setContextDirty(context, true);
|
||||
}
|
||||
|
||||
if (playerBuildersAreDirty) {
|
||||
setContextPlayersDirty(context, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets and resolves a single styling property/value on the provided `StylingContext` so
|
||||
* that they can be applied to the element once `renderStyling` is called.
|
||||
* that they can be applied to the element once `renderStyleAndClassBindings` is called.
|
||||
*
|
||||
* Note that prop-level styling values are considered higher priority than any styling that
|
||||
* has been applied using `updateStylingMap`, therefore, when styling values are rendered
|
||||
@ -318,13 +378,34 @@ export function updateStylingMap(
|
||||
* @param value The CSS style value that will be assigned
|
||||
*/
|
||||
export function updateStyleProp(
|
||||
context: StylingContext, index: number, value: string | boolean | null): void {
|
||||
context: StylingContext, index: number,
|
||||
input: string | boolean | null | BoundPlayerFactory<string|boolean|null>): void {
|
||||
const singleIndex = StylingIndex.SingleStylesStartPosition + index * StylingIndex.Size;
|
||||
const currValue = getValue(context, singleIndex);
|
||||
const currFlag = getPointers(context, singleIndex);
|
||||
const value: string|boolean|null = (input instanceof BoundPlayerFactory) ? input.value : input;
|
||||
|
||||
// didn't change ... nothing to make a note of
|
||||
if (hasValueChanged(currFlag, currValue, value)) {
|
||||
const isClassBased = (currFlag & StylingFlags.Class) === StylingFlags.Class;
|
||||
const element = context[StylingIndex.ElementPosition] !as HTMLElement;
|
||||
const playerBuilder = input instanceof BoundPlayerFactory ?
|
||||
new ClassAndStylePlayerBuilder(
|
||||
input as any, element, isClassBased ? BindingType.Class : BindingType.Style) :
|
||||
null;
|
||||
const value = (playerBuilder ? (input as BoundPlayerFactory<any>).value : input) as string |
|
||||
boolean | null;
|
||||
const currPlayerIndex = getPlayerBuilderIndex(context, singleIndex);
|
||||
|
||||
let playerBuildersAreDirty = false;
|
||||
let playerBuilderIndex = playerBuilder ? currPlayerIndex : 0;
|
||||
if (hasPlayerBuilderChanged(context, playerBuilder, currPlayerIndex)) {
|
||||
const newIndex = setPlayerBuilder(context, playerBuilder, currPlayerIndex);
|
||||
playerBuilderIndex = playerBuilder ? newIndex : 0;
|
||||
setPlayerBuilderIndex(context, singleIndex, playerBuilderIndex);
|
||||
playerBuildersAreDirty = true;
|
||||
}
|
||||
|
||||
// the value will always get updated (even if the dirty flag is skipped)
|
||||
setValue(context, singleIndex, value);
|
||||
const indexForMulti = getMultiOrSingleIndex(currFlag);
|
||||
@ -335,8 +416,6 @@ export function updateStyleProp(
|
||||
let multiDirty = false;
|
||||
let singleDirty = true;
|
||||
|
||||
const isClassBased = (currFlag & StylingFlags.Class) === StylingFlags.Class;
|
||||
|
||||
// only when the value is set to `null` should the multi-value get flagged
|
||||
if (!valueExists(value, isClassBased) && valueExists(valueForMulti, isClassBased)) {
|
||||
multiDirty = true;
|
||||
@ -347,6 +426,10 @@ export function updateStyleProp(
|
||||
setDirty(context, singleIndex, singleDirty);
|
||||
setContextDirty(context, true);
|
||||
}
|
||||
|
||||
if (playerBuildersAreDirty) {
|
||||
setContextPlayersDirty(context, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -360,7 +443,8 @@ export function updateStyleProp(
|
||||
* @param addOrRemove Whether or not to add or remove the CSS class
|
||||
*/
|
||||
export function updateClassProp(
|
||||
context: StylingContext, index: number, addOrRemove: boolean): void {
|
||||
context: StylingContext, index: number,
|
||||
addOrRemove: boolean | BoundPlayerFactory<boolean>): void {
|
||||
const adjustedIndex = index + context[StylingIndex.ClassOffsetPosition];
|
||||
updateStyleProp(context, adjustedIndex, addOrRemove);
|
||||
}
|
||||
@ -378,15 +462,19 @@ export function updateClassProp(
|
||||
* @param context The styling context that will be used to determine
|
||||
* what styles will be rendered
|
||||
* @param renderer the renderer that will be used to apply the styling
|
||||
* @param styleStore if provided, the updated style values will be applied
|
||||
* @param classesStore if provided, the updated class values will be applied
|
||||
* to this key/value map instead of being renderered via the renderer.
|
||||
* @param classStore if provided, the updated class values will be applied
|
||||
* @param stylesStore if provided, the updated style values will be applied
|
||||
* to this key/value map instead of being renderered via the renderer.
|
||||
* @returns number the total amount of players that got queued for animation (if any)
|
||||
*/
|
||||
export function renderStyling(
|
||||
context: StylingContext, renderer: Renderer3, styleStore?: {[key: string]: any},
|
||||
classStore?: {[key: string]: boolean}) {
|
||||
export function renderStyleAndClassBindings(
|
||||
context: StylingContext, renderer: Renderer3, rootOrView: RootContext | LViewData,
|
||||
classesStore?: BindingStore | null, stylesStore?: BindingStore | null): number {
|
||||
let totalPlayersQueued = 0;
|
||||
if (isContextDirty(context)) {
|
||||
const flushPlayerBuilders: any =
|
||||
context[StylingIndex.MasterFlagPosition] & StylingFlags.PlayerBuildersDirty;
|
||||
const native = context[StylingIndex.ElementPosition] !;
|
||||
const multiStartIndex = getMultiStartIndex(context);
|
||||
const styleSanitizer = getStyleSanitizer(context);
|
||||
@ -397,6 +485,7 @@ export function renderStyling(
|
||||
const prop = getProp(context, i);
|
||||
const value = getValue(context, i);
|
||||
const flag = getPointers(context, i);
|
||||
const playerBuilder = getPlayerBuilder(context, i);
|
||||
const isClassBased = flag & StylingFlags.Class ? true : false;
|
||||
const isInSingleRegion = i < multiStartIndex;
|
||||
|
||||
@ -422,17 +511,52 @@ export function renderStyling(
|
||||
}
|
||||
|
||||
if (isClassBased) {
|
||||
setClass(native, prop, valueToApply ? true : false, renderer, classStore);
|
||||
setClass(
|
||||
native, prop, valueToApply ? true : false, renderer, classesStore, playerBuilder);
|
||||
} else {
|
||||
const sanitizer = (flag & StylingFlags.Sanitize) ? styleSanitizer : null;
|
||||
setStyle(native, prop, valueToApply as string | null, renderer, sanitizer, styleStore);
|
||||
setStyle(
|
||||
native, prop, valueToApply as string | null, renderer, sanitizer, stylesStore,
|
||||
playerBuilder);
|
||||
}
|
||||
setDirty(context, i, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (flushPlayerBuilders) {
|
||||
const rootContext =
|
||||
Array.isArray(rootOrView) ? getRootContext(rootOrView) : rootOrView as RootContext;
|
||||
const playerContext = getPlayerContext(context) !;
|
||||
const playersStartIndex = playerContext[PlayerIndex.NonBuilderPlayersStart];
|
||||
for (let i = PlayerIndex.PlayerBuildersStartPosition; i < playersStartIndex;
|
||||
i += PlayerIndex.PlayerAndPlayerBuildersTupleSize) {
|
||||
const builder = playerContext[i] as ClassAndStylePlayerBuilder<any>| null;
|
||||
const playerInsertionIndex = i + PlayerIndex.PlayerOffsetPosition;
|
||||
const oldPlayer = playerContext[playerInsertionIndex] as Player | null;
|
||||
if (builder) {
|
||||
const player = builder.buildPlayer(oldPlayer);
|
||||
if (player !== undefined) {
|
||||
if (player != null) {
|
||||
const wasQueued = addPlayerInternal(
|
||||
playerContext, rootContext, native as HTMLElement, player, playerInsertionIndex);
|
||||
wasQueued && totalPlayersQueued++;
|
||||
}
|
||||
if (oldPlayer) {
|
||||
oldPlayer.destroy();
|
||||
}
|
||||
}
|
||||
} else if (oldPlayer) {
|
||||
// the player builder has been removed ... therefore we should delete the associated
|
||||
// player
|
||||
oldPlayer.destroy();
|
||||
}
|
||||
}
|
||||
setContextPlayersDirty(context, false);
|
||||
}
|
||||
setContextDirty(context, false);
|
||||
}
|
||||
|
||||
return totalPlayersQueued;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -449,10 +573,16 @@ export function renderStyling(
|
||||
*/
|
||||
function setStyle(
|
||||
native: any, prop: string, value: string | null, renderer: Renderer3,
|
||||
sanitizer: StyleSanitizeFn | null, store?: {[key: string]: any}) {
|
||||
sanitizer: StyleSanitizeFn | null, store?: BindingStore | null,
|
||||
playerBuilder?: ClassAndStylePlayerBuilder<any>| null) {
|
||||
value = sanitizer && value ? sanitizer(prop, value) : value;
|
||||
if (store) {
|
||||
store[prop] = value;
|
||||
if (store || playerBuilder) {
|
||||
if (store) {
|
||||
store.setValue(prop, value);
|
||||
}
|
||||
if (playerBuilder) {
|
||||
playerBuilder.setValue(prop, value);
|
||||
}
|
||||
} else if (value) {
|
||||
ngDevMode && ngDevMode.rendererSetStyle++;
|
||||
isProceduralRenderer(renderer) ?
|
||||
@ -479,10 +609,15 @@ function setStyle(
|
||||
* @param store an optional key/value map that will be used as a context to render styles on
|
||||
*/
|
||||
function setClass(
|
||||
native: any, className: string, add: boolean, renderer: Renderer3,
|
||||
store?: {[key: string]: boolean}) {
|
||||
if (store) {
|
||||
store[className] = add;
|
||||
native: any, className: string, add: boolean, renderer: Renderer3, store?: BindingStore | null,
|
||||
playerBuilder?: ClassAndStylePlayerBuilder<any>| null) {
|
||||
if (store || playerBuilder) {
|
||||
if (store) {
|
||||
store.setValue(className, add);
|
||||
}
|
||||
if (playerBuilder) {
|
||||
playerBuilder.setValue(className, add);
|
||||
}
|
||||
} else if (add) {
|
||||
ngDevMode && ngDevMode.rendererAddClass++;
|
||||
isProceduralRenderer(renderer) ? renderer.addClass(native, className) :
|
||||
@ -558,6 +693,54 @@ function setValue(context: StylingContext, index: number, value: string | null |
|
||||
context[index + StylingIndex.ValueOffset] = value;
|
||||
}
|
||||
|
||||
function hasPlayerBuilderChanged(
|
||||
context: StylingContext, builder: ClassAndStylePlayerBuilder<any>| null, index: number) {
|
||||
const playerContext = context[StylingIndex.PlayerContext] !;
|
||||
if (builder) {
|
||||
if (!playerContext || index === 0) {
|
||||
return true;
|
||||
}
|
||||
} else if (!playerContext) {
|
||||
return false;
|
||||
}
|
||||
return playerContext[index] !== builder;
|
||||
}
|
||||
|
||||
function setPlayerBuilder(
|
||||
context: StylingContext, builder: ClassAndStylePlayerBuilder<any>| null,
|
||||
insertionIndex: number): number {
|
||||
let playerContext = context[StylingIndex.PlayerContext] || allocPlayerContext(context);
|
||||
if (insertionIndex > 0) {
|
||||
playerContext[insertionIndex] = builder;
|
||||
} else {
|
||||
insertionIndex = playerContext[PlayerIndex.NonBuilderPlayersStart];
|
||||
playerContext.splice(insertionIndex, 0, builder, null);
|
||||
playerContext[PlayerIndex.NonBuilderPlayersStart] +=
|
||||
PlayerIndex.PlayerAndPlayerBuildersTupleSize;
|
||||
}
|
||||
return insertionIndex;
|
||||
}
|
||||
|
||||
function setPlayerBuilderIndex(context: StylingContext, index: number, playerBuilderIndex: number) {
|
||||
context[index + StylingIndex.PlayerBuilderIndexOffset] = playerBuilderIndex;
|
||||
}
|
||||
|
||||
function getPlayerBuilderIndex(context: StylingContext, index: number): number {
|
||||
return (context[index + StylingIndex.PlayerBuilderIndexOffset] as number) || 0;
|
||||
}
|
||||
|
||||
function getPlayerBuilder(context: StylingContext, index: number): ClassAndStylePlayerBuilder<any>|
|
||||
null {
|
||||
const playerBuilderIndex = getPlayerBuilderIndex(context, index);
|
||||
if (playerBuilderIndex) {
|
||||
const playerContext = context[StylingIndex.PlayerContext];
|
||||
if (playerContext) {
|
||||
return playerContext[playerBuilderIndex] as ClassAndStylePlayerBuilder<any>| null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setFlag(context: StylingContext, index: number, flag: number) {
|
||||
const adjustedIndex =
|
||||
index === StylingIndex.MasterFlagPosition ? index : (index + StylingIndex.FlagsOffset);
|
||||
@ -586,6 +769,14 @@ export function setContextDirty(context: StylingContext, isDirtyYes: boolean): v
|
||||
setDirty(context, StylingIndex.MasterFlagPosition, isDirtyYes);
|
||||
}
|
||||
|
||||
export function setContextPlayersDirty(context: StylingContext, isDirtyYes: boolean): void {
|
||||
if (isDirtyYes) {
|
||||
(context[StylingIndex.MasterFlagPosition] as number) |= StylingFlags.PlayerBuildersDirty;
|
||||
} else {
|
||||
(context[StylingIndex.MasterFlagPosition] as number) &= ~StylingFlags.PlayerBuildersDirty;
|
||||
}
|
||||
}
|
||||
|
||||
function findEntryPositionByProp(
|
||||
context: StylingContext, prop: string, startIndex?: number): number {
|
||||
for (let i = (startIndex || 0) + StylingIndex.PropertyOffset; i < context.length;
|
||||
@ -602,6 +793,7 @@ function swapMultiContextEntries(context: StylingContext, indexA: number, indexB
|
||||
const tmpValue = getValue(context, indexA);
|
||||
const tmpProp = getProp(context, indexA);
|
||||
const tmpFlag = getPointers(context, indexA);
|
||||
const tmpPlayerBuilderIndex = getPlayerBuilderIndex(context, indexA);
|
||||
|
||||
let flagA = tmpFlag;
|
||||
let flagB = getPointers(context, indexB);
|
||||
@ -623,10 +815,12 @@ function swapMultiContextEntries(context: StylingContext, indexA: number, indexB
|
||||
setValue(context, indexA, getValue(context, indexB));
|
||||
setProp(context, indexA, getProp(context, indexB));
|
||||
setFlag(context, indexA, getPointers(context, indexB));
|
||||
setPlayerBuilderIndex(context, indexA, getPlayerBuilderIndex(context, indexB));
|
||||
|
||||
setValue(context, indexB, tmpValue);
|
||||
setProp(context, indexB, tmpProp);
|
||||
setFlag(context, indexB, tmpFlag);
|
||||
setPlayerBuilderIndex(context, indexB, tmpPlayerBuilderIndex);
|
||||
}
|
||||
|
||||
function updateSinglePointerValues(context: StylingContext, indexStartPosition: number) {
|
||||
@ -647,13 +841,13 @@ function updateSinglePointerValues(context: StylingContext, indexStartPosition:
|
||||
|
||||
function insertNewMultiProperty(
|
||||
context: StylingContext, index: number, classBased: boolean, name: string, flag: number,
|
||||
value: string | boolean): void {
|
||||
value: string | boolean, playerIndex: number): void {
|
||||
const doShift = index < context.length;
|
||||
|
||||
// prop does not exist in the list, add it in
|
||||
context.splice(
|
||||
index, 0, flag | StylingFlags.Dirty | (classBased ? StylingFlags.Class : StylingFlags.None),
|
||||
name, value);
|
||||
name, value, playerIndex);
|
||||
|
||||
if (doShift) {
|
||||
// because the value was inserted midway into the array then we
|
||||
@ -696,3 +890,35 @@ function hasValueChanged(
|
||||
// everything else is safe to check with a normal equality check
|
||||
return a !== b;
|
||||
}
|
||||
|
||||
export class ClassAndStylePlayerBuilder<T> implements PlayerBuilder {
|
||||
private _values: {[key: string]: string | null} = {};
|
||||
private _dirty = false;
|
||||
private _factory: BoundPlayerFactory<T>;
|
||||
|
||||
constructor(factory: PlayerFactory, private _element: HTMLElement, private _type: BindingType) {
|
||||
this._factory = factory as any;
|
||||
}
|
||||
|
||||
setValue(prop: string, value: any) {
|
||||
if (this._values[prop] !== value) {
|
||||
this._values[prop] = value;
|
||||
this._dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
buildPlayer(currentPlayer?: Player|null): Player|undefined|null {
|
||||
// if no values have been set here then this means the binding didn't
|
||||
// change and therefore the binding values were not updated through
|
||||
// `setValue` which means no new player will be provided.
|
||||
if (this._dirty) {
|
||||
const player =
|
||||
this._factory.fn(this._element, this._type, this._values !, currentPlayer || null);
|
||||
this._values = {};
|
||||
this._dirty = false;
|
||||
return player;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
* 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 {Player, PlayerHandler} from '../interfaces/player';
|
||||
import {PlayState, Player, PlayerHandler} from '../interfaces/player';
|
||||
|
||||
export class CorePlayerHandler implements PlayerHandler {
|
||||
private _players: Player[] = [];
|
||||
@ -13,7 +13,7 @@ export class CorePlayerHandler implements PlayerHandler {
|
||||
flushPlayers() {
|
||||
for (let i = 0; i < this._players.length; i++) {
|
||||
const player = this._players[i];
|
||||
if (!player.parent) {
|
||||
if (!player.parent && player.state === PlayState.Pending) {
|
||||
player.play();
|
||||
}
|
||||
}
|
||||
|
33
packages/core/src/render3/styling/player_factory.ts
Normal file
33
packages/core/src/render3/styling/player_factory.ts
Normal file
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* @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 {PlayerFactory, PlayerFactoryBuildFn} from '../interfaces/player';
|
||||
|
||||
/**
|
||||
* Combines the binding value and a factory for an animation player.
|
||||
*
|
||||
* Used to bind a player to an element template binding (currently only
|
||||
* `[style]`, `[style.prop]`, `[class]` and `[class.name]` bindings
|
||||
* supported). The provided `factoryFn` function will be run once all
|
||||
* the associated bindings have been evaluated on the element and is
|
||||
* designed to return a player which will then be placed on the element.
|
||||
*
|
||||
* @param factoryFn The function that is used to create a player
|
||||
* once all the rendering-related (styling values) have been
|
||||
* processed for the element binding.
|
||||
* @param value The raw value that will be exposed to the binding
|
||||
* so that the binding can update its internal values when
|
||||
* any changes are evaluated.
|
||||
*/
|
||||
export function bindPlayerFactory<T>(factoryFn: PlayerFactoryBuildFn, value: T): PlayerFactory {
|
||||
return new BoundPlayerFactory(factoryFn, value) as any;
|
||||
}
|
||||
|
||||
export class BoundPlayerFactory<T> {
|
||||
'__brand__': 'Brand for PlayerFactory that nothing will match';
|
||||
constructor(public fn: PlayerFactoryBuildFn, public value: T) {}
|
||||
}
|
@ -5,19 +5,19 @@
|
||||
* 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 '../ng_dev_mode';
|
||||
|
||||
import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
|
||||
import {getContext} from '../context_discovery';
|
||||
import {ACTIVE_INDEX, LContainer} from '../interfaces/container';
|
||||
import {LContext} from '../interfaces/context';
|
||||
import {PlayerContext} from '../interfaces/player';
|
||||
import {PlayState, Player, PlayerContext, PlayerIndex} from '../interfaces/player';
|
||||
import {RElement} from '../interfaces/renderer';
|
||||
import {InitialStyles, StylingContext, StylingIndex} from '../interfaces/styling';
|
||||
import {FLAGS, HEADER_OFFSET, HOST, LViewData} from '../interfaces/view';
|
||||
import {FLAGS, HEADER_OFFSET, HOST, LViewData, RootContext} from '../interfaces/view';
|
||||
import {getTNode} from '../util';
|
||||
|
||||
export const EMPTY_ARR: any[] = [];
|
||||
export const EMPTY_OBJ: {[key: string]: any} = {};
|
||||
import {CorePlayerHandler} from './core_player_handler';
|
||||
|
||||
export function createEmptyStylingContext(
|
||||
element?: RElement | null, sanitizer?: StyleSanitizeFn | null,
|
||||
@ -75,7 +75,10 @@ export function getStylingContext(index: number, viewData: LViewData): StylingCo
|
||||
// This is an LViewData or an LContainer
|
||||
const stylingTemplate = getTNode(index, viewData).stylingTemplate;
|
||||
|
||||
if (wrapper !== viewData) storageIndex = HOST;
|
||||
if (wrapper !== viewData) {
|
||||
storageIndex = HOST;
|
||||
}
|
||||
|
||||
return wrapper[storageIndex] = stylingTemplate ?
|
||||
allocStylingContext(slotValue, stylingTemplate) :
|
||||
createEmptyStylingContext(slotValue);
|
||||
@ -87,18 +90,88 @@ function isStylingContext(value: LViewData | LContainer | StylingContext) {
|
||||
return typeof value[FLAGS] !== 'number' && typeof value[ACTIVE_INDEX] !== 'number';
|
||||
}
|
||||
|
||||
export function getOrCreatePlayerContext(target: {}, context?: LContext | null): PlayerContext {
|
||||
export function addPlayerInternal(
|
||||
playerContext: PlayerContext, rootContext: RootContext, element: HTMLElement,
|
||||
player: Player | null, playerContextIndex: number, ref?: any): boolean {
|
||||
ref = ref || element;
|
||||
if (playerContextIndex) {
|
||||
playerContext[playerContextIndex] = player;
|
||||
} else {
|
||||
playerContext.push(player);
|
||||
}
|
||||
|
||||
if (player) {
|
||||
player.addEventListener(PlayState.Destroyed, () => {
|
||||
const index = playerContext.indexOf(player);
|
||||
const nonFactoryPlayerIndex = playerContext[PlayerIndex.NonBuilderPlayersStart];
|
||||
|
||||
// if the player is being removed from the factory side of the context
|
||||
// (which is where the [style] and [class] bindings do their thing) then
|
||||
// that side of the array cannot be resized since the respective bindings
|
||||
// have pointer index values that point to the associated factory instance
|
||||
if (index) {
|
||||
if (index < nonFactoryPlayerIndex) {
|
||||
playerContext[index] = null;
|
||||
} else {
|
||||
playerContext.splice(index, 1);
|
||||
}
|
||||
}
|
||||
player.destroy();
|
||||
});
|
||||
|
||||
const playerHandler =
|
||||
rootContext.playerHandler || (rootContext.playerHandler = new CorePlayerHandler());
|
||||
playerHandler.queuePlayer(player, ref);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getPlayersInternal(playerContext: PlayerContext): Player[] {
|
||||
const players: Player[] = [];
|
||||
const nonFactoryPlayersStart = playerContext[PlayerIndex.NonBuilderPlayersStart];
|
||||
|
||||
// add all factory-based players (which are apart of [style] and [class] bindings)
|
||||
for (let i = PlayerIndex.PlayerBuildersStartPosition + PlayerIndex.PlayerOffsetPosition;
|
||||
i < nonFactoryPlayersStart; i += PlayerIndex.PlayerAndPlayerBuildersTupleSize) {
|
||||
const player = playerContext[i] as Player | null;
|
||||
if (player) {
|
||||
players.push(player);
|
||||
}
|
||||
}
|
||||
|
||||
// add all custom players (not apart of [style] and [class] bindings)
|
||||
for (let i = nonFactoryPlayersStart; i < playerContext.length; i++) {
|
||||
players.push(playerContext[i] as Player);
|
||||
}
|
||||
|
||||
return players;
|
||||
}
|
||||
|
||||
|
||||
export function getOrCreatePlayerContext(target: {}, context?: LContext | null): PlayerContext|
|
||||
null {
|
||||
context = context || getContext(target) !;
|
||||
if (ngDevMode && !context) {
|
||||
throw new Error(
|
||||
'Only elements that exist in an Angular application can be used for player access');
|
||||
if (!context) {
|
||||
ngDevMode && throwInvalidRefError();
|
||||
return null;
|
||||
}
|
||||
|
||||
const {lViewData, nodeIndex} = context;
|
||||
const stylingContext = getStylingContext(nodeIndex - HEADER_OFFSET, lViewData);
|
||||
return stylingContext[StylingIndex.PlayerContext] || allocPlayerContext(stylingContext);
|
||||
return getPlayerContext(stylingContext) || allocPlayerContext(stylingContext);
|
||||
}
|
||||
|
||||
function allocPlayerContext(data: StylingContext): PlayerContext {
|
||||
return data[StylingIndex.PlayerContext] = [];
|
||||
export function getPlayerContext(stylingContext: StylingContext): PlayerContext|null {
|
||||
return stylingContext[StylingIndex.PlayerContext];
|
||||
}
|
||||
|
||||
export function allocPlayerContext(data: StylingContext): PlayerContext {
|
||||
return data[StylingIndex.PlayerContext] =
|
||||
[PlayerIndex.SinglePlayerBuildersStartPosition, null, null, null, null];
|
||||
}
|
||||
|
||||
export function throwInvalidRefError() {
|
||||
throw new Error('Only elements that exist in an Angular application can be used for animations');
|
||||
}
|
||||
|
Reference in New Issue
Block a user