refactor: simplify multiSelect module (remove duplicates, etc)
- remove duplicate code - use one uniq selection bar (for both list & grid view)
This commit is contained in:
@@ -34,7 +34,7 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.batch-action-bar {
|
.batch-selection-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.batch-action-bar.visible {
|
.batch-selection-bar.visible {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
max-height: 60px;
|
max-height: 60px;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
@@ -140,7 +140,7 @@
|
|||||||
accent-color: #ff5e3a;
|
accent-color: #ff5e3a;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .batch-action-bar {
|
[data-theme="dark"] .batch-selection-bar {
|
||||||
background-color: #1e293b;
|
background-color: #1e293b;
|
||||||
border-color: #334155;
|
border-color: #334155;
|
||||||
color: #e2e8f0;
|
color: #e2e8f0;
|
||||||
|
|||||||
@@ -255,6 +255,8 @@
|
|||||||
<span class="breadcrumb-item" data-i18n="breadcrumb.home">Home</span>
|
<span class="breadcrumb-item" data-i18n="breadcrumb.home">Home</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="batch-selection-bar" class="batch-selection-bar"></div>
|
||||||
|
|
||||||
<!-- Files Container -->
|
<!-- Files Container -->
|
||||||
<div class="files-container">
|
<div class="files-container">
|
||||||
<!-- Grid View -->
|
<!-- Grid View -->
|
||||||
@@ -332,5 +334,6 @@
|
|||||||
|
|
||||||
<!-- Upload Progress Toast (hidden – driven by notification bell) -->
|
<!-- Upload Progress Toast (hidden – driven by notification bell) -->
|
||||||
<div class="upload-toast hidden" id="upload-toast"></div>
|
<div class="upload-toast hidden" id="upload-toast"></div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -653,7 +653,7 @@ function setupEventListeners() {
|
|||||||
// selection state; this handler only covers the legacy CSS class removal.
|
// 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.
|
// Skip if a rubber-band selection just finished — the click is a side-effect.
|
||||||
if (window.__rubberBandJustFinished) return;
|
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-action-bar') && !e.target.closest('.list-header.selection-mode')) {
|
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-card.selected').forEach(c => c.classList.remove('selected'));
|
||||||
document.querySelectorAll('.file-item.selected').forEach(c => c.classList.remove('selected'));
|
document.querySelectorAll('.file-item.selected').forEach(c => c.classList.remove('selected'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
* provides batch delete / move / download / favorites operations.
|
* provides batch delete / move / download / favorites operations.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// TODO: rename into selection-bar ?
|
||||||
|
// TODO: merge with photo part
|
||||||
|
|
||||||
const multiSelect = {
|
const multiSelect = {
|
||||||
/** Currently selected items: Map<id, { id, name, type, parentId }> */
|
/** Currently selected items: Map<id, { id, name, type, parentId }> */
|
||||||
_selected: new Map(),
|
_selected: new Map(),
|
||||||
@@ -16,9 +19,6 @@ const multiSelect = {
|
|||||||
/** Whether the selection bar is currently visible */
|
/** Whether the selection bar is currently visible */
|
||||||
_barVisible: false,
|
_barVisible: false,
|
||||||
|
|
||||||
/** Saved original list-header HTML so we can restore it */
|
|
||||||
_savedHeaderHTML: '',
|
|
||||||
|
|
||||||
// ── Public API ──────────────────────────────────────────
|
// ── Public API ──────────────────────────────────────────
|
||||||
|
|
||||||
get count() { return this._selected.size; },
|
get count() { return this._selected.size; },
|
||||||
@@ -93,11 +93,14 @@ const multiSelect = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
_getAllVisibleItems() {
|
_getAllVisibleItems() {
|
||||||
|
return [...document.querySelectorAll('.file-item, .file-card')];
|
||||||
|
/*
|
||||||
const grid = document.getElementById('files-grid');
|
const grid = document.getElementById('files-grid');
|
||||||
if (grid && grid.style.display !== 'none') {
|
if (grid && grid.style.display !== 'none') {
|
||||||
return [...grid.querySelectorAll('.file-card')];
|
return [...grid.querySelectorAll('.file-card')];
|
||||||
}
|
}
|
||||||
return [...document.querySelectorAll('#files-list-view .file-item')];
|
return [...document.querySelectorAll('#files-list-view .file-item')];
|
||||||
|
*/
|
||||||
},
|
},
|
||||||
|
|
||||||
_extractInfo(el) {
|
_extractInfo(el) {
|
||||||
@@ -151,146 +154,61 @@ const multiSelect = {
|
|||||||
* Build the inner HTML for the selection bar that replaces the
|
* Build the inner HTML for the selection bar that replaces the
|
||||||
* normal list-header columns (Name / Type / Size / Modified).
|
* normal list-header columns (Name / Type / Size / Modified).
|
||||||
*/
|
*/
|
||||||
_buildSelectionBarHTML(n) {
|
_buildSelectionBarHTML() {
|
||||||
const countText = n === 1
|
//FIXME: should support i18n lang change
|
||||||
? (this._t('batch.one_selected') || '1 item selected')
|
|
||||||
: (this._t('batch.n_selected', { count: n }) || `${n} items selected`);
|
|
||||||
|
|
||||||
const favLabel = this._t('batch.add_favorites') || 'Add to favorites';
|
|
||||||
const moveLabel = this._t('batch.move_copy') || 'Move or copy';
|
|
||||||
const dlLabel = this._t('actions.download') || 'Download';
|
|
||||||
const delLabel = this._t('actions.delete') || 'Delete';
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="list-header-checkbox">
|
<div x-class="batch-bar-left" class="list-header-checkbox">
|
||||||
<input type="checkbox" id="select-all-checkbox" title="Toggle all" checked>
|
<button class="batch-bar-close" id="batch-grid-close" title="Cancel selection">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
<span class="batch-bar-count" id="batch-bar-count"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="batch-selection-info">
|
<div class="batch-selection-info">
|
||||||
<span class="batch-bar-count">${countText}</span>
|
|
||||||
<div class="batch-bar-actions">
|
<div class="batch-bar-actions">
|
||||||
<button class="batch-btn" id="batch-fav" title="${favLabel}">
|
<button class="batch-btn" id="batch-fav" title="Add to favorites" data-i18n-title="batch.add_favorites">
|
||||||
<i class="fas fa-star"></i>
|
<i class="fas fa-star"></i>
|
||||||
<span>${favLabel}</span>
|
<span data-i18n="batch.add_favorites">Add to favorites</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="batch-btn" id="batch-move" title="${moveLabel}">
|
<button class="batch-btn" id="batch-move" title="Move or copy" data-i18n-title="batch.move_copy">
|
||||||
<i class="fas fa-arrows-alt"></i>
|
<i class="fas fa-arrows-alt"></i>
|
||||||
<span>${moveLabel}</span>
|
<span data-i18n="batch.move_copy">Move or copy</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="batch-btn" id="batch-download" title="${dlLabel}">
|
<button class="batch-btn" id="batch-download" title="Download" data-i18n-title="actions.download">
|
||||||
<i class="fas fa-download"></i>
|
<i class="fas fa-download"></i>
|
||||||
<span>${dlLabel}</span>
|
<span data-i18n="actions.download">Download</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="batch-btn batch-btn-danger" id="batch-delete" title="${delLabel}">
|
<button class="batch-btn batch-btn-danger" id="batch-delete" title="Delete" data-i18-title="actions.delete">
|
||||||
<i class="fas fa-trash-alt"></i>
|
<i class="fas fa-trash-alt"></i>
|
||||||
<span>${delLabel}</span>
|
<span data-i18n="actions.delete">Delete</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Ensure the grid-view batch bar exists (shown only when grid is visible) */
|
|
||||||
_ensureGridBar() {
|
|
||||||
if (document.getElementById('batch-grid-bar')) return;
|
|
||||||
const bar = document.createElement('div');
|
|
||||||
bar.id = 'batch-grid-bar';
|
|
||||||
bar.className = 'batch-action-bar'; // reuse same styles
|
|
||||||
const container = document.querySelector('.files-container');
|
|
||||||
if (container) {
|
|
||||||
container.insertBefore(bar, container.firstChild);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Main UI sync — called after every selection change */
|
/** Main UI sync — called after every selection change */
|
||||||
_syncUI() {
|
_syncUI() {
|
||||||
const listHeader = document.querySelector('.list-header');
|
|
||||||
const n = this._selected.size;
|
const n = this._selected.size;
|
||||||
|
|
||||||
// ── Save original header HTML on first use ──
|
const batchSelectionBar = document.getElementById('batch-selection-bar');
|
||||||
if (listHeader && !this._savedHeaderHTML) {
|
|
||||||
this._savedHeaderHTML = listHeader.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (n > 0) {
|
if (n > 0) {
|
||||||
this._barVisible = true;
|
this._barVisible = true;
|
||||||
|
|
||||||
// ── List view: replace header with selection bar ──
|
const countText = n === 1
|
||||||
if (listHeader) {
|
? (this._t('batch.one_selected') || '1 item selected')
|
||||||
listHeader.classList.add('selection-mode');
|
: (this._t('batch.n_selected', { count: n }) || `${n} items selected`);
|
||||||
listHeader.innerHTML = this._buildSelectionBarHTML(n);
|
document.getElementById("batch-bar-count").innerText = countText;
|
||||||
|
|
||||||
// Wire checkbox
|
batchSelectionBar.classList.add('visible');
|
||||||
const cb = document.getElementById('select-all-checkbox');
|
|
||||||
if (cb) cb.addEventListener('change', () => this.toggleAll());
|
|
||||||
|
|
||||||
// Wire action buttons
|
|
||||||
this._wireBarButtons();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Grid view: show floating bar ──
|
|
||||||
this._ensureGridBar();
|
|
||||||
const gridBar = document.getElementById('batch-grid-bar');
|
|
||||||
if (gridBar) {
|
|
||||||
const grid = document.getElementById('files-grid');
|
|
||||||
const gridVisible = grid && grid.style.display !== 'none';
|
|
||||||
if (gridVisible) {
|
|
||||||
gridBar.classList.add('visible');
|
|
||||||
gridBar.innerHTML = `
|
|
||||||
<div class="batch-bar-left">
|
|
||||||
<button class="batch-bar-close" id="batch-grid-close" title="Cancel selection">
|
|
||||||
<i class="fas fa-times"></i>
|
|
||||||
</button>
|
|
||||||
<span class="batch-bar-count">${
|
|
||||||
n === 1
|
|
||||||
? (this._t('batch.one_selected') || '1 item selected')
|
|
||||||
: (this._t('batch.n_selected', { count: n }) || `${n} items selected`)
|
|
||||||
}</span>
|
|
||||||
</div>
|
|
||||||
<div class="batch-bar-actions">
|
|
||||||
<button class="batch-btn" id="batch-fav" title="Add to favorites">
|
|
||||||
<i class="fas fa-star"></i>
|
|
||||||
<span>${this._t('batch.add_favorites') || 'Add to favorites'}</span>
|
|
||||||
</button>
|
|
||||||
<button class="batch-btn" id="batch-move" title="Move or copy">
|
|
||||||
<i class="fas fa-arrows-alt"></i>
|
|
||||||
<span>${this._t('batch.move_copy') || 'Move or copy'}</span>
|
|
||||||
</button>
|
|
||||||
<button class="batch-btn" id="batch-download" title="Download">
|
|
||||||
<i class="fas fa-download"></i>
|
|
||||||
<span>${this._t('actions.download') || 'Download'}</span>
|
|
||||||
</button>
|
|
||||||
<button class="batch-btn batch-btn-danger" id="batch-delete" title="Delete">
|
|
||||||
<i class="fas fa-trash-alt"></i>
|
|
||||||
<span>${this._t('actions.delete') || 'Delete'}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
const closeBtn = document.getElementById('batch-grid-close');
|
|
||||||
if (closeBtn) closeBtn.addEventListener('click', () => this.clear());
|
|
||||||
this._wireBarButtons();
|
|
||||||
} else {
|
|
||||||
gridBar.classList.remove('visible');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this._barVisible = false;
|
this._barVisible = false;
|
||||||
|
|
||||||
// Restore original list header
|
|
||||||
if (listHeader) {
|
|
||||||
listHeader.classList.remove('selection-mode');
|
|
||||||
if (this._savedHeaderHTML) {
|
|
||||||
listHeader.innerHTML = this._savedHeaderHTML;
|
|
||||||
}
|
|
||||||
// Re-wire the select-all checkbox
|
|
||||||
const cb = document.getElementById('select-all-checkbox');
|
|
||||||
if (cb) cb.addEventListener('change', () => this.toggleAll());
|
|
||||||
// Translate restored header (scoped to list header)
|
|
||||||
if (window.i18n && window.i18n.translateElement) window.i18n.translateElement(listHeader);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide grid bar
|
// Hide grid bar
|
||||||
const gridBar = document.getElementById('batch-grid-bar');
|
batchSelectionBar.classList.remove('visible');
|
||||||
if (gridBar) gridBar.classList.remove('visible');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync individual item checkboxes
|
// Sync individual item checkboxes
|
||||||
@@ -301,14 +219,16 @@ const multiSelect = {
|
|||||||
|
|
||||||
/** Wire click handlers on batch action buttons (idempotent per render) */
|
/** Wire click handlers on batch action buttons (idempotent per render) */
|
||||||
_wireBarButtons() {
|
_wireBarButtons() {
|
||||||
const del = document.getElementById('batch-delete');
|
const del = document.getElementById('batch-delete');
|
||||||
const move = document.getElementById('batch-move');
|
const move = document.getElementById('batch-move');
|
||||||
const dl = document.getElementById('batch-download');
|
const dl = document.getElementById('batch-download');
|
||||||
const fav = document.getElementById('batch-fav');
|
const fav = document.getElementById('batch-fav');
|
||||||
if (del) del.onclick = () => this.batchDelete();
|
const closeBtn = document.getElementById('batch-grid-close');
|
||||||
if (move) move.onclick = () => this.batchMove();
|
if (del) del.onclick = () => this.batchDelete();
|
||||||
if (dl) dl.onclick = () => this.batchDownload();
|
if (move) move.onclick = () => this.batchMove();
|
||||||
if (fav) fav.onclick = () => this.batchFavorites();
|
if (dl) dl.onclick = () => this.batchDownload();
|
||||||
|
if (fav) fav.onclick = () => this.batchFavorites();
|
||||||
|
if (closeBtn) closeBtn.onclick = () => this.clear();
|
||||||
},
|
},
|
||||||
|
|
||||||
_syncItemCheckboxes() {
|
_syncItemCheckboxes() {
|
||||||
@@ -522,6 +442,15 @@ const multiSelect = {
|
|||||||
if (e.key === 'Escape' && this.hasSelection) this.clear();
|
if (e.key === 'Escape' && this.hasSelection) this.clear();
|
||||||
if (e.key === 'Delete' && this.hasSelection) this.batchDelete();
|
if (e.key === 'Delete' && this.hasSelection) this.batchDelete();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const batchSelectionBar = document.getElementById('batch-selection-bar');
|
||||||
|
batchSelectionBar.innerHTML = this._buildSelectionBarHTML();
|
||||||
|
|
||||||
|
if (window.i18n && window.i18n.translateElement) {
|
||||||
|
window.i18n.translateElement(batchSelectionBar);
|
||||||
|
}
|
||||||
|
this._wireBarButtons();
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_injectListHeaderCheckbox() {
|
_injectListHeaderCheckbox() {
|
||||||
@@ -533,7 +462,7 @@ const multiSelect = {
|
|||||||
_hookGlobalDeselect() {
|
_hookGlobalDeselect() {
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
if (window.__rubberBandJustFinished) return;
|
if (window.__rubberBandJustFinished) return;
|
||||||
if (e.target.closest('.file-card, .file-item, .context-menu, .batch-action-bar, .list-header.selection-mode, .about-modal, .rename-dialog, .share-dialog, .confirm-dialog, .modal-overlay, input, button')) 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();
|
if (this.hasSelection) this.clear();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user