1 import {Component} from './component';
3 function reverseMap(map) {
5 for (const [key, value] of Object.entries(map)) {
11 export class Shortcuts extends Component {
14 this.container = this.$el;
15 this.mapById = JSON.parse(this.$opts.keyMap);
16 this.mapByShortcut = reverseMap(this.mapById);
18 this.hintsShowing = false;
20 this.hideHints = this.hideHints.bind(this);
22 this.setupListeners();
26 window.addEventListener('keydown', event => {
27 if (event.target.closest('input, select, textarea')) {
31 this.handleShortcutPress(event);
34 window.addEventListener('keydown', event => {
35 if (event.key === '?') {
36 const action = this.hintsShowing ? this.hideHints : this.showHints;
43 * @param {KeyboardEvent} event
45 handleShortcutPress(event) {
47 event.ctrlKey ? 'Ctrl' : '',
48 event.metaKey ? 'Cmd' : '',
52 const combo = keys.filter(s => Boolean(s)).join(' + ');
54 const shortcutId = this.mapByShortcut[combo];
56 const wasHandled = this.runShortcut(shortcutId);
58 event.preventDefault();
64 * Run the given shortcut, and return a boolean to indicate if the event
65 * was successfully handled by a shortcut action.
70 const el = this.container.querySelector(`[data-shortcut="${id}"]`);
75 if (el.matches('input, textarea, select')) {
80 if (el.matches('a, button')) {
85 if (el.matches('div[tabindex]')) {
91 console.error('Shortcut attempted to be ran for element type that does not have handling setup', el);
97 const wrapper = document.createElement('div');
98 wrapper.classList.add('shortcut-container');
99 this.container.append(wrapper);
101 const shortcutEls = this.container.querySelectorAll('[data-shortcut]');
102 const displayedIds = new Set();
103 for (const shortcutEl of shortcutEls) {
104 const id = shortcutEl.getAttribute('data-shortcut');
105 if (displayedIds.has(id)) {
109 const key = this.mapById[id];
110 this.showHintLabel(shortcutEl, key, wrapper);
111 displayedIds.add(id);
114 window.addEventListener('scroll', this.hideHints);
115 window.addEventListener('focus', this.hideHints);
116 window.addEventListener('blur', this.hideHints);
117 window.addEventListener('click', this.hideHints);
119 this.hintsShowing = true;
123 * @param {Element} targetEl
124 * @param {String} key
125 * @param {Element} wrapper
127 showHintLabel(targetEl, key, wrapper) {
128 const targetBounds = targetEl.getBoundingClientRect();
130 const label = document.createElement('div');
131 label.classList.add('shortcut-hint');
132 label.textContent = key;
134 const linkage = document.createElement('div');
135 linkage.classList.add('shortcut-linkage');
136 linkage.style.left = `${targetBounds.x}px`;
137 linkage.style.top = `${targetBounds.y}px`;
138 linkage.style.width = `${targetBounds.width}px`;
139 linkage.style.height = `${targetBounds.height}px`;
141 wrapper.append(label, linkage);
143 const labelBounds = label.getBoundingClientRect();
145 label.style.insetInlineStart = `${((targetBounds.x + targetBounds.width) - (labelBounds.width + 6))}px`;
146 label.style.insetBlockStart = `${(targetBounds.y + (targetBounds.height - labelBounds.height) / 2)}px`;
150 const wrapper = this.container.querySelector('.shortcut-container');
153 window.removeEventListener('scroll', this.hideHints);
154 window.removeEventListener('focus', this.hideHints);
155 window.removeEventListener('blur', this.hideHints);
156 window.removeEventListener('click', this.hideHints);
158 this.hintsShowing = false;