Initial Commit
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
// 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;
|
||||
Reference in New Issue
Block a user