2018-09-21 14:51:24 -07:00

914 lines
37 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 {StyleSanitizeFn} from '../sanitization/style_sanitizer';
import {AnimationContext} from './animations/interfaces';
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 `updateStylingMap`, `updateStyleProp`
* and `updateClassProp` 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/class 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 styling exists.
* 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` or `updateClassProp` (fixed set)
*
* multi: any styles that are updated using `updateStylingMap` (dynamic set)
*
* Note that context is only used to collect style information. Only when `renderStyling`
* 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 styling values 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;"
* class="foo"
*
* Then the initial state of the context (once initialized) will look like so:
*
* ```
* context = [
* element,
* styleSanitizer | null,
* [null, '100px', '200px', true], // property names are not needed since they have already been
* written to DOM.
*
* configMasterVal,
* 1, // this instructs how many `style` values there are so that class index values can be
* offsetted
* 'last class string applied',
*
* // 6
* 'width',
* pointers(1, 15); // Point to static `width`: `100px` and multi `width`.
* null,
*
* // 9
* 'height',
* pointers(2, 18); // Point to static `height`: `200px` and multi `height`.
* null,
*
* // 12
* 'foo',
* pointers(1, 21); // Point to static `foo`: `true` and multi `foo`.
* null,
*
* // 15
* 'width',
* pointers(1, 6); // Point to static `width`: `100px` and single `width`.
* null,
*
* // 18
* 'height',
* pointers(2, 9); // Point to static `height`: `200px` and single `height`.
* null,
*
* // 21
* 'foo',
* pointers(3, 12); // Point to static `foo`: `true` and single `foo`.
* 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 [class])
* and single ([style.prop] or [class.named]) 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` or `updateClassProp` are called then they
* must be called using an index value (not a property string) which references the index
* value of the initial style prop/class when the context was created. This also means that
* `updateStyleProp` or `updateClassProp` cannot be called with a new property (only
* `updateStylingMap` can include new CSS properties that will be added to the context).
*/
export interface StylingContext extends
Array<InitialStyles|number|string|boolean|LElementNode|StyleSanitizeFn|AnimationContext|null> {
/**
* Location of element that is used as a target for this context.
*/
[StylingIndex.ElementPosition]: LElementNode|null;
/**
* Location of animation context (which contains the active players) for this element styling
* context.
*/
[StylingIndex.AnimationContext]: AnimationContext|null;
/**
* The style sanitizer that is used within this context
*/
[StylingIndex.StyleSanitizerPosition]: StyleSanitizeFn|null;
/**
* Location of initial data shared by all instances of this style.
*/
[StylingIndex.InitialStylesPosition]: 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.
*/
[StylingIndex.MasterFlagPosition]: number;
/**
* A numeric value representing the class index offset value. Whenever a single class is
* applied (using `elementClassProp`) it should have an styling index value that doesn't
* need to take into account any style values that exist in the context.
*/
[StylingIndex.ClassOffsetPosition]: number;
/**
* The last CLASS STRING VALUE that was interpreted by elementStylingMap. This is cached
* So that the algorithm can exit early incase the string has not changed.
*/
[StylingIndex.CachedCssClassString]: string|null;
}
/**
* 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|boolean> { [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 = 0b000,
// Whether or not the entry or context itself is dirty
Dirty = 0b001,
// Whether or not this is a class-based assignment
Class = 0b010,
// Whether or not a sanitizer was applied to this property
Sanitize = 0b100,
// The max amount of bits used to represent these configuration values
BitCountSize = 3,
// There are only three bits here
BitMask = 0b111
}
/** 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
ElementPosition = 0,
// Position of where the initial styles are stored in the styling context
AnimationContext = 1,
// Position of where the style sanitizer is stored within the styling context
StyleSanitizerPosition = 2,
// Position of where the initial styles are stored in the styling context
InitialStylesPosition = 3,
// Index of location where the start of single properties are stored. (`updateStyleProp`)
MasterFlagPosition = 4,
// Index of location where the class index offset value is located
ClassOffsetPosition = 5,
// Position of where the last string-based CSS class value was stored
CachedCssClassString = 6,
// Location of single (prop) value entries are stored within the context
SingleStylesStartPosition = 7,
// 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 = 14, // (32 - 3) / 2 = ~14
// The binary digit value as a mask
BitMask = 0b11111111111111 // 14 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(
lElement: LElementNode | null, templateStyleContext: StylingContext): StylingContext {
// each instance gets a copy
const context = templateStyleContext.slice() as any as StylingContext;
context[StylingIndex.ElementPosition] = lElement;
return context;
}
export function createEmptyStylingContext(
element?: LElementNode | null, sanitizer?: StyleSanitizeFn | null,
initialStylingValues?: InitialStyles): StylingContext {
return [element || null, null, sanitizer || null, initialStylingValues || [null], 0, 0, null];
}
/**
* 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`.
*
* @param initialClassDeclarations a list of class declarations and initial class values
* that are used later within the styling context.
*
* -> ['foo', 'bar', SPECIAL_ENUM_VAL, 'foo', true]
* This implies that `foo` and `bar` will be later styled and that the `foo`
* class will be applied to the element as an initial class since it's true
*/
export function createStylingContextTemplate(
initialClassDeclarations?: (string | boolean | InitialStylingFlags)[] | null,
initialStyleDeclarations?: (string | boolean | InitialStylingFlags)[] | null,
styleSanitizer?: StyleSanitizeFn | null): StylingContext {
const initialStylingValues: InitialStyles = [null];
const context: StylingContext =
createEmptyStylingContext(null, styleSanitizer, initialStylingValues);
// we use two maps since a class name might collide with a CSS style prop
const stylesLookup: {[key: string]: number} = {};
const classesLookup: {[key: string]: number} = {};
let totalStyleDeclarations = 0;
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.VALUES_MODE) {
hasPassedDeclarations = true;
} else {
const prop = v as string;
if (hasPassedDeclarations) {
const value = initialStyleDeclarations[++i] as string;
initialStylingValues.push(value);
stylesLookup[prop] = initialStylingValues.length - 1;
} else {
totalStyleDeclarations++;
stylesLookup[prop] = 0;
}
}
}
}
// make where the class offsets begin
context[StylingIndex.ClassOffsetPosition] = totalStyleDeclarations;
if (initialClassDeclarations) {
let hasPassedDeclarations = false;
for (let i = 0; i < initialClassDeclarations.length; i++) {
const v = initialClassDeclarations[i] as string | boolean | InitialStylingFlags;
// this flag value marks where the declarations end the initial values begin
if (v === InitialStylingFlags.VALUES_MODE) {
hasPassedDeclarations = true;
} else {
const className = v as string;
if (hasPassedDeclarations) {
const value = initialClassDeclarations[++i] as boolean;
initialStylingValues.push(value);
classesLookup[className] = initialStylingValues.length - 1;
} else {
classesLookup[className] = 0;
}
}
}
}
const styleProps = Object.keys(stylesLookup);
const classNames = Object.keys(classesLookup);
const classNamesIndexStart = styleProps.length;
const totalProps = styleProps.length + classNames.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 < totalProps; i++) {
const isClassBased = i >= classNamesIndexStart;
const prop = isClassBased ? classNames[i - classNamesIndexStart] : styleProps[i];
const indexForInitial = isClassBased ? classesLookup[prop] : stylesLookup[prop];
const initialValue = initialStylingValues[indexForInitial];
const indexForMulti = i * StylingIndex.Size + multiStart;
const indexForSingle = i * StylingIndex.Size + singleStart;
const initialFlag = prepareInitialFlag(prop, isClassBased, styleSanitizer || null);
setFlag(context, indexForSingle, pointers(initialFlag, indexForInitial, indexForMulti));
setProp(context, indexForSingle, prop);
setValue(context, indexForSingle, null);
const flagForMulti =
initialFlag | (initialValue !== null ? StylingFlags.Dirty : StylingFlags.None);
setFlag(context, indexForMulti, pointers(flagForMulti, 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, initialStylingValues.length > 1);
return context;
}
const EMPTY_ARR: any[] = [];
const EMPTY_OBJ: {[key: string]: any} = {};
/**
* Sets and resolves all `multi` styling on an `StylingContext` so that they can be
* applied to the element once `renderStyling` 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
* in the context.
*
* @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.
*/
export function updateStylingMap(
context: StylingContext, classes: {[key: string]: any} | string | null,
styles?: {[key: string]: any} | null): void {
let classNames: string[] = EMPTY_ARR;
let applyAllClasses = false;
let ignoreAllClassUpdates = false;
// each time a string-based value pops up then it shouldn't require a deep
// check of what's changed.
if (typeof classes == 'string') {
const cachedClassString = context[StylingIndex.CachedCssClassString] as string | null;
if (cachedClassString && cachedClassString === classes) {
ignoreAllClassUpdates = true;
} else {
context[StylingIndex.CachedCssClassString] = classes;
classNames = classes.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;
context[StylingIndex.CachedCssClassString] = null;
}
classes = (classes || EMPTY_OBJ) as{[key: string]: any};
const styleProps = styles ? Object.keys(styles) : EMPTY_ARR;
styles = styles || EMPTY_OBJ;
const classesStartIndex = styleProps.length;
const multiStartIndex = getMultiStartIndex(context);
let dirty = false;
let ctxIndex = multiStartIndex;
let propIndex = 0;
const propLimit = styleProps.length + classNames.length;
// 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/classes
// are off-balance then they will be dealt in another loop after this one
while (ctxIndex < context.length && propIndex < propLimit) {
const isClassBased = propIndex >= classesStartIndex;
// when there is a cache-hit for a string-based class then we should
// avoid doing any work diffing any of the changes
if (!ignoreAllClassUpdates || !isClassBased) {
const adjustedPropIndex = isClassBased ? propIndex - classesStartIndex : propIndex;
const newProp: string =
isClassBased ? classNames[adjustedPropIndex] : styleProps[adjustedPropIndex];
const newValue: string|boolean =
isClassBased ? (applyAllClasses ? true : classes[newProp]) : styles[newProp];
const prop = getProp(context, ctxIndex);
if (prop === newProp) {
const value = getValue(context, ctxIndex);
const flag = getPointers(context, ctxIndex);
if (hasValueChanged(flag, 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
const valueToCompare = getValue(context, indexOfEntry);
const flagToCompare = getPointers(context, indexOfEntry);
swapMultiContextEntries(context, ctxIndex, indexOfEntry);
if (valueToCompare !== newValue) {
const initialValue = getInitialValue(context, flagToCompare);
setValue(context, ctxIndex, newValue);
if (initialValue !== newValue) {
setDirty(context, ctxIndex, true);
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);
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/classes and in this
// case the goal is to "remove" them from the context (by nullifying)
while (ctxIndex < context.length) {
const flag = getPointers(context, ctxIndex);
const isClassBased = (flag & StylingFlags.Class) === StylingFlags.Class;
if (ignoreAllClassUpdates && isClassBased) break;
const value = getValue(context, ctxIndex);
const doRemoveValue = valueExists(value, isClassBased);
if (doRemoveValue) {
setDirty(context, ctxIndex, true);
setValue(context, ctxIndex, null);
dirty = true;
}
ctxIndex += StylingIndex.Size;
}
// this means that there are left-over properties 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
const sanitizer = getStyleSanitizer(context);
while (propIndex < propLimit) {
const isClassBased = propIndex >= classesStartIndex;
if (ignoreAllClassUpdates && isClassBased) break;
const adjustedPropIndex = isClassBased ? propIndex - classesStartIndex : propIndex;
const prop = isClassBased ? classNames[adjustedPropIndex] : styleProps[adjustedPropIndex];
const value: string|boolean =
isClassBased ? (applyAllClasses ? true : classes[prop]) : styles[prop];
const flag = prepareInitialFlag(prop, isClassBased, sanitizer) | StylingFlags.Dirty;
context.push(flag, prop, value);
propIndex++;
dirty = true;
}
if (dirty) {
setContextDirty(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.
*
* 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
* then any styles/classes 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 | boolean | 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 (hasValueChanged(currFlag, 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;
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;
singleDirty = false;
}
setDirty(context, indexForMulti, multiDirty);
setDirty(context, singleIndex, singleDirty);
setContextDirty(context, true);
}
}
}
/**
* This method will toggle the referenced CSS class (by the provided index)
* within the given context.
*
* @param context The styling context that will be updated with the
* newly provided class value.
* @param index The index of the CSS class which is being updated.
* @param addOrRemove Whether or not to add or remove the CSS class
*/
export function updateClassProp(
context: StylingContext, index: number, addOrRemove: boolean): void {
const adjustedIndex = index + context[StylingIndex.ClassOffsetPosition];
updateStyleProp(context, adjustedIndex, addOrRemove);
}
/**
* Renders all queued styling using a renderer onto the given element.
*
* This function works by rendering any styles (that have been applied
* using `updateStylingMap`) and any classes (that have been applied using
* `updateStyleProp`) onto the provided element using the provided renderer.
* Just before the styles/classes are rendered a final key/value style map
* will be assembled (if `styleStore` or `classStore` are provided).
*
* @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.
* @param classStore if provided, the updated class values will be applied
* to this key/value map instead of being renderered via the renderer.
*/
export function renderStyling(
context: StylingContext, renderer: Renderer3, styleStore?: {[key: string]: any},
classStore?: {[key: string]: boolean}) {
if (isContextDirty(context)) {
const native = context[StylingIndex.ElementPosition] !.native;
const multiStartIndex = getMultiStartIndex(context);
const styleSanitizer = getStyleSanitizer(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 isClassBased = flag & StylingFlags.Class ? true : false;
const isInSingleRegion = i < multiStartIndex;
let valueToApply: string|boolean|null = value;
// VALUE 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 && !valueExists(valueToApply, isClassBased)) {
// single values ALWAYS have a reference to a multi index
const multiIndex = getMultiOrSingleIndex(flag);
valueToApply = getValue(context, multiIndex);
}
// VALUE DEFER CASE 2: Use the initial value if all else fails (is falsy)
// the initial value will always be a string or null,
// therefore we can safely adopt it incase there's nothing else
// note that this should always be a falsy check since `false` is used
// for both class and style comparisons (styles can't be false and false
// classes are turned off and should therefore defer to their initial values)
if (!valueExists(valueToApply, isClassBased)) {
valueToApply = getInitialValue(context, flag);
}
if (isClassBased) {
setClass(native, prop, valueToApply ? true : false, renderer, classStore);
} else {
const sanitizer = (flag & StylingFlags.Sanitize) ? styleSanitizer : null;
setStyle(native, prop, valueToApply as string | null, renderer, sanitizer, styleStore);
}
setDirty(context, i, false);
}
}
setContextDirty(context, false);
}
}
/**
* This function renders a given CSS prop/value entry using the
* provided renderer. If a `store` 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 store 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,
sanitizer: StyleSanitizeFn | null, store?: {[key: string]: any}) {
value = sanitizer && value ? sanitizer(prop, value) : value;
if (store) {
store[prop] = value;
} else if (value) {
ngDevMode && ngDevMode.rendererSetStyle++;
isProceduralRenderer(renderer) ?
renderer.setStyle(native, prop, value, RendererStyleFlags3.DashCase) :
native['style'].setProperty(prop, value);
} else {
ngDevMode && ngDevMode.rendererRemoveStyle++;
isProceduralRenderer(renderer) ?
renderer.removeStyle(native, prop, RendererStyleFlags3.DashCase) :
native['style'].removeProperty(prop);
}
}
/**
* This function renders a given CSS class value using the provided
* renderer (by adding or removing it from the provided element).
* If a `store` 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 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;
} else if (add) {
ngDevMode && ngDevMode.rendererAddClass++;
isProceduralRenderer(renderer) ? renderer.addClass(native, className) :
native['classList'].add(className);
} else {
ngDevMode && ngDevMode.rendererRemoveClass++;
isProceduralRenderer(renderer) ? renderer.removeClass(native, className) :
native['classList'].remove(className);
}
}
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 isClassBased(context: StylingContext, index: number): boolean {
const adjustedIndex =
index >= StylingIndex.SingleStylesStartPosition ? (index + StylingIndex.FlagsOffset) : index;
return ((context[adjustedIndex] as number) & StylingFlags.Class) == StylingFlags.Class;
}
function isSanitizable(context: StylingContext, index: number): boolean {
const adjustedIndex =
index >= StylingIndex.SingleStylesStartPosition ? (index + StylingIndex.FlagsOffset) : index;
return ((context[adjustedIndex] as number) & StylingFlags.Sanitize) == StylingFlags.Sanitize;
}
function pointers(configFlag: number, staticIndex: number, dynamicIndex: number) {
return (configFlag & StylingFlags.BitMask) | (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 getStyleSanitizer(context: StylingContext): StyleSanitizeFn|null {
return context[StylingIndex.StyleSanitizerPosition];
}
function setProp(context: StylingContext, index: number, prop: string) {
context[index + StylingIndex.PropertyOffset] = prop;
}
function setValue(context: StylingContext, index: number, value: string | null | boolean) {
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|boolean|null {
return context[index + StylingIndex.ValueOffset] as string | boolean | 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 flagValue = (isDirty(context, singleIndex) ? StylingFlags.Dirty : StylingFlags.None) |
(isClassBased(context, singleIndex) ? StylingFlags.Class : StylingFlags.None) |
(isSanitizable(context, singleIndex) ? StylingFlags.Sanitize : StylingFlags.None);
const updatedFlag = pointers(flagValue, initialIndexForSingle, i);
setFlag(context, singleIndex, updatedFlag);
}
}
}
function insertNewMultiProperty(
context: StylingContext, index: number, classBased: boolean, name: string, flag: number,
value: string | boolean): 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);
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);
}
}
function valueExists(value: string | null | boolean, isClassBased?: boolean) {
if (isClassBased) {
return value ? true : false;
}
return value !== null;
}
function prepareInitialFlag(
name: string, isClassBased: boolean, sanitizer?: StyleSanitizeFn | null) {
if (isClassBased) {
return StylingFlags.Class;
} else if (sanitizer && sanitizer(name)) {
return StylingFlags.Sanitize;
}
return StylingFlags.None;
}
function hasValueChanged(
flag: number, a: string | boolean | null, b: string | boolean | null): boolean {
const isClassBased = flag & StylingFlags.Class;
const hasValues = a && b;
const usesSanitizer = flag & StylingFlags.Sanitize;
// the toString() comparison ensures that a value is checked
// ... otherwise (during sanitization bypassing) the === comparsion
// would fail since a new String() instance is created
if (!isClassBased && hasValues && usesSanitizer) {
// we know for sure we're dealing with strings at this point
return (a as string).toString() !== (b as string).toString();
}
// everything else is safe to check with a normal equality check
return a !== b;
}