const { HandlebarsApplicationMixin } = foundry.applications.api; import { MGT2Helper } from "../../helper.js"; export default class TravellerItemSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ItemSheetV2) { /** @override */ static DEFAULT_OPTIONS = { classes: ["mgt2", "sheet", "item"], position: { width: 630 }, form: { submitOnChange: true, closeOnSubmit: false, }, window: { resizable: true }, actions: { careerEventCreate: TravellerItemSheet.#onCareerEventCreate, careerEventDelete: TravellerItemSheet.#onCareerEventDelete, optionCreate: TravellerItemSheet.#onOptionCreate, optionDelete: TravellerItemSheet.#onOptionDelete, modifierCreate: TravellerItemSheet.#onModifierCreate, modifierDelete: TravellerItemSheet.#onModifierDelete, }, } /** Dynamic PARTS: template resolved per item type */ get PARTS() { const type = this.document?.type ?? "item"; return { sheet: { template: `systems/mgt2/templates/items/${type}-sheet.html`, }, }; } /** Resolve template dynamically based on item type */ get template() { return `systems/mgt2/templates/items/${this.document.type}-sheet.html`; } tabGroups = { primary: "tab1" } /** @override */ async _prepareContext() { const item = this.document; const source = item.toObject(); const settings = { usePronouns: game.settings.get("mgt2", "usePronouns"), }; let containers = null; let computers = null; let hadContainer = false; if (item.actor !== null) { hadContainer = true; containers = [{ name: "", _id: "" }].concat(item.actor.getContainers()); computers = [{ name: "", _id: "" }].concat(item.actor.getComputers()); } let weight = null; if (item.system.hasOwnProperty("weight")) { weight = MGT2Helper.convertWeightForDisplay(item.system.weight); } let skills = []; if (this.actor !== null) { for (let actorItem of this.actor.items) { if (actorItem.type === "talent" && actorItem.system.subType === "skill") skills.push({ _id: actorItem._id, name: actorItem.getRollDisplay() }); } } skills.sort(MGT2Helper.compareByName); skills = [{ _id: "NP", name: game.i18n.localize("MGT2.Items.NotProficient") }].concat(skills); const enrich = (html) => foundry.applications.ux.TextEditor.implementation.enrichHTML(html ?? "", { async: true }); return { item: item, document: item, cssClass: this.isEditable ? "editable" : "locked", system: item.system, source: source.system, fields: item.schema.fields, systemFields: item.system.schema.fields, isEditable: this.isEditable, isGM: game.user.isGM, config: CONFIG.MGT2, settings: settings, containers: containers, computers: computers, hadContainer: hadContainer, weight: weight, unitlabels: { weight: MGT2Helper.getWeightLabel() }, skills: skills, enrichedDescription: await enrich(item.system.description), enrichedDescriptionLong: await enrich(item.system.descriptionLong), enrichedNotes: await enrich(item.system.notes), enrichedLockedDescription: await enrich(item.system.lockedDescription), }; } /** @override — resolve the per-type template before rendering */ async _renderHTML(context, options) { const templatePath = `systems/mgt2/templates/items/${this.document.type}-sheet.html`; const html = await foundry.applications.handlebars.renderTemplate(templatePath, context); return { sheet: html }; } /** @override — put rendered HTML into the window content */ _replaceHTML(result, content, options) { content.innerHTML = result.sheet; // Inject theme class dynamically (can't use game.settings in static DEFAULT_OPTIONS) const theme = game.settings.get("mgt2", "theme"); if (theme) this.element.classList.add(theme); this._activateTabGroups(); this._bindItemEvents(); } /** Bind CSS class-based events (templates not yet migrated to data-action) */ _bindItemEvents() { const html = this.element; if (!this.isEditable) return; const bind = (sel, handler) => { for (const el of html.querySelectorAll(sel)) { el.addEventListener("click", (ev) => handler.call(this, ev, ev.currentTarget)); } }; bind(".event-create", TravellerItemSheet.#onCareerEventCreate); bind(".event-delete", TravellerItemSheet.#onCareerEventDelete); bind(".options-create", TravellerItemSheet.#onOptionCreate); bind(".options-delete", TravellerItemSheet.#onOptionDelete); bind(".modifiers-create", TravellerItemSheet.#onModifierCreate); bind(".modifiers-delete", TravellerItemSheet.#onModifierDelete); // Activate ProseMirror editors for HTMLField fields for (const btn of html.querySelectorAll(".editor-edit")) { btn.addEventListener("click", async (event) => { event.preventDefault(); const editorWrapper = btn.closest(".editor"); if (!editorWrapper) return; const editorContent = editorWrapper.querySelector(".editor-content"); if (!editorContent || editorContent.classList.contains("ProseMirror")) return; const target = editorContent.dataset.edit; const value = foundry.utils.getProperty(this.document, target) ?? ""; btn.remove(); editorWrapper.classList.add("prosemirror"); await ProseMirrorEditor.create(editorContent, value, { document: this.document, fieldName: target, plugins: {}, collaborate: false, }); }); } } _activateTabGroups() { for (const [group, activeTab] of Object.entries(this.tabGroups)) { const nav = this.element.querySelector(`nav[data-group="${group}"], .horizontal-tabs`); if (!nav) continue; nav.querySelectorAll('[data-tab]').forEach(link => { link.classList.toggle('active', link.dataset.tab === activeTab); link.addEventListener('click', event => { event.preventDefault(); this.tabGroups[group] = link.dataset.tab; this.render(); }); }); this.element.querySelectorAll(`.itemsheet-panel [data-tab], [data-group="${group}"][data-tab]`).forEach(content => { content.classList.toggle('active', content.dataset.tab === activeTab); }); } } /** @override — process form data before submit (weight/qty/cost conversions + container logic) */ _prepareSubmitData(event, form, formData) { const data = foundry.utils.expandObject(formData.object); if (data.hasOwnProperty("weight")) { data.system = data.system || {}; data.system.weight = MGT2Helper.convertWeightFromInput(data.weight); delete data.weight; } if (data.system?.hasOwnProperty("quantity")) { data.system.quantity = MGT2Helper.getIntegerFromInput(data.system.quantity); } if (data.system?.hasOwnProperty("cost")) { data.system.cost = MGT2Helper.getIntegerFromInput(data.system.cost); } // Container/equipped logic if (data.system?.hasOwnProperty("container") && this.document.system.hasOwnProperty("equipped")) { const equippedChange = this.document.system.equipped !== data.system.equipped; const containerChange = this.document.system.container?.id !== data.system.container?.id; if (equippedChange && data.system.equipped === true) { data.system.container = { id: "" }; } else if (containerChange && data.system.container?.id !== "" && this.document.system.container?.id === "") { data.system.equipped = false; } } return foundry.utils.flattenObject(data); } // ========================================================= // Actions // ========================================================= static async #onCareerEventCreate(event) { event.preventDefault(); const events = this.document.system.events; let newEvents; if (!events || events.length === 0) { newEvents = [{ age: "", description: "" }]; } else { newEvents = [...events, { age: "", description: "" }]; } return this.document.update({ system: { events: newEvents } }); } static async #onCareerEventDelete(event, target) { event.preventDefault(); const element = target.closest("[data-events-part]"); const index = Number(element.dataset.eventsPart); const events = foundry.utils.deepClone(this.document.system.events); const newEvents = Object.entries(events) .filter(([key]) => Number(key) !== index) .map(([, val]) => val); return this.document.update({ system: { events: newEvents } }); } static async #onOptionCreate(event, target) { event.preventDefault(); const property = target.dataset.property; const options = this.document.system[property]; let newOptions; if (!options || options.length === 0) { newOptions = [{ name: "", description: "" }]; } else { newOptions = [...options, { name: "", description: "" }]; } return this.document.update({ [`system.${property}`]: newOptions }); } static async #onOptionDelete(event, target) { event.preventDefault(); const element = target.closest("[data-options-part]"); const property = element.dataset.property; const index = Number(element.dataset.optionsPart); const options = foundry.utils.deepClone(this.document.system[property]); const newOptions = Object.entries(options) .filter(([key]) => Number(key) !== index) .map(([, val]) => val); return this.document.update({ [`system.${property}`]: newOptions }); } static async #onModifierCreate(event) { event.preventDefault(); const modifiers = this.document.system.modifiers; let newModifiers; if (!modifiers || modifiers.length === 0) { newModifiers = [{ characteristic: "Endurance", value: null }]; } else { newModifiers = [...modifiers, { characteristic: "Endurance", value: null }]; } return this.document.update({ system: { modifiers: newModifiers } }); } static async #onModifierDelete(event, target) { event.preventDefault(); const element = target.closest("[data-modifiers-part]"); const index = Number(element.dataset.modifiersPart); const modifiers = foundry.utils.deepClone(this.document.system.modifiers); const newModifiers = Object.entries(modifiers) .filter(([key]) => Number(key) !== index) .map(([, val]) => val); return this.document.update({ system: { modifiers: newModifiers } }); } }