From db3cad909e93b5799fbcd131f7c4cc8dabf16cd6 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Thu, 24 Jul 2025 20:23:42 -0600 Subject: [PATCH 1/3] Add a systemFilePath handlebars helper for better file path referencing --- module/handlebarsHelpers/_index.mjs | 5 +++++ module/hooks/init.mjs | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 module/handlebarsHelpers/_index.mjs diff --git a/module/handlebarsHelpers/_index.mjs b/module/handlebarsHelpers/_index.mjs new file mode 100644 index 0000000..d0923e8 --- /dev/null +++ b/module/handlebarsHelpers/_index.mjs @@ -0,0 +1,5 @@ +import { filePath } from "../consts.mjs"; + +export default { + systemFilePath: filePath, +}; diff --git a/module/hooks/init.mjs b/module/hooks/init.mjs index 6f1c54e..e57a6c6 100644 --- a/module/hooks/init.mjs +++ b/module/hooks/init.mjs @@ -13,6 +13,7 @@ import { registerWorldSettings } from "../settings/world.mjs"; // Utils import { __ID__ } from "../consts.mjs"; +import helpers from "../handlebarsHelpers/_index.mjs"; import { Logger } from "../utils/Logger.mjs"; import { registerCustomComponents } from "../apps/elements/_index.mjs"; @@ -33,6 +34,8 @@ Hooks.on(`init`, () => { }, ); - registerCustomComponents(); registerWorldSettings(); + + registerCustomComponents(); + Handlebars.registerHelper(helpers); }); From 02b49687cfec732561136be3d388390fb63c5c2f Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Thu, 24 Jul 2025 20:33:07 -0600 Subject: [PATCH 2/3] Cleanup logger statements --- module/apps/AttributeManager.mjs | 2 -- module/documents/Actor.mjs | 3 --- module/documents/Token.mjs | 4 +--- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/module/apps/AttributeManager.mjs b/module/apps/AttributeManager.mjs index d3874ba..2029e0d 100644 --- a/module/apps/AttributeManager.mjs +++ b/module/apps/AttributeManager.mjs @@ -1,6 +1,5 @@ import { __ID__, filePath } from "../consts.mjs"; import { attributeSorter } from "../utils/attributeSort.mjs"; -import { Logger } from "../utils/Logger.mjs"; import { toID } from "../utils/toID.mjs"; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -133,7 +132,6 @@ export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2) }; }; - Logger.debug(`Updating ${binding} value to ${value}`); setProperty(this.#attributes, binding, value); await this.render({ parts: [ `attributes` ]}); }; diff --git a/module/documents/Actor.mjs b/module/documents/Actor.mjs index de78395..ff72c77 100644 --- a/module/documents/Actor.mjs +++ b/module/documents/Actor.mjs @@ -1,10 +1,7 @@ -import { Logger } from "../utils/Logger.mjs"; - const { Actor } = foundry.documents; export class TAFActor extends Actor { async modifyTokenAttribute(attribute, value, isDelta = false, isBar = true) { - Logger.table({ attribute, value, isDelta, isBar }); const attr = foundry.utils.getProperty(this.system, attribute); const current = isBar ? attr.value : attr; const update = isDelta ? current + value : value; diff --git a/module/documents/Token.mjs b/module/documents/Token.mjs index b6baf2d..068c737 100644 --- a/module/documents/Token.mjs +++ b/module/documents/Token.mjs @@ -1,5 +1,3 @@ -import { Logger } from "../utils/Logger.mjs"; - const { TokenDocument } = foundry.documents; const { getProperty, getType, hasProperty, isSubclass } = foundry.utils; @@ -61,7 +59,7 @@ export class TAFTokenDocument extends TokenDocument { */ getBarAttribute(barName, {alternative} = {}) { const attribute = alternative || this[barName]?.attribute; - Logger.log(barName, attribute); + if (!attribute || !this.actor) { return null; }; From 44a88cc7b52cb4a8fc34dacfb80042f47dbc982d Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Thu, 24 Jul 2025 20:34:17 -0600 Subject: [PATCH 3/3] Add DialogManager to a global API and create the Ask dialog for the user --- module/api.mjs | 30 ++++++++ module/apps/Ask.mjs | 115 +++++++++++++++++++++++++++++++ module/main.mjs | 1 + module/utils/DialogManager.mjs | 106 ++++++++++++++++++++++++++++ styles/Apps/Ask.css | 40 +++++++++++ styles/main.css | 3 +- templates/Ask/controls.hbs | 13 ++++ templates/Ask/inputs.hbs | 12 ++++ templates/Ask/inputs/details.hbs | 3 + templates/Ask/inputs/input.hbs | 12 ++++ 10 files changed, 334 insertions(+), 1 deletion(-) create mode 100644 module/api.mjs create mode 100644 module/apps/Ask.mjs create mode 100644 module/utils/DialogManager.mjs create mode 100644 styles/Apps/Ask.css create mode 100644 templates/Ask/controls.hbs create mode 100644 templates/Ask/inputs.hbs create mode 100644 templates/Ask/inputs/details.hbs create mode 100644 templates/Ask/inputs/input.hbs diff --git a/module/api.mjs b/module/api.mjs new file mode 100644 index 0000000..b139ef9 --- /dev/null +++ b/module/api.mjs @@ -0,0 +1,30 @@ +// Apps +import { Ask } from "./apps/Ask.mjs"; +import { AttributeManager } from "./apps/AttributeManager.mjs"; +import { PlayerSheet } from "./apps/PlayerSheet.mjs"; + +// Utils +import { attributeSorter } from "./utils/attributeSort.mjs"; +import { DialogManager } from "./utils/DialogManager.mjs"; +import { toID } from "./utils/toID.mjs"; + +const { deepFreeze } = foundry.utils; + +Object.defineProperty( + globalThis, + `taf`, + { + value: deepFreeze({ + DialogManager, + Apps: { + Ask, + AttributeManager, + PlayerSheet, + }, + utils: { + attributeSorter, + toID, + }, + }), + }, +); diff --git a/module/apps/Ask.mjs b/module/apps/Ask.mjs new file mode 100644 index 0000000..bcd5f4a --- /dev/null +++ b/module/apps/Ask.mjs @@ -0,0 +1,115 @@ +import { __ID__, filePath } from "../consts.mjs"; + +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; + +export class Ask extends HandlebarsApplicationMixin(ApplicationV2) { + static DEFAULT_OPTIONS = { + tag: `dialog`, + classes: [ + __ID__, + `dialog`, // accesses some Foundry-provided styling + `Ask`, + ], + position: { + width: 330, + }, + window: { + title: `Questions`, + resizable: true, + minimizable: true, + contentTag: `form`, + }, + form: { + closeOnSubmit: true, + submitOnChange: false, + handler: this.#submit, + }, + actions: { + cancel: this.#cancel, + }, + }; + + static PARTS = { + inputs: { + template: filePath(`templates/Ask/inputs.hbs`), + templates: [ + filePath(`templates/Ask/inputs/input.hbs`), + filePath(`templates/Ask/inputs/details.hbs`), + ], + }, + controls: { + template: filePath(`templates/Ask/controls.hbs`), + }, + }; + + _inputs = []; + alwaysUseAnswerObject = false; + + /** @type {string | undefined} */ + _description = undefined; + + /** @type {Function | undefined} */ + _userOnConfirm; + + /** @type {Function | undefined} */ + _userOnCancel; + + /** @type {Function | undefined} */ + _userOnClose; + + constructor({ + inputs = [], + description = undefined, + onConfirm, + onCancel, + onClose, + alwaysUseAnswerObject, + ...options + } = {}) { + super(options); + this.alwaysUseAnswerObject = alwaysUseAnswerObject; + this._inputs = inputs; + this._description = description; + this._userOnCancel = onCancel; + this._userOnConfirm = onConfirm; + this._userOnClose = onClose; + }; + + // #region Lifecycle + async _onFirstRender() { + super._onFirstRender(); + this.element.show(); + }; + + async _prepareContext() { + return { + inputs: this._inputs, + description: this._description, + }; + }; + + async _onClose() { + super._onClose(); + this._userOnClose?.(); + }; + // #endregion Lifecycle + + // #region Actions + /** @this {AskDialog} */ + static async #submit(_event, _element, formData) { + const answers = formData.object; + const keys = Object.keys(answers); + if (keys.length === 1 && !this.alwaysUseAnswerObject) { + this._userOnConfirm?.(answers[keys[0]]); + return; + }; + this._userOnConfirm?.(answers); + }; + + /** @this {AskDialog} */ + static async #cancel() { + this._userOnCancel?.(); + this.close(); + }; + // #endregion Actions +}; diff --git a/module/main.mjs b/module/main.mjs index 8f3b5cc..abde62f 100644 --- a/module/main.mjs +++ b/module/main.mjs @@ -1 +1,2 @@ +import "./api.mjs"; import "./hooks/init.mjs"; diff --git a/module/utils/DialogManager.mjs b/module/utils/DialogManager.mjs new file mode 100644 index 0000000..772f02b --- /dev/null +++ b/module/utils/DialogManager.mjs @@ -0,0 +1,106 @@ +import { Ask } from "../apps/Ask.mjs"; + +export class DialogManager { + /** @type {Map} */ + static #promises = new Map(); + static #dialogs = new Map(); + + static async close(id) { + this.#dialogs.get(id)?.close(); + this.#dialogs.delete(id); + this.#promises.delete(id); + }; + + /** + * Asks the user to provide a simple piece of information, this is primarily + * intended to be used within macros so that it can have better info gathering + * as needed. This returns an object of input keys/labels to the value the user + * input for that label, if there is only one input, this will return the value + * without an object wrapper, allowing for easier access. + * + * @param {AskConfig} data + * @param {AskOptions} opts + * @returns {AskResult} + */ + static async ask( + data, + { + onlyOneWaiting = true, + alwaysUseAnswerObject = true, + } = {}, + ) { + if (!data.id) { + return { + state: `errored`, + error: `An ID must be provided`, + }; + }; + if (!data.inputs.length) { + return { + state: `errored`, + error: `At least one input must be provided`, + }; + }; + const id = data.id; + + // Don't do multi-thread waiting + if (this.#dialogs.has(id)) { + const app = this.#dialogs.get(id); + app.bringToFront(); + if (onlyOneWaiting) { + return { state: `fronted` }; + } else { + return this.#promises.get(id); + }; + }; + + let autofocusClaimed = false; + for (const i of data.inputs) { + i.id ??= foundry.utils.randomID(16); + i.key ??= i.label; + + switch (i.type) { + case `input`: { + i.inputType ??= `text`; + } + } + + // Only ever allow one input to claim autofocus + i.autofocus &&= !autofocusClaimed; + autofocusClaimed ||= i.autofocus; + + // Set the value's attribute name if it isn't specified explicitly + if (!i.valueAttribute) { + switch (i.inputType) { + case `checkbox`: + i.valueAttribute = `checked`; + break; + default: + i.valueAttribute = `value`; + }; + }; + }; + + const promise = new Promise((resolve) => { + const app = new Ask({ + ...data, + alwaysUseAnswerObject, + onClose: () => { + this.#dialogs.delete(id); + this.#promises.delete(id); + resolve({ state: `prompted` }); + }, + onConfirm: (answers) => resolve({ state: `prompted`, answers }), + }); + app.render({ force: true }); + this.#dialogs.set(id, app); + }); + + this.#promises.set(id, promise); + return promise; + }; + + static get size() { + return this.#dialogs.size; + }; +}; diff --git a/styles/Apps/Ask.css b/styles/Apps/Ask.css new file mode 100644 index 0000000..e58bcaa --- /dev/null +++ b/styles/Apps/Ask.css @@ -0,0 +1,40 @@ +.taf.Ask { + min-width: 330px; + + .prompt { + display: grid; + grid-template-columns: 1fr 2fr; + gap: 1rem; + align-items: center; + } + + .window-content { + gap: 1rem; + overflow: auto; + } + + .dialog-content { + display: flex; + flex-direction: column; + gap: 8px; + } + + .control-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + + button { + flex-grow: 1; + } + } + + label { + color: var(--color-form-label); + font-weight: bold; + } + + p { + margin: 0; + } +} diff --git a/styles/main.css b/styles/main.css index a07aa4f..bf6264b 100644 --- a/styles/main.css +++ b/styles/main.css @@ -12,5 +12,6 @@ /* Apps */ @import url("./Apps/common.css") layer(apps); -@import url("./Apps/PlayerSheet.css") layer(apps); +@import url("./Apps/Ask.css") layer(apps); @import url("./Apps/AttributeManager.css") layer(apps); +@import url("./Apps/PlayerSheet.css") layer(apps); diff --git a/templates/Ask/controls.hbs b/templates/Ask/controls.hbs new file mode 100644 index 0000000..a619549 --- /dev/null +++ b/templates/Ask/controls.hbs @@ -0,0 +1,13 @@ +
+ + +
diff --git a/templates/Ask/inputs.hbs b/templates/Ask/inputs.hbs new file mode 100644 index 0000000..f5bdac4 --- /dev/null +++ b/templates/Ask/inputs.hbs @@ -0,0 +1,12 @@ +
+ {{#if description}} +

+ {{ description }} +

+ {{/if}} + {{#each inputs as | i |}} +
+ {{> (concat (systemFilePath "templates/Ask/inputs/" ) i.type ".hbs") i}} +
+ {{/each}} +
diff --git a/templates/Ask/inputs/details.hbs b/templates/Ask/inputs/details.hbs new file mode 100644 index 0000000..fec6878 --- /dev/null +++ b/templates/Ask/inputs/details.hbs @@ -0,0 +1,3 @@ +

+ {{{ details }}} +

diff --git a/templates/Ask/inputs/input.hbs b/templates/Ask/inputs/input.hbs new file mode 100644 index 0000000..196b12d --- /dev/null +++ b/templates/Ask/inputs/input.hbs @@ -0,0 +1,12 @@ + +