UNPKG

@corvu/resizable

Version:

Unstyled, accessible and customizable UI primitives for SolidJS

1,384 lines (1,381 loc) 56.7 kB
import { createContext, useContext, mergeProps, splitProps, createSignal, createMemo, createEffect, onCleanup, on, Show, createUniqueId, untrack, batch } from 'solid-js'; import { useKeyedContext, createKeyedContext } from '@corvu/utils/create/keyedContext'; import { createComponent, mergeProps as mergeProps$1, effect, memo, template } from 'solid-js/web'; import { combineStyle, callEventHandler, sortByDocumentPosition } from '@corvu/utils/dom'; import { Dynamic } from '@corvu/utils/dynamic'; import { mergeRefs, some } from '@corvu/utils/reactivity'; import { dataIf, isFunction } from '@corvu/utils'; import createOnce from '@corvu/utils/create/once'; import createControllableSignal from '@corvu/utils/create/controllableSignal'; import createSize from '@corvu/utils/create/size'; // src/context.ts var ResizableContext = createContext(); var createResizableContext = (contextId) => { if (contextId === undefined) return ResizableContext; const context = createKeyedContext( `resizable-${contextId}` ); return context; }; var useResizableContext = (contextId) => { if (contextId === undefined) { const context2 = useContext(ResizableContext); if (!context2) { throw new Error( "[corvu]: Resizable context not found. Make sure to wrap Resizable components in <Resizable.Root>" ); } return context2; } const context = useKeyedContext( `resizable-${contextId}` ); if (!context) { throw new Error( `[corvu]: Resizable context with id "${contextId}" not found. Make sure to wrap Resizable components in <Resizable.Root contextId="${contextId}">` ); } return context; }; var InternalResizableContext = createContext(); var createInternalResizableContext = (contextId) => { if (contextId === undefined) return InternalResizableContext; const context = createKeyedContext( `resizable-internal-${contextId}` ); return context; }; var useInternalResizableContext = (contextId) => { if (contextId === undefined) { const context2 = useContext(InternalResizableContext); if (!context2) { throw new Error( "[corvu]: Resizable context not found. Make sure to wrap Resizable components in <Resizable.Root>" ); } return context2; } const context = useKeyedContext( `resizable-internal-${contextId}` ); if (!context) { throw new Error( `[corvu]: Resizable context with id "${contextId}" not found. Make sure to wrap Resizable components in <Resizable.Root contextId="${contextId}">` ); } return context; }; // src/lib/utils.ts var resolveSize = (size, rootSize) => { if (typeof size === "number") { return size; } if (!size.endsWith("px")) { throw new Error( `[corvu] Sizes must be a number or a string ending with 'px'. Got ${size}` ); } return fixToPrecision(parseFloat(size) / rootSize); }; var splitPanels = (props) => { const precedingPanels = props.panels.filter( (panel) => !!(props.focusedElement.compareDocumentPosition(panel.data.element) & Node.DOCUMENT_POSITION_PRECEDING) ); const followingPanels = props.panels.filter( (panel) => !!(props.focusedElement.compareDocumentPosition(panel.data.element) & Node.DOCUMENT_POSITION_FOLLOWING) ); return [precedingPanels, followingPanels]; }; var PRECISION = 6; var fixToPrecision = (value) => parseFloat(value.toFixed(PRECISION)); // src/lib/cursor.ts var globalCursorStyle = null; var cursorStyleElement = null; var globalResizeConstraints = 0; var constraintToCursorMap = { 1: "e-resize", 2: "w-resize", 3: "ew-resize", 4: "s-resize", 8: "n-resize", 12: "ns-resize", 5: "se-resize", 9: "ne-resize", 6: "sw-resize", 10: "nw-resize" }; var cachedCursorStyle = null; var updateCursorStyle = () => { if (!globalCursorStyle) { if (cursorStyleElement) { cachedCursorStyle = null; cursorStyleElement.remove(); cursorStyleElement = null; } return; } let cursorStyle = constraintToCursorMap[globalResizeConstraints] ?? null; if (cursorStyle === null) { switch (globalCursorStyle) { case "horizontal": cursorStyle = "col-resize"; break; case "vertical": cursorStyle = "row-resize"; break; case "both": cursorStyle = "move"; break; } } if (cursorStyle === cachedCursorStyle) return; cachedCursorStyle = cursorStyle; if (!cursorStyleElement) { cursorStyleElement = document.createElement("style"); document.head.appendChild(cursorStyleElement); } cursorStyleElement.innerHTML = `*{cursor: ${cursorStyle}!important;}`; }; var reportResizeConstraints = (orientation, constraints) => { switch (orientation) { case "horizontal": if (constraints === 1) { globalResizeConstraints |= 1; globalResizeConstraints &= -3; } else if (constraints === 2) { globalResizeConstraints |= 2; globalResizeConstraints &= -2; } else if (constraints === 3) { globalResizeConstraints |= 3; } else { globalResizeConstraints &= -4; } break; case "vertical": if (constraints === 1) { globalResizeConstraints |= 4; globalResizeConstraints &= -9; } else if (constraints === 2) { globalResizeConstraints |= 8; globalResizeConstraints &= -5; } else if (constraints === 3) { globalResizeConstraints |= 12; } else { globalResizeConstraints &= -13; } break; } updateCursorStyle(); }; var resetResizeConstraints = () => { globalResizeConstraints = 0; updateCursorStyle(); }; var setGlobalCursorStyle = (cursorStyle) => { globalCursorStyle = cursorStyle; updateCursorStyle(); }; var handleResizeConstraints = (props) => { if (fixToPrecision(props.distributablePercentage) !== fixToPrecision(props.desiredPercentage)) { let constraints = null; if (props.betweenCollapse === true) { constraints = 3; } else if (props.desiredPercentage < props.distributablePercentage && props.revertConstraints !== true || props.desiredPercentage > props.distributablePercentage && props.revertConstraints === true) { constraints = 1; } else { constraints = 2; } reportResizeConstraints(props.orientation, constraints); } else { reportResizeConstraints(props.orientation, 0); } }; var handles = []; var dragStartPos = null; var globalHovered = null; var INTERSECTION_TOLERANCE = 1; var equalsWithTolerance = (a, b) => Math.abs(a - b) <= INTERSECTION_TOLERANCE; var registerHandle = (handle) => { handles.push(handle); for (const handle2 of handles) { for (const compareHandle of handles) { if (handle2.orientation === compareHandle.orientation || handle2.element === compareHandle.element) { continue; } const handleRect = handle2.element.getBoundingClientRect(); const compareHandleRect = compareHandle.element.getBoundingClientRect(); if (handle2.orientation === "horizontal") { if (handleRect.left > compareHandleRect.right || handleRect.right < compareHandleRect.left) { continue; } } if (handle2.orientation === "vertical") { if (handleRect.top > compareHandleRect.bottom || handleRect.bottom < compareHandleRect.top) { continue; } } const isStartIntersection = handle2.orientation === "horizontal" ? equalsWithTolerance(handleRect.top, compareHandleRect.bottom) : equalsWithTolerance(handleRect.left, compareHandleRect.right); const isEndIntersection = handle2.orientation === "horizontal" ? equalsWithTolerance(handleRect.bottom, compareHandleRect.top) : equalsWithTolerance(handleRect.right, compareHandleRect.left); if (isStartIntersection) { handle2.startIntersection.setHandle(compareHandle); } if (isEndIntersection) { handle2.endIntersection.setHandle(compareHandle); } } } return { onDragStart: (event, target) => onDragStart(handle, event, target), onHoveredChange: (state) => { globalHovered = state; const dragging = !!dragStartPos; let cursorStyle = null; batch(() => { switch (state) { case "handle": { const startHandle = handle.startIntersection.handle(); const endHandle = handle.endIntersection.handle(); if (!dragging) { handle.setActive(true); startHandle?.setActive(false); endHandle?.setActive(false); } startHandle?.setHoveredAsIntersection(false); endHandle?.setHoveredAsIntersection(false); cursorStyle = handle.orientation; break; } case "startIntersection": { const startHandle = handle.startIntersection.handle(); if (!dragging) { startHandle?.setActive(true); } startHandle?.setHoveredAsIntersection(true); cursorStyle = "both"; break; } case "endIntersection": { const endHandle = handle.endIntersection.handle(); if (!dragging) { endHandle?.setActive(true); } endHandle?.setHoveredAsIntersection(true); cursorStyle = "both"; break; } case null: { const startHandle = handle.startIntersection.handle(); const endHandle = handle.endIntersection.handle(); if (!dragging && !handle.focused()) { handle.setActive(false); startHandle?.setActive(false); endHandle?.setActive(false); } startHandle?.setHoveredAsIntersection(false); endHandle?.setHoveredAsIntersection(false); cursorStyle = null; break; } } if (!dragging && handle.handleCursorStyle()) { setGlobalCursorStyle(cursorStyle); } }); } }; }; var unregisterHandle = (handle) => { handles.splice(handles.indexOf(handle), 1); }; var onDragStart = (handle, event, target) => { dragStartPos = { x: event.clientX, y: event.clientY }; batch(() => { handle.setDragging(true); if (target === "startIntersection") { handle.startIntersection.handle()?.setDragging(true); } if (target === "endIntersection") { handle.endIntersection.handle()?.setDragging(true); } }); window.addEventListener("pointermove", onPointerMove); window.addEventListener("touchmove", onTouchMove); window.addEventListener("pointerup", onDragEnd); window.addEventListener("touchend", onDragEnd); window.addEventListener("contextmenu", onDragEnd); }; var onPointerMove = (event) => onMove(event.clientX, event.clientY, event.altKey); var onTouchMove = (event) => { if (!event.touches[0]) return; onMove(event.touches[0].clientX, event.touches[0].clientY, event.altKey); }; var altKeyCache = false; var onMove = (x, y, altKey) => { if (!dragStartPos) return; if (handles.some((handle) => handle.dragging() && handle.altKey === "only")) { altKey = true; } if (handles.some((handle) => handle.dragging() && handle.altKey === false)) { altKey = false; } if (altKeyCache !== altKey) { dragStartPos = { x, y }; altKeyCache = altKey; } for (const handle of handles) { if (!handle.dragging()) continue; handle.onDrag( handle.orientation === "horizontal" ? x - dragStartPos.x : y - dragStartPos.y, altKey ); } }; var onDragEnd = (event) => { batch(() => { for (const handle of handles) { if (!handle.dragging()) { if (some(handle.hovered, handle.focused, handle.hoveredAsIntersection)) { handle.setActive(true); if (handle.handleCursorStyle()) { setGlobalCursorStyle(handle.orientation); } } } else { handle.setDragging(false); handle.onDragEnd(event); if (!handle.hovered() && !handle.hoveredAsIntersection()) { handle.setActive(false); } if (!globalHovered && handle.handleCursorStyle()) { setGlobalCursorStyle(null); } } } }); resetResizeConstraints(); dragStartPos = null; window.removeEventListener("pointermove", onPointerMove); window.removeEventListener("touchmove", onTouchMove); window.removeEventListener("pointerup", onDragEnd); window.removeEventListener("touchend", onDragEnd); window.removeEventListener("contextmenu", onDragEnd); }; var _tmpl$ = /* @__PURE__ */ template(`<div data-corvu-resizable-handle-start-intersection>`); var _tmpl$2 = /* @__PURE__ */ template(`<div data-corvu-resizable-handle-end-intersection>`); var ResizableHandle = (props) => { const defaultedProps = mergeProps({ startIntersection: true, endIntersection: true, altKey: true }, props); const [localProps, otherProps] = splitProps(defaultedProps, ["startIntersection", "endIntersection", "altKey", "onHandleDragStart", "onHandleDrag", "onHandleDragEnd", "contextId", "ref", "style", "disabled", "children", "onMouseEnter", "onMouseLeave", "onKeyDown", "onKeyUp", "onFocus", "onBlur", "onPointerDown"]); const [ref, setRef] = createSignal(null); const [hoveredAsIntersection, setHoveredAsIntersection] = createSignal(false); const [hovered, setHovered] = createSignal(null); const [focused, setFocused] = createSignal(false); const [active, setActive] = createSignal(false); const [dragging, setDragging] = createSignal(false); const [startIntersection, setStartIntersection] = createSignal(null); const [endIntersection, setEndIntersection] = createSignal(null); const context = createMemo(() => useInternalResizableContext(localProps.contextId)); const ariaInformation = createMemo(() => { const handle = ref(); if (!handle) { return undefined; } const panels = context().panels(); const [precidingPanels, followingPanels] = splitPanels({ panels, focusedElement: handle }); const ariaControls = precidingPanels[precidingPanels.length - 1]?.data.id; const ariaValueMax = followingPanels.reduce((acc, panel) => acc - resolveSize(panel.data.minSize, context().rootSize()), 1); const ariaValueMin = precidingPanels.reduce((acc, panel) => acc + resolveSize(panel.data.minSize, context().rootSize()), 0); const ariaValueNow = precidingPanels.reduce((acc, panel) => acc + resolveSize(panel.size(), context().rootSize()), 0); return { ariaControls, ariaValueMax: fixToPrecision(ariaValueMax), ariaValueMin: fixToPrecision(ariaValueMin), ariaValueNow: fixToPrecision(ariaValueNow) }; }); let globalHandleCallbacks = null; createEffect(() => { if (localProps.disabled === true) return; const element = ref(); if (!element) return; const globalHandle = { element, orientation: context().orientation(), handleCursorStyle: context().handleCursorStyle, altKey: localProps.altKey, startIntersection: { handle: startIntersection, setHandle: (handle) => { if (localProps.startIntersection !== true) return; setStartIntersection(handle); } }, endIntersection: { handle: endIntersection, setHandle: (handle) => { if (localProps.endIntersection !== true) return; setEndIntersection(handle); } }, hovered, focused, hoveredAsIntersection, setHoveredAsIntersection, active, setActive, dragging, setDragging, onDrag: (delta, altKey) => { if (localProps.onHandleDrag !== undefined) { const dragEvent = new CustomEvent("drag", { cancelable: true }); localProps.onHandleDrag(dragEvent); if (dragEvent.defaultPrevented) return; } context().onDrag(element, delta, altKey); }, onDragEnd: (event) => { localProps.onHandleDragEnd?.(event); context().onDragEnd(); } }; globalHandleCallbacks = registerHandle(globalHandle); onCleanup(() => { unregisterHandle(globalHandle); globalHandleCallbacks = null; }); }); createEffect(on(hovered, () => { globalHandleCallbacks?.onHoveredChange(hovered()); })); const onMouseEnter = (e) => { if (callEventHandler(localProps.onMouseEnter, e) || localProps.disabled === true) return; setHovered("handle"); }; const onMouseLeave = (e) => { if (callEventHandler(localProps.onMouseLeave, e)) return; setHovered(null); }; const onKeyDown = (e) => { if (callEventHandler(localProps.onKeyDown, e) || dragging()) return; const element = ref(); if (!element) return; const altKey = localProps.altKey === "only" || localProps.altKey !== false && e.altKey; context().onKeyDown(element, e, altKey); }; const onKeyUp = (e) => { if (callEventHandler(localProps.onKeyUp, e) || e.key !== "Tab") return; setFocused(true); }; const onFocus = (e) => { if (callEventHandler(localProps.onFocus, e) || hovered()) return; setFocused(true); setActive(true); }; const onBlur = (e) => { if (callEventHandler(localProps.onBlur, e)) return; setFocused(false); if (hovered()) return; setActive(false); }; const onPointerDown = (e) => { if (callEventHandler(localProps.onPointerDown, e)) return; if (callEventHandler(localProps.onHandleDragStart, e)) return; const targetElement = e.target; let target = "handle"; if (targetElement.hasAttribute("data-corvu-resizable-handle-start-intersection")) { target = "startIntersection"; } if (targetElement.hasAttribute("data-corvu-resizable-handle-end-intersection")) { target = "endIntersection"; } globalHandleCallbacks?.onDragStart(e, target); }; return createComponent(Dynamic, mergeProps$1({ as: "button", ref(r$) { var _ref$ = mergeRefs(setRef, localProps.ref); typeof _ref$ === "function" && _ref$(r$); }, get style() { return combineStyle({ position: "relative", cursor: context().handleCursorStyle() ? "inherit" : undefined, "touch-action": "none", "flex-shrink": 0 }, localProps.style); }, get disabled() { return localProps.disabled; }, onBlur, onFocus, onKeyDown, onKeyUp, onMouseEnter, onMouseLeave, onPointerDown, role: "separator", get ["aria-controls"]() { return ariaInformation()?.ariaControls; }, get ["aria-orientation"]() { return context().orientation(); }, get ["aria-valuemax"]() { return ariaInformation()?.ariaValueMax; }, get ["aria-valuemin"]() { return ariaInformation()?.ariaValueMin; }, get ["aria-valuenow"]() { return ariaInformation()?.ariaValueNow; }, get ["data-active"]() { return dataIf(active()); }, get ["data-dragging"]() { return dataIf(dragging()); }, get ["data-orientation"]() { return context().orientation(); }, "data-corvu-resizable-handle": "" }, otherProps, { get children() { return [createComponent(Show, { get when() { return startIntersection(); }, get children() { var _el$ = _tmpl$(); _el$.addEventListener("mouseleave", (e) => { if (ref()?.contains(e.relatedTarget) === true) { setHovered("handle"); } else { setHovered(null); } }); _el$.addEventListener("mouseenter", () => setHovered("startIntersection")); _el$.style.setProperty("position", "absolute"); _el$.style.setProperty("aspect-ratio", "1 / 1"); _el$.style.setProperty("top", "0"); _el$.style.setProperty("left", "0"); _el$.style.setProperty("z-index", "1"); effect((_p$) => { var _v$ = context().orientation() === "horizontal" ? undefined : "100%", _v$2 = context().orientation() === "horizontal" ? "100%" : undefined, _v$3 = context().orientation() === "horizontal" ? "translate3d(0, -100%, 0)" : "translate3d(-100%, 0, 0)"; _v$ !== _p$.e && ((_p$.e = _v$) != null ? _el$.style.setProperty("height", _v$) : _el$.style.removeProperty("height")); _v$2 !== _p$.t && ((_p$.t = _v$2) != null ? _el$.style.setProperty("width", _v$2) : _el$.style.removeProperty("width")); _v$3 !== _p$.a && ((_p$.a = _v$3) != null ? _el$.style.setProperty("transform", _v$3) : _el$.style.removeProperty("transform")); return _p$; }, { e: undefined, t: undefined, a: undefined }); return _el$; } }), memo(() => localProps.children), createComponent(Show, { get when() { return endIntersection(); }, get children() { var _el$2 = _tmpl$2(); _el$2.addEventListener("mouseleave", (e) => { if (ref()?.contains(e.relatedTarget) === true) { setHovered("handle"); } else { setHovered(null); } }); _el$2.addEventListener("mouseenter", () => setHovered("endIntersection")); _el$2.style.setProperty("position", "absolute"); _el$2.style.setProperty("aspect-ratio", "1 / 1"); _el$2.style.setProperty("bottom", "0"); _el$2.style.setProperty("right", "0"); _el$2.style.setProperty("z-index", "1"); effect((_p$) => { var _v$4 = context().orientation() === "horizontal" ? undefined : "100%", _v$5 = context().orientation() === "horizontal" ? "100%" : undefined, _v$6 = context().orientation() === "horizontal" ? "translate3d(0, 100%, 0)" : "translate3d(100%, 0, 0)"; _v$4 !== _p$.e && ((_p$.e = _v$4) != null ? _el$2.style.setProperty("height", _v$4) : _el$2.style.removeProperty("height")); _v$5 !== _p$.t && ((_p$.t = _v$5) != null ? _el$2.style.setProperty("width", _v$5) : _el$2.style.removeProperty("width")); _v$6 !== _p$.a && ((_p$.a = _v$6) != null ? _el$2.style.setProperty("transform", _v$6) : _el$2.style.removeProperty("transform")); return _p$; }, { e: undefined, t: undefined, a: undefined }); return _el$2; } })]; } })); }; var Handle_default = ResizableHandle; var ResizablePanelContext = createContext(); var createResizablePanelContext = (contextId) => { if (contextId === undefined) return ResizablePanelContext; const context = createKeyedContext( `resizable-panel-${contextId}` ); return context; }; var useResizablePanelContext = (contextId) => { if (contextId === undefined) { const context2 = useContext(ResizablePanelContext); if (!context2) { throw new Error( "[corvu]: Resizable panel context not found. Make sure to call usePanelContext under <Resizable.Panel>" ); } return context2; } const context = useKeyedContext( `resizable-panel-${contextId}` ); if (!context) { throw new Error( `[corvu]: Resizable context with id "${contextId}" not found. Make sure to call usePanelContext under <Resizable.Panel contextId="${contextId}">` ); } return context; }; var ResizablePanel = (props) => { const defaultedProps = mergeProps({ initialSize: null, minSize: 0, maxSize: 1, collapsible: false, collapsedSize: 0, collapseThreshold: 0.05, panelId: createUniqueId() }, props); const [localProps, otherProps] = splitProps(defaultedProps, ["initialSize", "minSize", "maxSize", "collapsible", "collapsedSize", "collapseThreshold", "onResize", "onCollapse", "onExpand", "contextId", "panelId", "ref", "style", "children"]); const [ref, setRef] = createSignal(null); const context = createMemo(() => useInternalResizableContext(localProps.contextId)); const [panelInstance, setPanelInstance] = createSignal(null); createEffect(() => { const element = ref(); if (!element) return; const _context = context(); const instance = untrack(() => { return _context.registerPanel({ id: localProps.panelId, element, initialSize: localProps.initialSize, minSize: localProps.minSize, maxSize: localProps.maxSize, collapsible: localProps.collapsible, collapsedSize: localProps.collapsedSize, collapseThreshold: localProps.collapseThreshold, onResize: localProps.onResize }); }); setPanelInstance(instance); onCleanup(() => { _context.unregisterPanel(instance.data.id); }); }); const panelSize = () => { const instance = panelInstance(); if (!instance) { if (typeof localProps.initialSize === "number") { return localProps.initialSize; } return 1; } return instance.size(); }; const collapsed = createMemo((prev) => { const instance = panelInstance(); if (localProps.collapsible !== true) { return false; } const collapsed2 = instance ? instance.size() === resolveSize(localProps.collapsedSize, context().rootSize()) : false; if (instance && prev !== collapsed2) { if (collapsed2 && localProps.onCollapse !== undefined) { localProps.onCollapse(instance.size()); } else if (!collapsed2 && localProps.onExpand !== undefined) { localProps.onExpand(instance.size()); } } return collapsed2; }); const resize2 = (size, strategy) => { const instance = panelInstance(); if (!instance) { return; } instance.resize(size, strategy ?? "both"); }; const collapse = (strategy) => { const instance = panelInstance(); if (!instance) { return; } instance.collapse(strategy ?? "both"); }; const expand = (strategy) => { const instance = panelInstance(); if (!instance) { return; } instance.expand(strategy ?? "both"); }; const childrenProps = { get size() { return panelSize(); }, get minSize() { return localProps.minSize; }, get maxSize() { return localProps.maxSize; }, get collapsible() { return localProps.collapsible; }, get collapsedSize() { return localProps.collapsedSize; }, get collapseThreshold() { return localProps.collapseThreshold; }, get collapsed() { return collapsed(); }, resize: resize2, collapse, expand, get panelId() { return localProps.panelId; } }; const memoizedChildren = createOnce(() => localProps.children); const resolveChildren = () => { const children = memoizedChildren()(); if (isFunction(children)) { return children(childrenProps); } return children; }; const memoizedResizablePanel = createMemo(() => { const ResizablePanelContext2 = createResizablePanelContext(localProps.contextId); return createComponent(ResizablePanelContext2.Provider, { value: { size: panelSize, minSize: () => localProps.minSize, maxSize: () => localProps.maxSize, collapsible: () => localProps.collapsible, collapsedSize: () => localProps.collapsedSize, collapseThreshold: () => localProps.collapseThreshold, collapsed, resize: resize2, collapse, expand, panelId: () => localProps.panelId }, get children() { return createComponent(Dynamic, mergeProps$1({ as: "div", ref(r$) { var _ref$ = mergeRefs(setRef, localProps.ref); typeof _ref$ === "function" && _ref$(r$); }, get style() { return combineStyle({ "flex-basis": panelSize() * 100 + "%" }, localProps.style); }, get id() { return localProps.panelId; }, get ["data-collapsed"]() { return dataIf(collapsed()); }, get ["data-expanded"]() { return dataIf(localProps.collapsible === true && !collapsed()); }, get ["data-orientation"]() { return context().orientation(); }, "data-corvu-resizable-panel": "" }, otherProps, { get children() { return untrack(() => resolveChildren()); } })); } }); }); return memoizedResizablePanel; }; var Panel_default = ResizablePanel; var getDistributablePercentage = (props) => { let distributablePercentage = props.desiredPercentage >= 0 ? Infinity : -Infinity; let newSizes = props.initialSizes; for (const resizeAction of props.resizeActions) { const desiredPercentage = resizeAction.negate !== true ? props.desiredPercentage : -props.desiredPercentage; let [ distributedPercentagePreceding, // eslint-disable-next-line prefer-const distributedSizesPreceding, // eslint-disable-next-line prefer-const collapsedPreceding ] = distributePercentage({ desiredPercentage, side: "preceding", panels: resizeAction.precedingPanels, initialSizes: newSizes, initialSizesStartIndex: 0, collapsible: props.collapsible, rootSize: props.resizableData.rootSize }); let [ distributedPercentageFollowing, // eslint-disable-next-line prefer-const distributedSizesFollowing, // eslint-disable-next-line prefer-const collapsedFollowing ] = distributePercentage({ desiredPercentage, side: "following", panels: resizeAction.followingPanels, initialSizes: newSizes, initialSizesStartIndex: resizeAction.precedingPanels.length, collapsible: props.collapsible, rootSize: props.resizableData.rootSize }); if (resizeAction.negate === true) { distributedPercentagePreceding = -distributedPercentagePreceding; distributedPercentageFollowing = -distributedPercentageFollowing; } if (collapsedPreceding) { distributedPercentageFollowing = distributedPercentagePreceding; } if (collapsedFollowing) { distributedPercentagePreceding = distributedPercentageFollowing; } if (props.desiredPercentage >= 0) { distributablePercentage = Math.min( distributablePercentage, Math.min( distributedPercentagePreceding, distributedPercentageFollowing ) ); } else { distributablePercentage = Math.max( distributablePercentage, Math.max( distributedPercentagePreceding, distributedPercentageFollowing ) ); } newSizes = [...distributedSizesPreceding, ...distributedSizesFollowing]; } return distributablePercentage; }; var distributePercentage = (props) => { props.desiredPercentage = fixToPrecision(props.desiredPercentage); const resizeDirection = getResizeDirection({ side: props.side, desiredPercentage: props.desiredPercentage }); let distributedPercentage = 0; const distributedSizes = props.initialSizes.slice( props.initialSizesStartIndex, props.initialSizesStartIndex + props.panels.length ); for (let i = props.side === "preceding" ? props.panels.length - 1 : 0; props.side === "preceding" ? i >= 0 : i < props.panels.length; props.side === "preceding" ? i-- : i++) { const panel2 = props.panels[i]; const panelSize2 = props.initialSizes[i + props.initialSizesStartIndex]; const collapsedSize2 = resolveSize( panel2.data.collapsedSize ?? 0, props.rootSize ); if (panel2.data.collapsible && panelSize2 === collapsedSize2) continue; const availablePercentage2 = fixToPrecision( props.desiredPercentage - distributedPercentage ); if (availablePercentage2 === 0) break; switch (resizeDirection) { case "precedingDecreasing": { const minSize2 = resolveSize(panel2.data.minSize, props.rootSize); distributedSizes[i] = Math.max(minSize2, panelSize2 + availablePercentage2); distributedPercentage += distributedSizes[i] - panelSize2; break; } case "followingDecreasing": { const minSize2 = resolveSize(panel2.data.minSize, props.rootSize); distributedSizes[i] = Math.max(minSize2, panelSize2 - availablePercentage2); distributedPercentage -= distributedSizes[i] - panelSize2; break; } case "precedingIncreasing": { const maxSize = resolveSize(panel2.data.maxSize, props.rootSize); distributedSizes[i] = Math.min(maxSize, panelSize2 + availablePercentage2); distributedPercentage += distributedSizes[i] - panelSize2; break; } case "followingIncreasing": { const maxSize = resolveSize(panel2.data.maxSize, props.rootSize); distributedSizes[i] = Math.min(maxSize, panelSize2 - availablePercentage2); distributedPercentage -= distributedSizes[i] - panelSize2; break; } } } distributedPercentage = fixToPrecision(distributedPercentage); if (!props.collapsible || distributedPercentage === props.desiredPercentage) { return [distributedPercentage, distributedSizes, false]; } const panelIndex = props.side === "preceding" ? props.panels.length - 1 : 0; const panel = props.panels[panelIndex]; if (!panel.data.collapsible) { return [distributedPercentage, distributedSizes, false]; } const availablePercentage = fixToPrecision( props.desiredPercentage - distributedPercentage ); let collapsed = false; const panelSize = props.initialSizes[panelIndex + props.initialSizesStartIndex]; const minSize = resolveSize(panel.data.minSize, props.rootSize); const collapsedSize = resolveSize( panel.data.collapsedSize ?? 0, props.rootSize ); const collapseThreshold = Math.min( resolveSize(panel.data.collapseThreshold ?? 0, props.rootSize), minSize - collapsedSize ); const isCollapsed = panelSize === collapsedSize; if (resizeDirection === "precedingDecreasing" && !isCollapsed && Math.abs(availablePercentage) >= collapseThreshold) { distributedPercentage -= distributedSizes[panelIndex] - panelSize; distributedSizes[panelIndex] = collapsedSize; distributedPercentage += distributedSizes[panelIndex] - panelSize; collapsed = true; } else if (resizeDirection === "precedingIncreasing" && isCollapsed && Math.abs(availablePercentage) >= collapseThreshold) { const minSize2 = resolveSize(panel.data.minSize, props.rootSize); distributedSizes[panelIndex] = minSize2; if (Math.abs(availablePercentage) >= minSize2 - collapsedSize) { const maxSize = resolveSize(panel.data.maxSize, props.rootSize); distributedSizes[panelIndex] = Math.min( maxSize, panelSize + availablePercentage ); } else { collapsed = true; } distributedPercentage += distributedSizes[panelIndex] - panelSize; } else if (resizeDirection === "followingDecreasing" && !isCollapsed && Math.abs(availablePercentage) >= collapseThreshold) { distributedPercentage += distributedSizes[panelIndex] - panelSize; distributedSizes[panelIndex] = collapsedSize; distributedPercentage -= distributedSizes[panelIndex] - panelSize; collapsed = true; } else if (resizeDirection === "followingIncreasing" && isCollapsed && Math.abs(availablePercentage) >= collapseThreshold) { const minSize2 = resolveSize(panel.data.minSize, props.rootSize); distributedSizes[panelIndex] = minSize2; if (Math.abs(availablePercentage) >= minSize2 - collapsedSize) { const maxSize = resolveSize(panel.data.maxSize, props.rootSize); distributedSizes[panelIndex] = Math.min( maxSize, panelSize - availablePercentage ); } else { collapsed = true; } distributedPercentage -= distributedSizes[panelIndex] - panelSize; } return [distributedPercentage, distributedSizes, collapsed]; }; var getResizeDirection = (props) => { switch (props.side) { case "preceding": return props.desiredPercentage >= 0 ? "precedingIncreasing" : "precedingDecreasing"; case "following": return props.desiredPercentage >= 0 ? "followingDecreasing" : "followingIncreasing"; } }; var resize = (props) => { let newSizes = props.initialSizes; for (const resizeAction of props.resizeActions) { const [, distributedSizesPreceding] = distributePercentage({ desiredPercentage: resizeAction.deltaPercentage, side: "preceding", panels: resizeAction.precedingPanels, initialSizes: newSizes, initialSizesStartIndex: 0, collapsible: props.collapsible, rootSize: props.resizableData.rootSize }); const [, distributedSizesFollowing] = distributePercentage({ desiredPercentage: resizeAction.deltaPercentage, side: "following", panels: resizeAction.followingPanels, initialSizes: newSizes, initialSizesStartIndex: resizeAction.precedingPanels.length, collapsible: props.collapsible, rootSize: props.resizableData.rootSize }); newSizes = [...distributedSizesPreceding, ...distributedSizesFollowing]; } newSizes = newSizes.map(fixToPrecision); const totalSize = newSizes.reduce((totalSize2, size) => totalSize2 + size, 0); if (totalSize !== 1) { const offset = totalSize - 1; const offsetPerPanel = offset / newSizes.length; newSizes = newSizes.map((size) => size - offsetPerPanel); } props.resizableData.setSizes(newSizes.map(fixToPrecision)); }; var resizePanel = (props) => { let [precedingPanels, followingPanels] = splitPanels({ panels: props.panels, focusedElement: props.panel.data.element }); const panelIndex = props.panels.indexOf(props.panel); if (panelIndex === 0) { props.strategy = "following"; } else if (panelIndex === props.panels.length - 1) { props.strategy = "preceding"; } if (props.strategy === "both") { const precedingPanelsIncluding = [...precedingPanels, props.panel]; const followingPanelsIncluding = [props.panel, ...followingPanels]; const distributablePercentage = getDistributablePercentage({ desiredPercentage: props.deltaPercentage / 2, initialSizes: props.initialSizes, collapsible: true, resizeActions: [ { precedingPanels: precedingPanelsIncluding, followingPanels }, { precedingPanels, followingPanels: followingPanelsIncluding, negate: true } ], resizableData: { rootSize: props.resizableData.rootSize } }); resize({ initialSizes: props.initialSizes, collapsible: true, resizeActions: [ { precedingPanels: precedingPanelsIncluding, followingPanels, deltaPercentage: distributablePercentage }, { precedingPanels, followingPanels: followingPanelsIncluding, deltaPercentage: -distributablePercentage } ], resizableData: props.resizableData }); } else { precedingPanels = props.strategy === "preceding" ? precedingPanels : [...precedingPanels, props.panel]; followingPanels = props.strategy === "following" ? followingPanels : [props.panel, ...followingPanels]; if (props.strategy === "preceding") { props.deltaPercentage = -props.deltaPercentage; } const distributablePercentage = getDistributablePercentage({ desiredPercentage: props.deltaPercentage, initialSizes: props.initialSizes, collapsible: props.collapsible, resizeActions: [ { precedingPanels, followingPanels } ], resizableData: { rootSize: props.resizableData.rootSize } }); resize({ initialSizes: props.initialSizes, collapsible: true, resizeActions: [ { precedingPanels, followingPanels, deltaPercentage: distributablePercentage } ], resizableData: props.resizableData }); } }; var deltaResize = (props) => { if (props.altKey && props.panels.length > 2) { let panelIndex = props.panels.filter( (panel2) => !!(props.handle.compareDocumentPosition(panel2.data.element) & Node.DOCUMENT_POSITION_PRECEDING) ).length - 1; const isPrecedingHandle = panelIndex === 0; if (isPrecedingHandle) { panelIndex++; props.deltaPercentage = -props.deltaPercentage; } const panel = props.panels[panelIndex]; const panelSize = props.initialSizes[panelIndex]; const minDelta = resolveSize(panel.data.minSize, props.resizableData.rootSize) - panelSize; const maxDelta = resolveSize(panel.data.maxSize, props.resizableData.rootSize) - panelSize; const cappedDeltaPercentage = Math.max(minDelta, Math.min(props.deltaPercentage * 2, maxDelta)) / 2; const [precedingPanels, followingPanels] = splitPanels({ panels: props.panels, focusedElement: panel.data.element }); const precedingPanelsIncluding = [...precedingPanels, panel]; const followingPanelsIncluding = [panel, ...followingPanels]; const distributablePercentage = getDistributablePercentage({ desiredPercentage: cappedDeltaPercentage, initialSizes: props.initialSizes, collapsible: false, resizeActions: [ { precedingPanels: precedingPanelsIncluding, followingPanels }, { precedingPanels, followingPanels: followingPanelsIncluding, negate: true } ], resizableData: { rootSize: props.resizableData.rootSize } }); if (props.resizableData.handleCursorStyle === true) { handleResizeConstraints({ orientation: props.resizableData.orientation, desiredPercentage: props.deltaPercentage, distributablePercentage, revertConstraints: isPrecedingHandle }); } resize({ initialSizes: props.initialSizes, collapsible: false, resizeActions: [ { precedingPanels: precedingPanelsIncluding, followingPanels, deltaPercentage: distributablePercentage }, { precedingPanels, followingPanels: followingPanelsIncluding, deltaPercentage: -distributablePercentage } ], resizableData: props.resizableData }); } else { const [precedingPanels, followingPanels] = splitPanels({ panels: props.panels, focusedElement: props.handle }); const distributablePercentage = getDistributablePercentage({ desiredPercentage: props.deltaPercentage, initialSizes: props.initialSizes, collapsible: true, resizeActions: [ { precedingPanels, followingPanels } ], resizableData: { rootSize: props.resizableData.rootSize } }); resize({ initialSizes: props.initialSizes, collapsible: true, resizeActions: [ { precedingPanels, followingPanels, deltaPercentage: distributablePercentage } ], resizableData: props.resizableData }); if (props.resizableData.handleCursorStyle) { const fixedDesiredPercentage = fixToPrecision(props.deltaPercentage); const fixedDistributablePercentage = fixToPrecision( distributablePercentage ); let betweenCollapse = false; const precedingPanel = precedingPanels[precedingPanels.length - 1]; if (precedingPanel.data.collapsible) { const precedingCollapsedSize = resolveSize( precedingPanel.data.collapsedSize ?? 0, props.resizableData.rootSize ); if (precedingPanel.size() === precedingCollapsedSize && fixedDesiredPercentage > fixedDistributablePercentage || precedingPanel.size() !== precedingCollapsedSize && fixedDesiredPercentage < fixedDistributablePercentage) { betweenCollapse = true; } } const followingPanel = followingPanels[0]; if (followingPanel.data.collapsible) { const followingCollapsedSize = resolveSize( followingPanel.data.collapsedSize ?? 0, props.resizableData.rootSize ); if (followingPanel.size() === followingCollapsedSize && fixedDesiredPercentage < fixedDistributablePercentage || followingPanel.size() !== followingCollapsedSize && fixedDesiredPercentage > fixedDistributablePercentage) { betweenCollapse = true; } } handleResizeConstraints({ orientation: props.resizableData.orientation, desiredPercentage: props.deltaPercentage, distributablePercentage, betweenCollapse }); } } }; var ResizableRoot = (props) => { const defaultedProps = mergeProps({ orientation: "horizontal", initialSizes: [], keyboardDelta: 0.1, handleCursorStyle: true }, props); const [localProps, otherProps] = splitProps(defaultedProps, ["orientation", "sizes", "onSizesChange", "initialSizes", "keyboardDelta", "handleCursorStyle", "contextId", "ref", "style", "children"]); const [sizes, setSizes] = createControllableSignal({ value: () => localProps.sizes, initialValue: [], onChange: localProps.onSizesChange }); const [ref, setRef] = createSignal(null); const rootSize = createSize({ element: ref, dimension: () => localProps.orientation === "horizontal" ? "width" : "height" }); const [panels, setPanels] = createSignal([]); const sizesToIds = []; const registerPanel = (panelData) => { const _panels = panels(); const panelIndex = _panels.filter((panel2) => !!(panelData.element.compareDocumentPosition(panel2.data.element) & Node.DOCUMENT_POSITION_PRECEDING)).length; const idExists = sizesToIds[panelIndex] === undefined || sizesToIds[panelIndex] === panelData.id; const sizeExists = sizes()[panelIndex] !== undefined; let panelSize = null; if (panelData.initialSize !== null) { panelSize = resolveSize(panelData.initialSize, rootSize()); } else if (localProps.initialSizes[panelIndex] !== undefined && idExists) { panelSize = resolveSize(localProps.initialSizes[panelIndex], rootSize()); } panelSize = panelSize ?? 0.5; setSizes((sizes2) => { let newSizes = [...sizes2]; const previousTotalSize = newSizes.reduce((totalSize, size) => totalSize + size, 0); if ((idExists && !sizeExists || !idExists) && previousTotalSize === 1) { const offsetPerPanel = panelSize / newSizes.length; newSizes = newSizes.map((size) => size - offsetPerPanel); } if (idExists) { if (!sizeExists) { newSizes[panelIndex] = panelSize; } sizesToIds[panelIndex] = panelData.id; } else { newSizes.splice(panelIndex, 0, panelSize); sizesToIds.splice(panelIndex, 0, panelData.id); } return newSizes; }); const panelSizeMemo = createMemo(() => { const index = sizesToIds.indexOf(panelData.id); return sizes()[index]; }); createEffect(() => panelData.onResize?.(panelSizeMemo())); const panel = { data: panelData, size: panelSizeMemo, resize: (size, strategy) => resize2(sizesToIds.indexOf(panelData.id), size, strategy), collapse: (strategy) => collapse(sizesToIds.indexOf(panelData.id), strategy), expand: (strategy) => expand(sizesToIds.indexOf(panelData.id), strategy) }; setPanels((panels2) => { const newPanels = [...panels2]; newPanels.push(panel); newPanels.sort((a, b) => sortByDocumentPosition(a.data.element, b.data.element)); return newPanels; }); return panel; }; const unregisterPanel = (id) => { setPanels((panels2) => panels2.filter((panel) => panel.data.id !== id)); const panelSizeIndex = sizesToIds.indexOf(id); sizesToIds.splice(panelSizeIndex, 1); setSizes((sizes2) => { let newSizes = [...sizes2]; newSizes.splice(panelSizeIndex, 1); const totalSize = newSizes.reduce((totalSize2, size) => totalSize2 + size, 0); const offset = totalSize - 1; const offsetPerPanel = offset / newSizes.length; newSizes = newSizes.map((size) => size + offsetPerPanel); return newSizes; }); }; createEffect(() => { if (localProps.onSizesChange !== undefined) { localProps.onSizesChange(sizes()); } }); const resize2 = (panelIndex, size, strategy) => { untrack(() => { const panel = panels()[panelIndex]; if (!panel) return; const minSize = resolveSize(panel.data.minSize, rootSize()); const maxSize = resolveSize(panel.data.maxSize, rootSize()); const newSize = resolveSize(size, rootSize()); const allowedSize = Math.max(minSize, Math.min(newSize, maxSize)); const deltaPercentage = allowedSize - sizes()[panelIndex]; resizePanel({ deltaPercentage, strategy: strategy ?? "both", panel, panels: panels(), initialSizes: panels().map((panel2) => panel2.size()), collapsible: false, resizableData: { rootSize: rootSize(), orientation: localProps.orientation, setSizes } }); }); }; const collapse = (panelIndex, strategy) => { untrack(() => { const panel = panels()[panelIndex]; if (!panel) return; const panelSize = sizes()[panelIndex]; const collapsedSize = resolveSize(panel.data.collapsedSize ?? 0, rootSize()); if (!panel.data.collapsible || panelSize === collapsedSize) return; const deltaPercentage = collapsedSize - panelSize; resizePanel({ deltaPercentage, strategy: strategy ?? "both", panel, panels: panels(), initialSizes: panels().map((panel2) => panel2.size(