diff --git a/css/style.css b/css/style.css index 87cf5d1..3ade320 100644 --- a/css/style.css +++ b/css/style.css @@ -136,8 +136,9 @@ input:focus, select:focus { border-color:var(--accent); } #entriesContainer.table-view th { text-align:left; padding:0.4rem 0.6rem; color:var(--text2); font-size:0.75rem; border-bottom:1px solid var(--border); } #entriesContainer.table-view td { padding:0.4rem 0.6rem; font-size:0.8rem; border-bottom:1px solid rgba(255,255,255,0.03); } -.entry-card, .entry-row, .entry-compact, .table-row-drag { cursor:grab; user-select:none; } +.entry-card, .entry-row, .entry-compact, .table-row-drag { cursor:pointer; user-select:none; } .entry-card.drag-over, .entry-row.drag-over, .entry-compact.drag-over, .table-row-drag.drag-over { border-color:var(--accent)!important; box-shadow:0 0 15px rgba(59,130,246,0.3); } +.entry-card.selected, .entry-row.selected, .entry-compact.selected, .table-row-drag.selected { border-color:var(--accent)!important; box-shadow:0 0 0 2px var(--accent); background:rgba(59,130,246,0.08); } .entry-card { background:var(--card); border:1px solid var(--border); border-radius:0.8rem; padding:0.9rem 2.8rem 0.9rem 0.9rem; transition:0.2s; position:relative; word-break:break-word; } .entry-card:hover { border-color:rgba(255,255,255,0.15); transform:translateY(-2px); } .card-site { font-weight:700; font-size:0.9rem; color:var(--text); margin-bottom:0.2rem; } diff --git a/index.html b/index.html index f986e42..5cb64c5 100644 --- a/index.html +++ b/index.html @@ -155,7 +155,6 @@
-
diff --git a/js/app.js b/js/app.js index 44121e4..fb0a9d8 100644 --- a/js/app.js +++ b/js/app.js @@ -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 = '' + (showMail ? '' : '') + '' + (!showTrash ? '' : '') + ''; filtered.forEach(e => { - h += '' + + const sel = selectedIds.has(e.id); + h += '' + '' + '' + (showMail ? '' : '') + @@ -665,7 +666,6 @@ function render() { if (!showTrash) { h += '' + '' + ''; @@ -695,10 +694,8 @@ function render() { } function gridC(e) { - let html = '
'; - if (selectMode) { - html += ``; - } + const sel = selectedIds.has(e.id); + let html = '
'; html += '
'; if (showTrash) { html += ''; @@ -722,10 +719,8 @@ function gridC(e) { return html; } function listC(e) { - let html = '
'; - if (selectMode) { - html += ``; - } + const sel = selectedIds.has(e.id); + let html = '
'; html += '
'; if (showTrash) { html += ''; @@ -749,10 +744,8 @@ function listC(e) { return html; } function compC(e) { - let html = '
'; - if (selectMode) { - html += ``; - } + const sel = selectedIds.has(e.id); + let html = '
'; html += '
'; if (showTrash) { html += ''; @@ -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 = `${selectedIds.size} selected`; - if (showTrash) { bar.innerHTML += ` @@ -956,7 +954,7 @@ function updateBatchBar() { `; } - bar.innerHTML += ``; + bar.innerHTML += ``; 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 = `

âŒ¨ī¸ Keyboard Shortcuts

Ctrl+NNew entryCtrl+FSearchCtrl+TToggle trashCtrl+LLock vaultCtrl+SSave entryEscClose modal / settings?Show this help
`; + overlay.innerHTML = `

âŒ¨ī¸ Keyboard Shortcuts

Ctrl+NNew entryCtrl+FSearchCtrl+TToggle trashCtrl+LLock vaultCtrl+SSave entryEscClose modal / deselect?Show this help

💡 Click any entry to select, Shift+click for range, Ctrl+click to toggle

`; 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;
SiteUserPasswordFolderDeletedActions
' + (e.favorite ? '⭐' : '') + '🌐 ' + highlightText(e.site, searchQuery) + '👤 ' + highlightText(e.username, searchQuery) + '📁 ' + esc(e.folder || 'All') + '' + - (selectMode ? `` : '') + ' ' + ' ' + ' ' + @@ -674,7 +674,6 @@ function render() { } else { h += 'đŸ—‘ī¸ ' + timeAgo(e.deleted_at) + '' + - (selectMode ? `` : '') + ' ' + '' + '