diff --git a/lang/en.json b/lang/en.json
index e08325c..05254bd 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -7,7 +7,9 @@
"SIMPLE.NotifyInitFormulaUpdated": "Initiative formula was updated to:",
"SIMPLE.NotifyInitFormulaInvalid": "Initiative formula was invalid:",
"SIMPLE.NotifyGroupDuplicate": "Attribute group already exists.",
+ "SIMPLE.NotifyGroupAttrDuplicate": "Attribute group already exists as an attribute.",
"SIMPLE.NotifyGroupAlphanumeric": "Attribute group names may not contain spaces or periods.",
+ "SIMPLE.NotifyAttrDuplicate": "Attribute key already exists as a group.",
"SIMPLE.ResourceMin": "Min",
"SIMPLE.ResourceValue": "Value",
diff --git a/module/actor-sheet.js b/module/actor-sheet.js
index f73829b..dc7a9ca 100644
--- a/module/actor-sheet.js
+++ b/module/actor-sheet.js
@@ -1,4 +1,4 @@
-import { ATTRIBUTE_TYPES } from "./constants.js";
+import { EntitySheetHelper } from "./helper.js";
/**
* Extend the basic ActorSheet with some very simple modifications
@@ -24,45 +24,9 @@ export class SimpleActorSheet extends ActorSheet {
/** @override */
getData() {
const data = super.getData();
- data.dtypes = ATTRIBUTE_TYPES;
- for ( let attr of Object.values(data.data.attributes) ) {
- if ( attr.dtype ) {
- attr.isCheckbox = attr.dtype === "Boolean";
- attr.isResource = attr.dtype === "Resource";
- }
- }
- // Initialize ungrouped attributes for later.
- data.data.ungroupedAttributes = {};
-
- // Build an array of sorted group keys.
- const groups = data.data.groups || {};
- let groupKeys = Object.keys(groups).sort((a, b) => {
- let aSort = groups[a].label ?? a;
- let bSort = groups[b].label ?? b;
- return aSort.localeCompare(bSort);
- });
-
- // Iterate over the sorted groups to add their attributes.
- for ( let key of groupKeys ) {
- let group = data.data.attributes[key] || {};
-
- // Initialize the attributes container for this group.
- if ( !data.data.groups[key]['attributes'] ) data.data.groups[key]['attributes'] = {};
-
- // Sort the attributes within the group, and then iterate over them.
- Object.keys(group).sort((a, b) => a.localeCompare(b)).forEach(attr => {
- // For each attribute, determine whether it's a checkbox or resource, and then add it to the group's attributes list.
- group[attr]['isCheckbox'] = group[attr]['dtype'] === 'Boolean';
- group[attr]['isResource'] = group[attr]['dtype'] === 'Resource';
- data.data.groups[key]['attributes'][attr] = group[attr];
- });
- }
-
- // Sort the remaining attributes attributes.
- Object.keys(data.data.attributes).filter(a => !groupKeys.includes(a)).sort((a, b) => a.localeCompare(b)).forEach(key => {
- data.data.ungroupedAttributes[key] = data.data.attributes[key];
- });
+ // Handle attribute groups.
+ EntitySheetHelper.getAttributeData(data);
// Add shorthand.
data.shorthand = !!game.settings.get("worldbuilding", "macroShorthand");
@@ -79,7 +43,7 @@ export class SimpleActorSheet extends ActorSheet {
html.find(".items .rollable").on("click", this._onItemRoll.bind(this));
// Handle rollable attributes.
- html.find(".attributes").on("click", "a.attribute-roll", this._onAttributeRoll.bind(this));
+ html.find(".attributes").on("click", "a.attribute-roll", EntitySheetHelper.onAttributeRoll.bind(this));
// Everything below here is only needed if the sheet is editable
if ( !this.options.editable ) return;
@@ -98,12 +62,6 @@ export class SimpleActorSheet extends ActorSheet {
li.slideUp(200, () => this.render(false));
});
- // Add or Remove Attribute
- html.find(".attributes").on("click", ".attribute-control", this._onClickAttributeControl.bind(this));
-
- // Add attribute groups.
- html.find(".groups").on("click", ".group-control", this._onClickAttributeGroupControl.bind(this));
-
// Add draggable for macros.
html.find(".attributes a.attribute-roll").each((i, a) => {
a.setAttribute("draggable", true);
@@ -112,38 +70,30 @@ export class SimpleActorSheet extends ActorSheet {
ev.dataTransfer.setData('text/plain', JSON.stringify(dragData));
}, false);
});
+
+ // Add or Remove Attribute
+ html.find(".attributes").on("click", ".attribute-control", EntitySheetHelper.onClickAttributeControl.bind(this));
+
+ // Add attribute groups.
+ html.find(".groups").on("click", ".group-control", EntitySheetHelper.onClickAttributeGroupControl.bind(this));
}
/* -------------------------------------------- */
/** @override */
async _onSubmit(event, {updateData=null, preventClose=false, preventRender=false}={}) {
- // Exit early if this isn't a named attribute.
- if ( event.currentTarget ) {
- if ( event.currentTarget.tagName.toLowerCase() == 'input' && !event.currentTarget.hasAttribute('name')) {
- return;
- }
- }
+ let attr = EntitySheetHelper.onSubmit(event);
- let self = $(event.currentTarget);
- let attr = null;
-
- // If this is the attribute key, we need to make a note of it so that we can restore focus when its recreated.
- if ( self.hasClass('attribute-key') ) {
- let val = self.val();
- let oldVal = self.parents('.attribute').data('attribute');
- oldVal = oldVal.includes('.') ? oldVal.split('.')[1] : oldVal;
- attr = self.attr('name').replace(oldVal, val);
- }
-
- // Submit the form.
- await super._onSubmit(event, {updateData: updateData, preventClose: preventClose, preventRender: preventRender});
-
- // If this was the attribute key, set a very short timeout and retrigger focus after the original element is deleted and the new one is inserted.
+ // Submit the form if attr is true or an attr key.
if ( attr ) {
- setTimeout(() => {
- $(`input[name="${attr}"]`).parents('.attribute').find('.attribute-value').focus();
- }, 10);
+ await super._onSubmit(event, {updateData: updateData, preventClose: preventClose, preventRender: preventRender});
+
+ // If attr is a key and not just true, set a very short timeout and retrigger focus after the original element is deleted and the new one is inserted.
+ if ( attr !== true) {
+ setTimeout(() => {
+ $(`input[name="${attr}"]`).parents('.attribute').find('.attribute-value').focus();
+ }, 10);
+ }
}
}
@@ -176,324 +126,15 @@ export class SimpleActorSheet extends ActorSheet {
});
}
- /**
- * Listen for the roll button on attributes.
- * @param {MouseEvent} event The originating left click event
- */
- _onAttributeRoll(event) {
- event.preventDefault();
- const button = event.currentTarget;
- const formula = button.closest(".attribute").querySelector(".attribute-value")?.value;
- const label = button.closest(".attribute").querySelector(".attribute-label")?.value;
- const chatLabel = label ?? button.parentElement.querySelector(".attribute-key").value;
-
- // If there's a formula, attempt to roll it.
- if ( formula ) {
- let r = new Roll(formula, this.actor.getRollData());
- r.roll().toMessage({
- user: game.user._id,
- speaker: ChatMessage.getSpeaker({ actor: this.actor }),
- flavor: `${chatLabel}`
- });
- }
- }
-
- /**
- * Listen for click events on an attribute control to modify the composition of attributes in the sheet
- * @param {MouseEvent} event The originating left click event
- * @private
- */
- async _onClickAttributeControl(event) {
- event.preventDefault();
- const a = event.currentTarget;
- const action = a.dataset.action;
- const group = a.dataset.group;
- let dtype = a.dataset.dtype;
- const attrs = this.object.data.data.attributes;
- const groups = this.object.data.data.groups;
- const form = this.form;
-
- // Add new attribute
- if ( action === "create" ) {
- // Determine the new attribute key for ungrouped attributes.
- let objKeys = Object.keys(attrs).filter(k => !Object.keys(groups).includes(k));
- let nk = Object.keys(attrs).length + 1;
- let newValue = `attr${nk}`;
- let newKey = document.createElement("div");
- while ( objKeys.includes(newValue) ) {
- ++nk;
- newValue = `attr${nk}`;
- };
-
- // Build options for construction HTML inputs.
- let htmlItems = {
- key: {
- type: "text",
- value: newValue
- }
- };
-
- // Grouped attributes.
- if ( group ) {
- objKeys = attrs[group] ? Object.keys(attrs[group]) : [];
- nk = objKeys.length + 1;
- newValue = `attr${nk}`;
- while ( objKeys.includes(newValue) ) {
- ++nk;
- newValue = `attr${nk}`;
- }
-
- // Update the HTML options used to build the new input.
- htmlItems.key.value = newValue;
- htmlItems.group = {
- type: "hidden",
- value: group
- };
- htmlItems.dtype = {
- type: "hidden",
- value: dtype
- };
- }
- // Ungrouped attributes.
- else {
- // Choose a default dtype based on the last attribute, fall back to "String".
- if (!dtype) {
- let lastAttr = document.querySelector('.attributes > .attributes-group .attribute:last-child .attribute-dtype')?.value;
- dtype = lastAttr ? lastAttr : "String";
- htmlItems.dtype = {
- type: "hidden",
- value: dtype
- };
- }
- }
-
- // Build the form elements used to create the new grouped attribute.
- newKey.innerHTML = this._getAttributeHtml(htmlItems, nk, group);
-
- // Append the form element and submit the form.
- newKey = newKey.children[0];
- form.appendChild(newKey);
- await this._onSubmit(event);
- }
-
- // Remove existing attribute
- else if ( action === "delete" ) {
- const li = a.closest(".attribute");
- li.parentElement.removeChild(li);
- await this._onSubmit(event);
- }
- }
-
- /**
- * Listen for click events and modify attribute groups.
- * @param {MouseEvent} event The originating left click event
- */
- async _onClickAttributeGroupControl(event) {
- event.preventDefault();
- const a = event.currentTarget;
- const action = a.dataset.action;
- const form = this.form;
-
- // Add new attribute group.
- if ( action === "create-group" ) {
- let newValue = $(a).siblings('.group-prefix').val();
- // Verify the new group key is valid, and use it to create the group.
- if ( newValue.length > 0 && this._validateGroup(newValue) ) {
- let newKey = document.createElement("div");
- newKey.innerHTML = ``;
- // Append the form element and submit the form.
- newKey = newKey.children[0];
- form.appendChild(newKey);
- await this._onSubmit(event);
- }
-
- }
-
- // Remove existing attribute
- else if ( action === "delete-group" ) {
- let groupHeader = a.closest(".group-header");
- let group = $(groupHeader).find('.group-key');
- // Create a dialog to confirm group deletion.
- new Dialog({
- title: game.i18n.localize("SIMPLE.DeleteGroup"),
- content: `${game.i18n.localize("SIMPLE.DeleteGroupContent")} ${group.val()}`,
- buttons: {
- confirm: {
- icon: '',
- label: game.i18n.localize("Yes"),
- callback: async () => {
- groupHeader.parentElement.removeChild(groupHeader);
- await this._onSubmit(event);
- }
- },
- cancel: {
- icon: '',
- label: game.i18n.localize("No"),
- }
- }
- }).render(true);
- }
- }
-
- /* -------------------------------------------- */
-
-
- /**
- * Return HTML for a new attribute to be applied to the form for submission.
- *
- * @param {Object} items Keyed object where each item has a "type" and "value" property.
- * @param {string} index Numeric index or key of the new attribute.
- * @param {string|boolean} group String key of the group, or false.
- *
- * @returns {string} Html string.
- */
- _getAttributeHtml(items, index, group = false) {
- // Initialize the HTML.
- let result = '
';
- // Iterate over the supplied keys and build their inputs (including whether or not they need a group key).
- for (let [key, item] of Object.entries(items)) {
- result = result + ``;
- }
- // Close the HTML and return.
- return result + '
';
- }
-
- /* -------------------------------------------- */
-
- /**
- * Validate whether or not a group name can be used.
- * @param {string} groupName Groupname to validate
- * @returns {boolean}
- */
- _validateGroup(groupName) {
- let groups = Object.keys(this.actor.data.data.groups);
-
- // Check for duplicate group keys.
- if ( groups.includes(groupName) ) {
- ui.notifications.error(game.i18n.localize("SIMPLE.NotifyGroupDuplicate") + ` (${groupName})`);
- return false;
- }
-
- // Check for whitespace or periods.
- if ( groupName.match(/[\s|\.]/i) ) {
- ui.notifications.error(game.i18n.localize("SIMPLE.NotifyGroupAlphanumeric"));
- return false;
- }
-
- return true;
- }
-
/** @override */
_updateObject(event, formData) {
// Handle attribute and group updates.
- formData = this._updateAttributes(formData);
- formData = this._updateGroups(formData);
+ formData = EntitySheetHelper.updateAttributes(formData, this);
+ formData = EntitySheetHelper.updateGroups(formData, this);
// Update the Actor with the new form values.
return this.object.update(formData);
}
- /**
- * Update attributes when updating an actor object.
- *
- * @param {Object} formData Form data object to modify keys and values for.
- * @returns {Object} updated formData object.
- */
- _updateAttributes(formData) {
- let groupKeys = [];
-
- // Handle the free-form attributes list
- const formAttrs = expandObject(formData).data.attributes || {};
- const attributes = Object.values(formAttrs).reduce((obj, v) => {
- let attrs = [];
- let group = null;
- // Handle attribute keys for grouped attributes.
- if ( !v["key"] ) {
- attrs = Object.keys(v);
- attrs.forEach(attrKey => {
- group = v[attrKey]['group'];
- groupKeys.push(group);
- let attr = v[attrKey];
- let k = v[attrKey]["key"] ? v[attrKey]["key"].trim() : attrKey.trim();
- if ( /[\s\.]/.test(k) ) return ui.notifications.error("Attribute keys may not contain spaces or periods");
- delete attr["key"];
- // Add the new attribute if it's grouped, but we need to build the nested structure first.
- if ( !obj[group] ) {
- obj[group] = {};
- }
- obj[group][k] = attr;
- });
- }
- // Handle attribute keys for ungrouped attributes.
- else {
- let k = v["key"].trim();
- if ( /[\s\.]/.test(k) ) return ui.notifications.error("Attribute keys may not contain spaces or periods");
- delete v["key"];
- // Add the new attribute only if it's ungrouped.
- if ( !group ) {
- obj[k] = v;
- }
- }
- return obj;
- }, {});
-
- // Remove attributes which are no longer used
- for ( let k of Object.keys(this.object.data.data.attributes) ) {
- if ( !attributes.hasOwnProperty(k) ) attributes[`-=${k}`] = null;
- }
-
- // Remove grouped attributes which are no longer used.
- for ( let group of groupKeys) {
- if ( this.object.data.data.attributes[group] ) {
- for ( let k of Object.keys(this.object.data.data.attributes[group]) ) {
- if ( !attributes[group].hasOwnProperty(k) ) attributes[group][`-=${k}`] = null;
- }
- }
- }
-
- // Re-combine formData
- formData = Object.entries(formData).filter(e => !e[0].startsWith("data.attributes")).reduce((obj, e) => {
- obj[e[0]] = e[1];
- return obj;
- }, {_id: this.object._id, "data.attributes": attributes});
-
- return formData;
- }
-
- /**
- * Update attribute groups when updating an actor object.
- *
- * @param {Object} formData Form data object to modify keys and values for.
- * @returns {Object} updated formData object.
- */
- _updateGroups(formData) {
- // Handle the free-form groups list
- const formGroups = expandObject(formData).data.groups || {};
- const groups = Object.values(formGroups).reduce((obj, v) => {
- // If there are duplicate groups, collapse them.
- if ( Array.isArray(v["key"]) ) {
- v["key"] = v["key"][0];
- }
- // Trim and clean up.
- let k = v["key"].trim();
- if ( /[\s\.]/.test(k) ) return ui.notifications.error("Group keys may not contain spaces or periods");
- delete v["key"];
- obj[k] = v;
- return obj;
- }, {});
-
- // Remove groups which are no longer used
- for ( let k of Object.keys(this.object.data.data.groups) ) {
- if ( !groups.hasOwnProperty(k) ) groups[`-=${k}`] = null;
- }
-
- // Re-combine formData
- formData = Object.entries(formData).filter(e => !e[0].startsWith("data.groups")).reduce((obj, e) => {
- obj[e[0]] = e[1];
- return obj;
- }, {_id: this.object._id, "data.groups": groups});
-
- return formData;
- }
}
diff --git a/module/actor.js b/module/actor.js
index 1f6eb5a..3b667aa 100644
--- a/module/actor.js
+++ b/module/actor.js
@@ -1,3 +1,5 @@
+import { EntitySheetHelper } from "./helper.js";
+
/**
* Extend the base Actor entity by defining a custom roll data structure which is ideal for the Simple system.
* @extends {Actor}
@@ -18,15 +20,18 @@ export class SimpleActor extends Actor {
const data = super.getRollData();
const shorthand = game.settings.get("worldbuilding", "macroShorthand");
const formulaAttributes = [];
+ const itemAttributes = [];
// Handle formula attributes when the short syntax is disabled.
this._applyShorthand(data, formulaAttributes, shorthand);
// Map all items data using their slugified names
- this._applyItems(data, shorthand);
+ this._applyItems(data, itemAttributes, shorthand);
- // Evaluate formula attributes after all other attributes have been handled,
- // including items.
+ // Evaluate formula replacements on items.
+ this._applyItemsFormulaReplacements(data, itemAttributes, shorthand);
+
+ // Evaluate formula attributes after all other attributes have been handled, including items.
this._applyFormulaReplacements(data, formulaAttributes, shorthand);
// Remove the attributes if necessary.
@@ -61,9 +66,9 @@ export class SimpleActor extends Actor {
// Grouped attributes.
else {
data[k] = {};
- for ( let [attrKey, attrValue] of Object.entries(v) ) {
- data[k][attrKey] = attrValue.value;
- if ( attrValue.dtype == "Formula" ) formulaAttributes.push(`${k}.${attrKey}`);
+ for ( let [gk, gv] of Object.entries(v) ) {
+ data[k][gk] = gv.value;
+ if ( gv.dtype == "Formula" ) formulaAttributes.push(`${k}.${gk}`);
}
}
}
@@ -77,37 +82,42 @@ export class SimpleActor extends Actor {
* @param {Object} data The actor's data object.
* @param {Boolean} shorthand Whether or not the shorthand syntax is used.
*/
- _applyItems(data, shorthand) {
+ _applyItems(data, itemAttributes, shorthand) {
// Map all items data using their slugified names
data.items = this.data.items.reduce((obj, i) => {
let key = i.name.slugify({strict: true});
let itemData = duplicate(i.data);
- const itemAttributes = [];
// Add items to shorthand and note which ones are formula attributes.
for ( let [k, v] of Object.entries(itemData.attributes) ) {
- if ( v.dtype == "Formula" ) itemAttributes.push(k);
+ // When building the attribute list, prepend the item name for later use.
+ if ( v.dtype == "Formula" ) itemAttributes.push(`${key}..${k}`);
// Add shortened version of the attributes.
if ( !!shorthand ) {
if ( !(k in itemData) ) {
- itemData[k] = v.value;
+ // Non-grouped item attributes.
+ if ( v.dtype ) {
+ itemData[k] = v.value;
+ }
+ // Grouped item attributes.
+ else {
+ if ( !itemData[k] ) itemData[k] = {};
+ for ( let [gk, gv] of Object.entries(v) ) {
+ itemData[k][gk] = gv.value;
+ if ( gv.dtype == "Formula" ) itemAttributes.push(`${key}..${k}.${gk}`);
+ }
+ }
}
}
- }
-
- // Evaluate formula attributes after all other attributes have been handled.
- for ( let k of itemAttributes ) {
- if ( itemData.attributes[k].value ) {
- itemData.attributes[k].value = this._replaceData(itemData.attributes[k].value, itemData);
- itemData.attributes[k].value = this._replaceData(itemData.attributes[k].value, data, {missing: "0"});
- // TODO: Replace with:
- // itemData.attributes[k].value = Roll.replaceFormulaData(itemData.attributes[k].value, itemData);
- // itemData.attributes[k].value = Roll.replaceFormulaData(itemData.attributes[k].value, data, {missing: "0"});
- }
-
- // Duplicate values to shorthand.
- if ( !!shorthand ) {
- itemData[k] = itemData.attributes[k].value;
+ // Handle non-shorthand version of grouped attributes.
+ else {
+ if ( !v.dtype ) {
+ if ( !itemData[k] ) itemData[k] = {};
+ for ( let [gk, gv] of Object.entries(v) ) {
+ itemData[k][gk] = gv.value;
+ if ( gv.dtype == "Formula" ) itemAttributes.push(`${key}..${k}.${gk}`);
+ }
+ }
}
}
@@ -121,6 +131,50 @@ export class SimpleActor extends Actor {
}, {});
}
+ _applyItemsFormulaReplacements(data, itemAttributes, shorthand) {
+ for ( let k of itemAttributes ) {
+ // Get the item name and separate the key.
+ let item = null;
+ let itemKey = k.split('..');
+ item = itemKey[0];
+ k = itemKey[1];
+
+ // Handle group keys.
+ let gk = null;
+ if ( k.includes('.') ) {
+ let attrKey = k.split('.');
+ k = attrKey[0];
+ gk = attrKey[1];
+ }
+
+ let formula = '';
+ if ( !!shorthand ) {
+ // Handle grouped attributes first.
+ if ( data.items[item][k][gk] ) {
+ formula = data.items[item][k][gk];
+ data.items[item][k][gk] = EntitySheetHelper.replaceData(formula.replace('@item.', `@items.${item}.`), data, {missing: "0"});
+ }
+ // Handle non-grouped attributes.
+ else if ( data.items[item][k] ) {
+ formula = data.items[item][k];
+ data.items[item][k] = EntitySheetHelper.replaceData(formula.replace('@item.', `@items.${item}.`), data, {missing: "0"});
+ }
+ }
+ else {
+ // Handle grouped attributes first.
+ if ( data.items[item]['attributes'][k][gk] ) {
+ formula = data.items[item]['attributes'][k][gk]['value'];
+ data.items[item]['attributes'][k][gk]['value'] = EntitySheetHelper.replaceData(formula.replace('@item.', `@items.${item}.attributes.`), data, {missing: "0"});
+ }
+ // Handle non-grouped attributes.
+ else if ( data.items[item]['attributes'][k]['value'] ) {
+ formula = data.items[item]['attributes'][k]['value'];
+ data.items[item]['attributes'][k]['value'] = EntitySheetHelper.replaceData(formula.replace('@item.', `@items.${item}.attributes.`), data, {missing: "0"});
+ }
+ }
+ }
+ }
+
/**
* Apply replacements for derived formula attributes.
* @param {Object} data The actor's data object.
@@ -140,22 +194,22 @@ export class SimpleActor extends Actor {
attr = attrKey[1];
}
// Non-grouped attributes.
- if ( data.attributes[k].value ) {
- data.attributes[k].value = this._replaceData(data.attributes[k].value, data, {missing: "0"});
+ if ( data.attributes[k]?.value ) {
+ data.attributes[k].value = EntitySheetHelper.replaceData(data.attributes[k].value, data, {missing: "0"});
// TODO: Replace with:
// data.attributes[k].value = Roll.replaceFormulaData(data.attributes[k].value, data, {missing: "0"});
}
// Grouped attributes.
else {
if ( attr ) {
- data.attributes[k][attr].value = this._replaceData(data.attributes[k][attr].value, data, {missing: "0"});
+ data.attributes[k][attr].value = EntitySheetHelper.replaceData(data.attributes[k][attr].value, data, {missing: "0"});
}
}
// Duplicate values to shorthand.
if ( !!shorthand ) {
// Non-grouped attributes.
- if ( data.attributes[k].value ) {
+ if ( data.attributes[k]?.value ) {
data[k] = data.attributes[k].value;
}
// Grouped attributes.
@@ -171,31 +225,4 @@ export class SimpleActor extends Actor {
}
}
}
-
- /**
- * Replace referenced data attributes in the roll formula with the syntax `@attr` with the corresponding key from
- * the provided `data` object. This is a temporary helper function that will be replaced with Roll.replaceFormulaData()
- * in Foundry 0.7.1.
- *
- * @param {String} formula The original formula within which to replace.
- * @param {Object} data Data object to use for value replacements.
- * @param {Object} missing Value to use as missing replacements, such as {missing: "0"}.
- * @return {String} The formula with attributes replaced with values.
- */
- _replaceData(formula, data, {missing=null}={}) {
- // Exit early if the formula is invalid.
- if ( typeof formula != "string" ) {
- return 0;
- }
-
- // Replace attributes with their numeric equivalents.
- let dataRgx = new RegExp(/@([a-z.0-9_\-]+)/gi);
- let rollFormula = formula.replace(dataRgx, (match, term) => {
- // Replace matches with the value, or the missing value.
- let value = getProperty(data, term);
- return value ? String(value).trim() : (missing != null ? missing : `@${term}`);
- });
-
- return rollFormula;
- }
}
diff --git a/module/helper.js b/module/helper.js
new file mode 100644
index 0000000..d06504e
--- /dev/null
+++ b/module/helper.js
@@ -0,0 +1,545 @@
+import { ATTRIBUTE_TYPES } from "./constants.js";
+
+export class EntitySheetHelper {
+
+ static getAttributeData(data) {
+ data.dtypes = ATTRIBUTE_TYPES;
+
+ // Determine attribute type.
+ for ( let attr of Object.values(data.data.attributes) ) {
+ if ( attr.dtype ) {
+ attr.isCheckbox = attr.dtype === "Boolean";
+ attr.isResource = attr.dtype === "Resource";
+ attr.isFormula = attr.dtype === "Formula";
+ }
+ }
+
+ // Initialize ungrouped attributes for later.
+ data.data.ungroupedAttributes = {};
+
+ // Build an array of sorted group keys.
+ const groups = data.data.groups || {};
+ let groupKeys = Object.keys(groups).sort((a, b) => {
+ let aSort = groups[a].label ?? a;
+ let bSort = groups[b].label ?? b;
+ return aSort.localeCompare(bSort);
+ });
+
+ // Iterate over the sorted groups to add their attributes.
+ for ( let key of groupKeys ) {
+ let group = data.data.attributes[key] || {};
+
+ // Initialize the attributes container for this group.
+ if ( !data.data.groups[key]['attributes'] ) data.data.groups[key]['attributes'] = {};
+
+ // Sort the attributes within the group, and then iterate over them.
+ Object.keys(group).sort((a, b) => a.localeCompare(b)).forEach(attr => {
+ // Avoid errors if this is an invalid group.
+ if ( typeof group[attr] != "object" || !group[attr]) return;
+ // For each attribute, determine whether it's a checkbox or resource, and then add it to the group's attributes list.
+ group[attr]['isCheckbox'] = group[attr]['dtype'] === 'Boolean';
+ group[attr]['isResource'] = group[attr]['dtype'] === 'Resource';
+ group[attr]['isFormula'] = group[attr]['dtype'] === 'Formula';
+ data.data.groups[key]['attributes'][attr] = group[attr];
+ });
+ }
+
+ // Sort the remaining attributes attributes.
+ Object.keys(data.data.attributes).filter(a => !groupKeys.includes(a)).sort((a, b) => a.localeCompare(b)).forEach(key => {
+ data.data.ungroupedAttributes[key] = data.data.attributes[key];
+ });
+
+ // Modify attributes on items.
+ if ( data.items ) {
+ data.items.forEach(item => {
+ // Iterate over attributes.
+ for ( let [k, v] of Object.entries(item.data.attributes) ) {
+ // Grouped attributes.
+ if ( !v.dtype ) {
+ for ( let [gk, gv] of Object.entries(v) ) {
+ if ( gv.dtype ) {
+ // Add label fallback.
+ if ( !gv.label ) gv.label = gk;
+ // Add formula bool.
+ if ( gv.dtype == "Formula" ) {
+ gv.isFormula = true;
+ }
+ else {
+ gv.isFormula = false;
+ }
+ }
+ }
+ }
+ // Ungrouped attributes.
+ else {
+ // Add label fallback.
+ if ( !v.label ) v.label = k;
+ // Add formula bool.
+ if ( v.dtype == "Formula" ) {
+ v.isFormula = true;
+ }
+ else {
+ v.isFormula = false;
+ }
+ }
+ }
+ });
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ static onSubmit(event) {
+ // Closing the form/sheet will also trigger a submit, so only evaluate if this is an event.
+ if ( event.currentTarget ) {
+ // Exit early if this isn't a named attribute.
+ if ( event.currentTarget.tagName.toLowerCase() == 'input' && !event.currentTarget.hasAttribute('name')) {
+ return false;
+ }
+
+ let attr = false;
+ // If this is the attribute key, we need to make a note of it so that we can restore focus when its recreated.
+ const el = event.currentTarget;
+ if ( el.classList.contains("attribute-key") ) {
+ let val = el.value;
+ let oldVal = el.closest(".attribute").dataset.attribute;
+ let attrError = false;
+ // Prevent attributes that already exist as groups.
+ let groups = document.querySelectorAll('.group-key');
+ for ( let i = 0; i < groups.length; i++ ) {
+ if (groups[i].value == val) {
+ ui.notifications.error(game.i18n.localize("SIMPLE.NotifyAttrDuplicate") + ` (${val})`);
+ el.value = oldVal;
+ attrError = true;
+ break;
+ }
+ }
+ // Handle value and name replacement otherwise.
+ if ( !attrError ) {
+ oldVal = oldVal.includes('.') ? oldVal.split('.')[1] : oldVal;
+ attr = $(el).attr('name').replace(oldVal, val);
+ }
+ }
+
+ // Return the attribute key if set, or true to confirm the submission should be triggered.
+ return attr ? attr : true;
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Listen for click events on an attribute control to modify the composition of attributes in the sheet
+ * @param {MouseEvent} event The originating left click event
+ * @private
+ */
+ static async onClickAttributeControl(event) {
+ event.preventDefault();
+ const a = event.currentTarget;
+ const action = a.dataset.action;
+
+ // Perform create and delete actions.
+ switch ( action ) {
+ case "create":
+ EntitySheetHelper.createAttribute(event, this);
+ break;
+ case "delete":
+ EntitySheetHelper.deleteAttribute(event, this);
+ break;
+ }
+ }
+
+ /**
+ * Listen for click events and modify attribute groups.
+ * @param {MouseEvent} event The originating left click event
+ */
+ static async onClickAttributeGroupControl(event) {
+ event.preventDefault();
+ const a = event.currentTarget;
+ const action = a.dataset.action;
+
+ switch ( action ) {
+ case "create-group":
+ EntitySheetHelper.createAttributeGroup(event, this);
+ break;
+ case "delete-group":
+ EntitySheetHelper.deleteAttributeGroup(event, this);
+ break;
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Listen for the roll button on attributes.
+ * @param {MouseEvent} event The originating left click event
+ */
+ static onAttributeRoll(event) {
+ event.preventDefault();
+ const button = event.currentTarget;
+ const label = button.closest(".attribute").querySelector(".attribute-label")?.value;
+ const chatLabel = label ?? button.parentElement.querySelector(".attribute-key").value;
+ const shorthand = game.settings.get("worldbuilding", "macroShorthand");
+ const rollData = this.actor.getRollData();
+ let formula = button.closest(".attribute").querySelector(".attribute-value")?.value;
+
+ // If there's a formula, attempt to roll it.
+ if ( formula ) {
+ // Get the machine safe version of the item name.
+ let replacement = null;
+ if ( formula.includes('@item.') && this.item ) {
+ let itemName = this.item.name.slugify({strict: true});
+ replacement = !!shorthand ? `@items.${itemName}.` : `@items.${itemName}.attributes.`;
+ formula = formula.replace('@item.', replacement);
+ }
+ formula = EntitySheetHelper.replaceData(formula, rollData, {missing: null});
+ // Replace `@item` shorthand with the item name and make the roll.
+ let r = new Roll(formula, rollData);
+ r.roll().toMessage({
+ user: game.user._id,
+ speaker: ChatMessage.getSpeaker({ actor: this.actor }),
+ flavor: `${chatLabel}`
+ });
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Return HTML for a new attribute to be applied to the form for submission.
+ *
+ * @param {Object} items Keyed object where each item has a "type" and "value" property.
+ * @param {string} index Numeric index or key of the new attribute.
+ * @param {string|boolean} group String key of the group, or false.
+ *
+ * @returns {string} Html string.
+ */
+ static getAttributeHtml(items, index, group = false) {
+ // Initialize the HTML.
+ let result = '
';
+ // Iterate over the supplied keys and build their inputs (including whether or not they need a group key).
+ for (let [key, item] of Object.entries(items)) {
+ result = result + ``;
+ }
+ // Close the HTML and return.
+ return result + '
';
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Validate whether or not a group name can be used.
+ * @param {string} groupName Groupname to validate
+ * @returns {boolean}
+ */
+ static validateGroup(groupName, entity) {
+ let groups = Object.keys(entity.object.data.data.groups);
+ let attributes = Object.keys(entity.object.data.data.attributes).filter(a => !groups.includes(a));
+
+ // Check for duplicate group keys.
+ if ( groups.includes(groupName) ) {
+ ui.notifications.error(game.i18n.localize("SIMPLE.NotifyGroupDuplicate") + ` (${groupName})`);
+ return false;
+ }
+
+ // Check for group keys that match attribute keys.
+ if ( attributes.includes(groupName) ) {
+ ui.notifications.error(game.i18n.localize("SIMPLE.NotifyGroupAttrDuplicate") + ` (${groupName})`);
+ return false;
+ }
+
+ // Check for whitespace or periods.
+ if ( groupName.match(/[\s|\.]/i) ) {
+ ui.notifications.error(game.i18n.localize("SIMPLE.NotifyGroupAlphanumeric"));
+ return false;
+ }
+
+ return true;
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Create new attributes.
+ * @param {MouseEvent} event The originating left click event
+ * @param {Object} app The form application object.
+ * @private
+ */
+ static async createAttribute(event, app) {
+ const a = event.currentTarget;
+ const group = a.dataset.group;
+ let dtype = a.dataset.dtype;
+ const attrs = app.object.data.data.attributes;
+ const groups = app.object.data.data.groups;
+ const form = app.form;
+
+ // Determine the new attribute key for ungrouped attributes.
+ let objKeys = Object.keys(attrs).filter(k => !Object.keys(groups).includes(k));
+ let nk = Object.keys(attrs).length + 1;
+ let newValue = `attr${nk}`;
+ let newKey = document.createElement("div");
+ while ( objKeys.includes(newValue) ) {
+ ++nk;
+ newValue = `attr${nk}`;
+ };
+
+ // Build options for construction HTML inputs.
+ let htmlItems = {
+ key: {
+ type: "text",
+ value: newValue
+ }
+ };
+
+ // Grouped attributes.
+ if ( group ) {
+ objKeys = attrs[group] ? Object.keys(attrs[group]) : [];
+ nk = objKeys.length + 1;
+ newValue = `attr${nk}`;
+ while ( objKeys.includes(newValue) ) {
+ ++nk;
+ newValue = `attr${nk}`;
+ }
+
+ // Update the HTML options used to build the new input.
+ htmlItems.key.value = newValue;
+ htmlItems.group = {
+ type: "hidden",
+ value: group
+ };
+ htmlItems.dtype = {
+ type: "hidden",
+ value: dtype
+ };
+ }
+ // Ungrouped attributes.
+ else {
+ // Choose a default dtype based on the last attribute, fall back to "String".
+ if (!dtype) {
+ let lastAttr = document.querySelector('.attributes > .attributes-group .attribute:last-child .attribute-dtype')?.value;
+ dtype = lastAttr ? lastAttr : "String";
+ htmlItems.dtype = {
+ type: "hidden",
+ value: dtype
+ };
+ }
+ }
+
+ // Build the form elements used to create the new grouped attribute.
+ newKey.innerHTML = EntitySheetHelper.getAttributeHtml(htmlItems, nk, group);
+
+ // Append the form element and submit the form.
+ newKey = newKey.children[0];
+ form.appendChild(newKey);
+ await app._onSubmit(event);
+ }
+
+ /**
+ * Delete an attribute.
+ * @param {MouseEvent} event The originating left click event
+ * @param {Object} app The form application object.
+ * @private
+ */
+ static async deleteAttribute(event, app) {
+ const a = event.currentTarget;
+ const li = a.closest(".attribute");
+ if ( li ) {
+ li.parentElement.removeChild(li);
+ await app._onSubmit(event);
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Create new attribute groups.
+ * @param {MouseEvent} event The originating left click event
+ * @param {Object} app The form application object.
+ * @private
+ */
+ static async createAttributeGroup(event, app) {
+ const a = event.currentTarget;
+ const form = app.form;
+ let newValue = $(a).siblings('.group-prefix').val();
+ // Verify the new group key is valid, and use it to create the group.
+ if ( newValue.length > 0 && EntitySheetHelper.validateGroup(newValue, app) ) {
+ let newKey = document.createElement("div");
+ newKey.innerHTML = ``;
+ // Append the form element and submit the form.
+ newKey = newKey.children[0];
+ form.appendChild(newKey);
+ await app._onSubmit(event);
+ }
+ }
+
+ /**
+ * Delete an attribute group.
+ * @param {MouseEvent} event The originating left click event
+ * @param {Object} app The form application object.
+ * @private
+ */
+ static async deleteAttributeGroup(event, app) {
+ const a = event.currentTarget;
+ let groupHeader = a.closest(".group-header");
+ let groupContainer = groupHeader.closest(".group");
+ let group = $(groupHeader).find('.group-key');
+ // Create a dialog to confirm group deletion.
+ new Dialog({
+ title: game.i18n.localize("SIMPLE.DeleteGroup"),
+ content: `${game.i18n.localize("SIMPLE.DeleteGroupContent")} ${group.val()}`,
+ buttons: {
+ confirm: {
+ icon: '',
+ label: game.i18n.localize("Yes"),
+ callback: async () => {
+ groupContainer.parentElement.removeChild(groupContainer);
+ await app._onSubmit(event);
+ }
+ },
+ cancel: {
+ icon: '',
+ label: game.i18n.localize("No"),
+ }
+ }
+ }).render(true);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Update attributes when updating an actor object.
+ *
+ * @param {Object} formData Form data object to modify keys and values for.
+ * @returns {Object} updated formData object.
+ */
+ static updateAttributes(formData, entity) {
+ let groupKeys = [];
+
+ // Handle the free-form attributes list
+ const formAttrs = expandObject(formData).data.attributes || {};
+ const attributes = Object.values(formAttrs).reduce((obj, v) => {
+ let attrs = [];
+ let group = null;
+ // Handle attribute keys for grouped attributes.
+ if ( !v["key"] ) {
+ attrs = Object.keys(v);
+ attrs.forEach(attrKey => {
+ group = v[attrKey]['group'];
+ groupKeys.push(group);
+ let attr = v[attrKey];
+ let k = v[attrKey]["key"] ? v[attrKey]["key"].trim() : attrKey.trim();
+ if ( /[\s\.]/.test(k) ) return ui.notifications.error("Attribute keys may not contain spaces or periods");
+ delete attr["key"];
+ // Add the new attribute if it's grouped, but we need to build the nested structure first.
+ if ( !obj[group] ) {
+ obj[group] = {};
+ }
+ obj[group][k] = attr;
+ });
+ }
+ // Handle attribute keys for ungrouped attributes.
+ else {
+ let k = v["key"].trim();
+ if ( /[\s\.]/.test(k) ) return ui.notifications.error("Attribute keys may not contain spaces or periods");
+ delete v["key"];
+ // Add the new attribute only if it's ungrouped.
+ if ( !group ) {
+ obj[k] = v;
+ }
+ }
+ return obj;
+ }, {});
+
+ // Remove attributes which are no longer used
+ for ( let k of Object.keys(entity.object.data.data.attributes) ) {
+ if ( !attributes.hasOwnProperty(k) ) attributes[`-=${k}`] = null;
+ }
+
+ // Remove grouped attributes which are no longer used.
+ for ( let group of groupKeys) {
+ if ( entity.object.data.data.attributes[group] ) {
+ for ( let k of Object.keys(entity.object.data.data.attributes[group]) ) {
+ if ( !attributes[group].hasOwnProperty(k) ) attributes[group][`-=${k}`] = null;
+ }
+ }
+ }
+
+ // Re-combine formData
+ formData = Object.entries(formData).filter(e => !e[0].startsWith("data.attributes")).reduce((obj, e) => {
+ obj[e[0]] = e[1];
+ return obj;
+ }, {_id: entity.object._id, "data.attributes": attributes});
+
+ return formData;
+ }
+
+ /**
+ * Update attribute groups when updating an actor object.
+ *
+ * @param {Object} formData Form data object to modify keys and values for.
+ * @returns {Object} updated formData object.
+ */
+ static updateGroups(formData, entity) {
+ // Handle the free-form groups list
+ const formGroups = expandObject(formData).data.groups || {};
+ const groups = Object.values(formGroups).reduce((obj, v) => {
+ // If there are duplicate groups, collapse them.
+ if ( Array.isArray(v["key"]) ) {
+ v["key"] = v["key"][0];
+ }
+ // Trim and clean up.
+ let k = v["key"].trim();
+ if ( /[\s\.]/.test(k) ) return ui.notifications.error("Group keys may not contain spaces or periods");
+ delete v["key"];
+ obj[k] = v;
+ return obj;
+ }, {});
+
+ // Remove groups which are no longer used
+ for ( let k of Object.keys(entity.object.data.data.groups) ) {
+ if ( !groups.hasOwnProperty(k) ) groups[`-=${k}`] = null;
+ }
+
+ // Re-combine formData
+ formData = Object.entries(formData).filter(e => !e[0].startsWith("data.groups")).reduce((obj, e) => {
+ obj[e[0]] = e[1];
+ return obj;
+ }, {_id: entity.object._id, "data.groups": groups});
+
+ return formData;
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Replace referenced data attributes in the roll formula with the syntax `@attr` with the corresponding key from
+ * the provided `data` object. This is a temporary helper function that will be replaced with Roll.replaceFormulaData()
+ * in Foundry 0.7.1.
+ *
+ * @param {String} formula The original formula within which to replace.
+ * @param {Object} data Data object to use for value replacements.
+ * @param {Object} missing Value to use as missing replacements, such as {missing: "0"}.
+ * @return {String} The formula with attributes replaced with values.
+ */
+ static replaceData(formula, data, {missing=null,depth=1}={}) {
+ // Exit early if the formula is invalid.
+ if ( typeof formula != "string" || depth < 1) {
+ return 0;
+ }
+
+ // Replace attributes with their numeric equivalents.
+ let dataRgx = new RegExp(/@([a-z.0-9_\-]+)/gi);
+ let rollFormula = formula.replace(dataRgx, (match, term) => {
+ // Replace matches with the value, or the missing value.
+ let value = getProperty(data, term);
+ value = value ? String(value).trim() : (missing != null ? missing : `@${term}`);
+ // If there's still an attribute in the returned string, nest it in parentheses so that it's evaluated first in the roll.
+ value = value && value.includes('@') ? `(${value})` : value;
+ return value;
+ });
+
+ return rollFormula;
+ }
+
+}
\ No newline at end of file
diff --git a/module/item-sheet.js b/module/item-sheet.js
index fed2f96..248cd2c 100644
--- a/module/item-sheet.js
+++ b/module/item-sheet.js
@@ -1,4 +1,4 @@
-import { ATTRIBUTE_TYPES } from "./constants.js";
+import { EntitySheetHelper } from "./helper.js";
/**
* Extend the basic ItemSheet with some very simple modifications
@@ -7,14 +7,15 @@ import { ATTRIBUTE_TYPES } from "./constants.js";
export class SimpleItemSheet extends ItemSheet {
/** @override */
- static get defaultOptions() {
- return mergeObject(super.defaultOptions, {
- classes: ["worldbuilding", "sheet", "item"],
- template: "systems/worldbuilding/templates/item-sheet.html",
- width: 520,
- height: 480,
- tabs: [{navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description"}]
- });
+ static get defaultOptions() {
+ return mergeObject(super.defaultOptions, {
+ classes: ["worldbuilding", "sheet", "item"],
+ template: "systems/worldbuilding/templates/item-sheet.html",
+ width: 520,
+ height: 480,
+ tabs: [{navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description"}],
+ scrollY: [".attributes"],
+ });
}
/* -------------------------------------------- */
@@ -22,16 +23,34 @@ export class SimpleItemSheet extends ItemSheet {
/** @override */
getData() {
const data = super.getData();
- data.dtypes = ATTRIBUTE_TYPES;
- for ( let attr of Object.values(data.data.attributes) ) {
- attr.isCheckbox = attr.dtype === "Boolean";
- attr.isResource = attr.dtype === "Resource";
- }
+
+ // Handle attribute groups.
+ EntitySheetHelper.getAttributeData(data);
+
return data;
}
/* -------------------------------------------- */
+ /** @override */
+ async _onSubmit(event, {updateData=null, preventClose=false, preventRender=false}={}) {
+ let attr = EntitySheetHelper.onSubmit(event);
+
+ // Submit the form if attr is true or an attr key.
+ if ( attr ) {
+ await super._onSubmit(event, {updateData: updateData, preventClose: preventClose, preventRender: preventRender});
+
+ // If attr is a key and not just true, set a very short timeout and retrigger focus after the original element is deleted and the new one is inserted.
+ if ( attr !== true) {
+ setTimeout(() => {
+ $(`input[name="${attr}"]`).parents('.attribute').find('.attribute-value').focus();
+ }, 10);
+ }
+ }
+ }
+
+ /* -------------------------------------------- */
+
/** @override */
setPosition(options={}) {
const position = super.setPosition(options);
@@ -47,49 +66,26 @@ export class SimpleItemSheet extends ItemSheet {
activateListeners(html) {
super.activateListeners(html);
+ // Handle rollable attributes.
+ html.find(".attributes").on("click", "a.attribute-roll", EntitySheetHelper.onAttributeRoll.bind(this));
+
// Everything below here is only needed if the sheet is editable
if (!this.options.editable) return;
+ // Add draggable for macros.
+ html.find(".attributes a.attribute-roll").each((i, a) => {
+ a.setAttribute("draggable", true);
+ a.addEventListener("dragstart", ev => {
+ let dragData = ev.currentTarget.dataset;
+ ev.dataTransfer.setData('text/plain', JSON.stringify(dragData));
+ }, false);
+ });
+
// Add or Remove Attribute
- html.find(".attributes").on("click", ".attribute-control", this._onClickAttributeControl.bind(this));
- }
+ html.find(".attributes").on("click", ".attribute-control", EntitySheetHelper.onClickAttributeControl.bind(this));
- /* -------------------------------------------- */
-
- /**
- * Listen for click events on an attribute control to modify the composition of attributes in the sheet
- * @param {MouseEvent} event The originating left click event
- * @private
- */
- async _onClickAttributeControl(event) {
- event.preventDefault();
- const a = event.currentTarget;
- const action = a.dataset.action;
- const attrs = this.object.data.data.attributes;
- const form = this.form;
-
- // Add new attribute
- if ( action === "create" ) {
- const objKeys = Object.keys(attrs);
- let nk = Object.keys(attrs).length + 1;
- let newValue = `attr${nk}`;
- let newKey = document.createElement("div");
- while ( objKeys.includes(newValue) ) {
- ++nk;
- newValue = `attr${nk}`;
- }
- newKey.innerHTML = ``;
- newKey = newKey.children[0];
- form.appendChild(newKey);
- await this._onSubmit(event);
- }
-
- // Remove existing attribute
- else if ( action === "delete" ) {
- const li = a.closest(".attribute");
- li.parentElement.removeChild(li);
- await this._onSubmit(event);
- }
+ // Add attribute groups.
+ html.find(".groups").on("click", ".group-control", EntitySheetHelper.onClickAttributeGroupControl.bind(this));
}
/* -------------------------------------------- */
@@ -97,28 +93,11 @@ export class SimpleItemSheet extends ItemSheet {
/** @override */
_updateObject(event, formData) {
- // Handle the free-form attributes list
- const formAttrs = expandObject(formData).data.attributes || {};
- const attributes = Object.values(formAttrs).reduce((obj, v) => {
- let k = v["key"].trim();
- if ( /[\s\.]/.test(k) ) return ui.notifications.error("Attribute keys may not contain spaces or periods");
- delete v["key"];
- obj[k] = v;
- return obj;
- }, {});
+ // Handle attribute and group updates.
+ formData = EntitySheetHelper.updateAttributes(formData, this);
+ formData = EntitySheetHelper.updateGroups(formData, this);
- // Remove attributes which are no longer used
- for ( let k of Object.keys(this.object.data.data.attributes) ) {
- if ( !attributes.hasOwnProperty(k) ) attributes[`-=${k}`] = null;
- }
-
- // Re-combine formData
- formData = Object.entries(formData).filter(e => !e[0].startsWith("data.attributes")).reduce((obj, e) => {
- obj[e[0]] = e[1];
- return obj;
- }, {_id: this.object._id, "data.attributes": attributes});
-
- // Update the Item
+ // Update the Actor with the new form values.
return this.object.update(formData);
}
}
diff --git a/templates/actor-sheet.html b/templates/actor-sheet.html
index f953b5d..bac456b 100644
--- a/templates/actor-sheet.html
+++ b/templates/actor-sheet.html
@@ -43,17 +43,34 @@
{{!-- Iterate through all attributes on the item and output buttons for any that are formula. --}}
{{#each item.data.attributes as |itemAttr key|}}
- {{#if (eq itemAttr.dtype "Formula")}}
- {{!-- Use the items.name.key format for shorthand. --}}
- {{#if ../../shorthand}}
-
- {{!-- Use the items.name.attributes.key.value format otherwise. --}}
+ {{#if itemAttr.dtype}}
+ {{#if itemAttr.isFormula}}
+ {{!-- Use the items.name.key format for shorthand. --}}
+ {{#if ../../shorthand}}
+
+ {{!-- Use the items.name.attributes.key.value format otherwise. --}}
+ {{else}}
+
+ {{/if}}
+ {{/if}}
{{else}}
-
- {{/if}}
+ {{#each itemAttr as |itemGroupedAttr groupedKey|}}
+ {{#if itemGroupedAttr.isFormula}}
+ {{!-- Use the items.name.key format for shorthand. --}}
+ {{#if ../../../shorthand}}
+
+ {{!-- Use the items.name.attributes.key.value format otherwise. --}}
+ {{else}}
+
+ {{/if}}
+ {{/if}}
+ {{/each}}
{{/if}}
{{/each}}
\ No newline at end of file
diff --git a/templates/parts/sheet-attributes.html b/templates/parts/sheet-attributes.html
index 764cfdf..983e4e3 100644
--- a/templates/parts/sheet-attributes.html
+++ b/templates/parts/sheet-attributes.html
@@ -3,7 +3,7 @@
{{#each attributes as |attr key|}}