X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/5f07f31c9fc21c4f82b757eb2e78027ff3ad6337..d13abc7e1d9c204640fe9a6624a162a7cc106de6:/resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts diff --git a/resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts b/resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts index ccced6858..890d5b325 100644 --- a/resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts +++ b/resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts @@ -34,57 +34,104 @@ function positionMenu(menu: HTMLElement, toggle: HTMLElement, showAside: boolean } } -export function handleDropdown(options: HandleDropdownParams) { - const {menu, toggle, onClose, onOpen, showOnHover, showAside} = options; - let clickListener: Function|null = null; +export class DropDownManager { - const hide = () => { + protected dropdownOptions: WeakMap = new WeakMap(); + protected openDropdowns: Set = new Set(); + + constructor() { + this.onMenuMouseOver = this.onMenuMouseOver.bind(this); + this.onWindowClick = this.onWindowClick.bind(this); + + window.addEventListener('click', this.onWindowClick); + } + + teardown(): void { + window.removeEventListener('click', this.onWindowClick); + } + + protected onWindowClick(event: MouseEvent): void { + 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 { menu.hidden = true; menu.style.removeProperty('position'); menu.style.removeProperty('left'); menu.style.removeProperty('top'); - if (clickListener) { - window.removeEventListener('click', clickListener as EventListener); - } + + this.openDropdowns.delete(menu); + menu.removeEventListener('mouseover', this.onMenuMouseOver); + + const onClose = this.getOptions(menu).onClose; if (onClose) { onClose(); } - }; + } - const show = () => { + protected openDropdown(menu: HTMLElement): void { + const {toggle, showAside, onOpen} = this.getOptions(menu); menu.hidden = false positionMenu(menu, toggle, Boolean(showAside)); - clickListener = (event: MouseEvent) => { - if (!toggle.contains(event.target as HTMLElement) && !menu.contains(event.target as HTMLElement)) { - hide(); - } - } - window.addEventListener('click', clickListener as EventListener); + + this.openDropdowns.add(menu); + menu.addEventListener('mouseover', this.onMenuMouseOver); + if (onOpen) { onOpen(); } - }; - - const toggleShowing = (event: MouseEvent) => { - menu.hasAttribute('hidden') ? show() : hide(); - }; - toggle.addEventListener('click', toggleShowing); - if (showOnHover) { - toggle.addEventListener('mouseenter', toggleShowing); } - menu.parentElement?.addEventListener('mouseleave', (event: MouseEvent) => { + protected getOptions(menu: HTMLElement): HandleDropdownParams { + const options = this.dropdownOptions.get(menu); + if (!options) { + throw new Error(`Can't find options for dropdown menu`); + } - // Prevent mouseleave hiding if withing the same bounds of the toggle. - // Avoids hiding in the event the mouse is interrupted by a high z-index - // item like a browser scrollbar. - const toggleBounds = toggle.getBoundingClientRect(); - const withinX = event.clientX <= toggleBounds.right && event.clientX >= toggleBounds.left; - const withinY = event.clientY <= toggleBounds.bottom && event.clientY >= toggleBounds.top; - const withinToggle = withinX && withinY; + return options; + } - if (!withinToggle) { - hide(); + /** + * Add handling for a new dropdown. + */ + public handle(options: HandleDropdownParams) { + const {menu, toggle, showOnHover} = options; + + // Register dropdown + this.dropdownOptions.set(menu, options); + + // 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); + }); } - }); + } } \ No newline at end of file