mirror of
https://github.com/RoY7x/worldbuilding.git
synced 2025-04-30 02:31:41 -04:00
Merge branch '10-item-attr-groups' into 'master'
10: Add item attribute groups See merge request foundrynet/worldbuilding!10
This commit is contained in:
commit
f69f9c80ea
8 changed files with 748 additions and 528 deletions
|
@ -7,7 +7,9 @@
|
||||||
"SIMPLE.NotifyInitFormulaUpdated": "Initiative formula was updated to:",
|
"SIMPLE.NotifyInitFormulaUpdated": "Initiative formula was updated to:",
|
||||||
"SIMPLE.NotifyInitFormulaInvalid": "Initiative formula was invalid:",
|
"SIMPLE.NotifyInitFormulaInvalid": "Initiative formula was invalid:",
|
||||||
"SIMPLE.NotifyGroupDuplicate": "Attribute group already exists.",
|
"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.NotifyGroupAlphanumeric": "Attribute group names may not contain spaces or periods.",
|
||||||
|
"SIMPLE.NotifyAttrDuplicate": "Attribute key already exists as a group.",
|
||||||
|
|
||||||
"SIMPLE.ResourceMin": "Min",
|
"SIMPLE.ResourceMin": "Min",
|
||||||
"SIMPLE.ResourceValue": "Value",
|
"SIMPLE.ResourceValue": "Value",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ATTRIBUTE_TYPES } from "./constants.js";
|
import { EntitySheetHelper } from "./helper.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extend the basic ActorSheet with some very simple modifications
|
* Extend the basic ActorSheet with some very simple modifications
|
||||||
|
@ -24,45 +24,9 @@ export class SimpleActorSheet extends ActorSheet {
|
||||||
/** @override */
|
/** @override */
|
||||||
getData() {
|
getData() {
|
||||||
const data = super.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.
|
// Handle attribute groups.
|
||||||
data.data.ungroupedAttributes = {};
|
EntitySheetHelper.getAttributeData(data);
|
||||||
|
|
||||||
// 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];
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add shorthand.
|
// Add shorthand.
|
||||||
data.shorthand = !!game.settings.get("worldbuilding", "macroShorthand");
|
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));
|
html.find(".items .rollable").on("click", this._onItemRoll.bind(this));
|
||||||
|
|
||||||
// Handle rollable attributes.
|
// 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
|
// Everything below here is only needed if the sheet is editable
|
||||||
if ( !this.options.editable ) return;
|
if ( !this.options.editable ) return;
|
||||||
|
@ -98,12 +62,6 @@ export class SimpleActorSheet extends ActorSheet {
|
||||||
li.slideUp(200, () => this.render(false));
|
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.
|
// Add draggable for macros.
|
||||||
html.find(".attributes a.attribute-roll").each((i, a) => {
|
html.find(".attributes a.attribute-roll").each((i, a) => {
|
||||||
a.setAttribute("draggable", true);
|
a.setAttribute("draggable", true);
|
||||||
|
@ -112,38 +70,30 @@ export class SimpleActorSheet extends ActorSheet {
|
||||||
ev.dataTransfer.setData('text/plain', JSON.stringify(dragData));
|
ev.dataTransfer.setData('text/plain', JSON.stringify(dragData));
|
||||||
}, false);
|
}, 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 */
|
/** @override */
|
||||||
async _onSubmit(event, {updateData=null, preventClose=false, preventRender=false}={}) {
|
async _onSubmit(event, {updateData=null, preventClose=false, preventRender=false}={}) {
|
||||||
// Exit early if this isn't a named attribute.
|
let attr = EntitySheetHelper.onSubmit(event);
|
||||||
if ( event.currentTarget ) {
|
|
||||||
if ( event.currentTarget.tagName.toLowerCase() == 'input' && !event.currentTarget.hasAttribute('name')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let self = $(event.currentTarget);
|
// Submit the form if attr is true or an attr key.
|
||||||
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.
|
|
||||||
if ( attr ) {
|
if ( attr ) {
|
||||||
setTimeout(() => {
|
await super._onSubmit(event, {updateData: updateData, preventClose: preventClose, preventRender: preventRender});
|
||||||
$(`input[name="${attr}"]`).parents('.attribute').find('.attribute-value').focus();
|
|
||||||
}, 10);
|
// 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 = `<input type="text" name="data.groups.${newValue}.key" value="${newValue}"/>`;
|
|
||||||
// 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")} <strong>${group.val()}</strong>`,
|
|
||||||
buttons: {
|
|
||||||
confirm: {
|
|
||||||
icon: '<i class="fas fa-trash"></i>',
|
|
||||||
label: game.i18n.localize("Yes"),
|
|
||||||
callback: async () => {
|
|
||||||
groupHeader.parentElement.removeChild(groupHeader);
|
|
||||||
await this._onSubmit(event);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cancel: {
|
|
||||||
icon: '<i class="fas fa-times"></i>',
|
|
||||||
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 = '<div>';
|
|
||||||
// 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 + `<input type="${item.type}" name="data.attributes${group ? '.' + group : '' }.attr${index}.${key}" value="${item.value}"/>`;
|
|
||||||
}
|
|
||||||
// Close the HTML and return.
|
|
||||||
return result + '</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 */
|
/** @override */
|
||||||
_updateObject(event, formData) {
|
_updateObject(event, formData) {
|
||||||
|
|
||||||
// Handle attribute and group updates.
|
// Handle attribute and group updates.
|
||||||
formData = this._updateAttributes(formData);
|
formData = EntitySheetHelper.updateAttributes(formData, this);
|
||||||
formData = this._updateGroups(formData);
|
formData = EntitySheetHelper.updateGroups(formData, this);
|
||||||
|
|
||||||
// Update the Actor with the new form values.
|
// Update the Actor with the new form values.
|
||||||
return this.object.update(formData);
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
139
module/actor.js
139
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.
|
* Extend the base Actor entity by defining a custom roll data structure which is ideal for the Simple system.
|
||||||
* @extends {Actor}
|
* @extends {Actor}
|
||||||
|
@ -18,15 +20,18 @@ export class SimpleActor extends Actor {
|
||||||
const data = super.getRollData();
|
const data = super.getRollData();
|
||||||
const shorthand = game.settings.get("worldbuilding", "macroShorthand");
|
const shorthand = game.settings.get("worldbuilding", "macroShorthand");
|
||||||
const formulaAttributes = [];
|
const formulaAttributes = [];
|
||||||
|
const itemAttributes = [];
|
||||||
|
|
||||||
// Handle formula attributes when the short syntax is disabled.
|
// Handle formula attributes when the short syntax is disabled.
|
||||||
this._applyShorthand(data, formulaAttributes, shorthand);
|
this._applyShorthand(data, formulaAttributes, shorthand);
|
||||||
|
|
||||||
// Map all items data using their slugified names
|
// 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,
|
// Evaluate formula replacements on items.
|
||||||
// including items.
|
this._applyItemsFormulaReplacements(data, itemAttributes, shorthand);
|
||||||
|
|
||||||
|
// Evaluate formula attributes after all other attributes have been handled, including items.
|
||||||
this._applyFormulaReplacements(data, formulaAttributes, shorthand);
|
this._applyFormulaReplacements(data, formulaAttributes, shorthand);
|
||||||
|
|
||||||
// Remove the attributes if necessary.
|
// Remove the attributes if necessary.
|
||||||
|
@ -61,9 +66,9 @@ export class SimpleActor extends Actor {
|
||||||
// Grouped attributes.
|
// Grouped attributes.
|
||||||
else {
|
else {
|
||||||
data[k] = {};
|
data[k] = {};
|
||||||
for ( let [attrKey, attrValue] of Object.entries(v) ) {
|
for ( let [gk, gv] of Object.entries(v) ) {
|
||||||
data[k][attrKey] = attrValue.value;
|
data[k][gk] = gv.value;
|
||||||
if ( attrValue.dtype == "Formula" ) formulaAttributes.push(`${k}.${attrKey}`);
|
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 {Object} data The actor's data object.
|
||||||
* @param {Boolean} shorthand Whether or not the shorthand syntax is used.
|
* @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
|
// Map all items data using their slugified names
|
||||||
data.items = this.data.items.reduce((obj, i) => {
|
data.items = this.data.items.reduce((obj, i) => {
|
||||||
let key = i.name.slugify({strict: true});
|
let key = i.name.slugify({strict: true});
|
||||||
let itemData = duplicate(i.data);
|
let itemData = duplicate(i.data);
|
||||||
const itemAttributes = [];
|
|
||||||
|
|
||||||
// Add items to shorthand and note which ones are formula attributes.
|
// Add items to shorthand and note which ones are formula attributes.
|
||||||
for ( let [k, v] of Object.entries(itemData.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.
|
// Add shortened version of the attributes.
|
||||||
if ( !!shorthand ) {
|
if ( !!shorthand ) {
|
||||||
if ( !(k in itemData) ) {
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
// Handle non-shorthand version of grouped attributes.
|
||||||
|
else {
|
||||||
// Evaluate formula attributes after all other attributes have been handled.
|
if ( !v.dtype ) {
|
||||||
for ( let k of itemAttributes ) {
|
if ( !itemData[k] ) itemData[k] = {};
|
||||||
if ( itemData.attributes[k].value ) {
|
for ( let [gk, gv] of Object.entries(v) ) {
|
||||||
itemData.attributes[k].value = this._replaceData(itemData.attributes[k].value, itemData);
|
itemData[k][gk] = gv.value;
|
||||||
itemData.attributes[k].value = this._replaceData(itemData.attributes[k].value, data, {missing: "0"});
|
if ( gv.dtype == "Formula" ) itemAttributes.push(`${key}..${k}.${gk}`);
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.
|
* Apply replacements for derived formula attributes.
|
||||||
* @param {Object} data The actor's data object.
|
* @param {Object} data The actor's data object.
|
||||||
|
@ -140,22 +194,22 @@ export class SimpleActor extends Actor {
|
||||||
attr = attrKey[1];
|
attr = attrKey[1];
|
||||||
}
|
}
|
||||||
// Non-grouped attributes.
|
// Non-grouped attributes.
|
||||||
if ( data.attributes[k].value ) {
|
if ( data.attributes[k]?.value ) {
|
||||||
data.attributes[k].value = this._replaceData(data.attributes[k].value, data, {missing: "0"});
|
data.attributes[k].value = EntitySheetHelper.replaceData(data.attributes[k].value, data, {missing: "0"});
|
||||||
// TODO: Replace with:
|
// TODO: Replace with:
|
||||||
// data.attributes[k].value = Roll.replaceFormulaData(data.attributes[k].value, data, {missing: "0"});
|
// data.attributes[k].value = Roll.replaceFormulaData(data.attributes[k].value, data, {missing: "0"});
|
||||||
}
|
}
|
||||||
// Grouped attributes.
|
// Grouped attributes.
|
||||||
else {
|
else {
|
||||||
if ( attr ) {
|
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.
|
// Duplicate values to shorthand.
|
||||||
if ( !!shorthand ) {
|
if ( !!shorthand ) {
|
||||||
// Non-grouped attributes.
|
// Non-grouped attributes.
|
||||||
if ( data.attributes[k].value ) {
|
if ( data.attributes[k]?.value ) {
|
||||||
data[k] = data.attributes[k].value;
|
data[k] = data.attributes[k].value;
|
||||||
}
|
}
|
||||||
// Grouped attributes.
|
// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
545
module/helper.js
Normal file
545
module/helper.js
Normal file
|
@ -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 = '<div>';
|
||||||
|
// 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 + `<input type="${item.type}" name="data.attributes${group ? '.' + group : '' }.attr${index}.${key}" value="${item.value}"/>`;
|
||||||
|
}
|
||||||
|
// Close the HTML and return.
|
||||||
|
return result + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = `<input type="text" name="data.groups.${newValue}.key" value="${newValue}"/>`;
|
||||||
|
// 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")} <strong>${group.val()}</strong>`,
|
||||||
|
buttons: {
|
||||||
|
confirm: {
|
||||||
|
icon: '<i class="fas fa-trash"></i>',
|
||||||
|
label: game.i18n.localize("Yes"),
|
||||||
|
callback: async () => {
|
||||||
|
groupContainer.parentElement.removeChild(groupContainer);
|
||||||
|
await app._onSubmit(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
icon: '<i class="fas fa-times"></i>',
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { ATTRIBUTE_TYPES } from "./constants.js";
|
import { EntitySheetHelper } from "./helper.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extend the basic ItemSheet with some very simple modifications
|
* Extend the basic ItemSheet with some very simple modifications
|
||||||
|
@ -7,14 +7,15 @@ import { ATTRIBUTE_TYPES } from "./constants.js";
|
||||||
export class SimpleItemSheet extends ItemSheet {
|
export class SimpleItemSheet extends ItemSheet {
|
||||||
|
|
||||||
/** @override */
|
/** @override */
|
||||||
static get defaultOptions() {
|
static get defaultOptions() {
|
||||||
return mergeObject(super.defaultOptions, {
|
return mergeObject(super.defaultOptions, {
|
||||||
classes: ["worldbuilding", "sheet", "item"],
|
classes: ["worldbuilding", "sheet", "item"],
|
||||||
template: "systems/worldbuilding/templates/item-sheet.html",
|
template: "systems/worldbuilding/templates/item-sheet.html",
|
||||||
width: 520,
|
width: 520,
|
||||||
height: 480,
|
height: 480,
|
||||||
tabs: [{navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description"}]
|
tabs: [{navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description"}],
|
||||||
});
|
scrollY: [".attributes"],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
@ -22,16 +23,34 @@ export class SimpleItemSheet extends ItemSheet {
|
||||||
/** @override */
|
/** @override */
|
||||||
getData() {
|
getData() {
|
||||||
const data = super.getData();
|
const data = super.getData();
|
||||||
data.dtypes = ATTRIBUTE_TYPES;
|
|
||||||
for ( let attr of Object.values(data.data.attributes) ) {
|
// Handle attribute groups.
|
||||||
attr.isCheckbox = attr.dtype === "Boolean";
|
EntitySheetHelper.getAttributeData(data);
|
||||||
attr.isResource = attr.dtype === "Resource";
|
|
||||||
}
|
|
||||||
return 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 */
|
/** @override */
|
||||||
setPosition(options={}) {
|
setPosition(options={}) {
|
||||||
const position = super.setPosition(options);
|
const position = super.setPosition(options);
|
||||||
|
@ -47,49 +66,26 @@ export class SimpleItemSheet extends ItemSheet {
|
||||||
activateListeners(html) {
|
activateListeners(html) {
|
||||||
super.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
|
// Everything below here is only needed if the sheet is editable
|
||||||
if (!this.options.editable) return;
|
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
|
// 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));
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
// Add attribute groups.
|
||||||
|
html.find(".groups").on("click", ".group-control", EntitySheetHelper.onClickAttributeGroupControl.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 = `<input type="text" name="data.attributes.attr${nk}.key" value="${newValue}"/>`;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
@ -97,28 +93,11 @@ export class SimpleItemSheet extends ItemSheet {
|
||||||
/** @override */
|
/** @override */
|
||||||
_updateObject(event, formData) {
|
_updateObject(event, formData) {
|
||||||
|
|
||||||
// Handle the free-form attributes list
|
// Handle attribute and group updates.
|
||||||
const formAttrs = expandObject(formData).data.attributes || {};
|
formData = EntitySheetHelper.updateAttributes(formData, this);
|
||||||
const attributes = Object.values(formAttrs).reduce((obj, v) => {
|
formData = EntitySheetHelper.updateGroups(formData, this);
|
||||||
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;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
// Remove attributes which are no longer used
|
// Update the Actor with the new form values.
|
||||||
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
|
|
||||||
return this.object.update(formData);
|
return this.object.update(formData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,17 +43,34 @@
|
||||||
{{!-- Iterate through all attributes on the item and output buttons for any that are formula. --}}
|
{{!-- Iterate through all attributes on the item and output buttons for any that are formula. --}}
|
||||||
<div class="item-buttons">
|
<div class="item-buttons">
|
||||||
{{#each item.data.attributes as |itemAttr key|}}
|
{{#each item.data.attributes as |itemAttr key|}}
|
||||||
{{#if (eq itemAttr.dtype "Formula")}}
|
{{#if itemAttr.dtype}}
|
||||||
{{!-- Use the items.name.key format for shorthand. --}}
|
{{#if itemAttr.isFormula}}
|
||||||
{{#if ../../shorthand}}
|
{{!-- Use the items.name.key format for shorthand. --}}
|
||||||
<button class="item-button rollable" data-roll="@items.{{slugify item.name}}.{{key}}" data-label="{{ itemAttr.label }}"
|
{{#if ../../shorthand}}
|
||||||
title="{{itemAttr.value}}">{{itemAttr.label}}</button>
|
<button class="item-button rollable" data-roll="@items.{{slugify item.name}}.{{key}}" data-label="{{ itemAttr.label }}"
|
||||||
{{!-- Use the items.name.attributes.key.value format otherwise. --}}
|
title="{{itemAttr.value}}">{{itemAttr.label}}</button>
|
||||||
|
{{!-- Use the items.name.attributes.key.value format otherwise. --}}
|
||||||
|
{{else}}
|
||||||
|
<button class="item-button rollable"
|
||||||
|
data-roll="@items.{{slugify item.name}}.attributes.{{key}}.value" data-label="{{ itemAttr.label }}"
|
||||||
|
title="{{itemAttr.value}}">{{itemAttr.label}}</button>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<button class="item-button rollable"
|
{{#each itemAttr as |itemGroupedAttr groupedKey|}}
|
||||||
data-roll="@items.{{slugify item.name}}.attributes.{{key}}.value" data-label="{{ itemAttr.label }}"
|
{{#if itemGroupedAttr.isFormula}}
|
||||||
title="{{itemAttr.value}}">{{itemAttr.label}}</button>
|
{{!-- Use the items.name.key format for shorthand. --}}
|
||||||
{{/if}}
|
{{#if ../../../shorthand}}
|
||||||
|
<button class="item-button rollable" data-roll="@items.{{slugify item.name}}.{{key}}.{{groupedKey}}" data-label="{{ itemGroupedAttr.label }}"
|
||||||
|
title="{{itemGroupedAttr.value}}">{{itemGroupedAttr.label}}</button>
|
||||||
|
{{!-- Use the items.name.attributes.key.value format otherwise. --}}
|
||||||
|
{{else}}
|
||||||
|
<button class="item-button rollable"
|
||||||
|
data-roll="@items.{{slugify item.name}}.attributes.{{key}}.{{groupedKey}}.value" data-label="{{ itemGroupedAttr.label }}"
|
||||||
|
title="{{itemGroupedAttr.value}}">{{itemGroupedAttr.label}}</button>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
{{/each}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -31,16 +31,25 @@
|
||||||
{{!-- Attributes Tab --}}
|
{{!-- Attributes Tab --}}
|
||||||
<div class="tab attributes" data-group="primary" data-tab="attributes">
|
<div class="tab attributes" data-group="primary" data-tab="attributes">
|
||||||
<header class="attributes-header flexrow">
|
<header class="attributes-header flexrow">
|
||||||
<span class="attribute-roll"></span>
|
<span class="attribute-key">{{localize "SIMPLE.AttributeKey"}}</span>
|
||||||
<span class="attribute-key">Attribute Key</span>
|
<span class="attribute-value">{{localize "SIMPLE.AttributeValue"}}</span>
|
||||||
<span class="attribute-value">Value</span>
|
<span class="attribute-label">{{localize "SIMPLE.AttributeLabel"}}</span>
|
||||||
<span class="attribute-label">Label</span>
|
<span class="attribute-dtype">{{localize "SIMPLE.AttributeDtype"}}</span>
|
||||||
<span class="attribute-dtype">Data Type</span>
|
<a class="attribute-control" data-action="create" data-group="{{group}}"><i class="fas fa-plus"></i></a>
|
||||||
<a class="attribute-control" data-action="create"><i class="fas fa-plus"></i></a>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{{!-- Render the attribute list partial. --}}
|
{{!-- Render the attribute list partial. --}}
|
||||||
{{> "systems/worldbuilding/templates/parts/sheet-attributes.html" attributes=data.attributes dtypes=dtypes}}
|
{{> "systems/worldbuilding/templates/parts/sheet-attributes.html" attributes=data.ungroupedAttributes dtypes=dtypes}}
|
||||||
|
|
||||||
|
{{!-- Render the grouped attributes partial and control. --}}
|
||||||
|
<div class="groups">
|
||||||
|
{{> "systems/worldbuilding/templates/parts/sheet-groups.html" attributes=data.groupedAttributes groups=data.groups dtypes=dtypes}}
|
||||||
|
|
||||||
|
<div class="group-controls flexrow">
|
||||||
|
<input class="group-prefix" type="text" val=""/>
|
||||||
|
<a class="button group-control" data-action="create-group"><i class="fas fa-plus"></i>Add Attribute Group</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</form>
|
</form>
|
|
@ -3,7 +3,7 @@
|
||||||
{{#each attributes as |attr key|}}
|
{{#each attributes as |attr key|}}
|
||||||
<li class="attribute flexrow" data-attribute="{{#if attr.group}}{{attr.group}}.{{/if}}{{key}}">
|
<li class="attribute flexrow" data-attribute="{{#if attr.group}}{{attr.group}}.{{/if}}{{key}}">
|
||||||
<div class="attribute-key-wrapper flexrow">
|
<div class="attribute-key-wrapper flexrow">
|
||||||
{{#if (eq attr.dtype "Formula")}}
|
{{#if attr.isFormula}}
|
||||||
<a class="attribute-roll" data-label="{{attr.label}}" data-roll="{{attr.value}}"><i class="fas fa-dice-d20"></i></a>
|
<a class="attribute-roll" data-label="{{attr.label}}" data-roll="{{attr.value}}"><i class="fas fa-dice-d20"></i></a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<input class="attribute-key" type="text" name="data.attributes.{{#if attr.group}}{{attr.group}}.{{/if}}{{key}}.key" value="{{key}}" placeholder="{{localize "SIMPLE.AttributeKey"}}"/>
|
<input class="attribute-key" type="text" name="data.attributes.{{#if attr.group}}{{attr.group}}.{{/if}}{{key}}.key" value="{{key}}" placeholder="{{localize "SIMPLE.AttributeKey"}}"/>
|
||||||
|
|
Loading…
Add table
Reference in a new issue