package.dist.index.mjs Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of splitter Show documentation
Show all versions of splitter Show documentation
Core logic for the splitter widget implemented as a state machine
The newest version!
import { createAnatomy } from '@zag-js/anatomy';
import { getEventStep, getEventKey, trackPointerMove, getRelativePoint } from '@zag-js/dom-event';
import { createScope, queryAll, dataAttr, raf } from '@zag-js/dom-query';
import { createMachine } from '@zag-js/core';
import { createSplitProps, compact } from '@zag-js/utils';
import { createProps } from '@zag-js/types';
// src/splitter.anatomy.ts
var anatomy = createAnatomy("splitter").parts("root", "panel", "resizeTrigger");
var parts = anatomy.build();
var dom = createScope({
getRootId: (ctx) => ctx.ids?.root ?? `splitter:${ctx.id}`,
getResizeTriggerId: (ctx, id) => ctx.ids?.resizeTrigger?.(id) ?? `splitter:${ctx.id}:splitter:${id}`,
getLabelId: (ctx) => ctx.ids?.label ?? `splitter:${ctx.id}:label`,
getPanelId: (ctx, id) => ctx.ids?.panel?.(id) ?? `splitter:${ctx.id}:panel:${id}`,
getGlobalCursorId: (ctx) => `splitter:${ctx.id}:global-cursor`,
getRootEl: (ctx) => dom.getById(ctx, dom.getRootId(ctx)),
getResizeTriggerEl: (ctx, id) => dom.getById(ctx, dom.getResizeTriggerId(ctx, id)),
getPanelEl: (ctx, id) => dom.getById(ctx, dom.getPanelId(ctx, id)),
getCursor(ctx) {
const x = ctx.isHorizontal;
let cursor = x ? "col-resize" : "row-resize";
if (ctx.activeResizeState.isAtMin) cursor = x ? "e-resize" : "s-resize";
if (ctx.activeResizeState.isAtMax) cursor = x ? "w-resize" : "n-resize";
return cursor;
},
getPanelStyle(ctx, id) {
const flexGrow = ctx.panels.find((panel) => panel.id === id)?.size ?? "0";
return {
flexBasis: 0,
flexGrow,
flexShrink: 1,
overflow: "hidden"
};
},
getActiveHandleEl(ctx) {
const activeId = ctx.activeResizeId;
if (activeId == null) return;
return dom.getById(ctx, dom.getResizeTriggerId(ctx, activeId));
},
getResizeTriggerEls(ctx) {
const ownerId = CSS.escape(dom.getRootId(ctx));
return queryAll(dom.getRootEl(ctx), `[role=separator][data-ownedby='${ownerId}']`);
},
setupGlobalCursor(ctx) {
const styleEl = dom.getById(ctx, dom.getGlobalCursorId(ctx));
const textContent = `* { cursor: ${dom.getCursor(ctx)} !important; }`;
if (styleEl) {
styleEl.textContent = textContent;
} else {
const style = dom.getDoc(ctx).createElement("style");
style.id = dom.getGlobalCursorId(ctx);
style.textContent = textContent;
dom.getDoc(ctx).head.appendChild(style);
}
},
removeGlobalCursor(ctx) {
dom.getById(ctx, dom.getGlobalCursorId(ctx))?.remove();
}
});
// src/splitter.utils.ts
function validateSize(key, size) {
if (Math.floor(size) > 100) {
throw new Error(`Total ${key} of panels cannot be greater than 100`);
}
}
function getNormalizedPanels(ctx) {
let numOfPanelsWithoutSize = 0;
let totalSize = 0;
let totalMinSize = 0;
const panels = ctx.size.map((panel) => {
const minSize = panel.minSize ?? 0;
const maxSize = panel.maxSize ?? 100;
totalMinSize += minSize;
if (panel.size == null) {
numOfPanelsWithoutSize++;
} else {
totalSize += panel.size;
}
return {
...panel,
minSize,
maxSize
};
});
validateSize("minSize", totalMinSize);
validateSize("size", totalSize);
let end = 0;
let remainingSize = 0;
const result = panels.map((panel) => {
let start = end;
if (panel.size != null) {
end += panel.size;
remainingSize = panel.size - panel.minSize;
return {
...panel,
start,
end,
remainingSize
};
}
const size = (100 - totalSize) / numOfPanelsWithoutSize;
end += size;
remainingSize = size - panel.minSize;
return { ...panel, size, start, end, remainingSize };
});
return result;
}
function getHandlePanels(ctx, id = ctx.activeResizeId) {
const [beforeId, afterId] = id?.split(":") ?? [];
if (!beforeId || !afterId) return;
const beforeIndex = ctx.previousPanels.findIndex((panel) => panel.id === beforeId);
const afterIndex = ctx.previousPanels.findIndex((panel) => panel.id === afterId);
if (beforeIndex === -1 || afterIndex === -1) return;
const before = ctx.previousPanels[beforeIndex];
const after = ctx.previousPanels[afterIndex];
return {
before: {
...before,
index: beforeIndex
},
after: {
...after,
index: afterIndex
}
};
}
function getHandleBounds(ctx, id = ctx.activeResizeId) {
const panels = getHandlePanels(ctx, id);
if (!panels) return;
const { before, after } = panels;
return {
min: Math.max(before.start + before.minSize, after.end - after.maxSize),
max: Math.min(after.end - after.minSize, before.maxSize + before.start)
};
}
function getPanelBounds(ctx, id) {
const bounds = getHandleBounds(ctx, id);
const panels = getHandlePanels(ctx, id);
if (!bounds || !panels) return;
const { before, after } = panels;
const beforeMin = Math.abs(before.start - bounds.min);
const afterMin = after.size + (before.size - beforeMin);
const beforeMax = Math.abs(before.start - bounds.max);
const afterMax = after.size - (beforeMax - before.size);
return {
before: {
index: before.index,
min: beforeMin,
max: beforeMax,
isAtMin: beforeMin === before.size,
isAtMax: beforeMax === before.size,
up(step) {
return Math.min(before.size + step, beforeMax);
},
down(step) {
return Math.max(before.size - step, beforeMin);
}
},
after: {
index: after.index,
min: afterMin,
max: afterMax,
isAtMin: afterMin === after.size,
isAtMax: afterMax === after.size,
up(step) {
return Math.min(after.size + step, afterMin);
},
down(step) {
return Math.max(after.size - step, afterMax);
}
}
};
}
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
// src/splitter.connect.ts
function connect(state, send, normalize) {
const horizontal = state.context.isHorizontal;
const focused = state.hasTag("focus");
const dragging = state.matches("dragging");
const panels = state.context.panels;
function getResizeTriggerState(props2) {
const { id, disabled } = props2;
const ids = id.split(":");
const panelIds = ids.map((id2) => dom.getPanelId(state.context, id2));
const panels2 = getHandleBounds(state.context, id);
return {
disabled: !!disabled,
focused: state.context.activeResizeId === id && focused,
panelIds,
min: panels2?.min,
max: panels2?.max,
value: 0
};
}
return {
focused,
dragging,
getResizeTriggerState,
bounds: getHandleBounds(state.context),
setToMinSize(id) {
const panel = panels.find((panel2) => panel2.id === id);
send({ type: "SET_PANEL_SIZE", id, size: panel?.minSize, src: "setToMinSize" });
},
setToMaxSize(id) {
const panel = panels.find((panel2) => panel2.id === id);
send({ type: "SET_PANEL_SIZE", id, size: panel?.maxSize, src: "setToMaxSize" });
},
setSize(id, size) {
send({ type: "SET_PANEL_SIZE", id, size });
},
getRootProps() {
return normalize.element({
...parts.root.attrs,
"data-orientation": state.context.orientation,
id: dom.getRootId(state.context),
dir: state.context.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": state.context.orientation,
dir: state.context.dir,
id: dom.getPanelId(state.context, id),
"data-ownedby": dom.getRootId(state.context),
style: dom.getPanelStyle(state.context, id)
});
},
getResizeTriggerProps(props2) {
const { id, disabled, step = 1 } = props2;
const triggerState = getResizeTriggerState(props2);
return normalize.element({
...parts.resizeTrigger.attrs,
dir: state.context.dir,
id: dom.getResizeTriggerId(state.context, id),
role: "separator",
"data-ownedby": dom.getRootId(state.context),
tabIndex: disabled ? void 0 : 0,
"aria-valuenow": triggerState.value,
"aria-valuemin": triggerState.min,
"aria-valuemax": triggerState.max,
"data-orientation": state.context.orientation,
"aria-orientation": state.context.orientation,
"aria-controls": triggerState.panelIds.join(" "),
"data-focus": dataAttr(triggerState.focused),
"data-disabled": dataAttr(disabled),
style: {
touchAction: "none",
userSelect: "none",
WebkitUserSelect: "none",
flex: "0 0 auto",
pointerEvents: dragging && !triggerState.focused ? "none" : void 0,
cursor: horizontal ? "col-resize" : "row-resize",
[horizontal ? "minHeight" : "minWidth"]: "0"
},
onPointerDown(event) {
if (disabled) {
event.preventDefault();
return;
}
send({ type: "POINTER_DOWN", id });
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() {
send("BLUR");
},
onFocus() {
send({ type: "FOCUS", id });
},
onDoubleClick() {
if (disabled) return;
send({ type: "DOUBLE_CLICK", id });
},
onKeyDown(event) {
if (event.defaultPrevented) return;
if (disabled) return;
const moveStep = getEventStep(event) * step;
const keyMap = {
Enter() {
send("ENTER");
},
ArrowUp() {
send({ type: "ARROW_UP", step: moveStep });
},
ArrowDown() {
send({ type: "ARROW_DOWN", step: moveStep });
},
ArrowLeft() {
send({ type: "ARROW_LEFT", step: moveStep });
},
ArrowRight() {
send({ type: "ARROW_RIGHT", step: moveStep });
},
Home() {
send("HOME");
},
End() {
send("END");
}
};
const key = getEventKey(event, state.context);
const exec = keyMap[key];
if (exec) {
exec(event);
event.preventDefault();
}
}
});
}
};
}
function machine(userContext) {
const ctx = compact(userContext);
return createMachine(
{
id: "splitter",
initial: "idle",
context: {
orientation: "horizontal",
activeResizeId: null,
previousPanels: [],
size: [],
initialSize: [],
activeResizeState: {
isAtMin: false,
isAtMax: false
},
...ctx
},
created: ["setPreviousPanels", "setInitialSize"],
watch: {
size: ["setActiveResizeState"]
},
computed: {
isHorizontal: (ctx2) => ctx2.orientation === "horizontal",
panels: (ctx2) => getNormalizedPanels(ctx2)
},
on: {
SET_PANEL_SIZE: {
actions: "setPanelSize"
}
},
states: {
idle: {
entry: ["clearActiveHandleId"],
on: {
POINTER_OVER: {
target: "hover:temp",
actions: ["setActiveHandleId"]
},
FOCUS: {
target: "focused",
actions: ["setActiveHandleId"]
},
DOUBLE_CLICK: {
actions: ["resetStartPanel", "setPreviousPanels"]
}
}
},
"hover:temp": {
after: {
HOVER_DELAY: "hover"
},
on: {
POINTER_DOWN: {
target: "dragging",
actions: ["setActiveHandleId"]
},
POINTER_LEAVE: "idle"
}
},
hover: {
tags: ["focus"],
on: {
POINTER_DOWN: "dragging",
POINTER_LEAVE: "idle"
}
},
focused: {
tags: ["focus"],
on: {
BLUR: "idle",
POINTER_DOWN: {
target: "dragging",
actions: ["setActiveHandleId"]
},
ARROW_LEFT: {
guard: "isHorizontal",
actions: ["shrinkStartPanel", "setPreviousPanels"]
},
ARROW_RIGHT: {
guard: "isHorizontal",
actions: ["expandStartPanel", "setPreviousPanels"]
},
ARROW_UP: {
guard: "isVertical",
actions: ["shrinkStartPanel", "setPreviousPanels"]
},
ARROW_DOWN: {
guard: "isVertical",
actions: ["expandStartPanel", "setPreviousPanels"]
},
ENTER: [
{
guard: "isStartPanelAtMax",
actions: ["setStartPanelToMin", "setPreviousPanels"]
},
{ actions: ["setStartPanelToMax", "setPreviousPanels"] }
],
HOME: {
actions: ["setStartPanelToMin", "setPreviousPanels"]
},
END: {
actions: ["setStartPanelToMax", "setPreviousPanels"]
}
}
},
dragging: {
tags: ["focus"],
entry: "focusResizeHandle",
activities: ["trackPointerMove"],
on: {
POINTER_MOVE: {
actions: ["setPointerValue", "setGlobalCursor", "invokeOnResize"]
},
POINTER_UP: {
target: "focused",
actions: ["setPreviousPanels", "clearGlobalCursor", "blurResizeHandle", "invokeOnResizeEnd"]
}
}
}
}
},
{
activities: {
trackPointerMove: (ctx2, _evt, { send }) => {
const doc = dom.getDoc(ctx2);
return trackPointerMove(doc, {
onPointerMove(info) {
send({ type: "POINTER_MOVE", point: info.point });
},
onPointerUp() {
send("POINTER_UP");
}
});
}
},
guards: {
isStartPanelAtMin: (ctx2) => ctx2.activeResizeState.isAtMin,
isStartPanelAtMax: (ctx2) => ctx2.activeResizeState.isAtMax,
isHorizontal: (ctx2) => ctx2.isHorizontal,
isVertical: (ctx2) => !ctx2.isHorizontal
},
delays: {
HOVER_DELAY: 250
},
actions: {
setGlobalCursor(ctx2) {
dom.setupGlobalCursor(ctx2);
},
clearGlobalCursor(ctx2) {
dom.removeGlobalCursor(ctx2);
},
invokeOnResize(ctx2) {
ctx2.onSizeChange?.({ size: Array.from(ctx2.size), activeHandleId: ctx2.activeResizeId });
},
invokeOnResizeEnd(ctx2) {
ctx2.onSizeChangeEnd?.({ size: Array.from(ctx2.size), activeHandleId: ctx2.activeResizeId });
},
setActiveHandleId(ctx2, evt) {
ctx2.activeResizeId = evt.id;
},
clearActiveHandleId(ctx2) {
ctx2.activeResizeId = null;
},
setInitialSize(ctx2) {
ctx2.initialSize = ctx2.panels.slice().map((panel) => ({
id: panel.id,
size: panel.size
}));
},
setPanelSize(ctx2, evt) {
const { id, size } = evt;
ctx2.size = ctx2.size.map((panel) => {
const panelSize = clamp(size, panel.minSize ?? 0, panel.maxSize ?? 100);
return panel.id === id ? { ...panel, size: panelSize } : panel;
});
},
setStartPanelToMin(ctx2) {
const bounds = getPanelBounds(ctx2);
if (!bounds) return;
const { before, after } = bounds;
ctx2.size[before.index].size = before.min;
ctx2.size[after.index].size = after.min;
},
setStartPanelToMax(ctx2) {
const bounds = getPanelBounds(ctx2);
if (!bounds) return;
const { before, after } = bounds;
ctx2.size[before.index].size = before.max;
ctx2.size[after.index].size = after.max;
},
expandStartPanel(ctx2, evt) {
const bounds = getPanelBounds(ctx2);
if (!bounds) return;
const { before, after } = bounds;
ctx2.size[before.index].size = before.up(evt.step);
ctx2.size[after.index].size = after.down(evt.step);
},
shrinkStartPanel(ctx2, evt) {
const bounds = getPanelBounds(ctx2);
if (!bounds) return;
const { before, after } = bounds;
ctx2.size[before.index].size = before.down(evt.step);
ctx2.size[after.index].size = after.up(evt.step);
},
resetStartPanel(ctx2, evt) {
const bounds = getPanelBounds(ctx2, evt.id);
if (!bounds) return;
const { before, after } = bounds;
ctx2.size[before.index].size = ctx2.initialSize[before.index].size;
ctx2.size[after.index].size = ctx2.initialSize[after.index].size;
},
focusResizeHandle(ctx2) {
raf(() => {
dom.getActiveHandleEl(ctx2)?.focus({ preventScroll: true });
});
},
blurResizeHandle(ctx2) {
raf(() => {
dom.getActiveHandleEl(ctx2)?.blur();
});
},
setPreviousPanels(ctx2) {
ctx2.previousPanels = ctx2.panels.slice();
},
setActiveResizeState(ctx2) {
const panels = getPanelBounds(ctx2);
if (!panels) return;
const { before } = panels;
ctx2.activeResizeState = {
isAtMin: before.isAtMin,
isAtMax: before.isAtMax
};
},
setPointerValue(ctx2, evt) {
const panels = getHandlePanels(ctx2);
const bounds = getHandleBounds(ctx2);
if (!panels || !bounds) return;
const rootEl = dom.getRootEl(ctx2);
if (!rootEl) return;
const relativePoint = getRelativePoint(evt.point, rootEl);
const percentValue = relativePoint.getPercentValue({
dir: ctx2.dir,
orientation: ctx2.orientation
});
let pointValue = percentValue * 100;
ctx2.activeResizeState = {
isAtMin: pointValue < bounds.min,
isAtMax: pointValue > bounds.max
};
pointValue = clamp(pointValue, bounds.min, bounds.max);
const { before, after } = panels;
const offset = pointValue - before.end;
ctx2.size[before.index].size = before.size + offset;
ctx2.size[after.index].size = after.size - offset;
}
}
}
);
}
var props = createProps()([
"dir",
"getRootNode",
"id",
"ids",
"onSizeChange",
"onSizeChangeEnd",
"orientation",
"size"
]);
var splitProps = createSplitProps(props);
var panelProps = createProps()(["id", "snapSize"]);
var splitPanelProps = createSplitProps(panelProps);
var resizeTriggerProps = createProps()(["disabled", "id", "step"]);
var splitResizeTriggerProps = createSplitProps(resizeTriggerProps);
export { anatomy, connect, machine, panelProps, props, resizeTriggerProps, splitPanelProps, splitProps, splitResizeTriggerProps };
© 2015 - 2025 Weber Informatics LLC | Privacy Policy