feat!: complete ApplicationV2 migration with item sheet port

Convert DS4ItemSheet and all item sheet templates from ApplicationV1 to
ItemSheetV2. Add V2 action handlers, tab navigation, and form
processing. Update effect management to use DialogV2. Preserve all
functionality including tab state and override handling.
This commit is contained in:
Alexander Minges 2025-07-12 21:18:18 +02:00
parent f0c5fa07dd
commit 4821eba0a9
Signed by: Athemis
GPG key ID: 31FBDEF92DDB162B
13 changed files with 284 additions and 147 deletions

View file

@ -67,12 +67,12 @@ export class DS4ActorSheet extends foundry.applications.api.DocumentSheetV2 {
}
/** @override */
async _renderHTML(context, options) {
async _renderHTML(context) {
return await foundry.applications.handlebars.renderTemplate(this.template, context);
}
/** @override */
_replaceHTML(result, content, options) {
_replaceHTML(result, content) {
content.innerHTML = result;
}
@ -210,7 +210,7 @@ export class DS4ActorSheet extends foundry.applications.api.DocumentSheetV2 {
try {
const roll = new Roll(value);
return roll.evaluateSync().total;
} catch (error) {
} catch {
return value;
}
}

View file

@ -4,24 +4,42 @@
//
// SPDX-License-Identifier: MIT
import { DS4 } from "../config";
import { DS4ActiveEffect } from "../documents/active-effect";
import { enforce, getGame } from "../utils/utils";
import { disableOverriddenFields } from "./sheet-helpers";
/**
* The Sheet class for DS4 Items
*/
export class DS4ItemSheet extends foundry.appv1.sheets.ItemSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["sheet", "ds4-item-sheet"],
height: 400,
scrollY: [".ds4-sheet-body"],
tabs: [{ navSelector: ".ds4-sheet-tab-nav", contentSelector: ".ds4-sheet-body", initial: "description" }],
export class DS4ItemSheet extends foundry.applications.sheets.ItemSheetV2 {
static DEFAULT_OPTIONS = {
classes: ["sheet", "ds4-item-sheet"],
tag: "form",
form: {
submitOnChange: true,
closeOnSubmit: false,
},
position: {
width: 540,
});
height: 400,
},
window: {
resizable: true,
},
actions: {
controlEffect: DS4ItemSheet.prototype._onControlEffect,
createEffect: DS4ItemSheet.prototype._onCreateEffect,
editEffect: DS4ItemSheet.prototype._onEditEffect,
deleteEffect: DS4ItemSheet.prototype._onDeleteEffect,
changeTab: DS4ItemSheet.prototype._onChangeTab,
},
};
static TABS = {};
constructor(options = {}) {
super(options);
this.activeTab = "description"; // Default active tab
}
get title() {
return `${this.item.name} [${game.i18n.localize("DS4.ItemSheet")}]`;
}
/** @override */
@ -31,123 +49,242 @@ export class DS4ItemSheet extends foundry.appv1.sheets.ItemSheet {
}
/** @override */
async getData(options = {}) {
const superContext = await super.getData(options);
superContext.data.system.description = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
superContext.data.system.description,
{
async: true,
},
);
const context = {
...superContext,
config: DS4,
isOwned: this.item.isOwned,
actor: this.item.actor,
};
async _renderHTML(context) {
return await foundry.applications.handlebars.renderTemplate(this.template, context);
}
/** @override */
_replaceHTML(result, content) {
content.innerHTML = result;
}
/** @override */
async _prepareContext(options) {
const context = await super._prepareContext(options);
// Validate document exists
if (!this.item) {
throw new Error("Item not available for sheet rendering");
}
// Add document data
context.data = this.item;
context.system = this.item.system;
context.source = this.item.toObject();
context.cssClass = this.item.constructor.name.toLowerCase();
context.editable = this.isEditable;
context.owner = this.item.isOwner;
context.isOwned = this.item.isOwned;
context.actor = this.actor;
// Add configuration
context.config = CONFIG.DS4;
// Add enriched effects
context.enrichedEffects = [];
if (this.item.effects && this.item.effects.size > 0) {
for (const effect of this.item.effects) {
const enrichedEffect = effect.toObject();
enrichedEffect.sourceName = effect.sourceName;
context.enrichedEffects.push(enrichedEffect);
}
}
// Enrich description HTML content
if (context.data.system.description) {
context.data.system.description = await TextEditor.enrichHTML(
context.data.system.description,
{
async: true,
relativeTo: this.item,
}
);
}
return context;
}
/** @override */
_getSubmitData(updateData = {}) {
const data = super._getSubmitData(updateData);
/**
* Process form data for submission
* @param {Event} event - The form submission event
* @param {HTMLFormElement} form - The form element
* @param {FormDataExtended} formData - The form data
* @returns {object} The processed form data
*/
_processFormData(event, form, formData) {
const submitData = foundry.utils.expandObject(formData.object);
// Prevent submitting overridden values
const overrides = foundry.utils.flattenObject(this.item.overrides);
for (const k of Object.keys(overrides)) {
delete data[k];
foundry.utils.setProperty(submitData, k, undefined);
delete submitData[k];
}
return submitData;
}
/**
* Handle effect control actions
* @param {Event} event - The triggering event
* @param {HTMLElement} target - The target element
*/
async _onControlEffect(event, target) {
const action = target.dataset.action;
const effectId = target.closest("[data-effect-id]")?.dataset.effectId;
if (!effectId) return;
const effect = this.item.effects.get(effectId);
if (!effect) return;
switch (action) {
case "edit":
await this._onEditEffect(event, target);
break;
case "delete":
await this._onDeleteEffect(event, target);
break;
}
}
/**
* Handle creating a new effect
*/
async _onCreateEffect() {
const effectData = {
name: game.i18n.localize("DS4.EffectNew"),
icon: "icons/svg/aura.svg",
};
await this.item.createEmbeddedDocuments("ActiveEffect", [effectData]);
}
/**
* Handle editing an effect
* @param {Event} event - The triggering event
* @param {HTMLElement} target - The target element
*/
async _onEditEffect(event, target) {
const effectId = target.closest("[data-effect-id]")?.dataset.effectId;
if (!effectId) return;
const effect = this.item.effects.get(effectId);
if (!effect) {
throw new Error(game.i18n.format("DS4.ErrorItemDoesNotHaveEffect", {
id: effectId,
item: this.item.name
}));
}
await effect.sheet.render(true);
}
/**
* Handle deleting an effect
* @param {Event} event - The triggering event
* @param {HTMLElement} target - The target element
*/
async _onDeleteEffect(event, target) {
const effectId = target.closest("[data-effect-id]")?.dataset.effectId;
if (!effectId) return;
const effect = this.item.effects.get(effectId);
if (!effect) return;
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: { title: game.i18n.localize("DS4.UserInteractionDeleteEffectTitle") },
content: game.i18n.format("DS4.UserInteractionDeleteEffectContent", { effect: effect.name }),
defaultYes: false,
});
if (confirmed) {
await effect.delete();
}
}
/**
* Handle tab changes manually for custom tab behavior
* @param {Event} event - The triggering event
* @param {HTMLElement} target - The target element
*/
async _onChangeTab(event, target) {
event.preventDefault();
const tab = target.dataset.tab;
if (!tab) return;
// Store the active tab
this.activeTab = tab;
// Find tab navigation elements
const nav = target.closest(".ds4-sheet-tab-nav");
const sheet = this.element.querySelector(".ds4-sheet-body");
if (!nav || !sheet) return;
// Update navigation active state
nav.querySelectorAll(".ds4-sheet-tab-nav__item").forEach(item => {
item.classList.remove("active");
});
target.classList.add("active");
// Update tab content visibility
sheet.querySelectorAll(".ds4-sheet-tab").forEach(tabContent => {
tabContent.classList.remove("active");
});
const activeTab = sheet.querySelector(`.ds4-sheet-tab[data-tab="${tab}"]`);
if (activeTab) {
activeTab.classList.add("active");
}
return data;
}
/** @override */
setPosition(options = {}) {
const position = super.setPosition(options);
if (position) {
const sheetBody = this.element.find(".sheet-body");
const bodyHeight = position.height - 192;
sheetBody.css("height", bodyHeight);
async _onRender(context, options) {
await super._onRender(context, options);
// Initialize first tab as active
this._initializeTabs();
}
/**
* Initialize tab state - show first tab, hide others
*/
_initializeTabs() {
const nav = this.element.querySelector(".ds4-sheet-tab-nav");
const sheet = this.element.querySelector(".ds4-sheet-body");
if (!nav || !sheet) {
return;
}
return position;
}
// Get all tab navigation items and tab content
const navItems = nav.querySelectorAll(".ds4-sheet-tab-nav__item");
const tabContents = sheet.querySelectorAll(".ds4-sheet-tab");
/**
* @override
* @param {JQuery} html
*/
activateListeners(html) {
super.activateListeners(html);
// Remove active class from all items first
navItems.forEach(item => item.classList.remove("active"));
tabContents.forEach(content => content.classList.remove("active"));
if (!this.options.editable) return;
// Find the currently active tab or default to first
let targetTab = this.activeTab;
let targetNavItem = nav.querySelector(`[data-tab="${targetTab}"]`);
html.find(".control-effect").on("click", this.onControlEffect.bind(this));
disableOverriddenFields(this.form, this.item.overrides, (key) => `[name="${key}"]`);
}
/**
* Handles a click on an element of this sheet to control an embedded effect of the item corresponding to this
* sheet.
*
* @param {JQuery.ClickEvent} event The originating click event
* @protected
*/
onControlEffect(event) {
event.preventDefault();
const a = event.currentTarget;
switch (a.dataset["action"]) {
case "create":
return this.onCreateEffect();
case "edit":
return this.onEditEffect(event);
case "delete":
return this.onDeleteEffect(event);
// If stored tab doesn't exist, fall back to first tab
if (!targetNavItem) {
targetNavItem = navItems[0];
targetTab = targetNavItem?.dataset.tab;
}
}
/**
* Creates a new embedded effect.
* @protected
*/
onCreateEffect() {
DS4ActiveEffect.createDefault(this.item);
}
// Set target tab navigation as active
if (targetNavItem && targetTab) {
targetNavItem.classList.add("active");
/**
* Opens the sheet of the embedded effect corresponding to the clicked element.
*
* @param {JQuery.ClickEvent} event The originating click event
* @porotected
*/
onEditEffect(event) {
const id = $(event.currentTarget)
.parents(embeddedDocumentListEntryProperties.ActiveEffect.selector)
.data(embeddedDocumentListEntryProperties.ActiveEffect.idDataAttribute);
const effect = this.item.effects.get(id);
enforce(effect, getGame().i18n.format("DS4.ErrorItemDoesNotHaveEffect", { id, item: this.item.name }));
effect.sheet?.render(true);
}
/**
* Deletes the embedded item corresponding to the clicked element.
*
* @param {JQuery.ClickEvent} event The originating click event
* @protected
*/
onDeleteEffect(event) {
const li = $(event.currentTarget).parents(embeddedDocumentListEntryProperties.ActiveEffect.selector);
const id = li.data(embeddedDocumentListEntryProperties.ActiveEffect.idDataAttribute);
this.item.deleteEmbeddedDocuments("ActiveEffect", [id]);
li.slideUp(200, () => this.render(false));
// Set corresponding tab content as active
const activeTabContent = sheet.querySelector(`.ds4-sheet-tab[data-tab="${targetTab}"]`);
if (activeTabContent) {
activeTabContent.classList.add("active");
}
}
}
}
/**
* This object contains information about specific properties embedded document list entries for each different type.
*/
const embeddedDocumentListEntryProperties = Object.freeze({
ActiveEffect: {
selector: ".effect",
idDataAttribute: "effectId",
},
});

View file

@ -10,8 +10,8 @@ SPDX-License-Identifier: MIT
{{!-- Sheet Tab Navigation --}}
<nav class="ds4-sheet-tab-nav tabs" data-group="primary">
<a class="ds4-sheet-tab-nav__item" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
</nav>
{{!-- Sheet Body --}}

View file

@ -10,9 +10,9 @@ SPDX-License-Identifier: MIT
{{!-- Sheet Tab Navigation --}}
<nav class="ds4-sheet-tab-nav tabs" data-group="primary">
<a class="ds4-sheet-tab-nav__item" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-action="changeTab" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-action="changeTab" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-action="changeTab" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
</nav>
{{!-- Sheet Body --}}

View file

@ -10,9 +10,9 @@ SPDX-License-Identifier: MIT
{{!-- Sheet Tab Navigation --}}
<nav class="ds4-sheet-tab-nav tabs" data-group="primary">
<a class="ds4-sheet-tab-nav__item" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
</nav>
{{!-- Sheet Body --}}

View file

@ -10,8 +10,8 @@ SPDX-License-Identifier: MIT
{{!-- Sheet Tab Navigation --}}
<nav class="ds4-sheet-tab-nav tabs" data-group="primary">
<a class="ds4-sheet-tab-nav__item" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
</nav>
{{!-- Sheet Body --}}

View file

@ -10,9 +10,9 @@ SPDX-License-Identifier: MIT
{{!-- Sheet Tab Navigation --}}
<nav class="ds4-sheet-tab-nav tabs" data-group="primary">
<a class="ds4-sheet-tab-nav__item" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
</nav>
{{!-- Sheet Body --}}

View file

@ -10,8 +10,8 @@ SPDX-License-Identifier: MIT
{{!-- Sheet Tab Navigation --}}
<nav class="ds4-sheet-tab-nav tabs" data-group="primary">
<a class="ds4-sheet-tab-nav__item" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
</nav>
{{!-- Sheet Body --}}

View file

@ -10,9 +10,9 @@ SPDX-License-Identifier: MIT
{{!-- Sheet Tab Navigation --}}
<nav class="ds4-sheet-tab-nav tabs" data-group="primary">
<a class="ds4-sheet-tab-nav__item" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
</nav>
{{!-- Sheet Body --}}

View file

@ -10,9 +10,9 @@ SPDX-License-Identifier: MIT
{{!-- Sheet Tab Navigation --}}
<nav class="ds4-sheet-tab-nav tabs" data-group="primary">
<a class="ds4-sheet-tab-nav__item" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
</nav>
{{!-- Sheet Body --}}

View file

@ -10,9 +10,9 @@ SPDX-License-Identifier: MIT
{{!-- Sheet Tab Navigation --}}
<nav class="ds4-sheet-tab-nav tabs" data-group="primary">
<a class="ds4-sheet-tab-nav__item" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-action="changeTab" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-action="changeTab" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-action="changeTab" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
</nav>
{{!-- Sheet Body --}}

View file

@ -10,9 +10,9 @@ SPDX-License-Identifier: MIT
{{!-- Sheet Tab Navigation --}}
<nav class="ds4-sheet-tab-nav tabs" data-group="primary">
<a class="ds4-sheet-tab-nav__item" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-action="changeTab" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-action="changeTab" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-action="changeTab" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
</nav>
{{!-- Sheet Body --}}

View file

@ -10,9 +10,9 @@ SPDX-License-Identifier: MIT
{{!-- Sheet Tab Navigation --}}
<nav class="ds4-sheet-tab-nav tabs" data-group="primary">
<a class="ds4-sheet-tab-nav__item" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-action="changeTab" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-action="changeTab" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a>
<a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-action="changeTab" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
</nav>
{{!-- Sheet Body --}}