From e2443833eb6dfe0e3b45685e058d52ed4ac4d6fe Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sun, 7 Jan 2024 00:11:42 -0700 Subject: [PATCH] Add utility data field from the dnd5e system --- module/models/fields/MappingField.mjs | 132 ++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 module/models/fields/MappingField.mjs diff --git a/module/models/fields/MappingField.mjs b/module/models/fields/MappingField.mjs new file mode 100644 index 0000000..9d62f66 --- /dev/null +++ b/module/models/fields/MappingField.mjs @@ -0,0 +1,132 @@ +/* +This class was pulled from the official D&D 5e Foundry System + +Original Authors: + Jeff Hitchcock (https://github.com/arbron) + Andrew (https://github.com/aaclayton) + +Accessed on: 2024/01/06 + +Source Code: https://github.com/foundryvtt/dnd5e/blob/677c6ae127aa885bd4cb9a83f95180a655a8623b/module/data/fields.mjs#L264 +*/ + + +/** + * A subclass of ObjectField that represents a mapping of keys to the provided DataField type. + * + * @param {DataField} model The class of DataField which should be embedded in this field. + * @param {MappingFieldOptions} [options={}] Options which configure the behavior of the field. + * @property {string[]} [initialKeys] Keys that will be created if no data is provided. + * @property {MappingFieldInitialValueBuilder} [initialValue] Function to calculate the initial value for a key. + * @property {boolean} [initialKeysOnly=false] Should the keys in the initialized data be limited to the keys provided + * by `options.initialKeys`? + */ +export class MappingField extends foundry.data.fields.ObjectField { + constructor(model, options) { + if ( !(model instanceof foundry.data.fields.DataField) ) { + throw new Error("MappingField must have a DataField as its contained element"); + } + super(options); + + /** + * The embedded DataField definition which is contained in this field. + * @type {DataField} + */ + this.model = model; + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + static get _defaults() { + return foundry.utils.mergeObject(super._defaults, { + initialKeys: null, + initialValue: null, + initialKeysOnly: false + }); + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + _cleanType(value, options) { + Object.entries(value).forEach(([k, v]) => value[k] = this.model.clean(v, options)); + return value; + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + getInitialValue(data) { + let keys = this.initialKeys; + const initial = super.getInitialValue(data); + if ( !keys || !foundry.utils.isEmpty(initial) ) return initial; + if ( !(keys instanceof Array) ) keys = Object.keys(keys); + for ( const key of keys ) initial[key] = this._getInitialValueForKey(key); + return initial; + } + + /* -------------------------------------------- */ + + /** + * Get the initial value for the provided key. + * @param {string} key Key within the object being built. + * @param {object} [object] Any existing mapping data. + * @returns {*} Initial value based on provided field type. + */ + _getInitialValueForKey(key, object) { + const initial = this.model.getInitialValue(); + return this.initialValue?.(key, initial, object) ?? initial; + } + + /* -------------------------------------------- */ + + /** @override */ + _validateType(value, options={}) { + if ( foundry.utils.getType(value) !== "Object" ) throw new Error("must be an Object"); + const errors = this._validateValues(value, options); + if ( !foundry.utils.isEmpty(errors) ) throw new foundry.data.fields.ModelValidationError(errors); + } + + /* -------------------------------------------- */ + + /** + * Validate each value of the object. + * @param {object} value The object to validate. + * @param {object} options Validation options. + * @returns {Object} An object of value-specific errors by key. + */ + _validateValues(value, options) { + const errors = {}; + for ( const [k, v] of Object.entries(value) ) { + const error = this.model.validate(v, options); + if ( error ) errors[k] = error; + } + return errors; + } + + /* -------------------------------------------- */ + + /** @override */ + initialize(value, model, options={}) { + if ( !value ) return value; + const obj = {}; + const initialKeys = (this.initialKeys instanceof Array) ? this.initialKeys : Object.keys(this.initialKeys ?? {}); + const keys = this.initialKeysOnly ? initialKeys : Object.keys(value); + for ( const key of keys ) { + const data = value[key] ?? this._getInitialValueForKey(key, value); + obj[key] = this.model.initialize(data, model, options); + } + return obj; + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + _getField(path) { + if ( path.length === 0 ) return this; + else if ( path.length === 1 ) return this.model; + path.shift(); + return this.model._getField(path); + } +} \ No newline at end of file