2024-07-04 23:16:16 +08:00
|
|
|
interface HandleDropdownParams {
|
|
|
|
toggle: HTMLElement;
|
|
|
|
menu: HTMLElement;
|
|
|
|
showOnHover?: boolean,
|
|
|
|
onOpen?: Function | undefined;
|
|
|
|
onClose?: Function | undefined;
|
2024-12-15 22:03:08 +08:00
|
|
|
showAside?: boolean;
|
|
|
|
}
|
|
|
|
|
|
|
|
function positionMenu(menu: HTMLElement, toggle: HTMLElement, showAside: boolean) {
|
|
|
|
const toggleRect = toggle.getBoundingClientRect();
|
|
|
|
const menuBounds = menu.getBoundingClientRect();
|
|
|
|
|
|
|
|
menu.style.position = 'fixed';
|
|
|
|
|
|
|
|
if (showAside) {
|
|
|
|
let targetLeft = toggleRect.right;
|
|
|
|
const isRightOOB = toggleRect.right + menuBounds.width > window.innerWidth;
|
|
|
|
if (isRightOOB) {
|
|
|
|
targetLeft = Math.max(toggleRect.left - menuBounds.width, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
menu.style.top = toggleRect.top + 'px';
|
|
|
|
menu.style.left = targetLeft + 'px';
|
|
|
|
} else {
|
|
|
|
const isRightOOB = toggleRect.left + menuBounds.width > window.innerWidth;
|
|
|
|
let targetLeft = toggleRect.left;
|
|
|
|
if (isRightOOB) {
|
|
|
|
targetLeft = Math.max(toggleRect.right - menuBounds.width, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
menu.style.top = toggleRect.bottom + 'px';
|
|
|
|
menu.style.left = targetLeft + 'px';
|
|
|
|
}
|
2024-07-04 23:16:16 +08:00
|
|
|
}
|
|
|
|
|
2025-05-25 23:28:42 +08:00
|
|
|
export class DropDownManager {
|
2024-06-13 02:51:42 +08:00
|
|
|
|
2025-05-25 23:28:42 +08:00
|
|
|
protected dropdownOptions: WeakMap<HTMLElement, HandleDropdownParams> = new WeakMap();
|
|
|
|
protected openDropdowns: Set<HTMLElement> = new Set();
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
this.onMenuMouseOver = this.onMenuMouseOver.bind(this);
|
|
|
|
|
|
|
|
window.addEventListener('click', (event: MouseEvent) => {
|
|
|
|
const target = event.target as HTMLElement;
|
|
|
|
this.closeAllNotContainingElement(target);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
protected closeAllNotContainingElement(element: HTMLElement): void {
|
|
|
|
for (const menu of this.openDropdowns) {
|
|
|
|
if (!menu.parentElement?.contains(element)) {
|
|
|
|
this.closeDropdown(menu);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected onMenuMouseOver(event: MouseEvent): void {
|
|
|
|
const target = event.target as HTMLElement;
|
|
|
|
this.closeAllNotContainingElement(target);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Close all open dropdowns.
|
|
|
|
*/
|
|
|
|
public closeAll(): void {
|
|
|
|
for (const menu of this.openDropdowns) {
|
|
|
|
this.closeDropdown(menu);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected closeDropdown(menu: HTMLElement): void {
|
2024-06-13 02:51:42 +08:00
|
|
|
menu.hidden = true;
|
2024-12-15 22:03:08 +08:00
|
|
|
menu.style.removeProperty('position');
|
|
|
|
menu.style.removeProperty('left');
|
|
|
|
menu.style.removeProperty('top');
|
2025-05-25 23:28:42 +08:00
|
|
|
|
|
|
|
this.openDropdowns.delete(menu);
|
|
|
|
menu.removeEventListener('mouseover', this.onMenuMouseOver);
|
|
|
|
|
|
|
|
const onClose = this.getOptions(menu).onClose;
|
2024-06-13 02:51:42 +08:00
|
|
|
if (onClose) {
|
|
|
|
onClose();
|
|
|
|
}
|
2025-05-25 23:28:42 +08:00
|
|
|
}
|
2024-06-13 02:51:42 +08:00
|
|
|
|
2025-05-25 23:28:42 +08:00
|
|
|
protected openDropdown(menu: HTMLElement): void {
|
|
|
|
const {toggle, showAside, onOpen} = this.getOptions(menu);
|
2024-06-13 02:51:42 +08:00
|
|
|
menu.hidden = false
|
2024-12-15 22:03:08 +08:00
|
|
|
positionMenu(menu, toggle, Boolean(showAside));
|
2025-05-25 23:28:42 +08:00
|
|
|
|
|
|
|
this.openDropdowns.add(menu);
|
|
|
|
menu.addEventListener('mouseover', this.onMenuMouseOver);
|
|
|
|
|
2024-06-13 02:51:42 +08:00
|
|
|
if (onOpen) {
|
|
|
|
onOpen();
|
|
|
|
}
|
2024-07-04 23:16:16 +08:00
|
|
|
}
|
|
|
|
|
2025-05-25 23:28:42 +08:00
|
|
|
protected getOptions(menu: HTMLElement): HandleDropdownParams {
|
|
|
|
const options = this.dropdownOptions.get(menu);
|
|
|
|
if (!options) {
|
|
|
|
throw new Error(`Can't find options for dropdown menu`);
|
|
|
|
}
|
|
|
|
|
|
|
|
return options;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add handling for a new dropdown.
|
|
|
|
*/
|
|
|
|
public handle(options: HandleDropdownParams) {
|
|
|
|
const {menu, toggle, showOnHover} = options;
|
2024-12-15 22:03:08 +08:00
|
|
|
|
2025-05-25 23:28:42 +08:00
|
|
|
// Register dropdown
|
|
|
|
this.dropdownOptions.set(menu, options);
|
2024-12-15 22:03:08 +08:00
|
|
|
|
2025-05-25 23:28:42 +08:00
|
|
|
// Configure default events
|
|
|
|
const toggleShowing = (event: MouseEvent) => {
|
|
|
|
menu.hasAttribute('hidden') ? this.openDropdown(menu) : this.closeDropdown(menu);
|
|
|
|
};
|
|
|
|
toggle.addEventListener('click', toggleShowing);
|
|
|
|
if (showOnHover) {
|
|
|
|
toggle.addEventListener('mouseenter', () => {
|
|
|
|
this.openDropdown(menu);
|
|
|
|
});
|
2024-12-15 22:03:08 +08:00
|
|
|
}
|
2025-05-25 23:28:42 +08:00
|
|
|
}
|
2024-06-13 02:51:42 +08:00
|
|
|
}
|