]> BookStack Code Mirror - bookstack/blob - resources/js/components/shortcuts.js
Started interface user shortcut form interface
[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             const shortcutId = this.mapByShortcut[event.key];
34             if (shortcutId) {
35                 const wasHandled = this.runShortcut(shortcutId);
36                 if (wasHandled) {
37                     event.preventDefault();
38                 }
39             }
40         });
41
42         window.addEventListener('keydown', event => {
43             if (event.key === '?') {
44                 this.hintsShowing ? this.hideHints() : this.showHints();
45             }
46         });
47     }
48
49     /**
50      * Run the given shortcut, and return a boolean to indicate if the event
51      * was successfully handled by a shortcut action.
52      * @param {String} id
53      * @return {boolean}
54      */
55     runShortcut(id) {
56         const el = this.container.querySelector(`[data-shortcut="${id}"]`);
57         console.info('Shortcut run', el);
58         if (!el) {
59             return false;
60         }
61
62         if (el.matches('input, textarea, select')) {
63             el.focus();
64             return true;
65         }
66
67         if (el.matches('a, button')) {
68             el.click();
69             return true;
70         }
71
72         if (el.matches('div[tabindex]')) {
73             el.click();
74             el.focus();
75             return true;
76         }
77
78         console.error(`Shortcut attempted to be ran for element type that does not have handling setup`, el);
79
80         return false;
81     }
82
83     showHints() {
84         const wrapper = document.createElement('div');
85         wrapper.classList.add('shortcut-container');
86         this.container.append(wrapper);
87
88         const shortcutEls = this.container.querySelectorAll('[data-shortcut]');
89         const displayedIds = new Set();
90         for (const shortcutEl of shortcutEls) {
91             const id = shortcutEl.getAttribute('data-shortcut');
92             if (displayedIds.has(id)) {
93                 continue;
94             }
95
96             const key = this.mapById[id];
97             this.showHintLabel(shortcutEl, key, wrapper);
98             displayedIds.add(id);
99         }
100
101         window.addEventListener('scroll', this.hideHints);
102         window.addEventListener('focus', this.hideHints);
103         window.addEventListener('blur', this.hideHints);
104         window.addEventListener('click', this.hideHints);
105
106         this.hintsShowing = true;
107     }
108
109     /**
110      * @param {Element} targetEl
111      * @param {String} key
112      * @param {Element} wrapper
113      */
114     showHintLabel(targetEl, key, wrapper) {
115         const targetBounds = targetEl.getBoundingClientRect();
116
117         const label = document.createElement('div');
118         label.classList.add('shortcut-hint');
119         label.textContent = key;
120
121         const linkage = document.createElement('div');
122         linkage.classList.add('shortcut-linkage');
123         linkage.style.left = targetBounds.x + 'px';
124         linkage.style.top = targetBounds.y + 'px';
125         linkage.style.width = targetBounds.width + 'px';
126         linkage.style.height = targetBounds.height + 'px';
127
128         wrapper.append(label, linkage);
129
130         const labelBounds = label.getBoundingClientRect();
131
132         label.style.insetInlineStart = `${((targetBounds.x + targetBounds.width) - (labelBounds.width + 6))}px`;
133         label.style.insetBlockStart = `${(targetBounds.y + (targetBounds.height - labelBounds.height) / 2)}px`;
134     }
135
136     hideHints() {
137         const wrapper = this.container.querySelector('.shortcut-container');
138         wrapper.remove();
139
140         window.removeEventListener('scroll', this.hideHints);
141         window.removeEventListener('focus', this.hideHints);
142         window.removeEventListener('blur', this.hideHints);
143         window.removeEventListener('click', this.hideHints);
144
145         this.hintsShowing = false;
146     }
147 }
148
149 export default Shortcuts;