235 lines
7.4 KiB
JavaScript
235 lines
7.4 KiB
JavaScript
/* eslint-disable */
|
|
// wait-helper.js
|
|
// Listen for text appearance/disappearance in the current document using MutationObserver.
|
|
// Returns a stable ref (compatible with accessibility-tree-helper) for the first matching element.
|
|
|
|
(function () {
|
|
if (window.__WAIT_HELPER_INITIALIZED__) return;
|
|
window.__WAIT_HELPER_INITIALIZED__ = true;
|
|
|
|
// Ensure ref mapping infra exists (compatible with accessibility-tree-helper.js)
|
|
if (!window.__claudeElementMap) window.__claudeElementMap = {};
|
|
if (!window.__claudeRefCounter) window.__claudeRefCounter = 0;
|
|
|
|
function isVisible(el) {
|
|
try {
|
|
if (!(el instanceof Element)) return false;
|
|
const style = getComputedStyle(el);
|
|
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0')
|
|
return false;
|
|
const rect = el.getBoundingClientRect();
|
|
if (rect.width <= 0 || rect.height <= 0) return false;
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function normalize(str) {
|
|
return String(str || '')
|
|
.replace(/\s+/g, ' ')
|
|
.trim()
|
|
.toLowerCase();
|
|
}
|
|
|
|
function matchesText(el, needle) {
|
|
const t = normalize(needle);
|
|
if (!t) return false;
|
|
try {
|
|
if (!isVisible(el)) return false;
|
|
const aria = el.getAttribute('aria-label');
|
|
if (aria && normalize(aria).includes(t)) return true;
|
|
const title = el.getAttribute('title');
|
|
if (title && normalize(title).includes(t)) return true;
|
|
const alt = el.getAttribute('alt');
|
|
if (alt && normalize(alt).includes(t)) return true;
|
|
const placeholder = el.getAttribute('placeholder');
|
|
if (placeholder && normalize(placeholder).includes(t)) return true;
|
|
// input/textarea value
|
|
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
const value = el.value || el.getAttribute('value');
|
|
if (value && normalize(value).includes(t)) return true;
|
|
}
|
|
const text = el.innerText || el.textContent || '';
|
|
if (normalize(text).includes(t)) return true;
|
|
} catch {}
|
|
return false;
|
|
}
|
|
|
|
function findElementByText(text) {
|
|
// Fast path: query common interactive elements first
|
|
const prioritized = Array.from(
|
|
document.querySelectorAll('a,button,input,textarea,select,label,summary,[role]'),
|
|
);
|
|
for (const el of prioritized) if (matchesText(el, text)) return el;
|
|
|
|
// Fallback: broader scan with cap to avoid blocking on huge pages
|
|
const walker = document.createTreeWalker(
|
|
document.body || document.documentElement,
|
|
NodeFilter.SHOW_ELEMENT,
|
|
);
|
|
let count = 0;
|
|
while (walker.nextNode()) {
|
|
const el = /** @type {Element} */ (walker.currentNode);
|
|
if (matchesText(el, text)) return el;
|
|
if (++count > 5000) break; // Hard cap to avoid long scans
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function ensureRefForElement(el) {
|
|
// Try to reuse an existing ref
|
|
for (const k in window.__claudeElementMap) {
|
|
const weak = window.__claudeElementMap[k];
|
|
if (weak && typeof weak.deref === 'function' && weak.deref() === el) return k;
|
|
}
|
|
const refId = `ref_${++window.__claudeRefCounter}`;
|
|
window.__claudeElementMap[refId] = new WeakRef(el);
|
|
return refId;
|
|
}
|
|
|
|
function centerOf(el) {
|
|
const r = el.getBoundingClientRect();
|
|
return { x: Math.round(r.left + r.width / 2), y: Math.round(r.top + r.height / 2) };
|
|
}
|
|
|
|
function waitFor({ text, appear = true, timeout = 5000 }) {
|
|
return new Promise((resolve) => {
|
|
const start = Date.now();
|
|
let resolved = false;
|
|
|
|
const check = () => {
|
|
try {
|
|
const match = findElementByText(text);
|
|
if (appear) {
|
|
if (match) {
|
|
const ref = ensureRefForElement(match);
|
|
const center = centerOf(match);
|
|
done({ success: true, matched: { ref, center }, tookMs: Date.now() - start });
|
|
}
|
|
} else {
|
|
// wait for disappearance
|
|
if (!match) {
|
|
done({ success: true, matched: null, tookMs: Date.now() - start });
|
|
}
|
|
}
|
|
} catch {}
|
|
};
|
|
|
|
const done = (result) => {
|
|
if (resolved) return;
|
|
resolved = true;
|
|
obs && obs.disconnect();
|
|
clearTimeout(timer);
|
|
resolve(result);
|
|
};
|
|
|
|
const obs = new MutationObserver(() => check());
|
|
try {
|
|
obs.observe(document.documentElement || document.body, {
|
|
subtree: true,
|
|
childList: true,
|
|
characterData: true,
|
|
attributes: true,
|
|
});
|
|
} catch {}
|
|
|
|
// Initial check
|
|
check();
|
|
const timer = setTimeout(
|
|
() => {
|
|
done({ success: false, reason: 'timeout', tookMs: Date.now() - start });
|
|
},
|
|
Math.max(0, timeout),
|
|
);
|
|
});
|
|
}
|
|
|
|
function waitForSelector({ selector, visible = true, timeout = 5000 }) {
|
|
return new Promise((resolve) => {
|
|
const start = Date.now();
|
|
let resolved = false;
|
|
|
|
const isMatch = () => {
|
|
try {
|
|
const el = document.querySelector(selector);
|
|
if (!el) return null;
|
|
if (!visible) return el;
|
|
return isVisible(el) ? el : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const done = (result) => {
|
|
if (resolved) return;
|
|
resolved = true;
|
|
obs && obs.disconnect();
|
|
clearTimeout(timer);
|
|
resolve(result);
|
|
};
|
|
|
|
const check = () => {
|
|
const el = isMatch();
|
|
if (el) {
|
|
const ref = ensureRefForElement(el);
|
|
const center = centerOf(el);
|
|
done({ success: true, matched: { ref, center }, tookMs: Date.now() - start });
|
|
}
|
|
};
|
|
|
|
const obs = new MutationObserver(check);
|
|
try {
|
|
obs.observe(document.documentElement || document.body, {
|
|
subtree: true,
|
|
childList: true,
|
|
characterData: true,
|
|
attributes: true,
|
|
});
|
|
} catch {}
|
|
|
|
// initial check
|
|
check();
|
|
const timer = setTimeout(
|
|
() => done({ success: false, reason: 'timeout', tookMs: Date.now() - start }),
|
|
Math.max(0, timeout),
|
|
);
|
|
});
|
|
}
|
|
|
|
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
|
|
try {
|
|
if (request && request.action === 'wait_helper_ping') {
|
|
sendResponse({ status: 'pong' });
|
|
return false;
|
|
}
|
|
if (request && request.action === 'waitForText') {
|
|
const text = String(request.text || '').trim();
|
|
const appear = request.appear !== false; // default true
|
|
const timeout = Number(request.timeout || 5000);
|
|
if (!text) {
|
|
sendResponse({ success: false, error: 'text is required' });
|
|
return true;
|
|
}
|
|
waitFor({ text, appear, timeout }).then((res) => sendResponse(res));
|
|
return true; // async
|
|
}
|
|
if (request && request.action === 'waitForSelector') {
|
|
const selector = String(request.selector || '').trim();
|
|
const visible = request.visible !== false; // default true
|
|
const timeout = Number(request.timeout || 5000);
|
|
if (!selector) {
|
|
sendResponse({ success: false, error: 'selector is required' });
|
|
return true;
|
|
}
|
|
waitForSelector({ selector, visible, timeout }).then((res) => sendResponse(res));
|
|
return true; // async
|
|
}
|
|
} catch (e) {
|
|
sendResponse({ success: false, error: String(e && e.message ? e.message : e) });
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
})();
|