// Manages run-comparison selection on the runs list. // HTMX re-renders #runs-slot every 3s, so we keep state in a Set outside // the polled region and re-apply checked state on every afterSwap. (function () { const MIN = 2; const MAX = 8; const selected = new Set(); const btn = document.getElementById('compare-btn'); const countEl = document.getElementById('compare-count'); const slot = document.getElementById('runs-slot'); if (!btn || !countEl || !slot) return; function refreshButton() { const n = selected.size; countEl.textContent = `(${n}/${MAX})`; btn.disabled = n < MIN || n > MAX; } function applyToDOM() { // Selections persist across swaps — with server-side filtering, rows // leave the DOM when they don't match the current filter, but the user // still has them "in the cart". const atCap = selected.size >= MAX; slot.querySelectorAll('.compare-cb').forEach((cb) => { const stem = cb.dataset.stem; cb.checked = selected.has(stem); cb.disabled = atCap && !cb.checked; }); refreshButton(); } slot.addEventListener('change', (e) => { const cb = e.target; if (!cb.matches || !cb.matches('.compare-cb')) return; const stem = cb.dataset.stem; if (cb.checked) { if (selected.size >= MAX) { cb.checked = false; return; } selected.add(stem); } else { selected.delete(stem); } applyToDOM(); }); document.body.addEventListener('htmx:afterSwap', (e) => { if (e.target && e.target.id === 'runs-slot') applyToDOM(); }); btn.addEventListener('click', () => { const n = selected.size; if (n < MIN || n > MAX) return; const qs = [...selected].map((s) => `stem=${encodeURIComponent(s)}`).join('&'); window.open(`/compare?${qs}`, '_blank', 'noopener'); }); applyToDOM(); })();