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