feat: add content script with form detection and autofill
Login form detector using password field + username heuristics, native value setter fill for React/Vue compatibility, inline "id" icon injection with autofill candidate picker, and MutationObserver for SPA support. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
88
extension/src/content/fill.ts
Normal file
88
extension/src/content/fill.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/// Fill listener — receives credentials from the service worker and fills form fields.
|
||||
///
|
||||
/// Uses the native value setter trick to work with React/Vue controlled inputs
|
||||
/// that override the value property.
|
||||
|
||||
/// Set up a listener for fill_credentials messages from the service worker.
|
||||
export function setupFillListener(): void {
|
||||
chrome.runtime.onMessage.addListener(
|
||||
(message: { type: string; username: string; password: string }, _sender: chrome.runtime.MessageSender, sendResponse: (response: { ok: boolean }) => void) => {
|
||||
if (message.type !== 'fill_credentials') return false;
|
||||
fillFields(message.username, message.password);
|
||||
sendResponse({ ok: true });
|
||||
return false;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Fill username and password fields on the page.
|
||||
///
|
||||
/// Finds the first visible password field and its associated username field,
|
||||
/// then sets their values using the native setter trick for React/Vue compat.
|
||||
export function fillFields(username: string, password: string): void {
|
||||
const pwField = document.querySelector<HTMLInputElement>('input[type="password"]');
|
||||
if (!pwField) return;
|
||||
|
||||
// Set the password.
|
||||
setNativeValue(pwField, password);
|
||||
|
||||
// Find the username field (same logic as detector).
|
||||
if (username) {
|
||||
const usernameField = findUsernameForFill(pwField);
|
||||
if (usernameField) {
|
||||
setNativeValue(usernameField, username);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Use the native HTMLInputElement.value setter to bypass React/Vue wrappers.
|
||||
/// Then dispatch input and change events so the framework picks up the change.
|
||||
function setNativeValue(input: HTMLInputElement, value: string): void {
|
||||
const nativeSetter = Object.getOwnPropertyDescriptor(
|
||||
HTMLInputElement.prototype,
|
||||
'value',
|
||||
)?.set;
|
||||
|
||||
if (nativeSetter) {
|
||||
nativeSetter.call(input, value);
|
||||
} else {
|
||||
input.value = value;
|
||||
}
|
||||
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
||||
/// Find the username field associated with a password field (simplified version for fill).
|
||||
function findUsernameForFill(pwField: HTMLInputElement): HTMLInputElement | null {
|
||||
const form = pwField.closest('form');
|
||||
const scope = form ?? document;
|
||||
const inputs = scope.querySelectorAll<HTMLInputElement>('input');
|
||||
|
||||
// Priority: autocomplete > type=email > name pattern > preceding text input.
|
||||
for (const input of inputs) {
|
||||
if (input === pwField) continue;
|
||||
if (input.autocomplete === 'username' || input.autocomplete === 'email') return input;
|
||||
}
|
||||
|
||||
for (const input of inputs) {
|
||||
if (input === pwField) continue;
|
||||
if (input.type === 'email') return input;
|
||||
}
|
||||
|
||||
const pattern = /user|email|login|account/i;
|
||||
for (const input of inputs) {
|
||||
if (input === pwField || input.type === 'hidden' || input.type === 'password') continue;
|
||||
if (pattern.test(input.name) || pattern.test(input.id)) return input;
|
||||
}
|
||||
|
||||
const allInputs = Array.from(inputs);
|
||||
const pwIndex = allInputs.indexOf(pwField);
|
||||
for (let i = pwIndex - 1; i >= 0; i--) {
|
||||
const input = allInputs[i];
|
||||
if (input.type === 'hidden' || input.type === 'password' || input.type === 'submit') continue;
|
||||
if (input.offsetWidth > 0 && input.offsetHeight > 0) return input;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user