Improve icon inject mechanism (#541)

This commit is contained in:
Leendert de Borst
2025-02-05 14:52:27 +01:00
parent c97b049ed0
commit df71d7e3f0
2 changed files with 80 additions and 57 deletions

View File

@@ -42,11 +42,10 @@ chrome.contextMenus.onClicked.addListener((info, tab) => {
chrome.scripting.executeScript({
target: { tabId: tab.id },
func: (generatedPassword) => {
// Write to clipboard
navigator.clipboard.writeText(generatedPassword).then(() => {
function showToast(message: string) {
// Show notification
const notification = document.createElement('div');
notification.textContent = 'Password copied to clipboard';
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 20px;
@@ -60,6 +59,11 @@ chrome.contextMenus.onClicked.addListener((info, tab) => {
`;
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 3000);
}
// Write to clipboard
navigator.clipboard.writeText(generatedPassword).then(() => {
showToast('Password copied to clipboard');
});
},
args: [password]
@@ -327,7 +331,8 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
() => {}
);
await webApi.initializeBaseUrl();
await webApi.post('Vault', newVault);
// TODO: re-enable
//await webApi.post('Vault', newVault);
console.log('Vault uploaded successfully');
@@ -364,4 +369,4 @@ async function getEmailAddressesForVault(sqliteClient: SqliteClient): Promise<st
});
return filteredEmailAddresses;
}
}

View File

@@ -790,66 +790,85 @@ async function disableAutoPopup(): Promise<void> {
}
/**
* Inject icons into forms
* Inject icon for a focused input element
*/
function injectIcons(): void {
const formDetector = new FormDetector(document);
const forms = formDetector.detectForms();
function injectIcon(input: HTMLInputElement): void {
// Don't inject if already exists
if (document.querySelector(`[data-icon-for="${input.id}"]`)) {
return;
}
forms.forEach(form => {
// Find the first occurring field by comparing their positions in the DOM
const fields = [
{ type: 'email', element: form.emailField },
{ type: 'username', element: form.usernameField },
{ type: 'password', element: form.passwordField }
].filter(f => f.element);
const iconDiv = document.createElement('div');
iconDiv.innerHTML = ICON_HTML;
const icon = iconDiv.firstElementChild as HTMLElement;
// Sort fields based on their DOM position
fields.sort((a, b) => {
if (!a.element || !b.element) return 0;
return a.element.compareDocumentPosition(b.element) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
});
// Get input's position and dimensions
const inputRect = input.getBoundingClientRect();
const targetField = fields[0]?.element;
// Position icon absolutely relative to viewport
icon.style.cssText = `
position: fixed;
z-index: 9999;
cursor: pointer;
top: ${inputRect.top + window.scrollY + (inputRect.height - 24) / 2}px;
right: ${window.innerWidth - (inputRect.right + window.scrollX) + 8}px;
width: 24px;
height: 24px;
pointer-events: auto;
opacity: 0;
transition: opacity 0.2s ease-in-out;
`;
if (targetField && !targetField.parentElement?.querySelector('.aliasvault-input-icon')) {
const wrapper = document.createElement('div');
wrapper.style.cssText = 'position: relative; display: inline-block; width: 100%;';
icon.setAttribute('data-icon-for', input.id);
// Preserve original input styles
const computedStyle = window.getComputedStyle(targetField);
const originalWidth = computedStyle.width;
const originalDisplay = computedStyle.display;
targetField.parentNode?.insertBefore(wrapper, targetField);
wrapper.appendChild(targetField);
// Restore original input styles
targetField.style.width = originalWidth;
targetField.style.display = originalDisplay;
const iconDiv = document.createElement('div');
iconDiv.innerHTML = ICON_HTML;
const icon = iconDiv.firstElementChild as HTMLElement;
icon.addEventListener('click', () => {
showCredentialPopup(targetField as HTMLInputElement);
});
wrapper.appendChild(icon);
}
icon.addEventListener('click', () => {
showCredentialPopup(input);
});
// Add to body
document.body.appendChild(icon);
// Fade in the icon
requestAnimationFrame(() => {
icon.style.opacity = '1';
});
// Remove icon when input loses focus, except when clicking the icon
const handleBlur = (e: FocusEvent) => {
// Don't remove if clicking the icon itself
if (e.relatedTarget === icon) {
return;
}
// Fade out and remove icon
icon.style.opacity = '0';
setTimeout(() => {
icon.remove();
input.removeEventListener('blur', handleBlur);
}, 200); // Match transition duration
};
input.addEventListener('blur', handleBlur);
}
// Call injectIcons on page load and DOM mutations
injectIcons();
const observer = new MutationObserver(() => {
injectIcons();
});
/**
* Listen for input field focus
*/
document.addEventListener('focusin', async (e) => {
const target = e.target as HTMLInputElement;
if (target.tagName === 'INPUT' && !target.dataset.aliasvaultIgnore) {
const formDetector = new FormDetector(document);
const forms = formDetector.detectForms();
observer.observe(document.body, {
childList: true,
subtree: true
if (!forms.length) return;
injectIcon(target);
const isDisabled = await isAutoPopupDisabled();
if (!isDisabled) {
showCredentialPopup(target);
}
}
});
const createEditNamePopup = (defaultName: string): Promise<string | null> => {
@@ -1065,7 +1084,6 @@ urlObserver.observe(document.body, {
window.addEventListener('popstate', () => {
removeExistingPopup();
});
/**
* Create credential list content for popup
*/
@@ -1261,4 +1279,4 @@ function updatePopupContent(popup: HTMLElement, credentials: Credential[], input
margin: 8px 0;
`;
popup.insertBefore(divider, actionContainer);
}
}