diff --git a/static/css/components/multiSelect.css b/static/css/components/multiSelect.css index 0416753c..39c48cd6 100644 --- a/static/css/components/multiSelect.css +++ b/static/css/components/multiSelect.css @@ -43,21 +43,11 @@ padding: 10px 20px; border-radius: 12px; margin: 0 0 12px; - opacity: 0; - max-height: 0; + height: 60px; overflow: hidden; transform: translateY(-8px); transition: opacity 0.2s, max-height 0.25s, transform 0.2s, margin 0.2s, padding 0.2s; - pointer-events: none; -} - -.batch-selection-bar.visible { - opacity: 1; - max-height: 60px; - transform: translateY(0); pointer-events: auto; - padding: 10px 20px; - margin: 0 0 12px; } .batch-bar-left { diff --git a/static/css/layout/content.css b/static/css/layout/content.css index 061954d6..f4685c96 100644 --- a/static/css/layout/content.css +++ b/static/css/layout/content.css @@ -16,7 +16,9 @@ .actions-bar { display: flex; justify-content: space-between; - margin-bottom: 20px; + margin: 0 0 12px; + height: 60px; + padding: 10px; } .action-buttons { @@ -36,3 +38,51 @@ height: 300px; grid-column: 1 / -1; } + +/* invisible element, permits building of drag element without altering display */ +.drag-preview { + position: absolute; + top: -9999px; + left: -9999px; + pointer-events: none; + width: 360px; +} + +.dragged-items > div { + position: relative; + display: inline-block; + padding: 4px; + width: 360px; + background-color: white; + height: 46px; +} + +.dragged-items > div.fading { + -webkit-mask-image: linear-gradient(to bottom, white 20%, transparent); + mask-image: linear-gradient(to bottom, white 20%, transparent); +} + +.dragged-items-badge { + position: absolute; + top: 0; + right: 0; + + transform: translate(50%, -50%); + + background: red; + color: white; + + border-radius: 50%; + min-width: 20px; + height: 20px; + + display: flex; + align-items: center; + justify-content: center; + + font-size: 12px; + font-weight: bold; + + padding: 2px; +} + diff --git a/static/index.html b/static/index.html index 04e3de44..3989ff6b 100644 --- a/static/index.html +++ b/static/index.html @@ -204,7 +204,7 @@

Files

-
+
+ +

Drag files here or click to select

@@ -255,8 +257,6 @@ Home
-
-
diff --git a/static/js/app/main.js b/static/js/app/main.js index 8427b411..6f39aab4 100644 --- a/static/js/app/main.js +++ b/static/js/app/main.js @@ -105,8 +105,9 @@ function setActionsBarMode(mode, force = false) { if (!elements.actionsBar) return; if (mode === 'hidden') { - elements.actionsBar.style.display = 'none'; + elements.actionsBar.classList.add("hidden"); elements.actionsBar.dataset.mode = 'hidden'; + console.log("......setup actions bar to hidden"); return; } @@ -118,7 +119,7 @@ function setActionsBarMode(mode, force = false) { if (!html) return; elements.actionsBar.innerHTML = html; - elements.actionsBar.style.display = 'flex'; + elements.actionsBar.classList.remove("hidden"); elements.actionsBar.dataset.mode = mode; // Refresh cached action elements after rebuild @@ -414,7 +415,7 @@ function cacheElements() { elements.listViewBtn = document.getElementById('list-view-btn'); elements.breadcrumb = document.querySelector('.breadcrumb'); elements.pageTitle = document.querySelector('.page-title'); - elements.actionsBar = document.querySelector('.actions-bar'); + elements.actionsBar = document.getElementById('actions-bar'); elements.navItems = document.querySelectorAll('.nav-item'); elements.searchInput = document.querySelector('.search-container input'); } @@ -647,16 +648,6 @@ function setupEventListeners() { !fileMenu.contains(e.target)) { ui.closeFileContextMenu(); } - - // Deselect all cards when clicking empty area (not on a card, menu, or modal) - // Note: multiSelect._hookGlobalDeselect() handles clearing the internal - // selection state; this handler only covers the legacy CSS class removal. - // Skip if a rubber-band selection just finished — the click is a side-effect. - if (window.__rubberBandJustFinished) return; - if (!e.target.closest('.file-card') && !e.target.closest('.file-item') && !e.target.closest('.context-menu') && !e.target.closest('.about-modal') && !e.target.closest('.batch-selection-bar') && !e.target.closest('.list-header.selection-mode')) { - document.querySelectorAll('.file-card.selected').forEach(c => c.classList.remove('selected')); - document.querySelectorAll('.file-item.selected').forEach(c => c.classList.remove('selected')); - } }); } diff --git a/static/js/app/navigation.js b/static/js/app/navigation.js index 3b519658..d1d8e98c 100644 --- a/static/js/app/navigation.js +++ b/static/js/app/navigation.js @@ -182,6 +182,8 @@ function switchToSharedSection() { window.sharedView.init(); window.sharedView.show(); } + if (window.multiSelect) window.multiSelect.clear(); + } function switchToFilesSection() { @@ -208,6 +210,7 @@ function switchToFilesSection() { window.app.currentPath = window.app.userHomeFolderId || ''; window.app.breadcrumbPath = []; window.ui.updateBreadcrumb(); + if (window.multiSelect) window.multiSelect.clear(); window.loadFiles(); } @@ -246,6 +249,9 @@ function switchToFavoritesSection() { `; } } + + if (window.multiSelect) window.multiSelect.clear(); + } function switchToRecentFilesSection() { @@ -282,6 +288,7 @@ function switchToRecentFilesSection() { `; } } + if (window.multiSelect) window.multiSelect.clear(); } function switchToPhotosSection() { @@ -307,6 +314,8 @@ function switchToPhotosSection() { if (window.photosView) { window.photosView.show(); } + if (window.multiSelect) window.multiSelect.clear(); + } function switchToTrashSection() { diff --git a/static/js/app/ui.js b/static/js/app/ui.js index 37bf9a93..894d17ac 100644 --- a/static/js/app/ui.js +++ b/static/js/app/ui.js @@ -3,6 +3,8 @@ * This file handles UI-related functions, view toggling, and interface interactions */ +// @ts-check + // UI Module const ui = { /** @@ -278,6 +280,12 @@ const ui = { * Set up drag and drop functionality */ setupDragAndDrop() { + // prepare area to build dragged elements + this.dragPreview = document.createElement("div"); + this.dragPreview.className="drag-preview"; + document.body.appendChild(this.dragPreview); + this.draggedItems = null; + const dropzone = document.getElementById('dropzone'); const collectDroppedEntries = async (dataTransfer) => { @@ -577,11 +585,8 @@ const ui = { e.preventDefault(); card.classList.remove('drop-target'); - const id = e.dataTransfer.getData('text/plain'); - const isFolder = - e.dataTransfer.getData('application/oxicloud-folder') === 'true'; - - await self.move( id, isFolder, targetFolderId); + const action = e.dataTransfer?.dropEffect; + await self._dropToFolder( action, targetFolderId, e.dataTransfer ); }); } else { // Last segment: current location, not clickable @@ -591,32 +596,6 @@ const ui = { }); }, - - // TODO: support multiple elements to move ? (API does) - /** - * proceed to the drag & drop - * @param {string} sourceId to move (can be a uniq object or) - * @param {boolean} sourceIsAFolder - * @param {string} targetFolderId - */ - async move(sourceId, sourceIsAFolder, targetFolderId) { - if (!sourceId) return; - - console.log(`request move ${ sourceIsAFolder ? "folder": "file"} ${sourceId} to folder ${targetFolderId}`); - - if (sourceIsAFolder) { - if (sourceId === targetFolderId) { - alert("You cannot move a folder to itself"); - return; - } - // TODO: handle errors... - await fileOps.moveFolder(sourceId, targetFolderId); - } else { - // TODO: handle errors... - await fileOps.moveFile(sourceId, targetFolderId); - } - }, - /** * Check if a file can be previewed in the viewer * @param {Object} file - File object with mime_type property @@ -742,6 +721,66 @@ const ui = { } }, + /** + * handle the drop + * @param {string} action copy|move + * @param {string} targetFolderId the target + * @param {any} dataTransfer fallback if nothing is selected + */ + async _dropToFolder(action, targetFolderId, dataTransfer) { + let selection = window.multiSelect.getSelection(targetFolderId); + + window.multiSelect.clear(); + + if (selection.fileIds.length == 0 && selection.folderIds.length == 0) { + + // try to use dataTransfer (direct move without selection) + const id = dataTransfer.getData('text/plain'); + const isFolder = dataTransfer.getData('application/oxicloud-folder') === 'true'; + + if (isFolder && id === targetFolderId) { + console.log("nothing to do"); + return; //nothing to do + } + // append current item to selection + if (isFolder) { + console.log(`adding ${id} as folder`); + selection.folderIds.push(id); + } + else { + console.log(`adding ${id} as file`); + selection.fileIds.push(id); + } + } + + console.log(`request ${action} of: `, selection); + /* + TODO do we prefer use atomic operation on 1 item ? like: + await fileOps.moveFolder(sourceId, targetFolderId); + await fileOps.moveFile(sourceId, targetFolderId); + */ + + let result; + switch (action) { + case "copy": + result = await window.fileOps.batchCopy(selection.fileIds, selection.folderIds, targetFolderId); + break; + + case "move": + result = await window.fileOps.batchMove(selection.fileIds, selection.folderIds, targetFolderId); + // redraw directory + if (result.success > 0) + window.loadFiles(); + break; + + default: + console.error(`drag and drop: action ${action} unknown`); + return; + } + window.multiSelect.showBatchResult(action, result); + console.log( result); + }, + _hydrateViewIfNeeded(view) { // Only hydrate if there is at least one rendered item in the opposite/current DOM. // This prevents stale cache hydration in empty-state screens. @@ -755,7 +794,6 @@ const ui = { this._renderFoldersToView(this._lastFolders, 'grid'); this._renderFilesToView(this._lastFiles, 'grid'); - return; } if (view === 'list') { @@ -787,9 +825,9 @@ const ui = { const itemInfo = (card) => { if (!card) return null; const fileId = card.dataset.fileId; - if (fileId) return { type: 'file', id: fileId, data: self._items.get(fileId) }; + if (fileId) return { type: 'file', id: fileId, name: card.dataset.fileName, data: self._items.get(fileId) }; const folderId = card.dataset.folderId; - if (folderId) return { type: 'folder', id: folderId, data: self._items.get(folderId) }; + if (folderId) return { type: 'folder', id: folderId, name: card.dataset.folderName, data: self._items.get(folderId) }; return null; }; @@ -947,31 +985,75 @@ const ui = { // dragstart container.addEventListener('dragstart', (e) => { - const card = e.target.closest(sel); + let card = e.target.closest(sel); if (!card) { e.preventDefault(); return; } - - // Grid items must be selected to start dragging - if (container === grid && - !card.classList.contains('selected')) { - e.preventDefault(); - return; - } - + const info = itemInfo(card); if (!info) { e.preventDefault(); return; } + if (container === grid) { + // pickup the equivalent item in list + const selector = (info.type === 'folder') ? "data-folder-id" : "data-file-id"; + card = list.querySelector(`div.file-item[${selector}="${info.id}"]`); + } + e.dataTransfer.setData('text/plain', info.id); - if (info.type === 'folder') { + if (info.type === 'folder') { e.dataTransfer.setData( 'application/oxicloud-folder', 'true'); } - card.classList.add('dragging'); + // allow copy or move (handled by the browser) + e.dataTransfer.effectAllowed = "copyMove"; + + self.draggedItems = document.createElement("div"); + self.draggedItems.className = "dragged-items"; + + // Fplease note that dragged elements are taken from list view (uniformisation) + let selectedCardFromList = list.querySelectorAll(`div.selected > div.name-cell`); + if (selectedCardFromList.length == 0) { + // fallback to current element + selectedCardFromList = card.querySelectorAll('div.name-cell'); + } + + let index = 0; + const maxElements = 4; + let lastItemDiv = null; + + while (index < selectedCardFromList.length && index < maxElements) { + let div = document.createElement("div"); + div.className="file-item"; + let clone = selectedCardFromList[index].cloneNode(true); + let star = clone.querySelector('.favorite-star-inline'); + clone.querySelectorAll('img').forEach((img) => { img.loading="eager"; } ); + if (star) clone.removeChild(star); + div.appendChild(clone); + self.draggedItems.appendChild( div); + index += 1; + lastItemDiv = div; + } + + // if more than 1 item, display the badge + if (selectedCardFromList.length > 1) { + let badge = document.createElement("span"); + badge.className="dragged-items-badge"; + badge.innerText=selectedCardFromList.length; + self.draggedItems.appendChild( badge); + } + + // if more than maxElements display the fading + if (selectedCardFromList.length > maxElements) { + lastItemDiv.classList.add("fading"); + } + + self.dragPreview.appendChild( self.draggedItems); + e.dataTransfer.setDragImage(self.draggedItems, -20, -20); + }); // dragend container.addEventListener('dragend', (e) => { - const card = e.target.closest(sel); - if (card) card.classList.remove('dragging'); + + self.dragPreview.removeChild( self.draggedItems); document.querySelectorAll('.drop-target') .forEach(el => el.classList.remove('drop-target')); }); @@ -1002,11 +1084,8 @@ const ui = { e.preventDefault(); card.classList.remove('drop-target'); - const id = e.dataTransfer.getData('text/plain'); - const isFolder = - e.dataTransfer.getData('application/oxicloud-folder') === 'true'; - - await self.move( id, isFolder, targetFolderId); + const action = e.dataTransfer.dropEffect; + await self._dropToFolder( action, targetFolderId, e.dataTransfer); }); } }, @@ -1266,7 +1345,8 @@ const ui = { this._items.set(folder.id, folder); } - this._renderFoldersToView(safeFolders, this._getActiveView()); + this._renderFoldersToView(safeFolders, 'grid'); + this._renderFoldersToView(safeFolders, 'list'); }, /** @@ -1282,7 +1362,8 @@ const ui = { this._items.set(file.id, file); } - this._renderFilesToView(safeFiles, this._getActiveView()); + this._renderFilesToView(safeFiles, 'grid'); + this._renderFilesToView(safeFiles, 'list'); }, /* ================================================================ @@ -1305,7 +1386,8 @@ const ui = { this._items.set(folder.id, folder); this._upsertById(this._lastFolders, folder); - this._renderFoldersToView([folder], this._getActiveView()); + this._renderFoldersToView([folder], 'grid'); + this._renderFoldersToView([folder], 'list'); }, /** @@ -1324,7 +1406,8 @@ const ui = { this._items.set(file.id, file); this._upsertById(this._lastFiles, file); - this._renderFilesToView([file], this._getActiveView()); + this._renderFilesToView([file], 'grid'); + this._renderFilesToView([file], 'list'); } }; @@ -1393,6 +1476,7 @@ function initRubberBandSelection() { let active = false; let startX = 0, startY = 0; + const list = document.getElementById('files-list-view'); // We listen on the whole files-container (covers grid + empty space) const container = document.querySelector('.files-container') || document.getElementById('files-grid'); @@ -1454,9 +1538,16 @@ function initRubberBandSelection() { if (intersects) { card.classList.add('selected'); + // Sync with multiSelect module if (window.multiSelect) { const info = window.multiSelect._extractInfo(card); + + // select the equivalent list-item to activate it too + let selector = (info.type === 'folder') ? "data-folder-id" : "data-file-id"; + const fileItem = list?.querySelector(`div.file-item[${selector}="${info.id}"]`); + fileItem?.classList.add('selected'); + if (info) window.multiSelect.select(info.id, info.name, info.type, info.parentId); } } else { @@ -1464,6 +1555,12 @@ function initRubberBandSelection() { // Deselect from multiSelect module if (window.multiSelect) { const info = window.multiSelect._extractInfo(card); + + // select the equivalent list-item to activate it too + let selector = (info.type === 'folder') ? "data-folder-id" : "data-file-id"; + const fileItem = list?.querySelector(`div.file-item[${selector}="${info.id}"]`); + fileItem?.classList.remove('selected'); + if (info) window.multiSelect.deselect(info.id); } } diff --git a/static/js/core/notifications.js b/static/js/core/notifications.js index 69b723de..117bfb01 100644 --- a/static/js/core/notifications.js +++ b/static/js/core/notifications.js @@ -79,7 +79,6 @@ const notifications = (() => { } function _renderBadge() { const badge = $('notif-badge'); - console.log(`badge`, badge); if (!badge) return; if (_badgeCount > 0) { badge.classList.remove("hidden"); diff --git a/static/js/features/files/contextMenus.js b/static/js/features/files/contextMenus.js index 095c0e68..d6f57c34 100644 --- a/static/js/features/files/contextMenus.js +++ b/static/js/features/files/contextMenus.js @@ -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; } diff --git a/static/js/features/files/fileOperations.js b/static/js/features/files/fileOperations.js index dc959a18..13346a1a 100644 --- a/static/js/features/files/fileOperations.js +++ b/static/js/features/files/fileOperations.js @@ -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} - 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} - 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 diff --git a/static/js/features/files/multiSelect.js b/static/js/features/files/multiSelect.js index cca75273..4cd111f7 100644 --- a/static/js/features/files/multiSelect.js +++ b/static/js/features/files/multiSelect.js @@ -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