openkeynav
Version:
OpenKeyNav: A JavaScript plugin for enhancing keyboard navigation and accessibility on web pages.
1,261 lines (1,162 loc) • 141 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.OpenKeyNav = factory());
})(this, (function () { 'use strict';
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
var OpenKeyNav$1 = {};
var version = {};
Object.defineProperty(version, "__esModule", {
value: true
});
version.version = void 0;
version.version = "0.1.229";
var signals = {};
Object.defineProperty(signals, "__esModule", {
value: true
});
signals.derived = derived;
signals.effect = effect;
signals.signal = signal;
var subscriber = null;
function signal(value) {
var subscriptions = new Set();
return {
get value() {
if (subscriber) {
subscriptions.add(subscriber);
}
return value;
},
set value(updated) {
value = updated;
subscriptions.forEach(function (fn) {
return fn();
});
}
};
}
function effect(fn) {
subscriber = fn;
fn();
subscriber = null;
}
function derived(fn) {
var derived = signal();
effect(function () {
derived.value = fn();
});
return derived;
}
var toolbar = {};
var keyButton = {};
Object.defineProperty(keyButton, "__esModule", {
value: true
});
keyButton.keyButton = void 0;
keyButton.keyButton = function keyButton(keyCodes, text, reverseOrder) {
// let styledKeyCode = `<span class="keyButton">${keyCode}</span>`;
var styledKeyCodes = keyCodes.map(function (keyCode) {
return "<span class=\"keyButton\">".concat(keyCode, "</span>");
}).join("");
if (!text) {
return "".concat(styledKeyCodes);
}
if (reverseOrder) {
return "\n <span class=\"keyButtonContainer\"> \n <span>\n ".concat(styledKeyCodes, "\n </span>\n <span class=\"keyButtonLabel\">").concat(text, "</span> \n </span>\n ");
}
return "\n <span class=\"keyButtonContainer\"> \n <span class=\"keyButtonLabel\">".concat(text, "</span> \n <span>\n ").concat(styledKeyCodes, "\n </span>\n </span>\n ");
};
var keypress = {};
var clicking = {};
var hasRequiredClicking;
function requireClicking() {
if (hasRequiredClicking) return clicking;
hasRequiredClicking = 1;
Object.defineProperty(clicking, "__esModule", {
value: true
});
clicking.placeCursorAndScrollToCursor = clicking.handleTargetClickInteraction = void 0;
_interopRequireDefault(requireOpenKeyNav());
function _interopRequireDefault(e) {
return e && e.__esModule ? e : {
default: e
};
}
clicking.handleTargetClickInteraction = function handleTargetClickInteraction(openKeyNav, target, e) {
var doc = target.ownerDocument;
var win = doc.defaultView || doc.parentWindow;
var target_tagName = target.tagName.toLowerCase();
if (target_tagName === 'input' || target_tagName === 'textarea' || target.contentEditable === 'true' || target.contentEditable === 'plaintext-only' || target.hasAttribute('tabindex') && target.tabIndex > -1) {
placeCursorAndScrollToCursor(openKeyNav, target);
} else {
if (e.shiftKey && target.tagName.toLowerCase() === 'a' && target.href) {
win.open(target.href, '_blank');
} else {
openKeyNav.focus(target); // Ensure the target element is focused before dispatching the click event
if (!openKeyNav.config.modesConfig.click.modifier) {
var clickEvent = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: win
});
target.dispatchEvent(clickEvent);
}
}
}
openKeyNav.removeOverlays();
openKeyNav.clearMoveAttributes();
};
var placeCursorAndScrollToCursor = clicking.placeCursorAndScrollToCursor = function placeCursorAndScrollToCursor(openKeyNav, target) {
var targetTagName = target.tagName.toLowerCase();
setTimeout(function () {
openKeyNav.focus(target);
if (targetTagName === 'input' && ['text', 'search', 'url', 'tel', 'email', 'password'].indexOf(target.type) > -1 || targetTagName === 'textarea') {
// Move the cursor to the end for input and textarea elements
var valueLength = target.value.length;
target.selectionStart = valueLength;
target.selectionEnd = valueLength;
// Scroll the element itself into view if it's not fully visible
target.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest'
});
} else if (target.contentEditable === 'true' || target.contentEditable === 'plaintext-only') {
// Move the caret to the end for contenteditable elements
var range = document.createRange();
var sel = window.getSelection();
range.selectNodeContents(target);
range.collapse(false); // false to move to the end
sel.removeAllRanges();
sel.addRange(range);
// Attempt to ensure the caret is visible, considering the element might be larger than the viewport
var rect = range.getBoundingClientRect();
if (rect.bottom > window.innerHeight || rect.top < 0) {
target.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest'
});
}
}
// For elements with tabIndex > -1, focusing them should scroll them into view,
// but additional logic might be needed based on specific requirements.
}, 0);
};
return clicking;
}
var dragAndDrop = {};
Object.defineProperty(dragAndDrop, "__esModule", {
value: true
});
dragAndDrop.simulateDragAndDrop = dragAndDrop.endDrag = dragAndDrop.beginDrag = void 0;
function _createForOfIteratorHelper(r, e) {
var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
if (!t) {
if (Array.isArray(r) || (t = _unsupportedIterableToArray$1(r)) || e) {
t && (r = t);
var _n = 0,
F = function F() {};
return {
s: F,
n: function n() {
return _n >= r.length ? {
done: !0
} : {
done: !1,
value: r[_n++]
};
},
e: function e(r) {
throw r;
},
f: F
};
}
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
var o,
a = !0,
u = !1;
return {
s: function s() {
t = t.call(r);
},
n: function n() {
var r = t.next();
return a = r.done, r;
},
e: function e(r) {
u = !0, o = r;
},
f: function f() {
try {
a || null == t.return || t.return();
} finally {
if (u) throw o;
}
}
};
}
function _unsupportedIterableToArray$1(r, a) {
if (r) {
if ("string" == typeof r) return _arrayLikeToArray$1(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$1(r, a) : void 0;
}
}
function _arrayLikeToArray$1(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;
}
dragAndDrop.simulateDragAndDrop = function simulateDragAndDrop(openKeyNav, sourceElement, targetElement) {
var handleStickyMove = function handleStickyMove() {
function findMatchingElementByHTML(htmlString) {
function removeComments(htmlString) {
return htmlString.replace(/<!--[\s\S]*?-->/g, '');
}
// Remove comments from the HTML string
var cleanedHTMLString = removeComments(htmlString);
// Get all elements in the document
var allElements = document.querySelectorAll('*');
var _iterator = _createForOfIteratorHelper(allElements),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var element = _step.value;
// Remove comments from the element's HTML
var cleanedElementHTML = removeComments(element.innerHTML);
// Compare the cleaned HTML of each element with the cleaned HTML string
if (cleanedElementHTML === cleanedHTMLString) {
return element;
}
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
return null;
}
if (!openKeyNav.config.modesConfig.move.modifier) {
return false;
}
openKeyNav.config.typedLabel.value = '';
openKeyNav.config.modesConfig.move.selectedDropZone = false;
// if the selected element (openKeyNav.config.modesConfig.move.selectedMoveable) is no longer in the DOM,
// try to find an element in the DOM with matching HTML
// that complies with the move inclusion criteria
// and doesn't go against the exclusion criteria
// set that element as the selected element
// and then move the openKeyNav-label-selected label to it
if (!document.contains(openKeyNav.config.modesConfig.move.selectedMoveable)) {
var matchingElement = findMatchingElementByHTML(openKeyNav.config.modesConfig.move.selectedMoveableHTML);
var selectedConfig = openKeyNav.config.modesConfig.move.config[openKeyNav.config.modesConfig.move.selectedConfig];
var passesInclusionCriteria = matchingElement && matchingElement.matches(selectedConfig.fromElements) || matchingElement.matches(selectedConfig.fromContainer + ' > *');
var passesExclusionCriteria = matchingElement && !matchingElement.matches(selectedConfig.fromExclude);
if (passesInclusionCriteria && passesExclusionCriteria) {
console.log('Matching element found:', matchingElement);
openKeyNav.config.modesConfig.move.selectedMoveable = matchingElement;
openKeyNav.config.modesConfig.move.selectedMoveableHTML = matchingElement.innerHTML;
openKeyNav.updateOverlayPosition(matchingElement, openKeyNav.config.modesConfig.move.selectedLabel);
beginDrag(openKeyNav);
} else {
console.log('No matching element found.');
openKeyNav.removeOverlays(true);
openKeyNav.clearMoveAttributes();
}
}
};
endDrag(openKeyNav, targetElement);
handleStickyMove();
// // Sequence the event dispatches with delays
// (() => { return new Promise((resolve) => {resolve()})})()
// .then(() => dispatchEvent(sourceElement, mouseDownEvent, 1))
// .then(() => dispatchEvent(sourceElement, dragStartEvent, 1))
// .then(() => dispatchEvent(targetElement, dragEnterEvent, 1))
// .then(() => dispatchEvent(targetElement, dragOverEvent, 1))
// .then(() => dispatchEvent(targetElement, dropEvent, 1))
// .then(() => dispatchEvent(sourceElement, dragEndEvent, 1))
// .then(() => dispatchEvent(targetElement, mouseUpEvent, 1))
// .then(() => handleStickyMove());
};
var endDrag = dragAndDrop.endDrag = function endDrag(openKeyNav, targetElement) {
var dataTransfer = new DataTransfer(); // Create a DataTransfer object to carry the drag data.
var clientX = 0;
var clientY = 0;
var sourceElement = openKeyNav.config.modesConfig.move.selectedMoveable;
if (typeof TouchEvent === 'undefined') {
openKeyNav.setupTouchEvent();
}
if (!sourceElement) {
sourceElement = document.body;
}
if (!targetElement) {
targetElement = document.body;
}
var rectTarget = targetElement.getBoundingClientRect();
if (targetElement != document) {
clientX = rectTarget.left + rectTarget.width / 2;
clientY = rectTarget.top + rectTarget.height / 2;
}
// Create mousemove event to simulate dragging
var mouseMoveEvent = new MouseEvent('mousemove', {
bubbles: true,
cancelable: true,
clientX: clientX,
clientY: clientY
});
// Create touchmove event to simulate dragging
var touchMoveEvent = new TouchEvent('touchmove', {
bubbles: true,
cancelable: true,
touches: [new Touch({
identifier: Date.now(),
target: targetElement,
clientX: clientX,
clientY: clientY
})]
});
// Create dragenter event
var dragEnterEvent = new DragEvent('dragenter', {
bubbles: true,
cancelable: true,
clientX: clientX,
clientY: clientY,
dataTransfer: dataTransfer
});
Object.defineProperty(dragEnterEvent, 'dataTransfer', {
value: dataTransfer
});
// Create dragover event
var dragOverEvent = new DragEvent('dragover', {
bubbles: true,
cancelable: true,
clientX: clientX,
clientY: clientY,
dataTransfer: dataTransfer
});
Object.defineProperty(dragOverEvent, 'dataTransfer', {
value: dataTransfer
});
// Create drop event
var dropEvent = new DragEvent('drop', {
bubbles: true,
cancelable: true,
clientX: clientX,
clientY: clientY,
dataTransfer: dataTransfer
});
Object.defineProperty(dropEvent, 'dataTransfer', {
value: dataTransfer
});
// Create dragend event
var dragEndEvent = new DragEvent('dragend', {
bubbles: true,
cancelable: true,
clientX: clientX,
clientY: clientY,
dataTransfer: dataTransfer
});
Object.defineProperty(dragEndEvent, 'dataTransfer', {
value: dataTransfer
});
// Create mouseup event to drop
var mouseUpEvent = new MouseEvent('mouseup', {
bubbles: true,
cancelable: true,
clientX: clientX,
clientY: clientY
});
// Create touchend event to drop
var touchEndEvent = new TouchEvent('touchend', {
bubbles: true,
cancelable: true,
changedTouches: [new Touch({
identifier: Date.now(),
target: targetElement,
clientX: clientX,
clientY: clientY
})]
});
// Dispatch the events
try {
document.dispatchEvent(mouseMoveEvent);
} catch (error) {
console.log(error);
}
try {
document.dispatchEvent(touchMoveEvent);
} catch (error) {
console.log(error);
}
try {
targetElement.dispatchEvent(dragEnterEvent);
} catch (error) {
console.log(error);
}
try {
targetElement.dispatchEvent(dragOverEvent);
} catch (error) {
console.log(error);
}
try {
if (targetElement != document) {
targetElement.dispatchEvent(dropEvent);
}
} catch (error) {
console.log(error);
}
try {
sourceElement.dispatchEvent(dragEndEvent);
} catch (error) {
console.log(error);
}
targetElement.dispatchEvent(mouseUpEvent);
targetElement.dispatchEvent(touchEndEvent);
};
var beginDrag = dragAndDrop.beginDrag = function beginDrag(openKeyNav) {
var sourceElement = openKeyNav.config.modesConfig.move.selectedMoveable;
var rectSource = sourceElement.getBoundingClientRect();
var dataTransfer = new DataTransfer(); // Create a DataTransfer object to carry the drag data.
if (typeof TouchEvent === 'undefined') {
openKeyNav.setupTouchEvent();
}
// Create and dispatch mousedown event
var mouseDownEvent = new MouseEvent('mousedown', {
bubbles: true,
cancelable: true,
clientX: rectSource.left + rectSource.width / 2,
clientY: rectSource.top + rectSource.height / 2
});
sourceElement.dispatchEvent(mouseDownEvent);
// Create and dispatch touchstart event (if needed)
var touchStartEvent = new TouchEvent('touchstart', {
bubbles: true,
cancelable: true,
touches: [new Touch({
identifier: Date.now(),
target: sourceElement,
clientX: rectSource.left + rectSource.width / 2,
clientY: rectSource.top + rectSource.height / 2
})]
});
sourceElement.dispatchEvent(touchStartEvent);
// Simulate mouse movement to trigger Dragula's drag start logic
var mouseMoveEvent = new MouseEvent('mousemove', {
bubbles: true,
cancelable: true,
clientX: rectSource.left + rectSource.width / 2 + 10,
// Move mouse 10 pixels to the right
clientY: rectSource.top + rectSource.height / 2 + 10 // Move mouse 10 pixels down
});
document.dispatchEvent(mouseMoveEvent);
// Create and dispatch dragstart event
var dragStartEvent = new DragEvent('dragstart', {
bubbles: true,
cancelable: true,
clientX: rectSource.left + rectSource.width / 2 + 10,
clientY: rectSource.top + rectSource.height / 2 + 10,
dataTransfer: dataTransfer
});
// Use Object.defineProperty to attach the dataTransfer object to the event.
Object.defineProperty(dragStartEvent, 'dataTransfer', {
value: dataTransfer
});
sourceElement.dispatchEvent(dragStartEvent);
};
var _escape$1 = {};
Object.defineProperty(_escape$1, "__esModule", {
value: true
});
_escape$1.handleEscape = void 0;
var _dragAndDrop = dragAndDrop;
_escape$1.handleEscape = function handleEscape(openKeyNav, e) {
var returnFalse = false;
if (openKeyNav.config.modes.clicking.value || openKeyNav.config.modes.moving.value || openKeyNav.config.modes.menu.value) {
e.preventDefault();
e.stopPropagation();
(0, _dragAndDrop.endDrag)(openKeyNav);
openKeyNav.removeOverlays();
openKeyNav.clearMoveAttributes();
returnFalse = true;
}
if (openKeyNav.isTextInputActive()) {
document.activeElement.blur(); // Removes focus from the active text input
}
if (returnFalse) {
return false;
} else {
if (document.activeElement != document.body) {
document.activeElement.blur();
}
}
};
var focus = {};
Object.defineProperty(focus, "__esModule", {
value: true
});
focus.focusOnScrollables = focus.focusOnHeadings = void 0;
focus.focusOnHeadings = function focusOnHeadings(openKeyNav, headings, e) {
openKeyNav.config.headings.list = Array.from(document.querySelectorAll(headings)) // Get all headings in the view
.filter(function (el) {
// Skip if the element is visually hidden
var style = getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden') return false;
// debug mode: debug mode: do isAnyCornerVisible check by default and disable the check if debug.screenReaderVisible is true
if (!openKeyNav.config.debug.screenReaderVisible) {
// Skip if the element's top left corner is covered by another element
if (!openKeyNav.isAnyCornerVisible(el)) {
return false;
}
}
return true;
});
if (openKeyNav.config.headings.list.length == 0) {
return true;
}
// handle moving to the next / previous heading
if (e.shiftKey) {
// shift key is pressed, so move backwards. If at the beginning, go to the end.
if (openKeyNav.config.headings.currentHeadingIndex > 0) {
openKeyNav.config.headings.currentHeadingIndex--;
} else {
openKeyNav.config.headings.currentHeadingIndex = openKeyNav.config.headings.list.length - 1;
}
} else {
// Move to the next heading. If at the end, go to the beginning.
if (openKeyNav.config.headings.currentHeadingIndex < openKeyNav.config.headings.list.length - 1) {
openKeyNav.config.headings.currentHeadingIndex++;
} else {
openKeyNav.config.headings.currentHeadingIndex = 0;
}
}
var nextHeading = openKeyNav.config.headings.list[openKeyNav.config.headings.currentHeadingIndex];
if (!nextHeading.hasAttribute('tabindex')) {
nextHeading.setAttribute('tabindex', '-1'); // Make the heading focusable
nextHeading.setAttribute('data-openkeynav-tabIndexed', true);
}
openKeyNav.focus(nextHeading); // Set focus on the next heading
// Listen for the blur event to remove the tabindex attribute
nextHeading.addEventListener('blur', function handler() {
if (nextHeading.hasAttribute('data-openkeynav-tabIndexed')) {
nextHeading.removeAttribute('tabindex'); // Remove the tabindex attribute
nextHeading.removeAttribute('data-openkeynav-tabIndexed');
}
nextHeading.removeEventListener('blur', handler); // Clean up the event listener
});
};
focus.focusOnScrollables = function focusOnScrollables(openKeyNav, e) {
openKeyNav.config.scrollables.list = openKeyNav.getScrollableElements(); // Populate or refresh the list of scrollable elements
if (openKeyNav.config.scrollables.list.length == 0) {
return; // If no scrollable elements, exit the function
}
// /*
{
// Navigate through scrollable elements
if (e.shiftKey) {
// Move backwards
openKeyNav.config.currentScrollableIndex = openKeyNav.config.currentScrollableIndex > 0 ? openKeyNav.config.currentScrollableIndex - 1 : openKeyNav.config.scrollables.list.length - 1;
} else {
// Move forwards
openKeyNav.config.currentScrollableIndex = openKeyNav.config.currentScrollableIndex < openKeyNav.config.scrollables.list.length - 1 ? openKeyNav.config.currentScrollableIndex + 1 : 0;
}
}
//*/
// Focus the current scrollable element
var currentScrollable = openKeyNav.config.scrollables.list[openKeyNav.config.currentScrollableIndex];
if (!currentScrollable.hasAttribute('tabindex')) {
currentScrollable.setAttribute('tabindex', '-1'); // Make the element focusable
currentScrollable.setAttribute('data-openkeynav-tabIndexed', true);
}
openKeyNav.focus(currentScrollable); // Set focus on the element
// Clean up: remove tabindex and blur listener when focus is lost
currentScrollable.addEventListener('blur', function handler() {
if (currentScrollable.hasAttribute('data-openkeynav-tabIndexed')) {
currentScrollable.removeAttribute('tabindex'); // Remove the tabindex attribute
currentScrollable.removeAttribute('data-openkeynav-tabIndexed');
}
currentScrollable.removeEventListener('blur', handler);
});
};
var isTabbable = {};
Object.defineProperty(isTabbable, "__esModule", {
value: true
});
isTabbable.isTabbable = void 0;
var isHiddenByOverflow = function isHiddenByOverflow(element) {
var parent = element.parentNode;
// Use the ownerDocument to get the correct document context
var doc = element.ownerDocument;
var body = doc.body;
while (parent && parent !== body) {
// Use the specific document body of the element
// if (parent instanceof HTMLElement) {
var parentStyle = getComputedStyle(parent);
if (['scroll', 'auto'].includes(parentStyle.overflow) || ['scroll', 'auto'].includes(parentStyle.overflowX) || ['scroll', 'auto'].includes(parentStyle.overflowY)) {
var parentRect = parent.getBoundingClientRect();
var rect = element.getBoundingClientRect();
if (rect.bottom < parentRect.top || rect.top > parentRect.bottom || rect.right < parentRect.left || rect.left > parentRect.right) {
return true; // Element is hidden by parent's overflow
}
}
// }
parent = parent.parentNode;
}
return false; // No parent hides the element by overflow
};
var inViewport = function inViewport(el) {
// check if the element's top left corner is within the window's viewport
var rect = el.getBoundingClientRect();
var isInViewport = rect.top < window.innerHeight && rect.left < window.innerWidth && rect.bottom > 0 && rect.right > 0;
return isInViewport;
};
isTabbable.isTabbable = function isTabbable(el, openKeyNav) {
var clickableElements = ['a', 'button', 'textarea', 'select', 'input', 'iframe', 'summary', '[onclick]'];
var interactiveRoles = ['button', 'link', 'menuitem', 'option', 'tab', 'treeitem', 'checkbox', 'radio'];
var isTypicallyClickableElement = function isTypicallyClickableElement(el) {
// Check if the element is a known clickable element
if (el.matches(clickableElements.join())) {
return true;
}
// Check if the element has an interactive ARIA role
var role = el.getAttribute('role');
if (role && interactiveRoles.includes(role)) {
return true;
}
return false;
};
// Ensure el is an Element before accessing styles
if (!(el instanceof Element)) {
// console.log(`!(el instanceof Element)`, el); //debug
return false;
}
// Skip if the element is set to not display (not the same as having zero size)
var style = getComputedStyle(el);
if (style.display === 'none') {
// console.log(`style.display === 'none'`, el); //debug
return false;
}
// Skip if the element is hidden by a parent's overflow
if (isHiddenByOverflow(el)) {
// console.log(`isHiddenByOverflow(el)`, el); //debug
return false;
}
// Skip if the element is within a <details> that is not open, but allow if it's a <summary> or a clickable element inside a <summary>
// aka it's hidden by the collapsed detail
if (el.matches('details:not([open]) *') && !el.matches('details:not([open]) > summary, details:not([open]) > summary *')) {
// console.log(`hidden details element`, el); //debug
return false;
}
// always include if tabindex > -1
// include this after checking if the element is hidden by a parent's overflow, which most screen readers respect
// (elements should not be tabbable by keyboard if they are visibly hidden,
// so include visibly hidden items that are explicitly tabbable to help with accessibility bug discovery)
// do not move this earlier in the heuristic
var tabIndex = el.getAttribute('tabindex');
if (tabIndex && parseInt(tabIndex, 10) > -1) {
// console.log(`tabindex > -1`, el); //debug
return true;
}
// Skip if the element is visually hidden (not the same as having zero size or set to not display)
if (style.visibility === 'hidden') {
// console.log(`style.visibility === 'hidden'`, el); //debug
return false;
}
// console.log("isTabbable() -> openKeyNav", openKeyNav);
// Skip if the element has no size (another way to visually hide something)
if (!openKeyNav.isNonzeroSize(el)) {
// console.log(`!openKeyNav.isNonzeroSize(el)`, el); //debug
return false;
}
// Skip if the element's top left corner is not within the window's viewport
if (!inViewport(el)) {
// console.log(`!inViewport(el)`, el); //debug
return false;
}
// do isAnyCornerVisible check by default and disable the check if debug.screenReaderVisible is true
if (!openKeyNav.config.debug.screenReaderVisible) {
// Skip if the element's top left corner is covered by another element
if (!openKeyNav.isAnyCornerVisible(el)) {
// console.log(`!openKeyNav.isAnyCornerVisible(el)`, el); //debug
return false;
}
}
// Skip if <summary> is not the first <summary> element of a <details>
if (el.tagName.toLowerCase() === 'summary') {
var details = el.parentElement;
if (details && details.tagName.toLowerCase() === 'details' && details.querySelector('summary') !== el) {
// console.log(`<summary> is not the first <summary> element of a <details>`, el); //debug
return false;
}
}
// lastly, elements that are inaccessible due to not being tabbable
if (tabIndex && parseInt(tabIndex, 10) == -1) {
if (isTypicallyClickableElement(el)) {
// if (openKeyNav.config.modes.clicking.value) {
openKeyNav.flagAsInaccessible(el, "\n <h2>Inaccessible Element</h2>\n <h3>Problem: </h3>\n <p>This element is not keyboard-focusable.</p>\n <h3>Solution: </h3>\n <p>Since this element has a tabindex attribute set to -1, it cannot be keyboard focusable.</p>\n <p>It must have a tabindex set to a value > -1, ideally 0.</p>\n <p>You can ignore this warning if this element is not meant to be clickable.</p>\n ", "keyboard");
// }
}
// return false; // let's keep it, since we are flagging it
}
// Skip if the element is an <a> without an href (unless it has an ARIA role that makes it tabbable)
var role = el.getAttribute('role');
switch (el.tagName.toLowerCase()) {
case 'a':
// console.log(el); //debug
if (!el.hasAttribute('href') || el.getAttribute('href') === '') {
if (!interactiveRoles.includes(role)) {
// if (openKeyNav.config.modes.clicking.value) {
openKeyNav.flagAsInaccessible(el, "\n <h2>Inaccessible Button</h2>\n <h3>Problem: </h3>\n <p>This clickable button is not keyboard-focusable.</p>\n <p>As a result, only mouse users can click on it.</p>\n <p>This usability disparity can create an accessibility barrier.</p>\n <h3>Solution: </h3>\n <p>Since it is an anchor tag (<a>), it needs a non-empty <em>href</em> attribute.</p>\n <p>Alternatively, it needs an ARIA <em>role</em> attribute set to something like 'button' or 'link' AND a tabindex attribute set to a value > -1, ideally 0.</p>\n ", "keyboard");
// return false;
// }
}
}
break;
case 'button':
case 'textarea':
case 'select':
case 'input':
case 'iframe':
case 'summary':
break;
default:
if (!!role && !interactiveRoles.includes(role)) {
if (openKeyNav.config.modesConfig.click.clickEventElements.has(el)) {
openKeyNav.flagAsInaccessible(el, "\n <!--\n !el(a,button,textarea,select,input,iframe,summary)\n !el[role('button', 'link', 'menuitem', 'option', 'tab', 'treeitem', 'checkbox', 'radio')]\n fromClickEvents\n -->\n <h2>Possibly Inaccessible Clickable Element</h2>\n <h3>Problem: </h3>\n <p>This element has a mouse click event handler attached to it, but it is not keyboard-focusable.</p>\n <p>As a result, only mouse users can click on it.</p>\n <p>This usability disparity can create an accessibility barrier.</p>\n <h3>Solution Options: </h3>\n <ol>\n <li>\n <p>If clicking this element takes the user to a different location, convert this element to an anchor link (<a>) with a non-empty <em>href</em> attribute.</p>\n </li>\n <li>\n <p>Otherwise if clicking this element triggers an action on the page, convert this element to a <button> without a <em>disabled</em> attribute.</p>\n <p>Alternatively, it needs an ARIA <em>role</em> attribute set to something like 'button' or 'link' AND a tabindex attribute set to a value > -1, ideally 0.</p>\n </li>\n <li>\n <p>Otherwise, if clicking this element does not do anything, then consider removing the click event handler attached to this element.</p>\n </li>\n </ol>\n ", "keyboard");
}
// return false;
// }
}
break;
}
// it must be a valid tabbable element
return true;
};
var keylabels = {};
var scrolling = {};
Object.defineProperty(scrolling, "__esModule", {
value: true
});
scrolling.disableScrolling = void 0;
scrolling.disableScrolling = function disableScrolling(openKeyNav) {
// Prevent scrolling on the webpage
var disableScrollingForEl = function disableScrollingForEl(el) {
el.addEventListener('scroll', openKeyNav.preventScroll, {
passive: false
});
el.addEventListener('wheel', openKeyNav.preventScroll, {
passive: false
});
el.addEventListener('touchmove', openKeyNav.preventScroll, {
passive: false
});
};
var disableScrollingForScrollableElements = function disableScrollingForScrollableElements() {
disableScrollingForEl(window);
openKeyNav.getScrollableElements().forEach(function (el) {
disableScrollingForEl(el);
});
};
disableScrollingForScrollableElements();
};
Object.defineProperty(keylabels, "__esModule", {
value: true
});
keylabels.showMoveableFromOverlays = keylabels.showClickableOverlays = keylabels.generateValidKeyChars = keylabels.generateLabels = keylabels.filterRemainingOverlays = void 0;
var _escape = _escape$1;
var _isTabbable = isTabbable;
var _scrolling = 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 = keylabels.generateLabels = function generateLabels(openKeyNav, count) {
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
};
keylabels.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
};
keylabels.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');
});
};
keylabels.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 = keylabels.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;
};
var lifecycle = {};
Object.defineProperty(lifecycle, "__esModule", {
value: true
});
lifecycle.enable = lifecycle.disable = void 0;
lifecycle.enable = function enable() {};
lifecycle.disable = function disable() {};
var hasRequiredKeypress;
function requireKeypress() {
if (hasRequiredKeypress) return keypress;
hasRequiredKeypress = 1;
Object.defineProperty(keypress, "__esModule", {
value: true
});
keypress.modiferKeyString = keypress.handleKeyPress = void 0;
var _clicking = requireClicking();
var _dragAndDrop = dragAndDrop;
var _escape = _escape$1;
var _focus = focus;
var _isTabbable = isTabbable;
var _keylabels = keylabels;
var _keyButton = keyButton;
function getMetaKeyName() {
var userAgent = window.navigator.userAgent.toLowerCase();
if (userAgent.indexOf('mac') >= 0) return 'Cmd';
if (userAgent.indexOf('win') >= 0) return 'Win';
if (userAgent.indexOf('linux') >= 0) return 'Super';
// fallback
return 'Meta';
}
var modiferKeyString = keypress.modiferKeyString = function modiferKeyString(openKeyNav) {
switch (openKeyNav.config.keys.modifierKey) {
case 'shiftKey':
return 'Shift';
case 'altKey':
return 'Alt';
case 'metaKey':
return getMetaKeyName();
default:
return openKeyNav.config.keys.modifierKey;
}
};
keypress.handleKeyPress = function handleKeyPress(openKeyNav, e) {
var isTextInputActive = openKeyNav.isTextInputActive();
// enable / disable openKeyNav
if (e[openKeyNav.config.keys.modifierKey] && openKeyNav.config.keys.menu.toLowerCase() == e.key.toLowerCase()) {
if (isTextInputActive) {
if (!e[openKeyNav.config.keys.inputEscape]) {
return true;
}
}
if (!openKeyNav.meta.enabled.value) {
// if openKeyNav disabled
openKeyNav.enable();
var message = "openKeyNav enabled. Press ".concat((0, _keyButton.keyButton)([modiferKeyString(openKeyNav), openKeyNav.config.keys.menu]), " to disable.");
openKeyNav.emitNotification(message);
return true;
} else {
(0, _escape.handleEscape)(openKeyNav, e);
openKeyNav.disable();
var _message = "openKeyNav disabled. Press ".concat((0, _keyButton.keyButton)([modiferKeyString(openKeyNav), openKeyNav.config.keys.menu]), " to enable.");
openKeyNav.emitNotification(_message);
return true;
}
}
// first check for modifier keys and escape
switch (e.key) {
case 'Shift': // exit this event listener if it's the shift key press
case 'Control': // exit this event listener if it's the control key press
case 'Alt': // exit this event listener if it's the alt key press
case 'Meta': // exit this event listener if it's the meta key (Command/Windows) press
case ' ':
// exit this event listener if it's the space bar key press
// Prevent default action and stop the function
// e.preventDefault();
return true;
// handle escape first
case 'Escape':
// escaping
// alert("Escape");
(0, _escape.handleEscape)(openKeyNav, e);
break;
}
// check if currently in any openkeynav modes
if (openKeyNav.config.modes.clicking.value) {
return handleClickMode(openKeyNav, e);
}
if (openKeyNav.config.modes.moving.value) {
return handleMoveMode(openKeyNav, e);
}
if (openKeyNav.config.modes.menu.value) {
handleMenuMode();
}
if (isTextInputActive) {
if (!e[openKeyNav.config.keys.inputEscape]) {
return true;
}
}
if (!openKeyNav.meta.enabled.value) {
return true;
}
// escape and toggles
switch (e.key) {
case openKeyNav.config.keys.escape:
// escaping
// alert("Escape");
(0, _escape.handleEscape)(openKeyNav, e);
return true;
// case openKeyNav.config.keys.toggleCursor: // toggle Cursor
// // toggle class openKeyNav-noCursor for body
// document.body.classList.toggle('openKeyNav-noCursor');
// return true;
// break;
}
// modes
switch (e.key) {
case openKeyNav.config.keys.click: // possibly attempting to initiate click mode
case openKeyNav.config.keys.click.toUpperCase():
e.preventDefault();
openKeyNav.config.modes.clicking.value = true;
if (e.key == openKeyNav.config.keys.click.toUpperCase()) {
openKeyNav.config.modesConfig.click.modifier = true;
}
(0, _keylabels.showClickableOverlays)(openKeyNav);
return true;
// possibly attempting to initiate moving mode
case openKeyNav.config.keys.move:
case openKeyNav.config.keys.move.toUpperCase():
// Toggle move mode
e.preventDefault();
openKeyNav.config.modes.moving.value = true; // Assuming you add a 'move' flag to your modes object
if (e.key == openKeyNav.config.keys.move.toUpperCase()) {
openKeyNav.config.modesConfig.move.modifier = true;
}
(0, _keylabels.showMoveableFromOverlays)(openKeyNav); // This will be a new function similar to showClickableOverlays
return true;
case openKeyNav.config.keys.menu:
case openKeyNav.config.keys.menu.toUpperCase():
openKeyNav.config.modes.menu.value = true;
if (e.key == openKeyNav.config.keys.menu.toUpperCase()) {
openKeyNav.config.modesConfig.menu.modifier = true;
}
return true;
}
// focus / navigation (can be modified by shift, so always check for lowercase)
switch (e.key.toLowerCase()) {
// Check if the pressed key is for headings
case openKeyNav.config.keys.heading.toLowerCase():
/*
const OpenKeyNav = {
currentHeadingIndex: 0,
keys: {