Files
Rivertalk/assets/js/ui/core/positioner.js
T
2026-02-12 17:10:03 -06:00

239 lines
6.2 KiB
JavaScript

// saladui/core/positioner.js - Updated to use fixed positioning
/**
* Core Positioning utility for SaladUI components
* Handles pure positioning calculations without side effects
*/
class Positioner {
/**
* Calculate position for an element relative to a reference element
*
* @param {HTMLElement} element - The element to position
* @param {HTMLElement} reference - The reference element to position against
* @param {Object} options - Positioning options
* @returns {Object} The computed position data
*/
static calculate(element, reference, options = {}) {
const {
placement = "bottom",
alignment = "center",
container = document.body,
flip = true,
alignOffset = 0,
sideOffset = 8,
} = options;
// Get element and reference rects for positioning
const referenceRect = reference.getBoundingClientRect();
const elementRect = {
width: element.offsetWidth,
height: element.offsetHeight,
};
// Find container bounds
let containerRect;
if (container === document.body) {
containerRect = {
top: 0,
right: window.innerWidth,
bottom: window.innerHeight,
left: 0,
width: window.innerWidth,
height: window.innerHeight,
};
} else {
containerRect = container.getBoundingClientRect();
}
// Calculate initial position
let { x, y } = this.getBasePosition(
placement,
alignment,
elementRect,
referenceRect,
alignOffset,
sideOffset,
);
// Apply flipping if needed
let actualPlacement = placement;
if (flip) {
const flippedPlacement = this.getFlippedPlacement(
placement,
{ x, y, width: elementRect.width, height: elementRect.height },
containerRect,
);
if (flippedPlacement !== placement) {
actualPlacement = flippedPlacement;
const flippedPosition = this.getBasePosition(
flippedPlacement,
alignment,
elementRect,
referenceRect,
alignOffset,
sideOffset,
);
x = flippedPosition.x;
y = flippedPosition.y;
}
}
return {
x,
y,
placement: actualPlacement,
};
}
/**
* Apply position to an element
* @param {HTMLElement} element - Element to position
* @param {number} x - X coordinate
* @param {number} y - Y coordinate
*/
static applyPosition(element, x, y) {
element.style.position = "fixed";
// element.style.transform = `translate(${x}px, ${y}px)`;
element.style.top = y + "px";
element.style.left = x + "px";
element.style.margin = "0"; // Reset margins to avoid positioning issues
}
/**
* Calculate base position based on placement and alignment
*/
static getBasePosition(
placement,
alignment,
elementRect,
referenceRect,
alignOffset = 0,
sideOffset = 8,
) {
let x = 0;
let y = 0;
// Position based on placement
switch (placement) {
case "top":
y = referenceRect.top - elementRect.height - sideOffset;
break;
case "right":
x = referenceRect.right + sideOffset;
y = referenceRect.top;
break;
case "bottom":
y = referenceRect.bottom + sideOffset;
break;
case "left":
x = referenceRect.left - elementRect.width - sideOffset;
y = referenceRect.top;
break;
}
// Adjust based on alignment
switch (alignment) {
case "start":
if (placement === "top" || placement === "bottom") {
x = referenceRect.left + alignOffset;
} else {
y = referenceRect.top + alignOffset;
}
break;
case "center":
if (placement === "top" || placement === "bottom") {
x =
referenceRect.left +
referenceRect.width / 2 -
elementRect.width / 2 +
alignOffset;
} else {
y =
referenceRect.top +
referenceRect.height / 2 -
elementRect.height / 2 +
alignOffset;
}
break;
case "end":
if (placement === "top" || placement === "bottom") {
x = referenceRect.right - elementRect.width + alignOffset;
} else {
y = referenceRect.bottom - elementRect.height + alignOffset;
}
break;
}
return { x, y };
}
/**
* Determine if placement should be flipped due to lack of space
*/
static getFlippedPlacement(placement, elementCoords, containerRect) {
const { x, y, width, height } = elementCoords;
// Check if element would overflow container
const overflowTop = y < containerRect.top;
const overflowRight = x + width > containerRect.right;
const overflowBottom = y + height > containerRect.bottom;
const overflowLeft = x < containerRect.left;
// Determine if we need to flip
switch (placement) {
case "top":
if (overflowTop && !overflowBottom) {
return "bottom";
}
break;
case "right":
if (overflowRight && !overflowLeft) {
return "left";
}
break;
case "bottom":
if (overflowBottom && !overflowTop) {
return "top";
}
break;
case "left":
if (overflowLeft && !overflowRight) {
return "right";
}
break;
}
return placement;
}
/**
* Utility method to find all scrollable parent elements
*/
static findScrollableParents(element) {
const scrollableParents = [];
let currentElement = element;
while (currentElement && currentElement !== document.body) {
const style = window.getComputedStyle(currentElement);
if (
style.overflow === "auto" ||
style.overflow === "scroll" ||
style.overflowX === "auto" ||
style.overflowX === "scroll" ||
style.overflowY === "auto" ||
style.overflowY === "scroll"
) {
scrollableParents.push(currentElement);
}
currentElement = currentElement.parentElement;
}
// Always include window for global scrolling
scrollableParents.push(window);
return scrollableParents;
}
}
export default Positioner;