Files
dmtools/assets/app.js
T
FrankandClaude Sonnet 5 9854e6c034 Add combat HP tracker
- one running tracker (Combatant rows); no encounters to manage
- Enemies list gets a "Track" column (checkbox + count) and an
  "Add to tracker" button; picking N of an enemy makes "Goblin 1..N",
  numbering continues if some are already on the tracker
- /tracker: per-combatant current/max HP with an HP bar, Damage / Heal
  by amount, remove one, "Clear all". HP is clamped to [0, max];
  0 HP shows a "Down" badge. Stats are snapshotted from the Enemy on add.
- "Tracker" nav item; tracker_picker Stimulus controller; app.js gains a
  data-format="pick" cell renderer

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 19:16:12 +02:00

108 lines
4.4 KiB
JavaScript

/*
* dmtools front-end entrypoint (Symfony AssetMapper / importmap).
* Loaded from base.html.twig via {{ importmap('app') }}.
*/
import './stimulus_bootstrap.js';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'datatables.net-bs5/css/dataTables.bootstrap5.min.css';
import './styles/app.css';
import * as bootstrap from 'bootstrap';
import DataTable from 'datatables.net-bs5';
window.bootstrap = bootstrap;
const esc = (v) => {
if (v === null || v === undefined) return '';
return String(v).replace(/[&<>"']/g, (c) => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
}[c]));
};
// Build a DataTables column definition from a <th> in the table head.
// data-data key in the row object (required for server-side tables)
// data-orderable "false" to disable sorting on the column
// data-format "link" | "truncate" | (default: plain escaped text)
// data-row-url (on the <table>) URL template with __ID__ for "link" columns
// NB: "format" not "render" - DataTables itself consumes a data-render attribute.
function columnFromTh(th, rowUrl) {
const key = th.dataset.data;
const col = { data: key || null, orderable: th.dataset.orderable !== 'false' };
if (th.dataset.format === 'link' && rowUrl) {
col.render = (val, type, row) => {
if (type !== 'display') return val ?? '';
return `<a href="${esc(rowUrl.replace('__ID__', row.id))}">${esc(val)}</a>`;
};
} else if (th.dataset.format === 'truncate') {
col.render = (val, type) => {
if (type !== 'display') return val ?? '';
const s = val ? String(val) : '';
const short = s.length > 70 ? s.slice(0, 70) + '…' : s;
return `<span title="${esc(s)}">${esc(short)}</span>`;
};
} else if (th.dataset.format === 'pick') {
// a row-select checkbox + quantity, for "add these to the tracker"
col.render = (val, type, row) => {
if (type !== 'display') return '';
return `<span class="track-pick d-inline-flex align-items-center gap-2">
<input type="checkbox" class="form-check-input track-check" value="${esc(row.id)}" aria-label="Select">
<input type="number" class="form-control form-control-sm track-qty" value="1" min="1" max="50"
style="width: 3.75rem" aria-label="Count">
</span>`;
};
} else {
col.render = (val, type) => (type === 'display' ? esc(val) : (val ?? ''));
}
return col;
}
// Live DataTable instances, keyed by their <table> element. Used to tear them
// down before Turbo caches a page - otherwise the back button restores a
// snapshot that still contains DataTables' injected controls, and re-init
// stacks a second set of "search" / "entries per page" rows on top.
const instances = new Map();
function initDataTables(root = document) {
root.querySelectorAll('table.datatable').forEach((table) => {
if (table.dataset.datatable === 'false' || instances.has(table) || DataTable.isDataTable(table)) {
return;
}
let options = { pageLength: 10, order: [], responsive: false };
if (table.dataset.datatableOptions) {
try {
options = { ...options, ...JSON.parse(table.dataset.datatableOptions) };
} catch (e) {
console.error('Invalid data-datatable-options on', table, e);
}
}
// Server-side mode: the backend does the paging, sorting and searching.
if (table.dataset.datatableSrc) {
const ths = [...table.querySelectorAll('thead th')];
options = {
...options,
serverSide: true,
processing: true,
ajax: { url: table.dataset.datatableSrc },
columns: ths.map((th) => columnFromTh(th, table.dataset.rowUrl)),
order: options.order.length ? options.order : [[0, 'asc']],
};
}
instances.set(table, new DataTable(table, options));
});
}
function destroyDataTables() {
instances.forEach((dt) => dt.destroy());
instances.clear();
}
document.addEventListener('DOMContentLoaded', () => initDataTables());
document.addEventListener('turbo:load', () => initDataTables());
// Restore the plain <table> before Turbo snapshots the page for its cache.
document.addEventListener('turbo:before-cache', destroyDataTables);