import { FormDetector } from "../../utils/formDetector/FormDetector"; import { FormFiller } from "../../utils/formDetector/FormFiller"; import { Credential } from "../../utils/types/Credential"; import { openAutofillPopup } from "./Popup"; /** * Global timestamp to track popup debounce time. * This is used to not show the popup again for a specific amount of time. * Used after autofill events to prevent spamming the popup from automatic * triggered browser events which can cause "focus" events to trigger. */ let popupDebounceTime = 0; /** * Check if popup can be shown based on debounce time. */ export function popupDebounceTimeHasPassed() : boolean { if (Date.now() < popupDebounceTime) { return false; } return true; } /** * Hide popup for a specific amount of time. */ export function hidePopupFor(ms: number) : void { popupDebounceTime = Date.now() + ms; } /** * Fill credential into current form. * * @param credential - The credential to fill. * @param input - The input element that triggered the popup. Required when filling credentials to know which form to fill. */ export function fillCredential(credential: Credential, input: HTMLInputElement) : void { // Set debounce time to 800ms to prevent the popup from being shown again within 800ms because of autofill events. hidePopupFor(800); const formDetector = new FormDetector(document, input); const form = formDetector.getForm(); if (!form) { // No form found, so we can't fill anything. return; } const formFiller = new FormFiller(form, triggerInputEvents); formFiller.fillFields(credential); } /** * Inject icon for a focused input element */ export function injectIcon(input: HTMLInputElement, container: HTMLElement): void { const aliasvaultIconSvg = ` `; const ICON_HTML = `
`; // Generate unique ID if input doesn't have one if (!input.id) { input.id = `aliasvault-input-${Math.random().toString(36).substring(2, 11)}`; } // Create an overlay container at document level if it doesn't exist let overlayContainer = container.querySelector('#aliasvault-overlay-container'); if (!overlayContainer) { overlayContainer = document.createElement('div') as HTMLElement; overlayContainer.id = 'aliasvault-overlay-container'; overlayContainer.className = 'av-overlay-container'; container.appendChild(overlayContainer); } // Create the icon element from the HTML template const iconContainer = document.createElement('div'); iconContainer.innerHTML = ICON_HTML; const icon = iconContainer.firstElementChild as HTMLElement; icon.setAttribute('data-icon-for', input.id); // Enable pointer events just for the icon icon.style.pointerEvents = 'auto'; /** * Update position of the icon. */ const updateIconPosition = () : void => { const rect = input.getBoundingClientRect(); icon.style.position = 'fixed'; icon.style.top = `${rect.top + (rect.height - 24) / 2}px`; icon.style.left = `${rect.right - 32}px`; }; // Update position initially and on relevant events updateIconPosition(); window.addEventListener('scroll', updateIconPosition, true); window.addEventListener('resize', updateIconPosition); // Add click event to trigger the autofill popup and refocus the input icon.addEventListener('click', (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); setTimeout(() => input.focus(), 0); openAutofillPopup(input, container); }); // Append the icon to the overlay container overlayContainer.appendChild(icon); // Fade in the icon requestAnimationFrame(() => { icon.style.opacity = '1'; }); /** * Remove the icon when the input loses focus. */ const handleBlur = (): void => { icon.style.opacity = '0'; setTimeout(() => { icon.remove(); input.removeEventListener('blur', handleBlur); input.removeEventListener('keydown', handleKeyPress); window.removeEventListener('scroll', updateIconPosition, true); window.removeEventListener('resize', updateIconPosition); // Remove overlay container if it's empty if (!overlayContainer.children.length) { overlayContainer.remove(); } }, 200); }; /** * Handle key press to dismiss icon. */ const handleKeyPress = (e: KeyboardEvent): void => { // Dismiss on Enter, Escape, or Tab. if (e.key === 'Enter' || e.key === 'Escape' || e.key === 'Tab') { handleBlur(); } }; input.addEventListener('blur', handleBlur); input.addEventListener('keydown', handleKeyPress); } /** * Trigger input events for an element to trigger form validation * which some websites require before the "continue" button is enabled. */ function triggerInputEvents(element: HTMLInputElement | HTMLSelectElement) : void { // Create an overlay div that will show the highlight effect const overlay = document.createElement('div'); /** * Update position of the overlay. */ const updatePosition = () : void => { const rect = element.getBoundingClientRect(); overlay.style.cssText = ` position: fixed; z-index: 999999991; pointer-events: none; top: ${rect.top}px; left: ${rect.left}px; width: ${rect.width}px; height: ${rect.height}px; background-color: rgba(244, 149, 65, 0.3); border-radius: ${getComputedStyle(element).borderRadius}; animation: fadeOut 1.4s ease-out forwards; `; }; updatePosition(); // Add scroll event listener window.addEventListener('scroll', updatePosition); // Add keyframe animation const style = document.createElement('style'); style.textContent = ` @keyframes fadeOut { 0% { opacity: 1; transform: scale(1.02); } 100% { opacity: 0; transform: scale(1); } } `; document.head.appendChild(style); document.body.appendChild(overlay); // Remove overlay and cleanup after animation setTimeout(() => { window.removeEventListener('scroll', updatePosition); overlay.remove(); style.remove(); }, 1400); // Trigger events element.dispatchEvent(new Event('input', { bubbles: true })); element.dispatchEvent(new Event('change', { bubbles: true })); if (element.type === 'radio') { element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })); element.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); element.dispatchEvent(new MouseEvent('click', { bubbles: true })); } }