148 lines
5.8 KiB
JavaScript
148 lines
5.8 KiB
JavaScript
() => {
|
|
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');
|
|
});
|
|
} |