diff --git a/src/apps/actor/base-sheet.js b/src/apps/actor/base-sheet.js index bbe99672..ffc97282 100644 --- a/src/apps/actor/base-sheet.js +++ b/src/apps/actor/base-sheet.js @@ -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; } } diff --git a/src/apps/item-sheet.js b/src/apps/item-sheet.js index e9b84533..6e4825f8 100644 --- a/src/apps/item-sheet.js +++ b/src/apps/item-sheet.js @@ -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", - }, -}); diff --git a/templates/sheets/item/alphabet-sheet.hbs b/templates/sheets/item/alphabet-sheet.hbs index f7089cf6..2f2d66de 100644 --- a/templates/sheets/item/alphabet-sheet.hbs +++ b/templates/sheets/item/alphabet-sheet.hbs @@ -10,8 +10,8 @@ SPDX-License-Identifier: MIT {{!-- Sheet Tab Navigation --}} {{!-- Sheet Body --}} diff --git a/templates/sheets/item/armor-sheet.hbs b/templates/sheets/item/armor-sheet.hbs index 78d8be64..548f7e68 100644 --- a/templates/sheets/item/armor-sheet.hbs +++ b/templates/sheets/item/armor-sheet.hbs @@ -10,9 +10,9 @@ SPDX-License-Identifier: MIT {{!-- Sheet Tab Navigation --}} {{!-- Sheet Body --}} diff --git a/templates/sheets/item/equipment-sheet.hbs b/templates/sheets/item/equipment-sheet.hbs index eeb322ff..05ae6f13 100644 --- a/templates/sheets/item/equipment-sheet.hbs +++ b/templates/sheets/item/equipment-sheet.hbs @@ -10,9 +10,9 @@ SPDX-License-Identifier: MIT {{!-- Sheet Tab Navigation --}} {{!-- Sheet Body --}} diff --git a/templates/sheets/item/language-sheet.hbs b/templates/sheets/item/language-sheet.hbs index f7089cf6..2f2d66de 100644 --- a/templates/sheets/item/language-sheet.hbs +++ b/templates/sheets/item/language-sheet.hbs @@ -10,8 +10,8 @@ SPDX-License-Identifier: MIT {{!-- Sheet Tab Navigation --}} {{!-- Sheet Body --}} diff --git a/templates/sheets/item/loot-sheet.hbs b/templates/sheets/item/loot-sheet.hbs index 0216ecc5..85524857 100644 --- a/templates/sheets/item/loot-sheet.hbs +++ b/templates/sheets/item/loot-sheet.hbs @@ -10,9 +10,9 @@ SPDX-License-Identifier: MIT {{!-- Sheet Tab Navigation --}} {{!-- Sheet Body --}} diff --git a/templates/sheets/item/racialAbility-sheet.hbs b/templates/sheets/item/racialAbility-sheet.hbs index f7089cf6..2f2d66de 100644 --- a/templates/sheets/item/racialAbility-sheet.hbs +++ b/templates/sheets/item/racialAbility-sheet.hbs @@ -10,8 +10,8 @@ SPDX-License-Identifier: MIT {{!-- Sheet Tab Navigation --}} {{!-- Sheet Body --}} diff --git a/templates/sheets/item/shield-sheet.hbs b/templates/sheets/item/shield-sheet.hbs index 6b9c8803..9a07cce9 100644 --- a/templates/sheets/item/shield-sheet.hbs +++ b/templates/sheets/item/shield-sheet.hbs @@ -10,9 +10,9 @@ SPDX-License-Identifier: MIT {{!-- Sheet Tab Navigation --}} {{!-- Sheet Body --}} diff --git a/templates/sheets/item/specialCreatureAbility-sheet.hbs b/templates/sheets/item/specialCreatureAbility-sheet.hbs index 83b3be7d..002852cc 100644 --- a/templates/sheets/item/specialCreatureAbility-sheet.hbs +++ b/templates/sheets/item/specialCreatureAbility-sheet.hbs @@ -10,9 +10,9 @@ SPDX-License-Identifier: MIT {{!-- Sheet Tab Navigation --}} {{!-- Sheet Body --}} diff --git a/templates/sheets/item/spell-sheet.hbs b/templates/sheets/item/spell-sheet.hbs index 1291a407..38874d00 100644 --- a/templates/sheets/item/spell-sheet.hbs +++ b/templates/sheets/item/spell-sheet.hbs @@ -10,9 +10,9 @@ SPDX-License-Identifier: MIT {{!-- Sheet Tab Navigation --}} {{!-- Sheet Body --}} diff --git a/templates/sheets/item/talent-sheet.hbs b/templates/sheets/item/talent-sheet.hbs index a6faf4f4..0e7d94d5 100644 --- a/templates/sheets/item/talent-sheet.hbs +++ b/templates/sheets/item/talent-sheet.hbs @@ -10,9 +10,9 @@ SPDX-License-Identifier: MIT {{!-- Sheet Tab Navigation --}} {{!-- Sheet Body --}} diff --git a/templates/sheets/item/weapon-sheet.hbs b/templates/sheets/item/weapon-sheet.hbs index c08ff3de..05319a7a 100644 --- a/templates/sheets/item/weapon-sheet.hbs +++ b/templates/sheets/item/weapon-sheet.hbs @@ -10,9 +10,9 @@ SPDX-License-Identifier: MIT {{!-- Sheet Tab Navigation --}} {{!-- Sheet Body --}}