]> BookStack Code Mirror - bookstack/blobdiff - resources/js/components/shortcuts.js
Guest create page: name field autofocus
[bookstack] / resources / js / components / shortcuts.js
index 799f0e629e8ea3927dd236ddc39f921ac2ae9a9a..a87213b2e8968070b5a0934cc3db0e6a1ef75fa0 100644 (file)
@@ -1,11 +1,4 @@
-/**
- * The default mapping of unique id to shortcut key.
- * @type {Object<string, string>}
- */
-const defaultMap = {
-    "edit": "e",
-    "global_search": "/",
-};
+import {Component} from "./component";
 
 function reverseMap(map) {
     const reversed = {};
@@ -15,21 +8,17 @@ function reverseMap(map) {
     return reversed;
 }
 
-/**
- * @extends {Component}
- */
-class Shortcuts {
+
+export class Shortcuts extends Component {
 
     setup() {
         this.container = this.$el;
-        this.mapById = defaultMap;
+        this.mapById = JSON.parse(this.$opts.keyMap);
         this.mapByShortcut = reverseMap(this.mapById);
 
         this.hintsShowing = false;
-        // TODO - Allow custom key maps
-        // TODO - Allow turning off shortcuts
-        // TODO - Roll out to interface elements
-        // TODO - Hide hints on focus, scroll, click
+
+        this.hideHints = this.hideHints.bind(this);
 
         this.setupListeners();
     }
@@ -41,23 +30,38 @@ class Shortcuts {
                 return;
             }
 
-            const shortcutId = this.mapByShortcut[event.key];
-            if (shortcutId) {
-                const wasHandled = this.runShortcut(shortcutId);
-                if (wasHandled) {
-                    event.preventDefault();
-                }
-            }
+            this.handleShortcutPress(event);
         });
 
         window.addEventListener('keydown', event => {
             if (event.key === '?') {
                 this.hintsShowing ? this.hideHints() : this.showHints();
-                this.hintsShowing = !this.hintsShowing;
             }
         });
     }
 
+    /**
+     * @param {KeyboardEvent} event
+     */
+    handleShortcutPress(event) {
+
+        const keys = [
+            event.ctrlKey ? 'Ctrl' : '',
+            event.metaKey ? 'Cmd' : '',
+            event.key,
+        ];
+
+        const combo = keys.filter(s => Boolean(s)).join(' + ');
+
+        const shortcutId = this.mapByShortcut[combo];
+        if (shortcutId) {
+            const wasHandled = this.runShortcut(shortcutId);
+            if (wasHandled) {
+                event.preventDefault();
+            }
+        }
+    }
+
     /**
      * Run the given shortcut, and return a boolean to indicate if the event
      * was successfully handled by a shortcut action.
@@ -66,7 +70,6 @@ class Shortcuts {
      */
     runShortcut(id) {
         const el = this.container.querySelector(`[data-shortcut="${id}"]`);
-        console.info('Shortcut run', el);
         if (!el) {
             return false;
         }
@@ -81,39 +84,79 @@ class Shortcuts {
             return true;
         }
 
+        if (el.matches('div[tabindex]')) {
+            el.click();
+            el.focus();
+            return true;
+        }
+
         console.error(`Shortcut attempted to be ran for element type that does not have handling setup`, el);
 
         return false;
     }
 
     showHints() {
+        const wrapper = document.createElement('div');
+        wrapper.classList.add('shortcut-container');
+        this.container.append(wrapper);
+
         const shortcutEls = this.container.querySelectorAll('[data-shortcut]');
+        const displayedIds = new Set();
         for (const shortcutEl of shortcutEls) {
             const id = shortcutEl.getAttribute('data-shortcut');
+            if (displayedIds.has(id)) {
+                continue;
+            }
+
             const key = this.mapById[id];
-            this.showHintLabel(shortcutEl, key);
+            this.showHintLabel(shortcutEl, key, wrapper);
+            displayedIds.add(id);
         }
+
+        window.addEventListener('scroll', this.hideHints);
+        window.addEventListener('focus', this.hideHints);
+        window.addEventListener('blur', this.hideHints);
+        window.addEventListener('click', this.hideHints);
+
+        this.hintsShowing = true;
     }
 
-    showHintLabel(targetEl, key) {
+    /**
+     * @param {Element} targetEl
+     * @param {String} key
+     * @param {Element} wrapper
+     */
+    showHintLabel(targetEl, key, wrapper) {
         const targetBounds = targetEl.getBoundingClientRect();
+
         const label = document.createElement('div');
         label.classList.add('shortcut-hint');
         label.textContent = key;
-        this.container.append(label);
+
+        const linkage = document.createElement('div');
+        linkage.classList.add('shortcut-linkage');
+        linkage.style.left = targetBounds.x + 'px';
+        linkage.style.top = targetBounds.y + 'px';
+        linkage.style.width = targetBounds.width + 'px';
+        linkage.style.height = targetBounds.height + 'px';
+
+        wrapper.append(label, linkage);
 
         const labelBounds = label.getBoundingClientRect();
 
-        label.style.insetInlineStart = `${((targetBounds.x + targetBounds.width) - (labelBounds.width + 12))}px`;
+        label.style.insetInlineStart = `${((targetBounds.x + targetBounds.width) - (labelBounds.width + 6))}px`;
         label.style.insetBlockStart = `${(targetBounds.y + (targetBounds.height - labelBounds.height) / 2)}px`;
     }
 
     hideHints() {
-        const hints = this.container.querySelectorAll('.shortcut-hint');
-        for (const hint of hints) {
-            hint.remove();
-        }
-    }
-}
+        const wrapper = this.container.querySelector('.shortcut-container');
+        wrapper.remove();
+
+        window.removeEventListener('scroll', this.hideHints);
+        window.removeEventListener('focus', this.hideHints);
+        window.removeEventListener('blur', this.hideHints);
+        window.removeEventListener('click', this.hideHints);
 
-export default Shortcuts;
\ No newline at end of file
+        this.hintsShowing = false;
+    }
+}
\ No newline at end of file