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