286 lines
12 KiB
JavaScript
286 lines
12 KiB
JavaScript
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 } });
|
|
}
|
|
}
|