622 lines
24 KiB
TypeScript

/**
* @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 {InitialStylingFlags} from './interfaces/definition';
import {LElementNode} from './interfaces/node';
import {Renderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces/renderer';
/**
* The styling context acts as a styling manifest (shaped as an array) for determining which
* styling properties have been assigned via the provided `updateStyleMap` and `updateStyleProp`
* functions. There are also two initialization functions `allocStylingContext` and
* `createStylingContextTemplate` which are used to initialize and/or clone the context.
*
* The context is an array where the first two cells are used for static data (initial styling)
* and dirty flags / index offsets). The remaining set of cells is used for multi (map) and single
* (prop) style values.
*
* each value from here onwards is mapped as so:
* [i] = mutation/type flag for the style value
* [i + 1] = prop string (or null incase it has been removed)
* [i + 2] = value string (or null incase it has been removed)
*
* There are three types of styling types stored in this context:
* initial: any styles that are passed in once the context is created
* (these are stored in the first cell of the array and the first
* value of this array is always `null` even if no initial styles exist.
* the `null` value is there so that any new styles have a parent to point
* to. This way we can always assume that there is a parent.)
*
* single: any styles that are updated using `updateStyleProp` (fixed set)
*
* multi: any styles that are updated using `updateStyleMap` (dynamic set)
*
* Note that context is only used to collect style information. Only when `renderStyles`
* is called is when the styling payload will be rendered (or built as a key/value map).
*
* When the context is created, depending on what initial styles are passed in, the context itself
* will be pre-filled with slots based on the initial style properties. Say for example we have a
* series of initial styles that look like so:
*
* style="width:100px; height:200px;"
*
* Then the initial state of the context (once initialized) will look like so:
*
* ```
* context = [
* [null, '100px', '200px'], // property names are not needed since they have already been
* written to DOM.
*
* configMasterVal,
*
* // 2
* 'width',
* pointers(1, 8); // Point to static `width`: `100px` and multi `width`.
* null,
*
* // 5
* 'height',
* pointers(2, 11); // Point to static `height`: `200px` and multi `height`.
* null,
*
* // 8
* 'width',
* pointers(1, 2); // Point to static `width`: `100px` and single `width`.
* null,
*
* // 11
* 'height',
* pointers(2, 5); // Point to static `height`: `200px` and single `height`.
* null,
* ]
*
* function pointers(staticIndex: number, dynamicIndex: number) {
* // combine the two indices into a single word.
* return (staticIndex << StylingFlags.BitCountSize) |
* (dynamicIndex << (StylingIndex.BitCountSize + StylingFlags.BitCountSize));
* }
* ```
*
* The values are duplicated so that space is set aside for both multi ([style])
* and single ([style.prop]) values. The respective config values (configValA, configValB, etc...)
* are a combination of the StylingFlags with two index values: the `initialIndex` (which points to
* the index location of the style value in the initial styles array in slot 0) and the
* `dynamicIndex` (which points to the matching single/multi index position in the context array
* for the same prop).
*
* This means that every time `updateStyleProp` is called it must be called using an index value
* (not a property string) which references the index value of the initial style when the context
* was created. This also means that `updateStyleProp` cannot be called with a new property
* (only `updateStyleMap` can include new CSS properties that will be added to the context).
*/
export interface StylingContext extends Array<InitialStyles|number|string|null> {
/**
* Location of initial data shared by all instances of this style.
*/
[0]: InitialStyles;
/**
* A numeric value representing the configuration status (whether the context is dirty or not)
* mixed together (using bit shifting) with a index value which tells the starting index value
* of where the multi style entries begin.
*/
[1]: number;
}
/**
* The initial styles is populated whether or not there are any initial styles passed into
* the context during allocation. The 0th value must be null so that index values of `0` within
* the context flags can always point to a null value safely when nothing is set.
*
* All other entries in this array are of `string` value and correspond to the values that
* were extracted from the `style=""` attribute in the HTML code for the provided template.
*/
export interface InitialStyles extends Array<string|null> { [0]: null; }
/**
* Used to set the context to be dirty or not both on the master flag (position 1)
* or for each single/multi property that exists in the context.
*/
export const enum StylingFlags {
// Implies no configurations
None = 0b0,
// Whether or not the entry or context itself is dirty
Dirty = 0b1,
// The max amount of bits used to represent these configuration values
BitCountSize = 1,
}
/** Used as numeric pointer values to determine what cells to update in the `StylingContext` */
export const enum StylingIndex {
// Position of where the initial styles are stored in the styling context
InitialStylesPosition = 0,
// Index of location where the start of single properties are stored. (`updateStyleProp`)
MasterFlagPosition = 1,
// Location of single (prop) value entries are stored within the context
SingleStylesStartPosition = 2,
// Multi and single entries are stored in `StylingContext` as: Flag; PropertyName; PropertyValue
FlagsOffset = 0,
PropertyOffset = 1,
ValueOffset = 2,
// Size of each multi or single entry (flag + prop + value)
Size = 3,
// Each flag has a binary digit length of this value
BitCountSize = 15, // (32 - 1) / 2 = ~15
// The binary digit value as a mask
BitMask = 0b111111111111111 // 15 bits
}
/**
* Used clone a copy of a pre-computed template of a styling context.
*
* A pre-computed template is designed to be computed once for a given element
* (instructions.ts has logic for caching this).
*/
export function allocStylingContext(templateStyleContext: StylingContext): StylingContext {
// each instance gets a copy
return templateStyleContext.slice() as any as StylingContext;
}
/**
* Creates a styling context template where styling information is stored.
* Any styles that are later referenced using `updateStyleProp` must be
* passed in within this function. Initial values for those styles are to
* be declared after all initial style properties are declared (this change in
* mode between declarations and initial styles is made possible using a special
* enum value found in `definition.ts`).
*
* @param initialStyleDeclarations a list of style declarations and initial style values
* that are used later within the styling context.
*
* -> ['width', 'height', SPECIAL_ENUM_VAL, 'width', '100px']
* This implies that `width` and `height` will be later styled and that the `width`
* property has an initial value of `100px`.
*/
export function createStylingContextTemplate(
initialStyleDeclarations?: (string | InitialStylingFlags)[] | null): StylingContext {
const initialStyles: InitialStyles = [null];
const context: StylingContext = [initialStyles, 0];
const indexLookup: {[key: string]: number} = {};
if (initialStyleDeclarations) {
let hasPassedDeclarations = false;
for (let i = 0; i < initialStyleDeclarations.length; i++) {
const v = initialStyleDeclarations[i] as string | InitialStylingFlags;
// this flag value marks where the declarations end the initial values begin
if (v === InitialStylingFlags.INITIAL_STYLES) {
hasPassedDeclarations = true;
} else {
const prop = v as string;
if (hasPassedDeclarations) {
const value = initialStyleDeclarations[++i] as string;
initialStyles.push(value);
indexLookup[prop] = initialStyles.length - 1;
} else {
// it's safe to use `0` since the default initial value for
// each property will always be null (which is at position 0)
indexLookup[prop] = 0;
}
}
}
}
const allProps = Object.keys(indexLookup);
const totalProps = allProps.length;
// *2 because we are filling for both single and multi style spaces
const maxLength = totalProps * StylingIndex.Size * 2 + StylingIndex.SingleStylesStartPosition;
// we need to fill the array from the start so that we can access
// both the multi and the single array positions in the same loop block
for (let i = StylingIndex.SingleStylesStartPosition; i < maxLength; i++) {
context.push(null);
}
const singleStart = StylingIndex.SingleStylesStartPosition;
const multiStart = totalProps * StylingIndex.Size + StylingIndex.SingleStylesStartPosition;
// fill single and multi-level styles
for (let i = 0; i < allProps.length; i++) {
const prop = allProps[i];
const indexForInitial = indexLookup[prop];
const indexForMulti = i * StylingIndex.Size + multiStart;
const indexForSingle = i * StylingIndex.Size + singleStart;
setFlag(context, indexForSingle, pointers(StylingFlags.None, indexForInitial, indexForMulti));
setProp(context, indexForSingle, prop);
setValue(context, indexForSingle, null);
setFlag(context, indexForMulti, pointers(StylingFlags.Dirty, indexForInitial, indexForSingle));
setProp(context, indexForMulti, prop);
setValue(context, indexForMulti, null);
}
// there is no initial value flag for the master index since it doesn't reference an initial style
// value
setFlag(context, StylingIndex.MasterFlagPosition, pointers(0, 0, multiStart));
setContextDirty(context, initialStyles.length > 1);
return context;
}
const EMPTY_ARR: any[] = [];
/**
* Sets and resolves all `multi` styles on an `StylingContext` so that they can be
* applied to the element once `renderStyles` is called.
*
* All missing styles (any values that are not provided in the new `styles` param)
* will resolve to `null` within their respective positions in the context.
*
* @param context The styling context that will be updated with the
* newly provided style values.
* @param styles The key/value map of CSS styles that will be used for the update.
*/
export function updateStyleMap(context: StylingContext, styles: {[key: string]: any} | null): void {
const propsToApply = styles ? Object.keys(styles) : EMPTY_ARR;
const multiStartIndex = getMultiStartIndex(context);
let dirty = false;
let ctxIndex = multiStartIndex;
let propIndex = 0;
// the main loop here will try and figure out how the shape of the provided
// styles differ with respect to the context. Later if the context/styles are
// off-balance then they will be dealt in another loop after this one
while (ctxIndex < context.length && propIndex < propsToApply.length) {
const flag = getPointers(context, ctxIndex);
const prop = getProp(context, ctxIndex);
const value = getValue(context, ctxIndex);
const newProp = propsToApply[propIndex];
const newValue = styles ![newProp];
if (prop === newProp) {
if (value !== newValue) {
setValue(context, ctxIndex, newValue);
const initialValue = getInitialValue(context, flag);
// there is no point in setting this to dirty if the previously
// rendered value was being referenced by the initial style (or null)
if (initialValue !== newValue) {
setDirty(context, ctxIndex, true);
dirty = true;
}
}
} else {
const indexOfEntry = findEntryPositionByProp(context, newProp, ctxIndex);
if (indexOfEntry > 0) {
// it was found at a later point ... just swap the values
swapMultiContextEntries(context, ctxIndex, indexOfEntry);
if (value !== newValue) {
setValue(context, ctxIndex, newValue);
dirty = true;
}
} else {
// we only care to do this if the insertion is in the middle
const doShift = ctxIndex < context.length;
insertNewMultiProperty(context, ctxIndex, newProp, newValue);
dirty = true;
}
}
ctxIndex += StylingIndex.Size;
propIndex++;
}
// this means that there are left-over values in the context that
// were not included in the provided styles and in this case the
// goal is to "remove" them from the context (by nullifying)
while (ctxIndex < context.length) {
const value = context[ctxIndex + StylingIndex.ValueOffset];
if (value !== null) {
setDirty(context, ctxIndex, true);
setValue(context, ctxIndex, null);
dirty = true;
}
ctxIndex += StylingIndex.Size;
}
// this means that there are left-over property in the context that
// were not detected in the context during the loop above. In that
// case we want to add the new entries into the list
while (propIndex < propsToApply.length) {
const prop = propsToApply[propIndex];
const value = styles ![prop];
context.push(StylingFlags.Dirty, prop, value);
propIndex++;
dirty = true;
}
if (dirty) {
setContextDirty(context, true);
}
}
/**
* Sets and resolves a single CSS style on a property on an `StylingContext` so that they
* can be applied to the element once `renderElementStyles` is called.
*
* Note that prop-level styles are considered higher priority than styles that are applied
* using `updateStyleMap`, therefore, when styles are rendered then any styles that
* have been applied using this function will be considered first (then multi values second
* and then initial values as a backup).
*
* @param context The styling context that will be updated with the
* newly provided style value.
* @param index The index of the property which is being updated.
* @param value The CSS style value that will be assigned
*/
export function updateStyleProp(
context: StylingContext, index: number, value: string | null): void {
const singleIndex = StylingIndex.SingleStylesStartPosition + index * StylingIndex.Size;
const currValue = getValue(context, singleIndex);
const currFlag = getPointers(context, singleIndex);
// didn't change ... nothing to make a note of
if (currValue !== value) {
// the value will always get updated (even if the dirty flag is skipped)
setValue(context, singleIndex, value);
const indexForMulti = getMultiOrSingleIndex(currFlag);
// if the value is the same in the multi-area then there's no point in re-assembling
const valueForMulti = getValue(context, indexForMulti);
if (!valueForMulti || valueForMulti !== value) {
let multiDirty = false;
let singleDirty = true;
// only when the value is set to `null` should the multi-value get flagged
if (value == null && valueForMulti) {
multiDirty = true;
singleDirty = false;
}
setDirty(context, indexForMulti, multiDirty);
setDirty(context, singleIndex, singleDirty);
setContextDirty(context, true);
}
}
}
/**
* Renders all queued styles using a renderer onto the given element.
*
* This function works by rendering any styles (that have been applied
* using `updateStyleMap` and `updateStyleProp`) onto the
* provided element using the provided renderer. Just before the styles
* are rendered a final key/value style map will be assembled.
*
* @param lElement the element that the styles will be rendered on
* @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
* to this key/value map instead of being renderered via the renderer.
* @returns an object literal. `{ color: 'red', height: 'auto'}`.
*/
export function renderStyles(
lElement: LElementNode, context: StylingContext, renderer: Renderer3,
styleStore?: {[key: string]: any}) {
if (isContextDirty(context)) {
const native = lElement.native;
const multiStartIndex = getMultiStartIndex(context);
for (let i = StylingIndex.SingleStylesStartPosition; i < context.length;
i += StylingIndex.Size) {
// there is no point in rendering styles that have not changed on screen
if (isDirty(context, i)) {
const prop = getProp(context, i);
const value = getValue(context, i);
const flag = getPointers(context, i);
const isInSingleRegion = i < multiStartIndex;
let styleToApply: string|null = value;
// STYLE DEFER CASE 1: Use a multi value instead of a null single value
// this check implies that a single value was removed and we
// should now defer to a multi value and use that (if set).
if (isInSingleRegion && styleToApply == null) {
// single values ALWAYS have a reference to a multi index
const multiIndex = getMultiOrSingleIndex(flag);
styleToApply = getValue(context, multiIndex);
}
// STYLE DEFER CASE 2: Use the initial value if all else fails (is null)
// the initial value will always be a string or null,
// therefore we can safely adopt it incase there's nothing else
if (styleToApply == null) {
styleToApply = getInitialValue(context, flag);
}
setStyle(native, prop, styleToApply, renderer, styleStore);
setDirty(context, i, false);
}
}
setContextDirty(context, false);
}
}
/**
* This function renders a given CSS prop/value entry using the
* provided renderer. If a `styleStore` value is provided then
* that will be used a render context instead of the provided
* renderer.
*
* @param native the DOM Element
* @param prop the CSS style property that will be rendered
* @param value the CSS style value that will be rendered
* @param renderer
* @param styleStore an optional key/value map that will be used as a context to render styles on
*/
function setStyle(
native: any, prop: string, value: string | null, renderer: Renderer3,
styleStore?: {[key: string]: any}) {
if (styleStore) {
styleStore[prop] = value;
} else if (value == null) {
ngDevMode && ngDevMode.rendererRemoveStyle++;
isProceduralRenderer(renderer) ?
renderer.removeStyle(native, prop, RendererStyleFlags3.DashCase) :
native['style'].removeProperty(prop);
} else {
ngDevMode && ngDevMode.rendererSetStyle++;
isProceduralRenderer(renderer) ?
renderer.setStyle(native, prop, value, RendererStyleFlags3.DashCase) :
native['style'].setProperty(prop, value);
}
}
function setDirty(context: StylingContext, index: number, isDirtyYes: boolean) {
const adjustedIndex =
index >= StylingIndex.SingleStylesStartPosition ? (index + StylingIndex.FlagsOffset) : index;
if (isDirtyYes) {
(context[adjustedIndex] as number) |= StylingFlags.Dirty;
} else {
(context[adjustedIndex] as number) &= ~StylingFlags.Dirty;
}
}
function isDirty(context: StylingContext, index: number): boolean {
const adjustedIndex =
index >= StylingIndex.SingleStylesStartPosition ? (index + StylingIndex.FlagsOffset) : index;
return ((context[adjustedIndex] as number) & StylingFlags.Dirty) == StylingFlags.Dirty;
}
function pointers(configFlag: number, staticIndex: number, dynamicIndex: number) {
return (configFlag & StylingFlags.Dirty) | (staticIndex << StylingFlags.BitCountSize) |
(dynamicIndex << (StylingIndex.BitCountSize + StylingFlags.BitCountSize));
}
function getInitialValue(context: StylingContext, flag: number): string|null {
const index = getInitialIndex(flag);
return context[StylingIndex.InitialStylesPosition][index] as null | string;
}
function getInitialIndex(flag: number): number {
return (flag >> StylingFlags.BitCountSize) & StylingIndex.BitMask;
}
function getMultiOrSingleIndex(flag: number): number {
const index =
(flag >> (StylingIndex.BitCountSize + StylingFlags.BitCountSize)) & StylingIndex.BitMask;
return index >= StylingIndex.SingleStylesStartPosition ? index : -1;
}
function getMultiStartIndex(context: StylingContext): number {
return getMultiOrSingleIndex(context[StylingIndex.MasterFlagPosition]) as number;
}
function setProp(context: StylingContext, index: number, prop: string) {
context[index + StylingIndex.PropertyOffset] = prop;
}
function setValue(context: StylingContext, index: number, value: string | null) {
context[index + StylingIndex.ValueOffset] = value;
}
function setFlag(context: StylingContext, index: number, flag: number) {
const adjustedIndex =
index === StylingIndex.MasterFlagPosition ? index : (index + StylingIndex.FlagsOffset);
context[adjustedIndex] = flag;
}
function getPointers(context: StylingContext, index: number): number {
const adjustedIndex =
index === StylingIndex.MasterFlagPosition ? index : (index + StylingIndex.FlagsOffset);
return context[adjustedIndex] as number;
}
function getValue(context: StylingContext, index: number): string|null {
return context[index + StylingIndex.ValueOffset] as string | null;
}
function getProp(context: StylingContext, index: number): string {
return context[index + StylingIndex.PropertyOffset] as string;
}
export function isContextDirty(context: StylingContext): boolean {
return isDirty(context, StylingIndex.MasterFlagPosition);
}
export function setContextDirty(context: StylingContext, isDirtyYes: boolean): void {
setDirty(context, StylingIndex.MasterFlagPosition, isDirtyYes);
}
function findEntryPositionByProp(
context: StylingContext, prop: string, startIndex?: number): number {
for (let i = (startIndex || 0) + StylingIndex.PropertyOffset; i < context.length;
i += StylingIndex.Size) {
const thisProp = context[i];
if (thisProp == prop) {
return i - StylingIndex.PropertyOffset;
}
}
return -1;
}
function swapMultiContextEntries(context: StylingContext, indexA: number, indexB: number) {
const tmpValue = getValue(context, indexA);
const tmpProp = getProp(context, indexA);
const tmpFlag = getPointers(context, indexA);
let flagA = tmpFlag;
let flagB = getPointers(context, indexB);
const singleIndexA = getMultiOrSingleIndex(flagA);
if (singleIndexA >= 0) {
const _flag = getPointers(context, singleIndexA);
const _initial = getInitialIndex(_flag);
setFlag(context, singleIndexA, pointers(_flag, _initial, indexB));
}
const singleIndexB = getMultiOrSingleIndex(flagB);
if (singleIndexB >= 0) {
const _flag = getPointers(context, singleIndexB);
const _initial = getInitialIndex(_flag);
setFlag(context, singleIndexB, pointers(_flag, _initial, indexA));
}
setValue(context, indexA, getValue(context, indexB));
setProp(context, indexA, getProp(context, indexB));
setFlag(context, indexA, getPointers(context, indexB));
setValue(context, indexB, tmpValue);
setProp(context, indexB, tmpProp);
setFlag(context, indexB, tmpFlag);
}
function updateSinglePointerValues(context: StylingContext, indexStartPosition: number) {
for (let i = indexStartPosition; i < context.length; i += StylingIndex.Size) {
const multiFlag = getPointers(context, i);
const singleIndex = getMultiOrSingleIndex(multiFlag);
if (singleIndex > 0) {
const singleFlag = getPointers(context, singleIndex);
const initialIndexForSingle = getInitialIndex(singleFlag);
const updatedFlag = pointers(
isDirty(context, singleIndex) ? StylingFlags.Dirty : StylingFlags.None,
initialIndexForSingle, i);
setFlag(context, singleIndex, updatedFlag);
}
}
}
function insertNewMultiProperty(
context: StylingContext, index: number, name: string, value: string): void {
const doShift = index < context.length;
// prop does not exist in the list, add it in
context.splice(index, 0, StylingFlags.Dirty, name, value);
if (doShift) {
// because the value was inserted midway into the array then we
// need to update all the shifted multi values' single value
// pointers to point to the newly shifted location
updateSinglePointerValues(context, index + StylingIndex.Size);
}
}