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