plugins(files): Feature 7 — Drag-and-Drop

- Items are draggable (draggable='true')
- Multi-file drag from selection (selectedPaths)
- Drop on folder moves files via api.files.move()
- Visual feedback: .files-dragging (opacity) + .files-drag-over (dashed outline)
- No-op when source === target folder
This commit is contained in:
mirivlad 2026-06-20 19:55:20 +08:00
parent e74499a329
commit e5c63576d0
1 changed files with 65 additions and 2 deletions

View File

@ -63,7 +63,9 @@
'.files-modal-btn.confirm{background:#4ecca3;color:#111;border-color:#4ecca3}',
'.files-modal-btn.confirm:hover{background:#3dbb92}',
'.files-modal-btn.danger{background:#e74c3c;color:#fff;border-color:#e74c3c}',
'.files-modal-btn.danger:hover{background:#c0392b}'
'.files-modal-btn.danger:hover{background:#c0392b}',
'.files-dragging{opacity:.5}',
'.files-drag-over{outline:2px dashed #4ecca3;outline-offset:-2px;background:rgba(78,204,163,.08)}'
].join('\n');
function el(tag, attrs, children) {
@ -375,9 +377,26 @@
'data-file-name': entry.name,
'data-file-type': entry.type,
'data-file-path': entry.relativePath,
draggable: 'true',
tabindex: '0',
onClick: function (e) { selectEntry(entry, e); },
onDblclick: function () { openEntry(entry); }
onDblclick: function () { openEntry(entry); },
onDragstart: function (e) {
var paths = [];
if (selectedPaths[entry.relativePath] && selectedCount() > 1) {
paths = Object.keys(selectedPaths);
} else {
selectedPaths = {};
selectedPaths[entry.relativePath] = true;
lastClickedPath = entry.relativePath;
renderList();
paths = [entry.relativePath];
}
e.dataTransfer.setData('application/files-paths', JSON.stringify(paths));
e.dataTransfer.effectAllowed = 'move';
row.classList.add('files-dragging');
},
onDragend: function () { row.classList.remove('files-dragging'); }
}, [
el('div', { className: 'files-namecell' }, [
el('span', { className: 'files-item-icon', innerHTML: fileIcon(entry) }),
@ -738,6 +757,50 @@
}
});
function moveFiles(sourcePaths, targetDirPath) {
var promises = sourcePaths.filter(function (p) { return parentPath(p) !== targetDirPath; }).map(function (p) {
var name = baseName(p);
var to = targetDirPath ? targetDirPath + '/' + name : name;
return api.files.move(p, to, { overwrite: false });
});
if (promises.length === 0) return Promise.resolve();
return Promise.allSettled(promises).then(function () { loadEntries(); });
}
listContainer.addEventListener('dragover', function (e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
var row = e.target.closest('.files-item');
if (row) {
row.classList.add('files-drag-over');
} else {
listContainer.classList.add('files-drag-over');
}
});
listContainer.addEventListener('dragleave', function (e) {
var row = e.target.closest('.files-item');
if (row) row.classList.remove('files-drag-over');
if (!row) listContainer.classList.remove('files-drag-over');
});
listContainer.addEventListener('drop', function (e) {
e.preventDefault();
listContainer.classList.remove('files-drag-over');
var rows = listContainer.querySelectorAll('.files-drag-over');
for (var i = 0; i < rows.length; i++) rows[i].classList.remove('files-drag-over');
var raw = e.dataTransfer.getData('application/files-paths');
if (!raw) return;
var sourcePaths;
try { sourcePaths = JSON.parse(raw); } catch (err) { return; }
if (!sourcePaths || !sourcePaths.length) return;
var row = e.target.closest('.files-item');
if (row && row.getAttribute('data-file-type') === 'folder') {
var targetRel = row.getAttribute('data-file-path');
moveFiles(sourcePaths, targetRel);
}
});
containerEl.addEventListener('keydown', function (event) {
if (event.target && ['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].indexOf(event.target.tagName) !== -1) return;
if (event.key === 'Enter') openEntry(selectedEntry());