Files
NativeScript/tns-core-modules/ui/core/properties/properties.ts
Hristo Hristov 0f14101238 recycling now happens only if nativeView and android properties are not accessed. (#4627)
recycleNativeView filed now accepts: "always" | "never" | "auto". Always will recycle the nativeView no matter if its nativeView or android proprties are accessed. Never will disable recycling. Auto will recycle it only if nativeView and android properties are not accessed.
2017-08-01 15:04:16 +03:00

1298 lines
48 KiB
TypeScript

// Definitions.
import * as definitions from "../view-base";
import { ViewBase } from "../view-base";
// Types.
import { WrappedValue, PropertyChangeData } from "../../../data/observable";
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<InheritedProperty<any, any>>();
let inheritableCssProperties = new Array<InheritedCssProperty<any, any>>();
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<any, any>[] {
return getPropertiesFromMap(symbolPropertyMap) as Property<any, any>[];
}
export function _getStyleProperties(): CssProperty<any, any>[] {
return getPropertiesFromMap(cssSymbolPropertyMap) as CssProperty<any, any>[];
}
function getPropertiesFromMap(map): Property<any, any>[] | CssProperty<any, any>[] {
const props = [];
Object.getOwnPropertySymbols(map).forEach(symbol => props.push(map[symbol]));
return props;
}
export class Property<T extends ViewBase, U> implements TypedPropertyDescriptor<U>, definitions.Property<T, U> {
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<T, U>) {
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 && (<any>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 (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<PropertyChangeData>({ 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);
}
}
}
};
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<PropertyChangeData>({ 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<T extends ViewBase, U> extends Property<T, U> implements definitions.CoercibleProperty<T, U> {
public readonly coerce: (target: T) => void;
constructor(options: definitions.CoerciblePropertyOptions<T, U>) {
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 && (<any>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<PropertyChangeData>({ 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<T extends ViewBase, U> extends Property<T, U> implements definitions.InheritedProperty<T, U> {
public readonly sourceKey: symbol;
public readonly setInheritedValue: (value: U) => void;
constructor(options: definitions.PropertyOptions<T, U>) {
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 = <T>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<T extends Style, U> implements definitions.CssProperty<T, U> {
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<T, U>) {
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, value: U): void {
const reset = value === unsetValue;
if (reset) {
value = defaultValue;
delete this[sourceKey];
}
else {
this[sourceKey] = ValueSource.Local;
if (valueConverter && typeof value === "string") {
value = valueConverter(value);
}
}
const oldValue: U = key in this ? this[key] : defaultValue;
const changed: boolean = equalityComparer ? !equalityComparer(oldValue, value) : oldValue !== value;
if (changed) {
const view = this.view;
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<PropertyChangeData>({ object: this, eventName, propertyName, value, oldValue });
}
if (affectsLayout) {
view.requestLayout();
}
}
}
function setCssValue(this: T, value: U): void {
const reset = value === unsetValue;
const currentValueSource: number = this[sourceKey] || ValueSource.Default;
// We have localValueSource - NOOP.
if (currentValueSource === ValueSource.Local) {
return;
}
if (reset) {
value = defaultValue;
delete this[sourceKey];
} else {
if (valueConverter && typeof value === "string") {
value = valueConverter(value);
}
this[sourceKey] = ValueSource.Css;
}
const oldValue: U = key in this ? this[key] : defaultValue;
const changed: boolean = equalityComparer ? !equalityComparer(oldValue, value) : oldValue !== value;
if (changed) {
const view = this.view;
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<PropertyChangeData>({ 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<T extends Style, U> {
public readonly name: string;
public readonly cssName: 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<any, any> } = {};
public _valueConverter?: (value: string) => any;
constructor(private options: definitions.CssAnimationPropertyOptions<T, U>) {
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 cssName = "css:" + (options.cssName || propertyName);
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) {
const oldValue = this[computedValue];
const oldSource = this[computedSource];
const wasSet = oldSource !== ValueSource.Default;
if (boxedValue === unsetValue) {
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);
}
const view = this.view;
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<PropertyChangeData>({ 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 defaultValueKey = this.defaultValueKey;
if (!(defaultValueKey in target)) {
const view = target.view;
const getDefault = this.getDefault;
target[defaultValueKey] = view[getDefault] ? view[getDefault]() : this.defaultValue;
}
}
public static _getByCssName(name: string): CssAnimationProperty<any, any> {
return this.properties[name];
}
public isSet(instance: T): boolean {
return instance[this.source] !== ValueSource.Default;
}
}
CssAnimationProperty.prototype.isStyleProperty = true;
export class InheritedCssProperty<T extends Style, U> extends CssProperty<T, U> implements definitions.InheritedCssProperty<T, U> {
public setInheritedValue: (value: U) => void;
constructor(options: definitions.CssPropertyOptions<T, U>) {
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 reset = boxedValue === unsetValue;
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 view = this.view;
let value: U;
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;
} else {
value = defaultValue;
delete this[sourceKey];
}
} else {
this[sourceKey] = valueSource;
if (valueConverter && typeof boxedValue === "string") {
value = valueConverter(boxedValue);
} else {
value = boxedValue;
}
}
const oldValue: U = key in this ? this[key] : defaultValue;
const changed: boolean = equalityComparer ? !equalityComparer(oldValue, value) : oldValue !== value;
if (changed) {
const view = this.view;
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<PropertyChangeData>({ 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<T extends Style, P> implements definitions.ShorthandProperty<T, P> {
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;
public readonly sourceKey: symbol;
constructor(options: definitions.ShorthandPropertyOptions<P>) {
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 {
this.view._batchUpdate(() => {
for (let [p, v] of converter(value)) {
this[p.name] = v;
}
});
}
function setCssValue(this: T, value: string): void {
this.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
};
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);
}
}
}
function inheritablePropertyValuesOn(view: ViewBase): Array<{ property: InheritedProperty<any, any>, value: any }> {
const array = new Array<{ property: InheritedProperty<any, any>, 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<any, any>, value: any }> {
const array = new Array<{ property: InheritedCssProperty<any, any>, 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<ViewBase, any> | CssProperty<Style, any> | CssAnimationProperty<Style, any>;
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 (var propertyName in suspendedUpdates) {
const property = <PropertyInterface>suspendedUpdates[propertyName];
const setNative = property.setNative;
if (view[setNative]) {
const { getDefault, isStyleProperty, defaultValueKey, defaultValue } = property;
let value;
if (isStyleProperty) {
const style = view.style;
if ((<CssProperty<Style, any> | CssAnimationProperty<Style, any>>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<ViewBase, any>>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<any, any> = 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<any, any> = 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<any, any> = 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<any, any> = 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<T>(...values: T[]): (value: any) => value is T {
const set = new Set(values);
return (value: any): value is T => set.has(value);
}
export function makeParser<T>(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 (var prop of cssPropertyNames) {
result.push([prop, style[prop]]);
}
return result;
}