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