/** * extension/content/content.js — PassKeeper content script. * * 1. Detects login forms → notifies background (badge count). * 2. Injects a small autofill button next to password fields when vault * has matching credentials for the current site. * 3. Listens for DO_AUTOFILL from the popup → fills fields. * 4. Watches form submissions → shows save-credentials banner. */ (() => { 'use strict'; const PK_ATTR = 'data-pk-decorated'; const PK_BTN_CLASS = '__pk_autofill_btn__'; let _bannerEl = null; let _hasNotifiedForm = false; let _formObserver = null; // ── Helpers ────────────────────────────────────────────────────────────────── function escHtml(str) { return String(str ?? '').replace(/&/g,'&').replace(//g,'>'); } function visiblePasswordFields() { return Array.from(document.querySelectorAll('input[type="password"]')) .filter(el => el.offsetParent !== null && !el.disabled); } function findUsernameField(pwField) { const all = Array.from(document.querySelectorAll('input')); const idx = all.indexOf(pwField); for (let i = idx - 1; i >= 0; i--) { const el = all[i]; if (!el.offsetParent || el.disabled) continue; if (['email', 'text', 'tel'].includes(el.type)) return el; } return null; } // ── Framework-compatible fill ───────────────────────────────────────────────── function fillField(el, value) { const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set; if (setter) setter.call(el, value); else el.value = value; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); } function doAutofill(username, password) { const pwFields = visiblePasswordFields(); if (!pwFields.length) return; const pwField = pwFields[0]; const usernameField = findUsernameField(pwField); if (usernameField && username) fillField(usernameField, username); if (password) fillField(pwField, password); [usernameField, pwField].filter(Boolean).forEach(el => { el.style.outline = '2px solid #1a73e8'; setTimeout(() => { el.style.outline = ''; }, 1500); }); } // ── Inline autofill button ──────────────────────────────────────────────────── /** * Inject a small PassKeeper icon button just inside the right edge of each * password field. Clicking it opens a tiny dropdown listing matching items. */ function injectAutofillButtons(matchingItems) { visiblePasswordFields().forEach(pwField => { if (pwField.getAttribute(PK_ATTR)) return; // already decorated pwField.setAttribute(PK_ATTR, '1'); // Wrap the field if it isn't already positioned const wrap = document.createElement('div'); wrap.style.cssText = 'position:relative;display:inline-block;width:100%;'; pwField.parentNode.insertBefore(wrap, pwField); wrap.appendChild(pwField); // Add right-side padding so text doesn't overlap the button pwField.style.paddingRight = '32px'; // The icon button const btn = document.createElement('button'); btn.type = 'button'; btn.className = PK_BTN_CLASS; btn.title = 'Autofill with PassKeeper'; btn.setAttribute('aria-label', 'Autofill with PassKeeper'); btn.style.cssText = [ 'position:absolute', 'right:6px', 'top:50%', 'transform:translateY(-50%)', 'background:none', 'border:none', 'cursor:pointer', 'padding:3px', 'display:flex', 'align-items:center', 'justify-content:center', 'z-index:2147483646', ].join(';'); btn.innerHTML = ``; wrap.appendChild(btn); btn.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); toggleDropdown(btn, pwField, matchingItems); }); }); } // Dropdown showing matching credentials function toggleDropdown(btn, pwField, items) { // Remove any existing dropdown const existing = document.getElementById('__pk_dropdown__'); if (existing) { existing.remove(); return; } const usernameField = findUsernameField(pwField); const dropdown = document.createElement('div'); dropdown.id = '__pk_dropdown__'; dropdown.style.cssText = [ 'position:fixed', 'background:#fff', 'border:1px solid #e2e8f0', 'border-radius:10px', 'box-shadow:0 8px 30px rgba(0,0,0,0.15)', 'z-index:2147483647', 'min-width:240px', 'max-width:300px', 'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif', 'font-size:13px', 'overflow:hidden', ].join(';'); // Position below the button const rect = btn.getBoundingClientRect(); dropdown.style.top = (rect.bottom + 6) + 'px'; dropdown.style.left = Math.max(8, rect.right - 260) + 'px'; // Header const header = document.createElement('div'); header.style.cssText = 'padding:9px 12px 8px;border-bottom:1px solid #f3f4f6;display:flex;align-items:center;gap:6px;'; header.innerHTML = `PassKeeper`; dropdown.appendChild(header); if (!items.length) { const empty = document.createElement('div'); empty.style.cssText = 'padding:14px 12px;color:#9ca3af;font-size:12px;text-align:center;'; empty.textContent = 'No matching credentials'; dropdown.appendChild(empty); } else { items.forEach(item => { const row = document.createElement('div'); row.style.cssText = 'display:flex;align-items:center;gap:9px;padding:9px 12px;cursor:pointer;transition:background 0.1s;'; row.onmouseenter = () => { row.style.background = '#f9fafb'; }; row.onmouseleave = () => { row.style.background = ''; }; const username = escHtml(item.plain?.username || ''); const name = escHtml(item.name); row.innerHTML = `
${user} on ${site}