feat: improve drag & drop
* permits multiple drag & drop * synchronize grid & list view on selection * permits copy during ddrag & drop (use sift/alt key according your OS) * use batch move / copy on drag & drop
This commit is contained in:
@@ -288,40 +288,13 @@ const contextMenus = {
|
||||
const fileIds = items.filter(i => i.type === 'file').map(i => i.id);
|
||||
const folderIds = items.filter(i => i.type === 'folder').map(i => i.id);
|
||||
|
||||
let success = 0, errors = 0;
|
||||
|
||||
try {
|
||||
// Batch copy files
|
||||
if (fileIds.length > 0) {
|
||||
const res = await fetch('/api/batch/files/copy', {
|
||||
method: 'POST',
|
||||
headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ file_ids: fileIds, target_folder_id: targetId })
|
||||
});
|
||||
const data = await res.json();
|
||||
success += data.stats?.successful || 0;
|
||||
errors += data.stats?.failed || 0;
|
||||
}
|
||||
|
||||
// Note: Folder copy is not yet implemented in batch API
|
||||
if (folderIds.length > 0) {
|
||||
window.ui.showNotification('Info', 'Folder copy is not yet supported in batch mode');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Batch copy error:', err);
|
||||
errors++;
|
||||
}
|
||||
|
||||
let result = await window.fileOps.batchCopy( fileIds, folderIds, targetId);
|
||||
|
||||
this.closeMoveDialog();
|
||||
window.multiSelect.clear();
|
||||
window.loadFiles();
|
||||
|
||||
if (errors > 0) {
|
||||
window.ui.showNotification('Batch copy', `${success} copied, ${errors} failed`);
|
||||
} else {
|
||||
window.ui.showNotification('Items copied',
|
||||
`${success} item${success !== 1 ? 's' : ''} copied successfully`);
|
||||
}
|
||||
window.multiSelect.showBatchResult( "copy", result);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -353,48 +326,14 @@ const contextMenus = {
|
||||
|
||||
const fileIds = items.filter(i => i.type === 'file').map(i => i.id);
|
||||
const folderIds = items.filter(i => i.type === 'folder' && i.id !== targetId).map(i => i.id);
|
||||
|
||||
let success = 0, errors = 0;
|
||||
|
||||
try {
|
||||
// Batch move files in a single request
|
||||
if (fileIds.length > 0) {
|
||||
const res = await fetch('/api/batch/files/move', {
|
||||
method: 'POST',
|
||||
headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ file_ids: fileIds, target_folder_id: targetId })
|
||||
});
|
||||
const data = await res.json();
|
||||
success += data.stats?.successful || 0;
|
||||
errors += data.stats?.failed || 0;
|
||||
}
|
||||
|
||||
// Batch move folders in a single request
|
||||
if (folderIds.length > 0) {
|
||||
const res = await fetch('/api/batch/folders/move', {
|
||||
method: 'POST',
|
||||
headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ folder_ids: folderIds, target_folder_id: targetId })
|
||||
});
|
||||
const data = await res.json();
|
||||
success += data.stats?.successful || 0;
|
||||
errors += data.stats?.failed || 0;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Batch move error:', err);
|
||||
errors++;
|
||||
}
|
||||
|
||||
|
||||
let result = await window.fileOps.batchMove( fileIds, folderIds, targetId);
|
||||
|
||||
this.closeMoveDialog();
|
||||
window.multiSelect.clear();
|
||||
window.loadFiles();
|
||||
|
||||
if (errors > 0) {
|
||||
window.ui.showNotification('Batch move', `${success} moved, ${errors} failed`);
|
||||
} else {
|
||||
window.ui.showNotification('Items moved',
|
||||
`${success} item${success !== 1 ? 's' : ''} moved successfully`);
|
||||
}
|
||||
window.multiSelect.showBatchResult( "move", result);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -771,6 +771,60 @@ const fileOps = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @typedef {Object} BatchResult
|
||||
* @property {number} success number of files|folders sucessfully updated
|
||||
* @property {number} errors number of files|folders in error
|
||||
* /
|
||||
|
||||
/**
|
||||
* Move files & folders
|
||||
* @param {string[]} fileIds - File IDs
|
||||
* @param {string[]} folderIds - Folder IDs
|
||||
* @param {string} targetFolderId - Target folder ID
|
||||
* @returns {Promise<BatchResult>} - Success status
|
||||
*/
|
||||
async batchMove(fileIds, folderIds, targetFolderId) {
|
||||
|
||||
// TODO ensure not moving a folder into itself
|
||||
let success = 0, errors = 0;
|
||||
|
||||
try {
|
||||
// Batch move files in a single request
|
||||
if (fileIds.length > 0) {
|
||||
const res = await fetch('/api/batch/files/move', {
|
||||
method: 'POST',
|
||||
headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ file_ids: fileIds, target_folder_id: targetFolderId })
|
||||
});
|
||||
const data = await res.json();
|
||||
success += data.stats?.successful || 0;
|
||||
errors += data.stats?.failed || 0;
|
||||
}
|
||||
|
||||
// Batch move folders in a single request
|
||||
if (folderIds.length > 0) {
|
||||
const res = await fetch('/api/batch/folders/move', {
|
||||
method: 'POST',
|
||||
headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ folder_ids: folderIds, target_folder_id: targetFolderId })
|
||||
});
|
||||
const data = await res.json();
|
||||
success += data.stats?.successful || 0;
|
||||
errors += data.stats?.failed || 0;
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Batch move error:', err);
|
||||
errors++;
|
||||
}
|
||||
|
||||
return {
|
||||
success,
|
||||
errors
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Copy a file to another folder
|
||||
* @param {string} fileId - File ID
|
||||
@@ -828,6 +882,47 @@ const fileOps = {
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Copy files & folders
|
||||
* @param {string[]} fileIds - File IDs
|
||||
* @param {string[]} folderIds - Folder IDs
|
||||
* @param {string} targetFolderId - Target folder ID
|
||||
* @returns {Promise<boolean>} - Success status
|
||||
*/
|
||||
async batchCopy(fileIds, folderIds, targetFolderId) {
|
||||
|
||||
// FIXME ensure not moving a folder into itself
|
||||
|
||||
let success = 0, errors = 0;
|
||||
try {
|
||||
// Batch copy files
|
||||
if (fileIds.length > 0) {
|
||||
const res = await fetch('/api/batch/files/copy', {
|
||||
method: 'POST',
|
||||
headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ file_ids: fileIds, target_folder_id: targetFolderId })
|
||||
});
|
||||
const data = await res.json();
|
||||
success += data.stats?.successful || 0;
|
||||
errors += data.stats?.failed || 0;
|
||||
}
|
||||
|
||||
// Note: Folder copy is not yet implemented in batch API
|
||||
if (folderIds.length > 0) {
|
||||
window.ui.showNotification('Info', 'Folder copy is not yet supported in batch mode');
|
||||
errors += folderIds.lenngth;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Batch copy error:', err);
|
||||
errors++;
|
||||
}
|
||||
|
||||
return {
|
||||
success,
|
||||
errors
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Rename a file
|
||||
* @param {string} fileId - File ID
|
||||
|
||||
@@ -76,6 +76,61 @@ const multiSelect = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @typedef {Object} ItemSelection
|
||||
* @property {string[]} fileIds list of files' id
|
||||
* @property {string[]} folderIds list of folders' id
|
||||
*/
|
||||
|
||||
/**
|
||||
* get selection
|
||||
* @param {string} [targtFolderId] an optional targget (will be removed from selected item)
|
||||
* @return {ItemSelection}
|
||||
*/
|
||||
getSelection(targtFolderId) {
|
||||
let fileIds=[];
|
||||
let folderIds=[];
|
||||
|
||||
// TODO optimize & check if _selected is a better use
|
||||
document.querySelectorAll(`div.file-item.selected`).forEach( (item) => {
|
||||
if (item.dataset.fileId) {
|
||||
fileIds.push(item.dataset.fileId);
|
||||
}
|
||||
else {
|
||||
// ignore selectedItem if this is the target
|
||||
if (targtFolderId && targtFolderId !== item.dataset.folderId)
|
||||
folderIds.push(item.dataset.folderId);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
"fileIds": fileIds,
|
||||
"folderIds": folderIds,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} action move|copy
|
||||
* @param {BatchResult} result result of batch
|
||||
*/
|
||||
showBatchResult(action, result) {
|
||||
if (action === "copy") {
|
||||
if (result.errors > 0) {
|
||||
window.ui.showNotification('Batch copy', `${result.success} copied, ${result.errors} failed`);
|
||||
} else {
|
||||
window.ui.showNotification('Items copied',
|
||||
`${result.success} item${result.success !== 1 ? 's' : ''} copied successfully`);
|
||||
}
|
||||
} else {
|
||||
if (result.errors > 0) {
|
||||
window.ui.showNotification('Batch move', `${result.success} moved, ${result.errors} failed`);
|
||||
} else {
|
||||
window.ui.showNotification('Items moved',
|
||||
`${result.success} item${result.success !== 1 ? 's' : ''} moved successfully`);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// ── DOM helpers ─────────────────────────────────────────
|
||||
|
||||
_selectElement(el) {
|
||||
@@ -94,13 +149,6 @@ const multiSelect = {
|
||||
|
||||
_getAllVisibleItems() {
|
||||
return [...document.querySelectorAll('.file-item, .file-card')];
|
||||
/*
|
||||
const grid = document.getElementById('files-grid');
|
||||
if (grid && grid.style.display !== 'none') {
|
||||
return [...grid.querySelectorAll('.file-card')];
|
||||
}
|
||||
return [...document.querySelectorAll('#files-list-view .file-item')];
|
||||
*/
|
||||
},
|
||||
|
||||
_extractInfo(el) {
|
||||
@@ -192,6 +240,7 @@ const multiSelect = {
|
||||
const n = this._selected.size;
|
||||
|
||||
const batchSelectionBar = document.getElementById('batch-selection-bar');
|
||||
const actionsBar = document.getElementById('actions-bar');
|
||||
|
||||
if (n > 0) {
|
||||
this._barVisible = true;
|
||||
@@ -201,14 +250,18 @@ const multiSelect = {
|
||||
: (this._t('batch.n_selected', { count: n }) || `${n} items selected`);
|
||||
document.getElementById("batch-bar-count").innerText = countText;
|
||||
|
||||
batchSelectionBar.classList.add('visible');
|
||||
actionsBar.classList.add('hidden');
|
||||
|
||||
batchSelectionBar.classList.remove('hidden');
|
||||
|
||||
} else {
|
||||
this._barVisible = false;
|
||||
|
||||
// Hide grid bar
|
||||
batchSelectionBar.classList.remove('visible');
|
||||
batchSelectionBar.classList.add('hidden');
|
||||
|
||||
if (actionsBar.dataset.mode !== "hidden")
|
||||
actionsBar.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Sync individual item checkboxes
|
||||
@@ -425,9 +478,6 @@ const multiSelect = {
|
||||
// Wire the initial select-all checkbox
|
||||
this._injectListHeaderCheckbox();
|
||||
|
||||
// Global deselect on empty-area click
|
||||
this._hookGlobalDeselect();
|
||||
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.target.closest('input, textarea, [contenteditable], .rename-dialog, .share-dialog, .confirm-dialog')) return;
|
||||
@@ -459,13 +509,6 @@ const multiSelect = {
|
||||
cb.addEventListener('change', () => this.toggleAll());
|
||||
},
|
||||
|
||||
_hookGlobalDeselect() {
|
||||
document.addEventListener('click', (e) => {
|
||||
if (window.__rubberBandJustFinished) return;
|
||||
if (e.target.closest('.file-card, .file-item, .context-menu, .batch-selection-bar, .list-header.selection-mode, .about-modal, .rename-dialog, .share-dialog, .confirm-dialog, .modal-overlay, input, button')) return;
|
||||
if (this.hasSelection) this.clear();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Expose globally
|
||||
|
||||
Reference in New Issue
Block a user