const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; export class DhPathBrowserApp extends HandlebarsApplicationMixin(ApplicationV2) { constructor(options) { super(options); this.paths = this.constructor.getChangeChoices(); } static DEFAULT_OPTIONS = { id: "dh-path-browser-app", classes: ["daggerheart", "dh-path-browser", "dh-style"], tag: "form", window: { title: "Data Path Browser", icon: "fa-solid fa-code", resizable: true, contentClasses: ["standard-form"] }, position: { width: 600, height: 700 }, actions: { copyPath: DhPathBrowserApp.#copyPath } }; static PARTS = { header: { template: "modules/dh-path-browser/templates/header.hbs" }, list: { template: "modules/dh-path-browser/templates/list.hbs", scrollable: [".paths-list"] } }; async _prepareContext(options) { const context = await super._prepareContext(options); const groups = {}; for (const p of this.paths) { if (!groups[p.group]) groups[p.group] = []; groups[p.group].push(p); } context.groups = Object.entries(groups).map(([group, paths]) => ({ group, paths: paths.sort((a, b) => a.label.localeCompare(b.label)) })).sort((a, b) => a.group.localeCompare(b.group)); return context; } _attachPartListeners(partId, htmlElement, options) { super._attachPartListeners(partId, htmlElement, options); if (partId === "header") { const searchInput = htmlElement.querySelector('input[name="search"]'); if (searchInput) { searchInput.addEventListener("input", (e) => this.#filterPaths(e.target.value)); } } } #filterPaths(query) { query = query.toLowerCase(); const listElement = this.element.querySelector(".paths-list"); if (!listElement) return; const items = listElement.querySelectorAll(".path-item"); items.forEach(item => { const label = item.dataset.label.toLowerCase(); const value = item.dataset.value.toLowerCase(); if (label.includes(query) || value.includes(query)) { item.style.display = ""; } else { item.style.display = "none"; } }); const groups = listElement.querySelectorAll(".path-group"); groups.forEach(group => { const visibleItems = Array.from(group.querySelectorAll(".path-item")).filter(i => i.style.display !== "none"); group.style.display = visibleItems.length > 0 ? "" : "none"; }); } static async #copyPath(event, target) { const path = target.dataset.value; if (path) { const fullPath = `@system.${path}`; await navigator.clipboard.writeText(fullPath); ui.notifications.info(`Copied ${fullPath} to clipboard!`); } } static getChangeChoices() { const ignoredActorKeys = ['config', 'DhEnvironment', 'DhParty']; const getTranslations = (model, path) => { if (path === 'resources.hope.max') return { label: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.FIELDS.maxHope.label'), hint: '' }; // Armor overrides if (path.endsWith('armor.max')) return { label: game.i18n.localize('DAGGERHEART.ArmorScoreMax'), hint: '' }; if (path.endsWith('armor.current')) return { label: game.i18n.localize('DAGGERHEART.ArmorScoreCurrent'), hint: '' }; const field = model.schema.getField(path); return { label: field ? game.i18n.localize(field.label) : path, hint: field ? game.i18n.localize(field.hint) : '' }; }; const getAllLeaves = (model, root, group, parentPath = '') => { if (!root) return []; const leaves = []; const rootKey = `${parentPath ? `${parentPath}.` : ''}${root.name}`; const fields = root.fields || root; for (const [name, field] of Object.entries(fields)) { const currentPath = `${rootKey}.${name}`; if (field instanceof foundry.data.fields.SchemaField) { leaves.push(...getAllLeaves(model, field, group, rootKey)); } else { const trans = getTranslations(model, currentPath); leaves.push({ value: currentPath, label: trans.label || name, hint: trans.hint || '', group }); } } return leaves; }; const choices = []; // Process Actors for (const [key, model] of Object.entries(game.system.api.models.actors)) { if (ignoredActorKeys.includes(key) || !model?.metadata) continue; const group = game.i18n.localize(model.metadata.label); const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model.metadata.type); const bars = attributes.bar.flatMap(x => { const baseJoined = x.join('.'); return [ { value: `${baseJoined}.max`, ...getTranslations(model, `${baseJoined}.max`), group }, { value: `${baseJoined}.value`, ...getTranslations(model, `${baseJoined}.value`), group } ]; }); const values = attributes.value.flatMap(x => { const joined = x.join('.'); return { value: joined, ...getTranslations(model, joined), group }; }); const bonuses = getAllLeaves(model, model.schema.fields.bonuses, group); const rules = getAllLeaves(model, model.schema.fields.rules, group); const armor = getAllLeaves(model, model.schema.fields.armor, group); choices.push(...bars, ...values, ...rules, ...bonuses, ...armor); } // Process Items for (const [key, model] of Object.entries(game.system.api.models.items)) { if (!model?.metadata) continue; const group = `${game.i18n.localize('DOCUMENT.Item')} (${game.i18n.localize(model.metadata.label)})`; const bonuses = getAllLeaves(model, model.schema.fields.bonuses, group); const armor = getAllLeaves(model, model.schema.fields.armor, group); choices.push(...bonuses, ...armor); } return choices; } }