/* eslint-disable */ // keyboard-helper.js // This script is injected into the page to handle keyboard event simulation if (window.__KEYBOARD_HELPER_INITIALIZED__) { // Already initialized, skip } else { window.__KEYBOARD_HELPER_INITIALIZED__ = true; // A map for special keys to their KeyboardEvent properties // Key names should be lowercase for matching const SPECIAL_KEY_MAP = { enter: { key: 'Enter', code: 'Enter', keyCode: 13 }, tab: { key: 'Tab', code: 'Tab', keyCode: 9 }, esc: { key: 'Escape', code: 'Escape', keyCode: 27 }, escape: { key: 'Escape', code: 'Escape', keyCode: 27 }, space: { key: ' ', code: 'Space', keyCode: 32 }, backspace: { key: 'Backspace', code: 'Backspace', keyCode: 8 }, delete: { key: 'Delete', code: 'Delete', keyCode: 46 }, del: { key: 'Delete', code: 'Delete', keyCode: 46 }, up: { key: 'ArrowUp', code: 'ArrowUp', keyCode: 38 }, arrowup: { key: 'ArrowUp', code: 'ArrowUp', keyCode: 38 }, down: { key: 'ArrowDown', code: 'ArrowDown', keyCode: 40 }, arrowdown: { key: 'ArrowDown', code: 'ArrowDown', keyCode: 40 }, left: { key: 'ArrowLeft', code: 'ArrowLeft', keyCode: 37 }, arrowleft: { key: 'ArrowLeft', code: 'ArrowLeft', keyCode: 37 }, right: { key: 'ArrowRight', code: 'ArrowRight', keyCode: 39 }, arrowright: { key: 'ArrowRight', code: 'ArrowRight', keyCode: 39 }, home: { key: 'Home', code: 'Home', keyCode: 36 }, end: { key: 'End', code: 'End', keyCode: 35 }, pageup: { key: 'PageUp', code: 'PageUp', keyCode: 33 }, pagedown: { key: 'PageDown', code: 'PageDown', keyCode: 34 }, insert: { key: 'Insert', code: 'Insert', keyCode: 45 }, // Function keys ...Object.fromEntries( Array.from({ length: 12 }, (_, i) => [ `f${i + 1}`, { key: `F${i + 1}`, code: `F${i + 1}`, keyCode: 112 + i }, ]), ), }; const MODIFIER_KEYS = { ctrl: 'ctrlKey', control: 'ctrlKey', alt: 'altKey', shift: 'shiftKey', meta: 'metaKey', command: 'metaKey', cmd: 'metaKey', }; /** * Parses a key string (e.g., "Ctrl+Shift+A", "Enter") into a main key and modifiers. * @param {string} keyString - String representation of a single key press (can include modifiers). * @returns { {key: string, code: string, keyCode: number, charCode?: number, modifiers: {ctrlKey:boolean, altKey:boolean, shiftKey:boolean, metaKey:boolean}} | null } * Returns null if the keyString is invalid or represents only modifiers. */ function parseSingleKeyCombination(keyString) { const parts = keyString.split('+').map((part) => part.trim().toLowerCase()); const modifiers = { ctrlKey: false, altKey: false, shiftKey: false, metaKey: false, }; let mainKeyPart = null; for (const part of parts) { if (MODIFIER_KEYS[part]) { modifiers[MODIFIER_KEYS[part]] = true; } else if (mainKeyPart === null) { // First non-modifier is the main key mainKeyPart = part; } else { // Invalid format: multiple main keys in a single combination (e.g., "Ctrl+A+B") console.error(`Invalid key combination string: ${keyString}. Multiple main keys found.`); return null; } } if (!mainKeyPart) { // This case could happen if the keyString is something like "Ctrl+" or just "Ctrl" // If the intent was to press JUST 'Control', the input should be 'Control' not 'Control+' // Let's check if mainKeyPart is actually a modifier name used as a main key if (Object.keys(MODIFIER_KEYS).includes(parts[parts.length - 1]) && parts.length === 1) { mainKeyPart = parts[parts.length - 1]; // e.g. user wants to press "Control" key itself // For "Control" key itself, key: "Control", code: "ControlLeft" (or Right) if (mainKeyPart === 'ctrl' || mainKeyPart === 'control') return { key: 'Control', code: 'ControlLeft', keyCode: 17, modifiers }; if (mainKeyPart === 'alt') return { key: 'Alt', code: 'AltLeft', keyCode: 18, modifiers }; if (mainKeyPart === 'shift') return { key: 'Shift', code: 'ShiftLeft', keyCode: 16, modifiers }; if (mainKeyPart === 'meta' || mainKeyPart === 'command' || mainKeyPart === 'cmd') return { key: 'Meta', code: 'MetaLeft', keyCode: 91, modifiers }; } else { console.error(`Invalid key combination string: ${keyString}. No main key specified.`); return null; } } const specialKey = SPECIAL_KEY_MAP[mainKeyPart]; if (specialKey) { return { ...specialKey, modifiers }; } // For single characters or other unmapped keys if (mainKeyPart.length === 1) { const charCode = mainKeyPart.charCodeAt(0); // If Shift is active and it's a letter, use the uppercase version for 'key' // This mimics more closely how keyboards behave. let keyChar = mainKeyPart; if (modifiers.shiftKey && mainKeyPart.match(/^[a-z]$/i)) { keyChar = mainKeyPart.toUpperCase(); } return { key: keyChar, code: `Key${mainKeyPart.toUpperCase()}`, // 'a' -> KeyA, 'A' -> KeyA keyCode: charCode, charCode: charCode, // charCode is legacy, but some old systems might use it modifiers, }; } console.error(`Unknown key: ${mainKeyPart} in string "${keyString}"`); return null; // Or handle as an error } /** * Simulates a single key press (keydown, (keypress), keyup) for a parsed key. * @param { {key: string, code: string, keyCode: number, charCode?: number, modifiers: object} } parsedKeyInfo * @param {Element} element - Target element. * @returns {{success: boolean, error?: string}} */ function dispatchKeyEvents(parsedKeyInfo, element) { if (!parsedKeyInfo) return { success: false, error: 'Invalid key info provided for dispatch.' }; const { key, code, keyCode, charCode, modifiers } = parsedKeyInfo; const eventOptions = { key: key, code: code, bubbles: true, cancelable: true, composed: true, // Important for shadow DOM view: window, ...modifiers, // ctrlKey, altKey, shiftKey, metaKey // keyCode/which are deprecated but often set for compatibility keyCode: keyCode || (key.length === 1 ? key.charCodeAt(0) : 0), which: keyCode || (key.length === 1 ? key.charCodeAt(0) : 0), }; try { const kdRes = element.dispatchEvent(new KeyboardEvent('keydown', eventOptions)); // keypress is deprecated, but simulate if it's a character key or Enter // Only dispatch if keydown was not cancelled and it's a character producing key if (kdRes && (key.length === 1 || key === 'Enter' || key === ' ')) { const keypressOptions = { ...eventOptions }; if (charCode) keypressOptions.charCode = charCode; element.dispatchEvent(new KeyboardEvent('keypress', keypressOptions)); } element.dispatchEvent(new KeyboardEvent('keyup', eventOptions)); return { success: true }; } catch (error) { console.error(`Error dispatching key events for "${key}":`, error); return { success: false, error: `Error dispatching key events for "${key}": ${error.message}`, }; } } /** * Simulate keyboard events on an element or document * @param {string} keysSequenceString - String representation of key(s) (e.g., "Enter", "Ctrl+C, A, B") * @param {Element} targetElement - Element to dispatch events on (optional) * @param {number} delay - Delay between key sequences in milliseconds (optional) * @returns {Promise} - Result of the keyboard operation */ async function simulateKeyboard(keysSequenceString, targetElement = null, delay = 0) { try { const element = targetElement || document.activeElement || document.body; if (element !== document.activeElement && typeof element.focus === 'function') { element.focus(); await new Promise((resolve) => setTimeout(resolve, 50)); // Small delay for focus } const keyCombinations = keysSequenceString .split(',') .map((k) => k.trim()) .filter((k) => k.length > 0); const operationResults = []; for (let i = 0; i < keyCombinations.length; i++) { const comboString = keyCombinations[i]; const parsedKeyInfo = parseSingleKeyCombination(comboString); if (!parsedKeyInfo) { operationResults.push({ keyCombination: comboString, success: false, error: `Invalid key string or combination: ${comboString}`, }); continue; // Skip to next combination in sequence } const dispatchResult = dispatchKeyEvents(parsedKeyInfo, element); operationResults.push({ keyCombination: comboString, ...dispatchResult, }); if (dispatchResult.error) { // Optionally, decide if sequence should stop on first error // For now, we continue but log the error in results console.warn( `Failed to simulate key combination "${comboString}": ${dispatchResult.error}`, ); } if (delay > 0 && i < keyCombinations.length - 1) { await new Promise((resolve) => setTimeout(resolve, delay)); } } // Check if all individual operations were successful const overallSuccess = operationResults.every((r) => r.success); return { success: overallSuccess, message: overallSuccess ? `Keyboard events simulated successfully: ${keysSequenceString}` : `Some keyboard events failed for: ${keysSequenceString}`, results: operationResults, // Detailed results for each key combination targetElement: { tagName: element.tagName, id: element.id, className: element.className, type: element.type, // if applicable e.g. for input }, }; } catch (error) { console.error('Error in simulateKeyboard:', error); return { success: false, error: `Error simulating keyboard events: ${error.message}`, results: [], }; } } // Listener for messages from the extension chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => { if (request.action === 'simulateKeyboard') { let targetEl = null; if (request.selector) { targetEl = document.querySelector(request.selector); if (!targetEl) { sendResponse({ success: false, error: `Element with selector "${request.selector}" not found`, results: [], }); return true; // Keep channel open for async response } } simulateKeyboard(request.keys, targetEl, request.delay) .then(sendResponse) .catch((error) => { // This catch is for unexpected errors in simulateKeyboard promise chain itself console.error('Unexpected error in simulateKeyboard promise chain:', error); sendResponse({ success: false, error: `Unexpected error during keyboard simulation: ${error.message}`, results: [], }); }); return true; // Indicates async response is expected } else if (request.action === 'chrome_keyboard_ping') { sendResponse({ status: 'pong', initialized: true }); // Respond that it's initialized return false; // Synchronous response } // Not our message, or no async response needed return false; }); }