import * as reduceCSSCalc from "reduce-css-calc"; // Definitions. import * as definitions from "../view-base"; import { ViewBase } from "../view-base"; // Types. import { WrappedValue, PropertyChangeData } from "../../../data/observable"; import { write as traceWrite, categories as traceCategories, messageType as traceMessageType, } from "../../../trace"; import { Style } from "../../styling/style"; import { profile } from "../../../profiling"; export { Style }; export const unsetValue: any = new Object(); let cssPropertyNames: string[] = []; let symbolPropertyMap = {}; let cssSymbolPropertyMap = {}; let inheritableProperties = new Array>(); let inheritableCssProperties = new Array>(); function print(map) { let symbols = Object.getOwnPropertySymbols(map); for (let symbol of symbols) { const prop = map[symbol]; if (!prop.registered) { console.log(`Property ${prop.name} not Registered!!!!!`); } } } export function _printUnregisteredProperties(): void { print(symbolPropertyMap); print(cssSymbolPropertyMap); } const enum ValueSource { Default = 0, Inherited = 1, Css = 2, Local = 3, Keyframe = 4 } export function _getProperties(): Property[] { return getPropertiesFromMap(symbolPropertyMap) as Property[]; } export function _getStyleProperties(): CssProperty[] { return getPropertiesFromMap(cssSymbolPropertyMap) as CssProperty[]; } const cssVariableExpressionRegexp = /\bvar\(\s*(--[^,\s]+?)(?:\s*,\s*(.+))?\s*\)/; const cssVariableAllExpressionsRegexp = /\bvar\(\s*(--[^,\s]+?)(?:\s*,\s*(.+))?\s*\)/g; export function isCssVariable(property: string) { return /^--[^,\s]+?$/.test(property); } export function isCssCalcExpression(value: string) { return /\bcalc\(/.test(value); } export function isCssVariableExpression(value: string) { return cssVariableExpressionRegexp.test(value); } export function _evaluateCssVariableExpression(view: ViewBase, cssName: string, value: string): string { if (typeof value !== "string") { return value; } if (!isCssVariableExpression(value)) { // Value is not using css-variable(s) return value; } let output = value.trim(); // Evaluate every (and nested) css-variables in the value. let lastValue: string; while (lastValue !== output) { lastValue = output; output = output.replace(cssVariableAllExpressionsRegexp, (matchStr, cssVariableName: string, fallbackStr: string) => { const cssVariableValue = view.style.getCssVariable(cssVariableName); if (cssVariableValue !== null) { return cssVariableValue; } if (fallbackStr) { // css-variable not found, using fallback-string. const fallbackOutput = _evaluateCssVariableExpression(view, cssName, fallbackStr); if (fallbackOutput) { // If the fallback have multiple values, return the first of them. return fallbackOutput.split(",")[0]; } } // Couldn't find a value for the css-variable or the fallback, return "unset" traceWrite(`Failed to get value for css-variable "${cssVariableName}" used in "${cssName}"=[${value}] to ${view}`, traceCategories.Style, traceMessageType.error); return "unset"; }); } return output; } export function _evaluateCssCalcExpression(value: string) { if (typeof value !== "string") { return value; } if (isCssCalcExpression(value)) { // WORKAROUND: reduce-css-calc can't handle the dip-unit. return reduceCSSCalc(value.replace(/([0-9]+(\.[0-9]+)?)dip\b/g, "$1")); } else { return value; } } function getPropertiesFromMap(map): Property[] | CssProperty[] { const props = []; Object.getOwnPropertySymbols(map).forEach(symbol => props.push(map[symbol])); return props; } export class Property implements TypedPropertyDescriptor, definitions.Property { private registered: boolean; public readonly name: string; public readonly key: symbol; public readonly getDefault: symbol; public readonly setNative: symbol; public readonly defaultValueKey: symbol; public readonly defaultValue: U; public readonly nativeValueChange: (owner: T, value: U) => void; public isStyleProperty: boolean; public get: () => U; public set: (value: U) => void; public enumerable: boolean = true; public configurable: boolean = true; constructor(options: definitions.PropertyOptions) { const propertyName = options.name; this.name = propertyName; const key = Symbol(propertyName + ":propertyKey"); this.key = key; const getDefault: symbol = Symbol(propertyName + ":getDefault"); this.getDefault = getDefault; const setNative: symbol = Symbol(propertyName + ":setNative"); this.setNative = setNative; const defaultValueKey = Symbol(propertyName + ":nativeDefaultValue"); this.defaultValueKey = defaultValueKey; const defaultValue: U = options.defaultValue; this.defaultValue = defaultValue; const eventName = propertyName + "Change"; const equalityComparer = options.equalityComparer; const affectsLayout: boolean = options.affectsLayout; const valueChanged = options.valueChanged; const valueConverter = options.valueConverter; const property = this; this.set = function (this: T, boxedValue: U): void { const reset = boxedValue === unsetValue; let value: U; let wrapped: boolean; if (reset) { value = defaultValue; } else { wrapped = boxedValue && (boxedValue).wrapped; value = wrapped ? WrappedValue.unwrap(boxedValue) : boxedValue; if (valueConverter && typeof value === "string") { value = valueConverter(value); } } const oldValue = key in this ? this[key] : defaultValue; const changed: boolean = equalityComparer ? !equalityComparer(oldValue, value) : oldValue !== value; if (wrapped || changed) { if (affectsLayout) { this.requestLayout(); } if (reset) { delete this[key]; if (valueChanged) { valueChanged(this, oldValue, value); } if (this[setNative]) { if (this._suspendNativeUpdatesCount) { if (this._suspendedUpdates) { this._suspendedUpdates[propertyName] = property; } } else { if (defaultValueKey in this) { this[setNative](this[defaultValueKey]); delete this[defaultValueKey]; } else { this[setNative](defaultValue); } } } } else { this[key] = value; if (valueChanged) { valueChanged(this, oldValue, value); } if (this[setNative]) { if (this._suspendNativeUpdatesCount) { if (this._suspendedUpdates) { this._suspendedUpdates[propertyName] = property; } } else { if (!(defaultValueKey in this)) { this[defaultValueKey] = this[getDefault] ? this[getDefault]() : defaultValue; } this[setNative](value); } } } if (this.hasListeners(eventName)) { this.notify({ object: this, eventName, propertyName, value, oldValue }); } if (this.domNode) { if (reset) { this.domNode.attributeRemoved(propertyName); } else { this.domNode.attributeModified(propertyName, value); } } } }; this.get = function (this: T): U { return key in this ? this[key] : defaultValue; }; this.nativeValueChange = function (owner: T, value: U): void { const oldValue = key in owner ? owner[key] : defaultValue; const changed = equalityComparer ? !equalityComparer(oldValue, value) : oldValue !== value; if (changed) { owner[key] = value; if (valueChanged) { valueChanged(owner, oldValue, value); } if (owner.nativeViewProtected && !(defaultValueKey in owner)) { owner[defaultValueKey] = owner[getDefault] ? owner[getDefault]() : defaultValue; } if (owner.hasListeners(eventName)) { owner.notify({ object: owner, eventName, propertyName, value, oldValue }); } if (affectsLayout) { owner.requestLayout(); } if (owner.domNode) { owner.domNode.attributeModified(propertyName, value); } } }; symbolPropertyMap[key] = this; } public register(cls: { prototype: T }): void { if (this.registered) { throw new Error(`Property ${this.name} already registered.`); } this.registered = true; Object.defineProperty(cls.prototype, this.name, this); } public isSet(instance: T): boolean { return this.key in instance; } } Property.prototype.isStyleProperty = false; export class CoercibleProperty extends Property implements definitions.CoercibleProperty { public readonly coerce: (target: T) => void; constructor(options: definitions.CoerciblePropertyOptions) { super(options); const propertyName = options.name; const key = this.key; const getDefault: symbol = this.getDefault; const setNative: symbol = this.setNative; const defaultValueKey = this.defaultValueKey; const defaultValue: U = this.defaultValue; const coerceKey = Symbol(propertyName + ":coerceKey"); const eventName = propertyName + "Change"; const affectsLayout: boolean = options.affectsLayout; const equalityComparer = options.equalityComparer; const valueChanged = options.valueChanged; const valueConverter = options.valueConverter; const coerceCallback = options.coerceValue; const property = this; this.coerce = function (target: T): void { const originalValue: U = coerceKey in target ? target[coerceKey] : defaultValue; // need that to make coercing but also fire change events target[propertyName] = originalValue; }; this.set = function (this: T, boxedValue: U): void { const reset = boxedValue === unsetValue; let value: U; let wrapped: boolean; if (reset) { value = defaultValue; delete this[coerceKey]; } else { wrapped = boxedValue && (boxedValue).wrapped; value = wrapped ? WrappedValue.unwrap(boxedValue) : boxedValue; if (valueConverter && typeof value === "string") { value = valueConverter(value); } this[coerceKey] = value; value = coerceCallback(this, value); } const oldValue = key in this ? this[key] : defaultValue; const changed: boolean = equalityComparer ? !equalityComparer(oldValue, value) : oldValue !== value; if (wrapped || changed) { if (reset) { delete this[key]; if (valueChanged) { valueChanged(this, oldValue, value); } if (this[setNative]) { if (this._suspendNativeUpdatesCount) { if (this._suspendedUpdates) { this._suspendedUpdates[propertyName] = property; } } else { if (defaultValueKey in this) { this[setNative](this[defaultValueKey]); delete this[defaultValueKey]; } else { this[setNative](defaultValue); } } } } else { this[key] = value; if (valueChanged) { valueChanged(this, oldValue, value); } if (this[setNative]) { if (this._suspendNativeUpdatesCount) { if (this._suspendedUpdates) { this._suspendedUpdates[propertyName] = property; } } else { if (!(defaultValueKey in this)) { this[defaultValueKey] = this[getDefault] ? this[getDefault]() : defaultValue; } this[setNative](value); } } } if (this.hasListeners(eventName)) { this.notify({ object: this, eventName, propertyName, value, oldValue }); } if (affectsLayout) { this.requestLayout(); } if (this.domNode) { if (reset) { this.domNode.attributeRemoved(propertyName); } else { this.domNode.attributeModified(propertyName, value); } } } }; } } export class InheritedProperty extends Property implements definitions.InheritedProperty { public readonly sourceKey: symbol; public readonly setInheritedValue: (value: U) => void; constructor(options: definitions.PropertyOptions) { super(options); const name = options.name; const key = this.key; const defaultValue = options.defaultValue; const sourceKey = Symbol(name + ":valueSourceKey"); this.sourceKey = sourceKey; const setBase = this.set; const setFunc = (valueSource: ValueSource) => function (value: U): void { const that = this; let unboxedValue: U; let newValueSource: number; if (value === unsetValue) { // If unsetValue - we want to reset the property. const parent: ViewBase = that.parent; // If we have parent and it has non-default value we use as our inherited value. if (parent && parent[sourceKey] !== ValueSource.Default) { unboxedValue = parent[name]; newValueSource = ValueSource.Inherited; } else { unboxedValue = defaultValue; newValueSource = ValueSource.Default; } } else { // else we are set through property set. unboxedValue = value; newValueSource = valueSource; } // take currentValue before calling base - base may change it. const currentValue = that[key]; setBase.call(that, unboxedValue); const newValue = that[key]; that[sourceKey] = newValueSource; if (currentValue !== newValue) { const reset = newValueSource === ValueSource.Default; that.eachChild((child) => { const childValueSource = child[sourceKey] || ValueSource.Default; if (reset) { if (childValueSource === ValueSource.Inherited) { setFunc.call(child, unsetValue); } } else { if (childValueSource <= ValueSource.Inherited) { setInheritedValue.call(child, newValue); } } return true; }); } }; const setInheritedValue = setFunc(ValueSource.Inherited); this.setInheritedValue = setInheritedValue; this.set = setFunc(ValueSource.Local); inheritableProperties.push(this); } } export class CssProperty implements definitions.CssProperty { private registered: boolean; public readonly name: string; public readonly cssName: string; public readonly cssLocalName: string; protected readonly cssValueDescriptor: PropertyDescriptor; protected readonly localValueDescriptor: PropertyDescriptor; public isStyleProperty: boolean; public readonly key: symbol; public readonly getDefault: symbol; public readonly setNative: symbol; public readonly sourceKey: symbol; public readonly defaultValueKey: symbol; public readonly defaultValue: U; constructor(options: definitions.CssPropertyOptions) { const propertyName = options.name; this.name = propertyName; cssPropertyNames.push(options.cssName); this.cssName = `css:${options.cssName}`; this.cssLocalName = options.cssName; const key = Symbol(propertyName + ":propertyKey"); this.key = key; const sourceKey = Symbol(propertyName + ":valueSourceKey"); this.sourceKey = sourceKey; const getDefault = Symbol(propertyName + ":getDefault"); this.getDefault = getDefault; const setNative = Symbol(propertyName + ":setNative"); this.setNative = setNative; const defaultValueKey = Symbol(propertyName + ":nativeDefaultValue"); this.defaultValueKey = defaultValueKey; const defaultValue: U = options.defaultValue; this.defaultValue = defaultValue; const eventName = propertyName + "Change"; const affectsLayout: boolean = options.affectsLayout; const equalityComparer = options.equalityComparer; const valueChanged = options.valueChanged; const valueConverter = options.valueConverter; const property = this; function setLocalValue(this: T, newValue: U | string): void { const view = this.viewRef.get(); if (!view) { traceWrite(`${newValue} not set to view because ".viewRef" is cleared`, traceCategories.Style, traceMessageType.warn); return; } const reset = newValue === unsetValue || newValue === ""; let value: U; if (reset) { value = defaultValue; delete this[sourceKey]; } else { this[sourceKey] = ValueSource.Local; value = (valueConverter && typeof newValue === "string") ? valueConverter(newValue) : newValue; } const oldValue: U = key in this ? this[key] : defaultValue; const changed: boolean = equalityComparer ? !equalityComparer(oldValue, value) : oldValue !== value; if (changed) { if (reset) { delete this[key]; if (valueChanged) { valueChanged(this, oldValue, value); } if (view[setNative]) { if (view._suspendNativeUpdatesCount) { if (view._suspendedUpdates) { view._suspendedUpdates[propertyName] = property; } } else { if (defaultValueKey in this) { view[setNative](this[defaultValueKey]); delete this[defaultValueKey]; } else { view[setNative](defaultValue); } } } } else { this[key] = value; if (valueChanged) { valueChanged(this, oldValue, value); } if (view[setNative]) { if (view._suspendNativeUpdatesCount) { if (view._suspendedUpdates) { view._suspendedUpdates[propertyName] = property; } } else { if (!(defaultValueKey in this)) { this[defaultValueKey] = view[getDefault] ? view[getDefault]() : defaultValue; } view[setNative](value); } } } if (this.hasListeners(eventName)) { this.notify({ object: this, eventName, propertyName, value, oldValue }); } if (affectsLayout) { view.requestLayout(); } } } function setCssValue(this: T, newValue: U | string): void { const view = this.viewRef.get(); if (!view) { traceWrite(`${newValue} not set to view because ".viewRef" is cleared`, traceCategories.Style, traceMessageType.warn); return; } const currentValueSource: number = this[sourceKey] || ValueSource.Default; // We have localValueSource - NOOP. if (currentValueSource === ValueSource.Local) { return; } const reset = newValue === unsetValue || newValue === ""; let value: U; if (reset) { value = defaultValue; delete this[sourceKey]; } else { value = valueConverter && typeof newValue === "string" ? valueConverter(newValue) : newValue; this[sourceKey] = ValueSource.Css; } const oldValue: U = key in this ? this[key] : defaultValue; const changed: boolean = equalityComparer ? !equalityComparer(oldValue, value) : oldValue !== value; if (changed) { if (reset) { delete this[key]; if (valueChanged) { valueChanged(this, oldValue, value); } if (view[setNative]) { if (view._suspendNativeUpdatesCount) { if (view._suspendedUpdates) { view._suspendedUpdates[propertyName] = property; } } else { if (defaultValueKey in this) { view[setNative](this[defaultValueKey]); delete this[defaultValueKey]; } else { view[setNative](defaultValue); } } } } else { this[key] = value; if (valueChanged) { valueChanged(this, oldValue, value); } if (view[setNative]) { if (view._suspendNativeUpdatesCount) { if (view._suspendedUpdates) { view._suspendedUpdates[propertyName] = property; } } else { if (!(defaultValueKey in this)) { this[defaultValueKey] = view[getDefault] ? view[getDefault]() : defaultValue; } view[setNative](value); } } } if (this.hasListeners(eventName)) { this.notify({ object: this, eventName, propertyName, value, oldValue }); } if (affectsLayout) { view.requestLayout(); } } } function get(): U { return key in this ? this[key] : defaultValue; } this.cssValueDescriptor = { enumerable: true, configurable: true, get: get, set: setCssValue }; this.localValueDescriptor = { enumerable: true, configurable: true, get: get, set: setLocalValue }; cssSymbolPropertyMap[key] = this; } public register(cls: { prototype: T }): void { if (this.registered) { throw new Error(`Property ${this.name} already registered.`); } this.registered = true; Object.defineProperty(cls.prototype, this.name, this.localValueDescriptor); Object.defineProperty(cls.prototype, this.cssName, this.cssValueDescriptor); if (this.cssLocalName !== this.cssName) { Object.defineProperty(cls.prototype, this.cssLocalName, this.localValueDescriptor); } } public isSet(instance: T): boolean { return this.key in instance; } } CssProperty.prototype.isStyleProperty = true; export class CssAnimationProperty implements definitions.CssAnimationProperty { public readonly name: string; public readonly cssName: string; public readonly cssLocalName: string; public readonly getDefault: symbol; public readonly setNative: symbol; public readonly register: (cls: { prototype }) => void; public readonly keyframe: string; public readonly defaultValueKey: symbol; public readonly key: symbol; private readonly source: symbol; public readonly defaultValue: U; public isStyleProperty: boolean; private static properties: { [cssName: string]: CssAnimationProperty } = {}; public _valueConverter?: (value: string) => any; constructor(options: definitions.CssAnimationPropertyOptions) { const { valueConverter, equalityComparer, valueChanged, defaultValue } = options; const propertyName = options.name; this.name = propertyName; cssPropertyNames.push(options.cssName); CssAnimationProperty.properties[propertyName] = this; if (options.cssName && options.cssName !== propertyName) { CssAnimationProperty.properties[options.cssName] = this; } this._valueConverter = options.valueConverter; const cssLocalName = (options.cssName || propertyName); this.cssLocalName = cssLocalName; const cssName = "css:" + cssLocalName; this.cssName = cssName; const keyframeName = "keyframe:" + propertyName; this.keyframe = keyframeName; const defaultName = "default:" + propertyName; const defaultValueKey = Symbol(defaultName); this.defaultValueKey = defaultValueKey; this.defaultValue = defaultValue; const cssValue = Symbol(cssName); const styleValue = Symbol(`local:${propertyName}`); const keyframeValue = Symbol(keyframeName); const computedValue = Symbol("computed-value:" + propertyName); this.key = computedValue; const computedSource = Symbol("computed-source:" + propertyName); this.source = computedSource; this.getDefault = Symbol(propertyName + ":getDefault"); const getDefault = this.getDefault; const setNative = this.setNative = Symbol(propertyName + ":setNative"); const eventName = propertyName + "Change"; const property = this; function descriptor(symbol: symbol, propertySource: ValueSource, enumerable: boolean, configurable: boolean, getsComputed: boolean): PropertyDescriptor { return { enumerable, configurable, get: getsComputed ? function (this: T) { return this[computedValue]; } : function (this: T) { return this[symbol]; }, set(this: T, boxedValue: U | string) { const view = this.viewRef.get(); if (!view) { traceWrite(`${boxedValue} not set to view because ".viewRef" is cleared`, traceCategories.Animation, traceMessageType.warn); return; } const oldValue = this[computedValue]; const oldSource = this[computedSource]; const wasSet = oldSource !== ValueSource.Default; const reset = boxedValue === unsetValue || boxedValue === ""; if (reset) { this[symbol] = unsetValue; if (this[computedSource] === propertySource) { // Fallback to lower value source. if (this[styleValue] !== unsetValue) { this[computedSource] = ValueSource.Local; this[computedValue] = this[styleValue]; } else if (this[cssValue] !== unsetValue) { this[computedSource] = ValueSource.Css; this[computedValue] = this[cssValue]; } else { delete this[computedSource]; delete this[computedValue]; } } } else { if (valueConverter && typeof boxedValue === "string") { boxedValue = valueConverter(boxedValue); } this[symbol] = boxedValue; if (this[computedSource] <= propertySource) { this[computedSource] = propertySource; this[computedValue] = boxedValue; } } const value = this[computedValue]; const source = this[computedSource]; const isSet = source !== ValueSource.Default; const computedValueChanged = oldValue !== value && (!equalityComparer || !equalityComparer(oldValue, value)); if (computedValueChanged && valueChanged) { valueChanged(this, oldValue, value); } if (view[setNative] && (computedValueChanged || isSet !== wasSet)) { if (view._suspendNativeUpdatesCount) { if (view._suspendedUpdates) { view._suspendedUpdates[propertyName] = property; } } else { if (isSet) { if (!wasSet && !(defaultValueKey in this)) { this[defaultValueKey] = view[getDefault] ? view[getDefault]() : defaultValue; } view[setNative](value); } else if (wasSet) { if (defaultValueKey in this) { view[setNative](this[defaultValueKey]); } else { view[setNative](defaultValue); } } } } if (computedValueChanged && this.hasListeners(eventName)) { this.notify({ object: this, eventName, propertyName, value, oldValue }); } } }; } const defaultPropertyDescriptor = descriptor(defaultValueKey, ValueSource.Default, false, false, false); const cssPropertyDescriptor = descriptor(cssValue, ValueSource.Css, false, false, false); const stylePropertyDescriptor = descriptor(styleValue, ValueSource.Local, true, true, true); const keyframePropertyDescriptor = descriptor(keyframeValue, ValueSource.Keyframe, false, false, false); symbolPropertyMap[computedValue] = this; cssSymbolPropertyMap[computedValue] = this; this.register = (cls: { prototype: T }) => { cls.prototype[computedValue] = options.defaultValue; cls.prototype[computedSource] = ValueSource.Default; cls.prototype[cssValue] = unsetValue; cls.prototype[styleValue] = unsetValue; cls.prototype[keyframeValue] = unsetValue; Object.defineProperty(cls.prototype, defaultName, defaultPropertyDescriptor); Object.defineProperty(cls.prototype, cssName, cssPropertyDescriptor); Object.defineProperty(cls.prototype, propertyName, stylePropertyDescriptor); if (options.cssName && options.cssName !== options.name) { Object.defineProperty(cls.prototype, options.cssName, stylePropertyDescriptor); } Object.defineProperty(cls.prototype, keyframeName, keyframePropertyDescriptor); }; } public _initDefaultNativeValue(target: T): void { const view = target.viewRef.get(); if (!view) { traceWrite(`_initDefaultNativeValue not executed to view because ".viewRef" is cleared`, traceCategories.Animation, traceMessageType.warn); return; } const defaultValueKey = this.defaultValueKey; if (!(defaultValueKey in target)) { const getDefault = this.getDefault; target[defaultValueKey] = view[getDefault] ? view[getDefault]() : this.defaultValue; } } public static _getByCssName(name: string): CssAnimationProperty { return this.properties[name]; } public static _getPropertyNames(): string[] { return Object.keys(CssAnimationProperty.properties); } public isSet(instance: T): boolean { return instance[this.source] !== ValueSource.Default; } } CssAnimationProperty.prototype.isStyleProperty = true; export class InheritedCssProperty extends CssProperty implements definitions.InheritedCssProperty { public setInheritedValue: (value: U) => void; constructor(options: definitions.CssPropertyOptions) { super(options); const propertyName = options.name; const key = this.key; const sourceKey = this.sourceKey; const getDefault = this.getDefault; const setNative = this.setNative; const defaultValueKey = this.defaultValueKey; const eventName = propertyName + "Change"; const defaultValue: U = options.defaultValue; const affectsLayout: boolean = options.affectsLayout; const equalityComparer = options.equalityComparer; const valueChanged = options.valueChanged; const valueConverter = options.valueConverter; const property = this; const setFunc = (valueSource: ValueSource) => function (this: T, boxedValue: any): void { const view = this.viewRef.get(); if (!view) { traceWrite(`${boxedValue} not set to view's property because ".viewRef" is cleared`, traceCategories.Style, traceMessageType.warn); return; } const reset = boxedValue === unsetValue || boxedValue === ""; const currentValueSource: number = this[sourceKey] || ValueSource.Default; if (reset) { // If we want to reset cssValue and we have localValue - return; if (valueSource === ValueSource.Css && currentValueSource === ValueSource.Local) { return; } } else { if (currentValueSource > valueSource) { return; } } const oldValue: U = key in this ? this[key] : defaultValue; let value: U; let unsetNativeValue = false; if (reset) { // If unsetValue - we want to reset this property. let parent = view.parent; let style = parent ? parent.style : null; // If we have parent and it has non-default value we use as our inherited value. if (style && style[sourceKey] > ValueSource.Default) { value = style[propertyName]; this[sourceKey] = ValueSource.Inherited; this[key] = value; } else { value = defaultValue; delete this[sourceKey]; delete this[key]; unsetNativeValue = true; } } else { this[sourceKey] = valueSource; if (valueConverter && typeof boxedValue === "string") { value = valueConverter(boxedValue); } else { value = boxedValue; } this[key] = value; } const changed: boolean = equalityComparer ? !equalityComparer(oldValue, value) : oldValue !== value; if (changed) { if (valueChanged) { valueChanged(this, oldValue, value); } if (view[setNative]) { if (view._suspendNativeUpdatesCount) { if (view._suspendedUpdates) { view._suspendedUpdates[propertyName] = property; } } else { if (unsetNativeValue) { if (defaultValueKey in this) { view[setNative](this[defaultValueKey]); delete this[defaultValueKey]; } else { view[setNative](defaultValue); } } else { if (!(defaultValueKey in this)) { this[defaultValueKey] = view[getDefault] ? view[getDefault]() : defaultValue; } view[setNative](value); } } } if (this.hasListeners(eventName)) { this.notify({ object: this, eventName, propertyName, value, oldValue }); } if (affectsLayout) { view.requestLayout(); } view.eachChild((child) => { const childStyle = child.style; const childValueSource = childStyle[sourceKey] || ValueSource.Default; if (reset) { if (childValueSource === ValueSource.Inherited) { setDefaultFunc.call(childStyle, unsetValue); } } else { if (childValueSource <= ValueSource.Inherited) { setInheritedFunc.call(childStyle, value); } } return true; }); } }; const setDefaultFunc = setFunc(ValueSource.Default); const setInheritedFunc = setFunc(ValueSource.Inherited); this.setInheritedValue = setInheritedFunc; this.cssValueDescriptor.set = setFunc(ValueSource.Css); this.localValueDescriptor.set = setFunc(ValueSource.Local); inheritableCssProperties.push(this); } } export class ShorthandProperty implements definitions.ShorthandProperty { private registered: boolean; public readonly key: symbol; public readonly name: string; public readonly cssName: string; public readonly cssLocalName: string; protected readonly cssValueDescriptor: PropertyDescriptor; protected readonly localValueDescriptor: PropertyDescriptor; protected readonly propertyBagDescriptor: PropertyDescriptor; public readonly sourceKey: symbol; constructor(options: definitions.ShorthandPropertyOptions

) { this.name = options.name; const key = Symbol(this.name + ":propertyKey"); this.key = key; this.cssName = `css:${options.cssName}`; this.cssLocalName = `${options.cssName}`; const converter = options.converter; function setLocalValue(this: T, value: string | P): void { const view = this.viewRef.get(); if (!view) { traceWrite(`setLocalValue not executed to view because ".viewRef" is cleared`, traceCategories.Animation, traceMessageType.warn); return; } view._batchUpdate(() => { for (let [p, v] of converter(value)) { this[p.name] = v; } }); } function setCssValue(this: T, value: string): void { const view = this.viewRef.get(); if (!view) { traceWrite(`setCssValue not executed to view because ".viewRef" is cleared`, traceCategories.Animation, traceMessageType.warn); return; } view._batchUpdate(() => { for (let [p, v] of converter(value)) { this[p.cssName] = v; } }); } this.cssValueDescriptor = { enumerable: true, configurable: true, get: options.getter, set: setCssValue }; this.localValueDescriptor = { enumerable: true, configurable: true, get: options.getter, set: setLocalValue }; this.propertyBagDescriptor = { enumerable: false, configurable: true, set(value: string) { converter(value).forEach(([property, value]) => { this[property.cssLocalName] = value; }); } }; cssSymbolPropertyMap[key] = this; } public register(cls: typeof Style): void { if (this.registered) { throw new Error(`Property ${this.name} already registered.`); } this.registered = true; Object.defineProperty(cls.prototype, this.name, this.localValueDescriptor); Object.defineProperty(cls.prototype, this.cssName, this.cssValueDescriptor); if (this.cssLocalName !== this.cssName) { Object.defineProperty(cls.prototype, this.cssLocalName, this.localValueDescriptor); } Object.defineProperty(cls.prototype.PropertyBag, this.cssLocalName, this.propertyBagDescriptor); } } function inheritablePropertyValuesOn(view: ViewBase): Array<{ property: InheritedProperty, value: any }> { const array = new Array<{ property: InheritedProperty, value: any }>(); for (let prop of inheritableProperties) { const sourceKey = prop.sourceKey; const valueSource: number = view[sourceKey] || ValueSource.Default; if (valueSource !== ValueSource.Default) { // use prop.name as it will return value or default value. // prop.key will return undefined if property is set the same value as default one. array.push({ property: prop, value: view[prop.name] }); } } return array; } function inheritableCssPropertyValuesOn(style: Style): Array<{ property: InheritedCssProperty, value: any }> { const array = new Array<{ property: InheritedCssProperty, value: any }>(); for (let prop of inheritableCssProperties) { const sourceKey = prop.sourceKey; const valueSource: number = style[sourceKey] || ValueSource.Default; if (valueSource !== ValueSource.Default) { // use prop.name as it will return value or default value. // prop.key will return undefined if property is set the same value as default one. array.push({ property: prop, value: style[prop.name] }); } } return array; } type PropertyInterface = Property | CssProperty | CssAnimationProperty; export const initNativeView = profile("\"properties\".initNativeView", function initNativeView(view: ViewBase): void { if (view._suspendedUpdates) { applyPendingNativeSetters(view); } else { applyAllNativeSetters(view); } // Would it be faster to delete all members of the old object? view._suspendedUpdates = {}; }); export function applyPendingNativeSetters(view: ViewBase): void { // TODO: Check what happens if a view was suspended and its value was reset, or set back to default! const suspendedUpdates = view._suspendedUpdates; for (let propertyName in suspendedUpdates) { const property = suspendedUpdates[propertyName]; const setNative = property.setNative; if (view[setNative]) { const { getDefault, isStyleProperty, defaultValueKey, defaultValue } = property; let value; if (isStyleProperty) { const style = view.style; if (( | CssAnimationProperty>property).isSet(view.style)) { if (!(defaultValueKey in style)) { style[defaultValueKey] = view[getDefault] ? view[getDefault]() : defaultValue; } value = view.style[propertyName]; } else { value = style[defaultValueKey]; } } else { if ((>property).isSet(view)) { if (!(defaultValueKey in view)) { view[defaultValueKey] = view[getDefault] ? view[getDefault]() : defaultValue; } value = view[propertyName]; } else { value = view[defaultValueKey]; } } // TODO: Only if value is different from the value before the scope was created. view[setNative](value); } } } export function applyAllNativeSetters(view: ViewBase): void { let symbols = Object.getOwnPropertySymbols(view); for (let symbol of symbols) { const property: Property = symbolPropertyMap[symbol]; if (!property) { continue; } const setNative = property.setNative; const getDefault = property.getDefault; if (setNative in view) { const defaultValueKey = property.defaultValueKey; if (!(defaultValueKey in view)) { view[defaultValueKey] = view[getDefault] ? view[getDefault]() : property.defaultValue; } const value = view[symbol]; view[setNative](value); } } const style = view.style; symbols = Object.getOwnPropertySymbols(style); for (let symbol of symbols) { const property: CssProperty = cssSymbolPropertyMap[symbol]; if (!property) { continue; } if (view[property.setNative]) { const defaultValueKey = property.defaultValueKey; if (!(defaultValueKey in style)) { style[defaultValueKey] = view[property.getDefault] ? view[property.getDefault]() : property.defaultValue; } const value = style[symbol]; view[property.setNative](value); } } } export function resetNativeView(view: ViewBase): void { let symbols = Object.getOwnPropertySymbols(view); for (let symbol of symbols) { const property: Property = symbolPropertyMap[symbol]; if (!property) { continue; } if (view[property.setNative]) { if (property.defaultValueKey in view) { view[property.setNative](view[property.defaultValueKey]); delete view[property.defaultValueKey]; } else { view[property.setNative](property.defaultValue); } } } const style = view.style; symbols = Object.getOwnPropertySymbols(style); for (let symbol of symbols) { const property: CssProperty = cssSymbolPropertyMap[symbol]; if (!property) { continue; } if (view[property.setNative]) { if (property.defaultValueKey in style) { view[property.setNative](style[property.defaultValueKey]); delete style[property.defaultValueKey]; } else { view[property.setNative](property.defaultValue); } } } } export function clearInheritedProperties(view: ViewBase): void { for (let prop of inheritableProperties) { const sourceKey = prop.sourceKey; if (view[sourceKey] === ValueSource.Inherited) { prop.set.call(view, unsetValue); } } const style = view.style; for (let prop of inheritableCssProperties) { const sourceKey = prop.sourceKey; if (style[sourceKey] === ValueSource.Inherited) { prop.setInheritedValue.call(style, unsetValue); } } } export function resetCSSProperties(style: Style): void { let symbols = Object.getOwnPropertySymbols(style); for (let symbol of symbols) { let cssProperty; if (cssProperty = cssSymbolPropertyMap[symbol]) { style[cssProperty.cssName] = unsetValue; if (cssProperty instanceof CssAnimationProperty) { style[cssProperty.keyframe] = unsetValue; } } } } export function propagateInheritableProperties(view: ViewBase, child: ViewBase): void { const inheritablePropertyValues = inheritablePropertyValuesOn(view); for (let pair of inheritablePropertyValues) { const prop = pair.property; const sourceKey = prop.sourceKey; const currentValueSource: number = child[sourceKey] || ValueSource.Default; if (currentValueSource <= ValueSource.Inherited) { prop.setInheritedValue.call(child, pair.value); } } } export function propagateInheritableCssProperties(parentStyle: Style, childStyle: Style): void { const inheritableCssPropertyValues = inheritableCssPropertyValuesOn(parentStyle); for (let pair of inheritableCssPropertyValues) { const prop = pair.property; const sourceKey = prop.sourceKey; const currentValueSource: number = childStyle[sourceKey] || ValueSource.Default; if (currentValueSource <= ValueSource.Inherited) { prop.setInheritedValue.call(childStyle, pair.value, ValueSource.Inherited); } } } export function makeValidator(...values: T[]): (value: any) => value is T { const set = new Set(values); return (value: any): value is T => set.has(value); } export function makeParser(isValid: (value: any) => boolean): (value: any) => T { return value => { const lower = value && value.toLowerCase(); if (isValid(lower)) { return lower; } else { throw new Error("Invalid value: " + value); } }; } export function getSetProperties(view: ViewBase): [string, any][] { const result = []; Object.getOwnPropertyNames(view).forEach(prop => { result.push([prop, view[prop]]); }); let symbols = Object.getOwnPropertySymbols(view); for (let symbol of symbols) { const property = symbolPropertyMap[symbol]; if (!property) { continue; } const value = view[property.key]; result.push([property.name, value]); } return result; } export function getComputedCssValues(view: ViewBase): [string, any][] { const result = []; const style = view.style; for (let prop of cssPropertyNames) { result.push([prop, style[prop]]); } // Add these to enable box model in chrome-devtools styles tab result.push(["top", "auto"]); result.push(["left", "auto"]); result.push(["bottom", "auto"]); result.push(["right", "auto"]); return result; }