]> BookStack Code Mirror - bookstack/blob - resources/js/components/dropdown.js
Merge branch 'feature_change_view_in_shelves_show' of git://github.com/philjak/BookSt...
[bookstack] / resources / js / components / dropdown.js
1 import {onSelect} from "../services/dom";
2
3 /**
4  * Dropdown
5  * Provides some simple logic to create simple dropdown menus.
6  */
7 class DropDown {
8
9     constructor(elem) {
10         this.container = elem;
11         this.menu = elem.querySelector('.dropdown-menu, [dropdown-menu]');
12         this.moveMenu = elem.hasAttribute('dropdown-move-menu');
13         this.toggle = elem.querySelector('[dropdown-toggle]');
14         this.direction = (document.dir === 'rtl') ? 'right' : 'left';
15         this.body = document.body;
16         this.showing = false;
17         this.setupListeners();
18     }
19
20     show(event = null) {
21         this.hideAll();
22
23         this.menu.style.display = 'block';
24         this.menu.classList.add('anim', 'menuIn');
25         this.toggle.setAttribute('aria-expanded', 'true');
26
27         if (this.moveMenu) {
28             // Move to body to prevent being trapped within scrollable sections
29             this.rect = this.menu.getBoundingClientRect();
30             this.body.appendChild(this.menu);
31             this.menu.style.position = 'fixed';
32             if (this.direction === 'right') {
33                 this.menu.style.right = `${(this.rect.right - this.rect.width)}px`;
34             } else {
35                 this.menu.style.left = `${this.rect.left}px`;
36             }
37             this.menu.style.top = `${this.rect.top}px`;
38             this.menu.style.width = `${this.rect.width}px`;
39         }
40
41         // Set listener to hide on mouse leave or window click
42         this.menu.addEventListener('mouseleave', this.hide.bind(this));
43         window.addEventListener('click', event => {
44             if (!this.menu.contains(event.target)) {
45                 this.hide();
46             }
47         });
48
49         // Focus on first input if existing
50         const input = this.menu.querySelector('input');
51         if (input !== null) input.focus();
52
53         this.showing = true;
54
55         const showEvent = new Event('show');
56         this.container.dispatchEvent(showEvent);
57
58         if (event) {
59             event.stopPropagation();
60         }
61     }
62
63     hideAll() {
64         for (let dropdown of window.components.dropdown) {
65             dropdown.hide();
66         }
67     }
68
69     hide() {
70         this.menu.style.display = 'none';
71         this.menu.classList.remove('anim', 'menuIn');
72         this.toggle.setAttribute('aria-expanded', 'false');
73         if (this.moveMenu) {
74             this.menu.style.position = '';
75             this.menu.style[this.direction] = '';
76             this.menu.style.top = '';
77             this.menu.style.width = '';
78             this.container.appendChild(this.menu);
79         }
80         this.showing = false;
81     }
82
83     getFocusable() {
84         return Array.from(this.menu.querySelectorAll('[tabindex],[href],button,input:not([type=hidden])'));
85     }
86
87     focusNext() {
88         const focusable = this.getFocusable();
89         const currentIndex = focusable.indexOf(document.activeElement);
90         let newIndex = currentIndex + 1;
91         if (newIndex >= focusable.length) {
92             newIndex = 0;
93         }
94
95         focusable[newIndex].focus();
96     }
97
98     focusPrevious() {
99         const focusable = this.getFocusable();
100         const currentIndex = focusable.indexOf(document.activeElement);
101         let newIndex = currentIndex - 1;
102         if (newIndex < 0) {
103             newIndex = focusable.length - 1;
104         }
105
106         focusable[newIndex].focus();
107     }
108
109     setupListeners() {
110         // Hide menu on option click
111         this.container.addEventListener('click', event => {
112              const possibleChildren = Array.from(this.menu.querySelectorAll('a'));
113              if (possibleChildren.includes(event.target)) {
114                  this.hide();
115              }
116         });
117
118         onSelect(this.toggle, event => {
119             event.stopPropagation();
120             this.show(event);
121             if (event instanceof KeyboardEvent) {
122                 this.focusNext();
123             }
124         });
125
126         // Keyboard navigation
127         const keyboardNavigation = event => {
128             if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
129                 this.focusNext();
130                 event.preventDefault();
131             } else if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
132                 this.focusPrevious();
133                 event.preventDefault();
134             } else if (event.key === 'Escape') {
135                 this.hide();
136                 this.toggle.focus();
137                 event.stopPropagation();
138             }
139         };
140         this.container.addEventListener('keydown', keyboardNavigation);
141         if (this.moveMenu) {
142             this.menu.addEventListener('keydown', keyboardNavigation);
143         }
144
145         // Hide menu on enter press or escape
146         this.menu.addEventListener('keydown ', event => {
147             if (event.key === 'Enter') {
148                 event.preventDefault();
149                 event.stopPropagation();
150                 this.hide();
151             }
152         });
153     }
154
155 }
156
157 export default DropDown;