refactor(js): move multiSelect into batchToolbar + move residual method into relevant components
This commit is contained in:
+1
-1
@@ -25,7 +25,7 @@
|
|||||||
@import url("./components/notifications.css");
|
@import url("./components/notifications.css");
|
||||||
@import url("./components/userMenu.css");
|
@import url("./components/userMenu.css");
|
||||||
@import url("./components/languageSelector.css");
|
@import url("./components/languageSelector.css");
|
||||||
@import url("./components/multiSelect.css");
|
@import url("./components/batchToolbar.css");
|
||||||
@import url("./components/spinner.css");
|
@import url("./components/spinner.css");
|
||||||
@import url("./components/search.css");
|
@import url("./components/search.css");
|
||||||
@import url("./components/icons.css");
|
@import url("./components/icons.css");
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { i18n } from '../core/i18n.js';
|
import { i18n } from '../core/i18n.js';
|
||||||
import { inlineViewer } from '../features/files/inlineViewer.js';
|
import { inlineViewer } from '../features/files/inlineViewer.js';
|
||||||
import { multiSelect } from '../features/files/multiSelect.js';
|
import { batchToolbar } from '../features/files/batchToolbar.js';
|
||||||
import { resolveHomeFolder } from './authSession.js';
|
import { resolveHomeFolder } from './authSession.js';
|
||||||
import { updateHistory } from './main.js';
|
import { updateHistory } from './main.js';
|
||||||
import { app } from './state.js';
|
import { app } from './state.js';
|
||||||
@@ -217,9 +217,9 @@ async function loadFiles(options = { insertHistory: true }) {
|
|||||||
|
|
||||||
ui._items.clear();
|
ui._items.clear();
|
||||||
ui.resetFilesList();
|
ui.resetFilesList();
|
||||||
if (multiSelect) {
|
if (batchToolbar) {
|
||||||
multiSelect.clear();
|
batchToolbar.clear();
|
||||||
multiSelect.init(); // this will wire buttons & select-all-checkbox
|
batchToolbar.init(); // this will wire buttons & select-all-checkbox
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {FolderItem[]} */
|
/** @type {FolderItem[]} */
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { formatFileSize, formatQuotaSize } from '../core/formatters.js';
|
|||||||
import { i18n } from '../core/i18n.js';
|
import { i18n } from '../core/i18n.js';
|
||||||
import { oxiIconsInit } from '../core/icons.js';
|
import { oxiIconsInit } from '../core/icons.js';
|
||||||
import { fileOps } from '../features/files/fileOperations.js';
|
import { fileOps } from '../features/files/fileOperations.js';
|
||||||
import { multiSelect } from '../features/files/multiSelect.js';
|
import { batchToolbar } from '../features/files/batchToolbar.js';
|
||||||
import { favorites } from '../features/library/favorites.js';
|
import { favorites } from '../features/library/favorites.js';
|
||||||
import { recent } from '../features/library/recent.js';
|
import { recent } from '../features/library/recent.js';
|
||||||
import { fileSharing } from '../features/sharing/fileSharing.js';
|
import { fileSharing } from '../features/sharing/fileSharing.js';
|
||||||
@@ -50,7 +50,7 @@ let uploadDropdownDocumentClickHandler = null;
|
|||||||
let uploadDropdownBindingsController = null;
|
let uploadDropdownBindingsController = null;
|
||||||
let actionsBarDelegationBound = false;
|
let actionsBarDelegationBound = false;
|
||||||
|
|
||||||
const _multiSelectButons = `
|
const _batchToolbarButons = `
|
||||||
<div class="action-buttons batch-selection-bar hidden" id="multi-select-buttons">
|
<div class="action-buttons batch-selection-bar hidden" id="multi-select-buttons">
|
||||||
<div class="list-header-checkbox">
|
<div class="list-header-checkbox">
|
||||||
<button class="batch-bar-close" id="batch-selection-close" title="Cancel selection">
|
<button class="batch-bar-close" id="batch-selection-close" title="Cancel selection">
|
||||||
@@ -117,7 +117,7 @@ const ACTIONS_BAR_TEMPLATES = {
|
|||||||
<span data-i18n="actions.new_folder">New folder</span>
|
<span data-i18n="actions.new_folder">New folder</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
${_multiSelectButons}
|
${_batchToolbarButons}
|
||||||
${_toggleButtons}
|
${_toggleButtons}
|
||||||
`,
|
`,
|
||||||
trash: `
|
trash: `
|
||||||
@@ -131,7 +131,7 @@ const ACTIONS_BAR_TEMPLATES = {
|
|||||||
`,
|
`,
|
||||||
favorites: `
|
favorites: `
|
||||||
<div class="action-buttons" id="default-buttons"></div>
|
<div class="action-buttons" id="default-buttons"></div>
|
||||||
${_multiSelectButons}
|
${_batchToolbarButons}
|
||||||
${_toggleButtons}
|
${_toggleButtons}
|
||||||
`,
|
`,
|
||||||
recent: `
|
recent: `
|
||||||
@@ -141,7 +141,7 @@ const ACTIONS_BAR_TEMPLATES = {
|
|||||||
<span data-i18n="actions.clear_recent">Clear recent</span>
|
<span data-i18n="actions.clear_recent">Clear recent</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
${_multiSelectButons}
|
${_batchToolbarButons}
|
||||||
${_toggleButtons}
|
${_toggleButtons}
|
||||||
`,
|
`,
|
||||||
sharedwithme: `
|
sharedwithme: `
|
||||||
@@ -386,9 +386,9 @@ function initApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize multi-select / batch actions
|
// Initialize multi-select / batch actions
|
||||||
if (multiSelect?.init) {
|
if (batchToolbar?.init) {
|
||||||
console.log('Initializing multi-select module');
|
console.log('Initializing multi-select module');
|
||||||
multiSelect.init();
|
batchToolbar.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('authenticationDone', async () => {
|
window.addEventListener('authenticationDone', async () => {
|
||||||
|
|||||||
+10
-10
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { i18n } from '../core/i18n.js';
|
import { i18n } from '../core/i18n.js';
|
||||||
import { multiSelect } from '../features/files/multiSelect.js';
|
import { batchToolbar } from '../features/files/batchToolbar.js';
|
||||||
import { favorites } from '../features/library/favorites.js';
|
import { favorites } from '../features/library/favorites.js';
|
||||||
import { musicView } from '../features/library/music.js';
|
import { musicView } from '../features/library/music.js';
|
||||||
import { photosView } from '../features/library/photos.js';
|
import { photosView } from '../features/library/photos.js';
|
||||||
@@ -201,7 +201,7 @@ function switchToSharedSection() {
|
|||||||
sharedView.show();
|
sharedView.show();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (multiSelect) multiSelect.clear();
|
if (batchToolbar) batchToolbar.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchToSharedWithMeSection() {
|
function switchToSharedWithMeSection() {
|
||||||
@@ -221,7 +221,7 @@ function switchToSharedWithMeSection() {
|
|||||||
toggleFileContainer(true);
|
toggleFileContainer(true);
|
||||||
syncViewContainers();
|
syncViewContainers();
|
||||||
|
|
||||||
if (multiSelect) multiSelect.clear();
|
if (batchToolbar) batchToolbar.clear();
|
||||||
|
|
||||||
// Load and render items into the files container
|
// Load and render items into the files container
|
||||||
sharedWithMeView.init();
|
sharedWithMeView.init();
|
||||||
@@ -253,7 +253,7 @@ function switchToFilesSection() {
|
|||||||
app.currentPath = app.userHomeFolderId || '';
|
app.currentPath = app.userHomeFolderId || '';
|
||||||
app.breadcrumbPath = [];
|
app.breadcrumbPath = [];
|
||||||
ui.updateBreadcrumb();
|
ui.updateBreadcrumb();
|
||||||
if (multiSelect) multiSelect.clear();
|
if (batchToolbar) batchToolbar.clear();
|
||||||
|
|
||||||
// temp solution
|
// temp solution
|
||||||
sharedView.loadItems().then(() => {
|
sharedView.loadItems().then(() => {
|
||||||
@@ -296,7 +296,7 @@ function switchToFavoritesSection() {
|
|||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (multiSelect) multiSelect.clear();
|
if (batchToolbar) batchToolbar.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchToRecentFilesSection() {
|
function switchToRecentFilesSection() {
|
||||||
@@ -329,7 +329,7 @@ function switchToRecentFilesSection() {
|
|||||||
<p>Error loading the recent module</p>
|
<p>Error loading the recent module</p>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
if (multiSelect) multiSelect.clear();
|
if (batchToolbar) batchToolbar.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchToPhotosSection() {
|
function switchToPhotosSection() {
|
||||||
@@ -352,7 +352,7 @@ function switchToPhotosSection() {
|
|||||||
if (photosView) {
|
if (photosView) {
|
||||||
photosView.show();
|
photosView.show();
|
||||||
}
|
}
|
||||||
if (multiSelect) multiSelect.clear();
|
if (batchToolbar) batchToolbar.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchToTrashSection() {
|
function switchToTrashSection() {
|
||||||
@@ -377,7 +377,7 @@ function switchToTrashSection() {
|
|||||||
// Load trash items
|
// Load trash items
|
||||||
loadTrashItems();
|
loadTrashItems();
|
||||||
|
|
||||||
if (multiSelect) multiSelect.clear();
|
if (batchToolbar) batchToolbar.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchToMusicSection() {
|
function switchToMusicSection() {
|
||||||
@@ -404,7 +404,7 @@ function switchToMusicSection() {
|
|||||||
if (musicView) {
|
if (musicView) {
|
||||||
musicView.show();
|
musicView.show();
|
||||||
}
|
}
|
||||||
if (multiSelect) multiSelect.clear();
|
if (batchToolbar) batchToolbar.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -423,7 +423,7 @@ function activateFilesUI() {
|
|||||||
breadcrumb?.classList.remove('hidden');
|
breadcrumb?.classList.remove('hidden');
|
||||||
toggleFileContainer(true);
|
toggleFileContainer(true);
|
||||||
syncViewContainers();
|
syncViewContainers();
|
||||||
if (multiSelect) multiSelect.clear();
|
if (batchToolbar) batchToolbar.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import { escapeHtml, formatDateTime } from '../core/formatters.js';
|
import { escapeHtml, formatDateTime } from '../core/formatters.js';
|
||||||
import { i18n } from '../core/i18n.js';
|
import { i18n } from '../core/i18n.js';
|
||||||
import { fileOps } from '../features/files/fileOperations.js';
|
import { fileOps } from '../features/files/fileOperations.js';
|
||||||
import { multiSelect } from '../features/files/multiSelect.js';
|
import { batchToolbar } from '../features/files/batchToolbar.js';
|
||||||
import * as pathTooltip from '../features/pathTooltip.js';
|
import * as pathTooltip from '../features/pathTooltip.js';
|
||||||
import { appElements } from './state.js';
|
import { appElements } from './state.js';
|
||||||
import { ui } from './ui.js';
|
import { ui } from './ui.js';
|
||||||
@@ -22,7 +22,7 @@ async function loadTrashItems() {
|
|||||||
const elements = appElements;
|
const elements = appElements;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (multiSelect) multiSelect.clear();
|
if (batchToolbar) batchToolbar.clear();
|
||||||
pathTooltip.destroy(elements.filesList);
|
pathTooltip.destroy(elements.filesList);
|
||||||
ui.resetFilesList(); // ensure also list visible & error hidden
|
ui.resetFilesList(); // ensure also list visible & error hidden
|
||||||
elements.filesList.innerHTML = `
|
elements.filesList.innerHTML = `
|
||||||
|
|||||||
+18
-18
@@ -13,7 +13,7 @@ import { OxiIcons } from '../core/icons.js';
|
|||||||
import { contextMenus } from '../features/files/contextMenus.js';
|
import { contextMenus } from '../features/files/contextMenus.js';
|
||||||
import { fileOps } from '../features/files/fileOperations.js';
|
import { fileOps } from '../features/files/fileOperations.js';
|
||||||
import { inlineViewer } from '../features/files/inlineViewer.js';
|
import { inlineViewer } from '../features/files/inlineViewer.js';
|
||||||
import { multiSelect } from '../features/files/multiSelect.js';
|
import { batchToolbar } from '../features/files/batchToolbar.js';
|
||||||
import { wopiEditor } from '../features/files/wopiEditor.js';
|
import { wopiEditor } from '../features/files/wopiEditor.js';
|
||||||
import { favorites } from '../features/library/favorites.js';
|
import { favorites } from '../features/library/favorites.js';
|
||||||
import { recent } from '../features/library/recent.js';
|
import { recent } from '../features/library/recent.js';
|
||||||
@@ -727,9 +727,9 @@ const ui = {
|
|||||||
* @param {any} dataTransfer fallback if nothing is selected
|
* @param {any} dataTransfer fallback if nothing is selected
|
||||||
*/
|
*/
|
||||||
async _dropToFolder(action, targetFolderId, dataTransfer) {
|
async _dropToFolder(action, targetFolderId, dataTransfer) {
|
||||||
const selection = multiSelect.getSelection(targetFolderId);
|
const selection = batchToolbar.getSelection(targetFolderId);
|
||||||
|
|
||||||
multiSelect.clear();
|
batchToolbar.clear();
|
||||||
|
|
||||||
if (selection.fileIds.length === 0 && selection.folderIds.length === 0) {
|
if (selection.fileIds.length === 0 && selection.folderIds.length === 0) {
|
||||||
// try to use dataTransfer (direct move without selection)
|
// try to use dataTransfer (direct move without selection)
|
||||||
@@ -774,7 +774,7 @@ const ui = {
|
|||||||
console.error(`drag and drop: action ${action} unknown`);
|
console.error(`drag and drop: action ${action} unknown`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
multiSelect.showBatchResult(action, result);
|
batchToolbar.showBatchResult(action, result);
|
||||||
console.log(result);
|
console.log(result);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -941,8 +941,8 @@ const ui = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// shiftkey is used to complete selection
|
// shiftkey is used to complete selection
|
||||||
if (e.shiftKey && multiSelect) {
|
if (e.shiftKey && batchToolbar) {
|
||||||
multiSelect.handleToggleItem(card, e);
|
batchToolbar.handleToggleItem(card, e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1512,13 +1512,13 @@ const ui = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle selection state of a file/folder card.
|
* Toggle selection state of a file/folder card.
|
||||||
* Routes through the multiSelect module so batch actions know about selected items.
|
* Routes through the batchToolbar module so batch actions know about selected items.
|
||||||
* @param {HTMLDivElement} card
|
* @param {HTMLDivElement} card
|
||||||
* @param {MouseEvent} event
|
* @param {MouseEvent} event
|
||||||
*/
|
*/
|
||||||
function toggleCardSelection(card, event) {
|
function toggleCardSelection(card, event) {
|
||||||
if (multiSelect) {
|
if (batchToolbar) {
|
||||||
multiSelect.handleToggleItem(card, event);
|
batchToolbar.handleToggleItem(card, event);
|
||||||
} else {
|
} else {
|
||||||
card.classList.toggle('selected');
|
card.classList.toggle('selected');
|
||||||
}
|
}
|
||||||
@@ -1641,17 +1641,17 @@ function initRubberBandSelection() {
|
|||||||
if (intersects) {
|
if (intersects) {
|
||||||
card.classList.add('selected');
|
card.classList.add('selected');
|
||||||
|
|
||||||
// Sync with multiSelect module
|
// Sync with batchToolbar module
|
||||||
if (multiSelect) {
|
if (batchToolbar) {
|
||||||
const info = multiSelect._extractInfo(/** @type {HTMLDivElement} */ (card));
|
const info = batchToolbar._extractInfo(/** @type {HTMLDivElement} */ (card));
|
||||||
if (info) multiSelect.select(info.id, info.name, info.type, info.parentId);
|
if (info) batchToolbar.select(info.id, info.name, info.type, info.parentId);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
card.classList.remove('selected');
|
card.classList.remove('selected');
|
||||||
// Deselect from multiSelect module
|
// Deselect from batchToolbar module
|
||||||
if (multiSelect) {
|
if (batchToolbar) {
|
||||||
const info = multiSelect._extractInfo(/** @type {HTMLDivElement} */ (card));
|
const info = batchToolbar._extractInfo(/** @type {HTMLDivElement} */ (card));
|
||||||
if (info) multiSelect.deselect(info.id);
|
if (info) batchToolbar.deselect(info.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1663,7 +1663,7 @@ function initRubberBandSelection() {
|
|||||||
const hadSelection = selRect.style.display === 'block';
|
const hadSelection = selRect.style.display === 'block';
|
||||||
selRect.style.display = 'none';
|
selRect.style.display = 'none';
|
||||||
// Update the batch bar after rubber band selection completes
|
// Update the batch bar after rubber band selection completes
|
||||||
if (multiSelect) multiSelect._syncUI();
|
if (batchToolbar) batchToolbar._syncUI();
|
||||||
// Suppress the click event that follows mouseup so the global
|
// Suppress the click event that follows mouseup so the global
|
||||||
// deselect handler doesn't immediately clear the selection.
|
// deselect handler doesn't immediately clear the selection.
|
||||||
if (hadSelection) {
|
if (hadSelection) {
|
||||||
|
|||||||
@@ -86,6 +86,9 @@ export class ResourceListComponent {
|
|||||||
/** @type {Set<string>} */
|
/** @type {Set<string>} */
|
||||||
this._selected = new Set();
|
this._selected = new Set();
|
||||||
|
|
||||||
|
/** Index of the last clicked item — used for shift-click range selection. */
|
||||||
|
this._lastClickedIndex = -1;
|
||||||
|
|
||||||
this._ownerVisible = this._cfg.showOwner;
|
this._ownerVisible = this._cfg.showOwner;
|
||||||
|
|
||||||
this._initDelegation();
|
this._initDelegation();
|
||||||
@@ -110,8 +113,10 @@ export class ResourceListComponent {
|
|||||||
|
|
||||||
this._selected.clear();
|
this._selected.clear();
|
||||||
this._items.clear();
|
this._items.clear();
|
||||||
|
this._lastClickedIndex = -1;
|
||||||
|
|
||||||
this._appendItems(folders, files, groupFn);
|
this._appendItems(folders, files, groupFn);
|
||||||
|
this._wireSelectAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -132,6 +137,40 @@ export class ResourceListComponent {
|
|||||||
if (header) this._container.appendChild(header);
|
if (header) this._container.appendChild(header);
|
||||||
this._selected.clear();
|
this._selected.clear();
|
||||||
this._items.clear();
|
this._items.clear();
|
||||||
|
this._lastClickedIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deselect all items without removing them from the DOM.
|
||||||
|
* Used by the batch toolbar after an operation completes.
|
||||||
|
*/
|
||||||
|
clearSelection() {
|
||||||
|
this._selected.clear();
|
||||||
|
this._lastClickedIndex = -1;
|
||||||
|
this._container.querySelectorAll('.file-item.selected').forEach((card) => {
|
||||||
|
card.classList.remove('selected');
|
||||||
|
const cb = /** @type {HTMLInputElement | null} */ (card.querySelector('.item-checkbox'));
|
||||||
|
if (cb) cb.checked = false;
|
||||||
|
});
|
||||||
|
this._syncSelectAllCheckbox();
|
||||||
|
this._cfg.onSelectionChange?.([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select all visible items in the container.
|
||||||
|
*/
|
||||||
|
selectAll() {
|
||||||
|
this._container.querySelectorAll('.file-item').forEach((card) => {
|
||||||
|
const el = /** @type {HTMLElement} */ (card);
|
||||||
|
const id = el.dataset.fileId || el.dataset.folderId || '';
|
||||||
|
if (!id) return;
|
||||||
|
el.classList.add('selected');
|
||||||
|
const cb = /** @type {HTMLInputElement | null} */ (el.querySelector('.item-checkbox'));
|
||||||
|
if (cb) cb.checked = true;
|
||||||
|
this._selected.add(id);
|
||||||
|
});
|
||||||
|
this._syncSelectAllCheckbox();
|
||||||
|
this._notifySelectionChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -406,9 +445,13 @@ export class ResourceListComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checkbox cell → selection
|
// Checkbox cell → selection (shift extends range)
|
||||||
if (cfg.selectable && target.closest('.checkbox-cell')) {
|
if (cfg.selectable && target.closest('.checkbox-cell')) {
|
||||||
this._toggleSelection(card, /** @type {MouseEvent} */ (e));
|
if (e.shiftKey) {
|
||||||
|
this._handleShiftSelect(card);
|
||||||
|
} else {
|
||||||
|
this._toggleSelection(card);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,7 +460,13 @@ export class ResourceListComponent {
|
|||||||
|
|
||||||
// Modifier-key click → selection toggle
|
// Modifier-key click → selection toggle
|
||||||
if (e.metaKey || e.altKey || e.ctrlKey) {
|
if (e.metaKey || e.altKey || e.ctrlKey) {
|
||||||
if (cfg.selectable) this._toggleSelection(card, /** @type {MouseEvent} */ (e));
|
if (cfg.selectable) this._toggleSelection(card);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift-click anywhere on the card → extend selection range
|
||||||
|
if (e.shiftKey && cfg.selectable) {
|
||||||
|
this._handleShiftSelect(card);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,11 +503,11 @@ export class ResourceListComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle selection state on a card and notify via `onSelectionChange`.
|
* Toggle selection on a single card and notify.
|
||||||
|
* Tracks `_lastClickedIndex` for subsequent shift-clicks.
|
||||||
* @param {HTMLElement} card
|
* @param {HTMLElement} card
|
||||||
* @param {MouseEvent} _e - Reserved for future shift-click range selection.
|
|
||||||
*/
|
*/
|
||||||
_toggleSelection(card, _e) {
|
_toggleSelection(card) {
|
||||||
const id = card.dataset.fileId || card.dataset.folderId || '';
|
const id = card.dataset.fileId || card.dataset.folderId || '';
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
@@ -474,13 +523,96 @@ export class ResourceListComponent {
|
|||||||
this._selected.delete(id);
|
this._selected.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._cfg.onSelectionChange) {
|
// Record position for the next shift-click
|
||||||
/** @type {Array<FileItem|FolderItem>} */
|
const items = [...this._container.querySelectorAll('.file-item')];
|
||||||
const selectedItems = [...this._selected].flatMap((sid) => {
|
this._lastClickedIndex = items.indexOf(card);
|
||||||
const item = this._items.get(sid);
|
|
||||||
return item ? [item] : [];
|
this._syncSelectAllCheckbox();
|
||||||
});
|
this._notifySelectionChange();
|
||||||
this._cfg.onSelectionChange(selectedItems);
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend the selection from `_lastClickedIndex` to `card` (inclusive).
|
||||||
|
* If no previous click exists, falls back to a plain toggle.
|
||||||
|
* @param {HTMLElement} card
|
||||||
|
*/
|
||||||
|
_handleShiftSelect(card) {
|
||||||
|
const items = /** @type {HTMLElement[]} */ ([...this._container.querySelectorAll('.file-item')]);
|
||||||
|
const index = items.indexOf(card);
|
||||||
|
|
||||||
|
if (this._lastClickedIndex >= 0 && index >= 0) {
|
||||||
|
const start = Math.min(this._lastClickedIndex, index);
|
||||||
|
const end = Math.max(this._lastClickedIndex, index);
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
const el = items[i];
|
||||||
|
const id = el.dataset.fileId || el.dataset.folderId || '';
|
||||||
|
if (!id) continue;
|
||||||
|
el.classList.add('selected');
|
||||||
|
const cb = /** @type {HTMLInputElement | null} */ (el.querySelector('.item-checkbox'));
|
||||||
|
if (cb) cb.checked = true;
|
||||||
|
this._selected.add(id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._toggleSelection(card);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._lastClickedIndex = index;
|
||||||
|
this._syncSelectAllCheckbox();
|
||||||
|
this._notifySelectionChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the select-all checkbox in the container header and wire its
|
||||||
|
* `change` event. Called after every `render()`.
|
||||||
|
*/
|
||||||
|
_wireSelectAll() {
|
||||||
|
if (!this._cfg.selectable) return;
|
||||||
|
const cb = /** @type {HTMLInputElement | null} */ (this._container.querySelector('#select-all-checkbox'));
|
||||||
|
if (!cb) return;
|
||||||
|
// Replace with a fresh listener to avoid duplicates across re-renders
|
||||||
|
const fresh = /** @type {HTMLInputElement} */ (cb.cloneNode(true));
|
||||||
|
cb.parentNode?.replaceChild(fresh, cb);
|
||||||
|
fresh.addEventListener('change', () => {
|
||||||
|
if (fresh.checked) {
|
||||||
|
this.selectAll();
|
||||||
|
} else {
|
||||||
|
this.clearSelection();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync the three-state select-all checkbox in the list header.
|
||||||
|
* Checked = all selected, indeterminate = some selected, unchecked = none.
|
||||||
|
*/
|
||||||
|
_syncSelectAllCheckbox() {
|
||||||
|
const cb = /** @type {HTMLInputElement | null} */ (this._container.querySelector('#select-all-checkbox'));
|
||||||
|
if (!cb) return;
|
||||||
|
const total = this._container.querySelectorAll('.file-item').length;
|
||||||
|
if (total === 0) {
|
||||||
|
cb.checked = false;
|
||||||
|
cb.indeterminate = false;
|
||||||
|
} else if (this._selected.size >= total) {
|
||||||
|
cb.checked = true;
|
||||||
|
cb.indeterminate = false;
|
||||||
|
} else if (this._selected.size > 0) {
|
||||||
|
cb.checked = false;
|
||||||
|
cb.indeterminate = true;
|
||||||
|
} else {
|
||||||
|
cb.checked = false;
|
||||||
|
cb.indeterminate = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Build the selected-items array and fire `onSelectionChange`. */
|
||||||
|
_notifySelectionChange() {
|
||||||
|
if (!this._cfg.onSelectionChange) return;
|
||||||
|
/** @type {Array<FileItem|FolderItem>} */
|
||||||
|
const selectedItems = [...this._selected].flatMap((id) => {
|
||||||
|
const item = this._items.get(id);
|
||||||
|
return item ? [item] : [];
|
||||||
|
});
|
||||||
|
this._cfg.onSelectionChange(selectedItems);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* OxiCloud - Multi-Select & Batch Actions Module
|
* OxiCloud — Batch Toolbar Module
|
||||||
*
|
*
|
||||||
* Adds checkboxes to grid and list views, replaces the list-view header
|
* Manages the floating selection bar that appears when items are selected,
|
||||||
* with a NextCloud-style selection bar when items are selected, and
|
* and executes batch operations (delete, move, download, favorites).
|
||||||
* provides batch delete / move / download / favorites operations.
|
*
|
||||||
|
* Selection state (_selected, handleToggleItem, selectAll, …) is kept here
|
||||||
|
* while the main file manager still uses its own delegation (ui.js).
|
||||||
|
* Once ui.js is migrated to ResourceListComponent (plan step B5), all
|
||||||
|
* selection mechanics will live in the component and this module will
|
||||||
|
* shrink to only the toolbar UI and batch-operation API calls.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// TODO: rename into selection-bar ?
|
|
||||||
// TODO: merge with photo part
|
|
||||||
|
|
||||||
import { loadFiles } from '../../app/filesView.js';
|
import { loadFiles } from '../../app/filesView.js';
|
||||||
import { app } from '../../app/state.js';
|
import { app } from '../../app/state.js';
|
||||||
import { showConfirmDialog, ui } from '../../app/ui.js';
|
import { showConfirmDialog, ui } from '../../app/ui.js';
|
||||||
@@ -22,7 +24,7 @@ import { getAuthHeaders } from './fileOperations.js';
|
|||||||
* @import {BatchResult} from './fileOperations.js'
|
* @import {BatchResult} from './fileOperations.js'
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const multiSelect = {
|
const batchToolbar = {
|
||||||
/** @type {Map<String, LightItem>} items: Map<id, { id, name, type, parentId }> */
|
/** @type {Map<String, LightItem>} items: Map<id, { id, name, type, parentId }> */
|
||||||
|
|
||||||
_selected: new Map(),
|
_selected: new Map(),
|
||||||
@@ -536,4 +538,4 @@ const multiSelect = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { multiSelect };
|
export { batchToolbar };
|
||||||
@@ -18,7 +18,7 @@ import { musicView } from '../library/music.js';
|
|||||||
import { fileSharing } from '../sharing/fileSharing.js';
|
import { fileSharing } from '../sharing/fileSharing.js';
|
||||||
import { fileOps } from './fileOperations.js';
|
import { fileOps } from './fileOperations.js';
|
||||||
import { inlineViewer } from './inlineViewer.js';
|
import { inlineViewer } from './inlineViewer.js';
|
||||||
import { multiSelect } from './multiSelect.js';
|
import { batchToolbar } from './batchToolbar.js';
|
||||||
import { wopiEditor } from './wopiEditor.js';
|
import { wopiEditor } from './wopiEditor.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -330,8 +330,8 @@ const contextMenus = {
|
|||||||
|
|
||||||
// Copy button handler
|
// Copy button handler
|
||||||
copyConfirmBtn.addEventListener('click', async () => {
|
copyConfirmBtn.addEventListener('click', async () => {
|
||||||
// Batch copy mode (from multiSelect)
|
// Batch copy mode (from batchToolbar)
|
||||||
if (app.moveDialogMode === 'batch' && multiSelect) {
|
if (app.moveDialogMode === 'batch' && batchToolbar) {
|
||||||
const targetId = app.selectedTargetFolderId;
|
const targetId = app.selectedTargetFolderId;
|
||||||
const items = app.batchMoveItems || [];
|
const items = app.batchMoveItems || [];
|
||||||
|
|
||||||
@@ -341,10 +341,10 @@ const contextMenus = {
|
|||||||
const result = await fileOps.batchCopy(fileIds, folderIds, targetId);
|
const result = await fileOps.batchCopy(fileIds, folderIds, targetId);
|
||||||
|
|
||||||
this.closeMoveDialog();
|
this.closeMoveDialog();
|
||||||
multiSelect.clear();
|
batchToolbar.clear();
|
||||||
loadFiles();
|
loadFiles();
|
||||||
|
|
||||||
multiSelect.showBatchResult('copy', result);
|
batchToolbar.showBatchResult('copy', result);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,8 +363,8 @@ const contextMenus = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
moveConfirmBtn.addEventListener('click', async () => {
|
moveConfirmBtn.addEventListener('click', async () => {
|
||||||
// Batch move mode (from multiSelect)
|
// Batch move mode (from batchToolbar)
|
||||||
if (app.moveDialogMode === 'batch' && multiSelect) {
|
if (app.moveDialogMode === 'batch' && batchToolbar) {
|
||||||
const targetId = app.selectedTargetFolderId;
|
const targetId = app.selectedTargetFolderId;
|
||||||
const items = app.batchMoveItems || [];
|
const items = app.batchMoveItems || [];
|
||||||
|
|
||||||
@@ -374,9 +374,9 @@ const contextMenus = {
|
|||||||
const result = await fileOps.batchMove(fileIds, folderIds, targetId);
|
const result = await fileOps.batchMove(fileIds, folderIds, targetId);
|
||||||
|
|
||||||
this.closeMoveDialog();
|
this.closeMoveDialog();
|
||||||
multiSelect.clear();
|
batchToolbar.clear();
|
||||||
loadFiles();
|
loadFiles();
|
||||||
multiSelect.showBatchResult('move', result);
|
batchToolbar.showBatchResult('move', result);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import { ui } from '../../app/ui.js';
|
import { ui } from '../../app/ui.js';
|
||||||
import { getCsrfHeaders } from '../../core/csrf.js';
|
import { getCsrfHeaders } from '../../core/csrf.js';
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
import { multiSelect } from '../files/multiSelect.js';
|
import { batchToolbar } from '../files/batchToolbar.js';
|
||||||
import * as pathTooltip from '../pathTooltip.js';
|
import * as pathTooltip from '../pathTooltip.js';
|
||||||
|
|
||||||
/** @import {FavoriteItem, FileItem, FolderItem} from '../../core/types.js' */
|
/** @import {FavoriteItem, FileItem, FolderItem} from '../../core/types.js' */
|
||||||
@@ -181,7 +181,7 @@ const favorites = {
|
|||||||
ui.resetFilesList(); // ensure also list visible & error hidden
|
ui.resetFilesList(); // ensure also list visible & error hidden
|
||||||
// wire buttons & select-all-checkbox as list header has changed in ui.resetFilesList()
|
// wire buttons & select-all-checkbox as list header has changed in ui.resetFilesList()
|
||||||
// FIXME: this case is not easy to understand, should apply better implementation
|
// FIXME: this case is not easy to understand, should apply better implementation
|
||||||
multiSelect.init();
|
batchToolbar.init();
|
||||||
|
|
||||||
ui.updateBreadcrumb();
|
ui.updateBreadcrumb();
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import { ui } from '../../app/ui.js';
|
import { ui } from '../../app/ui.js';
|
||||||
import { getCsrfHeaders } from '../../core/csrf.js';
|
import { getCsrfHeaders } from '../../core/csrf.js';
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
import { multiSelect } from '../files/multiSelect.js';
|
import { batchToolbar } from '../files/batchToolbar.js';
|
||||||
import * as pathTooltip from '../pathTooltip.js';
|
import * as pathTooltip from '../pathTooltip.js';
|
||||||
|
|
||||||
/** @import {FileItem, FolderItem, ItemTypeEnum, RecentItem} from '../../core/types.js' */
|
/** @import {FileItem, FolderItem, ItemTypeEnum, RecentItem} from '../../core/types.js' */
|
||||||
@@ -111,9 +111,9 @@ const recent = {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (multiSelect) {
|
if (batchToolbar) {
|
||||||
multiSelect.clear();
|
batchToolbar.clear();
|
||||||
multiSelect.init(); // this will wire buttons & select-all-checkbox
|
batchToolbar.init(); // this will wire buttons & select-all-checkbox
|
||||||
}
|
}
|
||||||
ui.updateBreadcrumb();
|
ui.updateBreadcrumb();
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
import { ui } from '../../app/ui.js';
|
import { ui } from '../../app/ui.js';
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
import { multiSelect } from '../../features/files/multiSelect.js';
|
import { batchToolbar } from '../../features/files/batchToolbar.js';
|
||||||
import { ownerTooltip } from '../../features/ownerTooltip.js';
|
import { ownerTooltip } from '../../features/ownerTooltip.js';
|
||||||
import { grants } from '../../model/grants.js';
|
import { grants } from '../../model/grants.js';
|
||||||
import { systemUsers } from '../../model/systemUsers.js';
|
import { systemUsers } from '../../model/systemUsers.js';
|
||||||
@@ -52,7 +52,7 @@ const sharedWithMeView = {
|
|||||||
|
|
||||||
// Standard files-view setup: clear list, show container, init multiselect
|
// Standard files-view setup: clear list, show container, init multiselect
|
||||||
ui.resetFilesList();
|
ui.resetFilesList();
|
||||||
multiSelect.init();
|
batchToolbar.init();
|
||||||
ui.updateBreadcrumb();
|
ui.updateBreadcrumb();
|
||||||
|
|
||||||
await this._loadPage();
|
await this._loadPage();
|
||||||
|
|||||||
Reference in New Issue
Block a user