mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-25 04:37:13 -05:00
242 lines
4.9 KiB
TypeScript
242 lines
4.9 KiB
TypeScript
export type Side = "top" | "bottom" | "left" | "right";
|
|
export type Align = "start" | "center" | "end";
|
|
|
|
export type Placement =
|
|
| "top"
|
|
| "bottom"
|
|
| "right"
|
|
| "bottom start"
|
|
| "bottom end";
|
|
|
|
export interface Size {
|
|
width: number;
|
|
height: number;
|
|
}
|
|
|
|
export interface Rect extends Size {
|
|
top: number;
|
|
left: number;
|
|
}
|
|
|
|
export interface Bounds {
|
|
top: number;
|
|
right: number;
|
|
bottom: number;
|
|
left: number;
|
|
}
|
|
|
|
export interface Resolution {
|
|
side: Side;
|
|
align: Align;
|
|
availableWidth: number;
|
|
availableHeight: number;
|
|
transformOrigin: string;
|
|
}
|
|
|
|
export interface Insets {
|
|
top: number | null;
|
|
right: number | null;
|
|
bottom: number | null;
|
|
left: number | null;
|
|
}
|
|
|
|
const OPPOSITE_SIDE: Record<Side, Side> = {
|
|
top: "bottom",
|
|
bottom: "top",
|
|
left: "right",
|
|
right: "left",
|
|
};
|
|
|
|
const ALIGN_ORIGIN: Record<Align, string> = {
|
|
start: "0%",
|
|
center: "50%",
|
|
end: "100%",
|
|
};
|
|
|
|
export function resolve({
|
|
anchor,
|
|
floating,
|
|
bounds,
|
|
placement,
|
|
gap,
|
|
padding,
|
|
rtl,
|
|
}: {
|
|
anchor: Rect;
|
|
floating: Size;
|
|
bounds: Bounds;
|
|
placement: Placement;
|
|
gap: number;
|
|
padding: number;
|
|
rtl: boolean;
|
|
}): Resolution {
|
|
const { side: preferred, align } = parsePlacement(placement);
|
|
|
|
const space = spaceAround(anchor, bounds, gap, padding);
|
|
const opposite = OPPOSITE_SIDE[preferred];
|
|
const needed = isVertical(preferred) ? floating.height : floating.width;
|
|
const across = spaceAcross(bounds, placement, padding);
|
|
|
|
const side =
|
|
needed <= space[preferred] || space[preferred] >= space[opposite]
|
|
? preferred
|
|
: opposite;
|
|
|
|
return {
|
|
side,
|
|
align,
|
|
availableWidth: Math.max(0, isVertical(side) ? across : space[side]),
|
|
availableHeight: Math.max(0, isVertical(side) ? space[side] : across),
|
|
transformOrigin: transformOrigin(side, align, rtl),
|
|
};
|
|
}
|
|
|
|
export function spaceAcross(
|
|
bounds: Bounds,
|
|
placement: Placement,
|
|
padding: number,
|
|
) {
|
|
const size = isVerticalPlacement(placement)
|
|
? bounds.right - bounds.left
|
|
: bounds.bottom - bounds.top;
|
|
|
|
return Math.max(0, size - 2 * padding);
|
|
}
|
|
|
|
export function isVerticalPlacement(placement: Placement) {
|
|
return isVertical(parsePlacement(placement).side);
|
|
}
|
|
|
|
export function insets({
|
|
anchor,
|
|
floating,
|
|
bounds,
|
|
side,
|
|
align,
|
|
gap,
|
|
padding,
|
|
rtl,
|
|
viewport,
|
|
}: {
|
|
anchor: Rect;
|
|
floating: Size;
|
|
bounds: Bounds;
|
|
side: Side;
|
|
align: Align;
|
|
gap: number;
|
|
padding: number;
|
|
rtl: boolean;
|
|
viewport: Size;
|
|
}): Insets {
|
|
if (isVertical(side)) {
|
|
const left = clamp(
|
|
alignedStart(
|
|
anchor.left,
|
|
anchor.width,
|
|
floating.width,
|
|
physicalAlign(align, rtl),
|
|
),
|
|
bounds.left + padding,
|
|
bounds.right - padding - floating.width,
|
|
);
|
|
|
|
return side === "bottom"
|
|
? {
|
|
top: anchor.top + anchor.height + gap,
|
|
right: null,
|
|
bottom: null,
|
|
left,
|
|
}
|
|
: {
|
|
top: null,
|
|
right: null,
|
|
bottom: viewport.height - (anchor.top - gap),
|
|
left,
|
|
};
|
|
}
|
|
|
|
const top = clamp(
|
|
alignedStart(anchor.top, anchor.height, floating.height, align),
|
|
bounds.top + padding,
|
|
bounds.bottom - padding - floating.height,
|
|
);
|
|
|
|
return side === "right"
|
|
? { top, right: null, bottom: null, left: anchor.left + anchor.width + gap }
|
|
: {
|
|
top,
|
|
right: viewport.width - (anchor.left - gap),
|
|
bottom: null,
|
|
left: null,
|
|
};
|
|
}
|
|
|
|
export function documentInsets(
|
|
viewportInsets: Insets,
|
|
scroll: { x: number; y: number },
|
|
): Insets {
|
|
const { top, right, bottom, left } = viewportInsets;
|
|
|
|
return {
|
|
top: top === null ? null : top + scroll.y,
|
|
right: right === null ? null : right - scroll.x,
|
|
bottom: bottom === null ? null : bottom - scroll.y,
|
|
left: left === null ? null : left + scroll.x,
|
|
};
|
|
}
|
|
|
|
function parsePlacement(placement: Placement): { side: Side; align: Align } {
|
|
const [side, align = "center"] = placement.split(" ") as [Side, Align?];
|
|
return { side, align };
|
|
}
|
|
|
|
function isVertical(side: Side) {
|
|
return side === "top" || side === "bottom";
|
|
}
|
|
|
|
function spaceAround(
|
|
anchor: Rect,
|
|
bounds: Bounds,
|
|
gap: number,
|
|
padding: number,
|
|
): Record<Side, number> {
|
|
const taken = gap + padding;
|
|
|
|
return {
|
|
top: anchor.top - bounds.top - taken,
|
|
bottom: bounds.bottom - (anchor.top + anchor.height) - taken,
|
|
left: anchor.left - bounds.left - taken,
|
|
right: bounds.right - (anchor.left + anchor.width) - taken,
|
|
};
|
|
}
|
|
|
|
function alignedStart(
|
|
anchorStart: number,
|
|
anchorSize: number,
|
|
floatingSize: number,
|
|
align: Align,
|
|
) {
|
|
if (align === "start") return anchorStart;
|
|
if (align === "end") return anchorStart + anchorSize - floatingSize;
|
|
return anchorStart + anchorSize / 2 - floatingSize / 2;
|
|
}
|
|
|
|
function physicalAlign(align: Align, rtl: boolean): Align {
|
|
if (!rtl || align === "center") return align;
|
|
return align === "start" ? "end" : "start";
|
|
}
|
|
|
|
function clamp(value: number, min: number, max: number) {
|
|
return Math.max(min, Math.min(value, max));
|
|
}
|
|
|
|
function transformOrigin(side: Side, align: Align, rtl: boolean) {
|
|
if (isVertical(side)) {
|
|
const x = ALIGN_ORIGIN[physicalAlign(align, rtl)];
|
|
return side === "bottom" ? `${x} 0%` : `${x} 100%`;
|
|
}
|
|
|
|
const y = ALIGN_ORIGIN[align];
|
|
return side === "right" ? `0% ${y}` : `100% ${y}`;
|
|
}
|