X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/9e43e03db4fbbb95f0219124162c9318c9c31531..c8cfec96dc11a3adaed7f7c3545ca35faa5deab3:/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 35886d2f9..890d5b325 100644 --- a/resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts +++ b/resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts @@ -1,34 +1,137 @@ +interface HandleDropdownParams { + toggle: HTMLElement; + menu: HTMLElement; + showOnHover?: boolean, + onOpen?: Function | undefined; + onClose?: Function | undefined; + showAside?: boolean; +} +function positionMenu(menu: HTMLElement, toggle: HTMLElement, showAside: boolean) { + const toggleRect = toggle.getBoundingClientRect(); + const menuBounds = menu.getBoundingClientRect(); + menu.style.position = 'fixed'; -export function handleDropdown(toggle: HTMLElement, menu: HTMLElement, onOpen: Function|undefined = undefined, onClose: Function|undefined = undefined) { - let clickListener: Function|null = null; + if (showAside) { + let targetLeft = toggleRect.right; + const isRightOOB = toggleRect.right + menuBounds.width > window.innerWidth; + if (isRightOOB) { + targetLeft = Math.max(toggleRect.left - menuBounds.width, 0); + } - const hide = () => { - menu.hidden = true; - if (clickListener) { - window.removeEventListener('click', clickListener as EventListener); + 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(); } - }; + } + + 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; + } - toggle.addEventListener('click', event => { - menu.hasAttribute('hidden') ? show() : hide(); - }); - 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