import { moveFocusLeft, moveFocusRight, moveFocusDown, moveFocusUp, enterFocused, executeShortcut, } from './navigation'; import { createConfirmationDialog } from './helpers'; /** * Shortcuts * @typedef {{ key: string, altKey?: boolean, ctrlKey?: boolean, metaKey?: boolean, shiftKey?: boolean }} KeyCombination * @typedef {{ code: string, altKey?: boolean, ctrlKey?: boolean, metaKey?: boolean, shiftKey?: boolean }} CodeCombination * @typedef {{ combination: KeyCombination, event: 'keydown' | 'keyup', callback: (evt: KeyboardEvent) => void, target: EventTarget }} RegisteredShortcut * @typedef {{ type?: 'keydown' | 'keyup', propagate?: boolean, disabledInInput?: boolean, target?: EventTarget }} ShortcutOptions */ export default function handleShortcuts() { Alpine.store('shortcuts', { /** * @type RegisteredShortcut[] */ registeredShortcuts: [], /** * @param {KeyCombination | CodeCombination} combination * A combination using a `code` representing a physical key on the keyboard or a `key` * representing the character generated by pressing the key. Modifiers can be added using the * `altKey`, `ctrlKey`, `metaKey` or `shiftKey` parameters. * @param {(evt: KeyboardEvent) => void} callback * The callback function that will be called when the correct combination is pressed. * @param {ShortcutOptions?} options * An object of options, containing the event `type`, whether it will `propagate`, the `target` * element, and whether it's `disabledInInput`. * @returns {this} The `Shortcuts` object. */ register(combination, callback, options) { /** @type ShortcutOptions */ const defaultOptions = { type: 'keydown', propagate: false, disabledInInput: false, target: document, }; options = { ...defaultOptions, ...options }; /** * @param {KeyboardEvent} evt */ const func = (evt) => { if (options.disabledInInput) { // Don't enable shortcut keys in input, textarea, select fields const element = evt.target.nodeType === 3 ? evt.target.parentNode : evt.target; if (['input', 'textarea', 'selectbox'].includes(element.tagName.toLowerCase())) { return; } } const validations = [ combination.code ? combination.code == evt.code : combination.key.toLowerCase() == evt.key.toLowerCase(), (combination.altKey && evt.altKey) || (!combination.altKey && !evt.altKey), (combination.ctrlKey && evt.ctrlKey) || (!combination.ctrlKey && !evt.ctrlKey), (combination.metaKey && evt.metaKey) || (!combination.metaKey && !evt.metaKey), (combination.shiftKey && evt.shiftKey) || (!combination.shiftKey && !evt.shiftKey), ]; const valid = validations.filter(Boolean); if (valid.length === validations.length) { callback(evt); if (!options.propagate) { evt.stopPropagation(); evt.preventDefault(); } } }; this.registeredShortcuts.push({ combination, callback: func, target: options.target, event: options.type, }); options.target.addEventListener(options.type, func); return this; }, /** * @param {KeyCombination | CodeCombination} combination * A combination using a `code` representing a physical key on the keyboard or a `key` * representing the character generated by pressing the key. Modifiers can be added using the * `altKey`, `ctrlKey`, `metaKey` or `shiftKey` parameters. * @returns {this} The `Shortcuts` object. */ unregister(combination) { const shortcut = this.registeredShortcuts.find( (shortcut) => JSON.stringify(shortcut.combination) == JSON.stringify(combination) ); if (!shortcut) { return; } this.registeredShortcuts = this.registeredShortcuts.filter( (shortcut) => JSON.stringify(shortcut.combination) != JSON.stringify(combination) ); shortcut.target.removeEventListener(shortcut.event, shortcut.callback, false); return this; }, }); Alpine.store('shortcuts') .register( { key: 'A' }, (_evt) => { const createButton = document.querySelector('a.js-button-create'); if (!createButton) { return; } location.href = createButton.href; }, { disabledInInput: true } ) .register( { key: 'A', ctrlKey: true, shiftKey: true }, (_evt) => { const checked = document.querySelector('.js-unit-checkbox:eq(0)').checked; document .querySelectorAll('.js-unit') .forEach((el) => el.classList.toggle('selected'), !checked); document.querySelectorAll('.js-unit-checkbox').forEach((el) => (el.checked = !checked)); }, { disabledInInput: true } ) .register({ code: 'Enter', ctrlKey: true }, (_evt) => { document.querySelector('#main-form').submit(); }) .register({ code: 'Backspace', ctrlKey: true }, (_evt) => { const redirect = document.querySelector('a.js-button-back').href; if (!redirect) { return; } if (Alpine.store('form').dirty && redirect) { createConfirmationDialog({ message: Alpine.store('globals').CONFIRM_LEAVE_PAGE, targetUrl: redirect, }); } else if (redirect) { location.href = redirect; } }) .register( { key: 'F' }, (_evt) => { const searchBox = document.querySelector('.js-search-input'); if (searchBox) { searchBox.focus(); } }, { disabledInInput: true } ) .register( { code: 'Digit1' }, (_evt) => { const target = document.querySelector('.main-menu .main-menu-item:nth-of-type(1) a'); if (!target) { return; } if (Alpine.store('form').dirty) { createConfirmationDialog({ message: Alpine.store('globals').CONFIRM_LEAVE_PAGE, targetUrl: target.href, }); } else { location.href = target.href; } }, { disabledInInput: true } ) .register( { code: 'Digit2' }, (_evt) => { const target = document.querySelector('.main-menu .main-menu-item:nth-of-type(2) a'); if (!target) { return; } if (Alpine.store('form').dirty) { createConfirmationDialog({ message: Alpine.store('globals').CONFIRM_LEAVE_PAGE, targetUrl: target.href, }); } else { location.href = target.href; } }, { disabledInInput: true } ) .register( { code: 'Digit3' }, (_evt) => { const target = document.querySelector('.main-menu .main-menu-item:nth-of-type(3) a'); if (!target) { return; } if (Alpine.store('form').dirty) { createConfirmationDialog({ message: Alpine.store('globals').CONFIRM_LEAVE_PAGE, targetUrl: target.href, }); } else { location.href = target.href; } }, { disabledInInput: true } ) .register( { code: 'Digit4' }, (_evt) => { const target = document.querySelector('.main-menu .main-menu-item:nth-of-type(4) a'); if (!target) { return; } if (Alpine.store('form').dirty) { createConfirmationDialog({ message: Alpine.store('globals').CONFIRM_LEAVE_PAGE, targetUrl: target.href, }); } else { location.href = target.href; } }, { disabledInInput: true } ) .register( { code: 'Digit5' }, (_evt) => { const target = document.querySelector('.main-menu .main-menu-item:nth-of-type(5) a'); if (!target) { return; } if (Alpine.store('form').dirty) { createConfirmationDialog({ message: Alpine.store('globals').CONFIRM_LEAVE_PAGE, targetUrl: target.href, }); } else { location.href = target.href; } }, { disabledInInput: true } ) .register( { code: 'Digit6' }, (_evt) => { const target = document.querySelector('.main-menu .main-menu-item:nth-of-type(6) a'); if (!target) { return; } if (Alpine.store('form').dirty) { createConfirmationDialog({ message: Alpine.store('globals').CONFIRM_LEAVE_PAGE, targetUrl: target.href, }); } else { location.href = target.href; } }, { disabledInInput: true } ) .register( { code: 'Digit7' }, (_evt) => { const target = document.querySelector('.main-menu .main-menu-item:nth-of-type(7) a'); if (!target) { return; } if (Alpine.store('form').dirty) { createConfirmationDialog({ message: Alpine.store('globals').CONFIRM_LEAVE_PAGE, targetUrl: target.href, }); } else { location.href = target.href; } }, { disabledInInput: true } ) .register( { key: 'H' }, (_evt) => { const shortcutsDialog = document.querySelector('.shortcuts'); if (shortcutsDialog.open) { shortcutsDialog.close(); } else { shortcutsDialog.showModal(); } }, { disabledInInput: true } ) .register({ code: 'Escape' }, (_evt) => { const openDialog = document.querySelector('dialog[open]'); if (openDialog) { openDialog.close(); } document.querySelectorAll('input, checkbox, textarea, select').forEach((el) => el.blur()); }) .register( { code: 'ArrowLeft' }, (_evt) => { moveFocusLeft(); }, { disabledInInput: true } ) .register( { code: 'ArrowRight' }, (_evt) => { moveFocusRight(); }, { disabledInInput: true } ) .register( { code: 'ArrowDown' }, (_evt) => { moveFocusDown(); }, { disabledInInput: true } ) .register( { code: 'ArrowUp' }, (_evt) => { moveFocusUp(); }, { disabledInInput: true } ) .register( { key: 'L' }, (_evt) => { const element = document.querySelector('.js-units-container .js-unit.focus .shortcut-l'); if (element) { executeShortcut(element); } }, { disabledInInput: true } ) .register( { key: 'S' }, (_evt) => { const element = document.querySelector('.js-units-container .js-unit.focus .shortcut-s'); if (element) { executeShortcut(element); } }, { disabledInInput: true } ) .register( { key: 'W' }, (_evt) => { const element = document.querySelector('.js-units-container .js-unit.focus .shortcut-w'); if (element) { executeShortcut(element); } }, { disabledInInput: true } ) .register( { key: 'D' }, (_evt) => { const element = document.querySelector('.js-units-container .js-unit.focus .shortcut-d'); if (element) { executeShortcut(element); } }, { disabledInInput: true } ) .register( { key: 'R' }, (_evt) => { const element = document.querySelector('.js-units-container .js-unit.focus .shortcut-r'); if (element) { executeShortcut(element); } }, { disabledInInput: true } ) .register( { key: 'N' }, (_evt) => { const element = document.querySelector('.js-units-container .js-unit.focus .shortcut-n'); if (element) { executeShortcut(element); } }, { disabledInInput: true } ) .register( { key: 'U' }, (_evt) => { const element = document.querySelector('.js-units-container .js-unit.focus .shortcut-u'); if (element) { executeShortcut(element); } }, { disabledInInput: true } ) .register( { code: 'Delete' }, (_evt) => { const element = document.querySelector( '.js-units-container .js-unit.focus .shortcut-delete' ); if (element) { executeShortcut(element); } }, { disabledInInput: true } ) .register( { code: 'Enter' }, (evt) => { if (evt.target.tagName === 'INPUT' && evt.target.form.id === 'main-form') { evt.target.form.submit(); } if (Alpine.store('form').dirty) { if (document.querySelector('dialog[open]')) { const dialog = document.querySelector('dialog[open]'); dialog.querySelector('button[type="submit"]').click(); } else { createConfirmationDialog({ message: Alpine.store('globals').CONFIRM_LEAVE_PAGE, targetUrl: document.querySelector('.main-menu-item.focus a').href, }); } } else if (document.querySelector('dialog[open]')) { const dialog = document.querySelector('dialog[open]'); dialog.querySelector('button[type="submit"]').click(); } else { const element = document.querySelector( '.js-units-container .js-unit.focus .shortcut-enter' ); if (element) { executeShortcut(element); } else { enterFocused(); } } }, { propagate: true } ); }