227 lines
7.2 KiB
JavaScript
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();
|
|
}
|