Use pointer based internal drag and drop

This commit is contained in:
Mikei386
2026-06-23 06:42:04 +02:00
parent cd284e8a72
commit e0a61c074e
6 changed files with 153 additions and 35 deletions
+134 -30
View File
@@ -8,6 +8,8 @@ const state = {
selectedEntry: null,
contextMenu: null,
draggedEntry: null,
pointerDrag: null,
suppressClickPath: null,
jobs: new Map(),
nextWindowId: 1,
zIndex: 20
@@ -233,41 +235,16 @@ function renderExplorer(body, win) {
for (const row of body.querySelectorAll('.file-row')) {
const entry = data.entries.find((item) => item.path === row.dataset.path);
row.addEventListener('pointerdown', () => lockExplorerScroll(win, body));
row.addEventListener('pointerdown', (event) => {
lockExplorerScroll(win, body);
startItemPointerDrag(event, win, entry);
});
row.addEventListener('click', () => selectEntry(win, entry, body));
row.addEventListener('dblclick', () => {
if (entry.type === 'directory') {
loadExplorer(win, entry.path);
}
});
row.addEventListener('dragstart', (event) => {
state.draggedEntry = { path: entry.path, name: entry.name, sourceWindowId: win.id };
setEffectAllowed(event);
row.classList.add('dragging');
});
row.addEventListener('dragend', () => {
row.classList.remove('dragging');
setTimeout(() => {
state.draggedEntry = null;
}, 0);
});
row.addEventListener('dragover', (event) => {
if (entry.type === 'directory') {
event.preventDefault();
event.stopPropagation();
setDropEffect(event, event.altKey ? 'copy' : 'move');
}
});
row.addEventListener('drop', async (event) => {
if (entry.type !== 'directory') return;
event.preventDefault();
event.stopPropagation();
try {
await handleDrop(event, win, entry.path);
} catch (error) {
showError(error);
}
});
row.addEventListener('contextmenu', (event) => {
event.preventDefault();
event.stopPropagation();
@@ -283,7 +260,7 @@ function renderFileTable(entries) {
}
const rows = entries.map((entry) => `
<tr class="file-row ${entry.path === state.selectedEntry?.path ? 'selected' : ''}" draggable="true" data-path="${escapeAttr(entry.path)}">
<tr class="file-row ${entry.path === state.selectedEntry?.path ? 'selected' : ''}" data-path="${escapeAttr(entry.path)}" data-type="${escapeAttr(entry.type)}">
<td><span class="name-cell"><span class="badge">${icons[entry.type] || 'Item'}</span>${escapeHtml(entry.name)}</span></td>
<td>${entry.type}</td>
<td>${entry.type === 'file' ? formatBytes(entry.size) : ''}</td>
@@ -528,6 +505,11 @@ async function pollJob(id) {
}
function selectEntry(win, entry, scrollBody = null) {
if (state.suppressClickPath === entry.path) {
state.suppressClickPath = null;
restoreExplorerScroll(win, scrollBody);
return;
}
win.data.selectedPath = entry.path;
state.selectedEntry = entry;
focusWindow(win);
@@ -563,6 +545,121 @@ function updatePropertiesWindows() {
}
}
function startItemPointerDrag(event, win, entry) {
if (event.button !== 0) return;
state.pointerDrag = {
sourceWindowId: win.id,
path: entry.path,
name: entry.name,
startX: event.clientX,
startY: event.clientY,
active: false,
targetWindowId: null,
targetPath: null
};
const move = (moveEvent) => updateItemPointerDrag(moveEvent);
const up = (upEvent) => finishItemPointerDrag(upEvent, move, up);
document.addEventListener('pointermove', move);
document.addEventListener('pointerup', up, { once: true });
}
function updateItemPointerDrag(event) {
const drag = state.pointerDrag;
if (!drag) return;
const distance = Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY);
if (!drag.active && distance < 6) {
return;
}
drag.active = true;
event.preventDefault();
document.body.classList.add('u-nav-dragging');
markDraggingRow(drag.path, true);
updatePointerDropTarget(event);
}
async function finishItemPointerDrag(event, move, up) {
document.removeEventListener('pointermove', move);
document.removeEventListener('pointerup', up);
const drag = state.pointerDrag;
clearPointerDropTarget();
document.body.classList.remove('u-nav-dragging');
if (!drag) return;
markDraggingRow(drag.path, false);
state.pointerDrag = null;
if (!drag.active) {
return;
}
state.suppressClickPath = drag.path;
const target = findPointerDropTarget(event);
if (!target) {
return;
}
try {
const destination = `${target.path.replace(/\/$/, '')}/${drag.name}`;
if (destination === drag.path) {
throw new Error('Quelle und Ziel sind identisch.');
}
await createJob(event.altKey ? 'copy' : 'move', drag.path, destination);
await reloadExplorerWindows([drag.sourceWindowId, target.windowId]);
} catch (error) {
showError(error);
}
}
function updatePointerDropTarget(event) {
clearPointerDropTarget();
const target = findPointerDropTarget(event);
if (!target) return;
state.pointerDrag.targetWindowId = target.windowId;
state.pointerDrag.targetPath = target.path;
const windowEl = workspace.querySelector(`.window[data-window-id="${target.windowId}"]`);
windowEl?.querySelector('.drop-zone')?.classList.add('drag-over');
if (target.row) {
target.row.classList.add('drop-target');
}
}
function clearPointerDropTarget() {
for (const item of workspace.querySelectorAll('.drop-zone.drag-over')) {
item.classList.remove('drag-over');
}
for (const item of workspace.querySelectorAll('.file-row.drop-target')) {
item.classList.remove('drop-target');
}
}
function findPointerDropTarget(event) {
const element = document.elementFromPoint(event.clientX, event.clientY);
const windowEl = element?.closest?.('.window');
if (!windowEl) return null;
const win = state.windows.find((item) => item.id === windowEl.dataset.windowId);
if (!win || win.kind !== 'explorer') return null;
const row = element.closest?.('.file-row');
if (row?.dataset.type === 'directory') {
return { windowId: win.id, path: row.dataset.path, row };
}
return { windowId: win.id, path: win.data.path, row: null };
}
function markDraggingRow(path, dragging) {
for (const row of workspace.querySelectorAll(`.file-row[data-path="${cssEscape(path)}"]`)) {
row.classList.toggle('dragging', dragging);
}
}
function lockExplorerScroll(win, body) {
win.data.lockScroll = true;
win.data.lockScrollTop = body.scrollTop;
@@ -695,6 +792,13 @@ function escapeAttr(value) {
return escapeHtml(value);
}
function cssEscape(value) {
if (window.CSS?.escape) {
return CSS.escape(String(value));
}
return String(value).replace(/["\\]/g, '\\$&');
}
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
+16 -2
View File
@@ -229,16 +229,30 @@
.u-nav .file-row:hover,
.u-nav .file-row.selected,
.u-nav .file-row.dragging {
.u-nav .file-row.dragging,
.u-nav .file-row.drop-target {
background: var(--surface-2);
}
.u-nav .file-row:hover td,
.u-nav .file-row.selected td,
.u-nav .file-row.dragging td {
.u-nav .file-row.dragging td,
.u-nav .file-row.drop-target td {
background: var(--surface-2) !important;
}
.u-nav .file-row {
user-select: none;
}
.u-nav .file-row.dragging {
opacity: 0.55;
}
.u-nav-dragging {
cursor: grabbing;
}
.u-nav .name-cell {
align-items: center;
display: flex;