Add a helper application to manage attributes on characters
This commit is contained in:
parent
91ccd93814
commit
6b81d8d83b
10 changed files with 298 additions and 12 deletions
171
module/apps/AttributeManager.mjs
Normal file
171
module/apps/AttributeManager.mjs
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
import { __ID__, filePath } from "../consts.mjs";
|
||||||
|
import { Logger } from "../utils/Logger.mjs";
|
||||||
|
import { toID } from "../utils/toID.mjs";
|
||||||
|
|
||||||
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
const { deepClone, diffObject, randomID, setProperty } = foundry.utils;
|
||||||
|
|
||||||
|
export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
|
|
||||||
|
// #region Options
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
tag: `form`,
|
||||||
|
classes: [
|
||||||
|
__ID__,
|
||||||
|
`AttributeManager`,
|
||||||
|
],
|
||||||
|
position: {
|
||||||
|
width: 400,
|
||||||
|
height: 350,
|
||||||
|
},
|
||||||
|
window: {
|
||||||
|
resizable: true,
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
submitOnChange: false,
|
||||||
|
closeOnSubmit: true,
|
||||||
|
handler: this.#onSubmit,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
addNew: this.#addNew,
|
||||||
|
removeAttribute: this.#remove,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
static PARTS = {
|
||||||
|
attributes: {
|
||||||
|
template: filePath(`templates/AttributeManager/attribute-list.hbs`),
|
||||||
|
},
|
||||||
|
controls: {
|
||||||
|
template: filePath(`templates/AttributeManager/controls.hbs`),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// #endregion Options
|
||||||
|
|
||||||
|
// #region Instance Data
|
||||||
|
/** @type {string | null} */
|
||||||
|
#doc = null;
|
||||||
|
|
||||||
|
#attributes;
|
||||||
|
|
||||||
|
constructor({ document , ...options } = {}) {
|
||||||
|
super(options);
|
||||||
|
this.#doc = document;
|
||||||
|
this.#attributes = deepClone(document.system.attr);
|
||||||
|
};
|
||||||
|
|
||||||
|
get title() {
|
||||||
|
return `Attributes: ${this.#doc.name}`;
|
||||||
|
};
|
||||||
|
// #endregion Instance Data
|
||||||
|
|
||||||
|
// #region Lifecycle
|
||||||
|
async _onRender(context, options) {
|
||||||
|
await super._onRender(context, options);
|
||||||
|
|
||||||
|
const elements = this.element
|
||||||
|
.querySelectorAll(`[data-bind]`);
|
||||||
|
for (const input of elements) {
|
||||||
|
input.addEventListener(`change`, this.#bindListener.bind(this));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
// #endregion Lifecycle
|
||||||
|
|
||||||
|
// #region Data Prep
|
||||||
|
async _preparePartContext(partId) {
|
||||||
|
const ctx = {};
|
||||||
|
|
||||||
|
ctx.actor = this.#doc;
|
||||||
|
|
||||||
|
switch (partId) {
|
||||||
|
case `attributes`: {
|
||||||
|
await this._prepareAttributeContext(ctx);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
|
|
||||||
|
async _prepareAttributeContext(ctx) {
|
||||||
|
const attrs = [];
|
||||||
|
for (const [id, data] of Object.entries(this.#attributes)) {
|
||||||
|
if (data == null) { continue };
|
||||||
|
attrs.push({
|
||||||
|
id,
|
||||||
|
name: data.name,
|
||||||
|
isRange: data.isRange,
|
||||||
|
isNew: data.isNew ?? false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
ctx.attrs = attrs;
|
||||||
|
};
|
||||||
|
// #endregion Data Prep
|
||||||
|
|
||||||
|
// #region Actions
|
||||||
|
/**
|
||||||
|
* @param {Event} event
|
||||||
|
*/
|
||||||
|
async #bindListener(event) {
|
||||||
|
const target = event.target;
|
||||||
|
const data = target.dataset;
|
||||||
|
const binding = data.bind;
|
||||||
|
|
||||||
|
let value = target.value;
|
||||||
|
switch (target.type) {
|
||||||
|
case `checkbox`: {
|
||||||
|
value = target.checked;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
Logger.debug(`Updating ${binding} value to ${value}`);
|
||||||
|
setProperty(this.#attributes, binding, value);
|
||||||
|
await this.render();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @this {AttributeManager} */
|
||||||
|
static async #addNew() {
|
||||||
|
const id = randomID();
|
||||||
|
this.#attributes[id] = {
|
||||||
|
name: ``,
|
||||||
|
isRange: false,
|
||||||
|
isNew: true,
|
||||||
|
};
|
||||||
|
await this.render({ parts: [ `attributes` ]});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @this {AttributeManager} */
|
||||||
|
static async #remove($e, element) {
|
||||||
|
const attribute = element.closest(`[data-attribute]`)?.dataset.attribute;
|
||||||
|
if (!attribute) { return };
|
||||||
|
delete this.#attributes[attribute];
|
||||||
|
this.#attributes[`-=${attribute}`] = null;
|
||||||
|
await this.render({ parts: [ `attributes` ] });
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @this {AttributeManager} */
|
||||||
|
static async #onSubmit() {
|
||||||
|
const entries = Object.entries(this.#attributes)
|
||||||
|
.map(([id, attr]) => {
|
||||||
|
if (attr == null) {
|
||||||
|
return [ id, attr ];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (attr.isNew) {
|
||||||
|
delete attr.isNew;
|
||||||
|
return [ toID(attr.name), attr ];
|
||||||
|
};
|
||||||
|
|
||||||
|
return [ id, attr ];
|
||||||
|
});
|
||||||
|
const data = Object.fromEntries(entries);
|
||||||
|
|
||||||
|
const diff = diffObject(
|
||||||
|
this.#doc.system.attr,
|
||||||
|
data,
|
||||||
|
{ inner: false, deletionKeys: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.#doc.update({ "system.attr": diff });
|
||||||
|
};
|
||||||
|
// #endregion Actions
|
||||||
|
};
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { __ID__, filePath } from "../consts.mjs";
|
import { __ID__, filePath } from "../consts.mjs";
|
||||||
|
import { AttributeManager } from "./AttributeManager.mjs";
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||||
const { ActorSheetV2 } = foundry.applications.sheets;
|
const { ActorSheetV2 } = foundry.applications.sheets;
|
||||||
|
|
@ -12,17 +13,29 @@ export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||||
`PlayerSheet`,
|
`PlayerSheet`,
|
||||||
],
|
],
|
||||||
position: {
|
position: {
|
||||||
width: 400,
|
width: 575,
|
||||||
height: 500,
|
height: 740,
|
||||||
},
|
},
|
||||||
window: {
|
window: {
|
||||||
resizable: true,
|
resizable: true,
|
||||||
|
controls: [
|
||||||
|
{
|
||||||
|
icon: `fa-solid fa-at`,
|
||||||
|
label: `Manage Attributes`,
|
||||||
|
action: `manageAttributes`,
|
||||||
|
visible: () => {
|
||||||
|
return game.user.isGM;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
submitOnChange: true,
|
submitOnChange: true,
|
||||||
closeOnSubmit: false,
|
closeOnSubmit: false,
|
||||||
},
|
},
|
||||||
actions: {},
|
actions: {
|
||||||
|
manageAttributes: this.#manageAttributes,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
static PARTS = {
|
static PARTS = {
|
||||||
|
|
@ -82,5 +95,10 @@ export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||||
// #endregion Data Prep
|
// #endregion Data Prep
|
||||||
|
|
||||||
// #region Actions
|
// #region Actions
|
||||||
|
/** @this {PlayerSheet} */
|
||||||
|
static async #manageAttributes() {
|
||||||
|
const app = new AttributeManager({ document: this.actor });
|
||||||
|
await app.render({ force: true });
|
||||||
|
};
|
||||||
// #endregion Actions
|
// #endregion Actions
|
||||||
};
|
};
|
||||||
|
|
|
||||||
13
module/utils/toID.mjs
Normal file
13
module/utils/toID.mjs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
/**
|
||||||
|
* A helper method that converts an arbitrary string into a format that can be
|
||||||
|
* used as an object key easily.
|
||||||
|
*
|
||||||
|
* @param {string} text The text to convert
|
||||||
|
* @returns The converted ID
|
||||||
|
*/
|
||||||
|
export function toID(text) {
|
||||||
|
return text
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s/g, `_`)
|
||||||
|
.replace(/\W/g, ``);
|
||||||
|
};
|
||||||
33
styles/Apps/AttributeManager.css
Normal file
33
styles/Apps/AttributeManager.css
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
.taf.AttributeManager {
|
||||||
|
.attributes {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attribute {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid rebeccapurple;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,10 @@
|
||||||
.taf.PlayerSheet {
|
.taf.PlayerSheet {
|
||||||
> .window-content {
|
|
||||||
padding: 0.5rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sheet-header, fieldset, .content {
|
.sheet-header, fieldset, .content {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid rebeccapurple;
|
border: 1px solid rebeccapurple;
|
||||||
}
|
}
|
||||||
|
|
||||||
.window-content > header {
|
.sheet-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
8
styles/Apps/common.css
Normal file
8
styles/Apps/common.css
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
.taf {
|
||||||
|
> .window-content {
|
||||||
|
padding: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,4 +9,6 @@
|
||||||
@import url("./elements/prose-mirror.css") layer(elements);
|
@import url("./elements/prose-mirror.css") layer(elements);
|
||||||
|
|
||||||
/* Apps */
|
/* Apps */
|
||||||
|
@import url("./Apps/common.css") layer(apps);
|
||||||
@import url("./Apps/PlayerSheet.css") layer(apps);
|
@import url("./Apps/PlayerSheet.css") layer(apps);
|
||||||
|
@import url("./Apps/AttributeManager.css") layer(apps);
|
||||||
|
|
|
||||||
35
templates/AttributeManager/attribute-list.hbs
Normal file
35
templates/AttributeManager/attribute-list.hbs
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<div class="attributes">
|
||||||
|
{{#each attrs as |attr|}}
|
||||||
|
<div
|
||||||
|
class="attribute"
|
||||||
|
data-attribute="{{ attr.id }}"
|
||||||
|
>
|
||||||
|
{{#if attr.isNew}}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
data-bind="{{ attr.id }}.name"
|
||||||
|
value="{{ attr.name }}"
|
||||||
|
placeholder="Attribute Name..."
|
||||||
|
>
|
||||||
|
{{else}}
|
||||||
|
<span>{{ attr.name }}</span>
|
||||||
|
{{/if}}
|
||||||
|
<label>
|
||||||
|
Has Maximum?
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
data-bind="{{ attr.id }}.isRange"
|
||||||
|
{{ checked attr.isRange }}
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-action="removeAttribute"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<p>No attributes yet</p>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
13
templates/AttributeManager/controls.hbs
Normal file
13
templates/AttributeManager/controls.hbs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<div class="controls">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-action="addNew"
|
||||||
|
>
|
||||||
|
Add New Attribute
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Save and Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{{#if hasAttributes}}
|
{{#if hasAttributes}}
|
||||||
<div class="attributes">
|
<div class="attributes">
|
||||||
{{#each attrs as | attr |}}
|
{{#each attrs as | attr |}}
|
||||||
<fieldset>
|
<fieldset data-attribute="{{ attr.id }}">
|
||||||
<legend>
|
<legend>
|
||||||
{{ attr.name }}
|
{{ attr.name }}
|
||||||
</legend>
|
</legend>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue