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