UNPKG

@zag-js/splitter

Version:

Core logic for the splitter widget implemented as a state machine

1,131 lines (1,123 loc) • 39.3 kB
'use strict'; var anatomy$1 = require('@zag-js/anatomy'); var domQuery = require('@zag-js/dom-query'); var utils = require('@zag-js/utils'); var core = require('@zag-js/core'); var types = require('@zag-js/types'); // src/splitter.anatomy.ts var anatomy = anatomy$1.createAnatomy("splitter").parts("root", "panel", "resizeTrigger"); var parts = anatomy.build(); var getRootId = (ctx) => ctx.ids?.root ?? `splitter:${ctx.id}`; var getResizeTriggerId = (ctx, id) => ctx.ids?.resizeTrigger?.(id) ?? `splitter:${ctx.id}:splitter:${id}`; var getPanelId = (ctx, id) => ctx.ids?.panel?.(id) ?? `splitter:${ctx.id}:panel:${id}`; var getGlobalCursorId = (ctx) => `splitter:${ctx.id}:global-cursor`; var getRootEl = (ctx) => ctx.getById(getRootId(ctx)); var getResizeTriggerEl = (ctx, id) => ctx.getById(getResizeTriggerId(ctx, id)); var getCursor = (state, x) => { let cursor = x ? "col-resize" : "row-resize"; if (state.isAtMin) cursor = x ? "e-resize" : "s-resize"; if (state.isAtMax) cursor = x ? "w-resize" : "n-resize"; return cursor; }; var getResizeTriggerEls = (ctx) => { return domQuery.queryAll(getRootEl(ctx), `[role=separator][data-ownedby='${CSS.escape(getRootId(ctx))}']`); }; var setupGlobalCursor = (ctx, state, x, nonce) => { const styleEl = ctx.getById(getGlobalCursorId(ctx)); const textContent = `* { cursor: ${getCursor(state, x)} !important; }`; if (styleEl) { styleEl.textContent = textContent; } else { const style = ctx.getDoc().createElement("style"); if (nonce) style.nonce = nonce; style.id = getGlobalCursorId(ctx); style.textContent = textContent; ctx.getDoc().head.appendChild(style); } }; var removeGlobalCursor = (ctx) => { const styleEl = ctx.getById(getGlobalCursorId(ctx)); styleEl?.remove(); }; function calculateAriaValues({ size, panels, pivotIndices }) { let currentMinSize = 0; let currentMaxSize = 100; let totalMinSize = 0; let totalMaxSize = 0; const firstIndex = pivotIndices[0]; utils.ensure(firstIndex, () => "No pivot index found"); panels.forEach((panel, index) => { const { maxSize = 100, minSize = 0 } = panel; if (index === firstIndex) { currentMinSize = minSize; currentMaxSize = maxSize; } else { totalMinSize += minSize; totalMaxSize += maxSize; } }); const valueMax = Math.min(currentMaxSize, 100 - totalMinSize); const valueMin = Math.max(currentMinSize, 100 - totalMaxSize); const valueNow = size[firstIndex]; return { valueMax, valueMin, valueNow }; } function getAriaValue(size, panels, handleId) { const [beforeId, afterId] = handleId.split(":"); const beforeIndex = panels.findIndex((panel) => panel.id === beforeId); const afterIndex = panels.findIndex((panel) => panel.id === afterId); const { valueMax, valueMin, valueNow } = calculateAriaValues({ size, panels, pivotIndices: [beforeIndex, afterIndex] }); return { beforeId, afterId, valueMax: Math.round(valueMax), valueMin: Math.round(valueMin), valueNow: valueNow != null ? Math.round(valueNow) : void 0 }; } // src/utils/fuzzy.ts var PRECISION = 10; function fuzzyCompareNumbers(actual, expected, fractionDigits = PRECISION) { if (actual.toFixed(fractionDigits) === expected.toFixed(fractionDigits)) { return 0; } else { return actual > expected ? 1 : -1; } } function fuzzyNumbersEqual(actual, expected, fractionDigits = PRECISION) { if (actual == null || expected == null) return false; return fuzzyCompareNumbers(actual, expected, fractionDigits) === 0; } function fuzzySizeEqual(actual, expected, fractionDigits) { if (actual.length !== expected.length) { return false; } for (let index = 0; index < actual.length; index++) { const actualSize = actual[index]; const expectedSize = expected[index]; if (!fuzzyNumbersEqual(actualSize, expectedSize, fractionDigits)) { return false; } } return true; } function getPanelById(panels, id) { const panel = panels.find((panel2) => panel2.id === id); utils.ensure(panel, () => `Panel data not found for id "${id}"`); return panel; } function findPanelDataIndex(panels, panel) { return panels.findIndex((prevPanel) => prevPanel === panel || prevPanel.id === panel.id); } function findPanelIndex(panels, id) { return panels.findIndex((panel) => panel.id === id); } function panelDataHelper(panels, panel, sizes) { const index = findPanelIndex(panels, panel.id); const pivotIndices = index === panels.length - 1 ? [index - 1, index] : [index, index + 1]; const panelSize = sizes[index]; return { ...panel, panelSize, pivotIndices }; } function sortPanels(panels) { return panels.sort((panelA, panelB) => { const orderA = panelA.order; const orderB = panelB.order; if (orderA == null && orderB == null) { return 0; } else if (orderA == null) { return -1; } else if (orderB == null) { return 1; } else { return orderA - orderB; } }); } function getPanelLayout(panels) { return panels.map((panel) => panel.id).sort().join(":"); } function serializePanels(panels) { const keys = panels.map((panel) => panel.id); const sortedKeys = keys.sort(); const serialized = sortedKeys.map((key) => { const panel = panels.find((panel2) => panel2.id === key); return JSON.stringify(panel); }); return serialized.join(","); } function getPanelFlexBoxStyle({ defaultSize, dragState, sizes, panels, panelIndex, precision = 3 }) { const size = sizes[panelIndex]; let flexGrow; if (size == null) { flexGrow = defaultSize != void 0 ? defaultSize.toPrecision(precision) : "1"; } else if (panels.length === 1) { flexGrow = "1"; } else { flexGrow = size.toPrecision(precision); } return { flexBasis: 0, flexGrow, flexShrink: 1, // Without this, Panel sizes may be unintentionally overridden by their content overflow: "hidden", // Disable pointer events inside of a panel during resize // This avoid edge cases like nested iframes pointerEvents: dragState !== null ? "none" : void 0 }; } function getUnsafeDefaultSize({ panels, size: sizes }) { const finalSizes = Array(panels.length); let numPanelsWithSizes = 0; let remainingSize = 100; for (let index = 0; index < panels.length; index++) { const panel = panels[index]; utils.ensure(panel, () => `Panel data not found for index ${index}`); const defaultSize = sizes[index]; if (defaultSize != null) { numPanelsWithSizes++; finalSizes[index] = defaultSize; remainingSize -= defaultSize; } } for (let index = 0; index < panels.length; index++) { const panel = panels[index]; utils.ensure(panel, () => `Panel data not found for index ${index}`); const defaultSize = sizes[index]; if (defaultSize != null) { continue; } const numRemainingPanels = panels.length - numPanelsWithSizes; const size = remainingSize / numRemainingPanels; numPanelsWithSizes++; finalSizes[index] = size; remainingSize -= size; } return finalSizes; } // src/splitter.connect.ts function connect(service, normalize) { const { state, send, prop, computed, context, scope } = service; const horizontal = computed("horizontal"); const dragging = state.matches("dragging"); const getPanelStyle = (id) => { const panels = prop("panels"); const panelIndex = panels.findIndex((panel) => panel.id === id); const defaultSize = context.initial("size")[panelIndex]; const dragState = context.get("dragState"); return getPanelFlexBoxStyle({ defaultSize, dragState, sizes: context.get("size"), panels, panelIndex }); }; return { dragging, getItems() { return prop("panels").flatMap((panel, index, arr) => { const nextPanel = arr[index + 1]; if (panel && nextPanel) { return [ { type: "panel", id: panel.id }, { type: "handle", id: `${panel.id}:${nextPanel.id}` } ]; } return [{ type: "panel", id: panel.id }]; }); }, getSizes() { return context.get("size"); }, setSizes(size) { send({ type: "SIZE.SET", size }); }, resetSizes() { send({ type: "SIZE.SET", size: context.initial("size") }); }, collapsePanel(id) { send({ type: "PANEL.COLLAPSE", id }); }, expandPanel(id, minSize) { send({ type: "PANEL.EXPAND", id, minSize }); }, resizePanel(id, unsafePanelSize) { send({ type: "PANEL.RESIZE", id, size: unsafePanelSize }); }, getPanelSize(id) { const panels = prop("panels"); const size = context.get("size"); const panelData = getPanelById(panels, id); const { panelSize } = panelDataHelper(panels, panelData, size); utils.ensure(panelSize, () => `Panel size not found for panel "${panelData.id}"`); return panelSize; }, isPanelCollapsed(id) { const panels = prop("panels"); const size = context.get("size"); const panelData = getPanelById(panels, id); const { collapsedSize = 0, collapsible, panelSize } = panelDataHelper(panels, panelData, size); utils.ensure(panelSize, () => `Panel size not found for panel "${panelData.id}"`); return collapsible === true && fuzzyNumbersEqual(panelSize, collapsedSize); }, isPanelExpanded(id) { const panels = prop("panels"); const size = context.get("size"); const panelData = getPanelById(panels, id); const { collapsedSize = 0, collapsible, panelSize } = panelDataHelper(panels, panelData, size); utils.ensure(panelSize, () => `Panel size not found for panel "${panelData.id}"`); return !collapsible || fuzzyCompareNumbers(panelSize, collapsedSize) > 0; }, getLayout() { return getPanelLayout(prop("panels")); }, getRootProps() { return normalize.element({ ...parts.root.attrs, "data-orientation": prop("orientation"), id: getRootId(scope), dir: prop("dir"), style: { display: "flex", flexDirection: horizontal ? "row" : "column", height: "100%", width: "100%", overflow: "hidden" } }); }, getPanelProps(props2) { const { id } = props2; return normalize.element({ ...parts.panel.attrs, "data-orientation": prop("orientation"), dir: prop("dir"), "data-id": id, "data-index": findPanelIndex(prop("panels"), id), id: getPanelId(scope, id), "data-ownedby": getRootId(scope), style: getPanelStyle(id) }); }, getResizeTriggerProps(props2) { const { id, disabled } = props2; const aria = getAriaValue(context.get("size"), prop("panels"), id); const dragging2 = context.get("dragState")?.resizeTriggerId === id; const focused = dragging2 || context.get("keyboardState")?.resizeTriggerId === id; return normalize.element({ ...parts.resizeTrigger.attrs, dir: prop("dir"), id: getResizeTriggerId(scope, id), role: "separator", "data-id": id, "data-ownedby": getRootId(scope), tabIndex: disabled ? void 0 : 0, "aria-valuenow": aria.valueNow, "aria-valuemin": aria.valueMin, "aria-valuemax": aria.valueMax, "data-orientation": prop("orientation"), "aria-orientation": prop("orientation"), "aria-controls": `${getPanelId(scope, aria.beforeId)} ${getPanelId(scope, aria.afterId)}`, "data-focus": domQuery.dataAttr(focused), "data-disabled": domQuery.dataAttr(disabled), style: { touchAction: "none", userSelect: "none", WebkitUserSelect: "none", flex: "0 0 auto", pointerEvents: dragging2 && !focused ? "none" : void 0, cursor: horizontal ? "col-resize" : "row-resize", [horizontal ? "minHeight" : "minWidth"]: "0" }, onPointerDown(event) { if (!domQuery.isLeftClick(event)) return; if (disabled) { event.preventDefault(); return; } const point = domQuery.getEventPoint(event); send({ type: "POINTER_DOWN", id, point }); event.currentTarget.setPointerCapture(event.pointerId); event.preventDefault(); event.stopPropagation(); }, onPointerUp(event) { if (disabled) return; if (event.currentTarget.hasPointerCapture(event.pointerId)) { event.currentTarget.releasePointerCapture(event.pointerId); } }, onPointerOver() { if (disabled) return; send({ type: "POINTER_OVER", id }); }, onPointerLeave() { if (disabled) return; send({ type: "POINTER_LEAVE", id }); }, onBlur() { if (disabled) return; send({ type: "BLUR" }); }, onFocus() { if (disabled) return; send({ type: "FOCUS", id }); }, onKeyDown(event) { if (event.defaultPrevented) return; if (disabled) return; const keyboardResizeBy = prop("keyboardResizeBy"); let delta = 0; if (event.shiftKey) { delta = 10; } else if (keyboardResizeBy != null) { delta = keyboardResizeBy; } else { delta = 1; } const keyMap = { Enter() { send({ type: "ENTER", id }); }, ArrowUp() { send({ type: "KEYBOARD_MOVE", id, delta: horizontal ? 0 : -delta }); }, ArrowDown() { send({ type: "KEYBOARD_MOVE", id, delta: horizontal ? 0 : delta }); }, ArrowLeft() { send({ type: "KEYBOARD_MOVE", id, delta: horizontal ? -delta : 0 }); }, ArrowRight() { send({ type: "KEYBOARD_MOVE", id, delta: horizontal ? delta : 0 }); }, Home() { send({ type: "KEYBOARD_MOVE", id, delta: -100 }); }, End() { send({ type: "KEYBOARD_MOVE", id, delta: 100 }); }, F6() { send({ type: "FOCUS.CYCLE", id, shiftKey: event.shiftKey }); } }; const key = domQuery.getEventKey(event, { dir: prop("dir"), orientation: prop("orientation") }); const exec = keyMap[key]; if (exec) { exec(event); event.preventDefault(); } } }); } }; } function resizePanel({ panels, index, size }) { const panel = panels[index]; utils.ensure(panel, () => `Panel data not found for index ${index}`); let { collapsedSize = 0, collapsible, maxSize = 100, minSize = 0 } = panel; if (fuzzyCompareNumbers(size, minSize) < 0) { if (collapsible) { const halfwayPoint = (collapsedSize + minSize) / 2; if (fuzzyCompareNumbers(size, halfwayPoint) < 0) { size = collapsedSize; } else { size = minSize; } } else { size = minSize; } } size = Math.min(maxSize, size); size = parseFloat(size.toFixed(PRECISION)); return size; } // src/utils/resize-by-delta.ts function resizeByDelta(props2) { let { delta, initialSize, panels, pivotIndices, prevSize, trigger } = props2; if (fuzzyNumbersEqual(delta, 0)) { return initialSize; } const nextSize = [...initialSize]; const [firstPivotIndex, secondPivotIndex] = pivotIndices; utils.ensure(firstPivotIndex, () => "Invalid first pivot index"); utils.ensure(secondPivotIndex, () => "Invalid second pivot index"); let deltaApplied = 0; { if (trigger === "keyboard") { { const index = delta < 0 ? secondPivotIndex : firstPivotIndex; const panel = panels[index]; utils.ensure(panel, () => `Panel data not found for index ${index}`); const { collapsedSize = 0, collapsible, minSize = 0 } = panel; if (collapsible) { const prevSize2 = initialSize[index]; utils.ensure(prevSize2, () => `Previous size not found for panel index ${index}`); if (fuzzyNumbersEqual(prevSize2, collapsedSize)) { const localDelta = minSize - prevSize2; if (fuzzyCompareNumbers(localDelta, Math.abs(delta)) > 0) { delta = delta < 0 ? 0 - localDelta : localDelta; } } } } { const index = delta < 0 ? firstPivotIndex : secondPivotIndex; const panel = panels[index]; utils.ensure(panel, () => `No panel data found for index ${index}`); const { collapsedSize = 0, collapsible, minSize = 0 } = panel; if (collapsible) { const prevSize2 = initialSize[index]; utils.ensure(prevSize2, () => `Previous size not found for panel index ${index}`); if (fuzzyNumbersEqual(prevSize2, minSize)) { const localDelta = prevSize2 - collapsedSize; if (fuzzyCompareNumbers(localDelta, Math.abs(delta)) > 0) { delta = delta < 0 ? 0 - localDelta : localDelta; } } } } } } { const increment = delta < 0 ? 1 : -1; let index = delta < 0 ? secondPivotIndex : firstPivotIndex; let maxAvailableDelta = 0; while (true) { const prevSize2 = initialSize[index]; utils.ensure(prevSize2, () => `Previous size not found for panel index ${index}`); const maxSafeSize = resizePanel({ panels, index, size: 100 }); const delta2 = maxSafeSize - prevSize2; maxAvailableDelta += delta2; index += increment; if (index < 0 || index >= panels.length) { break; } } const minAbsDelta = Math.min(Math.abs(delta), Math.abs(maxAvailableDelta)); delta = delta < 0 ? 0 - minAbsDelta : minAbsDelta; } { const pivotIndex = delta < 0 ? firstPivotIndex : secondPivotIndex; let index = pivotIndex; while (index >= 0 && index < panels.length) { const deltaRemaining = Math.abs(delta) - Math.abs(deltaApplied); const prevSize2 = initialSize[index]; utils.ensure(prevSize2, () => `Previous size not found for panel index ${index}`); const unsafeSize = prevSize2 - deltaRemaining; const safeSize = resizePanel({ panels, index, size: unsafeSize }); if (!fuzzyNumbersEqual(prevSize2, safeSize)) { deltaApplied += prevSize2 - safeSize; nextSize[index] = safeSize; if (deltaApplied.toPrecision(3).localeCompare(Math.abs(delta).toPrecision(3), void 0, { numeric: true }) >= 0) { break; } } if (delta < 0) { index--; } else { index++; } } } if (fuzzySizeEqual(prevSize, nextSize)) { return prevSize; } { const pivotIndex = delta < 0 ? secondPivotIndex : firstPivotIndex; const prevSize2 = initialSize[pivotIndex]; utils.ensure(prevSize2, () => `Previous size not found for panel index ${pivotIndex}`); const unsafeSize = prevSize2 + deltaApplied; const safeSize = resizePanel({ panels, index: pivotIndex, size: unsafeSize }); nextSize[pivotIndex] = safeSize; if (!fuzzyNumbersEqual(safeSize, unsafeSize)) { let deltaRemaining = unsafeSize - safeSize; const pivotIndex2 = delta < 0 ? secondPivotIndex : firstPivotIndex; let index = pivotIndex2; while (index >= 0 && index < panels.length) { const prevSize3 = nextSize[index]; utils.ensure(prevSize3, () => `Previous size not found for panel index ${index}`); const unsafeSize2 = prevSize3 + deltaRemaining; const safeSize2 = resizePanel({ panels, index, size: unsafeSize2 }); if (!fuzzyNumbersEqual(prevSize3, safeSize2)) { deltaRemaining -= safeSize2 - prevSize3; nextSize[index] = safeSize2; } if (fuzzyNumbersEqual(deltaRemaining, 0)) { break; } if (delta > 0) { index--; } else { index++; } } } } const totalSize = nextSize.reduce((total, size) => size + total, 0); if (!fuzzyNumbersEqual(totalSize, 100)) { return prevSize; } return nextSize; } function validateSizes({ size: prevSize, panels }) { const nextSize = [...prevSize]; const nextSizeTotalSize = nextSize.reduce((accumulated, current) => accumulated + current, 0); if (nextSize.length !== panels.length) { throw Error(`Invalid ${panels.length} panel size: ${nextSize.map((size) => `${size}%`).join(", ")}`); } else if (!fuzzyNumbersEqual(nextSizeTotalSize, 100) && nextSize.length > 0) { for (let index = 0; index < panels.length; index++) { const unsafeSize = nextSize[index]; utils.ensure(unsafeSize, () => `No size data found for index ${index}`); const safeSize = 100 / nextSizeTotalSize * unsafeSize; nextSize[index] = safeSize; } } let remainingSize = 0; for (let index = 0; index < panels.length; index++) { const unsafeSize = nextSize[index]; utils.ensure(unsafeSize, () => `No size data found for index ${index}`); const safeSize = resizePanel({ panels, index, size: unsafeSize }); if (unsafeSize != safeSize) { remainingSize += unsafeSize - safeSize; nextSize[index] = safeSize; } } if (!fuzzyNumbersEqual(remainingSize, 0)) { for (let index = 0; index < panels.length; index++) { const prevSize2 = nextSize[index]; utils.ensure(prevSize2, () => `No size data found for index ${index}`); const unsafeSize = prevSize2 + remainingSize; const safeSize = resizePanel({ panels, index, size: unsafeSize }); if (prevSize2 !== safeSize) { remainingSize -= safeSize - prevSize2; nextSize[index] = safeSize; if (fuzzyNumbersEqual(remainingSize, 0)) { break; } } } } return nextSize; } // src/splitter.machine.ts var machine = core.createMachine({ props({ props: props2 }) { utils.ensureProps(props2, ["panels"]); return { orientation: "horizontal", defaultSize: [], dir: "ltr", ...props2, panels: sortPanels(props2.panels) }; }, initialState() { return "idle"; }, context({ prop, bindable, getContext, getRefs }) { return { size: bindable(() => ({ value: prop("size"), defaultValue: prop("defaultSize"), isEqual(a, b) { return b != null && fuzzySizeEqual(a, b); }, onChange(value) { const ctx = getContext(); const refs = getRefs(); const sizesBeforeCollapse = refs.get("panelSizeBeforeCollapse"); const expandToSizes = Object.fromEntries(sizesBeforeCollapse.entries()); const resizeTriggerId = ctx.get("dragState")?.resizeTriggerId ?? null; const layout = getPanelLayout(prop("panels")); prop("onResize")?.({ size: value, layout, resizeTriggerId, expandToSizes }); } })), dragState: bindable(() => ({ defaultValue: null })), keyboardState: bindable(() => ({ defaultValue: null })) }; }, watch({ track, action, prop }) { track([() => serializePanels(prop("panels"))], () => { action(["syncSize"]); }); }, refs() { return { panelSizeBeforeCollapse: /* @__PURE__ */ new Map(), prevDelta: 0, panelIdToLastNotifiedSizeMap: /* @__PURE__ */ new Map() }; }, computed: { horizontal({ prop }) { return prop("orientation") === "horizontal"; } }, on: { "SIZE.SET": { actions: ["setSize"] }, "PANEL.COLLAPSE": { actions: ["collapsePanel"] }, "PANEL.EXPAND": { actions: ["expandPanel"] }, "PANEL.RESIZE": { actions: ["resizePanel"] } }, entry: ["syncSize"], states: { idle: { entry: ["clearDraggingState", "clearKeyboardState"], on: { POINTER_OVER: { target: "hover:temp", actions: ["setKeyboardState"] }, FOCUS: { target: "focused", actions: ["setKeyboardState"] }, POINTER_DOWN: { target: "dragging", actions: ["setDraggingState"] } } }, "hover:temp": { effects: ["waitForHoverDelay"], on: { HOVER_DELAY: { target: "hover" }, POINTER_DOWN: { target: "dragging", actions: ["setDraggingState"] }, POINTER_LEAVE: { target: "idle" } } }, hover: { tags: ["focus"], on: { POINTER_DOWN: { target: "dragging", actions: ["setDraggingState"] }, POINTER_LEAVE: { target: "idle" } } }, focused: { tags: ["focus"], on: { BLUR: { target: "idle" }, ENTER: { actions: ["collapseOrExpandPanel"] }, POINTER_DOWN: { target: "dragging", actions: ["setDraggingState"] }, KEYBOARD_MOVE: { actions: ["invokeOnResizeStart", "setKeyboardValue", "invokeOnResizeEnd"] }, "FOCUS.CYCLE": { actions: ["focusNextResizeTrigger"] } } }, dragging: { tags: ["focus"], effects: ["trackPointerMove"], entry: ["invokeOnResizeStart"], on: { POINTER_MOVE: { actions: ["setPointerValue", "setGlobalCursor"] }, POINTER_UP: { target: "idle", actions: ["invokeOnResizeEnd", "clearGlobalCursor"] } } } }, implementations: { effects: { waitForHoverDelay: ({ send }) => { return utils.setRafTimeout(() => { send({ type: "HOVER_DELAY" }); }, 250); }, trackPointerMove: ({ scope, send }) => { const doc = scope.getDoc(); return domQuery.trackPointerMove(doc, { onPointerMove(info) { send({ type: "POINTER_MOVE", point: info.point }); }, onPointerUp() { send({ type: "POINTER_UP" }); } }); } }, actions: { setSize(params) { const { context, event, prop } = params; const unsafeSize = event.size; const prevSize = context.get("size"); const panels = prop("panels"); const safeSize = validateSizes({ size: unsafeSize, panels }); if (!utils.isEqual(prevSize, safeSize)) { setSize(params, safeSize); } }, syncSize({ context, prop }) { const panels = prop("panels"); let prevSize = context.get("size"); let unsafeSize = null; if (prevSize.length === 0) { unsafeSize = getUnsafeDefaultSize({ panels, size: context.initial("size") }); } const nextSize = validateSizes({ size: unsafeSize ?? prevSize, panels }); if (!utils.isEqual(prevSize, nextSize)) { context.set("size", nextSize); } }, setDraggingState({ context, event, prop, scope }) { const orientation = prop("orientation"); const size = context.get("size"); const resizeTriggerId = event.id; const panelGroupEl = getRootEl(scope); if (!panelGroupEl) return; const handleElement = getResizeTriggerEl(scope, resizeTriggerId); utils.ensure(handleElement, () => `Drag handle element not found for id "${resizeTriggerId}"`); const initialCursorPosition = orientation === "horizontal" ? event.point.x : event.point.y; context.set("dragState", { resizeTriggerId: event.id, resizeTriggerRect: handleElement.getBoundingClientRect(), initialCursorPosition, initialSize: size }); }, clearDraggingState({ context }) { context.set("dragState", null); }, setKeyboardState({ context, event }) { context.set("keyboardState", { resizeTriggerId: event.id }); }, clearKeyboardState({ context }) { context.set("keyboardState", null); }, collapsePanel(params) { const { context, prop, event, refs } = params; const prevSize = context.get("size"); const panels = prop("panels"); const panel = panels.find((panel2) => panel2.id === event.id); utils.ensure(panel, () => `Panel data not found for id "${event.id}"`); if (panel.collapsible) { const { collapsedSize = 0, panelSize, pivotIndices } = panelDataHelper(panels, panel, prevSize); utils.ensure(panelSize, () => `Panel size not found for panel "${panel.id}"`); if (!fuzzyNumbersEqual(panelSize, collapsedSize)) { refs.get("panelSizeBeforeCollapse").set(panel.id, panelSize); const isLastPanel = findPanelDataIndex(panels, panel) === panels.length - 1; const delta = isLastPanel ? panelSize - collapsedSize : collapsedSize - panelSize; const nextSize = resizeByDelta({ delta, initialSize: prevSize, panels, pivotIndices, prevSize, trigger: "imperative-api" }); if (!utils.isEqual(prevSize, nextSize)) { setSize(params, nextSize); } } } }, expandPanel(params) { const { context, prop, event, refs } = params; const panels = prop("panels"); const prevSize = context.get("size"); const panel = panels.find((panel2) => panel2.id === event.id); utils.ensure(panel, () => `Panel data not found for id "${event.id}"`); if (panel.collapsible) { const { collapsedSize = 0, panelSize = 0, minSize: minSizeFromProps = 0, pivotIndices } = panelDataHelper(panels, panel, prevSize); const minSize = event.minSize ?? minSizeFromProps; if (fuzzyNumbersEqual(panelSize, collapsedSize)) { const prevPanelSize = refs.get("panelSizeBeforeCollapse").get(panel.id); const baseSize = prevPanelSize != null && prevPanelSize >= minSize ? prevPanelSize : minSize; const isLastPanel = findPanelDataIndex(panels, panel) === panels.length - 1; const delta = isLastPanel ? panelSize - baseSize : baseSize - panelSize; const nextSize = resizeByDelta({ delta, initialSize: prevSize, panels, pivotIndices, prevSize, trigger: "imperative-api" }); if (!utils.isEqual(prevSize, nextSize)) { setSize(params, nextSize); } } } }, resizePanel(params) { const { context, prop, event } = params; const prevSize = context.get("size"); const panels = prop("panels"); const panel = getPanelById(panels, event.id); const unsafePanelSize = event.size; const { panelSize, pivotIndices } = panelDataHelper(panels, panel, prevSize); utils.ensure(panelSize, () => `Panel size not found for panel "${panel.id}"`); const isLastPanel = findPanelDataIndex(panels, panel) === panels.length - 1; const delta = isLastPanel ? panelSize - unsafePanelSize : unsafePanelSize - panelSize; const nextSize = resizeByDelta({ delta, initialSize: prevSize, panels, pivotIndices, prevSize, trigger: "imperative-api" }); if (!utils.isEqual(prevSize, nextSize)) { setSize(params, nextSize); } }, setPointerValue(params) { const { context, event, prop, scope } = params; const dragState = context.get("dragState"); if (!dragState) return; const { resizeTriggerId, initialSize, initialCursorPosition } = dragState; const panels = prop("panels"); const panelGroupElement = getRootEl(scope); utils.ensure(panelGroupElement, () => `Panel group element not found`); const pivotIndices = resizeTriggerId.split(":").map((id) => panels.findIndex((panel) => panel.id === id)); const horizontal = prop("orientation") === "horizontal"; const cursorPosition = horizontal ? event.point.x : event.point.y; const groupRect = panelGroupElement.getBoundingClientRect(); const groupSizeInPixels = horizontal ? groupRect.width : groupRect.height; const offsetPixels = cursorPosition - initialCursorPosition; const offsetPercentage = offsetPixels / groupSizeInPixels * 100; const prevSize = context.get("size"); const nextSize = resizeByDelta({ delta: offsetPercentage, initialSize: initialSize ?? prevSize, panels, pivotIndices, prevSize, trigger: "mouse-or-touch" }); if (!utils.isEqual(prevSize, nextSize)) { setSize(params, nextSize); } }, setKeyboardValue(params) { const { context, event, prop } = params; const panelDataArray = prop("panels"); const resizeTriggerId = event.id; const delta = event.delta; const pivotIndices = resizeTriggerId.split(":").map((id) => panelDataArray.findIndex((panelData) => panelData.id === id)); const prevSize = context.get("size"); const nextSize = resizeByDelta({ delta, initialSize: prevSize, panels: panelDataArray, pivotIndices, prevSize, trigger: "keyboard" }); if (!utils.isEqual(prevSize, nextSize)) { setSize(params, nextSize); } }, invokeOnResizeEnd({ context, prop }) { queueMicrotask(() => { const dragState = context.get("dragState"); prop("onResizeEnd")?.({ size: context.get("size"), resizeTriggerId: dragState?.resizeTriggerId ?? null }); }); }, invokeOnResizeStart({ prop }) { queueMicrotask(() => { prop("onResizeStart")?.(); }); }, collapseOrExpandPanel(params) { const { context, prop } = params; const panelDataArray = prop("panels"); const sizes = context.get("size"); const resizeTriggerId = context.get("keyboardState")?.resizeTriggerId; const [idBefore, idAfter] = resizeTriggerId?.split(":") ?? []; const index = panelDataArray.findIndex((panelData2) => panelData2.id === idBefore); if (index === -1) return; const panelData = panelDataArray[index]; utils.ensure(panelData, () => `No panel data found for index ${index}`); const size = sizes[index]; const { collapsedSize = 0, collapsible, minSize = 0 } = panelData; if (size != null && collapsible) { const pivotIndices = [idBefore, idAfter].map( (id) => panelDataArray.findIndex((panelData2) => panelData2.id === id) ); const nextSize = resizeByDelta({ delta: fuzzyNumbersEqual(size, collapsedSize) ? minSize - collapsedSize : collapsedSize - size, initialSize: context.initial("size"), panels: panelDataArray, pivotIndices, prevSize: sizes, trigger: "keyboard" }); if (!utils.isEqual(sizes, nextSize)) { setSize(params, nextSize); } } }, setGlobalCursor({ context, scope, prop }) { const dragState = context.get("dragState"); if (!dragState) return; const panels = prop("panels"); const horizontal = prop("orientation") === "horizontal"; const [idBefore] = dragState.resizeTriggerId.split(":"); const indexBefore = panels.findIndex((panel2) => panel2.id === idBefore); const panel = panels[indexBefore]; const size = context.get("size"); const aria = getAriaValue(size, panels, dragState.resizeTriggerId); const isAtMin = fuzzyNumbersEqual(aria.valueNow, aria.valueMin) || fuzzyNumbersEqual(aria.valueNow, panel.collapsedSize); const isAtMax = fuzzyNumbersEqual(aria.valueNow, aria.valueMax); const cursorState = { isAtMin, isAtMax }; setupGlobalCursor(scope, cursorState, horizontal, prop("nonce")); }, clearGlobalCursor({ scope }) { removeGlobalCursor(scope); }, focusNextResizeTrigger({ event, scope }) { const resizeTriggers = getResizeTriggerEls(scope); const index = resizeTriggers.findIndex((el) => el.dataset.id === event.id); const handleEl = event.shiftKey ? utils.prev(resizeTriggers, index) : utils.next(resizeTriggers, index); handleEl?.focus(); } } } }); function setSize(params, sizes) { const { refs, prop, context } = params; const panelsArray = prop("panels"); const onCollapse = prop("onCollapse"); const onExpand = prop("onExpand"); const panelIdToLastNotifiedSizeMap = refs.get("panelIdToLastNotifiedSizeMap"); context.set("size", sizes); sizes.forEach((size, index) => { const panelData = panelsArray[index]; utils.ensure(panelData, () => `Panel data not found for index ${index}`); const { collapsedSize = 0, collapsible, id: panelId } = panelData; const lastNotifiedSize = panelIdToLastNotifiedSizeMap.get(panelId); if (lastNotifiedSize == null || size !== lastNotifiedSize) { panelIdToLastNotifiedSizeMap.set(panelId, size); if (collapsible && (onCollapse || onExpand)) { if ((lastNotifiedSize == null || fuzzyNumbersEqual(lastNotifiedSize, collapsedSize)) && !fuzzyNumbersEqual(size, collapsedSize)) { onExpand?.({ panelId, size }); } if (onCollapse && (lastNotifiedSize == null || !fuzzyNumbersEqual(lastNotifiedSize, collapsedSize)) && fuzzyNumbersEqual(size, collapsedSize)) { onCollapse?.({ panelId, size }); } } } }); } var props = types.createProps()([ "dir", "getRootNode", "id", "ids", "onResize", "onResizeStart", "onResizeEnd", "onCollapse", "onExpand", "orientation", "size", "defaultSize", "panels", "keyboardResizeBy", "nonce" ]); var splitProps = utils.createSplitProps(props); var panelProps = types.createProps()(["id"]); var splitPanelProps = utils.createSplitProps(panelProps); var resizeTriggerProps = types.createProps()(["disabled", "id"]); var splitResizeTriggerProps = utils.createSplitProps(resizeTriggerProps); exports.anatomy = anatomy; exports.connect = connect; exports.layout = getPanelLayout; exports.machine = machine; exports.panelProps = panelProps; exports.props = props; exports.resizeTriggerProps = resizeTriggerProps; exports.splitPanelProps = splitPanelProps; exports.splitProps = splitProps; exports.splitResizeTriggerProps = splitResizeTriggerProps;