@csedl/stimulus-dropdown
Version:
Dropdown and Tooltip with stimulus and floating-ui
106 lines (91 loc) • 3.7 kB
JavaScript
import {arrow, computePosition, flip, offset, shift} from "@floating-ui/dom";
import {debugLog} from "./utils.js";
// Positions a panel relative to its trigger button
export function positionPanelByButton(button, logContext) {
const toggleId = button.getAttribute('data-panel-id')
let panel = document.getElementById(toggleId)
positionPanel(button, panel)
debugLog('panel positioned', logContext)
}
// Positions a panel using its event listener
export function positionPanelSelf(panel) {
let button = document.querySelector(`[data-panel-id="${panel.id}"]`)
positionPanel(button, panel)
}
// Retrieves all currently open panels within a scope element
export function getAllOpenPanels(scopeElement) {
const buttons = scopeElement.getElementsByClassName('has-open-panel')
const elements = []
for (const button of buttons) {
if (button.getAttribute('data-controller') === 'csedl-dropdown') {
const getOrSetPanelId = button.getAttribute('data-panel-id')
const panel = document.getElementById(getOrSetPanelId)
elements.push([button, panel])
}
}
debugLog(`FOUND ${elements.length} OPEN DROPDOWNS`)
return (elements)
}
// Positions all provided panel-button pairs
export function positionAllPanels(elements) {
for (const el of elements) {
positionPanel(el[0], el[1])
}
}
// Positions a single panel relative to its button
export function positionPanel(button, panel) {
let arrowElement = panel.querySelector('#arrow')
panel.style.removeProperty('height')
panel.style.removeProperty('left')
panel.style.removeProperty('top')
let placementAttr = panel.getAttribute('data-placement')
let placement = (placementAttr ? placementAttr : 'bottom')
debugLog(`placement: «${placement}»`)
computePosition(button, panel, {
middleware: [flip(), offset(6), shift({padding: 5}), arrow({element: arrowElement})],
placement: placement
}).then(({x, y, placement, middlewareData}) => {
Object.assign(panel.style, {
left: `${x}px`,
top: `${y}px`,
});
if (arrowElement) {
// ARROW
const {x: arrowX, y: arrowY} = middlewareData.arrow;
const staticSide = {
top: 'bottom',
right: 'left',
bottom: 'top',
left: 'right',
}[placement.split('-')[0]];
Object.assign(arrowElement.style, {
left: arrowX != null ? `${arrowX}px` : '',
top: arrowY != null ? `${arrowY}px` : '',
right: '',
bottom: '',
[staticSide]: '-4px',
});
debugLog(`panel + arrow positioned`)
} else {
debugLog(`panel positioned`)
}
adjustToWindowBounds(button, panel)
});
}
// Adjusts panel to prevent overflow beyond window boundaries
function adjustToWindowBounds(button, panel) {
if (button.getAttribute('data-constrain-to-window-borders') === 'true') {
const rect = panel.getBoundingClientRect();
if (rect.top < 0) {
const newTop = parseFloat(panel.style.top) + Math.abs(rect.top) + 2
console.log('constrain top', 'actual top:', panel.style.top, 'absolute top:', rect.top, 'new top:', newTop)
panel.style.top = `${newTop}px`
}
const rect2 = panel.getBoundingClientRect();
if (rect2.bottom > window.innerHeight) {
console.log('constrain height')
const height = window.innerHeight - rect2.top - 2
panel.style.height = `${height}px`
}
}
}