first commit
This commit is contained in:
291
app/chrome-extension/inject-scripts/keyboard-helper.js
Normal file
291
app/chrome-extension/inject-scripts/keyboard-helper.js
Normal file
@@ -0,0 +1,291 @@
|
||||
/* 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<Object>} - 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;
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user