budibase/packages/bbui/src/Actions/position_dropdown.js

129 lines
3.3 KiB
JavaScript

export default function positionDropdown(element, opts) {
let resizeObserver
let latestOpts = opts
// We need a static reference to this function so that we can properly
// clean up the scroll listener.
const scrollUpdate = () => {
updatePosition(latestOpts)
}
// Updates the position of the dropdown
const updatePosition = opts => {
const {
anchor,
align,
maxHeight,
maxWidth,
useAnchorWidth,
offset = 5,
customUpdate,
offsetBelow,
} = opts
if (!anchor) {
return
}
// Compute bounds
const anchorBounds = anchor.getBoundingClientRect()
const elementBounds = element.getBoundingClientRect()
let styles = {
maxHeight: null,
minWidth: null,
maxWidth,
left: null,
top: null,
}
if (typeof customUpdate === "function") {
styles = customUpdate(anchorBounds, elementBounds, styles)
} else {
// Determine vertical styles
if (align === "right-outside") {
styles.top = anchorBounds.top
} else if (
window.innerHeight - anchorBounds.bottom <
(maxHeight || 100)
) {
styles.top = anchorBounds.top - elementBounds.height - offset
styles.maxHeight = maxHeight || 240
} else {
styles.top = anchorBounds.bottom + (offsetBelow || offset)
styles.maxHeight =
maxHeight || window.innerHeight - anchorBounds.bottom - 20
}
// Determine horizontal styles
if (!maxWidth && useAnchorWidth) {
styles.maxWidth = anchorBounds.width
}
if (useAnchorWidth) {
styles.minWidth = anchorBounds.width
}
if (align === "right") {
styles.left =
anchorBounds.left + anchorBounds.width - elementBounds.width
} else if (align === "right-outside") {
styles.left = anchorBounds.right + offset
} else if (align === "left-outside") {
styles.left = anchorBounds.left - elementBounds.width - offset
} else {
styles.left = anchorBounds.left
}
}
// Apply styles
Object.entries(styles).forEach(([style, value]) => {
if (value != null) {
element.style[style] = `${value.toFixed(0)}px`
} else {
element.style[style] = null
}
})
}
// The actual svelte action callback which creates observers on the relevant
// DOM elements
const update = newOpts => {
latestOpts = newOpts
// Cleanup old state
if (resizeObserver) {
resizeObserver.disconnect()
}
// Do nothing if no anchor
const { anchor } = newOpts
if (!anchor) {
return
}
// Observe both anchor and element and resize the popover as appropriate
resizeObserver = new ResizeObserver(() => updatePosition(newOpts))
resizeObserver.observe(anchor)
resizeObserver.observe(element)
resizeObserver.observe(document.body)
}
// Apply initial styles which don't need to change
element.style.position = "absolute"
element.style.zIndex = "9999"
// Set up a scroll listener
document.addEventListener("scroll", scrollUpdate, true)
// Perform initial update
update(opts)
return {
update,
destroy() {
// Cleanup
if (resizeObserver) {
resizeObserver.disconnect()
}
document.removeEventListener("scroll", scrollUpdate, true)
},
}
}