]> BookStack Code Mirror - bookstack/blob - resources/js/components/shortcuts.js
Added shortcut input controls to make custom shortcuts work
[bookstack] / resources / js / components / shortcuts.js
1 function reverseMap(map) {
2     const reversed = {};
3     for (const [key, value] of Object.entries(map)) {
4         reversed[value] = key;
5     }
6     return reversed;
7 }
8
9 /**
10  * @extends {Component}
11  */
12 class Shortcuts {
13
14     setup() {
15         this.container = this.$el;
16         this.mapById = JSON.parse(this.$opts.keyMap);
17         this.mapByShortcut = reverseMap(this.mapById);
18
19         this.hintsShowing = false;
20
21         this.hideHints = this.hideHints.bind(this);
22
23         this.setupListeners();
24     }
25
26     setupListeners() {
27         window.addEventListener('keydown', event => {
28
29             if (event.target.closest('input, select, textarea')) {
30                 return;
31             }
32
33             this.handleShortcutPress(event);
34         });
35
36         window.addEventListener('keydown', event => {
37             if (event.key === '?') {
38                 this.hintsShowing ? this.hideHints() : this.showHints();
39             }
40         });
41     }
42
43     /**
44      * @param {KeyboardEvent} event
45      */
46     handleShortcutPress(event) {
47
48         const keys = [
49             event.ctrlKey ? 'Ctrl' : '',
50             event.metaKey ? 'Cmd' : '',
51             event.key,
52         ];
53
54         const combo = keys.filter(s => Boolean(s)).join(' + ');
55
56         const shortcutId = this.mapByShortcut[combo];
57         if (shortcutId) {
58             const wasHandled = this.runShortcut(shortcutId);
59             if (wasHandled) {
60                 event.preventDefault();
61             }
62         }
63     }
64
65     /**
66      * Run the given shortcut, and return a boolean to indicate if the event
67      * was successfully handled by a shortcut action.
68      * @param {String} id
69      * @return {boolean}
70      */
71     runShortcut(id) {
72         const el = this.container.querySelector(`[data-shortcut="${id}"]`);
73         console.info('Shortcut run', el);
74         if (!el) {
75             return false;
76         }
77
78         if (el.matches('input, textarea, select')) {
79             el.focus();
80             return true;
81         }
82
83         if (el.matches('a, button')) {
84             el.click();
85             return true;
86         }
87
88         if (el.matches('div[tabindex]')) {
89             el.click();
90             el.focus();
91             return true;
92         }
93
94         console.error(`Shortcut attempted to be ran for element type that does not have handling setup`, el);
95
96         return false;
97     }
98
99     showHints() {
100         const wrapper = document.createElement('div');
101         wrapper.classList.add('shortcut-container');
102         this.container.append(wrapper);
103
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)) {
109                 continue;
110             }
111
112             const key = this.mapById[id];
113             this.showHintLabel(shortcutEl, key, wrapper);
114             displayedIds.add(id);
115         }
116
117         window.addEventListener('scroll', this.hideHints);
118         window.addEventListener('focus', this.hideHints);
119         window.addEventListener('blur', this.hideHints);
120         window.addEventListener('click', this.hideHints);
121
122         this.hintsShowing = true;
123     }
124
125     /**
126      * @param {Element} targetEl
127      * @param {String} key
128      * @param {Element} wrapper
129      */
130     showHintLabel(targetEl, key, wrapper) {
131         const targetBounds = targetEl.getBoundingClientRect();
132
133         const label = document.createElement('div');
134         label.classList.add('shortcut-hint');
135         label.textContent = key;
136
137         const linkage = document.createElement('div');
138         linkage.classList.add('shortcut-linkage');
139         linkage.style.left = targetBounds.x + 'px';
140         linkage.style.top = targetBounds.y + 'px';
141         linkage.style.width = targetBounds.width + 'px';
142         linkage.style.height = targetBounds.height + 'px';
143
144         wrapper.append(label, linkage);
145
146         const labelBounds = label.getBoundingClientRect();
147
148         label.style.insetInlineStart = `${((targetBounds.x + targetBounds.width) - (labelBounds.width + 6))}px`;
149         label.style.insetBlockStart = `${(targetBounds.y + (targetBounds.height - labelBounds.height) / 2)}px`;
150     }
151
152     hideHints() {
153         const wrapper = this.container.querySelector('.shortcut-container');
154         wrapper.remove();
155
156         window.removeEventListener('scroll', this.hideHints);
157         window.removeEventListener('focus', this.hideHints);
158         window.removeEventListener('blur', this.hideHints);
159         window.removeEventListener('click', this.hideHints);
160
161         this.hintsShowing = false;
162     }
163 }
164
165 export default Shortcuts;