// 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 '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 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
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 . 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 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 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 // width immediately, but the 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"); } } };