X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/2c96af9aeafe2d4943a76cd69679ee7dcec737a3..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 45c3f39d1..890d5b325 100644 --- a/resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts +++ b/resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts @@ -1,48 +1,137 @@ - - - interface HandleDropdownParams { toggle: HTMLElement; menu: HTMLElement; showOnHover?: boolean, onOpen?: Function | undefined; onClose?: Function | undefined; + showAside?: boolean; } -export function handleDropdown(options: HandleDropdownParams) { - const {menu, toggle, onClose, onOpen, showOnHover} = options; - let clickListener: Function|null = null; +function positionMenu(menu: HTMLElement, toggle: HTMLElement, showAside: boolean) { + const toggleRect = toggle.getBoundingClientRect(); + const menuBounds = menu.getBoundingClientRect(); - const hide = () => { - menu.hidden = true; - if (clickListener) { - window.removeEventListener('click', clickListener as EventListener); + 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'; + } +} + +export class DropDownManager { + + 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'); + + 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 - clickListener = (event: MouseEvent) => { - if (!toggle.contains(event.target as HTMLElement) && !menu.contains(event.target as HTMLElement)) { - hide(); - } - } - window.addEventListener('click', clickListener as EventListener); + positionMenu(menu, toggle, Boolean(showAside)); + + 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); + 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; } - menu.addEventListener('mouseleave', 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