@pilotlab/lux-attributes
Version:
A luxurious user experience framework, developed by your friends at Pilot.
342 lines (276 loc) • 13.8 kB
text/typescript
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;