Files
FoundryVTT/src/macro_enable-history-tab.js
centron\schwoerer f054a31b20 zischenstand
2025-11-14 14:52:43 +01:00

227 lines
7.2 KiB
JavaScript

/**
* Add a History tab to PF1 actor sheets with HP/XP/Currency subtabs.
* Requires the corresponding flag-tracking macros to populate data.
*/
const TAB_ID = "pf1-history";
const TAB_LABEL = "History";
if (!game.system.id.includes("pf1")) {
ui.notifications.warn("This macro is intended for the PF1 system.");
return;
}
game.pf1 ??= {};
game.pf1.historyTab ??= {};
const PF1_SHEET_HOOKS = [
"renderActorSheetPFCharacter",
"renderActorSheetPFNPC",
"renderActorSheetPFNPCLoot",
"renderActorSheetPFNPCLite",
"renderActorSheetPFTrap",
"renderActorSheetPFVehicle",
"renderActorSheetPFHaunt",
"renderActorSheetPFBasic",
];
game.pf1.historyTab.renderHooks ??= [];
if (game.pf1.historyTab.renderHooks.length) {
return ui.notifications.info("History tab hooks already active.");
}
for (const hook of PF1_SHEET_HOOKS) {
const id = Hooks.on(hook, (sheet, html) => {
const delays = [0, 50, 200];
delays.forEach((delay) =>
setTimeout(() => {
try {
injectHistoryTab(sheet);
} catch (err) {
console.error("PF1 History Tab | Failed to render history tab", err);
}
}, delay)
);
});
game.pf1.historyTab.renderHooks.push({ event: hook, id });
}
ui.notifications.info("PF1 History tab enabled. Reopen actor sheets to see the new tab.");
function injectHistoryTab(sheet) {
if (!sheet?.element?.length) return;
console.log("PF1 History Tab | Injecting for", sheet.actor?.name, sheet.id);
const actor = sheet.actor;
const nav = sheet.element.find('.sheet-navigation.tabs[data-group="primary"]').first();
const body = sheet.element.find(".sheet-body").first();
if (!nav.length || !body.length) return;
ensureHistoryTab(nav);
const existing = body.find(`.tab[data-tab="${TAB_ID}"]`).first();
const content = buildHistoryContent(actor);
if (existing.length) existing.html(content);
else body.append(`<div class="tab history-tab" data-tab="${TAB_ID}" data-group="primary">${content}</div>`);
sheet._tabs?.forEach((tabs) => tabs.bind(sheet.element[0]));
setupObserver(sheet, nav);
}
function ensureHistoryTab(nav) {
if (nav.find(`[data-tab="${TAB_ID}"]`).length) return;
nav.append(`<a class="item" data-group="primary" data-tab="${TAB_ID}">${TAB_LABEL}</a>`);
}
function setupObserver(sheet, nav) {
game.pf1.historyTab.observers ??= new Map();
const key = sheet.id;
if (game.pf1.historyTab.observers.has(key)) return;
const observer = new MutationObserver(() => {
const currentNav = sheet.element.find('.sheet-navigation.tabs[data-group="primary"]').first();
if (!currentNav.length) return;
ensureHistoryTab(currentNav);
});
observer.observe(sheet.element[0], { childList: true, subtree: true });
game.pf1.historyTab.observers.set(key, observer);
const closeHandler = () => {
observer.disconnect();
game.pf1.historyTab.observers.delete(key);
sheet.element.off("remove", closeHandler);
};
sheet.element.on("remove", closeHandler);
}
function buildHistoryContent(actor) {
const configs = [
{
id: "hp",
label: "HP",
flag: "pf1HpHistory",
columns: [
{ label: "Timestamp", render: (e) => formatDate(e.timestamp) },
{ label: "HP", render: (e) => e.hp },
{ label: "Δ", render: (e) => e.diff },
{ label: "User", render: (e) => e.user ?? "" },
{ label: "Source", render: (e) => e.source ?? "" },
],
},
{
id: "xp",
label: "XP",
flag: "pf1XpHistory",
columns: [
{ label: "Timestamp", render: (e) => formatDate(e.timestamp) },
{ label: "XP", render: (e) => e.value },
{ label: "Δ", render: (e) => e.diff },
{ label: "User", render: (e) => e.user ?? "" },
],
},
{
id: "currency",
label: "Currency",
flag: "pf1CurrencyHistory",
columns: [
{ label: "Timestamp", render: (e) => formatDate(e.timestamp) },
{ label: "Totals", render: (e) => e.value },
{ label: "Δ", render: (e) => e.diff },
{ label: "User", render: (e) => e.user ?? "" },
],
},
];
const nav = configs
.map((cfg, idx) => `<a class="history-subtab ${idx === 0 ? "active" : ""}" data-history-subtab="${cfg.id}">${cfg.label}</a>`)
.join("");
const panels = configs
.map((cfg, idx) => {
const entries = actor.getFlag("world", cfg.flag) ?? [];
const table = renderHistoryTable(entries, cfg.columns, cfg.id);
return `<section class="history-panel" data-history-panel="${cfg.id}" style="display:${idx === 0 ? "block" : "none"}">${table}</section>`;
})
.join("");
return `
<section class="pf1-history-root" data-history-block="history-tab">
<nav class="history-subnav">
${nav}
</nav>
<div class="history-panels">
${panels}
</div>
<script type="text/javascript">
(function(){
const root = document.currentScript.closest('[data-history-block="history-tab"]');
if (!root) return;
const tabs = Array.from(root.querySelectorAll('[data-history-subtab]'));
const panels = Array.from(root.querySelectorAll('[data-history-panel]'));
tabs.forEach((btn) => {
btn.addEventListener('click', () => {
const target = btn.dataset.historySubtab;
tabs.forEach((n) => n.classList.toggle('active', n === btn));
panels.forEach((panel) => (panel.style.display = panel.dataset.historyPanel === target ? 'block' : 'none'));
});
});
})();
</script>
</section>
`;
}
function renderHistoryTable(entries, columns, id) {
if (!entries.length) return `<p class="history-empty">No ${id.toUpperCase()} history recorded.</p>`;
const rows = entries
.map(
(entry) => `
<tr data-ts="${entry.timestamp}">
${columns.map((col) => `<td>${col.render(entry) ?? ""}</td>`).join("")}
</tr>`
)
.join("");
return `
<div class="history-filters">
<label>From <input type="date" data-filter="from-${id}"></label>
<label>To <input type="date" data-filter="to-${id}"></label>
</div>
<table class="history-table history-${id}">
<thead>
<tr>${columns.map((col) => `<th>${col.label}</th>`).join("")}</tr>
</thead>
<tbody>
${rows}
</tbody>
</table>
<script type="text/javascript">
(function(){
const root = document.currentScript.closest('.history-panel[data-history-panel="${id}"]');
if (!root) return;
const rows = Array.from(root.querySelectorAll("tbody tr"));
const fromInput = root.querySelector('input[data-filter="from-${id}"]');
const toInput = root.querySelector('input[data-filter="to-${id}"]');
function applyFilters(){
const from = fromInput?.value ? Date.parse(fromInput.value) : null;
const to = toInput?.value ? Date.parse(toInput.value) + 86399999 : null;
rows.forEach((row) => {
const ts = Number(row.dataset.ts);
const visible = (!from || ts >= from) && (!to || ts <= to);
row.style.display = visible ? "" : "none";
});
}
fromInput?.addEventListener("input", applyFilters);
toInput?.addEventListener("input", applyFilters);
applyFilters();
})();
</script>
`;
}
function formatDate(ts) {
return new Date(ts).toLocaleString();
}