UNPKG

keyboardly

Version:
1,740 lines (1,416 loc) 47.6 kB
/*! * Keyboardly v0.2.1 (https://github.com/victornpb/keyboardly) * Copyright (c) victornpb * @license MIT */ let DEBUG = false; let PREFIX = '[Keyboardly]'; const setDebug = bool => DEBUG = bool; function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _classPrivateFieldGet(receiver, privateMap) { var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "get"); return _classApplyDescriptorGet(receiver, descriptor); } function _classPrivateFieldSet(receiver, privateMap, value) { var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "set"); _classApplyDescriptorSet(receiver, descriptor, value); return value; } function _classExtractFieldDescriptor(receiver, privateMap, action) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to " + action + " private field on non-instance"); } return privateMap.get(receiver); } function _classApplyDescriptorGet(receiver, descriptor) { if (descriptor.get) { return descriptor.get.call(receiver); } return descriptor.value; } function _classApplyDescriptorSet(receiver, descriptor, value) { if (descriptor.set) { descriptor.set.call(receiver, value); } else { if (!descriptor.writable) { throw new TypeError("attempted to set read only private field"); } descriptor.value = value; } } function _checkPrivateRedeclaration(obj, privateCollection) { if (privateCollection.has(obj)) { throw new TypeError("Cannot initialize the same private elements twice on an object"); } } function _classPrivateFieldInitSpec(obj, privateMap, value) { _checkPrivateRedeclaration(obj, privateMap); privateMap.set(obj, value); } const isMacLike = !!navigator.platform.match(/Mac|iOS|iPod|iPad|Apple/i); function testElement(elm, event, attribute) { const isEnabled = !elm.disabled; const keyBinding = elm.getAttribute(attribute); return isEnabled && keyBinding && testKeybinding(keyBinding, event); } /** * Test if a keyboard event matches a keybinding string like ctrl-c * @param {string} keyBinding A key binding string e.g. 'crtl-shift-a', 'shift-x', 'enter' * @param {KeyboardEvent} event A keyboard event * @return {boolean} Return true when the event matches the keybiding string */ function testKeybinding(keyBinding, event) { function test(keyBinding, event) { const keys = keyBinding.split(/-|\+/); const k = keys[keys.length - 1]; return event.ctrlKey === keys.includes('ctrl') && event.shiftKey === keys.includes('shift') && event.altKey === keys.includes('alt') && event.metaKey === keys.includes('meta') && (event.key === k || event.code === k) //TODO: keycodes are its own nightmare ; } // Mac if (isMacLike) { //TODO: make this a setting keyBinding = keyBinding.replace(/cmd|command/g, 'meta'); keyBinding = keyBinding.replace(/ctrl/g, 'meta'); // Use Coommand instead of commoon ctrl shortcuts } if (keyBinding.includes('|')) { // has alternative keybidings const aliases = keyBinding.split('|'); for (const kb of aliases) { if (test(kb, event)) return true; } return false; } else { // single keybiding return test(keyBinding, event); } } // const keyCodes = { // esc: 27, // tab: 9, // enter: 13, // space: 32, // up: 38, // left: 37, // right: 39, // down: 40, // 'delete': [8, 46] // }; const ATTR_OVERRIDE_DEFAULT = 'data-prevent'; function shortcutDelegator(container, event, keybindingAttr, dispatcherFn = _defaultAction) { if (container) { // focused on input const activeElm = document.activeElement; const isFocusedOnInput = activeElm.nodeName === 'INPUT' || activeElm.nodeName === 'TEXTAREA'; // find elements with shortcuts // TODO: cache oportunity const matchedElements = Array.from(container.querySelectorAll(`[${keybindingAttr}]`)).filter(elm => testElement(elm, event, keybindingAttr)); if (matchedElements.length) { if (isFocusedOnInput && matchedElements.every(elm => !elm.hasAttribute(ATTR_OVERRIDE_DEFAULT))) return; //ignore event unless a hotkey is set to prevent // trigger clicks matchedElements.forEach(element => { if (typeof dispatcherFn === 'function') { if (DEBUG) console.log(PREFIX, 'dispatching shortcut...', container, element, event, dispatcherFn); dispatcherFn({ container, element, event }); } }); } } } function _defaultAction({ container, element, event }) { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); try { element.focus(); } catch (_) { /* ignore */ } try { element.click(); } catch (_) { /* ignore */ } } function findNextItem(elements, current, e) { const rects = elements.map(getRect); const focused = getRect(current || elements[0]); const dryRun = e.shiftKey; let next; if (e.key === 'ArrowUp') { next = findAbove(focused, rects, dryRun); } else if (e.key === 'ArrowDown') { next = findBelow(focused, rects, dryRun); } else if (e.key === 'ArrowLeft') { next = findLeft(focused, rects, dryRun); } else if (e.key === 'ArrowRight') { next = findRight(focused, rects, dryRun); } if (dryRun) { //debug //DEBUG_TARGET(current); return; } if (next) { e.preventDefault(); if (dryRun) DEBUG_TARGET(next); return next.element; } return null; } function getRect(element) { const r = element.getBoundingClientRect(); const rect = { element: element, left: r.left, top: r.top, right: r.right, bottom: r.bottom, width: r.width, height: r.height, x: r.left + Math.floor(r.width / 2), y: r.top + Math.floor(r.height / 2) }; return rect; } // function inViewport(el) { // let rect = el.getBoundingClientRect(); // return ( // rect.top >= 0 && // rect.left >= 0 && // rect.bottom <= window.innerHeight && // rect.right <= window.innerWidth // ); // } /** * return direction of angle * @param {number} angle Degrees in range 0-360 * 45 up 135 * \ / * left \/ right * /\ * / \ * 315 down 225 */ function direction(angle) { if (angle >= 45 && angle < 135) return 'up'; if (angle >= 135 && angle < 225) return 'right'; if (angle >= 225 && angle < 315) return 'down'; if (angle >= 315 || angle < 45) return 'left'; } function distance(fromItem, toItem) { const dx = toItem.x - fromItem.x; const dy = toItem.y - fromItem.y; return Math.sqrt(dx * dx + dy * dy); } function findAngle(fromItem, toItem) { const dx = fromItem.x - toItem.x; const dy = fromItem.y - toItem.y; const angle = Math.atan2(dy, dx); const deg = angle * 180 / Math.PI; // radians to degrees return deg % 360 + (deg < 0 ? 360 : 0); // normalize angles to 0-360 } function findBelow(fromItem, rects, dryRun) { let dest; for (const item of rects) { if (dryRun) DEBUG_TARGET(item, 0); if (item.element === fromItem.element) continue; const angle = findAngle(fromItem, item); // item.element.innerHTML = `${angle}º`; if (direction(angle) === 'down') { if (!dest) dest = item; if (dryRun) DEBUG_TARGET(item, 1); if (distance(fromItem, item) < distance(fromItem, dest)) { dest = item; } } } return dest; } function findAbove(fromItem, rects, dryRun) { let dest; for (const item of rects) { if (dryRun) DEBUG_TARGET(item, 0); if (item.element === fromItem.element) continue; const angle = findAngle(fromItem, item); // item.element.innerHTML = `${angle}º`; if (direction(angle) === 'up') { if (!dest) dest = item; if (dryRun) DEBUG_TARGET(item, 1); if (distance(fromItem, item) < distance(fromItem, dest)) { dest = item; } } } return dest; } function findRight(fromItem, rects, dryRun) { let dest; for (const item of rects) { if (dryRun) DEBUG_TARGET(item, 0); if (item.element === fromItem.element) continue; const angle = findAngle(fromItem, item); // item.element.innerHTML = `${angle}º`; if (direction(angle) === 'right') { if (!dest) dest = item; if (dryRun) DEBUG_TARGET(item, 1); if (distance(fromItem, item) < distance(fromItem, dest)) { dest = item; } } } return dest; } function findLeft(fromItem, rects, dryRun) { let dest; for (const item of rects) { if (dryRun) DEBUG_TARGET(item, 0); if (item.element === fromItem.element) continue; const angle = findAngle(fromItem, item); // item.element.innerHTML = `${angle}º`; if (direction(angle) === 'left') { if (!dest) dest = item; if (dryRun) DEBUG_TARGET(item, 1); if (distance(fromItem, item) < distance(fromItem, dest)) { dest = item; } } } return dest; } //// function DEBUG_TARGET(item, x) { if (DEBUG) item.element.style.backgroundColor = x ? '#FF0' : '#333'; setTimeout(() => item.element.style.backgroundColor = '', 1000 * 3); } function isEnabled(elm) { return !(elm.disabled !== undefined ? elm.disabled : elm.hasAttribute('disabled')); } function isFocusedOnInput() { const elm = document.activeElement; return elm && (elm.nodeName === 'INPUT' || elm.nodeName === 'TEXTAREA'); } var _focusPanelOnClickHandler = /*#__PURE__*/new WeakMap(); var _tabFocusFollowerHandler = /*#__PURE__*/new WeakMap(); var _navigationHandler = /*#__PURE__*/new WeakMap(); class ComponentFocusManager { constructor() { _defineProperty(this, "ACTIVE_CLASS", "focus"); _defineProperty(this, "COMPONENT_SELECTOR", "[data-shortcut-component]"); _defineProperty(this, "activeComponent", null); _classPrivateFieldInitSpec(this, _focusPanelOnClickHandler, { writable: true, value: null }); _classPrivateFieldInitSpec(this, _tabFocusFollowerHandler, { writable: true, value: null }); _classPrivateFieldInitSpec(this, _navigationHandler, { writable: true, value: null }); _classPrivateFieldSet(this, _focusPanelOnClickHandler, this._focusPanelOnClickHandler.bind(this)); _classPrivateFieldSet(this, _tabFocusFollowerHandler, this._tabFocusFollowerHandler.bind(this)); _classPrivateFieldSet(this, _navigationHandler, this._navigationHandler.bind(this)); // this.init(); } init() { document.addEventListener("click", _classPrivateFieldGet(this, _focusPanelOnClickHandler)); document.addEventListener("focusin", _classPrivateFieldGet(this, _tabFocusFollowerHandler)); document.addEventListener("keydown", _classPrivateFieldGet(this, _navigationHandler)); } destroy() { document.removeEventListener('click', _classPrivateFieldGet(this, _focusPanelOnClickHandler)); document.removeEventListener('focusin', _classPrivateFieldGet(this, _tabFocusFollowerHandler)); document.removeEventListener('keydown', _classPrivateFieldGet(this, _navigationHandler)); } /** Focus a element when clicked */ _focusPanelOnClickHandler(event) { if (DEBUG) console.log(PREFIX, 'focusPanelOnClickHandler', event); const clickedElm = event.target; this.focusComponentOfTargetElm(clickedElm); } /** Watch for focus changing via Tab */ _tabFocusFollowerHandler(event) { if (DEBUG) console.log(PREFIX, 'tabFocusFollowerHandler', event); const focusedElm = document.activeElement; this.focusComponentOfTargetElm(focusedElm); } focusComponentOfTargetElm(targetElm) { const component = targetElm && targetElm.closest(this.COMPONENT_SELECTOR); if (component && isEnabled(component)) { if (component !== this.activeComponent) { this.setCurrentComponent(component); } } else { this.unselectCurrentComponent(); } return component; } unselectCurrentComponent() { if (this.activeComponent) { if (document.activeElement === this.activeComponent) this.activeComponent.blur(); this.blurElm(this.activeComponent); this.activeComponent = null; } } setCurrentComponent(elm) { if (this.activeComponent && this.activeComponent !== elm) this.unselectCurrentComponent(); this.activeComponent = elm; this.focusElm(this.activeComponent); this.activeComponent.setAttribute("tabindex", 0); // make focusable // focus the actual panel, unless there's something focused inside it if (!this.activeComponent.contains(document.activeElement)) { // TODO: optimize? this.activeComponent.focus(); } } focusElm(elm) { elm.classList.add(this.ACTIVE_CLASS); } blurElm(elm) { elm.classList.remove(this.ACTIVE_CLASS); } getActiveComponent() { return this.activeComponent; } _navigationHandler(event) { if (DEBUG) console.log(PREFIX, 'navigationHandler', event); if (isFocusedOnInput()) return; // do not navigate if focused on input let components = Array.from(document.querySelectorAll(this.COMPONENT_SELECTOR)); components = components.filter(isEnabled); let nextPanel; const currentPanel = this.getActiveComponent(); // TODO: handle which key moves in which direction should be handled outside the findNextItem function nextPanel = findNextItem(components, currentPanel, event); if (nextPanel && nextPanel !== currentPanel) { event.preventDefault(); event.stopPropagation(); this.setCurrentComponent(nextPanel); // nextPanel.scrollIntoView(); } } } /// --- shortcut handling var _isInit = /*#__PURE__*/new WeakMap(); var _shortcutDelegatorHandler = /*#__PURE__*/new WeakMap(); class Shortcuts { constructor() { _defineProperty(this, "KEYBIDING_ATTR", 'data-shortcut'); _defineProperty(this, "mngr", null); _classPrivateFieldInitSpec(this, _isInit, { writable: true, value: false }); _classPrivateFieldInitSpec(this, _shortcutDelegatorHandler, { writable: true, value: null }); _classPrivateFieldSet(this, _shortcutDelegatorHandler, this._shortcutDelegatorHandler.bind(this)); this.mngr = new ComponentFocusManager(); this.init(); } init() { if (!_classPrivateFieldGet(this, _isInit)) { this.mngr.init(); // TODO: add support for holding keys keyup/keydown document.addEventListener('keydown', _classPrivateFieldGet(this, _shortcutDelegatorHandler)); _classPrivateFieldSet(this, _isInit, true); } else throw new Error("Already initialized!"); } destroy() { this.mngr.destroy(); document.removeEventListener('keydown', _classPrivateFieldGet(this, _shortcutDelegatorHandler)); _classPrivateFieldSet(this, _isInit, false); } _shortcutDelegatorHandler(event) { if (DEBUG) console.log(PREFIX, "_shortcutDelegatorHandler", event); const currentPanel = this.mngr.getActiveComponent(); shortcutDelegator(currentPanel, event, this.KEYBIDING_ATTR); } getShortcuts() { const currentPanel = this.mngr.getActiveComponent(); const elms = Array.from(currentPanel.querySelectorAll(`[${this.KEYBIDING_ATTR}]`)).filter(elm => !elm.disabled); const shortcuts = elms.map(elm => ({ keyBinding: elm.getAttribute(this.KEYBIDING_ATTR), text: elm.innerText, title: elm.title, elm: elm })); return shortcuts; } getAllShortcuts() { const elms = Array.from(document.querySelectorAll(`[${this.KEYBIDING_ATTR}]`)).filter(elm => !elm.disabled); const shortcuts = elms.map(elm => ({ keyBinding: elm.getAttribute(this.KEYBIDING_ATTR), text: elm.innerText, title: elm.title, elm: elm })); return shortcuts; } } /** * A hot key is a key combination that the user can press to perform an action quickly. * Global to the application, examples: save, undo, bring a menu on a game etc... */ var _handler = /*#__PURE__*/new WeakMap(); class Hotkeys { constructor() { _classPrivateFieldInitSpec(this, _handler, { writable: true, value: null }); this.init(); } init() { if (_classPrivateFieldGet(this, _handler)) throw new Error("Already initialized!"); _classPrivateFieldSet(this, _handler, this._hotkeysDelegatorHandler.bind(this)); document.addEventListener('keydown', _classPrivateFieldGet(this, _handler)); } destroy() { document.removeEventListener('keydown', _classPrivateFieldGet(this, _handler)); _classPrivateFieldSet(this, _handler, null); } _hotkeysDelegatorHandler(event) { if (DEBUG) console.log(PREFIX, "hotkeysDelegatorHandler", event); shortcutDelegator(document, event, Hotkeys.KEYBIDING_ATTR); } } _defineProperty(Hotkeys, "KEYBIDING_ATTR", 'data-hotkey'); function announceHotkeys() { const elms = Array.from(document.querySelectorAll(`[${Hotkeys.KEYBIDING_ATTR}]`)).filter(elm => !elm.disabled); const hotkeys = elms.map(elm => ({ keyBinding: elm.getAttribute(Hotkeys.KEYBIDING_ATTR), text: elm.innerText, title: elm.title, elm: elm })); return hotkeys; } const DIRECTIVE = 'tooltip'; /** * Creates the actual tooltip element, appends it to the document * at this point it is not visible or positioned. */ function createTooltipElm(doc = document) { const style = ` position: fixed; z-index: 9999999; pointer-events: none; will-change: top, left, opacity; --transition: opacity 250ms ease 100ms; font-size: 18px; font-family: sans-serif; padding: 5px 8px; border-radius: 2px; background: rgba(218,218,218, 0.85); color: rgb(0 0 0); box-shadow: rgb(0 0 0 / 20%) 0px 3px 1px -2px, rgb(0 0 0 / 14%) 0px 2px 2px 0px, rgb(0 0 0 / 12%) 0px 1px 5px 0px; font-weight: bold; min-width: 32px; text-align: center; text-transform: capitalize; top: 0; left: 0; opacity: 0; display: none; `; const tooltipElm = doc.createElement('div'); tooltipElm.setAttribute('class', `v-${DIRECTIVE}-directive`); tooltipElm.style.cssText = style; // ARIA https://www.w3.org/TR/wai-aria-practices-1.1/#tooltip tooltipElm.id = `id_${Math.random()}`; tooltipElm.setAttribute('role', 'tooltip'); tooltipElm.setAttribute('aria-hidden', 'true'); return tooltipElm; } function positionTooltip(tooltipElement, targetElement, margin = 0, doc) { // The tooltip is already in the document, but not visible. // It needs to be be a block, and be inside the screen for it to have dimensions. // Since we reuse tooltips, if the content changes, and it overflows off-screen, // the computed styles will not match when placed in the the real posision. if (tooltipElement.style.display === 'none') { tooltipElement.style.opacity = 0; tooltipElement.style.display = 'block'; tooltipElement.style.top = `${0}px`; tooltipElement.style.left = `${0}px`; } // Get dimensions and positioning of target and tooltip const tooltipRect = tooltipElement.getBoundingClientRect(); const targetRect = targetElement.getBoundingClientRect(); // set tooltip on top center of target let y = targetRect.top - margin - tooltipRect.height; let x = targetRect.left + targetRect.width / 2 - tooltipRect.width / 2; // is tooltip above screen? move it below the target if (y < 0) y = targetRect.bottom + margin; // is tooltip-left-side off screen? align with target-left-side if (x < 0) x = targetRect.left; // is tooltip-right-side off screen? align with target-right-side const vw = Math.max(doc.documentElement.clientWidth || 0, window.innerWidth || 0); if (x + tooltipRect.width > vw) x = targetRect.right - tooltipRect.width; // place the tooltip position to the x,y calculated tooltipElement.style.left = `${x}px`; tooltipElement.style.top = `${y}px`; } class Tooltip { constructor({ target, html, margin = 5, root = document.body }) { this.margin = margin; this.target = target; this.el = createTooltipElm(this.target.ownerDocument); if (typeof root === 'string') root = document.querySelector(root); root.appendChild(this.el); // ARIA https://www.w3.org/TR/wai-aria-practices-1.1/#tooltip this.target.setAttribute('aria-describedby', this.el.id); this.el.setAttribute('aria-hidden', 'false'); this.update(html); } update(html) { this.el.innerHTML = html; if (this.target && this.target.parentNode) { positionTooltip(this.el, this.target, this.margin, this.el.ownerDocument); this.el.style.opacity = 1; } } destroy() { if (this.target && this.target.parentNode) { this.target.removeAttribute('aria-describedby'); } if (this.el.parentElement) this.el.parentElement.removeChild(this.el); } } /*! * FooBar v0.0.0 (https://github.com/username/foo-bar) * Copyright (c) username * @license MIT */ function findHighestZIndex(tagName = '*') { let max = null; const elems = document.getElementsByTagName(tagName); for (const elm of elems) { let z = document.defaultView.getComputedStyle(elm, null).getPropertyValue('z-index'); if (z !== 'auto') { z = parseInt(z, 10); if (z > max || max === null) max = z; } } return max; } var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; function createCommonjsModule(fn, module) { return module = { exports: {} }, fn(module, module.exports), module.exports; } var penner = createCommonjsModule(function (module, exports) { /* Copyright © 2001 Robert Penner All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of the author nor the names of contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ (function () { var penner, umd; umd = function (factory) { { return module.exports = factory; } }; penner = { linear: function (t, b, c, d) { return c * t / d + b; }, easeInQuad: function (t, b, c, d) { return c * (t /= d) * t + b; }, easeOutQuad: function (t, b, c, d) { return -c * (t /= d) * (t - 2) + b; }, easeInOutQuad: function (t, b, c, d) { if ((t /= d / 2) < 1) { return c / 2 * t * t + b; } else { return -c / 2 * (--t * (t - 2) - 1) + b; } }, easeInCubic: function (t, b, c, d) { return c * (t /= d) * t * t + b; }, easeOutCubic: function (t, b, c, d) { return c * ((t = t / d - 1) * t * t + 1) + b; }, easeInOutCubic: function (t, b, c, d) { if ((t /= d / 2) < 1) { return c / 2 * t * t * t + b; } else { return c / 2 * ((t -= 2) * t * t + 2) + b; } }, easeInQuart: function (t, b, c, d) { return c * (t /= d) * t * t * t + b; }, easeOutQuart: function (t, b, c, d) { return -c * ((t = t / d - 1) * t * t * t - 1) + b; }, easeInOutQuart: function (t, b, c, d) { if ((t /= d / 2) < 1) { return c / 2 * t * t * t * t + b; } else { return -c / 2 * ((t -= 2) * t * t * t - 2) + b; } }, easeInQuint: function (t, b, c, d) { return c * (t /= d) * t * t * t * t + b; }, easeOutQuint: function (t, b, c, d) { return c * ((t = t / d - 1) * t * t * t * t + 1) + b; }, easeInOutQuint: function (t, b, c, d) { if ((t /= d / 2) < 1) { return c / 2 * t * t * t * t * t + b; } else { return c / 2 * ((t -= 2) * t * t * t * t + 2) + b; } }, easeInSine: function (t, b, c, d) { return -c * Math.cos(t / d * (Math.PI / 2)) + c + b; }, easeOutSine: function (t, b, c, d) { return c * Math.sin(t / d * (Math.PI / 2)) + b; }, easeInOutSine: function (t, b, c, d) { return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b; }, easeInExpo: function (t, b, c, d) { if (t === 0) { return b; } else { return c * Math.pow(2, 10 * (t / d - 1)) + b; } }, easeOutExpo: function (t, b, c, d) { if (t === d) { return b + c; } else { return c * (-Math.pow(2, -10 * t / d) + 1) + b; } }, easeInOutExpo: function (t, b, c, d) { if ((t /= d / 2) < 1) { return c / 2 * Math.pow(2, 10 * (t - 1)) + b; } else { return c / 2 * (-Math.pow(2, -10 * --t) + 2) + b; } }, easeInCirc: function (t, b, c, d) { return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b; }, easeOutCirc: function (t, b, c, d) { return c * Math.sqrt(1 - (t = t / d - 1) * t) + b; }, easeInOutCirc: function (t, b, c, d) { if ((t /= d / 2) < 1) { return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b; } else { return c / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b; } }, easeInElastic: function (t, b, c, d) { var a, p, s; s = 1.70158; p = 0; a = c; if (t === 0) ;else if ((t /= d) === 1) ; if (!p) { p = d * .3; } if (a < Math.abs(c)) { a = c; s = p / 4; } else { s = p / (2 * Math.PI) * Math.asin(c / a); } return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b; }, easeOutElastic: function (t, b, c, d) { var a, p, s; s = 1.70158; p = 0; a = c; if (t === 0) ;else if ((t /= d) === 1) ; if (!p) { p = d * .3; } if (a < Math.abs(c)) { a = c; s = p / 4; } else { s = p / (2 * Math.PI) * Math.asin(c / a); } return a * Math.pow(2, -10 * t) * Math.sin((t * d - s) * (2 * Math.PI) / p) + c + b; }, easeInOutElastic: function (t, b, c, d) { var a, p, s; s = 1.70158; p = 0; a = c; if (t === 0) ;else if ((t /= d / 2) === 2) ; if (!p) { p = d * (.3 * 1.5); } if (a < Math.abs(c)) { a = c; s = p / 4; } else { s = p / (2 * Math.PI) * Math.asin(c / a); } if (t < 1) { return -.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b; } else { return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p) * .5 + c + b; } }, easeInBack: function (t, b, c, d, s) { if (s === void 0) { s = 1.70158; } return c * (t /= d) * t * ((s + 1) * t - s) + b; }, easeOutBack: function (t, b, c, d, s) { if (s === void 0) { s = 1.70158; } return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b; }, easeInOutBack: function (t, b, c, d, s) { if (s === void 0) { s = 1.70158; } if ((t /= d / 2) < 1) { return c / 2 * (t * t * (((s *= 1.525) + 1) * t - s)) + b; } else { return c / 2 * ((t -= 2) * t * (((s *= 1.525) + 1) * t + s) + 2) + b; } }, easeInBounce: function (t, b, c, d) { var v; v = penner.easeOutBounce(d - t, 0, c, d); return c - v + b; }, easeOutBounce: function (t, b, c, d) { if ((t /= d) < 1 / 2.75) { return c * (7.5625 * t * t) + b; } else if (t < 2 / 2.75) { return c * (7.5625 * (t -= 1.5 / 2.75) * t + .75) + b; } else if (t < 2.5 / 2.75) { return c * (7.5625 * (t -= 2.25 / 2.75) * t + .9375) + b; } else { return c * (7.5625 * (t -= 2.625 / 2.75) * t + .984375) + b; } }, easeInOutBounce: function (t, b, c, d) { var v; if (t < d / 2) { v = penner.easeInBounce(t * 2, 0, c, d); return v * .5 + b; } else { v = penner.easeOutBounce(t * 2 - d, 0, c, d); return v * .5 + c * .5 + b; } } }; umd(penner); }).call(commonjsGlobal); }); /** * @function shorthand * @param {Number|Number[]} values A number, or Array of 4, 3, 2 or 1 Values * @return {[Number, Number, Number, Number]} Array of values [T, R, B, L] */ function shorthand(a = 0, b, c, d) { let l = arguments.length; if (Array.isArray(a)) { l = a.length; [a, b, c, d] = a; } if (l === 1) return [a, a, a, a]; if (l === 2) return [a, b, a, b]; if (l === 3) return [a, b, c, b]; if (l === 4) return [a, b, c, d]; return [0, 0, 0, 0]; } /** * Draws a rounded rectangle on a CanvasRenderingContext2D * @param {CanvasRenderingContext2D} context * @param {Number} x The x-axis coordinate of the rectangle's starting point. * @param {Number} y The y-axis coordinate of the rectangle's starting point. * @param {Number} width The rectangle's width. Positive values are to the right, and negative to the left. * @param {Number} height The rectangle's height. Positive values are down, and negative are up. * @param {Number|Number[]} [radius=5] Border radius; You can specify borders indivudially by passing an array containing 4 values [TL, TR, BR, BL] or 3 Values [TL, TR_BL, BR] or 2 Values [TL_BR, TR_BL]. */ function roundRect(context, x, y, width, height, radius = 5) { function constraintRadius(radius, width, height) { const limit = (width < height ? width : height) / 2; radius[0] = radius[0] > limit ? limit : radius[0]; radius[1] = radius[1] > limit ? limit : radius[1]; radius[2] = radius[2] > limit ? limit : radius[2]; radius[3] = radius[3] > limit ? limit : radius[3]; return radius; } radius = shorthand(radius); radius = constraintRadius(radius, width, height); context.beginPath(); context.moveTo(x + radius[0], y); context.lineTo(x + width - radius[1], y); context.quadraticCurveTo(x + width, y, x + width, y + radius[1]); context.lineTo(x + width, y + height - radius[2]); context.quadraticCurveTo(x + width, y + height, x + width - radius[2], y + height); context.lineTo(x + radius[3], y + height); context.quadraticCurveTo(x, y + height, x, y + height - radius[3]); context.lineTo(x, y + radius[0]); context.quadraticCurveTo(x, y, x + radius[0], y); context.closePath(); } class Spot { constructor(stage) { _defineProperty(this, "type", ''); _defineProperty(this, "_stage", null); this._stage = stage; } draw(ctx) { throw 'Not implemented'; } remove() { if (this._stage) { this._stage.removeSpot(this); this._stage = null; } } } class SpotCircle extends Spot { constructor(stage, x, y, radius) { super(stage); _defineProperty(this, "type", 'circle'); _defineProperty(this, "x", 0); _defineProperty(this, "y", 0); _defineProperty(this, "radius", 0); this.x = x; this.y = y; this.radius = radius; } draw(ctx) { ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); ctx.fill(); } } class SpotRetangle extends Spot { constructor(stage, x, y, width, height) { super(stage); _defineProperty(this, "type", 'retangle'); _defineProperty(this, "x", 0); _defineProperty(this, "y", 0); _defineProperty(this, "width", 0); _defineProperty(this, "height", 0); this.x = x; this.y = y; this.width = width; this.height = height; } draw(ctx) { ctx.beginPath(); ctx.fillRect(this.x, this.y, this.width, this.height); } } class SpotRoundRetangle extends Spot { constructor(stage, x, y, width, height, radius) { super(stage); _defineProperty(this, "type", 'round-retangle'); _defineProperty(this, "x", 0); _defineProperty(this, "y", 0); _defineProperty(this, "width", 0); _defineProperty(this, "height", 0); _defineProperty(this, "radius", 5); this.x = x; this.y = y; this.width = width; this.height = height; this.radius = radius; } draw(ctx) { ctx.beginPath(); roundRect(ctx, this.x, this.y, this.width, this.height, this.radius); ctx.fill(); } } class SpotPolygon extends Spot { constructor(stage, points) { super(stage); _defineProperty(this, "type", 'polygon'); _defineProperty(this, "points", []); this.points = points; } draw(ctx) { ctx.beginPath(); ctx.moveTo(this.points[0], this.points[1]); for (let i = 2; i < this.points.length; i += 2) { ctx.lineTo(this.points[i], this.points[i + 1]); } ctx.closePath(); ctx.fill(); } } class SpotElement extends Spot { constructor(stage, element, options) { super(stage); _defineProperty(this, "type", 'element'); _defineProperty(this, "elm", HTMLElement); _defineProperty(this, "radius", 0); _defineProperty(this, "padding", 0); this.elm = element; this.radius = options.radius; this.padding = options.padding; } draw(ctx) { const rect = this.elm.getBoundingClientRect(); const padding = shorthand(this.padding); const x = rect.left - padding[3]; const y = rect.top - padding[0]; const h = rect.height + padding[0] + padding[2]; const w = rect.width + padding[3] + padding[1]; ctx.beginPath(); if (this.radius) { roundRect(ctx, x, y, w, h, this.radius); ctx.fill(); } else { ctx.fillRect(x, y, w, h); } } } /** * spotlight-canvas: a canvas element that dims the screen except for spotlight locations formed by circles or polygons */ var _resizeHandler = /*#__PURE__*/new WeakMap(); var _scrollHandler = /*#__PURE__*/new WeakMap(); var _nextTick = /*#__PURE__*/new WeakMap(); class SpotlightStage { /** * create a spotlight div * @param {object} [options] * @param {number} [options.x=0] use to place layer on creation * @param {number} [options.y=0] * @param {number} [options.width=window.innerWidth] * @param {number} [options.height=window.innerHeight] * @param {number} [options.color=black] color of under layer * @param {number} [options.alpha=0.8] alpha of under layer * @param {number} [options.zIndex] zIndex of under layer * @param {number} [options.forceSyncRedraw] force redrawing after each syncronous operation * @param {HTMLElement} [options.parent=document.body] parent of spotlight layer */ constructor(options) { _defineProperty(this, "options", {}); _defineProperty(this, "canvas", undefined); _defineProperty(this, "spots", []); _defineProperty(this, "looping", false); _classPrivateFieldInitSpec(this, _resizeHandler, { writable: true, value: undefined }); _classPrivateFieldInitSpec(this, _scrollHandler, { writable: true, value: undefined }); _classPrivateFieldInitSpec(this, _nextTick, { writable: true, value: undefined }); this.options = options || {}; /** * @type {HTMLCanvasElement} */ this.canvas = undefined; /** * the list of spotlights. if manually changed then call redraw() to update the canvas * @type {Spot[]} */ this.spots = []; _classPrivateFieldSet(this, _resizeHandler, () => { if (this.isVisible) this.resize(); }); _classPrivateFieldSet(this, _scrollHandler, () => { if (this.isVisible) this.redraw(); }); /** requestAnimationFrame request id */ _classPrivateFieldSet(this, _nextTick, undefined); this.init(); } init(options) { if (this.canvas) throw new Error('Already initialized!'); if (options) this.options = options; this.canvas = document.createElement('canvas'); this.canvas.style.position = 'fixed'; this.canvas.style.top = this.options.x || 0; this.canvas.style.left = this.options.y || 0; this.canvas.style.zIndex = this.options.zIndex || findHighestZIndex() + 1; this.canvas.style.pointerEvents = 'none'; (this.options.parent || document.body).appendChild(this.canvas); this.spots = []; window.addEventListener('resize', _classPrivateFieldGet(this, _resizeHandler), { passive: true }); window.addEventListener('scroll', _classPrivateFieldGet(this, _scrollHandler), { passive: true }); this.resize(); } /** * resize the layer to ensure entire screen is covered; also calls redraw() * @returns {Spot} */ resize(forceRedrawNow = false) { const width = this.options.width || window.innerWidth; const height = this.options.height || window.innerHeight; this.canvas.width = width; this.canvas.height = height; if (forceRedrawNow === true) this.redrawNow();else this.redraw(); } startLoop() { const _this = this; if (this.looping) return; // already looping this.looping = true; function renderLoop() { if (_this.looping) _this.redraw(false, renderLoop); } renderLoop(); return true; } stopLoop() { this.looping = false; if (_classPrivateFieldGet(this, _nextTick)) { window.cancelAnimationFrame(_classPrivateFieldGet(this, _nextTick)); _classPrivateFieldSet(this, _nextTick, undefined); } } /** * schedule a redraw of the spotlight on next frame * @returns {Spot} */ redraw(syncRedraw, cb) { console.log('redraw'); if (syncRedraw ?? this.options.forceSyncRedraw) return this.redrawNow(); if (!_classPrivateFieldGet(this, _nextTick)) { _classPrivateFieldSet(this, _nextTick, requestAnimationFrame(() => { _classPrivateFieldSet(this, _nextTick, undefined); this.redrawNow(); if (cb) cb(this); })); } } /** * redraw of the spotlight (usually called internally) * @returns {Spot} */ redrawNow() { console.log('redraw now!'); const ctx = this.canvas.getContext('2d'); ctx.save(); ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); ctx.fillStyle = this.options.color || '#000000'; ctx.globalAlpha = this.options.alpha || 0.8; ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); ctx.restore(); ctx.save(); ctx.globalCompositeOperation = 'destination-out'; for (const spot of this.spots) { spot.draw(ctx, this.options); } ctx.restore(); } /** * clears any cutouts * @param {boolean} [redraw=true] Redraws the scene * @returns {Spot} */ clear(redraw = true) { this.spots = []; if (redraw == true) { this.resize(); } } /** * adds a circle spotlight * @param {number} x * @param {number} y * @param {number} radius * @param {boolean} [redraw=true] Redraws the scene * @returns {SpotCircle} */ addCircle(x, y, radius, redraw = true) { const spot = new SpotCircle(this, x, y, radius); this.spots.push(spot); if (redraw === true) this.redraw(); return spot; } /** * adds a rectangle spotlight * @param {number} x * @param {number} y * @param {number} width * @param {number} height * @param {boolean} [redraw=true] Redraws the scene * @returns {SpotRetangle} */ addRectangle(x, y, width, height, redraw = true) { const spot = new SpotRetangle(this, x, y, width, height); this.spots.push(spot); if (redraw === true) this.redraw(); return spot; } /** * adds a rectangle spotlight * @param {number} x * @param {number} y * @param {number} width * @param {number} height * @param {boolean} [redraw=true] Redraws the scene * @returns {SpotRetangle} */ addRoundRectangle(x, y, width, height, radius, redraw = true) { const spot = new SpotRoundRetangle(this, x, y, width, height, radius); this.spots.push(spot); if (redraw === true) this.redraw(); return spot; } /** * adds a polygon spotlight * @param {number[]} points - [x1, y1, x2, y2, ... xn, yn] * @param {boolean} [redraw=true] Redraws the scene * @returns {SpotPolygon} */ addPolygon(points, redraw = true) { const spot = new SpotPolygon(this, points); this.spots.push(spot); if (redraw === true) this.redraw(); return spot; } /** * adds a element spotlight * @param {HTMLElement} element a DOM element * @param {boolean} [redraw=true] Redraws the scene * @returns {SpotElement} */ addElement(element, options = {}, redraw = true) { const spot = new SpotElement(this, element, options); this.spots.push(spot); if (redraw === true) this.redraw(); return spot; } /** * adds a spotlight to a list of elements * @param {HTMLElement[]} elements array of DOM elements * @param {boolean} [redraw=true] Redraws the scene * @returns {Spot[]} */ addElements(elements, options, redraw = true) { const spots = []; for (const element of elements) { const spot = this.addElement(element, options, false); spots.push(spot); } if (redraw === true) this.redraw(); return spots; } /** * * @param {Spot} spot Removes a spot from the canvas * @param {boolean} [redraw=true] Redraws the scene * @return {boolean} True if the spotlight was removed */ removeSpot(spot, redraw = true) { const index = this.spots.indexOf(spot); if (index > -1) { this.spots.splice(index, 1); if (redraw === true) this.redraw(); return true; } return false; } /** * used internally for fade * @param {object} data * @private */ _fade(data) { return new Promise((resolve, reject) => { this.request = null; const now = performance.now(); const difference = now - this.last; this.last = now; data.time += difference; const change = data.end - data.start; if (data.time > data.duration) { this.canvas.style.opacity = data.end; if (data.onEnd) { data.onEnd(); } } else { this.canvas.style.opacity = data.ease(data.time, data.start, change, data.duration); this.request = requestAnimationFrame(() => this._fade(data)); } }); } /** * fade in the under layer * @param {*} [options] * @param {number} [options.start=0] starting opacity * @param {number} [options.end=1] ending opacity * @param {number} [options.duration=1000] duration of fade in milliseconds * @param {string|Function} [options.ease='easeInOutSine'] easing function (@see https://www.npmjs.com/package/penner) * @param {Function} [options.onEnd] callback after fading * @returns {Spot} */ fadeIn(options) { if (this.request) { cancelAnimationFrame(this.request); } options = options || {}; const start = typeof options.start === 'undefined' ? 0 : options.start; const end = typeof options.end === 'undefined' ? 1 : options.end; const ease = !options.ease ? penner.easeInOutSine : typeof options.ease === 'string' ? penner[options.ease] : options.ease; const onEnd = options.onEnd; this.canvas.style.opacity = start; const duration = options.duration || 1000; this.last = performance.now(); this.canvas.style.display = 'block'; this.canvas.style.opacity = 0; this._fade({ time: 0, start, end, duration, ease, onEnd }); return this; } /** * fade out the under layer * @param {*} [options] * @param {number} [options.start=1] starting opacity * @param {number} [options.end=0] ending opacity * @param {number} [options.duration=1000] duration of fade in milliseconds * @param {string|Function} [options.ease='easeInOutSine'] easing function (@see https://www.npmjs.com/package/penner) * @param {Function} [options.onEnd] callback after fading * @returns {Spot} */ fadeOut(options) { options = options || {}; options.start = typeof options.start === 'undefined' ? 1 : options.start; options.end = typeof options.end === 'undefined' ? 0 : options.end; this.fadeIn(options); return this; } /** * show spotlight * @return {SpotlightStage} */ show() { this.canvas.style.display = 'block'; this.canvas.style.opacity = 1; return this; } /** * hide spotlight * @return {SpotlightStage} */ hide() { this.canvas.style.display = 'none'; this.canvas.style.opacity = 0; return this; } /** * checks whether spotlight is visible * @returns {boolean} */ get isVisible() { return this.canvas && this.canvas.style.display !== 'none'; } /** * removes spotlight */ destroy() { if (this.canvas.parentElementNode) this.canvas.parentElementNode.removeChild(this.canvas); this.canvas = undefined; this.spots = []; if (_classPrivateFieldGet(this, _nextTick)) { cancelAnimationFrame(_classPrivateFieldGet(this, _nextTick)); _classPrivateFieldSet(this, _nextTick, undefined); } window.removeEventListener('scroll', _classPrivateFieldGet(this, _scrollHandler)); window.removeEventListener('resize', _classPrivateFieldGet(this, _resizeHandler)); } } window.Spotlight = SpotlightStage; const instances = new Map(); let stage = new SpotlightStage({ zIndex: 100 }); stage.hide(); window.stage = stage; function announce() { clear(); const array = []; const hotkeys = announceHotkeys(); for (const hotkey of hotkeys) { const tooltip = new Tooltip({ target: hotkey.elm, html: hotkey.keyBinding, margin: -5 }); instances.set(hotkey.elm, tooltip); array.push(tooltip); } stage.addElements(hotkeys.map(h => h.elm), { padding: -1, radius: 9999 }); stage.show(); return array; } function clear() { for (const tooltip of instances.values()) { tooltip.destroy(); } instances.clear(); } window.announce = announce; // const shortcuts = new Shortcuts(); var index = { Shortcuts, ComponentFocusManager, Hotkeys, announceHotkeys, announce, setDebug // hotkeys, // shortcuts, }; export { index as default };