UNPKG

openkeynav

Version:

OpenKeyNav: A JavaScript plugin for enhancing keyboard navigation and accessibility on web pages.

253 lines (236 loc) 11.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.showMoveableFromOverlays = exports.showClickableOverlays = exports.generateValidKeyChars = exports.generateLabels = exports.filterRemainingOverlays = void 0; var _escape = require("./escape"); var _isTabbable = require("./isTabbable"); var _scrolling = require("./scrolling"); function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); } function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } var generateLabels = exports.generateLabels = function generateLabels(openKeyNav, count) { function shuffle(array) { for (var i = array.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var _ref = [array[j], array[i]]; array[i] = _ref[0]; array[j] = _ref[1]; } return array; } var labels = []; var chars = generateValidKeyChars(openKeyNav); var maxLength = Math.pow(chars.length, 2); var useThirdChar = count > maxLength; if (useThirdChar) { maxLength = Math.pow(chars.length, 3); } for (var i = 0; i < count && labels.length < maxLength; i++) { var firstChar = chars[i % chars.length]; var secondChar = chars[Math.floor(i / chars.length) % chars.length] || ''; var thirdChar = useThirdChar ? chars[Math.floor(i / Math.pow(chars.length, 2)) % chars.length] : ''; labels.push(firstChar + secondChar + thirdChar); } // Attempt to shorten labels that are uniquely identifiable by their first character var labelCounts = {}; labels.forEach(function (label) { var firstChar = label[0]; labelCounts[firstChar] = (labelCounts[firstChar] || 0) + 1; }); labels = labels.map(function (label) { var firstChar = label[0]; if (labelCounts[firstChar] === 1 && !label.includes('.')) { // Check for uniqueness and ensure not shortened if it's a prefix return firstChar; } return label; }); // alert(labels) // now we have all the labels we will use. // Shuffle them for variable rewards. ++addiction // return shuffle(labels); return labels; // unshuffled }; var showClickableOverlays = exports.showClickableOverlays = function showClickableOverlays(openKeyNav) { (0, _scrolling.disableScrolling)(openKeyNav); setTimeout(function () { var clickables = _getAllCandidateElements(openKeyNav, document).filter(function (el) { return (0, _isTabbable.isTabbable)(el, openKeyNav); }); // console.log(clickables); var labels = generateLabels(openKeyNav, clickables.length); clickables.forEach(function (element, index) { element.setAttribute('data-openkeynav-label', labels[index]); }); clickables.forEach(function (element, index) { openKeyNav.createOverlay(element, labels[index]); }); }, 0); // Use timeout to ensure the operation completes }; var showMoveableFromOverlays = exports.showMoveableFromOverlays = function showMoveableFromOverlays(openKeyNav) { // alert("showMoveableFromOverlays()"); // return; // Combine all unique 'from' classes from moveConfig to query the document var moveables = []; // direct selectors of from elements var fromElementSelectors = _toConsumableArray(new Set(openKeyNav.config.modesConfig.move.config.filter(function (config) { return config.fromElements; }).map(function (config) { return config.fromElements; }))); if (!!fromElementSelectors.length) { document.querySelectorAll(fromElementSelectors.join(', ')).forEach(function (element) { var config = openKeyNav.config.modesConfig.move.config.find(function (c) { return element.matches(c.fromElements); }); if (config) { var configKey = openKeyNav.config.modesConfig.move.config.indexOf(config); if (openKeyNav.isNonzeroSize(element) && (!config.fromExclude || !element.matches(config.fromExclude))) { element.setAttribute('data-openkeynav-moveconfig', configKey); // Store the moveConfig key moveables.push(element); } } }); } // containers of from elements var fromContainerSelectors = _toConsumableArray(new Set(openKeyNav.config.modesConfig.move.config.filter(function (config) { return config.fromContainer; }).map(function (config) { return config.fromContainer; }))); if (!!fromContainerSelectors.length) { var fromContainers = document.querySelectorAll(fromContainerSelectors.join(', ')); // Collect all direct children of each fromContainer as moveable elements fromContainers.forEach(function (container) { var config = openKeyNav.config.modesConfig.move.config.find(function (c) { return container.matches(c.fromContainer); }); if (config) { var configKey = openKeyNav.config.modesConfig.move.config.indexOf(config); var children = Array.from(container.children); children.forEach(function (child) { if (openKeyNav.isNonzeroSize(child) && (!config.fromExclude || !child.matches(config.fromExclude))) { child.setAttribute('data-openkeynav-moveconfig', configKey); // Store the moveConfig key moveables.push(child); } }); } }); } // Resolve elements using provided callbacks if available openKeyNav.config.modesConfig.move.config.forEach(function (config) { if (config.resolveFromElements) { var resolvedElements = config.resolveFromElements(); resolvedElements.forEach(function (element) { var configKey = openKeyNav.config.modesConfig.move.config.indexOf(config); if (openKeyNav.isNonzeroSize(element) && (!config.fromExclude || !element.matches(config.fromExclude))) { element.setAttribute('data-openkeynav-moveconfig', configKey); // Store the moveConfig key moveables.push(element); } }); } }); // filter out moveables that would not be clickable moveables = moveables.filter(function (el) { return (0, _isTabbable.isTabbable)(el, openKeyNav); }); var labels = generateLabels(openKeyNav, moveables.length); moveables.forEach(function (element, index) { element.setAttribute('data-openkeynav-label', labels[index]); }); moveables.forEach(function (element, index) { openKeyNav.createOverlay(element, labels[index]); element.setAttribute('data-openkeynav-draggable', 'true'); }); }; var filterRemainingOverlays = exports.filterRemainingOverlays = function filterRemainingOverlays(openKeyNav, e) { // Filter overlays, removing non-matching ones document.querySelectorAll('.openKeyNav-label').forEach(function (overlay) { var label = overlay.textContent; // If the current typedLabel no longer matches the beginning of this element's label, remove both the overlay and clean up the target element if (!label.startsWith(openKeyNav.config.typedLabel.value)) { var targetElement = document.querySelector("[data-openkeynav-label=\"".concat(label, "\"]")); targetElement && targetElement.removeAttribute('data-openkeynav-label'); // Clean up the target element's attribute overlay.remove(); // Remove the overlay } }); if (document.querySelectorAll('.openKeyNav-label').length == 0) { // there are no overlays left. clean up and unblock. (0, _escape.handleEscape)(openKeyNav, e); return true; } }; var generateValidKeyChars = exports.generateValidKeyChars = function generateValidKeyChars(openKeyNav) { var chars = 'abcdefghijklmnopqrstuvwxyz'; // let chars = '1234567890'; // let chars = 'abcdefghijklmnopqrstuvwxyz1234567890'; // not a good idea because 1 and l can be confused // Remove letters from chars that are present in openKeyNav.config.keys // maybe this isn't necessary when in click mode (mode paradigm is common in screen readers) // Object.values(openKeyNav.config.keys).forEach(key => { // chars = chars.replace(key, ''); // }); // remove the secondary escape key code chars = chars.replace(openKeyNav.config.keys.escape, ''); return chars; }; var _getAllCandidateElements = function getAllCandidateElements(openKeyNav, doc) { var allElements = Array.from(doc.querySelectorAll("a," + // can be made non-tabbable by removing the href attribute or setting tabindex="-1". "button:not([disabled])," + // are not tabbable when disabled. "textarea:not([disabled])," + // are not tabbable when disabled. "select:not([disabled])," + // are not tabbable when disabled. "input:not([disabled])," + // are not tabbable when disabled. // "label," + // are not normally tabbable unless they contain tabbable content. "iframe," + // are tabbable by default. "details > summary," + // The summary element inside a details element can be tabbable "[role=button]," + // can be made non-tabbable by adding tabindex="-1". "[role=link]," + // can be made non-tabbable by adding tabindex="-1". "[role=menuitem]," + // can be made non-tabbable by adding tabindex="-1". "[role=option]," + // can be made non-tabbable by adding tabindex="-1". "[role=tab]," + // can be made non-tabbable by adding tabindex="-1". "[role=treeitem]," + // can be made non-tabbable by adding tabindex="-1". "[role=checkbox]," + // can be made non-tabbable by adding tabindex="-1". "[role=radio]," + // can be made non-tabbable by adding tabindex="-1". "[aria-checked]," + // not inherently tabbable or non-tabbable. "[contenteditable=true]," + // elements with contenteditable="true" are tabbable. "[contenteditable=plaintext-only]," + // elements with contenteditable="plaintext-only" are tabbable. "[tabindex]," + // elements with a tabindex attribute can be made tabbable or non-tabbable depending on the value of tabindex. "[onclick]" // elements with an onclick attribute are not inherently tabbable or non-tabbable. )); var iframes = doc.querySelectorAll('iframe'); iframes.forEach(function (iframe) { try { var iframeDoc = iframe.contentDocument || iframe.contentWindow.document; var iframeElements = _getAllCandidateElements(openKeyNav, iframeDoc); allElements = allElements.concat(Array.from(iframeElements)); // Add elements from each iframe } catch (error) { console.log('Access denied to iframe content:', error); } }); // Merge with clickEventElements var mergedSet = new Set([].concat(_toConsumableArray(allElements), _toConsumableArray(openKeyNav.config.modesConfig.click.clickEventElements))); return Array.from(mergedSet); // return allElements; };