UNPKG

openkeynav

Version:

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

1,151 lines (1,076 loc) 50.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _version = require("./version"); var _signals = require("./signals.js"); var _toolbar = require("./toolbar.js"); var _keyButton = require("./keyButton.js"); var _styles = require("./styles.js"); var _keypress = require("./keypress.js"); var _escape = require("./escape"); function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { 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(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 _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; } function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function _callSuper(t, o, e) { return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e)); } function _possibleConstructorReturn(t, e) { if (e && ("object" == _typeof(e) || "function" == typeof e)) return e; if (void 0 !== e) throw new TypeError("Derived constructors may only return object or undefined"); return _assertThisInitialized(t); } function _assertThisInitialized(e) { if (void 0 === e) throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); return e; } function _inherits(t, e) { if ("function" != typeof e && null !== e) throw new TypeError("Super expression must either be null or a function"); t.prototype = Object.create(e && e.prototype, { constructor: { value: t, writable: !0, configurable: !0 } }), Object.defineProperty(t, "prototype", { writable: !1 }), e && _setPrototypeOf(t, e); } function _wrapNativeSuper(t) { var r = "function" == typeof Map ? new Map() : void 0; return _wrapNativeSuper = function _wrapNativeSuper(t) { if (null === t || !_isNativeFunction(t)) return t; if ("function" != typeof t) throw new TypeError("Super expression must either be null or a function"); if (void 0 !== r) { if (r.has(t)) return r.get(t); r.set(t, Wrapper); } function Wrapper() { return _construct(t, arguments, _getPrototypeOf(this).constructor); } return Wrapper.prototype = Object.create(t.prototype, { constructor: { value: Wrapper, enumerable: !1, writable: !0, configurable: !0 } }), _setPrototypeOf(Wrapper, t); }, _wrapNativeSuper(t); } function _construct(t, e, r) { if (_isNativeReflectConstruct()) return Reflect.construct.apply(null, arguments); var o = [null]; o.push.apply(o, e); var p = new (t.bind.apply(t, o))(); return r && _setPrototypeOf(p, r.prototype), p; } function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); } function _isNativeFunction(t) { try { return -1 !== Function.toString.call(t).indexOf("[native code]"); } catch (n) { return "function" == typeof t; } } function _setPrototypeOf(t, e) { return _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function (t, e) { return t.__proto__ = e, t; }, _setPrototypeOf(t, e); } function _getPrototypeOf(t) { return _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function (t) { return t.__proto__ || Object.getPrototypeOf(t); }, _getPrototypeOf(t); } function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); } function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } } function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } /* OpenKeyNav.js Copyright Lawrence Weru / Aster Enterprises LLC 2014 - 2024. All rights reserved. */ /* Usage: NPM: // Import the unminified version (for development) import OpenKeyNav from 'openkeynav'; // Or import the minified version (for production) import OpenKeyNav from 'openkeynav/dist/openkeynav.min.js'; Importing from souce: import OpenKeyNav from '/path/to/openKeyNav'; # init: OpenKeyNav.init(); # then press g when you are not in a text input mode # to label the tab-accessible elements that have indicated they are buttons. # Press the key combinations on the labels to "click" their respective buttons # you can press h to navigate through headers within the viewport # You can also press or 1,2,3,4,5,6 to navigate through headers of the respective level OpenKeyNav.init({ spot : { backgroundColor : 'rgba(236, 255, 128, 1)', fontColor: 'black', outlineColor : 'rgb(134 148 53)', fontSize : '14px', }, focus : { outlineColor : '#0088cc', outlineStyle : 'solid' }, keys : { escape : 'q', // alternative escape key, for when escape key is too far or not available. // q works great because top left of letters, plus removes confusion with g, p click : 'k', // enter click mode, to click on clickable elements mouseOver : 'v', // toggle a mouseover event for an applicable element. In many cases this should trigger opening mouseover menus, etc // not yet wired move : 'm', // enter move mode, to move elements from and to, aka keyboard drag and drop // not yet fully wired scroll : 's', // focus on the next scrollable region heading : 'h', // focus on the next heading // as seen in JAWS, NVDA textBlock : 'n', // focus on the next block of text // as seen in JAWS, NVDA // not yet fully wired landmarkRegion : 'd', // focus on the next landmark region // as seen in NVDA // not yet fully wired formField : 'f', // move to the next form field // as seen in NVDA // not yet fully wired }, move: { // not yet fully wired, but would facilitate drag and drop config : [ { fromContainer: ".classContainerFrom1", fromElements: ".classElementFrom1", resolveFromElements: function(){ return NodeList }, // Optional callback to resolve fromElements // resolveToElements: function(){ return NodeList }, // Optional callback to resolve toElements // not yet wired fromExlude : ".excludeThisElement", toElements: '.classToA, .classToD, .classToE', callback : () => {} }, { fromContainer: ".classFrom2", toElements: ".classToB" }, { fromContainer: ".classFrom3", toElements: ".classToC" } ], selectedMoveable : false, selectedDropZone: false } }); */ var OpenKeyNav = /*#__PURE__*/function () { function OpenKeyNav() { var _this = this; _classCallCheck(this, OpenKeyNav); this.config = { spot: { fontColor: 'white', backgroundColor: '#333', insetColor: '#000', fontSize: 'inherit', arrowSize_px: 4 }, focus: { outlineColor: '#0088cc', outlineStyle: 'solid' }, toolBar: { height: 32, backgroundColor: (0, _signals.signal)('hsl(210 10% 95% / 1)'), contentColor: (0, _signals.signal)('#000') }, notifications: { enabled: true, displayToolName: true, duration: 3000 }, keys: { escape: 'q', // alternative escape key, for when escape key is too far or not available. // q works great because top left of letters, plus removes confusion with g, p click: 'k', // enter click mode, to click on clickable elements, such as links. Was g, now k, for kanga. Plus NVDA uses k to focus on link elements, which prevents conflicting modes as it's either openkeynav or NVDA. scroll: 's', // focus on the next scrollable region move: 'm', // enter move mode, to move elements from and to, aka keyboard drag and drop // not yet fully wired heading: 'h', // focus on the next heading // as seen in JAWS, NVDA textBlock: 'n', // focus on the next block of text // as seen in JAWS, NVDA // not yet fully wired landmarkRegion: 'd', // focus on the next landmark region // as seen in NVDA // not yet fully wired formField: 'f', // move to the next form field // as seen in NVDA // not yet fully wired mouseOver: 'v', // toggle a mouseover event for an applicable element. In many cases this should trigger opening mouseover menus, etc // not yet wired heading_1: '1', // focus on the next heading of level 1 // as seen in JAWS, NVDA // do not modify heading_2: '2', // focus on the next heading of level 2 // as seen in JAWS, NVDA // do not modify heading_3: '3', // focus on the next heading of level 3 // as seen in JAWS, NVDA // do not modify heading_4: '4', // focus on the next heading of level 4 // as seen in JAWS, NVDA // do not modify heading_5: '5', // focus on the next heading of level 5 // as seen in JAWS, NVDA // do not modify heading_6: '6', // focus on the next heading of level 6 // as seen in JAWS, NVDA // do not modify menu: 'o', inputEscape: 'ctrlKey', // for escaping input to trigger a command modifierKey: 'shiftKey' // one of: [altKey, shiftKey, metaKey] // useful for on/off switch. Avoid ctrlKey, which is used to escape input. }, modesConfig: { move: { // facilitates keyboard accessible drag and drop config: [ // { // fromContainer: ".classContainerFrom1", // fromElements: ".classElementFrom1", // resolveFromElements: function(){ return NodeList }, // Optional callback to resolve fromElements // // resolveToElements: function(){ return NodeList }, // Optional callback to resolve toElements // not yet wired // fromExlude : ".excludeThisElement", // toElements: '.classToA, .classToD, .classToE', callback : () => {} // }, // { fromContainer: ".classFrom2", toElements: ".classToB" }, // { fromContainer: ".classFrom3", toElements: ".classToC" } ], selectedConfig: false, selectedMoveable: false, selectedMoveableHTML: false, selectedDropZone: false, modifier: false }, click: { modifier: false, clickEventElements: new Set(), eventListenersMap: new Map() }, menu: { modifier: false } }, log: [], typedLabel: (0, _signals.signal)(''), headings: { currentHeadingIndex: 0, // Keep track of the current heading list: [] }, scrollables: { currentScrollableIndex: 0, // Keep track of the current scrollable list: [] }, modes: { clicking: (0, _signals.signal)(false), moving: (0, _signals.signal)(false), menu: (0, _signals.signal)(false) }, debug: { screenReaderVisible: false, keyboardAccessible: true }, enabledCookie: 'openKeyNav_enabled' }; this.meta = { enabled: (0, _signals.signal)(false) }; this.enable = function () { _this.meta.enabled.value = true; _this.injectStyles(); _this.getSetCookie(_this.config.enabledCookie, true); return _this; }; this.disable = function () { _this.meta.enabled.value = false; _this.getSetCookie(_this.config.enabledCookie, false); _this.removeStyles(); // maybe this should go in the destroy();, main concern is the toolbar. return _this; }; } return _createClass(OpenKeyNav, [{ key: "focus", value: function focus(target) { target.focus(); target.setAttribute('data-openkeynav-focused', true); target.addEventListener('blur', function handler() { target.removeAttribute('data-openkeynav-focused'); target.removeEventListener('blur', handler); // Clean up the event listener }); } // utility functions }, { key: "setupTouchEvent", value: function setupTouchEvent() { window.TouchEvent = /*#__PURE__*/function (_Event) { function TouchEvent(type, initDict) { var _this2; _classCallCheck(this, TouchEvent); _this2 = _callSuper(this, TouchEvent, [type, initDict]); _this2.touches = initDict.touches || []; _this2.targetTouches = initDict.targetTouches || []; _this2.changedTouches = initDict.changedTouches || []; _this2.altKey = initDict.altKey || false; _this2.metaKey = initDict.metaKey || false; _this2.ctrlKey = initDict.ctrlKey || false; _this2.shiftKey = initDict.shiftKey || false; return _this2; } _inherits(TouchEvent, _Event); return _createClass(TouchEvent); }( /*#__PURE__*/_wrapNativeSuper(Event)); window.Touch = /*#__PURE__*/_createClass(function _class(_ref) { var identifier = _ref.identifier, target = _ref.target, clientX = _ref.clientX, clientY = _ref.clientY; _classCallCheck(this, _class); this.identifier = identifier; this.target = target; this.clientX = clientX; this.clientY = clientY; this.screenX = clientX; this.screenY = clientY; this.pageX = clientX; this.pageY = clientY; }); } }, { key: "deepMerge", value: function deepMerge(target, source) { var _this3 = this; Object.keys(source).forEach(function (key) { if (source[key] && _typeof(source[key]) === 'object') { if (!target[key] || _typeof(target[key]) !== 'object') { target[key] = {}; } _this3.deepMerge(target[key], source[key]); } else { target[key] = source[key]; } }); return target; } }, { key: "injectStyles", value: function injectStyles(replace) { (0, _styles.injectStylesheet)(this, replace); } }, { key: "removeStyles", value: function removeStyles() { (0, _styles.deleteStylesheets)(); } }, { key: "initToolBar", value: function initToolBar() { (0, _toolbar.handleToolBar)(this); } }, { key: "isNonzeroSize", value: function isNonzeroSize(element) { var rect = element.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; } }, { key: "isTextInputActive", value: function isTextInputActive() { var tagName = document.activeElement.tagName.toLowerCase(); var editable = document.activeElement.getAttribute('contenteditable'); var inputTypes = ['input', 'textarea']; var isEditable = editable === 'true' || editable === 'plaintext-only' || editable === ''; return inputTypes.includes(tagName) || isEditable; } // avoids overlaps }, { key: "updateOverlayPosition", value: function updateOverlayPosition(element, overlay) { var elementsToAvoid = document.querySelectorAll('[data-openkeynav-label], .openKeyNav-label-selected, .openKeyNav-toolBar'); // maybe also add labeled elements to this list dynamically var rectAvoid = element.getBoundingClientRect(); var overlayWidth = overlay.getBoundingClientRect().width; var overlayHeight = overlay.getBoundingClientRect().height; var arrowWidth = this.config.spot.arrowSize_px; function isBoundingBoxIntersecting(rectOverlay, rectAvoid) { return !(rectOverlay.right <= rectAvoid.left || rectOverlay.left >= rectAvoid.right || rectOverlay.bottom <= rectAvoid.top || rectOverlay.top >= rectAvoid.bottom); } var isOverlapping = function isOverlapping(overlay, avoidEl) { var rectOverlay = overlay.getBoundingClientRect(); var rectAvoid = avoidEl.getBoundingClientRect(); var isOverlapping_OnTop = function isOverlapping_OnTop() { // et's check if they are right above each other in the view. // this ensures elements inside modals or other containers visually hiding avoidEls can still have adjacent labels. var padding = 0; //this.config.spot.arrowSize_px; var corners = [{ x: rectOverlay.left - padding, y: rectOverlay.top - padding }, // top left { x: rectOverlay.right + padding, y: rectOverlay.top - padding }, // top right { x: rectOverlay.left - padding, y: rectOverlay.bottom + padding }, // bottom left { x: rectOverlay.right + padding, y: rectOverlay.bottom + padding } // bottom right ]; // Hide the overlay element temporarily overlay.style.visibility = 'hidden'; var isOverlapping = corners.some(function (corner) { if (corner.x >= 0 && corner.x <= window.innerWidth && corner.y >= 0 && corner.y <= window.innerHeight) { var elementAtPoint = document.elementFromPoint(corner.x, corner.y); return avoidEl === elementAtPoint || avoidEl.contains(elementAtPoint) || elementAtPoint && (elementAtPoint.contains(element) || elementAtPoint.classList.contains("openKeyNav-ignore-overlap")); } return false; }); // Show the overlay element again overlay.style.visibility = 'visible'; return isOverlapping; }; return isBoundingBoxIntersecting(rectOverlay, rectAvoid) && isOverlapping_OnTop(); // return isBoundingBoxIntersecting(rectOverlay, rectAvoid); // return false }; function isCutOff(el) { var rect = el.getBoundingClientRect(); return rect.left < 0 || rect.right > window.innerWidth || rect.top < 0 || rect.bottom > window.innerHeight; } function checkOverlap(overlay) { return isCutOff(overlay) || Array.from(elementsToAvoid).some(function (avoidEl) { if (avoidEl === overlay || avoidEl === element) { return false; } // Check if the element is directly on top of the avoidEl var rectElement = element.getBoundingClientRect(); var rectAvoidEl = avoidEl.getBoundingClientRect(); var isElementOnTop = isBoundingBoxIntersecting(rectElement, rectAvoidEl); if (isElementOnTop) { return false; } return isOverlapping(overlay, avoidEl); }); } overlay.removeAttribute('data-openkeynav-position'); // Try placing overlay to the left of the element overlay.style.position = 'absolute'; overlay.style.left = "".concat(rectAvoid.left - (overlayWidth + arrowWidth) + window.scrollX, "px"); // Added scrollX adjustment overlay.style.top = "".concat(rectAvoid.top + window.scrollY, "px"); // Added scrollY adjustment var position = "left"; if (!checkOverlap(overlay)) { overlay.setAttribute('data-openkeynav-position', position); return; } // Try placing overlay to the right of the element overlay.style.left = "".concat(rectAvoid.right + arrowWidth - 2 + window.scrollX, "px"); // Added scrollX adjustment // overlay.style.top = `${rectAvoid.top + window.scrollY}px`; // same as above position = "right"; if (!checkOverlap(overlay)) { overlay.setAttribute('data-openkeynav-position', position); return; } // Try placing overlay above the element overlay.style.left = "".concat(rectAvoid.left + window.scrollX, "px"); // Added scrollX adjustment overlay.style.top = "".concat(rectAvoid.top - (overlayHeight + arrowWidth) + window.scrollY, "px"); // Added scrollY adjustment position = "top"; if (!checkOverlap(overlay)) { overlay.setAttribute('data-openkeynav-position', position); return; } // Try placing overlay below the element overlay.style.left = "".concat(rectAvoid.left + window.scrollX, "px"); // Added scrollX adjustment overlay.style.top = "".concat(rectAvoid.bottom + arrowWidth + window.scrollY, "px"); // Added scrollY adjustment position = "bottom"; if (!checkOverlap(overlay)) { overlay.setAttribute('data-openkeynav-position', position); return; } // If all placements result in overlaps or being cut off, place overlay on the element's top left position overlay.removeAttribute('data-openkeynav-position'); overlay.style.left = "".concat(rectAvoid.left + window.scrollX, "px"); // Added scrollX adjustment overlay.style.top = "".concat(rectAvoid.top + window.scrollY, "px"); // Added scrollY adjustment } }, { key: "updateOverlayPosition_bak", value: function updateOverlayPosition_bak(element, overlay) { // this one just places the overlay over the element on top left position var rect = element.getBoundingClientRect(); var adjustedLeft = rect.left; var adjustedTop = rect.top; // Check if the element is inside an iframe and adjust the position var parent = element.ownerDocument.defaultView.frameElement; while (parent) { var parentRect = parent.getBoundingClientRect(); adjustedLeft += parentRect.left; adjustedTop += parentRect.top; parent = parent.ownerDocument.defaultView.frameElement; } overlay.style.left = "".concat(adjustedLeft + window.scrollX, "px"); overlay.style.top = "".concat(adjustedTop + window.scrollY, "px"); } }, { key: "createOverlay", value: function createOverlay(element, label) { var _this4 = this; function getScrollParent(element) { var includeHidden = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; var style = getComputedStyle(element); var excludeStaticParent = style.position === 'absolute'; var overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/; if (style.position === 'fixed') return document.body; for (var parent = element; parent = parent.parentElement;) { style = getComputedStyle(parent); if (excludeStaticParent && style.position === 'static') { continue; } if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) return parent; } return document.body; } var overlay = document.createElement('div'); overlay.textContent = label; overlay.classList.add('openKeyNav-label'); overlay.setAttribute('data-openkeynav-label', label); // Add event listener to open the element in developer tools overlay.addEventListener('click', function () { try { // Attempt to use inspect inspect(element); } catch (error) { // Fallback if inspect is not available or fails console.log(element); // alert('Element logged to console. Manually inspect it using the developer tools.'); } }); document.body.appendChild(overlay); // element.setAttribute('data-openkeynav-label', label); // Initial position update this.updateOverlayPosition(element, overlay); // Find scrollable parent var scrollParent = getScrollParent(element); if (scrollParent) { scrollParent.addEventListener('scroll', function () { return _this4.updateOverlayPosition(element, overlay); }); } if (element.classList.contains('openKeyNav-inaccessible')) { overlay.classList.add('openKeyNav-inaccessible'); } ; return overlay; } }, { key: "isAnyCornerVisible", value: function isAnyCornerVisible(element) { var isElementInIframe = function isElementInIframe(element) { return element.ownerDocument !== window.document; }; var doc = element.ownerDocument; var win = doc.defaultView || doc.parentWindow; var rect = element.getBoundingClientRect(); // Coordinates for the four corners of the element var corners = [{ x: rect.left + 1, y: rect.top + 1 }, // top-left { x: rect.right - 1, y: rect.top + 1 }, // top-right { x: rect.left + 1, y: rect.bottom - 1 }, // bottom-left { x: rect.right - 1, y: rect.bottom - 1 } // bottom-right ]; if (isElementInIframe(element)) { var frameElement = win.frameElement; if (frameElement) { var frameRect = frameElement.getBoundingClientRect(); corners.forEach(function (corner) { corner.x += frameRect.left; corner.y += frameRect.top; }); // Adjust `doc` and `win` to the parent document/window that contains the iframe doc = frameElement.ownerDocument; win = doc.defaultView || doc.parentWindow; } } // Check if any of the corners are visible for (var _i = 0, _corners = corners; _i < _corners.length; _i++) { var corner = _corners[_i]; var elemAtPoint = doc.elementFromPoint(corner.x, corner.y); if (elemAtPoint === element || element.contains(elemAtPoint) || elemAtPoint && (elemAtPoint.contains(element) || elemAtPoint.classList.contains("openKeyNav-ignore-overlap"))) { return true; // At least one corner is visible } } return false; // None of the corners are visible } }, { key: "getScrollableElements", value: function getScrollableElements() { var _this5 = this; // Cross-browser way to get computed style var getComputedStyle = document.body && document.body.currentStyle ? function (elem) { return elem.currentStyle; } : function (elem) { return document.defaultView.getComputedStyle(elem, null); }; // Retrieve the actual value of a CSS property function getActualCss(elem, style) { return getComputedStyle(elem)[style]; } // Determine if the overflow style allows for scrolling function isOverflowScrollable(overflow) { return overflow === 'scroll' || overflow === 'auto' || overflow === 'overlay'; } // Check horizontal scrollability function isXScrollable(elem) { var overflowX = getActualCss(elem, 'overflow-x'); // Directly return true if overflowX is 'scroll', assuming you want to capture all elements with this setting if (overflowX === 'scroll') return true; return elem.offsetWidth < elem.scrollWidth && (overflowX === 'scroll' || overflowX === 'auto' || overflowX === 'overlay'); } // Check vertical scrollability function isYScrollable(elem) { var overflowY = getActualCss(elem, 'overflow-y'); // Directly return true if overflowY is 'scroll', assuming you want to capture all elements with this setting if (overflowY === 'scroll') return true; return elem.offsetHeight < elem.scrollHeight && (overflowY === 'scroll' || overflowY === 'auto' || overflowY === 'overlay'); } // Check for other CSS properties that might affect scrollability function isPotentiallyScrollable(elem) { var position = getActualCss(elem, 'position'); var display = getActualCss(elem, 'display'); var visibility = getActualCss(elem, 'visibility'); // Exclude elements that are not positioned in a way that could be scrollable if (position === 'static' && display === 'inline' && visibility !== 'hidden') { return false; } // Further checks can be added here as needed return true; } // Main function to check for scrollability var hasScroller = function hasScroller(elem) { // debug mode: do isAnyCornerVisible check by default and disable the check if debug.screenReaderVisible is true if (!_this5.config.debug.screenReaderVisible) { return _this5.isAnyCornerVisible(elem) && isPotentiallyScrollable(elem) && (isYScrollable(elem) || isXScrollable(elem)); } return isPotentiallyScrollable(elem) && (isYScrollable(elem) || isXScrollable(elem)); }; return [].filter.call(document.querySelectorAll('*'), hasScroller); } }, { key: "preventScroll", value: function preventScroll(e) { e.preventDefault(); e.stopPropagation(); return false; } }, { key: "clearMoveAttributes", value: function clearMoveAttributes() { document.querySelectorAll('[data-openkeynav-moveconfig]').forEach(function (el) { el.removeAttribute('data-openkeynav-moveconfig'); el.removeAttribute('data-openkeynav-draggable'); }); } }, { key: "removeOverlays", value: function removeOverlays(removeAll) { var _this6 = this; var resetModes = function resetModes() { for (var key in _this6.config.modes) { _this6.config.modes[key].value = false; } // reset move mode config _this6.config.modesConfig.move.selectedConfig = false; _this6.config.modesConfig.move.selectedMoveable = false; _this6.config.modesConfig.move.selectedMoveableHTML = false; _this6.config.modesConfig.move.selectedDropZone = false; _this6.config.modesConfig.move.modifier = false; // reset click mode config _this6.config.modesConfig.click.modifier = false; // reset menu mode config _this6.config.modesConfig.menu.modifier = false; }; var clearInaccessibleWarnings = function clearInaccessibleWarnings() { document.querySelectorAll('.openKeyNav-inaccessible').forEach(function (el) { // remove inaccessible indicator styles el.classList.remove('openKeyNav-inaccessible'); // Remove the event listeners if they exist in the map if (_this6.config.modesConfig.click.eventListenersMap.has(el)) { var _this6$config$modesCo = _this6.config.modesConfig.click.eventListenersMap.get(el), showTooltip = _this6$config$modesCo.showTooltip, hideTooltip = _this6$config$modesCo.hideTooltip; el.removeEventListener('mouseover', showTooltip); el.removeEventListener('mouseleave', hideTooltip); _this6.config.modesConfig.click.eventListenersMap.delete(el); } }); document.querySelectorAll('.openKeyNav-mouseover-tooltip').forEach(function (el) { return el.remove(); }); // remove the mouseover tooltips }; var enableScrolling = function enableScrolling() { // Re-enable scrolling on the webpage var enableScrollingForEl = function enableScrollingForEl(el) { el.removeEventListener('scroll', _this6.preventScroll, { passive: false }); el.removeEventListener('wheel', _this6.preventScroll, { passive: false }); el.removeEventListener('touchmove', _this6.preventScroll, { passive: false }); }; var enableScrollingForScrollableElements = function enableScrollingForScrollableElements() { enableScrollingForEl(window); _this6.getScrollableElements().forEach(function (el) { enableScrollingForEl(el); }); }; enableScrollingForScrollableElements(); }; var removeAllOverlays = function removeAllOverlays() { document.querySelectorAll('.openKeyNav-label').forEach(function (el) { return el.remove(); }); }; var removeAllOverlaysExceptThis = function removeAllOverlaysExceptThis(selectedLabel, typedLabel) { selectedLabel.innerHTML = "&bull;"; // selectedLabel.innerHTML="&middot;"; // selectedLabel.innerHTML="&nbsp;"; // selectedLabel.innerHTML="✔"; selectedLabel.classList.add('openKeyNav-label-selected'); document.querySelectorAll(".openKeyNav-label:not([data-openkeynav-label=\"".concat(typedLabel, "\"])")).forEach(function (el) { return el.remove(); }); }; // alert("removeOverlays()"); if (this.config.modes.clicking.value) { enableScrolling(); } // Remove overlay divs clearInaccessibleWarnings(); if (removeAll) { removeAllOverlays(); } else { if (!this.config.modes.moving.value) { // the only special modifer case so far for removing overlays is in moving mode, // where we may want to keep the selected element's label as a selected indicator removeAllOverlays(); } else { // in moving mode. // keep the selected element's label as a selected indicator var selectedLabel = document.querySelector(".openKeyNav-label[data-openkeynav-label=\"".concat(this.config.typedLabel.value, "\"]")); if (!selectedLabel) { removeAllOverlays(); } else { this.config.modesConfig.move.selectedLabel = selectedLabel; removeAllOverlaysExceptThis(selectedLabel, this.config.typedLabel.value); } } } document.querySelectorAll('[data-openkeynav-label]').forEach(function (el) { el.removeAttribute('data-openkeynav-label'); // Clean up data-openkeynav-label attributes }); resetModes(); this.config.typedLabel.value = ''; } }, { key: "flagAsInaccessible", value: function flagAsInaccessible(el, reason, modality) { switch (modality) { case "keyboard": if (!this.config.debug.keyboardAccessible) { return false; } default: break; } var openKeyNav = this; function createTooltip(el, innerHTML) { // Create the tooltip element var tooltip = document.createElement('div'); tooltip.className = 'openKeyNav-mouseover-tooltip'; tooltip.innerHTML = innerHTML; tooltip.style.display = 'none'; document.body.appendChild(tooltip); // Function to show the tooltip function showTooltip() { var rect = el.getBoundingClientRect(); tooltip.style.left = "".concat(rect.left + window.scrollX, "px"); tooltip.style.top = "".concat(rect.bottom + window.scrollY - 2, "px"); tooltip.style.display = 'block'; } // Function to hide the tooltip function hideTooltip() { // Get the mouse coordinates from the event var mouseX = event.clientX; var mouseY = event.clientY; // Get the bounding rectangle of the tooltip var tooltipRect = tooltip.getBoundingClientRect(); // Check if the mouse is currently over the tooltip var isMouseOverTooltip = mouseX >= tooltipRect.left && mouseX <= tooltipRect.right && mouseY >= tooltipRect.top && mouseY <= tooltipRect.bottom; // Only hide the tooltip if the mouse is not over it if (!isMouseOverTooltip) { tooltip.style.display = 'none'; } } el.addEventListener('mouseover', showTooltip); el.addEventListener('mouseleave', hideTooltip); tooltip.addEventListener('mouseleave', hideTooltip); // Store the event listeners for el in the map openKeyNav.config.modesConfig.click.eventListenersMap.set(el, { showTooltip: showTooltip, hideTooltip: hideTooltip }); } createTooltip(el, reason); el.classList.add('openKeyNav-inaccessible'); el.setAttribute('data-openkeynav-inaccessible-reason', reason); return true; } }, { key: "addKeydownEventListener", value: function addKeydownEventListener() { var _this7 = this; // Detect this.config.keys.click to enter label mode // Using an arrow function to maintain 'this' context of class document.addEventListener('keydown', function (e) { (0, _keypress.handleKeyPress)(_this7, e); }, true); // Also for the iframes window.addEventListener('message', function (e) { if (e.data.type === 'keydown') { console.log('Key pressed in iframe:', e.data.key); // Create a new event var newEvent = new KeyboardEvent('keydown', { key: e.data.key, keyCode: e.data.keyCode, altKey: e.data.altKey, ctrlKey: e.data.ctrlKey, shiftKey: e.data.shiftKey, metaKey: e.data.metaKey, bubbles: true, // This ensures the event bubbles up through the DOM cancelable: true // This lets it be cancelable }); if (newEvent.key === 'Escape') { // Execute escape logic (0, _escape.handleEscape)(_this7, e); } // Dispatch it on the document or specific element that your existing handler is attached to document.dispatchEvent(newEvent); } }); } // Function to emit a temporary notification }, { key: "emitNotification", value: function emitNotification(message) { // Function to create or select the notification container var getSetNotificationContainer = function getSetNotificationContainer() { // Create or select the notification container var notificationContainer = document.getElementById('okn-notification-container'); if (!notificationContainer) { notificationContainer = document.createElement('div'); notificationContainer.id = 'okn-notification-container'; notificationContainer.className = 'openKeyNav-ignore-overlap'; notificationContainer.style.position = 'fixed'; notificationContainer.style.bottom = '10px'; notificationContainer.style.left = '50%'; notificationContainer.style.transform = 'translateX(-50%)'; notificationContainer.style.display = 'flex'; notificationContainer.style.flexDirection = 'column'; notificationContainer.style.alignItems = 'center'; notificationContainer.style.gap = '10px'; notificationContainer.style.zIndex = '1000'; document.body.appendChild(notificationContainer); } return notificationContainer; }; // Check if notifications are enabled if (!this.config.notifications.enabled) { return; } // Get the notification container var notificationContainer = getSetNotificationContainer(); // Remove any existing notification before creating a new one while (notificationContainer.firstChild) { notificationContainer.firstChild.remove(); } // Create the notification element var notification = document.createElement('div'); notification.style.backgroundColor = 'rgba(0, 0, 0, 0.8)'; notification.style.color = '#fff'; notification.style.padding = '10px 20px'; notification.style.borderRadius = '5px'; notification.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)'; notification.style.maxWidth = '400px'; notification.style.textAlign = 'center'; notification.style.position = 'relative'; notification.style.display = 'inline-block'; // Add ARIA role for accessibility notification.setAttribute('role', 'alert'); notification.setAttribute('aria-live', 'assertive'); notification.setAttribute('aria-atomic', 'true'); // Optionally display the tool name in the notification if (this.config.notifications.displayToolName) { var logo = document.createElement('div'); logo.className = 'okn-logo-text tiny'; logo.setAttribute('role', 'img'); // Assigning an image role logo.setAttribute('aria-label', 'OpenKeyNav'); logo.innerHTML = 'Open<span class="key">Key</span>Nav'; notification.appendChild(logo); } // Create the message element var messageDiv = document.createElement('div'); messageDiv.innerHTML = message; // Append the message to the notification notification.appendChild(messageDiv); // Append the notification to the notification container notificationContainer.appendChild(notification); // Automatically remove the notification after the specified duration setTimeout(function () { notification.remove(); }, this.config.notifications.duration); } }, { key: "initStatusBar", value: function initStatusBar() { var _this8 = this; // Effect to emit a notification based on the current mode var lastMessage = "No mode active."; (0, _signals.effect)(function () { var modes = _this8.config.modes; var message; // Determine the message based on the current mode if (modes.clicking.value) { message = "In Click Mode. Press ".concat((0, _keyButton.keyButton)(["Esc"]), " to exit."); } else if (modes.moving.value) { message = "In Drag Mode. Press ".concat((0, _keyButton.keyButton)(["Esc"]), " to exit."); } else { message = "No mode active."; } // Only emit the notification if the message has changed if (message === lastMessage) { return; } // Emit the notification with the current message // console.log(message); _this8.emitNotification(message); lastMessage = message; }); // Effect to update the status bar based on the current mode (0, _signals.effect)(function () { var modes = _this8.config.modes; // DOM element to update var statusBar = document.getElementById('status-bar'); // Abort if no status bar is found if (!statusBar) { console.warn('Status bar element not found in the DOM.'); // TODO: is this depreciated? return; } // Update the status bar content based on the current mode if (modes.clicking.value) { statusBar.textContent = "In click mode. Press Esc to exit."; } else if (modes.moving.value) { statusBar.textContent = "In drag mode. Press Esc to exit."; } else { statusBar.textContent = "No mode active."; } }); } }, { key: "checkEnabled", value: function checkEnabled() { if (this.getSetCookie(this.config.enabledCookie)) { this.enable(); } } }, { key: "getSetCookie", value: function getSetCookie(cookieName, value) { // Helper: set cookie for domain, expires in 1 year function setCookie(cookieName, v) { var expires = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toUTCString(); document.cookie = "".concat(cookieName, "=").concat(v, "; expires=").concat(expires, "; path=/; domain=").concat(location.hostname); } // Helper: get cookie value function getCookie(cookieName) { var match = document.cookie.match(new RegExp('(^|; )' + cookieName + '=([^;]*)')); if (match) { return match[2] === 'true'; } return null; // not set } if (typeof value !== 'undefined') { setCookie(cookieName, value === true || value === 'true' ? 'true' : 'false'); return; } return getCookie(cookieName); } }, { key: "applicationSupport", value: function applicationSupport() { // Version Ping (POST https://applicationsupport.openkeynav.com/capture/) // This is anonymous and minimal, only sending the library version. No PII. // Necessary to know which versions are being used in the wild in order to provide proper support and plan roadmaps // no need to run app support on local develompent if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' || window.location.hostname === '::1') { return; } try { fetch("https://applicationsupport.openkeynav.com/capture/", { "method": "POST", "headers": { "Content-Type": "application/json" }, "body": JSON.stringify({ "properties": { "version": _version.version }, "event": "openKeyNav.js version ping" }) }); // .then((res) => res.text()) // .then(console.log.bind(console)) // .catch(console.error.bind(console)); } catch (error) { // fetch failed } } }, { key: "setupGlobalClickListenerTracking", value: function setupGlobalClickListenerTracking() { var clickEventElements = this.config.modesConfig.click.clickEventElements; var originalAddEventListener = EventTarget.prototype.addEventListener; var originalRemoveEventListener = EventTarget.prototype.removeEventListener; EventTarget.prototype.addEventListener = function (type, listener, options) { if (type === 'click') { // Add if the listener is not an empty function var isEmptyFunction = listener && /^\s*function\s*\(\)\s*\{\s*\}\s*$/.test(listener.toString()); if (!isEmptyFunction) { clickEventElements.add(this); } } return originalAddEventListener.call(this, type, listener, options); }; EventTarget.prototype.removeEventListener = function (type, listener, options) { if (type === 'click') { var remainingListeners = getEventListeners(this, 'click').filter(function (l) { return l.listener !== listener; }); if (remainingListeners.length === 0) { clickEventElements.delete(this); } } return originalRemoveEventListener.call(this, type, listener, options); }; var observer = new MutationObserver(function (mutationsList) { var _iterator = _createForOfIteratorHelper(mutationsList), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var mutation = _step.value; if (mutation.removedNodes.length > 0) { mutation.removedNodes.forEach(function (node) { if (node.nodeType === Node.ELEMENT_NODE) { removeDescendantsFromSet(node); } }); } } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } }); observer.observe(document.body, { childList: true, subtree: true }); function removeDescendantsFromSet(element) { if (clickEventElements.has(element)) { clickEventElements.delete(element); } element.querySelectorAll('*').forEach(function (child) { if (clickEventElements.has(child)) { clickEventElements.delete(child); } }); } function getEventListeners(el, eventType) { var listeners = []; var eventKey = "__eventListener__".concat(eventType); if (el[eventKey]) { listeners.push({ listener: el[eventKey] }); } return listeners; } } // Public API }, { key: "init", value: function init() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; this.deepMerge(this.config, options); this.addKeydownEventListener(); this.initStatusBar(); this.initToolBar(); this.applicationSupport(); this.checkEnabled(); console.log('Library initialized with config:', this.config); return this; } }]); }(); // optionally attach a syncronous event listener here for tracking