UNPKG

@pilotlab/data

Version:

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

395 lines (312 loc) 15.3 kB
import is from '@pilotlab/is'; import { List } from '@pilotlab/collections'; import { Signal } from '@pilotlab/signals'; import { Identifier } from '@pilotlab/ids'; import { IPromise, Result } from '@pilotlab/result'; import { NodeType, NodeChangeType, NodeBase } from '@pilotlab/nodes'; import { AttributeChangeActions, AttributesWrapType, DataType } from './attributeEnums'; import { IAnimationBatch, } from '@pilotlab/animation'; import DataTools from '../dataTools'; import AttributesBase from './attributesBase'; import AttributeSetReturn from './attributeSetReturn'; import AttributeChangeOptions from './attributeChangeOptions'; 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'; import Attributes from './attributes'; import AttributeRoot from './attributeRoot'; import {NodeEventArgs} from "../../../nodes/src/nodeEventArgs"; import INodeChangeOptions from "../../../nodes/src/interfaces/iNodeChangeOptions"; import Debug from "../../../debug"; import INode from "../../../nodes/src/interfaces/iNode"; export abstract class AttributeBase< TValue, TNode extends AttributeBase<any, any, any, any>, TNodes extends Attributes, TRoot extends AttributeRoot> extends NodeBase<TNode, TNodes, TRoot> { protected constructor( factory:IAttributeFactory, value?:any, dataType?:(DataType | string), label?:string, key:string = '', nodeType:NodeType = NodeType.BASIC, isInitialize:boolean = true ) { super(factory, nodeType, key, false); this.p_value = null; if (is.notEmpty(label, true)) this.p_label = label; this.p_dataType = DataType.UNDEFINED; this.p_changed = new Signal<AttributeEventArgs<TNode>>(false); this.p_isSignalChange = true; this.p_isAllowSave = true; this.p_isAllowAutoPropterties = true; this.p_returnValue = new AttributeSetReturn<TNode>(<TNode><AttributeBase<any, any, any, any>>this, false); this.p_formatter = (value:any) => value; if (isInitialize) this.initialize(value, dataType, nodeType); } protected p_onInitializeStarted(result:Result<any>, args:any[]):IPromise<any> { const value:any = args[0]; const dataType:(DataType | string) = args[1]; const nodeType:NodeType = args[2]; super.p_onInitializeStarted(new Result<any>(), [nodeType]).then(() => { /// Set the data type first, then the value if (is.notEmpty(dataType)) this.p_dataType = dataType; else { if (is.notEmpty(value, false)) this.p_dataType = DataTools.getDataType(value); else this.p_dataType = DataType.OBJECT; } if (this.create.getBaseDataType(this.p_dataType) === DataType.COLLECTION) { this.p_wrapAttributes(value, AttributesWrapType.CLEAR_BASE); } else if (is.notEmpty(value, false)) 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 instance was 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); this.p_value = attributes; 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); } } } /*====================================================================* START: Factory *====================================================================*/ get create():IAttributeFactory { return this.p_factory; } protected p_factory:IAttributeFactory; /*====================================================================* START: Properties *====================================================================*/ protected p_returnValue:IAttributeSetReturn; /** * A UI-friendly title for the attribute that can be presented to users. * If no label is set, the key of the node will be returned. */ get label():string { return is.notEmpty(this.p_label, true) ? this.p_label : <string>this.key; } set label(value:string) { this.p_label = value; } protected p_label:string; /** * A brief description of that attribute that can be used to supply info to the user * about how the attribute should be used. */ get summary():string { return is.notEmpty(this.p_summary, true) ? this.p_summary : ''; } set summary(value:string) { this.p_summary = value; } protected p_summary:string; /** * The value of the associated data. * This property cannot be set directly. You must use the set() method to change the value. * This allows attributes to be wrapped in an AttributeReader instance, which effectively * renders them immutable. */ get value():TValue { return <TValue>this.p_value; } set value(value:TValue) { this.set(value, this.isSignalChange ? AttributeChangeOptions.save : AttributeChangeOptions.zero); } get formatted():TValue { let value:TValue = <TValue>this.p_value; value = this.p_format(value); return value; } protected p_value:any; protected p_valueTarget:any; /// Used for animation. get isCollection():boolean { return ( this.nodeType === NodeType.ROOT || this.nodeType === NodeType.COLLECTION || this.create.getBaseDataType(this.dataType) === DataType.COLLECTION ); } /// This property will hold dynamically generated getters and setters /// for each child attribute that is added to this attribute. get attributes():IAttributes { return this.p_value; } get children():TNodes { return this.isCollection ? this.p_value : this.p_children; } get valuePrevious():TValue { return <TValue>this.p_valuePrevious; } protected p_valuePrevious:any; get dataType():(DataType | string) { return this.p_dataType; } protected p_dataType:(DataType | string); /** * Attributes will be considered empty if no key or value has been set. */ get isEmpty():boolean { if (is.undefined(this.p_value)) return true; if (is.empty(this.p_value, false)) return true; if (is.empty(this.key, true)) return true; if (is.empty(this.dataType, true)) return true; if (this.dataType === DataType.UNDEFINED) return true; if (this.dataType === DataType.NULL) return true; return false; } /** * If this function returns true, the attribute will be omitted from data transfer objects * when saving data to the data store. */ get omitFromDataStore():(value:TValue) => boolean { return this.p_omitFromDataStore; } set omitFromDataStore(fn:(value:TValue) => boolean) { this.p_omitFromDataStore = fn; } protected p_omitFromDataStore:(value:TValue) => boolean; get formatter():(value:any) => TValue { return this.p_formatter; } set formatter(value:(value:any) => TValue) { this.p_formatter = value; } protected p_formatter:(value:any) => TValue; protected p_format(value:any):TValue { return this.p_formatter(value); } get copy():TNode { return <TNode>this.create.fromObject(this.toObject()); } /** * 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; get isEditable():boolean { return this.p_isEditable; } set isEditable(value:boolean) { this.p_isEditable = value; } protected p_isEditable:boolean; get isHidden():boolean { return this.p_isHidden; } set isHidden(value:boolean) { this.p_isHidden = value; } protected p_isHidden:boolean; get isAllowSave():boolean { return this.p_isAllowSave; } set isAllowSave(value:boolean) { if (this.isCollection) this.children.isAllowSave = value; this.p_isAllowSave = value; } protected p_isAllowSave:boolean; /** * Should we allow accessor properties to be generated automatically when * a new child Attribute is added to our Attribute collection? */ get isAllowAutoProperties():boolean { return this.p_isAllowAutoPropterties; } set isAllowAutoProperties(value:boolean) { this.p_isAllowAutoPropterties = value; } protected p_isAllowAutoPropterties:boolean; /*====================================================================* START: Signals *====================================================================*/ get changed():Signal<AttributeEventArgs<TNode>> { return this.p_changed; } protected p_changed:Signal<AttributeEventArgs<TNode>>; /*====================================================================* START: Internal *====================================================================*/ /// Overridden to take isSave into account. internalChanged(args:AttributeEventArgs<TNode>):void { if (!this.isSignalChange && !this.isAllowSave) return; 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, ):IAttributeSetReturn { let returnValue:IAttributeSetReturn = this.p_returnValue; if (is.empty(value)) return returnValue; if (this.create.getBaseDataType(this.p_dataType) === DataType.COLLECTION) { /// This attribute holds a collection... if (is.notEmpty(this.p_value) && (this.p_value instanceof AttributesBase)) { let tracker:IAttributeUpdateTracker = this.p_value.update(value, changeOptions); returnValue.updateTracker = tracker; tracker.then((tracker:IAttributeUpdateTracker) => { returnValue.isChanged = tracker.isChanged; }); } } else { value = this.p_format(value); if (this.p_value === value) { if (changeOptions.action === AttributeChangeActions.SAVE) { this.doChanged(changeOptions.action); } return returnValue; } this.p_valuePrevious = this.p_valueTarget; this.p_value = value; this.p_valueTarget = value; returnValue.isChanged = true; this.doChanged(changeOptions.action); } return returnValue; } /** * Interrupt any animated value transitions that are in effect. */ interruptAnimation():void { if (is.notEmpty(this.p_animation)) this.p_animation.interrupt(); } /** * Manually trigger a dispatch on the 'changed' signal. */ doChanged(action:AttributeChangeActions):void { if (!this.isSignalChange && !this.isAllowSave) return; else if (action === AttributeChangeActions.SIGNAL_CHANGE && !this.isSignalChange) return; else if (action === AttributeChangeActions.SAVE && !this.isAllowSave) return; let args:AttributeEventArgs<TNode> = new AttributeEventArgs<TNode>( <TNode><AttributeBase<any, any, any, any>>this, NodeChangeType.UPDATED, new AttributeChangeOptions(action).durationZero ); this.internalChanged(args); } toObject(isIncludeDataTypes:boolean = true, isIncludeLabels:boolean = true, appendToObject?:any, isForceInclude:boolean = false):any { if (!isForceInclude && (is.notEmpty(this.p_omitFromDataStore) && this.p_omitFromDataStore(this.p_value))) return appendToObject; let object:any = {}; if ( isIncludeDataTypes || (isIncludeLabels && is.notEmpty(this.label, true) && this.label !== this.key) ) { if (isIncludeDataTypes) object['dataType'] = this.dataType.toString(); if (isIncludeLabels && (is.notEmpty(this.label, true) && this.label !== this.key)) object['label'] = this.label; object['isEditable'] = this.isEditable; object['isHidden'] = this.isHidden; if (this.p_value instanceof AttributesBase) { object['value'] = (<IAttributes>this.p_value).toObject(isIncludeDataTypes, isIncludeLabels, isForceInclude); } else object['value'] = this.formatted; } else { if (this.p_value instanceof AttributesBase) { object = (<IAttributes>this.p_value).toObject(isIncludeDataTypes, isIncludeLabels, isForceInclude); } else object = this.formatted; } if (is.notEmpty(appendToObject)) { appendToObject[this.key] = object; return appendToObject; } else return object; } internalAttached(changeOptions:INodeChangeOptions):void { if (is.notEmpty(this.parentCollection)) { this.isSignalChange = this.parentCollection.isSignalChange; this.isAllowSave = this.parentCollection.isAllowSave; } super.internalAttached(changeOptions); } } // End class export default AttributeBase;