/* eslint-disable */ // fill-helper.js // This script is injected into the page to handle form filling operations if (window.__FILL_HELPER_INITIALIZED__) { // Already initialized, skip } else { window.__FILL_HELPER_INITIALIZED__ = true; /** * Fill an input element with the specified value * @param {string} selector - CSS selector for the element to fill * @param {string} value - Value to fill into the element * @returns {Promise} - Result of the fill operation */ async function fillElement(selector, value, ref = null) { try { // Find the element let element = null; if (ref && typeof ref === 'string') { try { const map = window.__claudeElementMap; const weak = map && map[ref]; element = weak && typeof weak.deref === 'function' ? weak.deref() : null; } catch (e) { // ignore } if (!element || !(element instanceof Element)) { return { error: `Element ref "${ref}" not found. Please call chrome_read_page first and ensure the ref is still valid.`, }; } } else { element = document.querySelector(selector); } if (!element) { return { error: selector ? `Element with selector "${selector}" not found` : `Element for ref not found`, }; } // Get element information const rect = element.getBoundingClientRect(); const elementInfo = { tagName: element.tagName, id: element.id, className: element.className, type: element.type || null, isVisible: isElementVisible(element), 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, }, }; // Check if element is visible if (!elementInfo.isVisible) { return { error: `Element with selector "${selector}" is not visible`, elementInfo, }; } // Check if element is an input, textarea, or select const validTags = ['INPUT', 'TEXTAREA', 'SELECT']; // Keep a permissive list to allow type-specific branches below to handle behavior const validInputTypes = [ 'text', 'email', 'password', 'number', 'search', 'tel', 'url', 'date', 'datetime-local', 'month', 'time', 'week', 'color', 'checkbox', 'radio', 'range', ]; if (!validTags.includes(element.tagName)) { // If the element is a custom element with open shadow root, try to find a fillable inner control try { const anyEl = /** @type {any} */ (element); const sr = anyEl && anyEl.shadowRoot ? anyEl.shadowRoot : null; if (sr) { // Search common fillable targets inside shadow root (breadth-first) const queue = Array.from(sr.children || []); const isFillable = (el) => !!el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT'); while (queue.length) { const cur = queue.shift(); if (!cur) continue; if (isFillable(cur)) { element = cur; break; } try { const children = cur.children || []; for (let i = 0; i < children.length; i++) queue.push(children[i]); const innerSr = /** @type {any} */ (cur).shadowRoot; if (innerSr && innerSr.children) { for (let i = 0; i < innerSr.children.length; i++) queue.push(innerSr.children[i]); } } catch (_) {} } if (!validTags.includes(element.tagName)) { return { error: `Element with selector "${selector}" is not a fillable element (must be INPUT, TEXTAREA, or SELECT)`, elementInfo, }; } } else { return { error: `Element with selector "${selector}" is not a fillable element (must be INPUT, TEXTAREA, or SELECT)`, elementInfo, }; } } catch (_) { return { error: `Element with selector "${selector}" is not a fillable element (must be INPUT, TEXTAREA, or SELECT)`, elementInfo, }; } } // For input elements, check if the type is valid (allow type-specific branches below) if ( element.tagName === 'INPUT' && !validInputTypes.includes(element.type) && element.type !== null ) { return { error: `Input element with selector "${selector}" has type "${element.type}" which is not fillable`, elementInfo, }; } // Scroll element into view element.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' }); await new Promise((resolve) => setTimeout(resolve, 100)); // Focus the element element.focus(); // Type-specific handling for tricky inputs first if (element.tagName === 'INPUT' && element.type === 'checkbox') { // Accept boolean or string-like boolean let checkedVal; if (typeof value === 'boolean') { checkedVal = value; } else if (typeof value === 'string') { const v = value.trim().toLowerCase(); if (['true', '1', 'yes', 'on'].includes(v)) checkedVal = true; else if (['false', '0', 'no', 'off'].includes(v)) checkedVal = false; } if (typeof checkedVal !== 'boolean') { return { error: 'Checkbox requires a boolean (true/false) or a boolean-like string ("true"/"false"/"on"/"off").', elementInfo, }; } const previous = element.checked; element.checked = checkedVal; element.focus(); element.dispatchEvent(new Event('input', { bubbles: true })); element.dispatchEvent(new Event('change', { bubbles: true })); element.blur(); return { success: true, message: `Checkbox set to ${element.checked}`, elementInfo: { ...elementInfo, checked: element.checked, previousChecked: previous }, }; } if (element.tagName === 'INPUT' && element.type === 'radio') { // For radios, the selector/ref should target the specific input to select const previous = element.checked; element.checked = true; element.focus(); element.dispatchEvent(new Event('input', { bubbles: true })); element.dispatchEvent(new Event('change', { bubbles: true })); element.blur(); return { success: true, message: 'Radio selected', elementInfo: { ...elementInfo, checked: element.checked, previousChecked: previous, name: element.name || null, }, }; } if (element.tagName === 'INPUT' && element.type === 'range') { const numericValue = typeof value === 'number' ? value : Number(value); if (Number.isNaN(numericValue)) { return { error: 'Range input requires a numeric value', elementInfo }; } const previous = element.value; element.value = String(numericValue); element.focus(); element.dispatchEvent(new Event('input', { bubbles: true })); element.dispatchEvent(new Event('change', { bubbles: true })); element.blur(); return { success: true, message: `Set range to ${element.value} (min: ${element.min}, max: ${element.max})`, elementInfo: { ...elementInfo, value: element.value }, }; } if (element.tagName === 'INPUT' && element.type === 'number') { if (value !== '' && value !== null && value !== undefined && Number.isNaN(Number(value))) { return { error: 'Number input requires a numeric value', elementInfo }; } const previous = element.value; element.value = String(value ?? ''); element.focus(); element.dispatchEvent(new Event('input', { bubbles: true })); element.dispatchEvent(new Event('change', { bubbles: true })); element.blur(); return { success: true, message: `Set number input to ${element.value} (previous: ${previous})`, elementInfo: { ...elementInfo, value: element.value }, }; } // Fill the element based on its type if (element.tagName === 'SELECT') { // For select elements, find the option with matching value or text let optionFound = false; for (const option of element.options) { if (option.value === value || option.text === value) { element.value = option.value; optionFound = true; break; } } if (!optionFound) { return { error: `No option with value or text "${value}" found in select element`, elementInfo, }; } // Trigger change event element.dispatchEvent(new Event('change', { bubbles: true })); } else { // For input and textarea elements // Clear the current value then set new value element.value = ''; element.dispatchEvent(new Event('input', { bubbles: true })); element.value = String(value); element.dispatchEvent(new Event('input', { bubbles: true })); element.dispatchEvent(new Event('change', { bubbles: true })); } // Blur the element element.blur(); return { success: true, message: 'Element filled successfully', elementInfo: { ...elementInfo, value: element.value, // Include the final value in the response }, }; } catch (error) { return { error: `Error filling element: ${error.message}`, }; } } /** * 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; } // Check if element is within viewport if ( rect.bottom < 0 || rect.top > window.innerHeight || rect.right < 0 || rect.left > window.innerWidth ) { return false; } // Check if element is actually visible at its center point 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 === 'fillElement') { fillElement(request.selector, request.value, request.ref) .then(sendResponse) .catch((error) => { sendResponse({ error: `Unexpected error: ${error.message}`, }); }); return true; // Indicates async response } else if (request.action === 'chrome_fill_or_select_ping') { sendResponse({ status: 'pong' }); return false; } }); }