plugins(files): Feature 6 — Multi-select

- Ctrl+click: toggle single file selection
- Shift+click: range select from last clicked to current
- Ctrl+A: select all visible entries
- Click on empty space: deselect all
- Bulk trash: delete all selected files via Promise.allSettled
- Open/Rename disabled when multiple selected; Trash enabled for bulk
- Refactored: selectedPath (string) → selectedPaths (hash map)
This commit is contained in:
mirivlad 2026-06-20 19:51:13 +08:00
parent 281614e1b4
commit e74499a329
1 changed files with 88 additions and 19 deletions

View File

@ -180,7 +180,8 @@
var workspaceName = workspaceRoot || (workspaceNode && (workspaceNode.name || workspaceNode.title || workspaceNode.id)) || 'Workspace'; var workspaceName = workspaceRoot || (workspaceNode && (workspaceNode.name || workspaceNode.title || workspaceNode.id)) || 'Workspace';
var currentPath = ''; var currentPath = '';
var entries = []; var entries = [];
var selectedPath = ''; var selectedPaths = {};
var lastClickedPath = '';
var filterText = ''; var filterText = '';
var sortMode = 'folder-name'; var sortMode = 'folder-name';
var createMode = ''; var createMode = '';
@ -244,15 +245,21 @@
containerEl.appendChild(renamePanel); containerEl.appendChild(renamePanel);
function selectedEntry() { function selectedEntry() {
return entries.find(function (entry) { return entry.relativePath === selectedPath; }) || null; var keys = Object.keys(selectedPaths);
if (keys.length === 0) return null;
return entries.find(function (entry) { return entry.relativePath === keys[0]; }) || null;
}
function selectedCount() {
return Object.keys(selectedPaths).length;
} }
function updateButtons() { function updateButtons() {
var sel = selectedEntry(); var count = selectedCount();
upBtn.disabled = !currentPath; upBtn.disabled = !currentPath;
openBtn.disabled = !sel; openBtn.disabled = count !== 1;
renameBtn.disabled = !sel; renameBtn.disabled = count !== 1;
trashBtn.disabled = !sel; trashBtn.disabled = count === 0;
pasteBtn.disabled = !window.__filesClipboard; pasteBtn.disabled = !window.__filesClipboard;
} }
@ -305,8 +312,42 @@
return out; return out;
} }
function selectEntry(entry) { function selectEntry(entry, event) {
selectedPath = entry ? entry.relativePath : ''; if (!entry) {
selectedPaths = {};
lastClickedPath = '';
renderList();
return;
}
var targetPath = entry.relativePath;
var visible = visibleEntries();
if (event && (event.ctrlKey || event.metaKey)) {
if (selectedPaths[targetPath]) {
delete selectedPaths[targetPath];
} else {
selectedPaths[targetPath] = true;
}
lastClickedPath = targetPath;
} else if (event && event.shiftKey && lastClickedPath) {
var lastIdx = -1;
var targetIdx = -1;
for (var i = 0; i < visible.length; i++) {
if (visible[i].relativePath === lastClickedPath) lastIdx = i;
if (visible[i].relativePath === targetPath) targetIdx = i;
}
if (lastIdx !== -1 && targetIdx !== -1) {
var lo = Math.min(lastIdx, targetIdx);
var hi = Math.max(lastIdx, targetIdx);
for (var j = lo; j <= hi; j++) {
selectedPaths[visible[j].relativePath] = true;
}
}
} else {
selectedPaths = {};
selectedPaths[targetPath] = true;
lastClickedPath = targetPath;
}
renderList(); renderList();
} }
@ -330,12 +371,12 @@
shown.forEach(function (entry) { shown.forEach(function (entry) {
var row = el('div', { var row = el('div', {
className: 'files-item' + (entry.relativePath === selectedPath ? ' selected' : ''), className: 'files-item' + (selectedPaths[entry.relativePath] ? ' selected' : ''),
'data-file-name': entry.name, 'data-file-name': entry.name,
'data-file-type': entry.type, 'data-file-type': entry.type,
'data-file-path': entry.relativePath, 'data-file-path': entry.relativePath,
tabindex: '0', tabindex: '0',
onClick: function () { selectEntry(entry); }, onClick: function (e) { selectEntry(entry, e); },
onDblclick: function () { openEntry(entry); } onDblclick: function () { openEntry(entry); }
}, [ }, [
el('div', { className: 'files-namecell' }, [ el('div', { className: 'files-namecell' }, [
@ -358,7 +399,8 @@
} }
function loadEntries() { function loadEntries() {
selectedPath = ''; selectedPaths = {};
lastClickedPath = '';
listContainer.innerHTML = ''; listContainer.innerHTML = '';
listContainer.appendChild(el('div', { className: 'files-loading' }, ['Loading...'])); listContainer.appendChild(el('div', { className: 'files-loading' }, ['Loading...']));
updateBreadcrumb(); updateBreadcrumb();
@ -482,14 +524,27 @@
} }
function trashEntry(entry) { function trashEntry(entry) {
entry = entry || selectedEntry(); var count = selectedCount();
if (!entry) return; if (entry) {
confirmModal('Move "' + entry.name + '" to trash?', { danger: true }).then(function (ok) { confirmModal('Move "' + entry.name + '" to trash?', { danger: true }).then(function (ok) {
if (!ok) return; if (!ok) return;
api.files.trash(entry.relativePath).then(function () { api.files.trash(entry.relativePath).then(function () {
loadEntries(); loadEntries();
}).catch(function (err) { window.alert((err && err.message) ? err.message : String(err)); }); }).catch(function (err) { window.alert((err && err.message) ? err.message : String(err)); });
}); });
} else if (count > 1) {
confirmModal('Move ' + count + ' items to trash?', { danger: true }).then(function (ok) {
if (!ok) return;
var paths = Object.keys(selectedPaths);
Promise.allSettled(paths.map(function (p) { return api.files.trash(p); })).then(function () {
loadEntries();
});
});
} else {
var single = selectedEntry();
if (!single) return;
trashEntry(single);
}
} }
refreshBtn.addEventListener('click', loadEntries); refreshBtn.addEventListener('click', loadEntries);
@ -677,11 +732,25 @@
showCtxMenu(e.clientX, e.clientY, entry); showCtxMenu(e.clientX, e.clientY, entry);
}); });
listContainer.addEventListener('click', function (e) {
if (!e.target.closest('.files-item')) {
selectEntry(null);
}
});
containerEl.addEventListener('keydown', function (event) { containerEl.addEventListener('keydown', function (event) {
if (event.target && ['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].indexOf(event.target.tagName) !== -1) return; if (event.target && ['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].indexOf(event.target.tagName) !== -1) return;
if (event.key === 'Enter') openEntry(selectedEntry()); if (event.key === 'Enter') openEntry(selectedEntry());
if (event.key === 'Delete' || event.key === 'Backspace') trashEntry(); if (event.key === 'Delete' || event.key === 'Backspace') trashEntry();
if (event.key === 'F2') beginRename(); if (event.key === 'F2') beginRename();
if (event.key === 'a' && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
var vis = visibleEntries();
selectedPaths = {};
vis.forEach(function (entry) { selectedPaths[entry.relativePath] = true; });
lastClickedPath = vis.length > 0 ? vis[vis.length - 1].relativePath : '';
renderList();
}
}); });
loadEntries(); loadEntries();