refactor(ivy): move all styling util code into utils/styling_utils.ts (#32731)

PR Close #32731
This commit is contained in:
Matias Niemelä
2019-09-17 14:59:20 -07:00
committed by Andrew Kushnir
parent 5d12cb9fdf
commit 0450f39625
14 changed files with 13 additions and 14 deletions

View File

@ -10,9 +10,9 @@ import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanit
import {ProceduralRenderer3, RElement, Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer';
import {ApplyStylingFn, LStylingData, StylingMapArray, StylingMapArrayIndex, StylingMapsSyncMode, SyncStylingMapsFn, TStylingConfig, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from '../interfaces/styling';
import {NO_CHANGE} from '../tokens';
import {DEFAULT_BINDING_INDEX, DEFAULT_BINDING_VALUE, DEFAULT_GUARD_MASK_VALUE, MAP_BASED_ENTRY_PROP_NAME, getBindingValue, getConfig, getDefaultValue, getGuardMask, getMapProp, getMapValue, getProp, getPropValuesStartPosition, getStylingMapArray, getTotalSources, getValue, getValuesCount, hasConfig, hasValueChanged, isContextLocked, isHostStylingActive, isSanitizationRequired, isStylingValueDefined, lockContext, patchConfig, setDefaultValue, setGuardMask, setMapAsDirty, setValue} from '../util/styling_utils';
import {getStylingState, resetStylingState} from './state';
import {DEFAULT_BINDING_INDEX, DEFAULT_BINDING_VALUE, DEFAULT_GUARD_MASK_VALUE, MAP_BASED_ENTRY_PROP_NAME, getBindingValue, getConfig, getDefaultValue, getGuardMask, getMapProp, getMapValue, getProp, getPropValuesStartPosition, getStylingMapArray, getTotalSources, getValue, getValuesCount, hasConfig, hasValueChanged, isContextLocked, isHostStylingActive, isSanitizationRequired, isStylingValueDefined, lockContext, patchConfig, setDefaultValue, setGuardMask, setMapAsDirty, setValue} from './util';

View File

@ -9,9 +9,9 @@ import {unwrapSafeValue} from '../../sanitization/bypass';
import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanitizer';
import {ProceduralRenderer3, RElement, Renderer3} from '../interfaces/renderer';
import {ApplyStylingFn, LStylingData, StylingMapArray, StylingMapArrayIndex, StylingMapsSyncMode, SyncStylingMapsFn, TStylingContext, TStylingContextIndex} from '../interfaces/styling';
import {getBindingValue, getMapProp, getMapValue, getValue, getValuesCount, isStylingValueDefined} from '../util/styling_utils';
import {setStylingMapsSyncFn} from './bindings';
import {getBindingValue, getMapProp, getMapValue, getValue, getValuesCount, isStylingValueDefined} from './util';

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {RElement} from '../interfaces/renderer';
import {TEMPLATE_DIRECTIVE_INDEX} from './util';
import {TEMPLATE_DIRECTIVE_INDEX} from '../util/styling_utils';
/**
* --------

View File

@ -10,10 +10,10 @@ import {RElement} from '../interfaces/renderer';
import {ApplyStylingFn, LStylingData, TStylingConfig, TStylingContext, TStylingContextIndex} from '../interfaces/styling';
import {getCurrentStyleSanitizer} from '../state';
import {attachDebugObject} from '../util/debug_utils';
import {allowDirectStyling as _allowDirectStyling, getDefaultValue, getGuardMask, getProp, getPropValuesStartPosition, getValuesCount, hasConfig, isContextLocked, isMapBased, isSanitizationRequired} from '../util/styling_utils';
import {applyStylingViaContext} from './bindings';
import {activateStylingMapFeature} from './map_based_bindings';
import {allowDirectStyling as _allowDirectStyling, getDefaultValue, getGuardMask, getProp, getPropValuesStartPosition, getValuesCount, hasConfig, isContextLocked, isMapBased, isSanitizationRequired} from './util';

View File

@ -1,420 +0,0 @@
/**
* @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 {TNode, TNodeFlags} from '../interfaces/node';
import {LStylingData, StylingMapArray, StylingMapArrayIndex, TStylingConfig, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from '../interfaces/styling';
import {NO_CHANGE} from '../tokens';
export const MAP_BASED_ENTRY_PROP_NAME = '[MAP]';
export const TEMPLATE_DIRECTIVE_INDEX = 0;
/**
* Default fallback value for a styling binding.
*
* A value of `null` is used here which signals to the styling algorithm that
* the styling value is not present. This way if there are no other values
* detected then it will be removed once the style/class property is dirty and
* diffed within the styling algorithm present in `flushStyling`.
*/
export const DEFAULT_BINDING_VALUE = null;
export const DEFAULT_BINDING_INDEX = 0;
const DEFAULT_TOTAL_SOURCES = 1;
// The first bit value reflects a map-based binding value's bit.
// The reason why it's always activated for every entry in the map
// is so that if any map-binding values update then all other prop
// based bindings will pass the guard check automatically without
// any extra code or flags.
export const DEFAULT_GUARD_MASK_VALUE = 0b1;
/**
* Creates a new instance of the `TStylingContext`.
*
* The `TStylingContext` is used as a manifest of all style or all class bindings on
* an element. Because it is a T-level data-structure, it is only created once per
* tNode for styles and for classes. This function allocates a new instance of a
* `TStylingContext` with the initial values (see `interfaces.ts` for more info).
*/
export function allocTStylingContext(initialStyling?: StylingMapArray | null): TStylingContext {
initialStyling = initialStyling || allocStylingMapArray();
return [
TStylingConfig.Initial, // 1) config for the styling context
DEFAULT_TOTAL_SOURCES, // 2) total amount of styling sources (template, directives, etc...)
initialStyling, // 3) initial styling values
];
}
export function allocStylingMapArray(): StylingMapArray {
return [''];
}
export function getConfig(context: TStylingContext) {
return context[TStylingContextIndex.ConfigPosition];
}
export function hasConfig(context: TStylingContext, flag: TStylingConfig) {
return (getConfig(context) & flag) !== 0;
}
/**
* Determines whether or not to apply styles/classes directly or via context resolution.
*
* There are three cases that are matched here:
* 1. context is locked for template or host bindings (depending on `hostBindingsMode`)
* 2. There are no collisions (i.e. properties with more than one binding)
* 3. There are only "prop" or "map" bindings present, but not both
*/
export function allowDirectStyling(context: TStylingContext, hostBindingsMode: boolean): boolean {
const config = getConfig(context);
return ((config & getLockedConfig(hostBindingsMode)) !== 0) &&
((config & TStylingConfig.HasCollisions) === 0) &&
((config & TStylingConfig.HasPropAndMapBindings) !== TStylingConfig.HasPropAndMapBindings);
}
export function setConfig(context: TStylingContext, value: TStylingConfig): void {
context[TStylingContextIndex.ConfigPosition] = value;
}
export function patchConfig(context: TStylingContext, flag: TStylingConfig): void {
context[TStylingContextIndex.ConfigPosition] |= flag;
}
export function getProp(context: TStylingContext, index: number): string {
return context[index + TStylingContextIndex.PropOffset] as string;
}
function getPropConfig(context: TStylingContext, index: number): number {
return (context[index + TStylingContextIndex.ConfigOffset] as number) &
TStylingContextPropConfigFlags.Mask;
}
export function isSanitizationRequired(context: TStylingContext, index: number): boolean {
return (getPropConfig(context, index) & TStylingContextPropConfigFlags.SanitizationRequired) !==
0;
}
export function getGuardMask(
context: TStylingContext, index: number, isHostBinding: boolean): number {
const position = index + (isHostBinding ? TStylingContextIndex.HostBindingsBitGuardOffset :
TStylingContextIndex.TemplateBitGuardOffset);
return context[position] as number;
}
export function setGuardMask(
context: TStylingContext, index: number, maskValue: number, isHostBinding: boolean) {
const position = index + (isHostBinding ? TStylingContextIndex.HostBindingsBitGuardOffset :
TStylingContextIndex.TemplateBitGuardOffset);
context[position] = maskValue;
}
export function getValuesCount(context: TStylingContext): number {
return getTotalSources(context) + 1;
}
export function getTotalSources(context: TStylingContext): number {
return context[TStylingContextIndex.TotalSourcesPosition];
}
export function getBindingValue(context: TStylingContext, index: number, offset: number) {
return context[index + TStylingContextIndex.BindingsStartOffset + offset] as number | string;
}
export function getDefaultValue(context: TStylingContext, index: number): string|boolean|null {
return context[index + TStylingContextIndex.BindingsStartOffset + getTotalSources(context)] as
string |
boolean | null;
}
export function setDefaultValue(
context: TStylingContext, index: number, value: string | boolean | null) {
return context[index + TStylingContextIndex.BindingsStartOffset + getTotalSources(context)] =
value;
}
export function setValue(data: LStylingData, bindingIndex: number, value: any) {
data[bindingIndex] = value;
}
export function getValue<T = any>(data: LStylingData, bindingIndex: number): T|null {
return bindingIndex > 0 ? data[bindingIndex] as T : null;
}
export function lockContext(context: TStylingContext, hostBindingsMode: boolean): void {
patchConfig(context, getLockedConfig(hostBindingsMode));
}
export function isContextLocked(context: TStylingContext, hostBindingsMode: boolean): boolean {
return hasConfig(context, getLockedConfig(hostBindingsMode));
}
export function getLockedConfig(hostBindingsMode: boolean) {
return hostBindingsMode ? TStylingConfig.HostBindingsLocked :
TStylingConfig.TemplateBindingsLocked;
}
export function getPropValuesStartPosition(context: TStylingContext) {
let startPosition = TStylingContextIndex.ValuesStartPosition;
if (hasConfig(context, TStylingConfig.HasMapBindings)) {
startPosition += TStylingContextIndex.BindingsStartOffset + getValuesCount(context);
}
return startPosition;
}
export function isMapBased(prop: string) {
return prop === MAP_BASED_ENTRY_PROP_NAME;
}
export function hasValueChanged(
a: NO_CHANGE | StylingMapArray | number | String | string | null | boolean | undefined | {},
b: NO_CHANGE | StylingMapArray | number | String | string | null | boolean | undefined |
{}): boolean {
if (b === NO_CHANGE) return false;
let compareValueA = Array.isArray(a) ? a[StylingMapArrayIndex.RawValuePosition] : a;
let compareValueB = Array.isArray(b) ? b[StylingMapArrayIndex.RawValuePosition] : b;
// these are special cases for String based values (which are created as artifacts
// when sanitization is bypassed on a particular value)
if (compareValueA instanceof String) {
compareValueA = compareValueA.toString();
}
if (compareValueB instanceof String) {
compareValueB = compareValueB.toString();
}
return !Object.is(compareValueA, compareValueB);
}
/**
* Determines whether the provided styling value is truthy or falsy.
*/
export function isStylingValueDefined(value: any) {
// the reason why null is compared against is because
// a CSS class value that is set to `false` must be
// respected (otherwise it would be treated as falsy).
// Empty string values are because developers usually
// set a value to an empty string to remove it.
return value != null && value !== '';
}
export function concatString(a: string, b: string, separator = ' '): string {
return a + ((b.length && a.length) ? separator : '') + b;
}
export function hyphenate(value: string): string {
return value.replace(/[a-z][A-Z]/g, v => v.charAt(0) + '-' + v.charAt(1)).toLowerCase();
}
/**
* Returns an instance of `StylingMapArray`.
*
* This function is designed to find an instance of `StylingMapArray` in case it is stored
* inside of an instance of `TStylingContext`. When a styling context is created it
* will copy over an initial styling values from the tNode (which are stored as a
* `StylingMapArray` on the `tNode.classes` or `tNode.styles` values).
*/
export function getStylingMapArray(value: TStylingContext | StylingMapArray | null):
StylingMapArray|null {
return isStylingContext(value) ?
(value as TStylingContext)[TStylingContextIndex.InitialStylingValuePosition] :
value as StylingMapArray;
}
export function isStylingContext(value: TStylingContext | StylingMapArray | null): boolean {
// the StylingMapArray is in the format of [initial, prop, string, prop, string]
// and this is the defining value to distinguish between arrays
return Array.isArray(value) && value.length >= TStylingContextIndex.ValuesStartPosition &&
typeof value[1] !== 'string';
}
export function isStylingMapArray(value: TStylingContext | StylingMapArray | null): boolean {
// the StylingMapArray is in the format of [initial, prop, string, prop, string]
// and this is the defining value to distinguish between arrays
return Array.isArray(value) &&
(typeof(value as StylingMapArray)[StylingMapArrayIndex.ValuesStartPosition] === 'string');
}
export function getInitialStylingValue(context: TStylingContext | StylingMapArray | null): string {
const map = getStylingMapArray(context);
return map && (map[StylingMapArrayIndex.RawValuePosition] as string | null) || '';
}
export function hasClassInput(tNode: TNode) {
return (tNode.flags & TNodeFlags.hasClassInput) !== 0;
}
export function hasStyleInput(tNode: TNode) {
return (tNode.flags & TNodeFlags.hasStyleInput) !== 0;
}
export function getMapProp(map: StylingMapArray, index: number): string {
return map[index + StylingMapArrayIndex.PropOffset] as string;
}
const MAP_DIRTY_VALUE =
typeof ngDevMode !== 'undefined' && ngDevMode ? {} : {MAP_DIRTY_VALUE: true};
export function setMapAsDirty(map: StylingMapArray): void {
map[StylingMapArrayIndex.RawValuePosition] = MAP_DIRTY_VALUE;
}
export function setMapValue(
map: StylingMapArray, index: number, value: string | boolean | null): void {
map[index + StylingMapArrayIndex.ValueOffset] = value;
}
export function getMapValue(map: StylingMapArray, index: number): string|null {
return map[index + StylingMapArrayIndex.ValueOffset] as string | null;
}
export function forceClassesAsString(classes: string | {[key: string]: any} | null | undefined):
string {
if (classes && typeof classes !== 'string') {
classes = Object.keys(classes).join(' ');
}
return (classes as string) || '';
}
export function forceStylesAsString(styles: {[key: string]: any} | null | undefined): string {
let str = '';
if (styles) {
const props = Object.keys(styles);
for (let i = 0; i < props.length; i++) {
const prop = props[i];
str = concatString(str, `${prop}:${styles[prop]}`, ';');
}
}
return str;
}
export function isHostStylingActive(directiveOrSourceId: number): boolean {
return directiveOrSourceId !== TEMPLATE_DIRECTIVE_INDEX;
}
/**
* Converts the provided styling map array into a string.
*
* Classes => `one two three`
* Styles => `prop:value; prop2:value2`
*/
export function stylingMapToString(map: StylingMapArray, isClassBased: boolean): string {
let str = '';
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length;
i += StylingMapArrayIndex.TupleSize) {
const prop = getMapProp(map, i);
const value = getMapValue(map, i) as string;
const attrValue = concatString(prop, isClassBased ? '' : value, ':');
str = concatString(str, attrValue, isClassBased ? ' ' : '; ');
}
return str;
}
/**
* Converts the provided styling map array into a key value map.
*/
export function stylingMapToStringMap(map: StylingMapArray | null): {[key: string]: any} {
let stringMap: {[key: string]: any} = {};
if (map) {
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length;
i += StylingMapArrayIndex.TupleSize) {
const prop = getMapProp(map, i);
const value = getMapValue(map, i) as string;
stringMap[prop] = value;
}
}
return stringMap;
}
/**
* Inserts the provided item into the provided styling array at the right spot.
*
* The `StylingMapArray` type is a sorted key/value array of entries. This means
* that when a new entry is inserted it must be placed at the right spot in the
* array. This function figures out exactly where to place it.
*/
export function addItemToStylingMap(
stylingMapArr: StylingMapArray, prop: string, value: string | boolean | null,
allowOverwrite?: boolean) {
for (let j = StylingMapArrayIndex.ValuesStartPosition; j < stylingMapArr.length;
j += StylingMapArrayIndex.TupleSize) {
const propAtIndex = getMapProp(stylingMapArr, j);
if (prop <= propAtIndex) {
let applied = false;
if (propAtIndex === prop) {
const valueAtIndex = stylingMapArr[j];
if (allowOverwrite || !isStylingValueDefined(valueAtIndex)) {
applied = true;
setMapValue(stylingMapArr, j, value);
}
} else {
applied = true;
stylingMapArr.splice(j, 0, prop, value);
}
return applied;
}
}
stylingMapArr.push(prop, value);
return true;
}
/**
* Used to convert a {key:value} map into a `StylingMapArray` array.
*
* This function will either generate a new `StylingMapArray` instance
* or it will patch the provided `newValues` map value into an
* existing `StylingMapArray` value (this only happens if `bindingValue`
* is an instance of `StylingMapArray`).
*
* If a new key/value map is provided with an old `StylingMapArray`
* value then all properties will be overwritten with their new
* values or with `null`. This means that the array will never
* shrink in size (but it will also not be created and thrown
* away whenever the `{key:value}` map entries change).
*/
export function normalizeIntoStylingMap(
bindingValue: null | StylingMapArray,
newValues: {[key: string]: any} | string | null | undefined,
normalizeProps?: boolean): StylingMapArray {
const stylingMapArr: StylingMapArray = Array.isArray(bindingValue) ? bindingValue : [null];
stylingMapArr[StylingMapArrayIndex.RawValuePosition] = newValues || null;
// because the new values may not include all the properties
// that the old ones had, all values are set to `null` before
// the new values are applied. This way, when flushed, the
// styling algorithm knows exactly what style/class values
// to remove from the element (since they are `null`).
for (let j = StylingMapArrayIndex.ValuesStartPosition; j < stylingMapArr.length;
j += StylingMapArrayIndex.TupleSize) {
setMapValue(stylingMapArr, j, null);
}
let props: string[]|null = null;
let map: {[key: string]: any}|undefined|null;
let allValuesTrue = false;
if (typeof newValues === 'string') { // [class] bindings allow string values
if (newValues.length) {
props = newValues.split(/\s+/);
allValuesTrue = true;
}
} else {
props = newValues ? Object.keys(newValues) : null;
map = newValues;
}
if (props) {
for (let i = 0; i < props.length; i++) {
const prop = props[i] as string;
const newProp = normalizeProps ? hyphenate(prop) : prop;
const value = allValuesTrue ? true : map ![prop];
addItemToStylingMap(stylingMapArr, newProp, value, true);
}
}
return stylingMapArr;
}