04/17/2026 update add item modal disappears when lose focus on it
This commit is contained in:
+58
-58
@@ -187,11 +187,11 @@ const Vault = (() => {
|
||||
function getSortedItems(items) {
|
||||
const sorted = [...items];
|
||||
switch (_sortOrder) {
|
||||
case 'name-asc': sorted.sort((a,b) => a.name.localeCompare(b.name)); break;
|
||||
case 'name-desc': sorted.sort((a,b) => b.name.localeCompare(a.name)); break;
|
||||
case 'date-desc': sorted.sort((a,b) => new Date(b.created_at) - new Date(a.created_at)); break;
|
||||
case 'date-asc': sorted.sort((a,b) => new Date(a.created_at) - new Date(b.created_at)); break;
|
||||
case 'folder': sorted.sort((a,b) => folderName(a.folder_id).localeCompare(folderName(b.folder_id)) || a.name.localeCompare(b.name)); break;
|
||||
case 'name-asc': sorted.sort((a, b) => a.name.localeCompare(b.name)); break;
|
||||
case 'name-desc': sorted.sort((a, b) => b.name.localeCompare(a.name)); break;
|
||||
case 'date-desc': sorted.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); break;
|
||||
case 'date-asc': sorted.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); break;
|
||||
case 'folder': sorted.sort((a, b) => folderName(a.folder_id).localeCompare(folderName(b.folder_id)) || a.name.localeCompare(b.name)); break;
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
@@ -238,24 +238,24 @@ const Vault = (() => {
|
||||
li.className = 'vault-item';
|
||||
li.dataset.id = item.id;
|
||||
|
||||
const iconMap = { password:'🔑', note:'📝', card:'💳', bank:'🏦', address:'🏠', ssn:'🪪', passkey:'🔐' };
|
||||
const iconMap = { password: '🔑', note: '📝', card: '💳', bank: '🏦', address: '🏠', ssn: '🪪', passkey: '🔐' };
|
||||
const icon = iconMap[item.item_type] || '🔑';
|
||||
|
||||
let subText = '';
|
||||
if (item.plain) {
|
||||
switch (item.item_type) {
|
||||
case 'password': subText = item.plain.username || item.plain.url || ''; break;
|
||||
case 'note': subText = (item.plain.note_body || '').slice(0, 60); break;
|
||||
case 'card': subText = item.plain.card_number ? '•••• ' + String(item.plain.card_number).replace(/\s/g,'').slice(-4) : ''; break;
|
||||
case 'bank': subText = item.plain.bank_name || ''; break;
|
||||
case 'address': subText = [item.plain.first_name, item.plain.last_name].filter(Boolean).join(' '); break;
|
||||
case 'ssn': subText = '•••-••-••••'; break;
|
||||
case 'note': subText = (item.plain.note_body || '').slice(0, 60); break;
|
||||
case 'card': subText = item.plain.card_number ? '•••• ' + String(item.plain.card_number).replace(/\s/g, '').slice(-4) : ''; break;
|
||||
case 'bank': subText = item.plain.bank_name || ''; break;
|
||||
case 'address': subText = [item.plain.first_name, item.plain.last_name].filter(Boolean).join(' '); break;
|
||||
case 'ssn': subText = '•••-••-••••'; break;
|
||||
}
|
||||
}
|
||||
|
||||
const showCopyUser = item.item_type === 'password';
|
||||
const showCopyPass = ['password','card','bank','ssn'].includes(item.item_type);
|
||||
const showLaunch = item.item_type === 'password' && item.plain?.url;
|
||||
const showCopyPass = ['password', 'card', 'bank', 'ssn'].includes(item.item_type);
|
||||
const showLaunch = item.item_type === 'password' && item.plain?.url;
|
||||
|
||||
li.innerHTML = `
|
||||
<div class="item-icon">${icon}</div>
|
||||
@@ -264,7 +264,7 @@ const Vault = (() => {
|
||||
<span class="item-sub">${escHtml(subText)}</span>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
${showLaunch ? `<button class="btn-icon" title="Open URL" data-action="launch">🌐</button>` : ''}
|
||||
${showLaunch ? `<button class="btn-icon" title="Open URL" data-action="launch">🌐</button>` : ''}
|
||||
${showCopyUser ? `<button class="btn-icon" title="Copy username" data-action="copy-user">👤</button>` : ''}
|
||||
${showCopyPass ? `<button class="btn-icon" title="Copy secret" data-action="copy-pass">📋</button>` : ''}
|
||||
<button class="btn-icon" title="Edit" data-action="edit">✏️</button>
|
||||
@@ -289,8 +289,8 @@ const Vault = (() => {
|
||||
e.stopPropagation();
|
||||
const s = item.item_type === 'password' ? item.plain?.password
|
||||
: item.item_type === 'card' ? item.plain?.cvv
|
||||
: item.item_type === 'bank' ? item.plain?.account_number
|
||||
: item.plain?.ssn_number;
|
||||
: item.item_type === 'bank' ? item.plain?.account_number
|
||||
: item.plain?.ssn_number;
|
||||
copyToClipboard(s || '', 'Copied to clipboard');
|
||||
});
|
||||
}
|
||||
@@ -328,7 +328,7 @@ const Vault = (() => {
|
||||
const old = pwItems.filter(i => new Date(i.created_at).getTime() < cutoff);
|
||||
|
||||
const total = pwItems.length;
|
||||
const score = Math.max(0, Math.round(100 - (weak.length/total)*40 - (reused.length/total)*30 - (old.length/total)*15));
|
||||
const score = Math.max(0, Math.round(100 - (weak.length / total) * 40 - (reused.length / total) * 30 - (old.length / total) * 15));
|
||||
const cls = score >= 80 ? 'score-good' : score >= 50 ? 'score-fair' : 'score-poor';
|
||||
const label = score >= 80 ? 'Good' : score >= 50 ? 'Fair' : 'Needs attention';
|
||||
|
||||
@@ -473,9 +473,9 @@ const Vault = (() => {
|
||||
<span class="share-meta">From ${escHtml(s.owner_email)}</span>
|
||||
</div>
|
||||
${!s.accepted
|
||||
? `<button class="btn-primary btn-sm" data-accept="${s.id}">Accept</button>`
|
||||
: `<button class="btn-secondary btn-sm" data-view-share="${s.id}" data-owner-key="${escHtml(s.owner_public_key || '')}" data-enc="${escHtml(s.enc_data)}" data-iv="${escHtml(s.iv)}" data-name="${escHtml(s.item_name)}" data-type="${escHtml(s.item_type || '')}">View</button>`
|
||||
}
|
||||
? `<button class="btn-primary btn-sm" data-accept="${s.id}">Accept</button>`
|
||||
: `<button class="btn-secondary btn-sm" data-view-share="${s.id}" data-owner-key="${escHtml(s.owner_public_key || '')}" data-enc="${escHtml(s.enc_data)}" data-iv="${escHtml(s.iv)}" data-name="${escHtml(s.item_name)}" data-type="${escHtml(s.item_type || '')}">View</button>`
|
||||
}
|
||||
</li>`).join('');
|
||||
|
||||
ul.querySelectorAll('[data-accept]').forEach(btn => {
|
||||
@@ -836,8 +836,8 @@ const Vault = (() => {
|
||||
</button>
|
||||
<div class="em-vault-item-body hidden" id="em-vault-body-${idx}">
|
||||
${hasFields
|
||||
? '<div class="detail-body em-detail-body"></div>'
|
||||
: '<p class="vault-empty">Could not decrypt this item.</p>'}
|
||||
? '<div class="detail-body em-detail-body"></div>'
|
||||
: '<p class="vault-empty">Could not decrypt this item.</p>'}
|
||||
</div>
|
||||
</li>`;
|
||||
}).join('');
|
||||
@@ -866,18 +866,18 @@ const Vault = (() => {
|
||||
// ── Password Generator ────────────────────────────────────────────────
|
||||
|
||||
const GEN_CHARSETS = {
|
||||
upper: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
||||
lower: 'abcdefghijklmnopqrstuvwxyz',
|
||||
digits: '0123456789',
|
||||
upper: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
||||
lower: 'abcdefghijklmnopqrstuvwxyz',
|
||||
digits: '0123456789',
|
||||
symbols: '!@#$%^&*()-_=+[]{}|;:,.<>?',
|
||||
};
|
||||
|
||||
function generatePassword(length = 20, opts = { upper: true, lower: true, digits: true, symbols: true }) {
|
||||
let pool = '';
|
||||
const required = [];
|
||||
if (opts.upper) { pool += GEN_CHARSETS.upper; required.push(GEN_CHARSETS.upper); }
|
||||
if (opts.lower) { pool += GEN_CHARSETS.lower; required.push(GEN_CHARSETS.lower); }
|
||||
if (opts.digits) { pool += GEN_CHARSETS.digits; required.push(GEN_CHARSETS.digits); }
|
||||
if (opts.upper) { pool += GEN_CHARSETS.upper; required.push(GEN_CHARSETS.upper); }
|
||||
if (opts.lower) { pool += GEN_CHARSETS.lower; required.push(GEN_CHARSETS.lower); }
|
||||
if (opts.digits) { pool += GEN_CHARSETS.digits; required.push(GEN_CHARSETS.digits); }
|
||||
if (opts.symbols) { pool += GEN_CHARSETS.symbols; required.push(GEN_CHARSETS.symbols); }
|
||||
if (!pool) pool = GEN_CHARSETS.lower;
|
||||
|
||||
@@ -902,7 +902,7 @@ const Vault = (() => {
|
||||
function scorePassword(pw) {
|
||||
if (!pw) return { score: 0, label: '', cls: '' };
|
||||
let score = 0;
|
||||
if (pw.length >= 8) score++;
|
||||
if (pw.length >= 8) score++;
|
||||
if (pw.length >= 12) score++;
|
||||
if (pw.length >= 16) score++;
|
||||
if (/[A-Z]/.test(pw)) score++;
|
||||
@@ -912,7 +912,7 @@ const Vault = (() => {
|
||||
// clamp to 5 bands
|
||||
const band = Math.min(5, Math.ceil(score / 7 * 5));
|
||||
const labels = ['', 'Very Weak', 'Weak', 'Fair', 'Strong', 'Very Strong'];
|
||||
const clses = ['', 'strength-1','strength-2','strength-3','strength-4','strength-5'];
|
||||
const clses = ['', 'strength-1', 'strength-2', 'strength-3', 'strength-4', 'strength-5'];
|
||||
return { score: band, label: labels[band], cls: clses[band] };
|
||||
}
|
||||
|
||||
@@ -995,16 +995,16 @@ const Vault = (() => {
|
||||
</div>`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const lengthSlider = document.getElementById('gen-length');
|
||||
const lengthSlider = document.getElementById('gen-length');
|
||||
const lengthDisplay = document.getElementById('gen-length-display');
|
||||
const output = document.getElementById('gen-output');
|
||||
const strengthEl = document.getElementById('gen-strength');
|
||||
const output = document.getElementById('gen-output');
|
||||
const strengthEl = document.getElementById('gen-strength');
|
||||
|
||||
function refreshGen() {
|
||||
const pw = generatePassword(parseInt(lengthSlider.value), {
|
||||
upper: document.getElementById('gen-upper').checked,
|
||||
lower: document.getElementById('gen-lower').checked,
|
||||
digits: document.getElementById('gen-digits').checked,
|
||||
upper: document.getElementById('gen-upper').checked,
|
||||
lower: document.getElementById('gen-lower').checked,
|
||||
digits: document.getElementById('gen-digits').checked,
|
||||
symbols: document.getElementById('gen-symbols').checked,
|
||||
});
|
||||
output.value = pw;
|
||||
@@ -1017,7 +1017,7 @@ const Vault = (() => {
|
||||
lengthDisplay.textContent = lengthSlider.value;
|
||||
refreshGen();
|
||||
});
|
||||
['gen-upper','gen-lower','gen-digits','gen-symbols'].forEach(id => {
|
||||
['gen-upper', 'gen-lower', 'gen-digits', 'gen-symbols'].forEach(id => {
|
||||
document.getElementById(id).addEventListener('change', refreshGen);
|
||||
});
|
||||
document.getElementById('btn-regenerate').addEventListener('click', refreshGen);
|
||||
@@ -1036,9 +1036,9 @@ const Vault = (() => {
|
||||
const lengthSlider = document.getElementById('gen-length');
|
||||
document.getElementById('gen-length-display').textContent = lengthSlider.value;
|
||||
const pw = generatePassword(parseInt(lengthSlider.value), {
|
||||
upper: document.getElementById('gen-upper').checked,
|
||||
lower: document.getElementById('gen-lower').checked,
|
||||
digits: document.getElementById('gen-digits').checked,
|
||||
upper: document.getElementById('gen-upper').checked,
|
||||
lower: document.getElementById('gen-lower').checked,
|
||||
digits: document.getElementById('gen-digits').checked,
|
||||
symbols: document.getElementById('gen-symbols').checked,
|
||||
});
|
||||
document.getElementById('gen-output').value = pw;
|
||||
@@ -1269,7 +1269,7 @@ const Vault = (() => {
|
||||
|
||||
// ── Item Modal ────────────────────────────────────────────────────────────
|
||||
|
||||
const TYPE_LABELS = { password:'🔑 Password', note:'📝 Secure Note', card:'💳 Payment Card', bank:'🏦 Bank Account', address:'🏠 Address', ssn:'🪪 Identity (SSN)' };
|
||||
const TYPE_LABELS = { password: '🔑 Password', note: '📝 Secure Note', card: '💳 Payment Card', bank: '🏦 Bank Account', address: '🏠 Address', ssn: '🪪 Identity (SSN)' };
|
||||
|
||||
function switchModalType(type) {
|
||||
document.querySelectorAll('.type-fields').forEach(el => {
|
||||
@@ -1307,19 +1307,19 @@ const Vault = (() => {
|
||||
if (item.plain) {
|
||||
const p = item.plain;
|
||||
switch (t) {
|
||||
case 'password': setVal('field-url',p.url); setVal('field-username',p.username); setVal('field-password',p.password); setVal('field-notes',p.notes); break;
|
||||
case 'note': setVal('field-note-body',p.note_body); break;
|
||||
case 'card': setVal('field-cardholder',p.cardholder_name); setVal('field-card-number',p.card_number); setVal('field-expiry-month',p.expiry_month); setVal('field-expiry-year',p.expiry_year); setVal('field-cvv',p.cvv); setVal('field-notes',p.notes); break;
|
||||
case 'bank': setVal('field-bank-name',p.bank_name); setVal('field-account-type',p.account_type); setVal('field-routing',p.routing_number); setVal('field-account-number',p.account_number); setVal('field-notes',p.notes); break;
|
||||
case 'address': ['first_name','last_name','company','address_line','city','state','zip','country','phone','email'].forEach(k => setVal('field-'+k.replace(/_/g,'-'), p[k])); setVal('field-notes',p.notes); break;
|
||||
case 'ssn': setVal('field-ssn-number',p.ssn_number); setVal('field-notes',p.notes); break;
|
||||
case 'password': setVal('field-url', p.url); setVal('field-username', p.username); setVal('field-password', p.password); setVal('field-notes', p.notes); break;
|
||||
case 'note': setVal('field-note-body', p.note_body); break;
|
||||
case 'card': setVal('field-cardholder', p.cardholder_name); setVal('field-card-number', p.card_number); setVal('field-expiry-month', p.expiry_month); setVal('field-expiry-year', p.expiry_year); setVal('field-cvv', p.cvv); setVal('field-notes', p.notes); break;
|
||||
case 'bank': setVal('field-bank-name', p.bank_name); setVal('field-account-type', p.account_type); setVal('field-routing', p.routing_number); setVal('field-account-number', p.account_number); setVal('field-notes', p.notes); break;
|
||||
case 'address': ['first_name', 'last_name', 'company', 'address_line', 'city', 'state', 'zip', 'country', 'phone', 'email'].forEach(k => setVal('field-' + k.replace(/_/g, '-'), p[k])); setVal('field-notes', p.notes); break;
|
||||
case 'ssn': setVal('field-ssn-number', p.ssn_number); setVal('field-notes', p.notes); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
document.getElementById('item-modal').classList.add('open');
|
||||
document.getElementById('field-name').focus();
|
||||
if ((mode === 'add' && (document.getElementById('field-type').value || 'password') === 'password') ||
|
||||
(mode === 'edit' && (item?.item_type || 'password') === 'password')) {
|
||||
(mode === 'edit' && (item?.item_type || 'password') === 'password')) {
|
||||
initPasswordFieldEnhancements();
|
||||
}
|
||||
}
|
||||
@@ -1395,7 +1395,7 @@ const Vault = (() => {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${sessionStorage.getItem('access_token') || ''}`, 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken() },
|
||||
body: JSON.stringify({ refresh_token: refreshToken }),
|
||||
}).catch(() => {});
|
||||
}).catch(() => { });
|
||||
VaultSession.clear();
|
||||
SharingSession.clear();
|
||||
sessionStorage.removeItem('access_token');
|
||||
@@ -1407,15 +1407,15 @@ const Vault = (() => {
|
||||
// ── Utilities ─────────────────────────────────────────────────────────────
|
||||
|
||||
function copyToClipboard(text, msg = 'Copied') {
|
||||
navigator.clipboard.writeText(text).then(() => showToast(msg)).catch(() => {});
|
||||
navigator.clipboard.writeText(text).then(() => showToast(msg)).catch(() => { });
|
||||
}
|
||||
|
||||
function escHtml(str) {
|
||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function itemIcon(type) {
|
||||
return { password:'🔑', note:'📝', card:'💳', bank:'🏦', address:'🏠', ssn:'🪪' }[type] || '🔑';
|
||||
return { password: '🔑', note: '📝', card: '💳', bank: '🏦', address: '🏠', ssn: '🪪' }[type] || '🔑';
|
||||
}
|
||||
|
||||
function showToast(message, type = 'success') {
|
||||
@@ -1524,7 +1524,7 @@ const Vault = (() => {
|
||||
document.querySelectorAll('[data-type-filter]').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const type = el.dataset.typeFilter;
|
||||
const labels = { password:'Passwords', note:'Secure Notes', card:'Payment Cards', bank:'Bank Accounts', address:'Addresses', ssn:'Identities' };
|
||||
const labels = { password: 'Passwords', note: 'Secure Notes', card: 'Payment Cards', bank: 'Bank Accounts', address: 'Addresses', ssn: 'Identities' };
|
||||
_activeFilter = { type: 'itemType', value: type };
|
||||
switchView('vault');
|
||||
document.querySelectorAll('.sidebar-item').forEach(s => s.classList.remove('active'));
|
||||
@@ -1534,8 +1534,8 @@ const Vault = (() => {
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('item-modal')?.addEventListener('click', e => { if (e.target.id === 'item-modal') closeModal(); });
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') { closeModal(); closeModal('share-modal'); closeModal('emergency-modal'); closeModal('settings-modal'); closeModal('detail-modal'); closeModal('gen-modal'); } });
|
||||
document.getElementById('item-modal')?.addEventListener('click', e => { /* backdrop click intentionally disabled — modal closes only via Cancel or Save */ });
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') { closeModal('share-modal'); closeModal('emergency-modal'); closeModal('settings-modal'); closeModal('detail-modal'); closeModal('gen-modal'); } });
|
||||
|
||||
// Detail modal (shared item view)
|
||||
const closeDetailModal = () => closeModal('detail-modal');
|
||||
@@ -1544,8 +1544,8 @@ const Vault = (() => {
|
||||
document.getElementById('detail-modal')?.addEventListener('click', e => { if (e.target.id === 'detail-modal') closeDetailModal(); });
|
||||
|
||||
document.getElementById('field-card-number')?.addEventListener('input', e => {
|
||||
let v = e.target.value.replace(/\D/g,'').slice(0,16);
|
||||
e.target.value = v.replace(/(.{4})/g,'$1 ').trim();
|
||||
let v = e.target.value.replace(/\D/g, '').slice(0, 16);
|
||||
e.target.value = v.replace(/(.{4})/g, '$1 ').trim();
|
||||
});
|
||||
|
||||
// Settings modal
|
||||
@@ -1606,4 +1606,4 @@ const Vault = (() => {
|
||||
return { init };
|
||||
})();
|
||||
|
||||
document.addEventListener('DOMContentLoaded', Vault.init);
|
||||
document.addEventListener('DOMContentLoaded', Vault.init);
|
||||
Reference in New Issue
Block a user