UNPKG

@zag-js/splitter

Version:

Core logic for the splitter widget implemented as a state machine

672 lines (670 loc) • 25.8 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/splitter.machine.ts var splitter_machine_exports = {}; __export(splitter_machine_exports, { machine: () => machine }); module.exports = __toCommonJS(splitter_machine_exports); var import_core = require("@zag-js/core"); var import_dom_query = require("@zag-js/dom-query"); var import_utils = require("@zag-js/utils"); var dom = __toESM(require("./splitter.dom.js")); var import_aria = require("./utils/aria.js"); var import_fuzzy = require("./utils/fuzzy.js"); var import_panel = require("./utils/panel.js"); var import_preserve_fixed_panel_sizes = require("./utils/preserve-fixed-panel-sizes.js"); var import_resize_by_delta = require("./utils/resize-by-delta.js"); var import_size = require("./utils/size.js"); var import_validate_sizes = require("./utils/validate-sizes.js"); var machine = (0, import_core.createMachine)({ props({ props }) { (0, import_utils.ensureProps)(props, ["panels"]); return { orientation: "horizontal", defaultSize: [], dir: "ltr", ...props, panels: (0, import_panel.sortPanels)(props.panels) }; }, initialState() { return "idle"; }, context({ prop, bindable, getContext, getRefs }) { return { panels: bindable(() => ({ defaultValue: (0, import_size.normalizePanels)(prop("panels"), null, prop("orientation")) })), size: bindable(() => ({ defaultValue: [], isEqual(a, b) { return b != null && (0, import_fuzzy.fuzzySizeEqual)(a, b); }, onChange(value) { const ctx = getContext(); const refs = getRefs(); if (refs.get("suppressOnResize")) return; const sizesBeforeCollapse = refs.get("panelSizeBeforeCollapse"); const expandToSizes = Object.fromEntries(sizesBeforeCollapse.entries()); const resizeTriggerId = ctx.get("dragState")?.resizeTriggerId ?? null; const layout = (0, import_panel.getPanelLayout)(prop("panels")); prop("onResize")?.({ size: value, layout, resizeTriggerId, expandToSizes }); } })), dragState: bindable(() => ({ defaultValue: null })), keyboardState: bindable(() => ({ defaultValue: null })) }; }, watch({ track, action, prop }) { track( [ () => (0, import_panel.serializePanels)(prop("panels")), () => JSON.stringify(prop("size") ?? []), () => JSON.stringify(prop("defaultSize") ?? []) ], () => { action(["syncSize"]); } ); }, refs() { return { panelSizeBeforeCollapse: /* @__PURE__ */ new Map(), prevDelta: 0, panelIdToLastNotifiedSizeMap: /* @__PURE__ */ new Map(), initialSize: null, prevInitialLayout: null, prevGroupSize: null, lastRequestedSize: null, suppressOnResize: false }; }, computed: { horizontal({ prop }) { return prop("orientation") === "horizontal"; } }, on: { "SIZE.SET": { actions: ["setSize"] }, "SIZE.RESET": { actions: ["resetSize"] }, "PANEL.COLLAPSE": { actions: ["collapsePanel"] }, "PANEL.EXPAND": { actions: ["expandPanel"] }, "PANEL.RESIZE": { actions: ["resizePanel"] }, "ROOT.RESIZE": { actions: ["syncSize"] } }, entry: ["syncSize"], exit: ["clearGlobalCursor"], effects: ["trackResizeHandles", "trackRootResize"], 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: [ { guard: "isResizeTriggerFocused", target: "focused", actions: ["invokeOnResizeEnd", "setKeyboardState", "clearDraggingState", "clearGlobalCursor"] }, { target: "idle", actions: ["invokeOnResizeEnd", "clearGlobalCursor"] } ] } } }, implementations: { guards: { isResizeTriggerFocused({ context, scope }) { const dragState = context.get("dragState"); return scope.isActiveElement(dom.getResizeTriggerEl(scope, dragState?.resizeTriggerId)); } }, effects: { trackResizeHandles: ({ prop, scope, send }) => { const registry = prop("registry"); if (!registry) return; let cleanups = []; const exec = () => { cleanups.forEach((fn) => fn()); cleanups = dom.getResizeTriggerEls(scope).map((resizeTriggerEl) => { const id = resizeTriggerEl.dataset.id; if (!id) return; return registry.register({ id: dom.getResizeTriggerId(scope, id), element: resizeTriggerEl, orientation: prop("orientation"), onActivate(point) { send({ type: "POINTER_DOWN", id, point }); }, onDeactivate() { send({ type: "POINTER_UP" }); } }); }).filter(Boolean); }; exec(); const observeCleanup = (0, import_dom_query.observeChildren)(dom.getRootEl(scope), { callback: exec }); return () => { cleanups.forEach((fn) => fn()); observeCleanup?.(); }; }, trackRootResize: ({ scope, send }) => { const rootEl = dom.getRootEl(scope); if (!rootEl) return; return import_dom_query.resizeObserverBorderBox.observe(rootEl, () => { send({ type: "ROOT.RESIZE" }); }); }, waitForHoverDelay: ({ send }) => { return (0, import_utils.setRafTimeout)(() => { send({ type: "HOVER_DELAY" }); }, 250); }, trackPointerMove: ({ scope, send }) => { const doc = scope.getDoc(); return (0, import_dom_query.trackPointerMove)(doc, { onPointerMove(info) { send({ type: "POINTER_MOVE", point: info.point }); }, onPointerUp() { send({ type: "POINTER_UP" }); } }); } }, actions: { setSize(params) { const { context, event, prop, scope } = params; const unsafeSize = event.size; const prevSize = context.get("size"); const panels = context.get("panels"); const safeSize = (0, import_validate_sizes.validateSizes)({ size: (0, import_size.resolvePanelSizes)({ sizes: unsafeSize, panels: prop("panels"), rootEl: dom.getRootEl(scope), orientation: prop("orientation") }), panels }); if (!(0, import_utils.isEqual)(prevSize, safeSize)) { setSize(params, safeSize); } }, resetSize(params) { const { refs, context, prop, scope } = params; const initialSize = refs.get("initialSize"); const nextSize = initialSize ?? (0, import_validate_sizes.validateSizes)({ size: (0, import_size.resolvePanelSizes)({ sizes: prop("size") ?? prop("defaultSize"), panels: prop("panels"), rootEl: dom.getRootEl(scope), orientation: prop("orientation") }), panels: context.get("panels") }); setSize(params, nextSize); }, syncSize(params) { const { context, scope, prop, refs } = params; const rootEl = dom.getRootEl(scope); if (!rootEl) return; const orientation = prop("orientation"); const nextGroupSize = (0, import_size.getGroupSize)(rootEl, orientation); if (nextGroupSize <= 0) return; const panels = (0, import_size.normalizePanels)(prop("panels"), rootEl, prop("orientation")); context.set("panels", panels); const sizeSpec = prop("size") ?? prop("defaultSize"); const initialLayout = `${(0, import_panel.getPanelLayout)(prop("panels"))}:${JSON.stringify(prop("size") ?? [])}:${JSON.stringify(prop("defaultSize") ?? [])}`; const prevGroupSize = refs.get("prevGroupSize"); const currentSize = context.get("size"); const nextResolvedSize = (0, import_size.resolvePanelSizes)({ sizes: sizeSpec, panels: prop("panels"), rootEl, orientation }); const canPreserveLayout = prevGroupSize != null && prevGroupSize !== nextGroupSize && currentSize.length === panels.length; const nextSize = canPreserveLayout ? (0, import_preserve_fixed_panel_sizes.preserveFixedPanelSizes)({ panels, prevLayout: currentSize, prevGroupSize, nextGroupSize }) : nextResolvedSize; const safeSize = (0, import_validate_sizes.validateSizes)({ size: nextSize, panels }); if (refs.get("prevInitialLayout") !== initialLayout) { refs.set("initialSize", safeSize); refs.set("prevInitialLayout", initialLayout); } const prevSize = context.get("size"); if (!(0, import_utils.isEqual)(prevSize, safeSize)) { refs.set("suppressOnResize", prop("size") != null || prevSize.length === 0); context.set("size", safeSize); refs.set("suppressOnResize", false); } refs.set("prevGroupSize", nextGroupSize); }, setDraggingState({ context, event, prop, scope }) { const orientation = prop("orientation"); const size = context.get("size"); const resizeTriggerId = event.id; const resolvedResizeTriggerId = dom.resolveResizeTriggerId(scope, resizeTriggerId); if (!resolvedResizeTriggerId) return; const panelGroupEl = dom.getRootEl(scope); if (!panelGroupEl) return; const handleElement = dom.getResizeTriggerEl(scope, resizeTriggerId); (0, import_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, resolvedResizeTriggerId, resizeTriggerRect: handleElement.getBoundingClientRect(), initialCursorPosition, initialSize: size }); }, clearDraggingState({ context }) { context.set("dragState", null); }, setKeyboardState({ context, event, scope }) { const id = event.id ?? context.get("dragState")?.resizeTriggerId; if (id == null) return; context.set("keyboardState", { resizeTriggerId: id, resolvedResizeTriggerId: dom.resolveResizeTriggerId(scope, id) }); }, clearKeyboardState({ context }) { context.set("keyboardState", null); }, collapsePanel(params) { const { context, event, refs } = params; const prevSize = context.get("size"); const panels = context.get("panels"); const panel = panels.find((panel2) => panel2.id === event.id); (0, import_utils.ensure)(panel, () => `Panel data not found for id "${event.id}"`); if (panel.collapsible) { const { collapsedSize = 0, panelSize, pivotIndices } = (0, import_panel.panelDataHelper)(panels, panel, prevSize); (0, import_utils.ensure)(panelSize != null, () => `Panel size not found for panel "${panel.id}"`); if (!(0, import_fuzzy.fuzzyNumbersEqual)(panelSize, collapsedSize)) { refs.get("panelSizeBeforeCollapse").set(panel.id, panelSize); const isLastPanel = (0, import_panel.findPanelDataIndex)(panels, panel) === panels.length - 1; const delta = isLastPanel ? panelSize - collapsedSize : collapsedSize - panelSize; const nextSize = (0, import_resize_by_delta.resizeByDelta)({ delta, initialSize: prevSize, panels, pivotIndices, prevSize, trigger: "imperative-api" }); if (!(0, import_utils.isEqual)(prevSize, nextSize)) { setSize(params, nextSize); } } } }, expandPanel(params) { const { context, event, refs } = params; const panels = context.get("panels"); const prevSize = context.get("size"); const panel = panels.find((panel2) => panel2.id === event.id); (0, import_utils.ensure)(panel, () => `Panel data not found for id "${event.id}"`); if (panel.collapsible) { const { collapsedSize = 0, panelSize = 0, minSize: minSizeFromProps = 0, pivotIndices } = (0, import_panel.panelDataHelper)(panels, panel, prevSize); const minSize = event.minSize ?? minSizeFromProps; if ((0, import_fuzzy.fuzzyNumbersEqual)(panelSize, collapsedSize)) { const prevPanelSize = refs.get("panelSizeBeforeCollapse").get(panel.id); const baseSize = prevPanelSize != null && prevPanelSize >= minSize ? prevPanelSize : minSize; const isLastPanel = (0, import_panel.findPanelDataIndex)(panels, panel) === panels.length - 1; const delta = isLastPanel ? panelSize - baseSize : baseSize - panelSize; const nextSize = (0, import_resize_by_delta.resizeByDelta)({ delta, initialSize: prevSize, panels, pivotIndices, prevSize, trigger: "imperative-api" }); if (!(0, import_utils.isEqual)(prevSize, nextSize)) { setSize(params, nextSize); } } } }, resizePanel(params) { const { context, event } = params; const prevSize = context.get("size"); const panels = context.get("panels"); const panel = (0, import_panel.getPanelById)(panels, event.id); const unsafePanelSize = event.size; const { panelSize, pivotIndices } = (0, import_panel.panelDataHelper)(panels, panel, prevSize); (0, import_utils.ensure)(panelSize != null, () => `Panel size not found for panel "${panel.id}"`); const isLastPanel = (0, import_panel.findPanelDataIndex)(panels, panel) === panels.length - 1; const delta = isLastPanel ? panelSize - unsafePanelSize : unsafePanelSize - panelSize; const nextSize = (0, import_resize_by_delta.resizeByDelta)({ delta, initialSize: prevSize, panels, pivotIndices, prevSize, trigger: "imperative-api" }); if (!(0, import_utils.isEqual)(prevSize, nextSize)) { setSize(params, nextSize); } }, setPointerValue(params) { const { context, event, prop, scope } = params; const dragState = context.get("dragState"); if (!dragState) return; const { resolvedResizeTriggerId, initialSize, initialCursorPosition } = dragState; const panels = context.get("panels"); const panelGroupElement = dom.getRootEl(scope); (0, import_utils.ensure)(panelGroupElement, () => `Panel group element not found`); const pivotIndices = resolvedResizeTriggerId.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 = (0, import_resize_by_delta.resizeByDelta)({ delta: offsetPercentage, initialSize: initialSize ?? prevSize, panels, pivotIndices, prevSize, trigger: "mouse-or-touch" }); if (!(0, import_utils.isEqual)(prevSize, nextSize)) { setSize(params, nextSize); } }, setKeyboardValue(params) { const { context, event } = params; const panelDataArray = context.get("panels"); const resizeTriggerId = dom.resolveResizeTriggerId(params.scope, event.id); if (!resizeTriggerId) return; const delta = event.delta; const pivotIndices = resizeTriggerId.split(":").map((id) => panelDataArray.findIndex((panelData) => panelData.id === id)); const prevSize = context.get("size"); const nextSize = (0, import_resize_by_delta.resizeByDelta)({ delta, initialSize: prevSize, panels: panelDataArray, pivotIndices, prevSize, trigger: "keyboard" }); if (!(0, import_utils.isEqual)(prevSize, nextSize)) { setSize(params, nextSize); } }, invokeOnResizeEnd({ context, prop, refs }) { queueMicrotask(() => { const dragState = context.get("dragState"); prop("onResizeEnd")?.({ size: refs.get("lastRequestedSize") ?? context.get("size"), resizeTriggerId: dragState?.resizeTriggerId ?? null }); }); }, invokeOnResizeStart({ prop }) { queueMicrotask(() => { prop("onResizeStart")?.(); }); }, collapseOrExpandPanel(params) { const { context, refs } = params; const panelDataArray = context.get("panels"); const sizes = context.get("size"); const resizeTriggerId = context.get("keyboardState")?.resolvedResizeTriggerId; const [idBefore, idAfter] = resizeTriggerId?.split(":") ?? []; const index = panelDataArray.findIndex((panelData2) => panelData2.id === idBefore); if (index === -1) return; const panelData = panelDataArray[index]; (0, import_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 = (0, import_resize_by_delta.resizeByDelta)({ delta: (0, import_fuzzy.fuzzyNumbersEqual)(size, collapsedSize) ? minSize - collapsedSize : collapsedSize - size, initialSize: refs.get("initialSize") ?? sizes, panels: panelDataArray, pivotIndices, prevSize: sizes, trigger: "keyboard" }); if (!(0, import_utils.isEqual)(sizes, nextSize)) { setSize(params, nextSize); } } }, setGlobalCursor(params) { const { context, scope, prop } = params; const registry = prop("registry"); if (registry) return; const dragState = context.get("dragState"); if (!dragState) return; const panels = context.get("panels"); const horizontal = prop("orientation") === "horizontal"; const [idBefore] = dragState.resolvedResizeTriggerId.split(":"); const indexBefore = panels.findIndex((panel2) => panel2.id === idBefore); const panel = panels[indexBefore]; const size = context.get("size"); const aria = (0, import_aria.getAriaValue)(size, panels, dragState.resolvedResizeTriggerId); const isAtMin = (0, import_fuzzy.fuzzyNumbersEqual)(aria.valueNow, aria.valueMin) || (0, import_fuzzy.fuzzyNumbersEqual)(aria.valueNow, panel.collapsedSize); const isAtMax = (0, import_fuzzy.fuzzyNumbersEqual)(aria.valueNow, aria.valueMax); const cursorState = { isAtMin, isAtMax }; dom.setupGlobalCursor(scope, cursorState, horizontal, prop("nonce")); }, clearGlobalCursor({ scope }) { dom.removeGlobalCursor(scope); }, focusNextResizeTrigger({ event, scope }) { const resizeTriggers = dom.getResizeTriggerEls(scope); const index = resizeTriggers.findIndex((el) => el.dataset.id === event.id); const handleEl = event.shiftKey ? (0, import_utils.prev)(resizeTriggers, index) : (0, import_utils.next)(resizeTriggers, index); handleEl?.focus(); } } } }); function setSize(params, sizes) { const { refs, prop, context } = params; const panelsArray = context.get("panels"); const onCollapse = prop("onCollapse"); const onExpand = prop("onExpand"); const onResize = prop("onResize"); const onResizeStart = prop("onResizeStart"); const onResizeEnd = prop("onResizeEnd"); const panelIdToLastNotifiedSizeMap = refs.get("panelIdToLastNotifiedSizeMap"); const dragState = context.get("dragState"); const keyboardState = context.get("keyboardState"); const isProgrammatic = dragState === null && keyboardState === null; refs.set("lastRequestedSize", sizes); if (isProgrammatic && onResizeStart) { queueMicrotask(() => { onResizeStart(); }); } if (prop("size") == null) { context.set("size", sizes); } else if (onResize) { const sizesBeforeCollapse = refs.get("panelSizeBeforeCollapse"); const expandToSizes = Object.fromEntries(sizesBeforeCollapse.entries()); const resizeTriggerId = dragState?.resizeTriggerId ?? null; const layout = (0, import_panel.getPanelLayout)(prop("panels")); onResize({ size: sizes, layout, resizeTriggerId, expandToSizes }); } sizes.forEach((size, index) => { const panelData = panelsArray[index]; (0, import_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 && lastNotifiedSize != null && (onCollapse || onExpand)) { if ((0, import_fuzzy.fuzzyNumbersEqual)(lastNotifiedSize, collapsedSize) && !(0, import_fuzzy.fuzzyNumbersEqual)(size, collapsedSize)) { onExpand?.({ panelId, size }); } if (!(0, import_fuzzy.fuzzyNumbersEqual)(lastNotifiedSize, collapsedSize) && (0, import_fuzzy.fuzzyNumbersEqual)(size, collapsedSize)) { onCollapse?.({ panelId, size }); } } } }); if (isProgrammatic && onResizeEnd) { queueMicrotask(() => { onResizeEnd({ size: sizes, resizeTriggerId: null // Programmatic changes don't have a resize trigger }); }); } } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { machine });