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