UNPKG

webcoreui

Version:

UI component and template library for Astro, Svelte, and React apps styled with Sass.

250 lines (211 loc) 7.32 kB
import { debounce } from './debounce' import { closeModal } from './modal' export type PopoverPosition = 'top' | 'top-start' | 'top-end' | 'left' | 'left-start' | 'left-end' | 'right' | 'right-start' | 'right-end' | 'bottom' | 'bottom-start' | 'bottom-end' export type PopoverCallback = { trigger: HTMLElement popover: HTMLElement position: PopoverPosition | undefined } export type Popover = { trigger: string popover: string position?: PopoverPosition offset?: number closeOnBlur?: boolean closeOnEsc?: boolean onOpen?: (args: PopoverCallback) => unknown onClose?: (args: PopoverCallback) => unknown } export const popover = ({ trigger, popover, position, offset = 10, closeOnBlur = true, closeOnEsc = true, onOpen, onClose }: Popover) => { const triggerDOM = document.querySelector(trigger) as HTMLElement const popoverDOM = document.querySelector(popover) as HTMLElement if (triggerDOM && popoverDOM) { document.body.appendChild(popoverDOM) if (position) { popoverDOM.dataset.position = position.split('-')[0] } const handleOpen = () => { const triggerPosition = triggerDOM.getBoundingClientRect() const popoverPosition = popoverDOM.getBoundingClientRect() const topSide = triggerPosition.top + window.scrollY - popoverPosition.height - offset const bottomSide = triggerPosition.top + window.scrollY + triggerPosition.height + offset const leftSide = triggerPosition.left - popoverPosition.width - offset const rightSide = triggerPosition.left + triggerPosition.width + offset const hcenter = triggerPosition.left - ((popoverPosition.width - triggerPosition.width) / 2) const hEnd = triggerPosition.left - (popoverPosition.width - triggerPosition.width) const vcenter = triggerPosition.top + window.scrollY - ((popoverPosition.height - triggerPosition.height) / 2) const vStart = triggerPosition.top + window.scrollY const vEnd = triggerPosition.top + window.scrollY - (popoverPosition.height - triggerPosition.height) const positions = { 'top-start': { top: topSide, left: triggerPosition.left }, 'top': { top: topSide, left: hcenter }, 'top-end': { top: topSide, left: hEnd }, 'left-start': { top: vStart, left: leftSide }, 'left': { top: vcenter, left: leftSide }, 'left-end': { top: vEnd, left: leftSide }, 'right-start': { top: vStart, left: rightSide }, 'right': { top: vcenter, left: rightSide }, 'right-end': { top: vEnd, left: rightSide }, 'bottom-start': { top: bottomSide, left: triggerPosition.left }, 'bottom': { top: bottomSide, left: hcenter }, 'bottom-end': { top: bottomSide, left: hEnd } } const { top, left } = positions[position as keyof typeof positions] || positions.bottom popoverDOM.style.top = `${top}px` popoverDOM.style.left = `${left}px` setTimeout(() => { popoverDOM.dataset.show = popoverDOM.dataset.show === 'true' ? '' : 'true' }, 0) setTimeout(() => { if (!popoverDOM.dataset.show) { popoverDOM.removeAttribute('data-show') } }, 300) onOpen?.({ trigger: triggerDOM, popover: popoverDOM, position }) } const handleClose = (event: MouseEvent) => { const target = event.target as HTMLElement const hidePopover = !popoverDOM.contains(target) && !triggerDOM.contains(target) && popoverDOM.dataset.show if (hidePopover) { popoverDOM.dataset.show = '' onClose?.({ trigger: triggerDOM, popover: popoverDOM, position }) } setTimeout(() => { if (!popoverDOM.dataset.show) { popoverDOM.removeAttribute('data-show') } }, 300) } const handleCloseOnEsc = (event: KeyboardEvent) => { if (event.key === 'Escape' && popoverDOM.dataset.show) { popoverDOM.dataset.show = '' onClose?.({ trigger: triggerDOM, popover: popoverDOM, position }) setTimeout(() => { popoverDOM.removeAttribute('data-show') }, 300) } } const removeOnResize = debounce(() => { popoverDOM.dataset.show = '' setTimeout(() => { if (!popoverDOM.dataset.show) { popoverDOM.removeAttribute('data-show') } }, 300) }) const observer = new ResizeObserver(() => { if (popoverDOM.dataset.show) { removeOnResize() } }) triggerDOM.addEventListener('click', handleOpen) observer.observe(document.body) if (closeOnBlur) { document.addEventListener('click', handleClose) } if (closeOnEsc) { document.addEventListener('keydown', handleCloseOnEsc) } return { remove() { triggerDOM.removeEventListener('click', handleOpen) observer.disconnect() if (closeOnBlur) { document.removeEventListener('click', handleClose) } if (closeOnEsc) { document.removeEventListener('keydown', handleCloseOnEsc) } } } } } export const closePopover = closeModal