From 1b7b9f1e8e2e5683b0302854664aff55d78330c2 Mon Sep 17 00:00:00 2001 From: Alexander Minges Date: Sat, 12 Jul 2025 21:05:29 +0200 Subject: [PATCH] refactor: port DS4 dialogs to ApplicationV2 Convert DialogWithListeners and check-factory.js from deprecated Dialog V1 to DialogV2. Replace jQuery with native DOM methods and update button format. Fixes all remaining V1 Application framework deprecation warnings. --- src/apps/actor/base-sheet.js | 8 +- src/apps/dialog-with-listeners.js | 130 +++++++++++++++++++++++++++--- src/dice/check-factory.js | 85 ++++++++++--------- 3 files changed, 165 insertions(+), 58 deletions(-) diff --git a/src/apps/actor/base-sheet.js b/src/apps/actor/base-sheet.js index 27783b25..75f66255 100644 --- a/src/apps/actor/base-sheet.js +++ b/src/apps/actor/base-sheet.js @@ -311,8 +311,8 @@ export class DS4ActorSheet extends foundry.applications.api.DocumentSheetV2 { const item = this.document.items.get(itemId); if (!item) return; - const confirmed = await Dialog.confirm({ - title: game.i18n.localize("DS4.DialogDeleteItemTitle"), + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { title: game.i18n.localize("DS4.DialogDeleteItemTitle") }, content: game.i18n.format("DS4.DialogDeleteItemContent", { item: item.name }), defaultYes: false, }); @@ -414,8 +414,8 @@ export class DS4ActorSheet extends foundry.applications.api.DocumentSheetV2 { const effect = this.document.effects.get(effectId); if (!effect) return; - const confirmed = await Dialog.confirm({ - title: game.i18n.localize("DS4.DialogDeleteEffectTitle"), + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { title: game.i18n.localize("DS4.DialogDeleteEffectTitle") }, content: game.i18n.format("DS4.DialogDeleteEffectContent", { effect: effect.name }), defaultYes: false, }); diff --git a/src/apps/dialog-with-listeners.js b/src/apps/dialog-with-listeners.js index c9a3f2a2..5eba545d 100644 --- a/src/apps/dialog-with-listeners.js +++ b/src/apps/dialog-with-listeners.js @@ -3,19 +3,129 @@ // SPDX-License-Identifier: MIT /** - * @typedef {DialogOptions} DialogWithListenersOptions - * @property {(html: JQuery, app: DialogWithListeners) => void} [activateAdditionalListeners] An optional function to attach additional listeners to the dialog + * A simple extension to the DialogV2 class that allows attaching additional listeners. */ +export class DialogWithListeners extends foundry.applications.api.DialogV2 { + constructor(options = {}) { + super(options); + this.activateAdditionalListeners = options.activateAdditionalListeners; + } + + static DEFAULT_OPTIONS = { + ...super.DEFAULT_OPTIONS, + classes: ["dialog", "dialog-with-listeners"], + tag: "dialog", + window: { + resizable: true, + }, + }; -/** - * A simple extension to the {@link Dialog} class that allows attaching additional listeners. - */ -export class DialogWithListeners extends Dialog { /** @override */ - activateListeners(html) { - super.activateListeners(html); - if (this.options.activateAdditionalListeners !== undefined) { - this.options.activateAdditionalListeners(html, this); + async _onRender(context, options) { + await super._onRender(context, options); + + // Attach additional listeners if provided + if (this.activateAdditionalListeners && typeof this.activateAdditionalListeners === 'function') { + this.activateAdditionalListeners(this.element, this); } } + + /** + * Create a confirmation dialog using the V2 framework with additional listeners support + * @param {object} options - Dialog options + * @param {string} options.title - Dialog title (deprecated, use window.title) + * @param {object} options.window - Window configuration + * @param {string} options.window.title - Dialog title + * @param {string} options.content - Dialog content HTML + * @param {boolean} options.defaultYes - Whether "Yes" is the default button + * @param {Function} options.activateAdditionalListeners - Function to attach additional listeners + * @returns {Promise} True if confirmed, false if cancelled + */ + static async confirm(options = {}) { + const { title, window = {}, content, defaultYes = true, activateAdditionalListeners, ...rest } = options; + + // Handle backward compatibility with title parameter + if (title && !window.title) { + window.title = title; + } + + return new Promise((resolve) => { + const dialog = new DialogWithListeners({ + window: { + title: window.title || "Confirm", + ...window, + }, + content, + activateAdditionalListeners, + buttons: [ + { + action: "yes", + label: game.i18n.localize("Yes"), + icon: "fas fa-check", + default: defaultYes, + callback: () => resolve(true), + }, + { + action: "no", + label: game.i18n.localize("No"), + icon: "fas fa-times", + default: !defaultYes, + callback: () => resolve(false), + }, + ], + close: () => resolve(false), + ...rest, + }); + + dialog.render(true); + }); + } + + /** + * Create a prompt dialog using the V2 framework with additional listeners support + * @param {object} options - Dialog options + * @param {string} options.title - Dialog title (deprecated, use window.title) + * @param {object} options.window - Window configuration + * @param {string} options.window.title - Dialog title + * @param {string} options.content - Dialog content HTML + * @param {object} options.buttons - Button configuration + * @param {Function} options.activateAdditionalListeners - Function to attach additional listeners + * @returns {Promise} Promise that resolves with the result + */ + static async prompt(options = {}) { + const { title, window = {}, content, buttons = {}, activateAdditionalListeners, ...rest } = options; + + // Handle backward compatibility with title parameter + if (title && !window.title) { + window.title = title; + } + + return new Promise((resolve) => { + // Convert V1 button format to V2 format + const v2Buttons = Object.entries(buttons).map(([key, button]) => ({ + action: key, + label: button.label || key, + icon: button.icon || "", + default: button.default || false, + callback: (event) => { + const result = button.callback ? button.callback(event) : key; + resolve(result); + }, + })); + + const dialog = new DialogWithListeners({ + window: { + title: window.title || "Dialog", + ...window, + }, + content, + activateAdditionalListeners, + buttons: v2Buttons, + close: () => resolve(null), + ...rest, + }); + + dialog.render(true); + }); + } } diff --git a/src/dice/check-factory.js b/src/dice/check-factory.js index 7070e7e2..b76469e7 100644 --- a/src/dice/check-factory.js +++ b/src/dice/check-factory.js @@ -169,58 +169,55 @@ async function askForInteractiveRollData(checkTargetNumber, options = {}, { temp const renderedHtml = await foundry.applications.handlebars.renderTemplate(usedTemplate, templateData); const dialogPromise = new Promise((resolve) => { - new DialogWithListeners( - { + new DialogWithListeners({ + window: { title: usedTitle, - content: renderedHtml, - buttons: { - ok: { - icon: '', - label: getGame().i18n.localize("DS4.GenericOkButton"), - callback: (html) => { - if (!("jquery" in html)) { - throw new Error( - getGame().i18n.format("DS4.ErrorUnexpectedHtmlType", { - exType: "JQuery", - realType: "HTMLElement", - }), - ); - } else { - const innerForm = html[0]?.querySelector("form"); - if (!innerForm) { - throw new Error( - getGame().i18n.format("DS4.ErrorCouldNotFindHtmlElement", { - htmlElement: "form", - }), - ); - } - resolve(innerForm); - } - }, - }, - cancel: { - icon: '', - label: getGame().i18n.localize("DS4.GenericCancelButton"), + }, + content: renderedHtml, + buttons: [ + { + action: "ok", + icon: "fas fa-check", + label: getGame().i18n.localize("DS4.GenericOkButton"), + default: true, + callback: (event) => { + const dialog = event.target.closest("dialog"); + const innerForm = dialog?.querySelector("form"); + if (!innerForm) { + throw new Error( + getGame().i18n.format("DS4.ErrorCouldNotFindHtmlElement", { + htmlElement: "form", + }), + ); + } + resolve(innerForm); }, }, - default: "ok", - }, - { - activateAdditionalListeners: (html, app) => { - const checkModifierCustomFormGroup = html.find(`#check-modifier-custom-${id}`).parent(".form-group"); - html.find(`#check-modifier-${id}`).on("change", (event) => { - if (event.currentTarget.value === "custom" && checkModifierCustomFormGroup.hasClass("ds4-hidden")) { - checkModifierCustomFormGroup.removeClass("ds4-hidden"); + { + action: "cancel", + icon: "fas fa-times", + label: getGame().i18n.localize("DS4.GenericCancelButton"), + callback: () => resolve(null), + }, + ], + close: () => resolve(null), + activateAdditionalListeners: (html, app) => { + const checkModifierCustomFormGroup = html.querySelector(`#check-modifier-custom-${id}`)?.closest(".form-group"); + const checkModifierSelect = html.querySelector(`#check-modifier-${id}`); + + if (checkModifierSelect) { + checkModifierSelect.addEventListener("change", (event) => { + if (event.currentTarget.value === "custom" && checkModifierCustomFormGroup?.classList.contains("ds4-hidden")) { + checkModifierCustomFormGroup.classList.remove("ds4-hidden"); app.setPosition({ height: "auto" }); - } else if (!checkModifierCustomFormGroup.hasClass("ds4-hidden")) { - checkModifierCustomFormGroup.addClass("ds4-hidden"); + } else if (checkModifierCustomFormGroup && !checkModifierCustomFormGroup.classList.contains("ds4-hidden")) { + checkModifierCustomFormGroup.classList.add("ds4-hidden"); app.setPosition({ height: "auto" }); } }); - }, - id, + } }, - ).render(true); + }).render(true); }); const dialogForm = await dialogPromise; return parseDialogFormData(dialogForm);