You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
463 lines
12 KiB
463 lines
12 KiB
1 year ago
|
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 }
|
||
|
);
|
||
|
}
|