diff --git a/css/style.css b/css/style.css index 8146348..2b5dfc0 100644 --- a/css/style.css +++ b/css/style.css @@ -368,27 +368,6 @@ input[type="range"]::-webkit-slider-thumb { .chip button:hover { opacity: 1; background: rgba(0,0,0,0.1); } .chip button svg { width: 10px; height: 10px; } -/* Tag suggestions dropdown */ -.chip-suggestions { - position: absolute; - z-index: 50; - background: var(--bg-elev); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - box-shadow: var(--shadow); - padding: 4px; - max-height: 200px; - overflow-y: auto; - min-width: 160px; -} -.chip-suggestion { - padding: 6px 10px; - border-radius: 6px; - font-size: 12px; - cursor: pointer; -} -.chip-suggestion:hover, .chip-suggestion.is-active { background: var(--bg-elev-2); color: var(--text); } -.chip-suggestion-empty { padding: 6px 10px; color: var(--text-faint); font-size: 12px; } /* Settings rows */ .setting-row { diff --git a/index.html b/index.html index ead01ec..f47523f 100644 --- a/index.html +++ b/index.html @@ -1013,60 +1013,6 @@ - - - - - diff --git a/js/app.js b/js/app.js index ec3ef25..f29ab87 100644 --- a/js/app.js +++ b/js/app.js @@ -5026,210 +5026,6 @@ async function requestCloseSlideOver() { return true; } -// ============================================================ -// TAG CHIP INPUT -// ============================================================ -// Local mutable list of tags currently in the entry modal. Synced to the -// hidden #entryTags field on every change so saveEntry can read it. - -let editingTags = []; -let chipSuggestEl = null; -let chipSuggestActive = -1; - -function syncTagsHidden() { - $('#entryTags').value = editingTags.join(','); -} - -function renderChips() { - const container = $('#entryTagsInput'); - // Wipe existing chips but keep the input element - $$('.chip', container).forEach(c => c.remove()); - const input = $('#entryTagsField'); - editingTags.forEach((t, i) => { - const chip = el('span', { class: 'chip' }); - chip.appendChild(el('span', null, t)); - const x = el('button', { - type: 'button', - on: { click: ev => { - ev.stopPropagation(); - editingTags.splice(i, 1); - renderChips(); - syncTagsHidden(); - } }, - }); - x.appendChild(icon('i-x')); - chip.appendChild(x); - container.insertBefore(chip, input); - }); - syncTagsHidden(); -} - -function setEditingTags(arr) { - editingTags = (arr || []).filter(Boolean).map(t => t.trim()).filter(Boolean); - renderChips(); -} - -function addTag(raw) { - const t = (raw || '').trim().replace(/,/g, ''); - if (!t) return; - if (editingTags.includes(t)) return; - editingTags.push(t); - renderChips(); -} - -function closeChipSuggest() { - if (chipSuggestEl) { chipSuggestEl.remove(); chipSuggestEl = null; } - chipSuggestActive = -1; -} - -function openChipSuggest() { - closeChipSuggest(); - const field = $('#entryTagsField'); - const q = field.value.trim().toLowerCase(); - const existing = allTags(); - const candidates = existing - .filter(t => !editingTags.includes(t)) - .filter(t => !q || t.toLowerCase().includes(q)) - .slice(0, 8); - if (!q && candidates.length === 0) return; - - chipSuggestEl = el('div', { class: 'chip-suggestions' }); - if (candidates.length === 0) { - chipSuggestEl.appendChild(el('div', { class: 'chip-suggestion-empty' }, 'Press Enter to create "' + q + '"')); - } else { - candidates.forEach((t, i) => { - const item = el('div', { - class: 'chip-suggestion' + (i === 0 ? ' is-active' : ''), - on: { mousedown: ev => { ev.preventDefault(); addTag(t); field.value = ''; closeChipSuggest(); } }, - }, t); - chipSuggestEl.appendChild(item); - }); - chipSuggestActive = 0; - } - // Position under the chip input - const rect = $('#entryTagsInput').getBoundingClientRect(); - chipSuggestEl.style.position = 'fixed'; - chipSuggestEl.style.top = (rect.bottom + 4) + 'px'; - chipSuggestEl.style.left = rect.left + 'px'; - chipSuggestEl.style.width = Math.max(160, rect.width / 2) + 'px'; - document.body.appendChild(chipSuggestEl); -} - -function moveChipSuggest(dir) { - if (!chipSuggestEl) return; - const items = $$('.chip-suggestion', chipSuggestEl); - if (items.length === 0) return; - items.forEach(it => it.classList.remove('is-active')); - chipSuggestActive = (chipSuggestActive + dir + items.length) % items.length; - items[chipSuggestActive].classList.add('is-active'); -} - -function selectActiveSuggestion() { - if (!chipSuggestEl || chipSuggestActive < 0) return false; - const items = $$('.chip-suggestion', chipSuggestEl); - if (items[chipSuggestActive]) { - addTag(items[chipSuggestActive].textContent); - $('#entryTagsField').value = ''; - closeChipSuggest(); - return true; - } - return false; -} - -// ============================================================ -// ENTRY MODAL (new / edit) -// ============================================================ - -async function openEntryModal(entry) { - populateFolderSelect(); - if (entry) { - $('#entryModalTitle').textContent = 'Edit entry'; - $('#entryId').value = entry.id; - $('#entryTitle').value = entry.title || ''; - $('#entrySite').value = entry.site; - $('#entryUsername').value = entry.username || ''; - $('#entryFolder').value = entry.folder || 'All'; - setEditingTags(parseTags(entry.tags)); - $('#entryPassword').value = await decryptPwd(entry.encrypted_password, entry.iv); - if ($('#entryPassword').value === '[ERROR]') $('#entryPassword').value = ''; - } else { - $('#entryModalTitle').textContent = 'New entry'; - $('#entryId').value = ''; - $('#entryForm').reset(); - $('#entryFolder').value = state.view.startsWith('folder:') ? state.view.slice(7) : 'All'; - setEditingTags([]); - } - $('#entryTagsField').value = ''; - closeChipSuggest(); - updateEntryStrength(); - $('#entryModal').classList.remove('is-hidden'); - $('#entrySite').focus(); -} - -function closeEntryModal() { - $('#entryModal').classList.add('is-hidden'); -} - -function populateFolderSelect() { - const sel = $('#entryFolder'); - sel.innerHTML = ''; - state.folders.forEach(f => sel.appendChild(el('option', { value: f.name }, folderLabel(f.name)))); -} - -async function saveEntry(e) { - e && e.preventDefault(); - // Flush any pending text in the chip input as a final tag - const pending = $('#entryTagsField').value.trim(); - if (pending) { addTag(pending); $('#entryTagsField').value = ''; } - const id = $('#entryId').value; - const title = $('#entryTitle').value.trim(); - const site = $('#entrySite').value.trim(); - const user = $('#entryUsername').value.trim(); - const pwd = $('#entryPassword').value; - const fold = $('#entryFolder').value; - const tags = editingTags.join(','); - if (!site || !pwd) return toast('Site and password required', 'error'); - - const enc = await encryptPwd(pwd); - const body = JSON.stringify(await withEncryptedMeta({ - site, title, username: user, encrypted_password: enc.encrypted, iv: enc.iv, - folder: fold, tags, - })); - try { - let savedId = id ? parseInt(id) : null; - if (id) { - await api('/entries/' + id, { - method: 'PUT', - headers: authHeaders({ 'Content-Type': 'application/json' }), - body, - }); - toast('Updated'); - } else { - const r = await api('/entries', { - method: 'POST', - headers: authHeaders({ 'Content-Type': 'application/json' }), - body, - }); - if (r && typeof r.id === 'number') savedId = r.id; - toast('Saved'); - } - closeEntryModal(); - await loadEntries(); - if (typeof healthCache !== 'undefined') healthCache = null; - render(); - if (savedId) flashEntry(savedId); - // Fire-and-forget favicon fetch for the saved entry. Updates the - // card in place when it arrives. No-op when feature is off or - // the entry already has a cached icon. - if (savedId && state.faviconsEnabled) { - const saved = state.entries.find(e => e.id === savedId); - if (saved) ensureEntryFavicon(saved); // honours the toggle - } - } catch (err) { - toast(err.message, 'error'); - } -} - async function restoreEntry(id) { try { await api('/entries/' + id + '/restore', { method: 'POST', headers: authHeaders() }); @@ -5802,12 +5598,12 @@ function genPassword() { $('#genPreview').textContent = p; } -// genTarget: 'entry' (insert into entry form) or 'standalone' (just copy/dismiss) -let genTarget = 'entry'; +// genTarget: 'slideover' (insert into the editor) or 'standalone' (just copy) +let genTarget = 'slideover'; function openGen(target) { - genTarget = target || 'entry'; - // Show "Use" when targeting an editable field (entry modal or slide-over) + genTarget = target || 'slideover'; + // Show "Use" when targeting the editor; hide it in standalone (copy-only). $('#genUse').style.display = (genTarget === 'standalone') ? 'none' : ''; $('#genModal').classList.remove('is-hidden'); genPassword(); @@ -5832,11 +5628,6 @@ function updateRegStrength() { const p = $('#regPassword').value; $('#regStrengthBar').style.setProperty('--strength', computeStrength(p) + '%'); } -function updateEntryStrength() { - const p = $('#entryPassword').value; - $('#entryStrengthBar').style.setProperty('--strength', computeStrength(p) + '%'); -} - // ============================================================ // COMMAND PALETTE // ============================================================ @@ -7187,7 +6978,6 @@ async function init() { $('#loginForm').addEventListener('submit', doLogin); $('#registerForm').addEventListener('submit', doRegister); $('#regPassword').addEventListener('input', updateRegStrength); - $('#entryPassword').addEventListener('input', updateEntryStrength); // Top-bar function applyViewMode() { @@ -7446,14 +7236,6 @@ async function init() { closeSettings(); }); - // Entry modal - $('#entryForm').addEventListener('submit', saveEntry); - $('#entrySaveBtn').addEventListener('click', saveEntry); - $$('#entryModal [data-close]').forEach(b => b.addEventListener('click', closeEntryModal)); - $('#entryPwToggle').addEventListener('click', () => { - const input = $('#entryPassword'); - input.type = input.type === 'password' ? 'text' : 'password'; - }); $('#loginPwToggle').addEventListener('click', () => { const input = $('#loginPassword'); input.type = input.type === 'password' ? 'text' : 'password'; @@ -7467,30 +7249,6 @@ async function init() { const p = document.getElementById('loginPassword'); if (p) p.focus(); }); - $('#entryPwGen').addEventListener('click', openGen); - - // Chip input (tags) - $('#entryTagsInput').addEventListener('click', () => $('#entryTagsField').focus()); - $('#entryTagsField').addEventListener('keydown', e => { - const field = e.target; - if (e.key === 'Enter' || e.key === ',') { - e.preventDefault(); - if (!selectActiveSuggestion()) { - addTag(field.value); - field.value = ''; - closeChipSuggest(); - } - } else if (e.key === 'Backspace' && !field.value && editingTags.length) { - editingTags.pop(); - renderChips(); - } else if (e.key === 'ArrowDown') { e.preventDefault(); moveChipSuggest(+1); } - else if (e.key === 'ArrowUp') { e.preventDefault(); moveChipSuggest(-1); } - else if (e.key === 'Escape') { closeChipSuggest(); } - }); - $('#entryTagsField').addEventListener('input', openChipSuggest); - $('#entryTagsField').addEventListener('focus', openChipSuggest); - $('#entryTagsField').addEventListener('blur', () => setTimeout(closeChipSuggest, 150)); - // Generator $('#genLen').addEventListener('input', genPassword); $$('#genModal input[type=checkbox]').forEach(c => c.addEventListener('change', genPassword)); @@ -7507,12 +7265,11 @@ async function init() { } }); $('#genUse').addEventListener('click', () => { + // Only the slideover editor consumes a generated password now (the + // 'standalone' target hides #genUse). The old entry-modal target is gone. if (genTarget === 'slideover') { const soPw = $('#soPassword'); if (soPw) { soPw.value = genCurrent; soDirtyCheck(); } - } else { - $('#entryPassword').value = genCurrent; - updateEntryStrength(); } closeGen(); }); @@ -8085,10 +7842,6 @@ async function init() { closeHistoryModal(); return; } - if (!$('#entryModal').classList.contains('is-hidden')) { - closeEntryModal(); - return; - } if (!$('#genModal').classList.contains('is-hidden')) { closeGen(); return;