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);
21 this.hintAbortController = null;
23 this.setupListeners();
27 window.addEventListener('keydown', event => {
28 if (event.target.closest('input, select, textarea, .cm-editor, .editor-container')) {
32 if (event.key === '?') {
33 if (this.hintsShowing) {
41 this.handleShortcutPress(event);
46 * @param {KeyboardEvent} event
48 handleShortcutPress(event) {
50 event.ctrlKey ? 'Ctrl' : '',
51 event.metaKey ? 'Cmd' : '',
55 const combo = keys.filter(s => Boolean(s)).join(' + ');
57 const shortcutId = this.mapByShortcut[combo];
59 const wasHandled = this.runShortcut(shortcutId);
61 event.preventDefault();
67 * Run the given shortcut, and return a boolean to indicate if the event
68 * was successfully handled by a shortcut action.
73 const el = this.container.querySelector(`[data-shortcut="${id}"]`);
78 if (el.matches('input, textarea, select')) {
83 if (el.matches('a, button')) {
88 if (el.matches('div[tabindex]')) {
94 console.error('Shortcut attempted to be ran for element type that does not have handling setup', el);
100 const wrapper = document.createElement('div');
101 wrapper.classList.add('shortcut-container');
102 this.container.append(wrapper);
104 const shortcutEls = this.container.querySelectorAll('[data-shortcut]');
105 const displayedIds = new Set();
106 for (const shortcutEl of shortcutEls) {
107 const id = shortcutEl.getAttribute('data-shortcut');
108 if (displayedIds.has(id)) {
112 const key = this.mapById[id];
113 this.showHintLabel(shortcutEl, key, wrapper);
114 displayedIds.add(id);
117 this.hintAbortController = new AbortController();
118 const signal = this.hintAbortController.signal;
119 window.addEventListener('scroll', this.hideHints, {signal});
120 window.addEventListener('focus', this.hideHints, {signal});
121 window.addEventListener('blur', this.hideHints, {signal});
122 window.addEventListener('click', this.hideHints, {signal});
124 this.hintsShowing = true;
128 * @param {Element} targetEl
129 * @param {String} key
130 * @param {Element} wrapper
132 showHintLabel(targetEl, key, wrapper) {
133 const targetBounds = targetEl.getBoundingClientRect();
135 const label = document.createElement('div');
136 label.classList.add('shortcut-hint');
137 label.textContent = key;
139 const linkage = document.createElement('div');
140 linkage.classList.add('shortcut-linkage');
141 linkage.style.left = `${targetBounds.x}px`;
142 linkage.style.top = `${targetBounds.y}px`;
143 linkage.style.width = `${targetBounds.width}px`;
144 linkage.style.height = `${targetBounds.height}px`;
146 wrapper.append(label, linkage);
148 const labelBounds = label.getBoundingClientRect();
150 label.style.insetInlineStart = `${((targetBounds.x + targetBounds.width) - (labelBounds.width + 6))}px`;
151 label.style.insetBlockStart = `${(targetBounds.y + (targetBounds.height - labelBounds.height) / 2)}px`;
155 const wrapper = this.container.querySelector('.shortcut-container');
157 this.hintAbortController?.abort();
158 this.hintsShowing = false;