2022-11-04 11:20:19 -04:00
|
|
|
function reverseMap(map) {
|
|
|
|
const reversed = {};
|
|
|
|
for (const [key, value] of Object.entries(map)) {
|
|
|
|
reversed[value] = key;
|
|
|
|
}
|
|
|
|
return reversed;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @extends {Component}
|
|
|
|
*/
|
|
|
|
class Shortcuts {
|
|
|
|
|
|
|
|
setup() {
|
|
|
|
this.container = this.$el;
|
2022-11-08 16:17:45 -05:00
|
|
|
this.mapById = JSON.parse(this.$opts.keyMap);
|
2022-11-04 11:20:19 -04:00
|
|
|
this.mapByShortcut = reverseMap(this.mapById);
|
|
|
|
|
|
|
|
this.hintsShowing = false;
|
2022-11-05 09:39:17 -04:00
|
|
|
|
|
|
|
this.hideHints = this.hideHints.bind(this);
|
2022-11-04 11:20:19 -04:00
|
|
|
|
|
|
|
this.setupListeners();
|
|
|
|
}
|
|
|
|
|
|
|
|
setupListeners() {
|
|
|
|
window.addEventListener('keydown', event => {
|
|
|
|
|
|
|
|
if (event.target.closest('input, select, textarea')) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-11-09 09:40:44 -05:00
|
|
|
this.handleShortcutPress(event);
|
2022-11-04 11:20:19 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
window.addEventListener('keydown', event => {
|
|
|
|
if (event.key === '?') {
|
|
|
|
this.hintsShowing ? this.hideHints() : this.showHints();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-11-09 09:40:44 -05:00
|
|
|
/**
|
|
|
|
* @param {KeyboardEvent} event
|
|
|
|
*/
|
|
|
|
handleShortcutPress(event) {
|
|
|
|
|
|
|
|
const keys = [
|
|
|
|
event.ctrlKey ? 'Ctrl' : '',
|
|
|
|
event.metaKey ? 'Cmd' : '',
|
|
|
|
event.key,
|
|
|
|
];
|
|
|
|
|
|
|
|
const combo = keys.filter(s => Boolean(s)).join(' + ');
|
|
|
|
|
|
|
|
const shortcutId = this.mapByShortcut[combo];
|
|
|
|
if (shortcutId) {
|
|
|
|
const wasHandled = this.runShortcut(shortcutId);
|
|
|
|
if (wasHandled) {
|
|
|
|
event.preventDefault();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-04 11:20:19 -04:00
|
|
|
/**
|
|
|
|
* Run the given shortcut, and return a boolean to indicate if the event
|
|
|
|
* was successfully handled by a shortcut action.
|
|
|
|
* @param {String} id
|
|
|
|
* @return {boolean}
|
|
|
|
*/
|
|
|
|
runShortcut(id) {
|
|
|
|
const el = this.container.querySelector(`[data-shortcut="${id}"]`);
|
|
|
|
if (!el) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (el.matches('input, textarea, select')) {
|
|
|
|
el.focus();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (el.matches('a, button')) {
|
|
|
|
el.click();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-11-05 09:39:17 -04:00
|
|
|
if (el.matches('div[tabindex]')) {
|
|
|
|
el.click();
|
|
|
|
el.focus();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-11-04 11:20:19 -04:00
|
|
|
console.error(`Shortcut attempted to be ran for element type that does not have handling setup`, el);
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
showHints() {
|
2022-11-05 09:57:22 -04:00
|
|
|
const wrapper = document.createElement('div');
|
|
|
|
wrapper.classList.add('shortcut-container');
|
|
|
|
this.container.append(wrapper);
|
|
|
|
|
2022-11-04 11:20:19 -04:00
|
|
|
const shortcutEls = this.container.querySelectorAll('[data-shortcut]');
|
2022-11-05 09:39:17 -04:00
|
|
|
const displayedIds = new Set();
|
2022-11-04 11:20:19 -04:00
|
|
|
for (const shortcutEl of shortcutEls) {
|
|
|
|
const id = shortcutEl.getAttribute('data-shortcut');
|
2022-11-05 09:39:17 -04:00
|
|
|
if (displayedIds.has(id)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2022-11-04 11:20:19 -04:00
|
|
|
const key = this.mapById[id];
|
2022-11-05 09:57:22 -04:00
|
|
|
this.showHintLabel(shortcutEl, key, wrapper);
|
2022-11-05 09:39:17 -04:00
|
|
|
displayedIds.add(id);
|
2022-11-04 11:20:19 -04:00
|
|
|
}
|
2022-11-05 09:39:17 -04:00
|
|
|
|
|
|
|
window.addEventListener('scroll', this.hideHints);
|
|
|
|
window.addEventListener('focus', this.hideHints);
|
|
|
|
window.addEventListener('blur', this.hideHints);
|
|
|
|
window.addEventListener('click', this.hideHints);
|
|
|
|
|
|
|
|
this.hintsShowing = true;
|
2022-11-04 11:20:19 -04:00
|
|
|
}
|
|
|
|
|
2022-11-05 09:57:22 -04:00
|
|
|
/**
|
|
|
|
* @param {Element} targetEl
|
|
|
|
* @param {String} key
|
|
|
|
* @param {Element} wrapper
|
|
|
|
*/
|
|
|
|
showHintLabel(targetEl, key, wrapper) {
|
2022-11-04 11:20:19 -04:00
|
|
|
const targetBounds = targetEl.getBoundingClientRect();
|
2022-11-05 09:57:22 -04:00
|
|
|
|
2022-11-04 11:20:19 -04:00
|
|
|
const label = document.createElement('div');
|
|
|
|
label.classList.add('shortcut-hint');
|
|
|
|
label.textContent = key;
|
2022-11-05 09:57:22 -04:00
|
|
|
|
|
|
|
const linkage = document.createElement('div');
|
|
|
|
linkage.classList.add('shortcut-linkage');
|
|
|
|
linkage.style.left = targetBounds.x + 'px';
|
|
|
|
linkage.style.top = targetBounds.y + 'px';
|
|
|
|
linkage.style.width = targetBounds.width + 'px';
|
|
|
|
linkage.style.height = targetBounds.height + 'px';
|
|
|
|
|
|
|
|
wrapper.append(label, linkage);
|
2022-11-04 11:20:19 -04:00
|
|
|
|
|
|
|
const labelBounds = label.getBoundingClientRect();
|
|
|
|
|
2022-11-05 09:57:22 -04:00
|
|
|
label.style.insetInlineStart = `${((targetBounds.x + targetBounds.width) - (labelBounds.width + 6))}px`;
|
2022-11-04 11:20:19 -04:00
|
|
|
label.style.insetBlockStart = `${(targetBounds.y + (targetBounds.height - labelBounds.height) / 2)}px`;
|
|
|
|
}
|
|
|
|
|
|
|
|
hideHints() {
|
2022-11-05 09:57:22 -04:00
|
|
|
const wrapper = this.container.querySelector('.shortcut-container');
|
|
|
|
wrapper.remove();
|
2022-11-05 09:39:17 -04:00
|
|
|
|
|
|
|
window.removeEventListener('scroll', this.hideHints);
|
|
|
|
window.removeEventListener('focus', this.hideHints);
|
|
|
|
window.removeEventListener('blur', this.hideHints);
|
|
|
|
window.removeEventListener('click', this.hideHints);
|
|
|
|
|
|
|
|
this.hintsShowing = false;
|
2022-11-04 11:20:19 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default Shortcuts;
|