UNPKG

@mantine/hooks

Version:

A collection of 50+ hooks for state and UI management

456 lines (455 loc) 17.4 kB
"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