Live-ghost column drag; "only" app filter; disk size / free space filters
Custom View column drag rewritten with pointer events (replacing native drag-and-drop): grab a column by its grip or anywhere on its header (a click still sorts); the real column slides into the slot it would land in as a translucent striped ghost while a label follows the pointer. Only the horizontal position decides the slot. Esc cancels; data refreshes wait until the drop; wide tables scroll near the edges. Filters: - List columns (software, tags): "Has any ticked" / "Has only ticked" toggle - e.g. devices missing only Adobe Acrobat Pro - Space columns: filter by disk size (221 GB, 442 GB, no reading) plus a free-space rule (any / at least N GB / less than N GB, default 20); sort by disk size or by free space (numeric, no reading last); header clicks cancel these sorts - Menu controls no longer shrink when the menu is height-limited
This commit is contained in:
1 parent
973ff592a4
commit
50c2a18a68
3 files changed
+277
-92
No files matched your search
+104
-60
@@ -142,67 +142,108 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Custom View column drag: a ghost of the column follows the cursor, the column being
|
||||
// moved is dimmed, and a line shows exactly where it will land. Adds to the dashboard's
|
||||
// own drag-and-drop (attachColDragHandlers), which still performs the move.
|
||||
function enhanceColumnDrag() {
|
||||
// Custom View column drag (desktop). Grab a column by its grip or anywhere on its header
|
||||
// (a plain click still sorts). While dragging, the real column slides into the place it
|
||||
// would land, drawn as a translucent ghost, and a label follows the pointer. Only the
|
||||
// pointer's horizontal position matters: passing a column's middle moves the ghost past
|
||||
// it. Release to drop, Esc to cancel. Replaces the browser's native drag-and-drop.
|
||||
const colDrag = { active: null, pendingRender: false };
|
||||
|
||||
function setupColumnDrag() {
|
||||
const table = document.getElementById('table-custom');
|
||||
const host = document.getElementById('custom-table-host');
|
||||
if (!table || !host) return;
|
||||
const headers = Array.from(table.querySelectorAll('thead th.custom-th'));
|
||||
let from = null, marker = null;
|
||||
if (!table || table.dataset.pdrag) return;
|
||||
table.dataset.pdrag = '1';
|
||||
|
||||
const setDimmed = (idx, on) => {
|
||||
for (const row of table.rows) { const c = row.cells[idx]; if (c) c.classList.toggle('col-moving', on); }
|
||||
};
|
||||
const clear = () => {
|
||||
if (from !== null) setDimmed(from, false);
|
||||
if (marker) marker.remove();
|
||||
marker = null; from = null;
|
||||
};
|
||||
// Native drag-and-drop on the grips is replaced by the pointer drag below
|
||||
table.addEventListener('dragstart', e => { if (e.target.closest && e.target.closest('thead')) e.preventDefault(); }, true);
|
||||
|
||||
headers.forEach((th, idx) => {
|
||||
const grip = th.querySelector('.col-grip');
|
||||
if (!grip || grip.dataset.ghost) return;
|
||||
grip.dataset.ghost = '1';
|
||||
|
||||
grip.addEventListener('dragstart', e => {
|
||||
from = idx;
|
||||
const ghost = document.createElement('div');
|
||||
ghost.className = 'col-drag-ghost';
|
||||
const head = document.createElement('div');
|
||||
head.className = 'col-drag-ghost-head';
|
||||
head.textContent = th.querySelector('.label-text') ? th.querySelector('.label-text').textContent : th.textContent.trim();
|
||||
ghost.appendChild(head);
|
||||
Array.from(table.tBodies[0].rows).filter(r => r.style.display !== 'none').slice(0, 7).forEach(r => {
|
||||
const cell = document.createElement('div');
|
||||
cell.className = 'col-drag-ghost-cell';
|
||||
cell.textContent = r.cells[idx] ? r.cells[idx].textContent.trim() : '';
|
||||
ghost.appendChild(cell);
|
||||
});
|
||||
ghost.style.width = Math.min(280, Math.max(140, th.getBoundingClientRect().width)) + 'px';
|
||||
document.body.appendChild(ghost);
|
||||
try { e.dataTransfer.setDragImage(ghost, 24, 18); } catch (_) {}
|
||||
setTimeout(() => ghost.remove(), 0);
|
||||
setDimmed(idx, true);
|
||||
});
|
||||
grip.addEventListener('dragend', clear);
|
||||
|
||||
th.addEventListener('dragover', () => {
|
||||
if (from === null) return;
|
||||
if (!marker) { marker = document.createElement('div'); marker.className = 'col-drop-marker'; host.appendChild(marker); }
|
||||
// moveCustomColumn(from, to) lands the column after the target when moving right,
|
||||
// before it when moving left
|
||||
const hostBox = host.getBoundingClientRect(), box = th.getBoundingClientRect();
|
||||
const x = (idx > from ? box.right : box.left) - hostBox.left;
|
||||
marker.style.left = (x - 2) + 'px';
|
||||
marker.style.height = table.getBoundingClientRect().height + 'px';
|
||||
marker.hidden = idx === from;
|
||||
});
|
||||
th.addEventListener('drop', clear);
|
||||
table.addEventListener('pointerdown', e => {
|
||||
if (e.button !== 0 || cardsQuery.matches || colDrag.active) return;
|
||||
const th = e.target.closest('thead th.custom-th');
|
||||
if (!th || e.target.closest('.th-icons') || e.target.closest('.col-resize-handle')) return;
|
||||
const headers = Array.from(th.parentElement.cells);
|
||||
const from = headers.indexOf(th);
|
||||
if (from < 0) return;
|
||||
const st = { th, from, pos: from, x0: e.clientX, y0: e.clientY, started: false, cells: null, float: null };
|
||||
colDrag.active = st;
|
||||
const move = ev => onDragMove(ev, st, table);
|
||||
const up = ev => { finish(ev, st, table, false); };
|
||||
const key = ev => { if (ev.key === 'Escape' && st.started) finish(ev, st, table, true); };
|
||||
st.cleanup = () => {
|
||||
window.removeEventListener('pointermove', move);
|
||||
window.removeEventListener('pointerup', up);
|
||||
window.removeEventListener('pointercancel', up);
|
||||
window.removeEventListener('keydown', key, true);
|
||||
};
|
||||
window.addEventListener('pointermove', move);
|
||||
window.addEventListener('pointerup', up);
|
||||
window.addEventListener('pointercancel', up);
|
||||
window.addEventListener('keydown', key, true);
|
||||
});
|
||||
}
|
||||
|
||||
function onDragMove(e, st, table) {
|
||||
if (!st.started) {
|
||||
if (Math.hypot(e.clientX - st.x0, e.clientY - st.y0) < 6) return;
|
||||
st.started = true;
|
||||
st.cells = Array.from(table.rows).map(r => r.cells[st.from]).filter(Boolean);
|
||||
st.cells.forEach(c => c.classList.add('col-ghost'));
|
||||
st.float = document.createElement('div');
|
||||
st.float.className = 'col-drag-float';
|
||||
const label = st.th.querySelector('.label-text');
|
||||
st.float.textContent = label ? label.textContent : st.th.textContent.trim();
|
||||
document.body.appendChild(st.float);
|
||||
document.body.classList.add('col-dragging');
|
||||
const overlay = document.getElementById('custom-resize-overlay');
|
||||
if (overlay) overlay.style.visibility = 'hidden';
|
||||
}
|
||||
e.preventDefault();
|
||||
st.float.style.left = e.clientX + 'px';
|
||||
st.float.style.top = e.clientY + 'px';
|
||||
|
||||
// Nudge the sideways scrollbar when dragging near the edges of a wide table
|
||||
const bar = document.getElementById('custom-hscroll');
|
||||
const box = document.getElementById('scroll-custom');
|
||||
if (bar && !bar.hidden && box) {
|
||||
const r = box.getBoundingClientRect();
|
||||
if (e.clientX < r.left + 40) bar.scrollLeft -= 18;
|
||||
else if (e.clientX > r.right - 40) bar.scrollLeft += 18;
|
||||
}
|
||||
|
||||
// Target slot = number of other columns whose middle is left of the pointer
|
||||
const headerCells = Array.from(st.th.parentElement.cells).filter(c => c !== st.th);
|
||||
let pos = 0;
|
||||
for (const c of headerCells) { const b = c.getBoundingClientRect(); if (e.clientX > b.left + b.width / 2) pos++; }
|
||||
if (pos === st.pos) return;
|
||||
st.pos = pos;
|
||||
Array.from(table.rows).forEach((row, i) => {
|
||||
const cell = st.cells[i];
|
||||
if (!cell || cell.parentElement !== row) return;
|
||||
const others = Array.from(row.cells).filter(c => c !== cell);
|
||||
row.insertBefore(cell, others[pos] || null);
|
||||
});
|
||||
}
|
||||
|
||||
function finish(e, st, table, cancelled) {
|
||||
st.cleanup();
|
||||
colDrag.active = null;
|
||||
if (!st.started) return; // a click: let it sort as usual
|
||||
// swallow the click that follows a drag so it doesn't also sort
|
||||
const stop = ev => { ev.stopPropagation(); ev.preventDefault(); };
|
||||
window.addEventListener('click', stop, true);
|
||||
setTimeout(() => window.removeEventListener('click', stop, true), 0);
|
||||
|
||||
st.cells.forEach(c => c.classList.remove('col-ghost'));
|
||||
if (st.float) st.float.remove();
|
||||
document.body.classList.remove('col-dragging');
|
||||
const overlay = document.getElementById('custom-resize-overlay');
|
||||
if (overlay) overlay.style.visibility = '';
|
||||
|
||||
if (!cancelled && st.pos !== st.from) moveCustomColumn(st.from, st.pos); // re-renders in the new order
|
||||
else if (st.pos !== st.from || colDrag.pendingRender) renderCustomTable(); // put cells back
|
||||
colDrag.pendingRender = false;
|
||||
}
|
||||
|
||||
let queued = false;
|
||||
function run() {
|
||||
queued = false;
|
||||
@@ -225,11 +266,14 @@
|
||||
// Groups and pages are shown by toggling display (not a DOM change), so also
|
||||
// re-measure as the user scrolls; writes only happen when a value changes.
|
||||
window.addEventListener('scroll', schedule, { passive: true });
|
||||
// Custom View rebuilds its header (and re-attaches drag handlers) on every render
|
||||
if (typeof window.attachColDragHandlers === 'function') {
|
||||
const origDrag = window.attachColDragHandlers;
|
||||
window.attachColDragHandlers = function () { const r = origDrag.apply(this, arguments); enhanceColumnDrag(); return r; };
|
||||
enhanceColumnDrag();
|
||||
// Custom View column drag; a data refresh mid-drag waits until the drop
|
||||
setupColumnDrag();
|
||||
if (typeof window.renderCustomTable === 'function') {
|
||||
const origRender = window.renderCustomTable;
|
||||
window.renderCustomTable = function () {
|
||||
if (colDrag.active && colDrag.active.started) { colDrag.pendingRender = true; return; }
|
||||
return origRender.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
// Page switches show and hide pages without changing the DOM
|
||||
if (typeof window.switchPage === 'function') {
|
||||
|
||||
Reference in new issue
Block a user