191 lines
7.7 KiB
JavaScript
191 lines
7.7 KiB
JavaScript
// Audit results grid column UX (#23 follow-ups Task 10).
|
|
//
|
|
// A tiny, dependency-free helper for the AuditResultsGrid component:
|
|
// - drag-to-resize: a pointer-driven handle on each <th>'s right edge,
|
|
// - drag-to-reorder: native HTML5 drag-and-drop on the header row,
|
|
// - save/load: a sessionStorage round-trip, mirroring treeview-storage.js.
|
|
//
|
|
// The Blazor component owns the column model; this file is purely the
|
|
// browser-side drag plumbing. After a resize or reorder it calls back into
|
|
// .NET via a DotNetObjectReference so the component can persist + re-render.
|
|
//
|
|
// No drag-drop libraries — hand-rolled pointer + native-DnD handlers only.
|
|
window.auditGrid = {
|
|
// --- sessionStorage wrapper (mirrors window.treeviewStorage) -----------
|
|
// Keys are namespaced under "auditGrid:" so they never collide with the
|
|
// treeview's "treeview:" namespace.
|
|
save: function (key, json) {
|
|
try {
|
|
sessionStorage.setItem("auditGrid:" + key, json);
|
|
} catch {
|
|
// Quota / privacy-mode failures are non-fatal — the grid simply
|
|
// falls back to defaults on the next load.
|
|
}
|
|
},
|
|
|
|
load: function (key) {
|
|
try {
|
|
return sessionStorage.getItem("auditGrid:" + key);
|
|
} catch {
|
|
return null;
|
|
}
|
|
},
|
|
|
|
// Minimum column width in pixels. A column can never be dragged narrower
|
|
// than this so a header can't collapse to an unclickable sliver.
|
|
minWidth: 64,
|
|
|
|
// --- wire-up ----------------------------------------------------------
|
|
// `table` is the <table> element, `dotNet` is a DotNetObjectReference
|
|
// exposing OnColumnResized / OnColumnReordered. Safe to call on every
|
|
// render: it re-scans the header and binds only cells not already bound,
|
|
// and always refreshes the live .NET reference. Handlers read the column
|
|
// key live from data-col-key at event time, so Blazor reusing a <th> DOM
|
|
// node for a different column (after a reorder re-render) is harmless.
|
|
init: function (table, dotNet) {
|
|
if (!table) {
|
|
return;
|
|
}
|
|
table.__auditGridDotNet = dotNet;
|
|
|
|
var headerRow = table.tHead && table.tHead.rows[0];
|
|
if (!headerRow) {
|
|
return;
|
|
}
|
|
|
|
for (var i = 0; i < headerRow.cells.length; i++) {
|
|
this._bindHeaderCell(table, headerRow.cells[i]);
|
|
}
|
|
},
|
|
|
|
// Bind resize + reorder handlers to a single <th>. Idempotent — a cell
|
|
// already carrying handlers is skipped. The handlers resolve the column
|
|
// key live (th.getAttribute) so they stay correct if the renderer reuses
|
|
// the element for another column.
|
|
_bindHeaderCell: function (table, th) {
|
|
var self = this;
|
|
if (th.__auditGridCellBound) {
|
|
return;
|
|
}
|
|
th.__auditGridCellBound = true;
|
|
|
|
// --- resize: pointer drag on the handle ---------------------------
|
|
var handle = th.querySelector(".audit-grid-resize-handle");
|
|
if (handle) {
|
|
handle.addEventListener("pointerdown", function (ev) {
|
|
ev.preventDefault();
|
|
// Stop the pointerdown from also starting a header drag.
|
|
ev.stopPropagation();
|
|
|
|
var startX = ev.clientX;
|
|
var startWidth = th.getBoundingClientRect().width;
|
|
handle.setPointerCapture(ev.pointerId);
|
|
th.classList.add("resizing");
|
|
|
|
function onMove(moveEv) {
|
|
var next = Math.max(self.minWidth, startWidth + (moveEv.clientX - startX));
|
|
self._applyWidth(th, next);
|
|
}
|
|
|
|
function onUp() {
|
|
handle.releasePointerCapture(ev.pointerId);
|
|
handle.removeEventListener("pointermove", onMove);
|
|
handle.removeEventListener("pointerup", onUp);
|
|
handle.removeEventListener("pointercancel", onUp);
|
|
th.classList.remove("resizing");
|
|
|
|
var key = th.getAttribute("data-col-key");
|
|
var finalWidth = Math.round(th.getBoundingClientRect().width);
|
|
var dn = table.__auditGridDotNet;
|
|
if (key && dn) {
|
|
dn.invokeMethodAsync("OnColumnResized", key, finalWidth);
|
|
}
|
|
}
|
|
|
|
handle.addEventListener("pointermove", onMove);
|
|
handle.addEventListener("pointerup", onUp);
|
|
handle.addEventListener("pointercancel", onUp);
|
|
});
|
|
}
|
|
|
|
// --- reorder: native HTML5 drag-and-drop on the header ------------
|
|
// The whole <th> is draggable; dropping it onto another header swaps
|
|
// the dragged column into the drop target's position.
|
|
th.setAttribute("draggable", "true");
|
|
|
|
th.addEventListener("dragstart", function (ev) {
|
|
// A resize in progress sets .resizing; never start a reorder then.
|
|
if (th.classList.contains("resizing")) {
|
|
ev.preventDefault();
|
|
return;
|
|
}
|
|
var key = th.getAttribute("data-col-key");
|
|
if (!key) {
|
|
ev.preventDefault();
|
|
return;
|
|
}
|
|
table.__auditGridDragKey = key;
|
|
ev.dataTransfer.effectAllowed = "move";
|
|
// Some browsers require data to be set for the drag to begin.
|
|
try { ev.dataTransfer.setData("text/plain", key); } catch { /* ignore */ }
|
|
th.classList.add("dragging");
|
|
});
|
|
|
|
th.addEventListener("dragend", function () {
|
|
th.classList.remove("dragging");
|
|
table.__auditGridDragKey = null;
|
|
self._clearDropTargets(table);
|
|
});
|
|
|
|
th.addEventListener("dragover", function (ev) {
|
|
// Allowing the drop is what lets dragover/drop fire at all.
|
|
var key = th.getAttribute("data-col-key");
|
|
if (key && table.__auditGridDragKey && table.__auditGridDragKey !== key) {
|
|
ev.preventDefault();
|
|
ev.dataTransfer.dropEffect = "move";
|
|
th.classList.add("drop-target");
|
|
}
|
|
});
|
|
|
|
th.addEventListener("dragleave", function () {
|
|
th.classList.remove("drop-target");
|
|
});
|
|
|
|
th.addEventListener("drop", function (ev) {
|
|
ev.preventDefault();
|
|
th.classList.remove("drop-target");
|
|
var key = th.getAttribute("data-col-key");
|
|
var fromKey = table.__auditGridDragKey;
|
|
table.__auditGridDragKey = null;
|
|
if (!key || !fromKey || fromKey === key) {
|
|
return;
|
|
}
|
|
var dn = table.__auditGridDotNet;
|
|
if (dn) {
|
|
// fromKey moves to occupy toKey's slot; the component computes
|
|
// the resulting order and re-renders + persists.
|
|
dn.invokeMethodAsync("OnColumnReordered", fromKey, key);
|
|
}
|
|
});
|
|
},
|
|
|
|
// Apply a width to a <th> via a CSS custom property. The scoped stylesheet
|
|
// reads --audit-col-width; absent it, the column falls back to auto.
|
|
//
|
|
// Known, intentional behaviour: during a live resize drag this updates the
|
|
// <th> width immediately, but the <td> body cells only catch up on the next
|
|
// .NET re-render (driven by OnColumnResized at pointer-up). The brief
|
|
// header/body width mismatch mid-drag is an accepted trade-off for an
|
|
// internal tool — not a bug.
|
|
_applyWidth: function (th, widthPx) {
|
|
th.style.setProperty("--audit-col-width", widthPx + "px");
|
|
},
|
|
|
|
_clearDropTargets: function (table) {
|
|
var hits = table.querySelectorAll(".drop-target, .dragging");
|
|
for (var i = 0; i < hits.length; i++) {
|
|
hits[i].classList.remove("drop-target", "dragging");
|
|
}
|
|
}
|
|
};
|