const { HandlebarsApplicationMixin } = foundry.applications.api /** * Base Item Sheet for BoL system using AppV2 * @extends {ItemSheetV2} */ export default class BoLBaseItemSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ItemSheetV2) { constructor(options = {}) { super(options) this.#dragDrop = this.#createDragDropHandlers() } #dragDrop /** @override */ static DEFAULT_OPTIONS = { classes: ["bol", "sheet", "item"], position: { width: 650, height: 780, }, form: { submitOnChange: true, }, window: { resizable: true, }, actions: { editImage: BoLBaseItemSheet.#onEditImage, postItem: BoLBaseItemSheet.#onPostItem, }, } /** * Tab groups state * @type {object} */ tabGroups = { primary: "description" } /** @override */ async _prepareContext() { const context = { // Document & system fields: this.document.schema.fields, systemFields: this.document.system.schema.fields, item: this.document, system: this.document.system, source: this.document.toObject(), // Enriched content enrichedDescription: await foundry.applications.ux.TextEditor.implementation.enrichHTML( this.document.system.description, { async: true } ), // Properties category: this.document.system.category, itemProperties: this.document.itemProperties, // Config & permissions config: game.bol.config, isGM: game.user.isGM, isEditable: this.isEditable, // CSS classes for template cssClass: this.options.classes.join(" "), // Tab state tabs: this._getTabs(), activeTab: this.tabGroups.primary || "description" } // Add careers if item is on an actor if (this.document.actor) { context.careers = this.document.actor.careers } // Apply dynamic defaults based on item type this._applyDynamicDefaults(context) return context } /** * Get tabs configuration * @returns {object[]} * @private */ _getTabs() { return [ { id: "description", label: "BOL.ui.tab.description", icon: "fa-solid fa-book" }, { id: "properties", label: "BOL.ui.tab.details", icon: "fa-solid fa-cog" } ] } /** * Apply dynamic defaults to context based on item type and category * @param {object} context * @private */ _applyDynamicDefaults(context) { const itemData = context.item if (itemData.type === "item") { // Set default category if (!itemData.system.category) { itemData.system.category = "equipment" } // Handle equipment slot if (itemData.system.category === "equipment" && itemData.system.properties.equipable) { if (!itemData.system.properties.slot) { itemData.system.properties.slot = "-" } } // Handle spell conditions if (itemData.system.category === 'spell') { if (!itemData.system.properties.mandatoryconditions) { itemData.system.properties.mandatoryconditions = [] } if (!itemData.system.properties.optionnalconditions) { itemData.system.properties.optionnalconditions = [] } for (let i = 0; i < 4; i++) { itemData.system.properties.mandatoryconditions[i] = itemData.system.properties.mandatoryconditions[i] ?? "" } for (let i = 0; i < 8; i++) { itemData.system.properties.optionnalconditions[i] = itemData.system.properties.optionnalconditions[i] ?? "" } } } else if (itemData.type === "feature") { // Set default subtype/category if (!itemData.system.subtype) { itemData.system.category = "origin" } } } /** @override */ _onRender(context, options) { super._onRender(context, options) this.#dragDrop.forEach((d) => d.bind(this.element)) this._activateTabs() this._activateListeners() } /** * Activate tab navigation * @private */ _activateTabs() { const nav = this.element.querySelector('nav.tabs[data-group="primary"]') if (!nav) return const activeTab = this.tabGroups.primary || "description" // Activate tab links nav.querySelectorAll('[data-tab]').forEach(link => { const tab = link.dataset.tab link.classList.toggle('active', tab === activeTab) link.addEventListener('click', (event) => { event.preventDefault() this.tabGroups.primary = tab this.render() }) }) // Show/hide tab content this.element.querySelectorAll('.tab[data-tab]').forEach(content => { content.classList.toggle('active', content.dataset.tab === activeTab) }) } /** * Activate custom listeners * @private */ _activateListeners() { if (!this.isEditable) return // Armor quality change handler const armorQuality = this.element.querySelector('.armorQuality') if (armorQuality) { armorQuality.addEventListener('change', (ev) => { const value = ev.currentTarget.value const soakFormula = this.element.querySelector('.soakFormula') if (soakFormula && game.bol.config.soakFormulas[value]) { soakFormula.value = game.bol.config.soakFormulas[value] } }) } } // #region Drag-and-Drop Workflow /** * Create drag-and-drop workflow handlers for this Application * @returns {DragDrop[]} * @private */ #createDragDropHandlers() { return [] } // #endregion // #region Actions /** * Handle editing the item image * @param {PointerEvent} event * @param {HTMLElement} target * @private */ static async #onEditImage(event, target) { const fp = new FilePicker({ current: this.document.img, type: "image", callback: (path) => { this.document.update({ img: path }) }, }) return fp.browse() } /** * Handle posting the item to chat * @param {PointerEvent} event * @param {HTMLElement} target * @private */ static async #onPostItem(event, target) { const BoLUtility = (await import("../../system/bol-utility.js")).BoLUtility let chatData = foundry.utils.duplicate(this.document) if (this.document.actor) { chatData.actor = { id: this.document.actor.id } } BoLUtility.postItem(chatData) } // #endregion }