/* eslint-disable */
/**
* Element Picker Inject Script
*
* Injected script to let the user manually pick elements for chrome_request_element_selection.
* - Writes refs into window.__claudeElementMap (compatible with accessibility-tree-helper.js)
* - Generates stable CSS selectors (prefers id/data-testid/etc.)
* - Supports iframe picking by reporting selection via chrome.runtime.sendMessage (background reads sender.frameId)
*/
(function () {
'use strict';
// Prevent double initialization
if (window.__MCP_ELEMENT_PICKER_INITIALIZED__) return;
window.__MCP_ELEMENT_PICKER_INITIALIZED__ = true;
// ============================================================
// Constants
// ============================================================
const UI_HOST_ID = '__mcp_element_picker_host__';
const HIGHLIGHT_ID = '__mcp_element_picker_highlight__';
const MAX_TEXT_LEN = 160;
// Highlight colors matching Editorial accent (terracotta)
const HIGHLIGHT_COLOR = '#d97757';
const HIGHLIGHT_BG = 'rgba(217, 119, 87, 0.08)';
const HIGHLIGHT_BORDER = 'rgba(217, 119, 87, 0.4)';
// ============================================================
// State
// ============================================================
const STATE = {
active: false,
sessionId: null,
activeRequestId: null,
listenersAttached: false,
hoverRafId: null,
pendingHoverEvent: null,
lastHoverEl: null,
highlighter: null,
};
// ============================================================
// CSS Escape Helper
// ============================================================
function cssEscape(value) {
try {
if (window.CSS && typeof window.CSS.escape === 'function') {
return window.CSS.escape(value);
}
} catch {
// Fallback
}
return String(value).replace(/[^a-zA-Z0-9_-]/g, (c) => `\\${c}`);
}
// ============================================================
// UI Detection Helpers
// ============================================================
function getUiHost() {
try {
return document.getElementById(UI_HOST_ID);
} catch {
return null;
}
}
function isOverlayElement(node) {
if (!(node instanceof Node)) return false;
const host = getUiHost();
if (!host) return false;
if (node === host) return true;
const root = typeof node.getRootNode === 'function' ? node.getRootNode() : null;
return root instanceof ShadowRoot && root.host === host;
}
function isEventFromUi(ev) {
if (!ev) return false;
try {
if (typeof ev.composedPath === 'function') {
const path = ev.composedPath();
if (Array.isArray(path)) {
return path.some((n) => isOverlayElement(n));
}
}
} catch {
// Fallback
}
return isOverlayElement(ev.target);
}
/**
* Get the deepest page target from an event, handling Shadow DOM.
*/
function getDeepPageTarget(ev) {
if (!ev) return null;
try {
const path = typeof ev.composedPath === 'function' ? ev.composedPath() : null;
if (Array.isArray(path) && path.length > 0) {
for (const node of path) {
if (node instanceof Element && !isOverlayElement(node)) {
return node;
}
}
}
} catch {
// Fallback
}
const fallback = ev.target instanceof Element ? ev.target : null;
if (fallback && !isOverlayElement(fallback)) {
return fallback;
}
return null;
}
// ============================================================
// Highlighter
// ============================================================
function ensureHighlighter() {
if (STATE.highlighter && STATE.highlighter.isConnected) {
return STATE.highlighter;
}
// Remove any existing highlighter
try {
const existing = document.getElementById(HIGHLIGHT_ID);
if (existing) existing.remove();
} catch {
// Best effort
}
const hl = document.createElement('div');
hl.id = HIGHLIGHT_ID;
Object.assign(hl.style, {
position: 'fixed',
left: '0px',
top: '0px',
width: '0px',
height: '0px',
border: `2px solid ${HIGHLIGHT_COLOR}`,
borderRadius: '4px',
boxShadow: `0 0 0 1px ${HIGHLIGHT_BORDER}`,
background: HIGHLIGHT_BG,
pointerEvents: 'none',
zIndex: '2147483647',
display: 'none',
transition: 'transform 60ms linear, width 60ms linear, height 60ms linear',
});
try {
(document.documentElement || document.body).appendChild(hl);
} catch {
// Best effort
}
STATE.highlighter = hl;
return hl;
}
function clearHighlighter() {
const hl = STATE.highlighter;
if (!hl) return;
try {
hl.style.display = 'none';
} catch {
// Best effort
}
}
function moveHighlighterTo(el) {
const hl = ensureHighlighter();
if (!hl || !(el instanceof Element)) return;
let rect;
try {
rect = el.getBoundingClientRect();
} catch {
clearHighlighter();
return;
}
if (!rect || rect.width <= 0 || rect.height <= 0) {
clearHighlighter();
return;
}
try {
hl.style.display = 'block';
hl.style.transform = `translate(${Math.round(rect.left)}px, ${Math.round(rect.top)}px)`;
hl.style.width = `${Math.round(rect.width)}px`;
hl.style.height = `${Math.round(rect.height)}px`;
} catch {
// Best effort
}
}
// ============================================================
// Selector Uniqueness Check (Optimized)
// ============================================================
/**
* Check if element is inside a Shadow DOM.
*/
function isInShadowDom(el) {
try {
const root = el.getRootNode();
return root instanceof ShadowRoot;
} catch {
return false;
}
}
/**
* Fast uniqueness check using native querySelectorAll.
* For Shadow DOM elements, queries within their shadow root only.
*/
function isSelectorUnique(selector, target) {
if (!selector || !(target instanceof Element)) return false;
try {
// For elements not in Shadow DOM, use fast native query
if (!isInShadowDom(target)) {
const matches = document.querySelectorAll(selector);
return matches.length === 1 && matches[0] === target;
}
// For Shadow DOM elements, query within their root
const root = target.getRootNode();
if (root instanceof ShadowRoot) {
const matches = root.querySelectorAll(selector);
return matches.length === 1 && matches[0] === target;
}
return false;
} catch {
return false;
}
}
// ============================================================
// Selector Generation (Stable & Unique)
// ============================================================
function buildPathFromAncestor(ancestor, target) {
const segs = [];
let cur = target;
const root = target.getRootNode();
const isShadowElement = root instanceof ShadowRoot;
const boundary = isShadowElement ? root.host : document.body;
while (cur && cur !== ancestor && cur !== boundary) {
let seg = cur.tagName.toLowerCase();
const parent = cur.parentElement;
if (parent) {
const siblings = Array.from(parent.children).filter((c) => c.tagName === cur.tagName);
if (siblings.length > 1) {
seg += `:nth-of-type(${siblings.indexOf(cur) + 1})`;
}
}
segs.unshift(seg);
cur = parent;
if (isShadowElement && cur === boundary) break;
}
return segs.join(' > ');
}
function buildFullPath(el) {
let path = '';
let current = el;
const root = el.getRootNode();
const isShadowElement = root instanceof ShadowRoot;
const boundary = isShadowElement ? root.host : document.body;
while (current && current.nodeType === Node.ELEMENT_NODE && current !== boundary) {
let sel = current.tagName.toLowerCase();
const parent = current.parentElement;
if (parent) {
const siblings = Array.from(parent.children).filter((c) => c.tagName === current.tagName);
if (siblings.length > 1) {
sel += `:nth-of-type(${siblings.indexOf(current) + 1})`;
}
}
path = path ? `${sel} > ${path}` : sel;
current = parent;
if (isShadowElement && current === boundary) break;
}
if (isShadowElement) return path || el.tagName.toLowerCase();
return path ? `body > ${path}` : 'body';
}
/**
* Generate a stable CSS selector for an element.
* Prioritizes: id > data-testid/data-test/etc > anchor + relative path > full path
*/
function generateSelector(el) {
if (!(el instanceof Element)) return '';
// Prefer unique IDs
try {
if (el.id) {
const idSel = `#${cssEscape(el.id)}`;
if (isSelectorUnique(idSel, el)) return idSel;
}
} catch {
// Continue
}
// Prefer stable test attributes
try {
const attrNames = [
'data-testid',
'data-testId',
'data-test',
'data-qa',
'data-cy',
'name',
'aria-label',
'title',
'alt',
];
const tag = el.tagName.toLowerCase();
for (const attr of attrNames) {
const v = el.getAttribute(attr);
if (!v) continue;
const attrSel = `[${attr}="${cssEscape(v)}"]`;
const testSel = /^(input|textarea|select)$/i.test(tag) ? `${tag}${attrSel}` : attrSel;
if (isSelectorUnique(testSel, el)) return testSel;
}
} catch {
// Continue
}
// Anchor + relative path
try {
let cur = el;
const anchorAttrs = [
'id',
'data-testid',
'data-testId',
'data-test',
'data-qa',
'data-cy',
'name',
];
const root = el.getRootNode();
const isShadowElement = root instanceof ShadowRoot;
const boundary = isShadowElement ? root.host : document.body;
while (cur && cur !== boundary) {
if (cur.id) {
const anchor = `#${cssEscape(cur.id)}`;
if (isSelectorUnique(anchor, cur)) {
const rel = buildPathFromAncestor(cur, el);
const composed = rel ? `${anchor} ${rel}` : anchor;
if (isSelectorUnique(composed, el)) return composed;
}
}
for (const attr of anchorAttrs) {
const val = cur.getAttribute(attr);
if (!val) continue;
const aSel = `[${attr}="${cssEscape(val)}"]`;
if (isSelectorUnique(aSel, cur)) {
const rel = buildPathFromAncestor(cur, el);
const composed = rel ? `${aSel} ${rel}` : aSel;
if (isSelectorUnique(composed, el)) return composed;
}
}
cur = cur.parentElement;
}
} catch {
// Continue
}
// Fallback to full path
return buildFullPath(el);
}
// ============================================================
// Text Summarization
// ============================================================
function summarizeText(el) {
if (!(el instanceof Element)) return '';
try {
const aria = el.getAttribute('aria-label');
if (aria && aria.trim()) return aria.trim().slice(0, MAX_TEXT_LEN);
const placeholder = el.getAttribute('placeholder');
if (placeholder && placeholder.trim()) return placeholder.trim().slice(0, MAX_TEXT_LEN);
const title = el.getAttribute('title');
if (title && title.trim()) return title.trim().slice(0, MAX_TEXT_LEN);
const alt = el.getAttribute('alt');
if (alt && alt.trim()) return alt.trim().slice(0, MAX_TEXT_LEN);
} catch {
// Continue
}
try {
const t = (el.textContent || '').trim().replace(/\s+/g, ' ');
return t ? t.slice(0, MAX_TEXT_LEN) : '';
} catch {
return '';
}
}
// ============================================================
// Ref Management (Compatible with accessibility-tree-helper.js)
// ============================================================
function ensureRefForElement(el) {
try {
if (!window.__claudeElementMap) window.__claudeElementMap = {};
if (!window.__claudeRefCounter) window.__claudeRefCounter = 0;
} catch {
// Best effort
}
// Check if element already has a ref
let refId = null;
try {
for (const k in window.__claudeElementMap) {
const w = window.__claudeElementMap[k];
if (w && w.deref && w.deref() === el) {
refId = k;
break;
}
}
} catch {
// Continue
}
// Create new ref if needed
if (!refId) {
try {
refId = `ref_${++window.__claudeRefCounter}`;
window.__claudeElementMap[refId] = new WeakRef(el);
} catch {
// Continue
}
}
return refId || '';
}
// ============================================================
// Communication
// ============================================================
function sendFrameEvent(payload) {
try {
chrome.runtime.sendMessage(payload);
} catch {
// Best effort
}
}
// ============================================================
// Event Handlers
// ============================================================
function processMouseMove(ev) {
if (!STATE.active) return;
// Skip if event is from our UI
if (isEventFromUi(ev)) {
STATE.lastHoverEl = null;
clearHighlighter();
return;
}
const target = getDeepPageTarget(ev);
if (!target) {
STATE.lastHoverEl = null;
clearHighlighter();
return;
}
// Skip if same element
if (STATE.lastHoverEl === target) return;
STATE.lastHoverEl = target;
moveHighlighterTo(target);
}
function onMouseMove(ev) {
if (!STATE.active) return;
STATE.pendingHoverEvent = ev;
if (STATE.hoverRafId != null) return;
STATE.hoverRafId = requestAnimationFrame(() => {
STATE.hoverRafId = null;
const latest = STATE.pendingHoverEvent;
STATE.pendingHoverEvent = null;
if (!latest) return;
processMouseMove(latest);
});
}
function onClick(ev) {
if (!STATE.active) return;
// Allow UI interactions without interference
if (isEventFromUi(ev)) return;
const rawTarget = ev.target instanceof Element ? ev.target : null;
if (!rawTarget) return;
// Require an active request id so background can map the selection
if (!STATE.sessionId || !STATE.activeRequestId) return;
ev.preventDefault();
ev.stopPropagation();
const target = getDeepPageTarget(ev) || rawTarget;
if (!(target instanceof Element)) return;
const ref = ensureRefForElement(target);
const selector = generateSelector(target);
let rect;
try {
rect = target.getBoundingClientRect();
} catch {
rect = { x: 0, y: 0, width: 0, height: 0, left: 0, top: 0 };
}
const center = {
x: Math.round(rect.left + rect.width / 2),
y: Math.round(rect.top + rect.height / 2),
};
sendFrameEvent({
type: 'element_picker_frame_event',
sessionId: STATE.sessionId,
event: 'selected',
requestId: STATE.activeRequestId,
element: {
ref,
selector,
selectorType: 'css',
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
center,
text: summarizeText(target),
tagName: target.tagName ? String(target.tagName).toLowerCase() : '',
},
});
}
function onKeyDown(ev) {
if (!STATE.active) return;
if (ev && ev.key === 'Escape') {
if (isEventFromUi(ev)) return;
ev.preventDefault();
ev.stopPropagation();
if (STATE.sessionId) {
sendFrameEvent({
type: 'element_picker_frame_event',
sessionId: STATE.sessionId,
event: 'cancel',
});
}
}
}
// ============================================================
// Listener Management
// ============================================================
function attachListeners() {
if (STATE.listenersAttached) return;
window.addEventListener('mousemove', onMouseMove, true);
window.addEventListener('click', onClick, true);
window.addEventListener('keydown', onKeyDown, true);
STATE.listenersAttached = true;
}
function detachListeners() {
if (!STATE.listenersAttached) return;
window.removeEventListener('mousemove', onMouseMove, true);
window.removeEventListener('click', onClick, true);
window.removeEventListener('keydown', onKeyDown, true);
STATE.listenersAttached = false;
}
// ============================================================
// Session Management API
// ============================================================
function startSession(payload) {
const sessionId = payload && payload.sessionId ? String(payload.sessionId) : '';
if (!sessionId) return;
STATE.active = true;
STATE.sessionId = sessionId;
STATE.activeRequestId =
payload && payload.activeRequestId ? String(payload.activeRequestId) : null;
ensureHighlighter();
attachListeners();
}
function stopSession(payload) {
const sessionId = payload && payload.sessionId ? String(payload.sessionId) : '';
// Only stop if session matches or no specific session requested
if (sessionId && STATE.sessionId && sessionId !== STATE.sessionId) return;
STATE.active = false;
STATE.sessionId = null;
STATE.activeRequestId = null;
STATE.lastHoverEl = null;
detachListeners();
clearHighlighter();
// Remove highlighter element
try {
const hl = STATE.highlighter;
if (hl && hl.remove) hl.remove();
} catch {
// Best effort
}
STATE.highlighter = null;
}
function setActiveRequest(payload) {
const sessionId = payload && payload.sessionId ? String(payload.sessionId) : '';
if (sessionId && STATE.sessionId && sessionId !== STATE.sessionId) return;
STATE.activeRequestId =
payload && payload.activeRequestId ? String(payload.activeRequestId) : null;
}
// ============================================================
// Expose API for Background Script
// ============================================================
window.__mcpElementPicker = {
startSession,
stopSession,
setActiveRequest,
};
// ============================================================
// Message Listener (for direct communication)
// ============================================================
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
try {
if (request && request.action === 'chrome_request_element_selection_ping') {
sendResponse({ status: 'pong' });
return false;
}
if (request && request.action === 'elementPickerStart') {
startSession(request);
sendResponse({ success: true });
return false;
}
if (request && request.action === 'elementPickerStop') {
stopSession(request);
sendResponse({ success: true });
return false;
}
if (request && request.action === 'elementPickerSetActiveRequest') {
setActiveRequest(request);
sendResponse({ success: true });
return false;
}
} catch (e) {
sendResponse({ success: false, error: String(e && e.message ? e.message : e) });
return false;
}
return false;
});
})();