@mantine/hooks
Version:
A collection of 50+ hooks for state and UI management
456 lines (455 loc) • 17.4 kB
JavaScript
"use client";
const require_use_uncontrolled = require("../use-uncontrolled/use-uncontrolled.cjs");
let react = require("react");
//#region packages/@mantine/hooks/src/use-splitter/use-splitter.ts
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
function getMin(panel) {
return panel.min ?? 0;
}
function getMax(panel) {
return panel.max ?? 100;
}
function getCollapseThreshold(panel) {
return panel.collapseThreshold ?? getMin(panel);
}
function createInitialInternalState() {
return {
isDragging: false,
handleIndex: -1,
startPointer: 0,
containerSize: 0,
startSizes: [],
preCollapseSizes: []
};
}
function checkCollapse(sizes, panels, handleIndex, delta) {
const beforeIdx = handleIndex;
const afterIdx = handleIndex + 1;
const beforePanel = panels[beforeIdx];
const afterPanel = panels[afterIdx];
const rawBefore = sizes[beforeIdx] + delta;
const rawAfter = sizes[afterIdx] - delta;
if (beforePanel.collapsible && rawBefore < getCollapseThreshold(beforePanel) && rawBefore < sizes[beforeIdx]) {
const result = [...sizes];
result[afterIdx] += result[beforeIdx];
result[beforeIdx] = 0;
return result;
}
if (afterPanel.collapsible && rawAfter < getCollapseThreshold(afterPanel) && rawAfter < sizes[afterIdx]) {
const result = [...sizes];
result[beforeIdx] += result[afterIdx];
result[afterIdx] = 0;
return result;
}
return null;
}
function applyAdjacentOnly(sizes, panels, handleIndex, delta) {
const result = [...sizes];
const beforeIdx = handleIndex;
const afterIdx = handleIndex + 1;
const total = result[beforeIdx] + result[afterIdx];
const effectiveBeforeMax = Math.min(getMax(panels[beforeIdx]), total - getMin(panels[afterIdx]));
const effectiveBeforeMin = Math.max(getMin(panels[beforeIdx]), total - getMax(panels[afterIdx]));
const newBefore = clamp(result[beforeIdx] + delta, effectiveBeforeMin, effectiveBeforeMax);
result[beforeIdx] = newBefore;
result[afterIdx] = total - newBefore;
return result;
}
function redistributeNearest(sizes, panels, handleIndex, delta) {
const result = [...sizes];
if (delta > 0) {
const growIdx = handleIndex;
const maxGrow = getMax(panels[growIdx]) - result[growIdx];
const wantedGrow = Math.min(delta, maxGrow);
let taken = 0;
for (let i = handleIndex + 1; i < result.length && taken < wantedGrow; i += 1) {
const canGive = result[i] - getMin(panels[i]);
const take = Math.min(canGive, wantedGrow - taken);
result[i] -= take;
taken += take;
}
result[growIdx] += taken;
} else if (delta < 0) {
const growIdx = handleIndex + 1;
const maxGrow = getMax(panels[growIdx]) - result[growIdx];
const wantedGrow = Math.min(Math.abs(delta), maxGrow);
let taken = 0;
for (let i = handleIndex; i >= 0 && taken < wantedGrow; i -= 1) {
const canGive = result[i] - getMin(panels[i]);
const take = Math.min(canGive, wantedGrow - taken);
result[i] -= take;
taken += take;
}
result[growIdx] += taken;
}
return result;
}
function redistributeEqual(sizes, panels, handleIndex, delta) {
const result = [...sizes];
if (delta > 0) {
const growIdx = handleIndex;
const maxGrow = getMax(panels[growIdx]) - result[growIdx];
const wantedGrow = Math.min(delta, maxGrow);
const donors = [];
for (let i = handleIndex + 1; i < result.length; i += 1) if (result[i] > getMin(panels[i])) donors.push(i);
let remaining = wantedGrow;
while (remaining > .001 && donors.length > 0) {
const perDonor = remaining / donors.length;
const exhausted = [];
for (let d = 0; d < donors.length; d += 1) {
const idx = donors[d];
const canGive = result[idx] - getMin(panels[idx]);
const take = Math.min(canGive, perDonor);
result[idx] -= take;
remaining -= take;
if (canGive <= perDonor + .001) exhausted.push(d);
}
for (let i = exhausted.length - 1; i >= 0; i -= 1) donors.splice(exhausted[i], 1);
if (exhausted.length === 0) break;
}
result[growIdx] += wantedGrow - remaining;
} else if (delta < 0) {
const growIdx = handleIndex + 1;
const maxGrow = getMax(panels[growIdx]) - result[growIdx];
const wantedGrow = Math.min(Math.abs(delta), maxGrow);
const donors = [];
for (let i = handleIndex; i >= 0; i -= 1) if (result[i] > getMin(panels[i])) donors.push(i);
let remaining = wantedGrow;
while (remaining > .001 && donors.length > 0) {
const perDonor = remaining / donors.length;
const exhausted = [];
for (let d = 0; d < donors.length; d += 1) {
const idx = donors[d];
const canGive = result[idx] - getMin(panels[idx]);
const take = Math.min(canGive, perDonor);
result[idx] -= take;
remaining -= take;
if (canGive <= perDonor + .001) exhausted.push(d);
}
for (let i = exhausted.length - 1; i >= 0; i -= 1) donors.splice(exhausted[i], 1);
if (exhausted.length === 0) break;
}
result[growIdx] += wantedGrow - remaining;
}
return result;
}
function applyConstraints(sizes, panels, handleIndex, delta, redistribute) {
if (typeof redistribute === "function") return redistribute({
sizes: [...sizes],
panels,
handleIndex,
delta
});
if (redistribute === "nearest" || redistribute === "equal") {
const result = (redistribute === "nearest" ? redistributeNearest : redistributeEqual)(sizes, panels, handleIndex, delta);
const beforeIdx = handleIndex;
const afterIdx = handleIndex + 1;
const beforePanel = panels[beforeIdx];
const afterPanel = panels[afterIdx];
if (beforePanel.collapsible && result[beforeIdx] < getCollapseThreshold(beforePanel) && result[beforeIdx] < sizes[beforeIdx]) {
const freed = result[beforeIdx];
result[afterIdx] += freed;
result[beforeIdx] = 0;
} else if (afterPanel.collapsible && result[afterIdx] < getCollapseThreshold(afterPanel) && result[afterIdx] < sizes[afterIdx]) {
const freed = result[afterIdx];
result[beforeIdx] += freed;
result[afterIdx] = 0;
}
return result;
}
const collapsed = checkCollapse(sizes, panels, handleIndex, delta);
if (collapsed) return collapsed;
return applyAdjacentOnly(sizes, panels, handleIndex, delta);
}
function useSplitter(options) {
const { panels, orientation = "horizontal", sizes: controlledSizes, onSizeChange, onCollapseChange, redistribute, step = 1, shiftStep = 10, dir = "ltr", enabled = true } = options;
const defaultSizes = panels.map((p) => p.defaultSize);
const [currentSizes, setCurrentSizes] = require_use_uncontrolled.useUncontrolled({
value: controlledSizes,
defaultValue: defaultSizes,
finalValue: defaultSizes,
onChange: onSizeChange
});
const [activeHandle, setActiveHandle] = (0, react.useState)(-1);
const optionsRef = (0, react.useRef)(options);
optionsRef.current = options;
const internalStateRef = (0, react.useRef)(createInitialInternalState());
const containerRef = (0, react.useRef)(null);
const documentControllerRef = (0, react.useRef)(null);
const frameRef = (0, react.useRef)(0);
const currentSizesRef = (0, react.useRef)(currentSizes);
currentSizesRef.current = currentSizes;
const preCollapseSizesRef = (0, react.useRef)(defaultSizes);
const collapsed = currentSizes.map((size) => size === 0);
const updateSizes = (0, react.useCallback)((newSizes) => {
currentSizesRef.current = newSizes;
setCurrentSizes(newSizes);
}, [setCurrentSizes]);
const collapsePanel = (0, react.useCallback)((panelIndex) => {
if (!panels[panelIndex]?.collapsible) return;
const sizes = currentSizesRef.current;
if (sizes[panelIndex] === 0) return;
preCollapseSizesRef.current = [...sizes];
const newSizes = [...sizes];
const freedSize = newSizes[panelIndex];
newSizes[panelIndex] = 0;
const neighbor = panelIndex === 0 ? 1 : panelIndex - 1;
newSizes[neighbor] += freedSize;
updateSizes(newSizes);
onCollapseChange?.(panelIndex, true);
}, [
panels,
updateSizes,
onCollapseChange
]);
const expandPanel = (0, react.useCallback)((panelIndex) => {
if (!panels[panelIndex]?.collapsible) return;
const sizes = currentSizesRef.current;
if (sizes[panelIndex] !== 0) return;
const restoreSize = preCollapseSizesRef.current[panelIndex] || panels[panelIndex].defaultSize;
const newSizes = [...sizes];
const neighbor = panelIndex === 0 ? 1 : panelIndex - 1;
const available = Math.max(0, newSizes[neighbor] - getMin(panels[neighbor]));
const actualRestore = Math.min(restoreSize, available);
if (actualRestore <= 0) return;
newSizes[panelIndex] = actualRestore;
newSizes[neighbor] -= actualRestore;
updateSizes(newSizes);
onCollapseChange?.(panelIndex, false);
}, [
panels,
updateSizes,
onCollapseChange
]);
const toggleCollapsePanel = (0, react.useCallback)((panelIndex) => {
if (currentSizesRef.current[panelIndex] === 0) expandPanel(panelIndex);
else collapsePanel(panelIndex);
}, [collapsePanel, expandPanel]);
const containerRefCallback = (0, react.useCallback)((node) => {
containerRef.current = node;
}, []);
const handleRefCallbacks = (0, react.useRef)(/* @__PURE__ */ new Map());
const handleElementControllers = (0, react.useRef)(/* @__PURE__ */ new Map());
const getHandleRefCallback = (0, react.useCallback)((handleIndex) => {
if (handleRefCallbacks.current.has(handleIndex)) return handleRefCallbacks.current.get(handleIndex);
const callback = (node) => {
const existingController = handleElementControllers.current.get(handleIndex);
if (existingController) {
existingController.abort();
handleElementControllers.current.delete(handleIndex);
}
if (!node) return;
const elementController = new AbortController();
handleElementControllers.current.set(handleIndex, elementController);
const onPointerDown = (event) => {
if (optionsRef.current.enabled === false) return;
if (event.button !== 0) return;
const container = containerRef.current;
if (!container) return;
const rect = container.getBoundingClientRect();
const isHorizontal = (optionsRef.current.orientation ?? "horizontal") === "horizontal";
const containerSize = isHorizontal ? rect.width : rect.height;
const pointerPos = isHorizontal ? event.clientX : event.clientY;
const s = internalStateRef.current;
s.isDragging = true;
s.handleIndex = handleIndex;
s.startPointer = pointerPos;
s.containerSize = containerSize;
s.startSizes = [...currentSizesRef.current];
s.preCollapseSizes = [...preCollapseSizesRef.current];
setActiveHandle(handleIndex);
document.body.style.userSelect = "none";
document.body.style.webkitUserSelect = "none";
document.body.style.cursor = isHorizontal ? "col-resize" : "row-resize";
optionsRef.current.onResizeStart?.(handleIndex);
documentControllerRef.current?.abort();
documentControllerRef.current = new AbortController();
const sig = documentControllerRef.current.signal;
document.addEventListener("pointermove", onPointerMove, { signal: sig });
document.addEventListener("pointerup", onPointerUp, { signal: sig });
document.addEventListener("pointercancel", onPointerUp, { signal: sig });
};
const flushResize = (pointerEvent) => {
const s = internalStateRef.current;
if (!s.containerSize) return;
const isHorizontal = (optionsRef.current.orientation ?? "horizontal") === "horizontal";
const isRtl = isHorizontal && optionsRef.current.dir === "rtl";
const pixelDelta = (isHorizontal ? pointerEvent.clientX : pointerEvent.clientY) - s.startPointer;
const percentDelta = (isRtl ? -pixelDelta : pixelDelta) / s.containerSize * 100;
const opts = optionsRef.current;
const newSizes = applyConstraints(s.startSizes, opts.panels, s.handleIndex, percentDelta, opts.redistribute);
const prevSizes = currentSizesRef.current;
const beforeWasCollapsed = prevSizes[s.handleIndex] === 0;
const afterWasCollapsed = prevSizes[s.handleIndex + 1] === 0;
const beforeNowCollapsed = newSizes[s.handleIndex] === 0;
const afterNowCollapsed = newSizes[s.handleIndex + 1] === 0;
if (!beforeWasCollapsed && beforeNowCollapsed) {
preCollapseSizesRef.current = [...s.startSizes];
opts.onCollapseChange?.(s.handleIndex, true);
} else if (beforeWasCollapsed && !beforeNowCollapsed) opts.onCollapseChange?.(s.handleIndex, false);
if (!afterWasCollapsed && afterNowCollapsed) {
preCollapseSizesRef.current = [...s.startSizes];
opts.onCollapseChange?.(s.handleIndex + 1, true);
} else if (afterWasCollapsed && !afterNowCollapsed) opts.onCollapseChange?.(s.handleIndex + 1, false);
currentSizesRef.current = newSizes;
setCurrentSizes(newSizes);
};
const onPointerMove = (event) => {
if (!internalStateRef.current.isDragging) return;
cancelAnimationFrame(frameRef.current);
frameRef.current = requestAnimationFrame(() => {
flushResize(event);
});
};
const onPointerUp = (event) => {
const s = internalStateRef.current;
if (!s.isDragging) return;
cancelAnimationFrame(frameRef.current);
flushResize(event);
s.isDragging = false;
const finishedHandle = s.handleIndex;
s.handleIndex = -1;
setActiveHandle(-1);
document.body.style.userSelect = "";
document.body.style.webkitUserSelect = "";
document.body.style.cursor = "";
documentControllerRef.current?.abort();
documentControllerRef.current = null;
optionsRef.current.onResizeEnd?.(finishedHandle, [...currentSizesRef.current]);
};
node.addEventListener("pointerdown", onPointerDown, { signal: elementController.signal });
};
handleRefCallbacks.current.set(handleIndex, callback);
return callback;
}, [setCurrentSizes]);
const getHandleProps = (0, react.useCallback)((input) => {
const { index } = input;
const orient = orientation;
const beforeSize = currentSizes[index] ?? 0;
const beforePanel = panels[index];
const afterPanel = panels[index + 1];
return {
ref: getHandleRefCallback(index),
role: "separator",
"aria-orientation": orient,
"aria-valuenow": Math.round(beforeSize),
"aria-valuemin": Math.round(getMin(beforePanel)),
"aria-valuemax": Math.round(getMax(beforePanel)),
tabIndex: 0,
onKeyDown: (event) => {
if (!enabled) return;
const isHorizontal = orient === "horizontal";
const isRtl = dir === "rtl";
let delta = 0;
const currentStep = event.shiftKey ? shiftStep : step;
switch (event.key) {
case "ArrowLeft":
if (!isHorizontal) return;
delta = isRtl ? currentStep : -currentStep;
break;
case "ArrowRight":
if (!isHorizontal) return;
delta = isRtl ? -currentStep : currentStep;
break;
case "ArrowUp":
if (isHorizontal) return;
delta = -currentStep;
break;
case "ArrowDown":
if (isHorizontal) return;
delta = currentStep;
break;
case "Home":
delta = -(currentSizes[index] - getMin(beforePanel));
break;
case "End":
delta = getMax(beforePanel) - currentSizes[index];
break;
case "Enter": {
const beforeCollapsible = beforePanel?.collapsible;
const afterCollapsible = afterPanel?.collapsible;
if (beforeCollapsible && currentSizes[index] <= currentSizes[index + 1]) {
toggleCollapsePanel(index);
event.preventDefault();
return;
}
if (afterCollapsible) {
toggleCollapsePanel(index + 1);
event.preventDefault();
return;
}
if (beforeCollapsible) {
toggleCollapsePanel(index);
event.preventDefault();
return;
}
return;
}
default: return;
}
event.preventDefault();
if (delta !== 0) {
const newSizes = applyConstraints(currentSizes, panels, index, delta, redistribute);
const beforeWas = currentSizes[index] === 0;
const afterWas = currentSizes[index + 1] === 0;
const beforeNow = newSizes[index] === 0;
const afterNow = newSizes[index + 1] === 0;
if (!beforeWas && beforeNow) {
preCollapseSizesRef.current = [...currentSizes];
onCollapseChange?.(index, true);
} else if (beforeWas && !beforeNow) onCollapseChange?.(index, false);
if (!afterWas && afterNow) {
preCollapseSizesRef.current = [...currentSizes];
onCollapseChange?.(index + 1, true);
} else if (afterWas && !afterNow) onCollapseChange?.(index + 1, false);
updateSizes(newSizes);
}
},
"data-active": activeHandle === index || void 0,
"data-orientation": orient
};
}, [
orientation,
currentSizes,
panels,
enabled,
dir,
step,
shiftStep,
activeHandle,
redistribute,
getHandleRefCallback,
toggleCollapsePanel,
updateSizes,
onCollapseChange
]);
(0, react.useEffect)(() => () => {
documentControllerRef.current?.abort();
documentControllerRef.current = null;
handleElementControllers.current.forEach((controller) => controller.abort());
handleElementControllers.current.clear();
cancelAnimationFrame(frameRef.current);
if (internalStateRef.current.isDragging) {
internalStateRef.current.isDragging = false;
document.body.style.userSelect = "";
document.body.style.webkitUserSelect = "";
document.body.style.cursor = "";
}
}, []);
return {
ref: containerRefCallback,
sizes: currentSizes,
collapsed,
activeHandle,
getHandleProps,
setSizes: updateSizes,
collapse: collapsePanel,
expand: expandPanel,
toggleCollapse: toggleCollapsePanel
};
}
//#endregion
exports.useSplitter = useSplitter;
//# sourceMappingURL=use-splitter.cjs.map