hangwin--mcp-chrome
371 行
11 KiB
JavaScript
371 行
11 KiB
JavaScript
/* eslint-disable */
|
|
// click-helper.js
|
|
// This script is injected into the page to handle click operations
|
|
|
|
if (window.__CLICK_HELPER_INITIALIZED__) {
|
|
// Already initialized, skip
|
|
} else {
|
|
window.__CLICK_HELPER_INITIALIZED__ = true;
|
|
/**
|
|
* Click on an element matching the selector or at specific coordinates
|
|
* @param {string} selector - CSS selector for the element to click
|
|
* @param {boolean} waitForNavigation - Whether to wait for navigation to complete after click
|
|
* @param {number} timeout - Timeout in milliseconds for waiting for the element or navigation
|
|
* @param {Object} coordinates - Optional coordinates for clicking at a specific position
|
|
* @param {number} coordinates.x - X coordinate relative to the viewport
|
|
* @param {number} coordinates.y - Y coordinate relative to the viewport
|
|
* @returns {Promise<Object>} - Result of the click operation
|
|
*/
|
|
async function clickElement(
|
|
selector,
|
|
waitForNavigation = false,
|
|
timeout = 5000,
|
|
coordinates = null,
|
|
ref = null,
|
|
double = false,
|
|
options = {},
|
|
) {
|
|
try {
|
|
let element = null;
|
|
let elementInfo = null;
|
|
let clickX, clickY;
|
|
|
|
if (ref && typeof ref === 'string') {
|
|
// Resolve element from weak map
|
|
let target = null;
|
|
try {
|
|
const map = window.__claudeElementMap;
|
|
const weak = map && map[ref];
|
|
target = weak && typeof weak.deref === 'function' ? weak.deref() : null;
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
|
|
if (!target || !(target instanceof Element)) {
|
|
return {
|
|
error: `Element ref "${ref}" not found. Please call chrome_read_page first and ensure the ref is still valid.`,
|
|
};
|
|
}
|
|
|
|
element = target;
|
|
element.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
|
|
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
clickX = rect.left + rect.width / 2;
|
|
clickY = rect.top + rect.height / 2;
|
|
elementInfo = {
|
|
tagName: element.tagName,
|
|
id: element.id,
|
|
className: element.className,
|
|
text: element.textContent?.trim().substring(0, 100) || '',
|
|
href: element.href || null,
|
|
type: element.type || null,
|
|
isVisible: true,
|
|
rect: {
|
|
x: rect.x,
|
|
y: rect.y,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
top: rect.top,
|
|
right: rect.right,
|
|
bottom: rect.bottom,
|
|
left: rect.left,
|
|
},
|
|
clickMethod: 'ref',
|
|
ref,
|
|
};
|
|
} else if (
|
|
coordinates &&
|
|
typeof coordinates.x === 'number' &&
|
|
typeof coordinates.y === 'number'
|
|
) {
|
|
clickX = coordinates.x;
|
|
clickY = coordinates.y;
|
|
|
|
element = document.elementFromPoint(clickX, clickY);
|
|
|
|
if (element) {
|
|
const rect = element.getBoundingClientRect();
|
|
elementInfo = {
|
|
tagName: element.tagName,
|
|
id: element.id,
|
|
className: element.className,
|
|
text: element.textContent?.trim().substring(0, 100) || '',
|
|
href: element.href || null,
|
|
type: element.type || null,
|
|
isVisible: true,
|
|
rect: {
|
|
x: rect.x,
|
|
y: rect.y,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
top: rect.top,
|
|
right: rect.right,
|
|
bottom: rect.bottom,
|
|
left: rect.left,
|
|
},
|
|
clickMethod: 'coordinates',
|
|
clickPosition: { x: clickX, y: clickY },
|
|
};
|
|
} else {
|
|
elementInfo = {
|
|
clickMethod: 'coordinates',
|
|
clickPosition: { x: clickX, y: clickY },
|
|
warning: 'No element found at the specified coordinates',
|
|
};
|
|
}
|
|
} else {
|
|
element = document.querySelector(selector);
|
|
if (!element) {
|
|
return {
|
|
error: `Element with selector "${selector}" not found`,
|
|
};
|
|
}
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
elementInfo = {
|
|
tagName: element.tagName,
|
|
id: element.id,
|
|
className: element.className,
|
|
text: element.textContent?.trim().substring(0, 100) || '',
|
|
href: element.href || null,
|
|
type: element.type || null,
|
|
isVisible: true,
|
|
rect: {
|
|
x: rect.x,
|
|
y: rect.y,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
top: rect.top,
|
|
right: rect.right,
|
|
bottom: rect.bottom,
|
|
left: rect.left,
|
|
},
|
|
clickMethod: 'selector',
|
|
};
|
|
|
|
// First sroll so that the element is in view, then check visibility.
|
|
element.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
elementInfo.isVisible = isElementVisible(element);
|
|
if (!elementInfo.isVisible) {
|
|
return {
|
|
error: `Element with selector "${selector}" is not visible`,
|
|
elementInfo,
|
|
};
|
|
}
|
|
|
|
const updatedRect = element.getBoundingClientRect();
|
|
clickX = updatedRect.left + updatedRect.width / 2;
|
|
clickY = updatedRect.top + updatedRect.height / 2;
|
|
}
|
|
|
|
let navigationPromise;
|
|
if (waitForNavigation) {
|
|
navigationPromise = new Promise((resolve) => {
|
|
const beforeUnloadListener = () => {
|
|
window.removeEventListener('beforeunload', beforeUnloadListener);
|
|
resolve(true);
|
|
};
|
|
window.addEventListener('beforeunload', beforeUnloadListener);
|
|
|
|
setTimeout(() => {
|
|
window.removeEventListener('beforeunload', beforeUnloadListener);
|
|
resolve(false);
|
|
}, timeout);
|
|
});
|
|
}
|
|
|
|
if (
|
|
element &&
|
|
(elementInfo.clickMethod === 'selector' || elementInfo.clickMethod === 'ref')
|
|
) {
|
|
if (double) {
|
|
dispatchClickSequence(element, clickX, clickY, options, true);
|
|
} else {
|
|
dispatchClickSequence(element, clickX, clickY, options, false);
|
|
}
|
|
} else {
|
|
if (double) simulateDoubleClick(clickX, clickY, options);
|
|
else simulateClick(clickX, clickY, options);
|
|
}
|
|
|
|
// Wait for navigation if needed
|
|
let navigationOccurred = false;
|
|
if (waitForNavigation) {
|
|
navigationOccurred = await navigationPromise;
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
message: 'Element clicked successfully',
|
|
elementInfo,
|
|
navigationOccurred,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
error: `Error clicking element: ${error.message}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Simulate a mouse click at specific coordinates
|
|
* @param {number} x - X coordinate relative to the viewport
|
|
* @param {number} y - Y coordinate relative to the viewport
|
|
*/
|
|
function simulateClick(x, y, options = {}) {
|
|
const element = document.elementFromPoint(x, y);
|
|
if (!element) return;
|
|
dispatchClickSequence(element, x, y, options, false);
|
|
}
|
|
|
|
/**
|
|
* Simulate a double click sequence at specific coordinates
|
|
*/
|
|
function simulateDoubleClick(x, y, options = {}) {
|
|
const element = document.elementFromPoint(x, y);
|
|
if (!element) return;
|
|
dispatchClickSequence(element, x, y, options, true);
|
|
}
|
|
|
|
/**
|
|
* Simulate double click using element when available
|
|
*/
|
|
function simulateDomDoubleClick(element, x, y, options) {
|
|
dispatchClickSequence(element, x, y, options, true);
|
|
}
|
|
|
|
function normalizeMouseOpts(x, y, options = {}) {
|
|
const bubbles = options.bubbles !== false; // default true
|
|
const cancelable = options.cancelable !== false; // default true
|
|
const altKey = !!(options.modifiers && options.modifiers.altKey);
|
|
const ctrlKey = !!(options.modifiers && options.modifiers.ctrlKey);
|
|
const metaKey = !!(options.modifiers && options.modifiers.metaKey);
|
|
const shiftKey = !!(options.modifiers && options.modifiers.shiftKey);
|
|
const btn = String(options.button || 'left');
|
|
const button = btn === 'right' ? 2 : btn === 'middle' ? 1 : 0;
|
|
const buttons = btn === 'right' ? 2 : btn === 'middle' ? 4 : 1;
|
|
return {
|
|
bubbles,
|
|
cancelable,
|
|
altKey,
|
|
ctrlKey,
|
|
metaKey,
|
|
shiftKey,
|
|
button,
|
|
buttons,
|
|
clientX: x,
|
|
clientY: y,
|
|
view: window,
|
|
};
|
|
}
|
|
|
|
function dispatchClickSequence(element, x, y, options = {}, isDouble = false) {
|
|
const base = normalizeMouseOpts(x, y, options);
|
|
const down = new MouseEvent('mousedown', base);
|
|
const up = new MouseEvent('mouseup', base);
|
|
const click = new MouseEvent('click', base);
|
|
try {
|
|
element.dispatchEvent(down);
|
|
} catch {}
|
|
try {
|
|
element.dispatchEvent(up);
|
|
} catch {}
|
|
try {
|
|
element.dispatchEvent(click);
|
|
} catch {}
|
|
if (base.button === 2) {
|
|
// right button contextmenu
|
|
const ctx = new MouseEvent('contextmenu', base);
|
|
try {
|
|
element.dispatchEvent(ctx);
|
|
} catch {}
|
|
}
|
|
if (isDouble) {
|
|
// second sequence + dblclick
|
|
setTimeout(() => {
|
|
try {
|
|
element.dispatchEvent(new MouseEvent('mousedown', base));
|
|
} catch {}
|
|
try {
|
|
element.dispatchEvent(new MouseEvent('mouseup', base));
|
|
} catch {}
|
|
try {
|
|
element.dispatchEvent(new MouseEvent('click', base));
|
|
} catch {}
|
|
try {
|
|
element.dispatchEvent(new MouseEvent('dblclick', base));
|
|
} catch {}
|
|
}, 30);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if an element is visible
|
|
* @param {Element} element - The element to check
|
|
* @returns {boolean} - Whether the element is visible
|
|
*/
|
|
function isElementVisible(element) {
|
|
if (!element) return false;
|
|
|
|
const style = window.getComputedStyle(element);
|
|
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
|
|
return false;
|
|
}
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
if (rect.width === 0 || rect.height === 0) {
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
rect.bottom < 0 ||
|
|
rect.top > window.innerHeight ||
|
|
rect.right < 0 ||
|
|
rect.left > window.innerWidth
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
const centerX = rect.left + rect.width / 2;
|
|
const centerY = rect.top + rect.height / 2;
|
|
|
|
const elementAtPoint = document.elementFromPoint(centerX, centerY);
|
|
if (!elementAtPoint) return false;
|
|
|
|
return element === elementAtPoint || element.contains(elementAtPoint);
|
|
}
|
|
|
|
// Listen for messages from the extension
|
|
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
|
|
if (request.action === 'clickElement') {
|
|
clickElement(
|
|
request.selector,
|
|
request.waitForNavigation,
|
|
request.timeout,
|
|
request.coordinates,
|
|
request.ref,
|
|
!!request.double,
|
|
{
|
|
button: request.button,
|
|
bubbles: request.bubbles,
|
|
cancelable: request.cancelable,
|
|
modifiers: request.modifiers,
|
|
},
|
|
)
|
|
.then(sendResponse)
|
|
.catch((error) => {
|
|
sendResponse({
|
|
error: `Unexpected error: ${error.message}`,
|
|
});
|
|
});
|
|
return true; // Indicates async response
|
|
} else if (request.action === 'chrome_click_element_ping') {
|
|
sendResponse({ status: 'pong' });
|
|
return false;
|
|
}
|
|
});
|
|
}
|