| var MultimodalWebSurfer = MultimodalWebSurfer || (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,
|
| };
|
| })(); |