/** * @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 { /** * 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 { [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); } }