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