@zag-js/splitter
Version:
Core logic for the splitter widget implemented as a state machine
1,131 lines (1,123 loc) • 39.3 kB
JavaScript
;
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;