webcoreui
Version:
UI component and template library for Astro, Svelte, and React apps styled with Sass.
250 lines (211 loc) • 7.32 kB
text/typescript
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