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; 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 { private _defaultValue: any; private _options: number; private _onChanged: definition.PropertyChangedCallback; private _onValidateValue: definition.PropertyValidationCallback; private _equalityComparer: definition.PropertyEqualityComparer; constructor( defaultValue: any, options?: number, onChanged?: definition.PropertyChangedCallback, onValidateValue?: definition.PropertyValidationCallback, equalityComparer?: definition.PropertyEqualityComparer) { this._defaultValue = defaultValue; this._options = options; if (types.isNullOrUndefined(this._options)) { this._options = PropertyMetadataSettings.None; } this._onChanged = onChanged; this._onValidateValue = onValidateValue; this._equalityComparer = equalityComparer; } public get defaultValue(): any { return this._defaultValue; } public get options(): number { return this._options; } public get onValueChanged(): definition.PropertyChangedCallback { return this._onChanged; } public set onValueChanged(value: definition.PropertyChangedCallback) { this._onChanged = value; } public get onValidateValue(): definition.PropertyValidationCallback { return this._onValidateValue; } public get equalityComparer(): definition.PropertyEqualityComparer { return this._equalityComparer; } public get affectsLayout(): boolean { return (this._options & PropertyMetadataSettings.AffectsLayout) === PropertyMetadataSettings.AffectsLayout; } public get affectsStyle(): boolean { return (this._options & PropertyMetadataSettings.AffectsStyle) === PropertyMetadataSettings.AffectsStyle; } public get inheritable(): boolean { return (this._options & PropertyMetadataSettings.Inheritable) === PropertyMetadataSettings.Inheritable; } } export class Property implements definition.Property { private _metadata: PropertyMetadata; private _key: string; private _name: string; private _ownerType: string; private _id: number; private _valueConverter: (value: any) => any; constructor(name: string, ownerType: string, metadata: PropertyMetadata, 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._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; } public defaultValueGetter: (instance: definition.DependencyObservable) => definition.NativeValueResult; public get name(): string { return this._name; } public get id(): number { return this._id; } public get metadata(): PropertyMetadata { return this._metadata; } public isValidValue(value: Object): boolean { if (this.metadata.onValidateValue) { return this.metadata.onValidateValue(value); } // TODO: consider type check here (e.g. passing function where object is expected) return true; } public get valueConverter(): (value: string) => any { return this._valueConverter; } public _getEffectiveValue(entry: PropertyEntry): any { if (types.isDefined(entry.localValue)) { entry.valueSource = ValueSource.Local; return entry.localValue; } if (types.isDefined(entry.inheritedValue)) { entry.valueSource = ValueSource.Inherited; return entry.inheritedValue; } entry.valueSource = ValueSource.Default; return this.metadata.defaultValue; } } export class PropertyEntry implements definition.PropertyEntry { private _property: Property; private _valueSource: number; private _inheritedValue: any; private _cssValue: any; private _localValue: any; private _effectiveValue: any; private _visualStateValue: any; constructor(property: Property) { this._property = property; } get property(): Property { return this._property; } get effectiveValue() { if (!this._effectiveValue) { this._effectiveValue = this._property._getEffectiveValue(this); } return this._effectiveValue; } get valueSource(): number { return this._valueSource; } set valueSource(value: number) { this._valueSource = value; } get localValue(): any { return this._localValue; } set localValue(value: any) { this._localValue = value; this._effectiveValue = undefined; } get inheritedValue(): any { return this._inheritedValue; } set inheritedValue(value: any) { this._inheritedValue = value; this._effectiveValue = undefined; } get cssValue(): any { return this._cssValue; } set cssValue(value: any) { this._cssValue = value; this._effectiveValue = undefined; } get visualStateValue(): any { return this._visualStateValue; } set visualStateValue(value: any) { this._visualStateValue = value; this._effectiveValue = undefined; } public resetValue() { this._valueSource = ValueSource.Default; this._visualStateValue = undefined; this._localValue = undefined; this._cssValue = undefined; this._inheritedValue = undefined; this._effectiveValue = undefined; } } var defaultValueForPropertyPerType: Map = new Map(); export class DependencyObservable extends Observable { private _propertyEntries = {}; public set(name: string, value: any) { var property = getPropertyByNameAndType(name, this); if (property) { this._setValue(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) { let realValue = WrappedValue.unwrap(value); if (!property.isValidValue(realValue)) { throw new Error("Invalid value " + realValue + " for property " + property.name); } if (types.isUndefined(source)) { source = ValueSource.Local; } this._setValueInternal(property, value, source); } 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 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 (types.isUndefined(defaultValue) && view._nativeView) { let defaultValueResult = property.defaultValueGetter(this); defaultValue = defaultValueResult.result; if (defaultValueResult.cacheable) { defaultValueForPropertyPerType.set(key, defaultValue); } } return defaultValue; } return property.metadata.defaultValue; } public _resetValue(property: Property, source?: number) { if (!(property.id in this._propertyEntries)) { return; } if (types.isDefined(source)) { // resetting particular modifier to undefined will remove it from the effective value composition this._setValueInternal(property, undefined, source); } else { var currentValue = this._getValue(property); delete this._propertyEntries[property.id]; var newValue = this._getValue(property); var comparer: (x: any, y: any) => boolean = property.metadata.equalityComparer || this._defaultComparer; if (!comparer(currentValue, newValue)) { this._onPropertyChanged(property, currentValue, newValue); } } } public _onPropertyChanged(property: Property, oldValue: any, newValue: any) { let realNewValue = WrappedValue.unwrap(newValue); if (property.metadata.onValueChanged) { property.metadata.onValueChanged({ object: this, property: property, eventName: Observable.propertyChangeEvent, newValue: realNewValue, oldValue: oldValue }); } if (this.hasListeners(Observable.propertyChangeEvent)) { var changeData = super._createPropertyChangeData(property.name, newValue); this.notify(changeData); } let eventName = property.name + "Change"; if (this.hasListeners(eventName)) { var ngChangedData = { eventName: eventName, propertyName: property.name, object: this, value: realNewValue } this.notify(ngChangedData); } } public _eachSetProperty(callback: (property: Property) => boolean) { var i; var key; var entry: PropertyEntry; var retVal: boolean; var keys = Object.keys(this._propertyEntries); for (i = 0; i < keys.length; i++) { key = keys[i]; entry = this._propertyEntries[key]; retVal = callback(entry.property); if (!retVal) { break; } } } public toString(): string { return this.typeName; } private _setValueInternal(property: Property, value: any, source: number) { let realValue = WrappedValue.unwrap(value); // Convert the value to the real property type in case it is coming as a string from CSS or XML. if (types.isString(realValue) && property.valueConverter) { realValue = property.valueConverter(realValue); } var entry: PropertyEntry = this._propertyEntries[property.id]; if (!entry) { entry = new PropertyEntry(property); this._propertyEntries[property.id] = entry; } var currentValue = entry.effectiveValue; switch (source) { case ValueSource.Css: entry.cssValue = realValue; break; case ValueSource.Inherited: entry.inheritedValue = realValue; break; case ValueSource.Local: entry.localValue = realValue; break; case ValueSource.VisualState: entry.visualStateValue = realValue; break; } var comparer: (x: any, y: any) => boolean = property.metadata.equalityComparer || this._defaultComparer; if ((value && value.wrapped) || !comparer(currentValue, entry.effectiveValue)) { this._onPropertyChanged(property, currentValue, entry.effectiveValue); } } private _defaultComparer(x: any, y: any): boolean { return x === y; } }