diff --git a/scss/components/shared/_sheet_body.scss b/scss/components/shared/_sheet_body.scss index 0718c6a6..d0bcdd50 100644 --- a/scss/components/shared/_sheet_body.scss +++ b/scss/components/shared/_sheet_body.scss @@ -4,12 +4,14 @@ * SPDX-License-Identifier: MIT */ -.ds4-sheet-body { +.ds4-sheet-body, +.sheet-body { height: 100%; overflow-y: auto; // Prevent double scrollbars on biography tab - .ds4-sheet-tab.tab.biography.active { + .ds4-sheet-tab.tab.biography.active, + .tab[data-tab="biography"].active { overflow: hidden; } } diff --git a/scss/components/shared/_sheet_tab.scss b/scss/components/shared/_sheet_tab.scss index ea1d13fc..3074f121 100644 --- a/scss/components/shared/_sheet_tab.scss +++ b/scss/components/shared/_sheet_tab.scss @@ -4,7 +4,8 @@ * SPDX-License-Identifier: MIT */ -.ds4-sheet-tab { +.ds4-sheet-tab, +.tab { flex-direction: column; flex-wrap: nowrap; height: 100%; diff --git a/scss/components/shared/_sheet_tab_nav.scss b/scss/components/shared/_sheet_tab_nav.scss index 676fea60..3bef2dd7 100644 --- a/scss/components/shared/_sheet_tab_nav.scss +++ b/scss/components/shared/_sheet_tab_nav.scss @@ -6,7 +6,8 @@ @use "../../utils/variables"; -.ds4-sheet-tab-nav { +.ds4-sheet-tab-nav, +nav.tabs { border-bottom: variables.$border-groove; border-top: variables.$border-groove; display: flex; @@ -16,7 +17,8 @@ line-height: calc(2 * var(--line-height-16)); margin: variables.$margin-sm 0; - &__item { + .ds4-sheet-tab-nav__item, + .item { flex: 0 1 auto !important; // necessary to override the styling from lang-de, see https://gitlab.com/henry4k/foundryvtt-lang-de/-/issues/9 font-weight: bold; white-space: nowrap; diff --git a/src/apps/actor/base-sheet.js b/src/apps/actor/base-sheet.js index 285a023a..0d2193ca 100644 --- a/src/apps/actor/base-sheet.js +++ b/src/apps/actor/base-sheet.js @@ -40,16 +40,37 @@ export class DS4ActorSheet extends foundry.applications.api.HandlebarsApplicatio deleteEffect: DS4ActorSheet.prototype._onDeleteEffect, changeEffect: DS4ActorSheet.prototype._onChangeEffect, sortItems: DS4ActorSheet.prototype._onSortItems, - changeTab: DS4ActorSheet.prototype._onChangeTab, + editImage: DS4ActorSheet.prototype._onEditImage, }, }; - static TABS = {}; + static TABS = { + primary: { + initial: "values", + tabs: [ + { id: "values", label: "DS4.HeadingValues", icon: "fas fa-chart-bar" }, + { id: "inventory", label: "DS4.HeadingInventory", icon: "fas fa-backpack" }, + { id: "spells", label: "DS4.HeadingSpells", icon: "fas fa-magic" }, + { id: "abilities", label: "DS4.HeadingAbilities", icon: "fas fa-fist-raised" }, + { id: "effects", label: "DS4.HeadingEffects", icon: "fas fa-sparkles" }, + { id: "biography", label: "DS4.HeadingBiography", icon: "fas fa-book" } + ] + } + }; constructor(options = {}) { super(options); - this.activeTab = "values"; // Default active tab + + // Initialize tabGroups with default values + if (!this.tabGroups) { + this.tabGroups = {}; + } + // Set default tab for primary group + if (!this.tabGroups.primary) { + this.tabGroups.primary = this.constructor.TABS.primary?.initial || "values"; + } + } get title() { @@ -125,6 +146,9 @@ export class DS4ActorSheet extends foundry.applications.api.HandlebarsApplicatio // Add tooltips to data this.addTooltipsToData(context); + // Add tabs configuration using ApplicationTab typedef + context.tabs = this._prepareTabs("primary"); + return context; } @@ -518,86 +542,145 @@ export class DS4ActorSheet extends foundry.applications.api.HandlebarsApplicatio * @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; + /** + * Prepare tabs for a given group using ApplicationTab typedef + * @param {string} group - The tab group identifier + * @returns {Record} Prepared tab data + */ + _prepareTabs(group) { - // Find tab navigation elements - const nav = target.closest(".ds4-sheet-tab-nav"); - const sheet = this.element.querySelector(".ds4-sheet-body"); + const config = this.constructor.TABS[group]; + if (!config) return {}; - if (!nav || !sheet) return; + // Ensure tabGroups is initialized + if (!this.tabGroups[group]) { + this.tabGroups[group] = config.initial; - // 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"); } + + const tabs = {}; + for (const tabConfig of config.tabs) { + const isActive = this.tabGroups[group] === tabConfig.id; + + tabs[tabConfig.id] = { + id: tabConfig.id, + group: group, + icon: tabConfig.icon, + label: game.i18n.localize(tabConfig.label), + active: isActive, + cssClass: isActive ? "active" : "" + }; + } + + + return tabs; } /** @override */ async _onRender(context, options) { - await super._onRender(context, options); - // Initialize first tab as active - this._initializeTabs(); + await super._onRender(context, options); + this._initializeTabHandlers(); } /** - * Initialize tab state - show first tab, hide others + * Initialize tab event handlers manually since ApplicationV2 doesn't do this automatically */ - _initializeTabs() { - const nav = this.element.querySelector(".ds4-sheet-tab-nav"); - const sheet = this.element.querySelector(".ds4-sheet-body"); + _initializeTabHandlers() { + const tabNavigation = this.element.querySelector('.tabs[data-group="primary"]'); + if (!tabNavigation) return; - if (!nav || !sheet) { - return; - } + // Remove existing event listeners to prevent memory leaks + this._removeTabHandlers(); - // Get all tab navigation items and tab content - const navItems = nav.querySelectorAll(".ds4-sheet-tab-nav__item"); - const tabContents = sheet.querySelectorAll(".ds4-sheet-tab"); + // Store tab navigation reference for cleanup + this._tabNavigation = tabNavigation; - // Remove active class from all items first - navItems.forEach((item) => item.classList.remove("active")); - tabContents.forEach((content) => content.classList.remove("active")); + // Create bound handler function for later removal + this._tabClickHandler = (event) => { + event.preventDefault(); + const item = event.target.closest('.item[data-tab]'); + if (!item) return; - // Find the currently active tab or default to first - let targetTab = this.activeTab; - let targetNavItem = nav.querySelector(`[data-tab="${targetTab}"]`); - - // If stored tab doesn't exist, fall back to first tab - if (!targetNavItem) { - targetNavItem = navItems[0]; - targetTab = targetNavItem?.dataset.tab; - } - - // Set target tab navigation as active - if (targetNavItem && targetTab) { - targetNavItem.classList.add("active"); - - // Set corresponding tab content as active - const activeTabContent = sheet.querySelector(`.ds4-sheet-tab[data-tab="${targetTab}"]`); - if (activeTabContent) { - activeTabContent.classList.add("active"); + const tabId = item.dataset.tab; + const groupId = item.dataset.group; + if (tabId && groupId) { + this.changeTab(tabId, groupId); } + }; + + // Add single delegated event listener to the navigation container + tabNavigation.addEventListener('click', this._tabClickHandler); + } + + /** + * Remove tab event handlers to prevent memory leaks + */ + _removeTabHandlers() { + if (this._tabNavigation && this._tabClickHandler) { + this._tabNavigation.removeEventListener('click', this._tabClickHandler); } } + /** + * Override changeTab to ensure proper tab switching without forced re-render + */ + changeTab(tab, group, options = {}) { + // Call parent changeTab method + const result = super.changeTab(tab, group, options); + + // Update tab display without full re-render + this._updateTabDisplay(tab, group); + + return result; + } + + /** + * Update tab display without full re-render for better performance + */ + _updateTabDisplay(activeTab, group) { + const tabNavigation = this.element.querySelector(`.tabs[data-group="${group}"]`); + const tabContent = this.element.querySelector('.sheet-body'); + + if (!tabNavigation || !tabContent) return; + + // Update navigation active states + const navItems = tabNavigation.querySelectorAll('.item[data-tab]'); + navItems.forEach(item => { + if (item.dataset.tab === activeTab) { + item.classList.add('active'); + } else { + item.classList.remove('active'); + } + }); + + // Update content tab visibility + const contentTabs = tabContent.querySelectorAll('.tab[data-tab]'); + contentTabs.forEach(tab => { + if (tab.dataset.tab === activeTab) { + tab.classList.add('active'); + } else { + tab.classList.remove('active'); + } + }); + } + + /** + * Override _onClose to cleanup event listeners + */ + async _onClose(options) { + this._removeTabHandlers(); + return super._onClose(options); + } + + + + + + + + /** * Handle editing the actor's portrait image * @param {Event} event - The triggering event diff --git a/src/apps/actor/creature-sheet.js b/src/apps/actor/creature-sheet.js index 3c68de8e..09fd917d 100644 --- a/src/apps/actor/creature-sheet.js +++ b/src/apps/actor/creature-sheet.js @@ -14,6 +14,20 @@ export class DS4CreatureActorSheet extends DS4ActorSheet { classes: ["sheet", "ds4-actor-sheet", "ds4-creature-sheet", "themed"], }; + static TABS = { + primary: { + initial: "values", + tabs: [ + { id: "values", label: "DS4.HeadingValues", icon: "fas fa-chart-bar" }, + { id: "inventory", label: "DS4.HeadingInventory", icon: "fas fa-backpack" }, + { id: "spells", label: "DS4.HeadingSpells", icon: "fas fa-magic" }, + { id: "abilities", label: "DS4.HeadingAbilities", icon: "fas fa-fist-raised" }, + { id: "effects", label: "DS4.HeadingEffects", icon: "fas fa-sparkles" }, + { id: "description", label: "DS4.HeadingDescription", icon: "fas fa-file-text" } + ] + } + }; + /** @override */ async _prepareContext(options) { const context = await super._prepareContext(options); @@ -29,6 +43,9 @@ export class DS4CreatureActorSheet extends DS4ActorSheet { ); } + // Add tabs configuration using ApplicationTab typedef + context.tabs = this._prepareTabs("primary"); + return context; } } diff --git a/src/apps/item-sheet.js b/src/apps/item-sheet.js index 8ff85ee7..5d1c36e6 100644 --- a/src/apps/item-sheet.js +++ b/src/apps/item-sheet.js @@ -30,16 +30,30 @@ export class DS4ItemSheet extends foundry.applications.api.HandlebarsApplication createEffect: DS4ItemSheet.prototype._onCreateEffect, editEffect: DS4ItemSheet.prototype._onEditEffect, deleteEffect: DS4ItemSheet.prototype._onDeleteEffect, - changeTab: DS4ItemSheet.prototype._onChangeTab, editImage: DS4ItemSheet.prototype._onEditImage, }, }; - static TABS = {}; + static TABS = { + primary: { + initial: "description", + tabs: [ + { id: "description", label: "DS4.HeadingDescription", icon: "fas fa-book" }, + { id: "effects", label: "DS4.HeadingEffects", icon: "fas fa-sparkles" } + ] + } + }; constructor(options = {}) { super(options); - this.activeTab = "description"; // Default active tab + // Initialize tabGroups with default values + if (!this.tabGroups) { + this.tabGroups = {}; + } + // Set default tab for primary group + if (!this.tabGroups.primary) { + this.tabGroups.primary = this.constructor.TABS.primary?.initial || "description"; + } } get title() { @@ -84,6 +98,9 @@ export class DS4ItemSheet extends foundry.applications.api.HandlebarsApplication // Add configuration context.config = CONFIG.DS4; + // Add tabs configuration using ApplicationTab typedef + context.tabs = this._prepareTabs("primary"); + // Add enriched effects context.enrichedEffects = []; if (this.item.effects && this.item.effects.size > 0) { @@ -213,41 +230,7 @@ export class DS4ItemSheet extends foundry.applications.api.HandlebarsApplication } } - /** - * 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"); - } - } /** * Handle editing the items's image @@ -269,54 +252,160 @@ export class DS4ItemSheet extends foundry.applications.api.HandlebarsApplication /** @override */ async _onRender(context, options) { await super._onRender(context, options); - - // Initialize first tab as active - this._initializeTabs(); - } - - /** @override */ - async _onClose(options) { - await super._onClose(options); + this._initializeTabHandlers(); } /** - * Initialize tab state - show first tab, hide others + * Initialize tab event handlers manually since ApplicationV2 doesn't do this automatically */ - _initializeTabs() { - const nav = this.element.querySelector(".ds4-sheet-tab-nav"); - const sheet = this.element.querySelector(".ds4-sheet-body"); + _initializeTabHandlers() { + const tabNavigation = this.element.querySelector('.tabs[data-group="primary"]'); + if (!tabNavigation) return; - if (!nav || !sheet) { - return; - } + // Remove existing event listeners to prevent memory leaks + this._removeTabHandlers(); - // Get all tab navigation items and tab content - const navItems = nav.querySelectorAll(".ds4-sheet-tab-nav__item"); - const tabContents = sheet.querySelectorAll(".ds4-sheet-tab"); + // Store tab navigation reference for cleanup + this._tabNavigation = tabNavigation; - // Remove active class from all items first - navItems.forEach((item) => item.classList.remove("active")); - tabContents.forEach((content) => content.classList.remove("active")); + // Create bound handler function for later removal + this._tabClickHandler = (event) => { + event.preventDefault(); + const item = event.target.closest('.item[data-tab]'); + if (!item) return; - // Find the currently active tab or default to first - let targetTab = this.activeTab; - let targetNavItem = nav.querySelector(`[data-tab="${targetTab}"]`); - - // If stored tab doesn't exist, fall back to first tab - if (!targetNavItem) { - targetNavItem = navItems[0]; - targetTab = targetNavItem?.dataset.tab; - } - - // Set target tab navigation as active - if (targetNavItem && targetTab) { - targetNavItem.classList.add("active"); - - // Set corresponding tab content as active - const activeTabContent = sheet.querySelector(`.ds4-sheet-tab[data-tab="${targetTab}"]`); - if (activeTabContent) { - activeTabContent.classList.add("active"); + const tabId = item.dataset.tab; + const groupId = item.dataset.group; + if (tabId && groupId) { + this.changeTab(tabId, groupId); } + }; + + // Add single delegated event listener to the navigation container + tabNavigation.addEventListener('click', this._tabClickHandler); + } + + /** + * Remove tab event handlers to prevent memory leaks + */ + _removeTabHandlers() { + if (this._tabNavigation && this._tabClickHandler) { + this._tabNavigation.removeEventListener('click', this._tabClickHandler); } } + + /** + * Override changeTab to ensure proper tab switching without forced re-render + */ + changeTab(tab, group, options = {}) { + // Call parent changeTab method + const result = super.changeTab(tab, group, options); + + // Update tab display without full re-render + this._updateTabDisplay(tab, group); + + return result; + } + + /** + * Update tab display without full re-render for better performance + */ + _updateTabDisplay(activeTab, group) { + const tabNavigation = this.element.querySelector(`.tabs[data-group="${group}"]`); + const tabContent = this.element.querySelector('.sheet-body'); + + if (!tabNavigation || !tabContent) return; + + // Update navigation active states + const navItems = tabNavigation.querySelectorAll('.item[data-tab]'); + navItems.forEach(item => { + if (item.dataset.tab === activeTab) { + item.classList.add('active'); + } else { + item.classList.remove('active'); + } + }); + + // Update content tab visibility + const contentTabs = tabContent.querySelectorAll('.tab[data-tab]'); + contentTabs.forEach(tab => { + if (tab.dataset.tab === activeTab) { + tab.classList.add('active'); + } else { + tab.classList.remove('active'); + } + }); + } + + /** + * Override _onClose to cleanup event listeners + */ + async _onClose(options) { + this._removeTabHandlers(); + return super._onClose(options); + } + + /** + * Prepare tabs for a given group using ApplicationTab typedef + * @param {string} group - The tab group identifier + * @returns {Record} Prepared tab data + */ + _prepareTabs(group) { + const config = this._getTabsConfigForItemType(); + if (!config) return {}; + + // Ensure tabGroups is initialized + if (!this.tabGroups[group]) { + this.tabGroups[group] = config.initial || "description"; + } + + const tabs = {}; + for (const tabConfig of config.tabs) { + const isActive = this.tabGroups[group] === tabConfig.id; + tabs[tabConfig.id] = { + id: tabConfig.id, + group: group, + icon: tabConfig.icon, + label: game.i18n.localize(tabConfig.label), + active: isActive, + cssClass: isActive ? "active" : "" + }; + } + + return tabs; + } + + /** + * Get tab configuration based on item type + * @returns {Object} Tab configuration for this item type + */ + _getTabsConfigForItemType() { + const tabs = [ + { id: "description", label: "DS4.HeadingDescription", icon: "fas fa-book" } + ]; + + // Item types that have properties tabs + const itemsWithProperties = [ + "weapon", "armor", "shield", "equipment", "loot", + "spell", "talent", "specialCreatureAbility" + ]; + + if (itemsWithProperties.includes(this.item.type)) { + tabs.push({ + id: "properties", + label: "DS4.HeadingProperties", + icon: "fas fa-cogs" + }); + } + + // All items can have effects + tabs.push({ + id: "effects", + label: "DS4.HeadingEffects", + icon: "fas fa-sparkles" + }); + + return { tabs, initial: "description" }; + } + } diff --git a/templates/sheets/actor/character-sheet.hbs b/templates/sheets/actor/character-sheet.hbs index 33322d79..d725c796 100644 --- a/templates/sheets/actor/character-sheet.hbs +++ b/templates/sheets/actor/character-sheet.hbs @@ -13,38 +13,47 @@ SPDX-License-Identifier: MIT {{/systems/ds4/templates/sheets/actor/components/actor-header.hbs}} {{!-- Sheet Tab Navigation --}} -