]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts
Opensearch: Fixed XML declaration when php short tags enabled
[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 class DropDownManager {
38
39     protected dropdownOptions: WeakMap<HTMLElement, HandleDropdownParams> = new WeakMap();
40     protected openDropdowns: Set<HTMLElement> = new Set();
41
42     constructor() {
43         this.onMenuMouseOver = this.onMenuMouseOver.bind(this);
44
45         window.addEventListener('click', (event: MouseEvent) => {
46             const target = event.target as HTMLElement;
47             this.closeAllNotContainingElement(target);
48         });
49     }
50
51     protected closeAllNotContainingElement(element: HTMLElement): void {
52         for (const menu of this.openDropdowns) {
53             if (!menu.parentElement?.contains(element)) {
54                 this.closeDropdown(menu);
55             }
56         }
57     }
58
59     protected onMenuMouseOver(event: MouseEvent): void {
60         const target = event.target as HTMLElement;
61         this.closeAllNotContainingElement(target);
62     }
63
64     /**
65      * Close all open dropdowns.
66      */
67     public closeAll(): void {
68         for (const menu of this.openDropdowns) {
69             this.closeDropdown(menu);
70         }
71     }
72
73     protected closeDropdown(menu: HTMLElement): void {
74         menu.hidden = true;
75         menu.style.removeProperty('position');
76         menu.style.removeProperty('left');
77         menu.style.removeProperty('top');
78
79         this.openDropdowns.delete(menu);
80         menu.removeEventListener('mouseover', this.onMenuMouseOver);
81
82         const onClose = this.getOptions(menu).onClose;
83         if (onClose) {
84             onClose();
85         }
86     }
87
88     protected openDropdown(menu: HTMLElement): void {
89         const {toggle, showAside, onOpen} = this.getOptions(menu);
90         menu.hidden = false
91         positionMenu(menu, toggle, Boolean(showAside));
92
93         this.openDropdowns.add(menu);
94         menu.addEventListener('mouseover', this.onMenuMouseOver);
95
96         if (onOpen) {
97             onOpen();
98         }
99     }
100
101     protected getOptions(menu: HTMLElement): HandleDropdownParams {
102         const options = this.dropdownOptions.get(menu);
103         if (!options) {
104             throw new Error(`Can't find options for dropdown menu`);
105         }
106
107         return options;
108     }
109
110     /**
111      * Add handling for a new dropdown.
112      */
113      public handle(options: HandleDropdownParams) {
114         const {menu, toggle, showOnHover} = options;
115
116         // Register dropdown
117         this.dropdownOptions.set(menu, options);
118
119         // Configure default events
120         const toggleShowing = (event: MouseEvent) => {
121             menu.hasAttribute('hidden') ? this.openDropdown(menu) : this.closeDropdown(menu);
122         };
123         toggle.addEventListener('click', toggleShowing);
124         if (showOnHover) {
125             toggle.addEventListener('mouseenter', () => {
126                 this.openDropdown(menu);
127             });
128         }
129     }
130 }