| var WebSurfer = WebSurfer || (function () { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| let nextLabel = 10; |
|
|
| let roleMapping = { |
| "a": "link", |
| "area": "link", |
| "button": "button", |
| "input, type=button": "button", |
| "input, type=checkbox": "checkbox", |
| "input, type=email": "textbox", |
| "input, type=number": "spinbutton", |
| "input, type=radio": "radio", |
| "input, type=range": "slider", |
| "input, type=reset": "button", |
| "input, type=search": "searchbox", |
| "input, type=submit": "button", |
| "input, type=tel": "textbox", |
| "input, type=text": "textbox", |
| "input, type=url": "textbox", |
| "search": "search", |
| "select": "combobox", |
| "option": "option", |
| "textarea": "textbox" |
| }; |
|
|
| let getCursor = function (elm) { |
| return window.getComputedStyle(elm)["cursor"]; |
| }; |
|
|
| let isVisible = function (element) { |
| return !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length); |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| let getInteractiveElementsNoShaddow = function () { |
| let results = [] |
| let roles = ["scrollbar", "searchbox", "slider", "spinbutton", "switch", "tab", "treeitem", "button", "checkbox", "gridcell", "link", "menuitem", "menuitemcheckbox", "menuitemradio", "option", "progressbar", "radio", "textbox", "combobox", "menu", "tree", "treegrid", "grid", "listbox", "radiogroup", "widget"]; |
| let inertCursors = ["auto", "default", "none", "text", "vertical-text", "not-allowed", "no-drop"]; |
|
|
| |
| let nodeList = document.querySelectorAll("input, select, textarea, button, [href], [onclick], [contenteditable], [tabindex]:not([tabindex='-1'])"); |
| for (let i = 0; i < nodeList.length; i++) { |
| |
| if (nodeList[i].disabled || !isVisible(nodeList[i])) { |
| continue; |
| } |
| results.push(nodeList[i]); |
| } |
|
|
| |
| nodeList = document.querySelectorAll("[role]"); |
| for (let i = 0; i < nodeList.length; i++) { |
| |
| if (nodeList[i].disabled || !isVisible(nodeList[i])) { |
| continue; |
| } |
| if (results.indexOf(nodeList[i]) == -1) { |
| let role = nodeList[i].getAttribute("role"); |
| if (roles.indexOf(role) > -1) { |
| results.push(nodeList[i]); |
| } |
| } |
| } |
|
|
| |
| nodeList = document.querySelectorAll("*"); |
| for (let i = 0; i < nodeList.length; i++) { |
| let node = nodeList[i]; |
| if (node.disabled || !isVisible(node)) { |
| continue; |
| } |
|
|
| |
| let cursor = getCursor(node); |
| if (inertCursors.indexOf(cursor) >= 0) { |
| continue; |
| } |
|
|
| |
| let parent = node.parentNode; |
| while (parent && getCursor(parent) == cursor) { |
| node = parent; |
| parent = node.parentNode; |
| } |
|
|
| |
| if (results.indexOf(node) == -1) { |
| results.push(node); |
| } |
| } |
|
|
| return results; |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| function gatherAllElements(roles, root = document) { |
| const elements = []; |
| const stack = [root]; |
| const selector = roles.join(","); |
|
|
| while (stack.length > 0) { |
| const currentRoot = stack.pop(); |
|
|
| |
| elements.push(...Array.from(currentRoot.querySelectorAll(selector))); |
|
|
| |
| currentRoot.querySelectorAll("*").forEach(el => { |
| if (el.shadowRoot && el.shadowRoot.mode === "open") { |
| stack.push(el.shadowRoot); |
| } |
| }); |
| } |
|
|
| return elements; |
| } |
|
|
| |
| |
| |
| |
| |
| let getInteractiveElements = function () { |
| |
| const interactive_roles = ["input", "option", "select", "textarea", "button", "href", "onclick", "contenteditable", "tabindex:not([tabindex='-1'])"]; |
|
|
| let results = []; |
|
|
| let elements_no_shaddow = getInteractiveElementsNoShaddow(); |
| for (let i = 0; i < elements_no_shaddow.length; i++) { |
| if (results.indexOf(elements_no_shaddow[i]) == -1) { |
| |
| let rects = elements_no_shaddow[i].getClientRects(); |
| for (const rect of rects) { |
| let x = rect.left + rect.width / 2; |
| let y = rect.top + rect.height / 2; |
| if (isTopmost(elements_no_shaddow[i], x, y)) { |
| results.push(elements_no_shaddow[i]); |
| break; |
| } |
| } |
| } |
| } |
|
|
| |
| let elements_all = gatherAllElements(interactive_roles); |
|
|
| |
| elements_all.forEach(element => { |
| |
| if (element.tagName.toLowerCase() === "input" && element.getAttribute("type") == "file") { |
| results.push(element); |
| return; |
| } |
| |
| if (element.tagName.toLowerCase() === "option") { |
| results.push(element); |
| return; |
| } |
| if (element.disabled || !isVisible(element)) { |
| return; |
| } |
|
|
| if (interactive_roles.includes(element.tagName.toLowerCase())) { |
| results.push(element); |
| } |
| }); |
|
|
| return results; |
| }; |
|
|
| |
| |
| |
| |
| |
| let labelElements = function (elements) { |
| for (let i = 0; i < elements.length; i++) { |
| if (!elements[i].hasAttribute("__elementId")) { |
| elements[i].setAttribute("__elementId", "" + (nextLabel++)); |
| } |
| } |
| return getInteractiveElements(); |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| let isTopmost = function (element, x, y) { |
| let hit = document.elementFromPoint(x, y); |
|
|
| |
| if (hit === null) { |
| return true; |
| } |
|
|
| while (hit) { |
| if (hit == element) return true; |
| hit = hit.parentNode; |
| } |
| return false; |
| }; |
|
|
| let getFocusedElementId = function () { |
| let elm = document.activeElement; |
| while (elm) { |
| if (elm.hasAttribute && elm.hasAttribute("__elementId")) { |
| return elm.getAttribute("__elementId"); |
| } |
| elm = elm.parentNode; |
| } |
| return null; |
| }; |
|
|
| let trimmedInnerText = function (element) { |
| if (!element) { |
| return ""; |
| } |
| let text = element.innerText; |
| if (!text) { |
| return ""; |
| } |
| return text.trim(); |
| }; |
|
|
| let getApproximateAriaName = function (element) { |
| if (element.hasAttribute("aria-label")) { |
| return element.getAttribute("aria-label"); |
| } |
|
|
| |
| if (element.querySelector("span.label")) { |
| return element.querySelector("span.label").innerText; |
| } |
|
|
| |
| if (element.hasAttribute("aria-labelledby")) { |
| let buffer = ""; |
| let ids = element.getAttribute("aria-labelledby").split(" "); |
| for (let i = 0; i < ids.length; i++) { |
| let label = document.getElementById(ids[i]); |
| if (label) { |
| buffer = buffer + " " + trimmedInnerText(label); |
| } |
| } |
| return buffer.trim(); |
| } |
|
|
| if (element.hasAttribute("aria-label")) { |
| return element.getAttribute("aria-label"); |
| } |
|
|
| |
| if (element.hasAttribute("id")) { |
| let label_id = element.getAttribute("id"); |
| let label = ""; |
| try { |
| |
| let escaped_id = CSS.escape(label_id); |
| let labels = document.querySelectorAll(`label[for="${escaped_id}"]`); |
| for (let j = 0; j < labels.length; j++) { |
| label += labels[j].innerText + " "; |
| } |
| label = label.trim(); |
| if (label != "") { |
| return label; |
| } |
| } catch (e) { |
| console.warn("Error finding label for element:", e); |
| } |
| } |
|
|
| if (element.hasAttribute("name")) { |
| return element.getAttribute("name"); |
| } |
|
|
| if (element.parentElement && element.parentElement.tagName == "LABEL") { |
| return element.parentElement.innerText; |
| } |
|
|
| |
| if (element.hasAttribute("alt")) { |
| return element.getAttribute("alt") |
| } |
|
|
| if (element.hasAttribute("title")) { |
| return element.getAttribute("title") |
| } |
|
|
| return trimmedInnerText(element); |
| }; |
|
|
| let getApproximateAriaRole = function (element) { |
| let tag = element.tagName.toLowerCase(); |
| if (tag == "input" && element.hasAttribute("type")) { |
| tag = tag + ", type=" + element.getAttribute("type"); |
| } |
|
|
| if (element.hasAttribute("role")) { |
| return [element.getAttribute("role"), tag]; |
| } |
| else if (tag in roleMapping) { |
| return [roleMapping[tag], tag]; |
| } |
| else { |
| return ["", tag]; |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| let getInteractiveRects = function () { |
| let elements = labelElements(getInteractiveElements()); |
| let results = {}; |
| for (let i = 0; i < elements.length; i++) { |
| let key = elements[i].getAttribute("__elementId"); |
| let rects = elements[i].getBoundingClientRect(); |
|
|
| |
| if (elements[i].tagName.toLowerCase() === "option") { |
|
|
| let select_focused = false; |
| let select = elements[i].closest("select"); |
| if (select && select.hasAttribute("__elementId") && |
| getFocusedElementId() === select.getAttribute("__elementId")) { |
| select_focused = true; |
| } |
| |
| let option_visible = false; |
| if (isVisible(elements[i])) { |
| option_visible = true; |
| } |
| |
| let select_expanded = false; |
| if (select && select.hasAttribute("open")) { |
| select_expanded = true; |
| } |
| if (!(select_focused || option_visible || select_expanded)) { |
| continue; |
| } |
| } |
|
|
| let ariaRole = getApproximateAriaRole(elements[i]); |
| let ariaName = getApproximateAriaName(elements[i]); |
| let vScrollable = elements[i].scrollHeight - elements[i].clientHeight >= 1; |
|
|
| let record = { |
| "tag_name": ariaRole[1], |
| "role": ariaRole[0], |
| "aria-name": ariaName, |
| "v-scrollable": vScrollable, |
| "rects": [] |
| }; |
|
|
| if (rects.length > 0) { |
| for (const rect of rects) { |
| let x = rect.left + rect.width / 2; |
| let y = rect.top + rect.height / 2; |
| if (isTopmost(elements[i], x, y)) { |
| record["rects"].push(JSON.parse(JSON.stringify(rect))); |
| } |
| } |
| } |
| else { |
| record["rects"].push(JSON.parse(JSON.stringify(rects))); |
| } |
|
|
| results[key] = record; |
| } |
| return results; |
| }; |
|
|
| |
| |
| |
| |
| let getVisualViewport = function () { |
| let vv = window.visualViewport; |
| let de = document.documentElement; |
| return { |
| "height": vv ? vv.height : 0, |
| "width": vv ? vv.width : 0, |
| "offsetLeft": vv ? vv.offsetLeft : 0, |
| "offsetTop": vv ? vv.offsetTop : 0, |
| "pageLeft": vv ? vv.pageLeft : 0, |
| "pageTop": vv ? vv.pageTop : 0, |
| "scale": vv ? vv.scale : 0, |
| "clientWidth": de ? de.clientWidth : 0, |
| "clientHeight": de ? de.clientHeight : 0, |
| "scrollWidth": de ? de.scrollWidth : 0, |
| "scrollHeight": de ? de.scrollHeight : 0 |
| }; |
| }; |
|
|
| let _getMetaTags = function () { |
| let meta = document.querySelectorAll("meta"); |
| let results = {}; |
| for (let i = 0; i < meta.length; i++) { |
| let key = null; |
| if (meta[i].hasAttribute("name")) { |
| key = meta[i].getAttribute("name"); |
| } |
| else if (meta[i].hasAttribute("property")) { |
| key = meta[i].getAttribute("property"); |
| } |
| else { |
| continue; |
| } |
| if (meta[i].hasAttribute("content")) { |
| results[key] = meta[i].getAttribute("content"); |
| } |
| } |
| return results; |
| }; |
|
|
| let _getJsonLd = function () { |
| let jsonld = []; |
| let scripts = document.querySelectorAll('script[type="application/ld+json"]'); |
| for (let i = 0; i < scripts.length; i++) { |
| jsonld.push(scripts[i].innerHTML.trim()); |
| } |
| return jsonld; |
| }; |
|
|
| |
| let _getMicrodata = function () { |
| function sanitize(input) { |
| return input.replace(/\s/gi, ' ').trim(); |
| } |
|
|
| function addValue(information, name, value) { |
| if (information[name]) { |
| if (typeof information[name] === 'array') { |
| information[name].push(value); |
| } else { |
| const arr = []; |
| arr.push(information[name]); |
| arr.push(value); |
| information[name] = arr; |
| } |
| } else { |
| information[name] = value; |
| } |
| } |
|
|
| function traverseItem(item, information) { |
| const children = item.children; |
|
|
| for (let i = 0; i < children.length; i++) { |
| const child = children[i]; |
|
|
| if (child.hasAttribute('itemscope')) { |
| if (child.hasAttribute('itemprop')) { |
| const itemProp = child.getAttribute('itemprop'); |
| const itemType = child.getAttribute('itemtype'); |
|
|
| const childInfo = { |
| itemType: itemType |
| }; |
|
|
| traverseItem(child, childInfo); |
|
|
| itemProp.split(' ').forEach(propName => { |
| addValue(information, propName, childInfo); |
| }); |
| } |
|
|
| } else if (child.hasAttribute('itemprop')) { |
| const itemProp = child.getAttribute('itemprop'); |
| itemProp.split(' ').forEach(propName => { |
| if (propName === 'url') { |
| addValue(information, propName, child.href); |
| } else { |
| addValue(information, propName, sanitize(child.getAttribute("content") || child.content || child.textContent || child.src || "")); |
| } |
| }); |
| traverseItem(child, information); |
| } else { |
| traverseItem(child, information); |
| } |
| } |
| } |
|
|
| const microdata = []; |
|
|
| document.querySelectorAll("[itemscope]").forEach(function (elem, i) { |
| const itemType = elem.getAttribute('itemtype'); |
| const information = { |
| itemType: itemType |
| }; |
| traverseItem(elem, information); |
| microdata.push(information); |
| }); |
|
|
| return microdata; |
| }; |
|
|
| let getPageMetadata = function () { |
| let jsonld = _getJsonLd(); |
| let metaTags = _getMetaTags(); |
| let microdata = _getMicrodata(); |
| let results = {} |
| if (jsonld.length > 0) { |
| try { |
| results["jsonld"] = JSON.parse(jsonld); |
| } |
| catch (e) { |
| results["jsonld"] = jsonld; |
| } |
| } |
| if (microdata.length > 0) { |
| results["microdata"] = microdata; |
| } |
| for (let key in metaTags) { |
| if (metaTags.hasOwnProperty(key)) { |
| results["meta_tags"] = metaTags; |
| break; |
| } |
| } |
| return results; |
| }; |
|
|
| |
| |
| |
| |
| |
| let getVisibleText = function () { |
| |
| const viewportHeight = window.innerHeight || document.documentElement.clientHeight; |
| const viewportWidth = window.innerWidth || document.documentElement.clientWidth; |
|
|
| let textInView = ""; |
| const walker = document.createTreeWalker( |
| document.body, |
| NodeFilter.SHOW_TEXT, |
| null, |
| false |
| ); |
|
|
| while (walker.nextNode()) { |
| const textNode = walker.currentNode; |
| |
| const range = document.createRange(); |
| range.selectNodeContents(textNode); |
|
|
| const rects = range.getClientRects(); |
|
|
| |
| for (const rect of rects) { |
| const isVisible = |
| rect.width > 0 && |
| rect.height > 0 && |
| rect.bottom >= 0 && |
| rect.right >= 0 && |
| rect.top <= viewportHeight && |
| rect.left <= viewportWidth; |
|
|
| if (isVisible) { |
| textInView += textNode.nodeValue.replace(/\s+/g, " "); |
| |
| if (textNode.parentNode) { |
| const parent = textNode.parentNode; |
| const style = window.getComputedStyle(parent); |
| if (["inline", "hidden", "none"].indexOf(style.display) === -1) { |
| textInView += "\n"; |
| } |
| } |
| break; |
| } |
| } |
| } |
|
|
| |
| textInView = textInView.replace(/^\s*\n/gm, "").trim().replace(/\n+/g, "\n"); |
| return textInView; |
| }; |
|
|
| |
| return { |
| getInteractiveRects: getInteractiveRects, |
| getVisualViewport: getVisualViewport, |
| getFocusedElementId: getFocusedElementId, |
| getPageMetadata: getPageMetadata, |
| getVisibleText: getVisibleText, |
| }; |
| })(); |
|
|