Files
broswer-automation/releases/chrome-extension/latest/chrome-mcp-server-lastest/inject-scripts/keyboard-helper.js
nasir@endelospay.com d97cad1736 first commit
2025-08-12 02:54:17 +05:00

292 lines
11 KiB
JavaScript

/* 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;
});
}