UNPKG

@pilotlab/lux-attributes

Version:

A luxurious user experience framework, developed by your friends at Pilot.

342 lines (276 loc) 13.8 kB
import is from '@pilotlab/lux-is'; import { Identifier } from '@pilotlab/lux-ids'; import { Numbers } from '@pilotlab/lux-numbers'; import { IPromise, Result } from '@pilotlab/lux-result'; import { NodeType, NodeChangeType, NodeBase } from '@pilotlab/lux-nodes'; import { AttributeChangeActions, AttributesWrapType, DataType } from './attributeEnums'; import { Animation, IAnimationBatch, IAnimation, IAnimationEventArgs, IAnimationEaseFunction } from '@pilotlab/lux-animation'; import AttributeTools from './attributeTools'; import AttributesBase from './attributesBase'; import AttributeSetReturn from './attributeSetReturn'; import AttributeChangeOptions from './attributeChangeOptions'; import IAttribute from './interfaces/iAttribute'; import IAttributes from './interfaces/iAttributes'; import IAttributeFactory from './interfaces/iAttributeFactory'; import IAttributeChangeOptions from './interfaces/iAttributeChangeOptions'; import IAttributeSetReturn from './interfaces/iAttributeSetReturn'; import IAttributeUpdateTracker from './interfaces/iAttributeUpdateTracker'; import AttributeEventArgs from './attributeEventArgs'; export abstract class AttributeBase< TValue, TAttribute extends IAttribute, TAttributes extends IAttributes, TRoot extends IAttribute> extends NodeBase<TAttribute, TAttributes, TRoot> implements IAttribute { constructor( factory:IAttributeFactory, key:string = '', value?:any, dataType?:DataType, label?:string, isRoot:boolean = false, isInitialize:boolean = true ) { super(null, null, null, null, false); if (isInitialize) this.initialize(factory, key, value, dataType, label, isRoot); } protected p_onInitializeStarted(result:Result<any>, args:any[]):IPromise<any> { const factory:IAttributeFactory = args[0]; const key:string = args[1]; const value:any = args[2]; const dataType:DataType = args[3]; const label:string = args[4]; const isRoot:boolean = args[5]; super.p_onInitializeStarted(new Result<any>(), [factory, isRoot ? NodeType.ROOT : NodeType.BASIC, key, label]).then(() => { /// Set the data type first, then the value if (is.notEmpty(dataType)) this.p_dataType = dataType; else { if (is.notEmpty(value)) this.p_dataType = AttributeTools.getDataType(value); else this.p_dataType = DataType.OBJECT; } if (this.p_dataType === DataType.COLLECTION) { this.p_wrapAttributes(value, AttributesWrapType.CLEAR_BASE); } else if (is.notEmpty(value)) this.p_value = value; result.resolve(value); }); return result; } /** * All data associated with this entity will be saved to the provided attribute collection, * and all change events will be propagated through that collection. */ protected p_wrapAttributes(value?:any, wrapType:AttributesWrapType = AttributesWrapType.OVERWRITE_BASE):void { let changeOptions:IAttributeChangeOptions = AttributeChangeOptions.noSaveOrSignal; let attributes:IAttributes; /// If an Attributes is passed as the value, it will be directly assigned to this.p_value. if (is.notEmpty(value)) { attributes = this.create.collection.fromAny(value, this); } if (is.empty(attributes)) attributes = this.create.collection.instance(this); if (is.notEmpty(this.p_value)) { if (wrapType === AttributesWrapType.CLEAR_BASE) { /// Do nothing. } else if (wrapType === AttributesWrapType.CLEAR_WRAPPED) { attributes.clear(changeOptions); attributes.update(this.p_value, changeOptions); } else if (wrapType === AttributesWrapType.OVERWRITE_BASE) { this.p_value.update(attributes, changeOptions); attributes.clear(AttributeChangeOptions.noSaveOrSignal); attributes.update(this.p_value, changeOptions); } else if (wrapType === AttributesWrapType.OVERWRITE_WRAPPED) { attributes.update(this.p_value, changeOptions); } } this.p_value = attributes; this.p_value.internalParent = this; this.p_children = this.p_value; } /*====================================================================* START: Factory *====================================================================*/ get create():IAttributeFactory { return this.p_factory; } protected p_factory:IAttributeFactory; /*====================================================================* START: Properties *====================================================================*/ /** * The value of the associated data. */ get value():TValue { return <TValue>this.p_value; } set value(value:TValue) { this.set(value, AttributeChangeOptions.default.durationZero); } protected p_value:any; get valueTarget():TValue { return <TValue>this.p_valueTarget; } protected p_valueTarget:any; get valuePrevious():TValue { return <TValue>this.p_valuePrevious; } protected p_valuePrevious:any; get dataType():DataType { return this.p_dataType; } set dataType(value:DataType) { this.p_dataType = value; } protected p_dataType:DataType = DataType.STRING; get attributes():IAttributes { return this.p_children; } /** * Attributes will be considered empty if no key or value has been set. */ get isEmpty():boolean { return is.empty(this.key) || (is.empty(this.p_value) && is.empty(this.p_valueTarget)); } /** * If this function returns true, the attribute will be omitted from data transfer objects * when saving data to the data store. */ get omit():(value:TValue) => boolean { return this.p_omit; } set omit(fn:(value:TValue) => boolean) { this.p_omit = fn; } protected p_omit:(value:TValue) => boolean; set validate(fn:(value:TValue) => TValue) { this.p_validate = fn; } protected p_validate:(value:TValue) => TValue; get copy():TAttribute { return null } /** * An automatically generated unique ID that is used when animating attribute values. */ get animationKey():string { if (is.empty(this._animationKey)) this._animationKey = this.key + '_' + Identifier.getSessionUniqueInteger(); return this._animationKey; } private _animationKey:string; protected p_animation:IAnimationBatch; /*====================================================================* START: Internal *====================================================================*/ /// Overriden to take isSave into account. internalChanged(args:AttributeEventArgs<any>):void { if ((args.changeOptions.isSignalChange || args.changeOptions.isSave) && this.isSignalChange) this.changed.dispatch(args); if (is.notEmpty(this.parentCollection) && (is.notEmpty(this.parentCollection.internalChanged))) { this.parentCollection.internalChanged(args); } } /*====================================================================* START: Public Methods *====================================================================*/ /** * Save the value to the attribute. */ set( value:any, changeOptions:IAttributeChangeOptions = AttributeChangeOptions.default, ):IPromise<IAttributeSetReturn> { let result:IPromise<IAttributeSetReturn> = new Result<IAttributeSetReturn>(); let returnValue:IAttributeSetReturn = new AttributeSetReturn<TAttribute>(<TAttribute><IAttribute>this, false); /// Store the previous value this.p_valuePrevious = this.p_value; /// Validate the new value, if we can. if (is.notEmpty(this.p_validate)) value = this.p_validate(value); /// Round numerical values to 4 decimal places before saving. if (typeof value === 'number') { let rounded:any = Numbers.round(value, 4); value = <TValue>rounded; } /// If we're running an animation, interrupt it. if (is.notEmpty(this.p_animation)) { this.p_animation.interrupt(); this.p_animation = null; } /// Run this function where we simply need to set the value and update the returnValue. const setValue = ():void => { if (this.p_value !== value) { this.p_value = value; this.p_valueTarget = value; returnValue.isChanged = true; this.doChanged(changeOptions.action); } else if (changeOptions.action === AttributeChangeActions.SAVE) { this.doChanged(changeOptions.action); } result.resolve(returnValue); }; if (this.p_value === value) { if (changeOptions.action === AttributeChangeActions.SAVE) { this.doChanged(changeOptions.action); result.resolve(returnValue); } } else if (this.p_dataType === DataType.COLLECTION) { /// This attribute holds a collection... if (is.notEmpty(this.p_children) && (this.p_children instanceof AttributesBase)) { this.p_children.update(value, changeOptions).then((tracker:IAttributeUpdateTracker) => { returnValue.isChanged = tracker.isChanged; result.resolve(returnValue); }); } } else if (is.notEmpty(changeOptions.durationSpeed) && changeOptions.durationSpeed !== 0) { /// We're not a collection, and a duration or speed value was passed, /// so we may need to set up an animation. let duration:number = Animation.getDuration(changeOptions.durationSpeed); let easeFinal:IAnimationEaseFunction = Animation.validateSpeed(changeOptions.durationSpeed).ease; switch (this.dataType) { case DataType.NUMBER: case DataType.NUMBER_INT: case DataType.NUMBER_DOUBLE: if (duration > 0) { if (this.p_value !== value) { this.p_valueTarget = value; returnValue.isChanged = true; /// We need to animate const animation:IAnimation = Animation.go(this.p_value, value, duration, easeFinal, changeOptions.repeatCount, this.animationKey); animation.ticked.listen((args:IAnimationEventArgs) => { this.p_value = args.values[0].current; const action:AttributeChangeActions = changeOptions.action === AttributeChangeActions.SAVE ? AttributeChangeActions.SIGNAL_CHANGE : changeOptions.action; this.doChanged(action); }, this); animation.completed.listenOnce(() => { this.p_value = value; this.doChanged(changeOptions.action); result.resolve(returnValue); }); returnValue.animation.animations.add(animation); this.p_animation = returnValue.animation; result.resolve(returnValue); } else if (changeOptions.action === AttributeChangeActions.SAVE) { this.doChanged(changeOptions.action); } } else setValue(); break; default: setValue(); } } else setValue(); return result; } /** * Interrupt any animated value transitions that are in effect. */ interrupt():void { if (is.notEmpty(this.p_animation)) this.p_animation.interrupt(); } /** * Manually trigger a dispatch on the 'changed' signal. */ doChanged(action:AttributeChangeActions):void { let args:AttributeEventArgs<TAttribute> = new AttributeEventArgs<TAttribute>( <TAttribute><IAttribute>this, NodeChangeType.UPDATED, new AttributeChangeOptions(action).durationZero ); this.internalChanged(args); } toObject(isIncludeDataTypes:boolean = false, appendToObject?:any, isForceInclude:boolean = false):any { if (!isForceInclude && (is.notEmpty(this.p_omit) && this.p_omit(this.p_value))) return appendToObject; let object:any = {}; if (isIncludeDataTypes) { object['dataType'] = this.dataType; if (is.notEmpty(this.label) && this.label != this.key) object['label'] = this.label; if (this.p_value instanceof AttributesBase) { object['value'] = (<IAttributes>this.p_value).toObject(isIncludeDataTypes, isForceInclude); } else object['value'] = this.p_value; } else { if (this.p_value instanceof AttributesBase) { object = (<IAttributes>this.p_value).toObject(isIncludeDataTypes, isForceInclude); } else object = this.p_value; } if (is.notEmpty(appendToObject)) { appendToObject[this.key] = object; return appendToObject; } else return object; } toJson(isIncludeDataTypes:boolean = false):string { return JSON.stringify(this.toObject(isIncludeDataTypes)); } } // End class export default AttributeBase;