XanderJC's picture
setup
f0f6e5c
marked_elements_convergence = [];
const interactiveTags = new Set([
'a', 'button', 'details', 'embed', 'input', 'label',
'menu', 'menuitem', 'object', 'select', 'textarea', 'summary',
'video', 'audio', 'option', 'iframe'
]);
const interactiveRoles = new Set([
'button', 'menu', 'menuitem', 'link', 'checkbox', 'radio',
'slider', 'tab', 'tabpanel', 'textbox', 'combobox', 'grid',
'listbox', 'option', 'progressbar', 'scrollbar', 'searchbox',
'switch', 'tree', 'treeitem', 'spinbutton', 'tooltip',
'a-button-inner', 'a-dropdown-button', 'click',
'menuitemcheckbox', 'menuitemradio', 'a-button-text',
'button-text', 'button-icon', 'button-icon-only',
'button-text-icon-only', 'dropdown', 'combobox'
]);
findPOIsConvergence = (input = null) => {
let rootElement = input ? input : document.documentElement;
function isScrollable(element) {
if ((input === null) && (element === document.documentElement)) {
// we can always scroll the full page
return false;
}
const style = window.getComputedStyle(element);
const hasScrollableYContent = element.scrollHeight > element.clientHeight
const overflowYScroll = style.overflowY === 'scroll' || style.overflowY === 'auto';
const hasScrollableXContent = element.scrollWidth > element.clientWidth;
const overflowXScroll = style.overflowX === 'scroll' || style.overflowX === 'auto';
return (hasScrollableYContent && overflowYScroll) || (hasScrollableXContent && overflowXScroll);
}
function getEventListeners(element) {
try {
return window.getEventListeners?.(element) || {};
} catch (e) {
return {};
}
}
function isInteractive(element) {
if (!element) return false;
return (hasInteractiveTag(element) ||
hasInteractiveAttributes(element) ||
hasInteractiveEventListeners(element)) ||
isScrollable(element);
}
function hasInteractiveTag(element) {
return interactiveTags.has(element.tagName.toLowerCase());
}
function hasInteractiveAttributes(element) {
const role = element.getAttribute('role');
const ariaRole = element.getAttribute('aria-role');
const tabIndex = element.getAttribute('tabindex');
const onAttribute = element.getAttribute('on');
if (element.getAttribute('contenteditable') === 'true') return true;
if ((role && interactiveRoles.has(role)) ||
(ariaRole && interactiveRoles.has(ariaRole))) return true;
if (tabIndex !== null && tabIndex !== '-1') return true;
// Add check for AMP's 'on' attribute that starts with 'tap:'
if (onAttribute && onAttribute.startsWith('tap:')) return true;
const hasAriaProps = element.hasAttribute('aria-expanded') ||
element.hasAttribute('aria-pressed') ||
element.hasAttribute('aria-selected') ||
element.hasAttribute('aria-checked');
return hasAriaProps;
}
function hasInteractiveEventListeners(element) {
const hasClickHandler = element.onclick !== null ||
element.getAttribute('onclick') !== null ||
element.hasAttribute('ng-click') ||
element.hasAttribute('@click') ||
element.hasAttribute('v-on:click');
if (hasClickHandler) return true;
const listeners = getEventListeners(element);
return listeners && (
listeners.click?.length > 0 ||
listeners.mousedown?.length > 0 ||
listeners.mouseup?.length > 0 ||
listeners.touchstart?.length > 0 ||
listeners.touchend?.length > 0
);
}
function calculateArea(rects) {
return rects.reduce((acc, rect) => acc + rect.width * rect.height, 0);
}
function getElementRects(element, context) {
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
let rects = [...element.getClientRects()];
// If rects are empty (likely due to Shadow DOM), try to estimate position
if (rects.length === 0 && element.getBoundingClientRect) {
rects = [element.getBoundingClientRect()];
}
// Get iframe offset if element is in an iframe
let iframeOffset = { x: 0, y: 0 };
if (context !== document && context?.defaultView?.frameElement) {
const iframe = context.defaultView.frameElement;
if (iframe) {
const iframeRect = iframe.getBoundingClientRect();
iframeOffset = {
x: iframeRect.left,
y: iframeRect.top
};
}
}
return rects.filter(bb => {
const center_x = bb.left + bb.width / 2 + iframeOffset.x;
const center_y = bb.top + bb.height / 2 + iframeOffset.y;
const elAtCenter = context.elementFromPoint(center_x - iframeOffset.x, center_y - iframeOffset.y);
return elAtCenter === element || element.contains(elAtCenter);
}).map(bb => {
const rect = {
left: Math.max(0, bb.left + iframeOffset.x),
top: Math.max(0, bb.top + iframeOffset.y),
right: Math.min(vw, bb.right + iframeOffset.x),
bottom: Math.min(vh, bb.bottom + iframeOffset.y)
};
return {
...rect,
width: rect.right - rect.left,
height: rect.bottom - rect.top
};
});
}
function isElementVisible(element) {
const style = window.getComputedStyle(element);
return element.offsetWidth > 0 &&
element.offsetHeight > 0 &&
style.visibility !== 'hidden' &&
style.display !== 'none';
}
function isTopElement(element) {
let doc = element.ownerDocument;
if (doc !== window.document) {
// If in an iframe's document, treat as top
return true;
}
const shadowRoot = element.getRootNode();
if (shadowRoot instanceof ShadowRoot) {
const rect = element.getBoundingClientRect();
const point = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
try {
const topEl = shadowRoot.elementFromPoint(point.x, point.y);
if (!topEl) return false;
let current = topEl;
while (current && current !== shadowRoot) {
if (current === element) return true;
current = current.parentElement;
}
return false;
} catch (e) {
return true;
}
}
const rect = element.getBoundingClientRect();
const point = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
try {
const topEl = document.elementFromPoint(point.x, point.y);
if (!topEl) return false;
let current = topEl;
while (current && current !== document.documentElement) {
if (current === element) return true;
current = current.parentElement;
}
return false;
} catch (e) {
return true;
}
}
function getVisibleText(element, marked_elements_convergence = []) {
const blockLikeDisplays = [
// Basic block elements
'block', 'flow-root', 'inline-block',
// Lists
'list-item',
// Table elements
'table', 'inline-table', 'table-row', 'table-cell',
'table-caption', 'table-header-group', 'table-footer-group',
'table-row-group',
// Modern layouts
'flex', 'inline-flex', 'grid', 'inline-grid'
];
// Check if element is hidden
const style = window.getComputedStyle(element);
if (style.display === 'none' || style.visibility === 'hidden') {
return '';
}
let collectedText = [];
function isMarkedInteractive(el) {
return marked_elements_convergence.includes(el);
}
function traverse(node) {
if (
node.nodeType === Node.ELEMENT_NODE &&
node !== element &&
isMarkedInteractive(node)
) {
return false;
}
if (node.nodeType === Node.TEXT_NODE) {
const trimmed = node.textContent.trim();
if (trimmed) {
collectedText.push(trimmed);
}
} else if (node.nodeType === Node.ELEMENT_NODE) {
// Skip noscript elements
if (node.tagName === 'NOSCRIPT') {
return true;
}
const nodeStyle = window.getComputedStyle(node);
// Skip hidden elements
if (nodeStyle.display === 'none' || nodeStyle.visibility === 'hidden') {
return true;
}
// Add newline before block elements if we have text
if (blockLikeDisplays.includes(nodeStyle.display) && collectedText.length > 0) {
collectedText.push('\n');
}
if (node.tagName === 'IMG') {
const textParts = [];
const alt = node.getAttribute('alt');
const title = node.getAttribute('title');
const ariaLabel = node.getAttribute('aria-label');
// Add more as needed (e.g., 'aria-describedby', 'data-caption', etc.)
if (alt) textParts.push(`alt="${alt}"`);
if (title) textParts.push(`title="${title}"`);
if (ariaLabel) textParts.push(`aria-label="${ariaLabel}"`);
if (textParts.length > 0) {
collectedText.push(`[img - ${textParts.join(' ')}]`);
}
return true;
}
for (const child of node.childNodes) {
const shouldContinue = traverse(child);
if (shouldContinue === false) {
return false;
}
}
// Add newline after block elements
if (blockLikeDisplays.includes(nodeStyle.display)) {
collectedText.push('\n');
}
}
return true;
}
traverse(element);
// Join text and normalize whitespace
return collectedText.join(' ').trim().replace(/\s{2,}/g, ' ').trim();
}
function extractInteractiveItems(rootElement) {
const items = [];
function processElement(element, context) {
if (!element) return;
// Recursively process elements
if (element.nodeType === Node.ELEMENT_NODE && isInteractive(element) && isElementVisible(element) && isTopElement(element)) {
const rects = getElementRects(element, context);
const area = calculateArea(rects);
items.push({
element: element,
area,
rects,
is_scrollable: isScrollable(element),
});
}
if (element.shadowRoot) {
// if it's shadow DOM, process elements in the shadow DOM
Array.from(element.shadowRoot.childNodes || []).forEach(child => {
processElement(child, element.shadowRoot);
});
}
if (element.tagName === 'SLOT') {
// Handle both assigned elements and nodes
const assigned = element.assignedNodes ? element.assignedNodes() : element.assignedElements();
assigned.forEach(child => {
processElement(child, context);
});
}
else if (element.tagName === 'IFRAME') {
try {
const iframeDoc = element.contentDocument || element.contentWindow?.document;
if (iframeDoc && iframeDoc.body) {
// Process elements inside iframe
processElement(iframeDoc.body, iframeDoc);
}
} catch (e) {
console.warn('Unable to access iframe contents:', e);
}
} else {
// if it's regular child elements, process regular child elements
Array.from(element.children || []).forEach(child => {
processElement(child, context);
});
}
}
processElement(rootElement, document);
return items;
}
if (marked_elements_convergence) {
marked_elements_convergence = [];
}
let mark_centres = [];
let marked_element_descriptions = [];
var items = extractInteractiveItems(rootElement);
// Lets create a floating border on top of these elements that will always be visible
let index = 0;
items.forEach(function (item) {
item.rects.forEach((bbox) => {
marked_elements_convergence.push(item.element);
mark_centres.push({
x: Math.round((bbox.left + bbox.right) / 2),
y: Math.round((bbox.top + bbox.bottom) / 2),
left: bbox.left,
top: bbox.top,
right: bbox.right,
bottom: bbox.bottom,
});
marked_element_descriptions.push({
tag: item.element.tagName,
text: getVisibleText(item.element),
// NOTE: all other attributes will be shown to the model when present
// TODO: incorperate child attributes, e.g. <img alt="..."> when img is a child of the link element
value: item.element.value,
placeholder: item.element.getAttribute("placeholder"),
element_type: item.element.getAttribute("type"),
aria_label: item.element.getAttribute("aria-label"),
name: item.element.getAttribute("name"),
required: item.element.getAttribute("required"),
disabled: item.element.getAttribute("disabled"),
pattern: item.element.getAttribute("pattern"),
checked: item.element.getAttribute("checked"),
minlength: item.element.getAttribute("minlength"),
maxlength: item.element.getAttribute("maxlength"),
role: item.element.getAttribute("role"),
title: item.element.getAttribute("title"),
scrollable: item.is_scrollable
});
index++;
});
});
return {
element_descriptions: marked_element_descriptions,
element_centroids: mark_centres
};
}