import definition = require("ui/core/dependency-observable"); import {Observable, WrappedValue} from "data/observable"; import types = require("utils/types"); // use private variables in the scope of the module rather than static members of the class since a member is still accessible through JavaScript and may be changed. var propertyFromKey = {}; var propertyIdCounter = 0; export let unsetValue = new Object(); function generatePropertyKey(name: string, ownerType: string, validate?: boolean) { if (validate) { validateRegisterParameters(name, ownerType); } return ownerType + "." + name; } function validateRegisterParameters(name: string, ownerType: string) { if (name == null || name.trim().length === 0) { throw new Error("Name should not be null or empty string."); } if (ownerType == null || ownerType.trim().length === 0) { throw new Error("OwnerType should not be null or empty string."); } } function getPropertyByNameAndType(name: string, owner: any): Property { var result; var key; var classInfo = types.getClassInfo(owner); while (classInfo) { key = generatePropertyKey(name, classInfo.name); result = propertyFromKey[key]; if (result) { break; } classInfo = classInfo.baseClassInfo; } return result; } export module PropertyMetadataSettings { export var None = 0; export var AffectsLayout = 1; export var AffectsStyle = 1 << 1; export var Inheritable = 1 << 2; } export module ValueSource { export var Default = 0; export var Inherited = 1; export var Css = 2; export var Local = 3; export var VisualState = 4; } export class PropertyMetadata implements definition.PropertyMetadata { public inheritable: boolean; public affectsStyle: boolean; public affectsLayout: boolean; public onValueChanged: definition.PropertyChangedCallback; constructor( public defaultValue: any, public options: number = PropertyMetadataSettings.None, onChanged?: definition.PropertyChangedCallback, public onValidateValue?: definition.PropertyValidationCallback, public equalityComparer?: definition.PropertyEqualityComparer) { this.defaultValue = defaultValue; this.options = options; this.onValueChanged = onChanged; this.onValidateValue = onValidateValue; this.equalityComparer = equalityComparer; this.inheritable = (options & PropertyMetadataSettings.Inheritable) === PropertyMetadataSettings.Inheritable; this.affectsStyle = (options & PropertyMetadataSettings.AffectsStyle) === PropertyMetadataSettings.AffectsStyle; this.affectsLayout = (options & PropertyMetadataSettings.AffectsLayout) === PropertyMetadataSettings.AffectsLayout; } } // function property(property: Property): PropertyDecorator { // function _getValue() { // return this._getValue(property); // } // function _setValue(value: any) { // this._setValueInternal(property, value, ValueSource.Local); // } // return (target: Object, propertyKey: string) => { // Object.defineProperty(target, propertyKey, { // get: _getValue, // set: _setValue, // enumerable: true, // configurable: true // }); // }; // } export class Property implements definition.Property { public key: string; public id: number; public defaultValue; public onValidateValue; public equalityComparer; public inheritable; public affectsStyle; public affectsLayout; public onValueChanged: definition.PropertyChangedCallback; public nameEvent: string; constructor(public name: string, public ownerType: string, public metadata: PropertyMetadata, public valueConverter?: (value: string) => any) { // register key this.key = generatePropertyKey(name, ownerType, true); if (propertyFromKey[this.key]) { throw new Error("Property " + name + " already registered for type " + ownerType + "."); } propertyFromKey[this.key] = this; if (!metadata || !(metadata instanceof PropertyMetadata)) { throw new Error("Expected valid PropertyMetadata instance."); } this.name = name; this.nameEvent = name + "Change"; this.ownerType = ownerType; this.metadata = metadata; // generate a unique numeric id for each property (faster lookup than a string key) this.id = propertyIdCounter++; this.valueConverter = valueConverter; this.defaultValue = metadata.defaultValue; this.onValueChanged = metadata.onValueChanged; this.onValidateValue = metadata.onValidateValue; this.equalityComparer = metadata.equalityComparer || ((x, y) => x === y); this.inheritable = metadata.inheritable; this.affectsStyle = metadata.affectsStyle; this.affectsLayout = metadata.affectsLayout; } public defaultValueGetter: (instance: definition.DependencyObservable) => definition.NativeValueResult; } export class PropertyEntry implements definition.PropertyEntry { public valueSource: number = ValueSource.Default; public inheritedValue: any; public cssValue: any; public localValue: any; public effectiveValue: any; public visualStateValue: any; constructor(public property: Property) { this.property = property; } public resetValue() { this.valueSource = ValueSource.Default; this.inheritedValue = this.cssValue = this.localValue = this.visualStateValue = this.effectiveValue = undefined; } } var defaultValueForPropertyPerType: Map = new Map(); export class DependencyObservable extends Observable implements definition.DependencyObservable { private _propertyEntries = {}; public set(name: string, value: any) { var property = getPropertyByNameAndType(name, this); if (property) { this._setValueInternal(property, value, ValueSource.Local); } else { super.set(name, value); } } public get(name: string): any { var property = getPropertyByNameAndType(name, this); if (property) { return this._getValue(property); } else { return super.get(name); } } public _setValue(property: Property, value: any, source?: number) { this._setValueInternal(property, value, source || ValueSource.Local); } public _getValueSource(property: Property): number { var entry: PropertyEntry = this._propertyEntries[property.id]; if (entry) { return entry.valueSource; } return ValueSource.Default; } public _getValue(property: Property): any { var entry: PropertyEntry = this._propertyEntries[property.id]; if (entry) { return entry.effectiveValue; } else { return this._getDefaultValue(property); } } private _getDefaultValue(property: Property): any { if (property.defaultValueGetter) { // we check for cached properties only for these which have 'defaultValueGetter' defined; // When DependencyProperties are removed from Style - fix this check. var view = (this)._view || this; let key = types.getClass(view) + "." + property.id; let defaultValue = defaultValueForPropertyPerType.get(key); if (!defaultValueForPropertyPerType.has(key) && view._nativeView) { let defaultValueResult = property.defaultValueGetter(this); defaultValue = defaultValueResult.result; if (defaultValueResult.cacheable) { defaultValueForPropertyPerType.set(key, defaultValue); } } return defaultValue; } return property.defaultValue; } public _resetValue(property: Property, source: number = ValueSource.Local) { let entry: PropertyEntry = this._propertyEntries[property.id]; if (!entry) { return; } switch (source) { case ValueSource.Inherited: entry.inheritedValue = undefined; break; case ValueSource.Css: entry.cssValue = undefined; break; case ValueSource.Local: entry.localValue = undefined; break; case ValueSource.VisualState: entry.visualStateValue = undefined; break; } let currentValueSource = entry.valueSource; if (currentValueSource !== source) { // If current valueSource is larget than the one we reset - do nothing. // We are reseting property will lower priority and it won't change effectValue; // Reseting larger source means we somehow was able to set value without updating currentValueSource which is clearly a bug. return; } let currentValue = entry.effectiveValue; let newValue = this.getEffectiveValue(currentValueSource, entry, property); if (!property.equalityComparer(currentValue, newValue)) { // If we fallback to defalutValue - remove propertyEntry. if (entry.valueSource === ValueSource.Default) { delete this._propertyEntries[property.id]; } else { entry.effectiveValue = newValue; } this._onPropertyChanged(property, currentValue, newValue); } } public _onPropertyChanged(property: Property, oldValue: any, newValue: any) { // let realNewValue = WrappedValue.unwrap(newValue); let valueChanged = property.onValueChanged; if (valueChanged) { valueChanged({ object: this, property: property, eventName: Observable.propertyChangeEvent, newValue: newValue, oldValue: oldValue }); } let propName = property.name; if (this.hasListeners(Observable.propertyChangeEvent)) { let changeData = super._createPropertyChangeData(propName, newValue); this.notify(changeData); } let eventName = property.nameEvent; if (this.hasListeners(eventName)) { let ngChangedData = { eventName: eventName, propertyName: propName, object: this, value: newValue } this.notify(ngChangedData); } } public _eachSetProperty(callback: (property: Property) => boolean) { for (let i = 0, keys = Object.keys(this._propertyEntries); i < keys.length; i++) { let key = keys[i]; let entry: PropertyEntry = this._propertyEntries[key]; if (!callback(entry.property)) { break; } } } public _eachSetPropertyValue(callback: (property: Property, value: any) => void): void { for (let i = 0, keys = Object.keys(this._propertyEntries); i < keys.length; i++) { let key = keys[i]; let entry: PropertyEntry = this._propertyEntries[key]; callback(entry.property, entry.effectiveValue); } } public toString(): string { return this.typeName; } private _setValueInternal(property: Property, value: any, source: number) { if (value === unsetValue) { this._resetValue(property, source); return; } let wrapped = value && value.wrapped; let realValue = wrapped ? WrappedValue.unwrap(value) : value; let validate = property.onValidateValue; if (validate && !validate(realValue)) { throw new Error("Invalid value " + realValue + " for property " + property.name); } // Convert the value to the real property type in case it is coming as a string from CSS or XML. let converter = property.valueConverter; if (converter && types.isString(realValue)) { realValue = converter(realValue); } let entry: PropertyEntry = this._propertyEntries[property.id]; let currentValue; if (!entry) { entry = new PropertyEntry(property); this._propertyEntries[property.id] = entry; currentValue = this._getDefaultValue(property); // In rare case when we set local value equal to default value we need to update effectiveValue as well. // Otherwise effectiveValue will stay undefined. if (property.equalityComparer(currentValue, realValue)) { entry.effectiveValue = realValue; } } else { currentValue = entry.effectiveValue; } switch (source) { case ValueSource.Inherited: entry.inheritedValue = realValue; break; case ValueSource.Css: entry.cssValue = realValue; break; case ValueSource.Local: entry.localValue = realValue; break; case ValueSource.VisualState: entry.visualStateValue = realValue; break; } let currentValueSource = entry.valueSource; if (currentValueSource > source) { return; } else if (currentValueSource < source) { entry.valueSource = source; } if (wrapped || !property.equalityComparer(currentValue, realValue)) { entry.effectiveValue = realValue; this._onPropertyChanged(property, currentValue, realValue); } } private getEffectiveValue(currentValueSource: number, entry: PropertyEntry, property: Property): any { let newValue: any; switch (currentValueSource) { case ValueSource.Inherited: newValue = property.defaultValue; entry.valueSource = ValueSource.Default; break; case ValueSource.Css: if (entry.inheritedValue !== undefined) { newValue = entry.inheritedValue; entry.valueSource = ValueSource.Inherited; } else { newValue = property.defaultValue; entry.valueSource = ValueSource.Default; } break; case ValueSource.Local: if (entry.cssValue !== undefined) { newValue = entry.cssValue; entry.valueSource = ValueSource.Css; } else if (entry.inheritedValue !== undefined) { newValue = entry.inheritedValue; entry.valueSource = ValueSource.Inherited; } else { newValue = property.defaultValue; entry.valueSource = ValueSource.Default; } break; case ValueSource.VisualState: if (entry.localValue !== undefined) { newValue = entry.localValue; entry.valueSource = ValueSource.Local; } else if (entry.cssValue !== undefined) { newValue = entry.cssValue; entry.valueSource = ValueSource.Css; } else if (entry.inheritedValue !== undefined) { newValue = entry.inheritedValue; entry.valueSource = ValueSource.Inherited; } else { newValue = property.defaultValue; entry.valueSource = ValueSource.Default; } break; } return newValue; } }