webrl/VAB-WebArena-Lite/browser_env/html_tools/scripts/clickable_checker.js
2025-04-23 17:01:18 +08:00

148 lines
5.8 KiB
JavaScript
Executable File

() => {
var items = Array.prototype.slice.call(
document.querySelectorAll('*')
).map(function(element) {
var vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
var vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
var rects = [...element.getClientRects()].filter(bb => {
var center_x = bb.left + bb.width / 2;
var center_y = bb.top + bb.height / 2;
var elAtCenter = document.elementFromPoint(center_x, center_y);
if (!elAtCenter) return false;
return elAtCenter === element || element.contains(elAtCenter)
}).map(bb => {
const rect = {
left: Math.max(0, bb.left),
top: Math.max(0, bb.top),
right: Math.min(vw, bb.right),
bottom: Math.min(vh, bb.bottom)
};
return {
...rect,
width: rect.right - rect.left,
height: rect.bottom - rect.top
}
});
// var rects = [];
var area = rects.reduce((acc, rect) => acc + rect.width * rect.height, 0);
const tagName = element.tagName.toLowerCase?.() || "";
let isClickable = ((element.onclick != null) || window.getComputedStyle(element).cursor == "pointer");
// Insert area elements that provide click functionality to an img.
if (tagName === "img") {
let mapName = element.getAttribute("usemap");
if (mapName) {
const imgClientRects = element.getClientRects();
mapName = mapName.replace(/^#/, "").replace('"', '\\"');
const map = document.querySelector(`map[name=\"${mapName}\"]`);
if (map && (imgClientRects.length > 0)) isClickable = true;
}
}
if (!isClickable) {
const role = element.getAttribute("role");
const clickableRoles = [
"button",
"tab",
"link",
"checkbox",
"menuitem",
"menuitemcheckbox",
"menuitemradio",
"radio",
];
if (role != null && clickableRoles.includes(role.toLowerCase())) {
isClickable = true;
} else {
const contentEditable = element.getAttribute("contentEditable");
if (
contentEditable != null &&
["", "contenteditable", "true"].includes(contentEditable.toLowerCase())
) {
isClickable = true;
}
}
}
// Check for jsaction event listeners on the element.
if (!isClickable && element.hasAttribute("jsaction")) {
const jsactionRules = element.getAttribute("jsaction").split(";");
for (let jsactionRule of jsactionRules) {
const ruleSplit = jsactionRule.trim().split(":");
if ((ruleSplit.length >= 1) && (ruleSplit.length <= 2)) {
const [eventType, namespace, actionName] = ruleSplit.length === 1
? ["click", ...ruleSplit[0].trim().split("."), "_"]
: [ruleSplit[0], ...ruleSplit[1].trim().split("."), "_"];
if (!isClickable) {
isClickable = (eventType === "click") && (namespace !== "none") && (actionName !== "_");
}
}
}
}
if (!isClickable) {
const clickableTags = [
"input",
"textarea",
"select",
"button",
"a",
"iframe",
"video",
"object",
"embed",
"details"
];
isClickable = clickableTags.includes(tagName);
}
if (!isClickable) {
if (tagName === "label")
isClickable = (element.control != null) && !element.control.disabled;
else if (tagName === "img")
isClickable = ["zoom-in", "zoom-out"].includes(element.style.cursor);
}
// An element with a class name containing the text "button" might be clickable. However, real
// clickables are often wrapped in elements with such class names. So, when we find clickables
// based only on their class name, we mark them as unreliable.
const className = element.getAttribute("class");
if (!isClickable && className && className.toLowerCase().includes("button")) {
isClickable = true;
}
// Elements with tabindex are sometimes useful, but usually not. We can treat them as second
// class citizens when it improves UX, so take special note of them.
const tabIndexValue = element.getAttribute("tabindex");
const tabIndex = tabIndexValue ? parseInt(tabIndexValue) : -1;
if (!isClickable && !(tabIndex < 0) && !isNaN(tabIndex)) {
isClickable = true;
}
const idValue = element.getAttribute("id");
const id = idValue ? idValue.toLowerCase() : "";
if (isClickable && area == 0) {
const textValue = element.textContent.trim().replace(/\s{2,}/g, ' ');
clickable_msg = `${tagName}[id=${id}] ${isClickable} (${area}) ${textValue}`
}
return {
element: element,
include: isClickable,
area,
rects,
text: element.textContent.trim().replace(/\s{2,}/g, ' ')
};
}).filter(item =>
item.include && (item.area >= 1)
);
items = items.filter(x => !items.some(y => x.element.contains(y.element) && !(x == y)))
items.forEach(item => {
item.element.classList.add('possible-clickable-element');
});
}