]> BookStack Code Mirror - bookstack/blob - resources/js/services/keyboard-navigation.js
Extracted keyboard nav. from dropdowns to share w/ search
[bookstack] / resources / js / services / keyboard-navigation.js
1 /**
2  * Handle common keyboard navigation events within a given container.
3  */
4 export class KeyboardNavigationHandler {
5
6     /**
7      * @param {Element} container
8      * @param {Function|null} onEscape
9      * @param {Function|null} onEnter
10      */
11     constructor(container, onEscape = null, onEnter = null) {
12         this.containers = [container];
13         this.onEscape = onEscape;
14         this.onEnter = onEnter;
15         container.addEventListener('keydown', this.#keydownHandler.bind(this));
16     }
17
18     /**
19      * Also share the keyboard event handling to the given element.
20      * Only elements within the original container are considered focusable though.
21      * @param {Element} element
22      */
23     shareHandlingToEl(element) {
24         this.containers.push(element);
25         element.addEventListener('keydown', this.#keydownHandler.bind(this));
26     }
27
28     /**
29      * Focus on the next focusable element within the current containers.
30      */
31     focusNext() {
32         const focusable = this.#getFocusable();
33         const currentIndex = focusable.indexOf(document.activeElement);
34         let newIndex = currentIndex + 1;
35         if (newIndex >= focusable.length) {
36             newIndex = 0;
37         }
38
39         focusable[newIndex].focus();
40     }
41
42     /**
43      * Focus on the previous existing focusable element within the current containers.
44      */
45     focusPrevious() {
46         const focusable = this.#getFocusable();
47         const currentIndex = focusable.indexOf(document.activeElement);
48         let newIndex = currentIndex - 1;
49         if (newIndex < 0) {
50             newIndex = focusable.length - 1;
51         }
52
53         focusable[newIndex].focus();
54     }
55
56     /**
57      * @param {KeyboardEvent} event
58      */
59     #keydownHandler(event) {
60         if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
61             this.focusNext();
62             event.preventDefault();
63         } else if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
64             this.focusPrevious();
65             event.preventDefault();
66         } else if (event.key === 'Escape') {
67             if (this.onEscape) {
68                 this.onEscape(event);
69             } else if  (document.activeElement) {
70                 document.activeElement.blur();
71             }
72         } else if (event.key === 'Enter' && this.onEnter) {
73             this.onEnter(event);
74         }
75     }
76
77     /**
78      * Get an array of focusable elements within the current containers.
79      * @returns {Element[]}
80      */
81     #getFocusable() {
82         const focusable = [];
83         const selector = '[tabindex]:not([tabindex="-1"]),[href],button:not([tabindex="-1"]),input:not([type=hidden])';
84         for (const container of this.containers) {
85             focusable.push(...container.querySelectorAll(selector))
86         }
87         return focusable;
88     }
89 }