mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-16 03:31:45 +08:00

* Add css-perf app. * Style properties now check only properties that are set. Image utils module required on top (instead of in onMeasure) to improve performance. Remove try/catch block when Style applies native property. * fix tslint * Fix broken merge Update package.json version * Failed miserably - if the try/catch around applyProperty method is removed - TextField fails big time. TextField needs some good refactoring as well as calls to _updateTextDecoration & _updateTextTransform utils - setTextTransform & setTextDecoration should be split, typing support should be added.
445 lines
16 KiB
TypeScript
445 lines
16 KiB
TypeScript
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<string, any> = new Map<string, any>();
|
|
|
|
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 = (<any>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;
|
|
}
|
|
} |