// I grabbed the source code of this plugin from https://github.com/rodrigopv/vue3-shortkey // // At the time of writing this the released plugin was emitting debug console messages and // there was a PR open for about 1 year to address this https://github.com/rodrigopv/vue3-shortkey/pull/1 // If another library becomes available I think we should use it instead import 'element-matches'; import 'custom-event-polyfill'; const ShortKey = {}; const mapFunctions = {}; let objAvoided = []; let elementAvoided = []; let containerAvoided = []; let keyPressed = false; const parseValue = (value) => { value = typeof value === 'string' ? JSON.parse(value.replace(/\'/gi, '"')) : value; if (value instanceof Array) { return { '': value }; } return value; }; const bindValue = (value, el, binding, vnode) => { const push = binding.modifiers.push === true; const avoid = binding.modifiers.avoid === true; const focus = !binding.modifiers.focus === true; const once = binding.modifiers.once === true; const propagte = binding.modifiers.propagte === true; if (avoid) { objAvoided = objAvoided.filter((itm) => { return !itm === el; }); objAvoided.push(el); } else { mappingFunctions({ b: value, push, once, focus, propagte, el: vnode.el }); } }; const unbindValue = (value, el) => { for (const key in value) { const k = ShortKey.encodeKey(value[key]); const idxElm = mapFunctions[k].el.indexOf(el); if (mapFunctions[k].el.length > 1 && idxElm > -1) { mapFunctions[k].el.splice(idxElm, 1); } else { delete mapFunctions[k]; } } }; ShortKey.install = (Vue, options) => { elementAvoided = [...(options && options.prevent ? options.prevent : [])]; containerAvoided = [...(options && options.preventContainer ? options.preventContainer : [])]; Vue.directive('shortkey', { beforeMount: (el, binding, vnode) => { // Mapping the commands const value = parseValue(binding.value); bindValue(value, el, binding, vnode); }, updated: (el, binding, vnode) => { const oldValue = parseValue(binding.oldValue); unbindValue(oldValue, el); const newValue = parseValue(binding.value); bindValue(newValue, el, binding, vnode); }, unmounted: (el, binding) => { const value = parseValue(binding.value); unbindValue(value, el); } }); }; ShortKey.decodeKey = (pKey) => createShortcutIndex(pKey); ShortKey.encodeKey = (pKey) => { const shortKey = {}; shortKey.shiftKey = pKey.includes('shift'); shortKey.ctrlKey = pKey.includes('ctrl'); shortKey.metaKey = pKey.includes('meta'); shortKey.altKey = pKey.includes('alt'); let indexedKeys = createShortcutIndex(shortKey); const vKey = pKey.filter((item) => !['shift', 'ctrl', 'meta', 'alt'].includes(item)); indexedKeys += vKey.join(''); return indexedKeys; }; const createShortcutIndex = (pKey) => { let k = ''; if (pKey.key === 'Shift' || pKey.shiftKey) { k += 'shift'; } if (pKey.key === 'Control' || pKey.ctrlKey) { k += 'ctrl'; } if (pKey.key === 'Meta' || pKey.metaKey) { k += 'meta'; } if (pKey.key === 'Alt' || pKey.altKey) { k += 'alt'; } if (pKey.key === 'ArrowUp') { k += 'arrowup'; } if (pKey.key === 'ArrowLeft') { k += 'arrowleft'; } if (pKey.key === 'ArrowRight') { k += 'arrowright'; } if (pKey.key === 'ArrowDown') { k += 'arrowdown'; } if (pKey.key === 'AltGraph') { k += 'altgraph'; } if (pKey.key === 'Escape') { k += 'esc'; } if (pKey.key === 'Enter') { k += 'enter'; } if (pKey.key === 'Tab') { k += 'tab'; } if (pKey.key === ' ') { k += 'space'; } if (pKey.key === 'PageUp') { k += 'pageup'; } if (pKey.key === 'PageDown') { k += 'pagedown'; } if (pKey.key === 'Home') { k += 'home'; } if (pKey.key === 'End') { k += 'end'; } if (pKey.key === 'Delete') { k += 'del'; } if (pKey.key === 'Backspace') { k += 'backspace'; } if (pKey.key === 'Insert') { k += 'insert'; } if (pKey.key === 'NumLock') { k += 'numlock'; } if (pKey.key === 'CapsLock') { k += 'capslock'; } if (pKey.key === 'Pause') { k += 'pause'; } if (pKey.key === 'ContextMenu') { k += 'contextmenu'; } if (pKey.key === 'ScrollLock') { k += 'scrolllock'; } if (pKey.key === 'BrowserHome') { k += 'browserhome'; } if (pKey.key === 'MediaSelect') { k += 'mediaselect'; } if ((pKey.key && pKey.key !== ' ' && pKey.key.length === 1) || /F\d{1,2}|\//g.test(pKey.key)) k += pKey.key.toLowerCase(); return k; }; const dispatchShortkeyEvent = (pKey) => { const e = new CustomEvent('shortkey', { bubbles: false }); if (mapFunctions[pKey].key) e.srcKey = mapFunctions[pKey].key; const elm = mapFunctions[pKey].el; if (!mapFunctions[pKey].propagte) { elm[elm.length - 1].dispatchEvent(e); } else { elm.forEach((elmItem) => elmItem.dispatchEvent(e)); } }; ShortKey.keyDown = (pKey) => { if ((!mapFunctions[pKey].once && !mapFunctions[pKey].push) || (mapFunctions[pKey].push && !keyPressed)) { dispatchShortkeyEvent(pKey); } }; if (process && process.env && process.env.NODE_ENV !== 'test') { (function() { document.addEventListener('keydown', (pKey) => { const decodedKey = ShortKey.decodeKey(pKey); // Check avoidable elements if (availableElement(decodedKey)) { if (!mapFunctions[decodedKey].propagte) { pKey.preventDefault(); pKey.stopPropagation(); } if (mapFunctions[decodedKey].focus) { ShortKey.keyDown(decodedKey); keyPressed = true; } else if (!keyPressed) { const elm = mapFunctions[decodedKey].el; elm[elm.length - 1].focus(); keyPressed = true; } } }, true); document.addEventListener('keyup', (pKey) => { const decodedKey = ShortKey.decodeKey(pKey); if (availableElement(decodedKey)) { if (!mapFunctions[decodedKey].propagte) { pKey.preventDefault(); pKey.stopPropagation(); } if (mapFunctions[decodedKey].once || mapFunctions[decodedKey].push) { dispatchShortkeyEvent(decodedKey); } } keyPressed = false; }, true); })(); } const mappingFunctions = ({ b, push, once, focus, propagte, el }) => { for (const key in b) { const k = ShortKey.encodeKey(b[key]); const elm = mapFunctions[k] && mapFunctions[k].el ? mapFunctions[k].el : []; const propagated = mapFunctions[k] && mapFunctions[k].propagte; elm.push(el); mapFunctions[k] = { push, once, focus, key, propagte: propagated || propagte, el: elm }; } }; const availableElement = (decodedKey) => { const objectIsAvoided = !!objAvoided.find((r) => r === document.activeElement); const filterAvoided = !!(elementAvoided.find((selector) => document.activeElement && document.activeElement.matches(selector))); const filterAvoidedContainer = !!(containerAvoided.find((selector) => isActiveElementChildOf(selector))); return !!mapFunctions[decodedKey] && !(objectIsAvoided || filterAvoided) && !filterAvoidedContainer; }; const isActiveElementChildOf = (container) => { const activeElement = document.activeElement; return activeElement && activeElement.closest(container) !== null; }; export default ShortKey;