@mantine/hooks
Version:
A collection of 50+ hooks for state and UI management
263 lines (262 loc) • 7.7 kB
JavaScript
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
//#region packages/@mantine/hooks/src/use-floating-window/use-floating-window.ts
function useRefValue(value) {
const ref = useRef(value);
ref.current = value;
return ref;
}
function useFloatingWindow(options = {}) {
const [element, setElement] = useState(null);
const ref = useRef(null);
const pos = useRef({
x: 0,
y: 0
});
const offset = useRef({
x: 0,
y: 0
});
const [isDragging, setIsDragging] = useState(false);
const isDraggingRef = useRef(false);
const initialized = useRef(false);
const enabledRef = useRefValue(options.enabled);
const setDragging = useCallback((value) => {
setIsDragging(value);
isDraggingRef.current = value;
}, []);
const assignRef = useCallback((node) => {
if (node) {
ref.current = node;
setElement(node);
} else {
ref.current = null;
setElement(null);
}
}, []);
useEffect(() => {
const el = ref.current;
if (!initialized.current && el) {
initialized.current = true;
pos.current = calculateInitialPosition(el, options);
el.style.left = `${pos.current.x}px`;
el.style.top = `${pos.current.y}px`;
el.style.right = "unset";
el.style.bottom = "unset";
}
return () => {
initialized.current = false;
};
}, [
element,
options.constrainOffset,
options.initialPosition?.top,
options.initialPosition?.left,
options.initialPosition?.right,
options.initialPosition?.bottom,
options.constrainToViewport
]);
useEffect(() => {
const el = ref.current;
if (!el) return;
const controller = new AbortController();
const signal = controller.signal;
const onStart = (e) => {
if (enabledRef.current === false) return;
const point = "touches" in e ? e.touches[0] : e;
if ("button" in e && e.button !== 0) return;
if (!getHandle(el, e.target, options)) return;
setDragging(true);
document.body.style.userSelect = "none";
document.body.style.webkitUserSelect = "none";
const rect = el.getBoundingClientRect();
offset.current = {
x: point.clientX - rect.left,
y: point.clientY - rect.top
};
options.onDragStart?.();
document.addEventListener("mousemove", onMove, { signal });
document.addEventListener("mouseup", onEnd, { signal });
document.addEventListener("touchmove", onMove, {
signal,
passive: false
});
document.addEventListener("touchend", onEnd, { signal });
};
const onMove = (e) => {
if (!isDraggingRef.current) return;
const point = "touches" in e ? e.touches[0] : e;
e.preventDefault();
let x = point.clientX - offset.current.x;
let y = point.clientY - offset.current.y;
const constrained = getConstrainedPosition(el, {
x,
y
}, options);
if (options.axis === "x") {
x = constrained.x;
y = pos.current.y;
} else if (options.axis === "y") {
x = pos.current.x;
y = constrained.y;
} else {
x = constrained.x;
y = constrained.y;
}
pos.current = {
x,
y
};
if (ref.current) {
ref.current.style.left = `${x}px`;
ref.current.style.top = `${y}px`;
}
options.onPositionChange?.({
x,
y
});
};
const onEnd = () => {
if (isDraggingRef.current) {
setDragging(false);
document.body.style.userSelect = "";
document.body.style.webkitUserSelect = "";
options.onDragEnd?.();
}
};
el.addEventListener("mousedown", onStart, { signal });
el.addEventListener("touchstart", onStart, {
signal,
passive: false
});
return () => {
controller.abort();
};
}, [
options.constrainToViewport,
options.constrainOffset,
options.dragHandleSelector,
options.axis,
options.onPositionChange,
options.onDragStart,
options.onDragEnd,
options.initialPosition?.top,
options.initialPosition?.left,
options.initialPosition?.right,
options.initialPosition?.bottom,
element
]);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new ResizeObserver(() => {
const constrained = getConstrainedPosition(el, pos.current, options);
pos.current = constrained;
el.style.left = `${constrained.x}px`;
el.style.top = `${constrained.y}px`;
});
observer.observe(el);
return () => {
observer.disconnect();
};
}, [options.constrainToViewport, options.constrainOffset]);
return {
ref: assignRef,
setPosition: createSetPosition(ref, pos, options),
isDragging
};
}
function px(v) {
return v.endsWith("px") ? parseFloat(v) : 0;
}
function calculateInitialPosition(el, options) {
const rect = el.getBoundingClientRect();
const offset = options.constrainOffset ?? 0;
const winW = window.innerWidth;
const winH = window.innerHeight;
const style = window.getComputedStyle(el);
const top = options.initialPosition?.top;
const left = options.initialPosition?.left;
const right = options.initialPosition?.right;
const bottom = options.initialPosition?.bottom;
let x = offset;
let y = offset;
if (left != null) x = left;
else if (right != null) x = winW - rect.width - right;
else x = px(style.left) || winW - rect.width - px(style.right) || offset;
if (top != null) y = top;
else if (bottom != null) y = winH - rect.height - bottom;
else y = px(style.top) || winH - rect.height - px(style.bottom) || offset;
return options.constrainToViewport ? clampToViewport(x, y, el, options.constrainOffset) : {
x,
y
};
}
function getConstrainedPosition(el, pos, options) {
if (!options.constrainToViewport || !el) return pos;
const rect = el.getBoundingClientRect();
const offset = options.constrainOffset ?? 0;
const maxX = window.innerWidth - rect.width - offset;
const maxY = window.innerHeight - rect.height - offset;
return {
x: Math.min(Math.max(offset, pos.x), maxX),
y: Math.min(Math.max(offset, pos.y), maxY)
};
}
function matchesExcludeSelector(target, excludeSelector) {
if (!excludeSelector) return false;
if (!(target instanceof Element)) return false;
return Boolean(target.closest(excludeSelector));
}
function getHandle(el, target, options) {
if (!(target instanceof Node)) return false;
if (!options.dragHandleSelector) return !matchesExcludeSelector(target, options.excludeDragHandleSelector);
return Array.from(el.querySelectorAll(options.dragHandleSelector)).some((handle) => handle.contains(target) && !matchesExcludeSelector(target, options.excludeDragHandleSelector));
}
function clampToViewport(x, y, el, offset = 0) {
const rect = el.getBoundingClientRect();
const maxX = window.innerWidth - rect.width - offset;
const maxY = window.innerHeight - rect.height - offset;
return {
x: Math.min(Math.max(offset, x), maxX),
y: Math.min(Math.max(offset, y), maxY)
};
}
function createSetPosition(elRef, posRef, options) {
return useCallback((position) => {
const el = elRef.current;
if (!el) return;
const offset = options.constrainOffset ?? 0;
const rect = el.getBoundingClientRect();
let x;
let y;
if (position.left != null) x = position.left;
else if (position.right != null) x = window.innerWidth - rect.width - position.right;
if (position.top != null) y = position.top;
else if (position.bottom != null) y = window.innerHeight - rect.height - position.bottom;
x = x ?? posRef.current.x;
y = y ?? posRef.current.y;
if (options.constrainToViewport) {
const clamped = clampToViewport(x, y, el, offset);
x = clamped.x;
y = clamped.y;
}
posRef.current = {
x,
y
};
el.style.left = `${x}px`;
el.style.top = `${y}px`;
options.onPositionChange?.({
x,
y
});
}, [
options.constrainToViewport,
options.constrainOffset,
options.onPositionChange
]);
}
//#endregion
export { useFloatingWindow };
//# sourceMappingURL=use-floating-window.mjs.map