]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts
Comments: Fixed tab focus change & button placement on form usage
[bookstack] / resources / js / wysiwyg / ui / framework / helpers / dropdowns.ts
1 interface HandleDropdownParams {
2     toggle: HTMLElement;
3     menu: HTMLElement;
4     showOnHover?: boolean,
5     onOpen?: Function | undefined;
6     onClose?: Function | undefined;
7     showAside?: boolean;
8 }
9
10 function positionMenu(menu: HTMLElement, toggle: HTMLElement, showAside: boolean) {
11     const toggleRect = toggle.getBoundingClientRect();
12     const menuBounds = menu.getBoundingClientRect();
13
14     menu.style.position = 'fixed';
15
16     if (showAside) {
17         let targetLeft = toggleRect.right;
18         const isRightOOB = toggleRect.right + menuBounds.width > window.innerWidth;
19         if (isRightOOB) {
20             targetLeft = Math.max(toggleRect.left - menuBounds.width, 0);
21         }
22
23         menu.style.top = toggleRect.top + 'px';
24         menu.style.left = targetLeft + 'px';
25     } else {
26         const isRightOOB = toggleRect.left + menuBounds.width > window.innerWidth;
27         let targetLeft = toggleRect.left;
28         if (isRightOOB) {
29             targetLeft = Math.max(toggleRect.right - menuBounds.width, 0);
30         }
31
32         menu.style.top = toggleRect.bottom + 'px';
33         menu.style.left = targetLeft + 'px';
34     }
35 }
36
37 export function handleDropdown(options: HandleDropdownParams) {
38     const {menu, toggle, onClose, onOpen, showOnHover, showAside} = options;
39     let clickListener: Function|null = null;
40
41     const hide = () => {
42         menu.hidden = true;
43         menu.style.removeProperty('position');
44         menu.style.removeProperty('left');
45         menu.style.removeProperty('top');
46         if (clickListener) {
47             window.removeEventListener('click', clickListener as EventListener);
48         }
49         if (onClose) {
50             onClose();
51         }
52     };
53
54     const show = () => {
55         menu.hidden = false
56         positionMenu(menu, toggle, Boolean(showAside));
57         clickListener = (event: MouseEvent) => {
58             if (!toggle.contains(event.target as HTMLElement) && !menu.contains(event.target as HTMLElement)) {
59                 hide();
60             }
61         }
62         window.addEventListener('click', clickListener as EventListener);
63         if (onOpen) {
64             onOpen();
65         }
66     };
67
68     const toggleShowing = (event: MouseEvent) => {
69         menu.hasAttribute('hidden') ? show() : hide();
70     };
71     toggle.addEventListener('click', toggleShowing);
72     if (showOnHover) {
73         toggle.addEventListener('mouseenter', toggleShowing);
74     }
75
76     menu.parentElement?.addEventListener('mouseleave', (event: MouseEvent) => {
77
78         // Prevent mouseleave hiding if withing the same bounds of the toggle.
79         // Avoids hiding in the event the mouse is interrupted by a high z-index
80         // item like a browser scrollbar.
81         const toggleBounds = toggle.getBoundingClientRect();
82         const withinX = event.clientX <= toggleBounds.right && event.clientX >= toggleBounds.left;
83         const withinY = event.clientY <= toggleBounds.bottom && event.clientY >= toggleBounds.top;
84         const withinToggle = withinX && withinY;
85
86         if (!withinToggle) {
87             hide();
88         }
89     });
90 }