- Pin Dockerfile to Go 1.23 to match go.mod - Record README front-door audit findings
111 lines
4.2 KiB
JavaScript
111 lines
4.2 KiB
JavaScript
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
|
|
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
|
|
|
|
// Generic drag-to-reorder wiring for htmx + SortableJS — CSP-safe (self-hosted
|
|
// library, external init, class-based feedback; no inline scripts or styles).
|
|
//
|
|
// Any element carrying `data-sortable` becomes a SortableJS container; its
|
|
// options come from data attributes:
|
|
// data-sortable-direction "horizontal" | "vertical" (default vertical)
|
|
// data-sortable-draggable CSS selector for the draggable children
|
|
// data-sortable-handle CSS selector for the drag handle (optional)
|
|
// data-sortable-filter CSS selector for pinned/non-draggable items;
|
|
// also blocks dropping relative to such items
|
|
//
|
|
// On drop, SortableJS fires `end`; the container's own hx-* attributes POST the
|
|
// new order (hidden inputs serialized by htmx in their new DOM order). Used by
|
|
// the topology ladder columns (horizontal) and the ladder tier rows (vertical).
|
|
// Re-inits after every htmx swap via htmx.onLoad; a per-element guard prevents
|
|
// double-init, and onEnd freezes the instance while the request is in flight
|
|
// (the swapped-in container is a fresh element and gets its own instance).
|
|
(function () {
|
|
"use strict";
|
|
|
|
function init(root) {
|
|
var nodes = (root || document).querySelectorAll("[data-sortable]");
|
|
Array.prototype.forEach.call(nodes, function (el) {
|
|
if (el._sortableReady || typeof Sortable === "undefined") {
|
|
return;
|
|
}
|
|
el._sortableReady = true;
|
|
|
|
var opts = {
|
|
animation: 150,
|
|
direction: el.getAttribute("data-sortable-direction") || "vertical",
|
|
ghostClass: "sortable-ghost",
|
|
chosenClass: "sortable-chosen",
|
|
onEnd: function () {
|
|
this.option("disabled", true);
|
|
},
|
|
};
|
|
|
|
var draggable = el.getAttribute("data-sortable-draggable");
|
|
if (draggable) {
|
|
opts.draggable = draggable;
|
|
}
|
|
var handle = el.getAttribute("data-sortable-handle");
|
|
if (handle) {
|
|
opts.handle = handle;
|
|
}
|
|
var filterSel = el.getAttribute("data-sortable-filter");
|
|
if (filterSel) {
|
|
opts.filter = filterSel;
|
|
// Never let a draggable cross a pinned/filtered item.
|
|
opts.onMove = function (evt) {
|
|
return !evt.related.matches(filterSel);
|
|
};
|
|
}
|
|
|
|
new Sortable(el, opts);
|
|
});
|
|
}
|
|
|
|
// Recover a frozen drag-to-reorder after a suppressed swap. onEnd disables
|
|
// the instance while the reorder POST is in flight; on CSRF expiry / 5xx /
|
|
// network error, error-handler.js sets shouldSwap=false and fires
|
|
// `operator:reorder-recover`. Without recovery the table would keep the
|
|
// dropped row order with stale rank badges and drag would stay dead until a
|
|
// full refresh. Re-enable any frozen instance (belt-and-suspenders if the
|
|
// re-fetch also fails) and re-fetch the current page's #operator-body so the
|
|
// server's true order and rank badges are restored (audit finding #55).
|
|
function recover() {
|
|
var nodes = document.querySelectorAll("[data-sortable]");
|
|
var wasFrozen = false;
|
|
Array.prototype.forEach.call(nodes, function (el) {
|
|
var inst =
|
|
window.Sortable && typeof Sortable.get === "function"
|
|
? Sortable.get(el)
|
|
: null;
|
|
if (inst && inst.option("disabled")) {
|
|
inst.option("disabled", false);
|
|
wasFrozen = true;
|
|
}
|
|
});
|
|
// Only re-fetch when a reorder was actually in flight (a frozen instance).
|
|
// A frozen instance is re-enabled above BEFORE the re-fetch, so if that
|
|
// GET also fails the next recover event finds nothing frozen and stops —
|
|
// no reload loop.
|
|
if (wasFrozen && window.htmx && typeof htmx.ajax === "function") {
|
|
htmx.ajax("GET", window.location.pathname + window.location.search, {
|
|
target: "#operator-body",
|
|
swap: "outerHTML",
|
|
select: "#operator-body",
|
|
});
|
|
}
|
|
}
|
|
|
|
function boot() {
|
|
if (window.htmx && typeof htmx.onLoad === "function") {
|
|
htmx.onLoad(init);
|
|
}
|
|
document.body.addEventListener("operator:reorder-recover", recover);
|
|
init(document);
|
|
}
|
|
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", boot);
|
|
} else {
|
|
boot();
|
|
}
|
|
})();
|