Replace batch mode toggle with click/Shift/Ctrl selection
- Remove separate 'select mode' toggle and checkbox UI - Click any entry to select it (blue highlight border) - Shift+click for range selection between two entries - Ctrl/Cmd+click to toggle individual entries - Click entry background or press Escape to clear selection - Batch action bar appears automatically when items are selected - Selected entries get accent-color border and highlight - Single delEntry() cleans up selectedIds - Shortcut help updated with selection tips
This commit is contained in:
@@ -19,8 +19,8 @@ let searchQuery = '';
|
||||
let idleT, warnT, countT;
|
||||
let draggedId = null;
|
||||
let showTrash = false;
|
||||
let selectMode = false;
|
||||
let selectedIds = new Set();
|
||||
let lastSelectedId = null;
|
||||
|
||||
// ==================== SOUND ====================
|
||||
let soundEnabled = localStorage.getItem('soundEnabled') !== 'false';
|
||||
@@ -657,7 +657,8 @@ function render() {
|
||||
if (view === 'table') {
|
||||
let h = '<table><thead><tr><th></th><th>Site</th>' + (showMail ? '<th>User</th>' : '') + '<th>Password</th>' + (!showTrash ? '<th>Folder</th>' : '<th>Deleted</th>') + '<th>Actions</th></tr></thead><tbody>';
|
||||
filtered.forEach(e => {
|
||||
h += '<tr class="table-row-drag' + (e.favorite ? ' favorite' : '') + '" draggable="true" data-id="' + e.id + '">' +
|
||||
const sel = selectedIds.has(e.id);
|
||||
h += '<tr class="table-row-drag' + (e.favorite ? ' favorite' : '') + (sel ? ' selected' : '') + '" draggable="true" data-id="' + e.id + '" onclick="toggleSelectEntry(' + e.id + ', event)">' +
|
||||
'<td>' + (e.favorite ? '⭐' : '') + '</td>' +
|
||||
'<td>🌐 ' + highlightText(e.site, searchQuery) + '</td>' +
|
||||
(showMail ? '<td>👤 ' + highlightText(e.username, searchQuery) + '</td>' : '') +
|
||||
@@ -665,7 +666,6 @@ function render() {
|
||||
if (!showTrash) {
|
||||
h += '<td><span class="entry-folder">📁 ' + esc(e.folder || 'All') + '</span></td>' +
|
||||
'<td class="actions-cell">' +
|
||||
(selectMode ? `<input type="checkbox" class="select-checkbox" style="position:static;margin-right:8px;" ${selectedIds.has(e.id) ? 'checked' : ''} onclick="event.stopPropagation();toggleSelectEntry(${e.id})">` : '') +
|
||||
'<button class="star-btn" data-id="' + e.id + '">' + (e.favorite ? '⭐' : '☆') + '</button> ' +
|
||||
'<button class="icon-btn copy-p" data-id="' + e.id + '">📋</button> ' +
|
||||
'<button class="edit-btn" data-id="' + e.id + '" style="position:static;display:inline-flex;vertical-align:middle;">✏️</button> ' +
|
||||
@@ -674,7 +674,6 @@ function render() {
|
||||
} else {
|
||||
h += '<td><span class="trash-badge">🗑️ ' + timeAgo(e.deleted_at) + '</span></td>' +
|
||||
'<td class="actions-cell">' +
|
||||
(selectMode ? `<input type="checkbox" class="select-checkbox" style="position:static;margin-right:8px;" ${selectedIds.has(e.id) ? 'checked' : ''} onclick="event.stopPropagation();toggleSelectEntry(${e.id})">` : '') +
|
||||
'<button class="restore-btn" onclick="restoreEntry(' + e.id + ')">↩️</button> ' +
|
||||
'<button class="delete-btn" data-id="' + e.id + '" style="position:static;display:inline-flex;vertical-align:middle;">✕</button>' +
|
||||
'</td>';
|
||||
@@ -695,10 +694,8 @@ function render() {
|
||||
}
|
||||
|
||||
function gridC(e) {
|
||||
let html = '<div class="entry-card' + (e.favorite ? ' favorite' : '') + '" draggable="' + (!selectMode) + '" data-id="' + e.id + '">';
|
||||
if (selectMode) {
|
||||
html += `<input type="checkbox" class="select-checkbox" ${selectedIds.has(e.id) ? 'checked' : ''} onclick="event.stopPropagation();toggleSelectEntry(${e.id})">`;
|
||||
}
|
||||
const sel = selectedIds.has(e.id);
|
||||
let html = '<div class="entry-card' + (e.favorite ? ' favorite' : '') + (sel ? ' selected' : '') + '" draggable="true" data-id="' + e.id + '" onclick="toggleSelectEntry(' + e.id + ', event)">';
|
||||
html += '<div class="action-btns">';
|
||||
if (showTrash) {
|
||||
html += '<button class="restore-btn" onclick="restoreEntry(' + e.id + ')">↩️</button>';
|
||||
@@ -722,10 +719,8 @@ function gridC(e) {
|
||||
return html;
|
||||
}
|
||||
function listC(e) {
|
||||
let html = '<div class="entry-row' + (e.favorite ? ' favorite' : '') + '" draggable="' + (!selectMode) + '" data-id="' + e.id + '">';
|
||||
if (selectMode) {
|
||||
html += `<input type="checkbox" class="select-checkbox" ${selectedIds.has(e.id) ? 'checked' : ''} onclick="event.stopPropagation();toggleSelectEntry(${e.id})">`;
|
||||
}
|
||||
const sel = selectedIds.has(e.id);
|
||||
let html = '<div class="entry-row' + (e.favorite ? ' favorite' : '') + (sel ? ' selected' : '') + '" draggable="true" data-id="' + e.id + '" onclick="toggleSelectEntry(' + e.id + ', event)">';
|
||||
html += '<div class="action-btns">';
|
||||
if (showTrash) {
|
||||
html += '<button class="restore-btn" onclick="restoreEntry(' + e.id + ')">↩️</button>';
|
||||
@@ -749,10 +744,8 @@ function listC(e) {
|
||||
return html;
|
||||
}
|
||||
function compC(e) {
|
||||
let html = '<div class="entry-compact' + (e.favorite ? ' favorite' : '') + '" draggable="' + (!selectMode) + '" data-id="' + e.id + '">';
|
||||
if (selectMode) {
|
||||
html += `<input type="checkbox" class="select-checkbox" ${selectedIds.has(e.id) ? 'checked' : ''} onclick="event.stopPropagation();toggleSelectEntry(${e.id})">`;
|
||||
}
|
||||
const sel = selectedIds.has(e.id);
|
||||
let html = '<div class="entry-compact' + (e.favorite ? ' favorite' : '') + (sel ? ' selected' : '') + '" draggable="true" data-id="' + e.id + '" onclick="toggleSelectEntry(' + e.id + ', event)">';
|
||||
html += '<div class="action-btns">';
|
||||
if (showTrash) {
|
||||
html += '<button class="restore-btn" onclick="restoreEntry(' + e.id + ')">↩️</button>';
|
||||
@@ -911,37 +904,42 @@ async function saveEdit() {
|
||||
else { const d = await r.json(); toast('❌ ' + (d.error || 'Failed'), 'error'); }
|
||||
} catch (e) { toast('⚠️ Error', 'error'); }
|
||||
}
|
||||
//======================== batch operations ==========================
|
||||
function toggleSelectMode() {
|
||||
selectMode = !selectMode;
|
||||
document.getElementById('selectBtn').textContent = selectMode ? '☑ Deselect' : '☐ Select';
|
||||
document.getElementById('selectBtn').classList.toggle('active', selectMode);
|
||||
if (!selectMode) {
|
||||
selectedIds.clear();
|
||||
hideBatchBar();
|
||||
//======================== batch selection ==========================
|
||||
function toggleSelectEntry(id, e) {
|
||||
if (e?.shiftKey && lastSelectedId !== null) {
|
||||
const ids = getFilteredEntries().map(x => x.id);
|
||||
const i1 = ids.indexOf(lastSelectedId);
|
||||
const i2 = ids.indexOf(id);
|
||||
if (i1 > -1 && i2 > -1) {
|
||||
const start = Math.min(i1, i2), end = Math.max(i1, i2);
|
||||
for (let i = start; i <= end; i++) selectedIds.add(ids[i]);
|
||||
}
|
||||
} else if (e?.ctrlKey || e?.metaKey) {
|
||||
if (selectedIds.has(id)) selectedIds.delete(id); else selectedIds.add(id);
|
||||
} else {
|
||||
if (selectedIds.size === 1 && selectedIds.has(id)) { selectedIds.clear(); }
|
||||
else { selectedIds.clear(); selectedIds.add(id); }
|
||||
}
|
||||
lastSelectedId = id;
|
||||
updateBatchBar();
|
||||
render();
|
||||
}
|
||||
|
||||
function toggleSelectEntry(id) {
|
||||
if (selectedIds.has(id)) {
|
||||
selectedIds.delete(id);
|
||||
} else {
|
||||
selectedIds.add(id);
|
||||
}
|
||||
updateBatchBar();
|
||||
function clearSelection() {
|
||||
selectedIds.clear();
|
||||
lastSelectedId = null;
|
||||
hideBatchBar();
|
||||
render();
|
||||
}
|
||||
|
||||
function updateBatchBar() {
|
||||
const existing = document.getElementById('batchBar');
|
||||
if (existing) existing.remove();
|
||||
if (selectedIds.size === 0) return;
|
||||
|
||||
const bar = document.createElement('div');
|
||||
bar.id = 'batchBar';
|
||||
bar.className = 'batch-actions';
|
||||
bar.innerHTML = `<span class="selected-count">${selectedIds.size} selected</span>`;
|
||||
|
||||
if (showTrash) {
|
||||
bar.innerHTML += `
|
||||
<button class="btn btn-sm restore-btn" onclick="batchRestore()">↩️ Restore All</button>
|
||||
@@ -956,7 +954,7 @@ function updateBatchBar() {
|
||||
<button class="btn btn-sm btn-danger" onclick="batchDelete()">Delete</button>
|
||||
`;
|
||||
}
|
||||
bar.innerHTML += `<button class="btn btn-sm btn-outline" onclick="toggleSelectMode()">Cancel</button>`;
|
||||
bar.innerHTML += `<button class="btn btn-sm btn-outline" onclick="clearSelection()">Clear</button>`;
|
||||
document.body.appendChild(bar);
|
||||
}
|
||||
|
||||
@@ -968,21 +966,18 @@ function hideBatchBar() {
|
||||
async function batchDelete() {
|
||||
if (!confirm(`Move ${selectedIds.size} entries to trash?`)) return;
|
||||
for (const id of selectedIds) await delEntry(id);
|
||||
selectedIds.clear();
|
||||
hideBatchBar();
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
async function batchPermanentDelete() {
|
||||
if (!confirm(`Permanently delete ${selectedIds.size} entries?`)) return;
|
||||
for (const id of selectedIds) await permanentDelete(id);
|
||||
selectedIds.clear();
|
||||
hideBatchBar();
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
async function batchRestore() {
|
||||
for (const id of selectedIds) await restoreEntry(id);
|
||||
selectedIds.clear();
|
||||
hideBatchBar();
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
async function batchMove() {
|
||||
@@ -999,8 +994,7 @@ async function batchMove() {
|
||||
});
|
||||
}
|
||||
}
|
||||
selectedIds.clear();
|
||||
hideBatchBar();
|
||||
clearSelection();
|
||||
loadEntries();
|
||||
}
|
||||
// ==================== ADD / DELETE ====================
|
||||
@@ -1035,7 +1029,7 @@ async function addEntry() {
|
||||
async function delEntry(id) {
|
||||
try {
|
||||
const r = await fetch(API + '/entries/' + id, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken } });
|
||||
if (r.ok) { order = order.filter(x => x != id); localStorage.setItem('entryOrder', JSON.stringify(order)); toast('📦 Moved to trash'); await loadEntries(); playSound('delete'); }
|
||||
if (r.ok) { selectedIds.delete(id); order = order.filter(x => x != id); localStorage.setItem('entryOrder', JSON.stringify(order)); toast('📦 Moved to trash'); await loadEntries(); playSound('delete'); }
|
||||
} catch (e) { toast('Error', 'error'); }
|
||||
}
|
||||
|
||||
@@ -1090,7 +1084,7 @@ function highlightText(text, query) {
|
||||
function showShortcutsHelp() {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'custom-modal-overlay show';
|
||||
overlay.innerHTML = `<div class="custom-modal" style="min-width:360px"><h3>⌨️ Keyboard Shortcuts</h3><div style="display:grid;grid-template-columns:auto 1fr;gap:0.5rem 1.2rem;font-size:0.85rem;margin-bottom:1rem"><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">Ctrl+N</span><span>New entry</span><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">Ctrl+F</span><span>Search</span><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">Ctrl+T</span><span>Toggle trash</span><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">Ctrl+L</span><span>Lock vault</span><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">Ctrl+S</span><span>Save entry</span><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">Esc</span><span>Close modal / settings</span><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">?</span><span>Show this help</span></div><div class="modal-actions"><button class="btn btn-sm" onclick="this.closest('.custom-modal-overlay').remove()">Got it</button></div></div>`;
|
||||
overlay.innerHTML = `<div class="custom-modal" style="min-width:380px"><h3>⌨️ Keyboard Shortcuts</h3><div style="display:grid;grid-template-columns:auto 1fr;gap:0.5rem 1.2rem;font-size:0.85rem;margin-bottom:1rem"><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">Ctrl+N</span><span>New entry</span><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">Ctrl+F</span><span>Search</span><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">Ctrl+T</span><span>Toggle trash</span><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">Ctrl+L</span><span>Lock vault</span><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">Ctrl+S</span><span>Save entry</span><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">Esc</span><span>Close modal / deselect</span><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">?</span><span>Show this help</span></div><p style="color:var(--text2);font-size:0.8rem;margin-bottom:0.8rem">💡 Click any entry to select, Shift+click for range, Ctrl+click to toggle</p><div class="modal-actions"><button class="btn btn-sm" onclick="this.closest('.custom-modal-overlay').remove()">Got it</button></div></div>`;
|
||||
document.body.appendChild(overlay);
|
||||
overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
|
||||
}
|
||||
@@ -1123,6 +1117,7 @@ if (token && curUser) {
|
||||
}
|
||||
|
||||
['click', 'keypress', 'scroll', 'mousemove'].forEach(e => document.addEventListener(e, () => { if (token) resetIdle(); }));
|
||||
document.getElementById('entriesContainer').addEventListener('click', function(e) { if (e.target === this || (selectedIds.size > 0 && !e.target.closest('.entry-card,.entry-row,.entry-compact,.table-row-drag'))) { clearSelection(); } });
|
||||
document.addEventListener('keypress', e => { if (e.key === 'Enter') { if (document.getElementById('passwordInput') === document.activeElement) addEntry(); else if (document.getElementById('loginPassword') === document.activeElement) login(); else if (document.getElementById('regPassword') === document.activeElement) register(); } });
|
||||
document.getElementById('genModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGen(); });
|
||||
document.getElementById('editModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeEdit(); });
|
||||
@@ -1133,6 +1128,7 @@ document.addEventListener('keydown', function(e) {
|
||||
|
||||
// Escape – close any open modal or settings
|
||||
if (e.key === 'Escape') {
|
||||
if (selectedIds.size > 0) { clearSelection(); return; }
|
||||
if (!document.getElementById('settingsMenu').classList.contains('hidden')) {
|
||||
document.getElementById('settingsMenu').classList.add('hidden');
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user