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