121 lines
4.3 KiB
JavaScript
121 lines
4.3 KiB
JavaScript
import { computePosition } from "@floating-ui/dom";
|
|
import { simpleBox } from "svelte-toolbelt";
|
|
import { get, getDPR, roundByDPR } from "./floating-utils.svelte.js";
|
|
export function useFloating(options) {
|
|
/** Options */
|
|
const whileElementsMountedOption = options.whileElementsMounted;
|
|
const openOption = $derived(get(options.open) ?? true);
|
|
const middlewareOption = $derived(get(options.middleware));
|
|
const transformOption = $derived(get(options.transform) ?? true);
|
|
const placementOption = $derived(get(options.placement) ?? "bottom");
|
|
const strategyOption = $derived(get(options.strategy) ?? "absolute");
|
|
const sideOffsetOption = $derived(get(options.sideOffset) ?? 0);
|
|
const alignOffsetOption = $derived(get(options.alignOffset) ?? 0);
|
|
const reference = options.reference;
|
|
/** State */
|
|
let x = $state(0);
|
|
let y = $state(0);
|
|
const floating = simpleBox(null);
|
|
// svelte-ignore state_referenced_locally
|
|
let strategy = $state(strategyOption);
|
|
// svelte-ignore state_referenced_locally
|
|
let placement = $state(placementOption);
|
|
let middlewareData = $state({});
|
|
let isPositioned = $state(false);
|
|
const floatingStyles = $derived.by(() => {
|
|
// preserve last known position when floating ref is null (during transitions)
|
|
const xVal = floating.current ? roundByDPR(floating.current, x) : x;
|
|
const yVal = floating.current ? roundByDPR(floating.current, y) : y;
|
|
if (transformOption) {
|
|
return {
|
|
position: strategy,
|
|
left: "0",
|
|
top: "0",
|
|
transform: `translate(${xVal}px, ${yVal}px)`,
|
|
...(floating.current &&
|
|
getDPR(floating.current) >= 1.5 && {
|
|
willChange: "transform",
|
|
}),
|
|
};
|
|
}
|
|
return {
|
|
position: strategy,
|
|
left: `${xVal}px`,
|
|
top: `${yVal}px`,
|
|
};
|
|
});
|
|
/** Effects */
|
|
let whileElementsMountedCleanup;
|
|
function update() {
|
|
if (reference.current === null || floating.current === null)
|
|
return;
|
|
computePosition(reference.current, floating.current, {
|
|
middleware: middlewareOption,
|
|
placement: placementOption,
|
|
strategy: strategyOption,
|
|
}).then((position) => {
|
|
// ignore bad coordinates that cause jumping during close transitions
|
|
if (!openOption && x !== 0 && y !== 0) {
|
|
// if we had a good position and now getting coordinates near
|
|
// the expected offset bounds during close, ignore it
|
|
const maxExpectedOffset = Math.max(Math.abs(sideOffsetOption), Math.abs(alignOffsetOption), 15);
|
|
if (position.x <= maxExpectedOffset && position.y <= maxExpectedOffset)
|
|
return;
|
|
}
|
|
x = position.x;
|
|
y = position.y;
|
|
strategy = position.strategy;
|
|
placement = position.placement;
|
|
middlewareData = position.middlewareData;
|
|
isPositioned = true;
|
|
});
|
|
}
|
|
function cleanup() {
|
|
if (typeof whileElementsMountedCleanup === "function") {
|
|
whileElementsMountedCleanup();
|
|
whileElementsMountedCleanup = undefined;
|
|
}
|
|
}
|
|
function attach() {
|
|
cleanup();
|
|
if (whileElementsMountedOption === undefined) {
|
|
update();
|
|
return;
|
|
}
|
|
if (reference.current === null || floating.current === null)
|
|
return;
|
|
whileElementsMountedCleanup = whileElementsMountedOption(reference.current, floating.current, update);
|
|
}
|
|
function reset() {
|
|
if (!openOption) {
|
|
isPositioned = false;
|
|
}
|
|
}
|
|
$effect(update);
|
|
$effect(attach);
|
|
$effect(reset);
|
|
$effect(() => cleanup);
|
|
return {
|
|
floating,
|
|
reference,
|
|
get strategy() {
|
|
return strategy;
|
|
},
|
|
get placement() {
|
|
return placement;
|
|
},
|
|
get middlewareData() {
|
|
return middlewareData;
|
|
},
|
|
get isPositioned() {
|
|
return isPositioned;
|
|
},
|
|
get floatingStyles() {
|
|
return floatingStyles;
|
|
},
|
|
get update() {
|
|
return update;
|
|
},
|
|
};
|
|
}
|