fix(04): badge count + DnD init — use DOMContentLoaded/htmx:afterSettle

Replace htmx.onLoad (requires htmx at parse time) with native
document.addEventListener('DOMContentLoaded') + 'htmx:afterSettle'
so Sortable.js is guaranteed loaded before init runs.

Add task-count-badge-{status} wrapper IDs and updateBadges() that
recounts .task-card elements on every HTMX settle so badge counts
stay in sync after create, delete, and reorder operations.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arthur Belleville 2026-05-15 09:56:28 +02:00
parent 8ac0a51969
commit 68f2ccdea3
No known key found for this signature in database

View file

@ -38,38 +38,66 @@ templ KanbanBoard(tabloID uuid.UUID, csrfToken string, tasks []sqlc.Task) {
@KanbanColumn(tabloID, status, grouped[status], csrfToken)
}
<script>
htmx.onLoad(function(content) {
var cols = content.querySelectorAll ? content.querySelectorAll(".sortable-column") : [];
if (cols.length === 0 && document) {
cols = document.querySelectorAll(".sortable-column");
}
cols.forEach(function(col) {
new Sortable(col, {
group: "kanban",
animation: 150,
handle: ".task-drag-handle",
draggable: ".task-card",
ghostClass: "bg-slate-100",
chosenClass: "opacity-50",
onEnd: function(evt) {
var form = document.getElementById("reorder-form");
form.querySelectorAll("input[name=task_id],input[name=task_col]").forEach(function(el) { el.remove(); });
document.querySelectorAll(".sortable-column").forEach(function(c) {
c.querySelectorAll(".task-card").forEach(function(card) {
var tid = card.dataset.taskId;
var inp = document.createElement("input");
inp.type = "hidden"; inp.name = "task_id"; inp.value = tid;
form.appendChild(inp);
var col = document.createElement("input");
col.type = "hidden"; col.name = "task_col"; col.value = c.dataset.status;
form.appendChild(col);
(function() {
function initSortable(root) {
var cols = root.querySelectorAll ? root.querySelectorAll(".sortable-column") : [];
if (cols.length === 0) {
cols = document.querySelectorAll(".sortable-column");
}
cols.forEach(function(col) {
if (col._sortableInit) { return; }
col._sortableInit = true;
new Sortable(col, {
group: "kanban",
animation: 150,
handle: ".task-drag-handle",
draggable: ".task-card",
ghostClass: "bg-slate-100",
chosenClass: "opacity-50",
onEnd: function() {
var form = document.getElementById("reorder-form");
form.querySelectorAll("input[name=task_id],input[name=task_col]").forEach(function(el) { el.remove(); });
document.querySelectorAll(".sortable-column").forEach(function(c) {
c.querySelectorAll(".task-card").forEach(function(card) {
var inp = document.createElement("input");
inp.type = "hidden"; inp.name = "task_id"; inp.value = card.dataset.taskId;
form.appendChild(inp);
var colInp = document.createElement("input");
colInp.type = "hidden"; colInp.name = "task_col"; colInp.value = c.dataset.status;
form.appendChild(colInp);
});
});
});
htmx.trigger(form, "submit");
form.dispatchEvent(new Event("submit", { bubbles: true }));
htmx.trigger(form, "submit");
}
});
});
}
function updateBadges() {
document.querySelectorAll(".sortable-column").forEach(function(col) {
var status = col.dataset.status;
var count = col.querySelectorAll(".task-card").length;
var wrapper = document.getElementById("task-count-badge-" + status);
if (wrapper) {
var inner = wrapper.querySelector("span") || wrapper;
inner.textContent = String(count);
}
});
}
// Initial page load — runs after all defer scripts (htmx + sortable) have executed.
document.addEventListener("DOMContentLoaded", function() {
initSortable(document.body);
updateBadges();
});
});
// After any HTMX swap — re-init new columns and refresh badge counts.
document.addEventListener("htmx:afterSettle", function() {
initSortable(document.body);
updateBadges();
});
})();
</script>
</div>
}
@ -80,7 +108,9 @@ templ KanbanColumn(tabloID uuid.UUID, status sqlc.TaskStatus, tasks []sqlc.Task,
<div class="flex-shrink-0 w-72">
<div class="bg-slate-100 rounded px-3 py-2 mb-2 flex items-center justify-between">
<h3 class="text-sm font-semibold text-slate-700">{ TaskColumnLabels[status] }</h3>
@ui.Badge(ui.BadgeProps{Label: strconv.Itoa(len(tasks)), Variant: ui.BadgeVariantInfo})
<span id={ "task-count-badge-" + string(status) }>
@ui.Badge(ui.BadgeProps{Label: strconv.Itoa(len(tasks)), Variant: ui.BadgeVariantInfo})
</span>
</div>
<div
class="sortable-column min-h-16 space-y-2"