@zag-js/splitter 0.9.2 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-G4OIUPPJ.mjs → chunk-3GDOPT5W.mjs} +1 -1
- package/dist/{chunk-ECKRJG7O.mjs → chunk-53T2VZ2R.mjs} +13 -24
- package/dist/{chunk-PI3URGYH.mjs → chunk-X2E2LAC5.mjs} +21 -6
- package/dist/index.js +32 -28
- package/dist/index.mjs +3 -3
- package/dist/splitter.connect.d.ts +4 -8
- package/dist/splitter.connect.js +13 -24
- package/dist/splitter.connect.mjs +2 -2
- package/dist/splitter.dom.d.ts +1 -1
- package/dist/splitter.dom.js +1 -1
- package/dist/splitter.dom.mjs +1 -1
- package/dist/splitter.machine.js +20 -5
- package/dist/splitter.machine.mjs +2 -2
- package/package.json +10 -9
- package/src/index.ts +4 -0
- package/src/splitter.anatomy.ts +5 -0
- package/src/splitter.connect.ts +186 -0
- package/src/splitter.dom.ts +62 -0
- package/src/splitter.machine.ts +304 -0
- package/src/splitter.types.ts +100 -0
- package/src/splitter.utils.ts +143 -0
|
@@ -7,7 +7,7 @@ var dom = createScope({
|
|
|
7
7
|
getLabelId: (ctx) => ctx.ids?.label ?? `splitter:${ctx.id}:label`,
|
|
8
8
|
getPanelId: (ctx, id) => ctx.ids?.panel?.(id) ?? `splitter:${ctx.id}:panel:${id}`,
|
|
9
9
|
globalCursorId: (ctx) => `splitter:${ctx.id}:global-cursor`,
|
|
10
|
-
getRootEl: (ctx) => dom.
|
|
10
|
+
getRootEl: (ctx) => dom.queryById(ctx, dom.getRootId(ctx)),
|
|
11
11
|
getResizeTriggerEl: (ctx, id) => dom.getById(ctx, dom.getResizeTriggerId(ctx, id)),
|
|
12
12
|
getPanelEl: (ctx, id) => dom.getById(ctx, dom.getPanelId(ctx, id)),
|
|
13
13
|
getCursor(ctx) {
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-HPRMFGOY.mjs";
|
|
4
4
|
import {
|
|
5
5
|
dom
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-3GDOPT5W.mjs";
|
|
7
7
|
import {
|
|
8
8
|
getHandleBounds
|
|
9
9
|
} from "./chunk-MV44GBQY.mjs";
|
|
@@ -15,6 +15,7 @@ function connect(state, send, normalize) {
|
|
|
15
15
|
const isHorizontal = state.context.isHorizontal;
|
|
16
16
|
const isFocused = state.hasTag("focus");
|
|
17
17
|
const isDragging = state.matches("dragging");
|
|
18
|
+
const panels = state.context.panels;
|
|
18
19
|
const api = {
|
|
19
20
|
/**
|
|
20
21
|
* Whether the splitter is focused.
|
|
@@ -29,22 +30,18 @@ function connect(state, send, normalize) {
|
|
|
29
30
|
*/
|
|
30
31
|
bounds: getHandleBounds(state.context),
|
|
31
32
|
/**
|
|
32
|
-
* Function to
|
|
33
|
+
* Function to set a panel to its minimum size.
|
|
33
34
|
*/
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
setToMinSize(id) {
|
|
36
|
+
const panel = panels.find((panel2) => panel2.id === id);
|
|
37
|
+
send({ type: "SET_SIZE", id, size: panel?.minSize, src: "collapse" });
|
|
36
38
|
},
|
|
37
39
|
/**
|
|
38
|
-
* Function to
|
|
40
|
+
* Function to set a panel to its maximum size.
|
|
39
41
|
*/
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Function to toggle a panel between collapsed and expanded.
|
|
45
|
-
*/
|
|
46
|
-
toggle(id) {
|
|
47
|
-
send({ type: "TOGGLE", id });
|
|
42
|
+
setToMaxSize(id) {
|
|
43
|
+
const panel = panels.find((panel2) => panel2.id === id);
|
|
44
|
+
send({ type: "SET_SIZE", id, size: panel?.maxSize, src: "expand" });
|
|
48
45
|
},
|
|
49
46
|
/**
|
|
50
47
|
* Function to set the size of a panel.
|
|
@@ -59,13 +56,13 @@ function connect(state, send, normalize) {
|
|
|
59
56
|
const { id, disabled } = props;
|
|
60
57
|
const ids = id.split(":");
|
|
61
58
|
const panelIds = ids.map((id2) => dom.getPanelId(state.context, id2));
|
|
62
|
-
const
|
|
59
|
+
const panels2 = getHandleBounds(state.context, id);
|
|
63
60
|
return {
|
|
64
61
|
isDisabled: !!disabled,
|
|
65
62
|
isFocused: state.context.activeResizeId === id && isFocused,
|
|
66
63
|
panelIds,
|
|
67
|
-
min:
|
|
68
|
-
max:
|
|
64
|
+
min: panels2?.min,
|
|
65
|
+
max: panels2?.max,
|
|
69
66
|
value: 0
|
|
70
67
|
};
|
|
71
68
|
},
|
|
@@ -92,14 +89,6 @@ function connect(state, send, normalize) {
|
|
|
92
89
|
style: dom.getPanelStyle(state.context, id)
|
|
93
90
|
});
|
|
94
91
|
},
|
|
95
|
-
// toggleTriggerProps: normalize.element({
|
|
96
|
-
// ...parts.toggleButton.attrs,
|
|
97
|
-
// id: dom.getToggleButtonId(state.context),
|
|
98
|
-
// "aria-label": state.context.isAtMin ? "Expand Primary Pane" : "Collapse Primary Pane",
|
|
99
|
-
// onClick() {
|
|
100
|
-
// send("TOGGLE")
|
|
101
|
-
// },
|
|
102
|
-
// }),
|
|
103
92
|
getResizeTriggerProps(props) {
|
|
104
93
|
const { id, disabled, step = 1 } = props;
|
|
105
94
|
const triggerState = api.getResizeTriggerState(props);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
dom
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-3GDOPT5W.mjs";
|
|
4
4
|
import {
|
|
5
5
|
clamp,
|
|
6
6
|
getHandleBounds,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
|
|
12
12
|
// src/splitter.machine.ts
|
|
13
13
|
import { createMachine } from "@zag-js/core";
|
|
14
|
-
import {
|
|
14
|
+
import { getRelativePoint, trackPointerMove } from "@zag-js/dom-event";
|
|
15
15
|
import { raf } from "@zag-js/dom-query";
|
|
16
16
|
import { compact } from "@zag-js/utils";
|
|
17
17
|
function machine(userContext) {
|
|
@@ -55,7 +55,10 @@ function machine(userContext) {
|
|
|
55
55
|
{
|
|
56
56
|
actions: "setStartPanelToMin"
|
|
57
57
|
}
|
|
58
|
-
]
|
|
58
|
+
],
|
|
59
|
+
SET_SIZE: {
|
|
60
|
+
actions: "setPanelSize"
|
|
61
|
+
}
|
|
59
62
|
},
|
|
60
63
|
states: {
|
|
61
64
|
idle: {
|
|
@@ -202,6 +205,13 @@ function machine(userContext) {
|
|
|
202
205
|
size: panel.size
|
|
203
206
|
}));
|
|
204
207
|
},
|
|
208
|
+
setPanelSize(ctx2, evt) {
|
|
209
|
+
const { id, size } = evt;
|
|
210
|
+
ctx2.size = ctx2.size.map((panel) => {
|
|
211
|
+
const panelSize = clamp(size, panel.minSize ?? 0, panel.maxSize ?? 100);
|
|
212
|
+
return panel.id === id ? { ...panel, size: panelSize } : panel;
|
|
213
|
+
});
|
|
214
|
+
},
|
|
205
215
|
setStartPanelToMin(ctx2) {
|
|
206
216
|
const bounds = getPanelBounds(ctx2);
|
|
207
217
|
if (!bounds)
|
|
@@ -268,10 +278,15 @@ function machine(userContext) {
|
|
|
268
278
|
setPointerValue(ctx2, evt) {
|
|
269
279
|
const panels = getHandlePanels(ctx2);
|
|
270
280
|
const bounds = getHandleBounds(ctx2);
|
|
271
|
-
|
|
272
|
-
if (!panels || !rootEl || !bounds)
|
|
281
|
+
if (!panels || !bounds)
|
|
273
282
|
return;
|
|
274
|
-
|
|
283
|
+
const rootEl = dom.getRootEl(ctx2);
|
|
284
|
+
const relativePoint = getRelativePoint(evt.point, rootEl);
|
|
285
|
+
const percentValue = relativePoint.getPercentValue({
|
|
286
|
+
dir: ctx2.dir,
|
|
287
|
+
orientation: ctx2.orientation
|
|
288
|
+
});
|
|
289
|
+
let pointValue = percentValue * 100;
|
|
275
290
|
ctx2.activeResizeState = {
|
|
276
291
|
isAtMin: pointValue < bounds.min,
|
|
277
292
|
isAtMax: pointValue > bounds.max
|
package/dist/index.js
CHANGED
|
@@ -44,7 +44,7 @@ var dom = (0, import_dom_query.createScope)({
|
|
|
44
44
|
getLabelId: (ctx) => ctx.ids?.label ?? `splitter:${ctx.id}:label`,
|
|
45
45
|
getPanelId: (ctx, id) => ctx.ids?.panel?.(id) ?? `splitter:${ctx.id}:panel:${id}`,
|
|
46
46
|
globalCursorId: (ctx) => `splitter:${ctx.id}:global-cursor`,
|
|
47
|
-
getRootEl: (ctx) => dom.
|
|
47
|
+
getRootEl: (ctx) => dom.queryById(ctx, dom.getRootId(ctx)),
|
|
48
48
|
getResizeTriggerEl: (ctx, id) => dom.getById(ctx, dom.getResizeTriggerId(ctx, id)),
|
|
49
49
|
getPanelEl: (ctx, id) => dom.getById(ctx, dom.getPanelId(ctx, id)),
|
|
50
50
|
getCursor(ctx) {
|
|
@@ -219,6 +219,7 @@ function connect(state, send, normalize) {
|
|
|
219
219
|
const isHorizontal = state.context.isHorizontal;
|
|
220
220
|
const isFocused = state.hasTag("focus");
|
|
221
221
|
const isDragging = state.matches("dragging");
|
|
222
|
+
const panels = state.context.panels;
|
|
222
223
|
const api = {
|
|
223
224
|
/**
|
|
224
225
|
* Whether the splitter is focused.
|
|
@@ -233,22 +234,18 @@ function connect(state, send, normalize) {
|
|
|
233
234
|
*/
|
|
234
235
|
bounds: getHandleBounds(state.context),
|
|
235
236
|
/**
|
|
236
|
-
* Function to
|
|
237
|
+
* Function to set a panel to its minimum size.
|
|
237
238
|
*/
|
|
238
|
-
|
|
239
|
-
|
|
239
|
+
setToMinSize(id) {
|
|
240
|
+
const panel = panels.find((panel2) => panel2.id === id);
|
|
241
|
+
send({ type: "SET_SIZE", id, size: panel?.minSize, src: "collapse" });
|
|
240
242
|
},
|
|
241
243
|
/**
|
|
242
|
-
* Function to
|
|
244
|
+
* Function to set a panel to its maximum size.
|
|
243
245
|
*/
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Function to toggle a panel between collapsed and expanded.
|
|
249
|
-
*/
|
|
250
|
-
toggle(id) {
|
|
251
|
-
send({ type: "TOGGLE", id });
|
|
246
|
+
setToMaxSize(id) {
|
|
247
|
+
const panel = panels.find((panel2) => panel2.id === id);
|
|
248
|
+
send({ type: "SET_SIZE", id, size: panel?.maxSize, src: "expand" });
|
|
252
249
|
},
|
|
253
250
|
/**
|
|
254
251
|
* Function to set the size of a panel.
|
|
@@ -263,13 +260,13 @@ function connect(state, send, normalize) {
|
|
|
263
260
|
const { id, disabled } = props;
|
|
264
261
|
const ids = id.split(":");
|
|
265
262
|
const panelIds = ids.map((id2) => dom.getPanelId(state.context, id2));
|
|
266
|
-
const
|
|
263
|
+
const panels2 = getHandleBounds(state.context, id);
|
|
267
264
|
return {
|
|
268
265
|
isDisabled: !!disabled,
|
|
269
266
|
isFocused: state.context.activeResizeId === id && isFocused,
|
|
270
267
|
panelIds,
|
|
271
|
-
min:
|
|
272
|
-
max:
|
|
268
|
+
min: panels2?.min,
|
|
269
|
+
max: panels2?.max,
|
|
273
270
|
value: 0
|
|
274
271
|
};
|
|
275
272
|
},
|
|
@@ -296,14 +293,6 @@ function connect(state, send, normalize) {
|
|
|
296
293
|
style: dom.getPanelStyle(state.context, id)
|
|
297
294
|
});
|
|
298
295
|
},
|
|
299
|
-
// toggleTriggerProps: normalize.element({
|
|
300
|
-
// ...parts.toggleButton.attrs,
|
|
301
|
-
// id: dom.getToggleButtonId(state.context),
|
|
302
|
-
// "aria-label": state.context.isAtMin ? "Expand Primary Pane" : "Collapse Primary Pane",
|
|
303
|
-
// onClick() {
|
|
304
|
-
// send("TOGGLE")
|
|
305
|
-
// },
|
|
306
|
-
// }),
|
|
307
296
|
getResizeTriggerProps(props) {
|
|
308
297
|
const { id, disabled, step = 1 } = props;
|
|
309
298
|
const triggerState = api.getResizeTriggerState(props);
|
|
@@ -446,7 +435,10 @@ function machine(userContext) {
|
|
|
446
435
|
{
|
|
447
436
|
actions: "setStartPanelToMin"
|
|
448
437
|
}
|
|
449
|
-
]
|
|
438
|
+
],
|
|
439
|
+
SET_SIZE: {
|
|
440
|
+
actions: "setPanelSize"
|
|
441
|
+
}
|
|
450
442
|
},
|
|
451
443
|
states: {
|
|
452
444
|
idle: {
|
|
@@ -593,6 +585,13 @@ function machine(userContext) {
|
|
|
593
585
|
size: panel.size
|
|
594
586
|
}));
|
|
595
587
|
},
|
|
588
|
+
setPanelSize(ctx2, evt) {
|
|
589
|
+
const { id, size } = evt;
|
|
590
|
+
ctx2.size = ctx2.size.map((panel) => {
|
|
591
|
+
const panelSize = clamp(size, panel.minSize ?? 0, panel.maxSize ?? 100);
|
|
592
|
+
return panel.id === id ? { ...panel, size: panelSize } : panel;
|
|
593
|
+
});
|
|
594
|
+
},
|
|
596
595
|
setStartPanelToMin(ctx2) {
|
|
597
596
|
const bounds = getPanelBounds(ctx2);
|
|
598
597
|
if (!bounds)
|
|
@@ -659,10 +658,15 @@ function machine(userContext) {
|
|
|
659
658
|
setPointerValue(ctx2, evt) {
|
|
660
659
|
const panels = getHandlePanels(ctx2);
|
|
661
660
|
const bounds = getHandleBounds(ctx2);
|
|
662
|
-
|
|
663
|
-
if (!panels || !rootEl || !bounds)
|
|
661
|
+
if (!panels || !bounds)
|
|
664
662
|
return;
|
|
665
|
-
|
|
663
|
+
const rootEl = dom.getRootEl(ctx2);
|
|
664
|
+
const relativePoint = (0, import_dom_event2.getRelativePoint)(evt.point, rootEl);
|
|
665
|
+
const percentValue = relativePoint.getPercentValue({
|
|
666
|
+
dir: ctx2.dir,
|
|
667
|
+
orientation: ctx2.orientation
|
|
668
|
+
});
|
|
669
|
+
let pointValue = percentValue * 100;
|
|
666
670
|
ctx2.activeResizeState = {
|
|
667
671
|
isAtMin: pointValue < bounds.min,
|
|
668
672
|
isAtMax: pointValue > bounds.max
|
package/dist/index.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
connect
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-53T2VZ2R.mjs";
|
|
4
4
|
import {
|
|
5
5
|
anatomy
|
|
6
6
|
} from "./chunk-HPRMFGOY.mjs";
|
|
7
7
|
import {
|
|
8
8
|
machine
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import "./chunk-
|
|
9
|
+
} from "./chunk-X2E2LAC5.mjs";
|
|
10
|
+
import "./chunk-3GDOPT5W.mjs";
|
|
11
11
|
import "./chunk-MV44GBQY.mjs";
|
|
12
12
|
export {
|
|
13
13
|
anatomy,
|
|
@@ -19,17 +19,13 @@ declare function connect<T extends PropTypes>(state: State, send: Send, normaliz
|
|
|
19
19
|
max: number;
|
|
20
20
|
} | undefined;
|
|
21
21
|
/**
|
|
22
|
-
* Function to
|
|
22
|
+
* Function to set a panel to its minimum size.
|
|
23
23
|
*/
|
|
24
|
-
|
|
24
|
+
setToMinSize(id: PanelId): void;
|
|
25
25
|
/**
|
|
26
|
-
* Function to
|
|
26
|
+
* Function to set a panel to its maximum size.
|
|
27
27
|
*/
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Function to toggle a panel between collapsed and expanded.
|
|
31
|
-
*/
|
|
32
|
-
toggle(id: PanelId): void;
|
|
28
|
+
setToMaxSize(id: PanelId): void;
|
|
33
29
|
/**
|
|
34
30
|
* Function to set the size of a panel.
|
|
35
31
|
*/
|
package/dist/splitter.connect.js
CHANGED
|
@@ -40,7 +40,7 @@ var dom = (0, import_dom_query.createScope)({
|
|
|
40
40
|
getLabelId: (ctx) => ctx.ids?.label ?? `splitter:${ctx.id}:label`,
|
|
41
41
|
getPanelId: (ctx, id) => ctx.ids?.panel?.(id) ?? `splitter:${ctx.id}:panel:${id}`,
|
|
42
42
|
globalCursorId: (ctx) => `splitter:${ctx.id}:global-cursor`,
|
|
43
|
-
getRootEl: (ctx) => dom.
|
|
43
|
+
getRootEl: (ctx) => dom.queryById(ctx, dom.getRootId(ctx)),
|
|
44
44
|
getResizeTriggerEl: (ctx, id) => dom.getById(ctx, dom.getResizeTriggerId(ctx, id)),
|
|
45
45
|
getPanelEl: (ctx, id) => dom.getById(ctx, dom.getPanelId(ctx, id)),
|
|
46
46
|
getCursor(ctx) {
|
|
@@ -126,6 +126,7 @@ function connect(state, send, normalize) {
|
|
|
126
126
|
const isHorizontal = state.context.isHorizontal;
|
|
127
127
|
const isFocused = state.hasTag("focus");
|
|
128
128
|
const isDragging = state.matches("dragging");
|
|
129
|
+
const panels = state.context.panels;
|
|
129
130
|
const api = {
|
|
130
131
|
/**
|
|
131
132
|
* Whether the splitter is focused.
|
|
@@ -140,22 +141,18 @@ function connect(state, send, normalize) {
|
|
|
140
141
|
*/
|
|
141
142
|
bounds: getHandleBounds(state.context),
|
|
142
143
|
/**
|
|
143
|
-
* Function to
|
|
144
|
+
* Function to set a panel to its minimum size.
|
|
144
145
|
*/
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
setToMinSize(id) {
|
|
147
|
+
const panel = panels.find((panel2) => panel2.id === id);
|
|
148
|
+
send({ type: "SET_SIZE", id, size: panel?.minSize, src: "collapse" });
|
|
147
149
|
},
|
|
148
150
|
/**
|
|
149
|
-
* Function to
|
|
151
|
+
* Function to set a panel to its maximum size.
|
|
150
152
|
*/
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Function to toggle a panel between collapsed and expanded.
|
|
156
|
-
*/
|
|
157
|
-
toggle(id) {
|
|
158
|
-
send({ type: "TOGGLE", id });
|
|
153
|
+
setToMaxSize(id) {
|
|
154
|
+
const panel = panels.find((panel2) => panel2.id === id);
|
|
155
|
+
send({ type: "SET_SIZE", id, size: panel?.maxSize, src: "expand" });
|
|
159
156
|
},
|
|
160
157
|
/**
|
|
161
158
|
* Function to set the size of a panel.
|
|
@@ -170,13 +167,13 @@ function connect(state, send, normalize) {
|
|
|
170
167
|
const { id, disabled } = props;
|
|
171
168
|
const ids = id.split(":");
|
|
172
169
|
const panelIds = ids.map((id2) => dom.getPanelId(state.context, id2));
|
|
173
|
-
const
|
|
170
|
+
const panels2 = getHandleBounds(state.context, id);
|
|
174
171
|
return {
|
|
175
172
|
isDisabled: !!disabled,
|
|
176
173
|
isFocused: state.context.activeResizeId === id && isFocused,
|
|
177
174
|
panelIds,
|
|
178
|
-
min:
|
|
179
|
-
max:
|
|
175
|
+
min: panels2?.min,
|
|
176
|
+
max: panels2?.max,
|
|
180
177
|
value: 0
|
|
181
178
|
};
|
|
182
179
|
},
|
|
@@ -203,14 +200,6 @@ function connect(state, send, normalize) {
|
|
|
203
200
|
style: dom.getPanelStyle(state.context, id)
|
|
204
201
|
});
|
|
205
202
|
},
|
|
206
|
-
// toggleTriggerProps: normalize.element({
|
|
207
|
-
// ...parts.toggleButton.attrs,
|
|
208
|
-
// id: dom.getToggleButtonId(state.context),
|
|
209
|
-
// "aria-label": state.context.isAtMin ? "Expand Primary Pane" : "Collapse Primary Pane",
|
|
210
|
-
// onClick() {
|
|
211
|
-
// send("TOGGLE")
|
|
212
|
-
// },
|
|
213
|
-
// }),
|
|
214
203
|
getResizeTriggerProps(props) {
|
|
215
204
|
const { id, disabled, step = 1 } = props;
|
|
216
205
|
const triggerState = api.getResizeTriggerState(props);
|
package/dist/splitter.dom.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ declare const dom: {
|
|
|
28
28
|
getLabelId: (ctx: MachineContext) => string | ((id: string) => string);
|
|
29
29
|
getPanelId: (ctx: MachineContext, id: string | number) => string;
|
|
30
30
|
globalCursorId: (ctx: MachineContext) => string;
|
|
31
|
-
getRootEl: (ctx: MachineContext) => HTMLElement
|
|
31
|
+
getRootEl: (ctx: MachineContext) => HTMLElement;
|
|
32
32
|
getResizeTriggerEl: (ctx: MachineContext, id: string) => HTMLElement | null;
|
|
33
33
|
getPanelEl: (ctx: MachineContext, id: string | number) => HTMLElement | null;
|
|
34
34
|
getCursor(ctx: MachineContext): (string & {}) | "col-resize" | "e-resize" | "n-resize" | "row-resize" | "s-resize" | "w-resize";
|
package/dist/splitter.dom.js
CHANGED
|
@@ -31,7 +31,7 @@ var dom = (0, import_dom_query.createScope)({
|
|
|
31
31
|
getLabelId: (ctx) => ctx.ids?.label ?? `splitter:${ctx.id}:label`,
|
|
32
32
|
getPanelId: (ctx, id) => ctx.ids?.panel?.(id) ?? `splitter:${ctx.id}:panel:${id}`,
|
|
33
33
|
globalCursorId: (ctx) => `splitter:${ctx.id}:global-cursor`,
|
|
34
|
-
getRootEl: (ctx) => dom.
|
|
34
|
+
getRootEl: (ctx) => dom.queryById(ctx, dom.getRootId(ctx)),
|
|
35
35
|
getResizeTriggerEl: (ctx, id) => dom.getById(ctx, dom.getResizeTriggerId(ctx, id)),
|
|
36
36
|
getPanelEl: (ctx, id) => dom.getById(ctx, dom.getPanelId(ctx, id)),
|
|
37
37
|
getCursor(ctx) {
|
package/dist/splitter.dom.mjs
CHANGED
package/dist/splitter.machine.js
CHANGED
|
@@ -37,7 +37,7 @@ var dom = (0, import_dom_query.createScope)({
|
|
|
37
37
|
getLabelId: (ctx) => ctx.ids?.label ?? `splitter:${ctx.id}:label`,
|
|
38
38
|
getPanelId: (ctx, id) => ctx.ids?.panel?.(id) ?? `splitter:${ctx.id}:panel:${id}`,
|
|
39
39
|
globalCursorId: (ctx) => `splitter:${ctx.id}:global-cursor`,
|
|
40
|
-
getRootEl: (ctx) => dom.
|
|
40
|
+
getRootEl: (ctx) => dom.queryById(ctx, dom.getRootId(ctx)),
|
|
41
41
|
getResizeTriggerEl: (ctx, id) => dom.getById(ctx, dom.getResizeTriggerId(ctx, id)),
|
|
42
42
|
getPanelEl: (ctx, id) => dom.getById(ctx, dom.getPanelId(ctx, id)),
|
|
43
43
|
getCursor(ctx) {
|
|
@@ -249,7 +249,10 @@ function machine(userContext) {
|
|
|
249
249
|
{
|
|
250
250
|
actions: "setStartPanelToMin"
|
|
251
251
|
}
|
|
252
|
-
]
|
|
252
|
+
],
|
|
253
|
+
SET_SIZE: {
|
|
254
|
+
actions: "setPanelSize"
|
|
255
|
+
}
|
|
253
256
|
},
|
|
254
257
|
states: {
|
|
255
258
|
idle: {
|
|
@@ -396,6 +399,13 @@ function machine(userContext) {
|
|
|
396
399
|
size: panel.size
|
|
397
400
|
}));
|
|
398
401
|
},
|
|
402
|
+
setPanelSize(ctx2, evt) {
|
|
403
|
+
const { id, size } = evt;
|
|
404
|
+
ctx2.size = ctx2.size.map((panel) => {
|
|
405
|
+
const panelSize = clamp(size, panel.minSize ?? 0, panel.maxSize ?? 100);
|
|
406
|
+
return panel.id === id ? { ...panel, size: panelSize } : panel;
|
|
407
|
+
});
|
|
408
|
+
},
|
|
399
409
|
setStartPanelToMin(ctx2) {
|
|
400
410
|
const bounds = getPanelBounds(ctx2);
|
|
401
411
|
if (!bounds)
|
|
@@ -462,10 +472,15 @@ function machine(userContext) {
|
|
|
462
472
|
setPointerValue(ctx2, evt) {
|
|
463
473
|
const panels = getHandlePanels(ctx2);
|
|
464
474
|
const bounds = getHandleBounds(ctx2);
|
|
465
|
-
|
|
466
|
-
if (!panels || !rootEl || !bounds)
|
|
475
|
+
if (!panels || !bounds)
|
|
467
476
|
return;
|
|
468
|
-
|
|
477
|
+
const rootEl = dom.getRootEl(ctx2);
|
|
478
|
+
const relativePoint = (0, import_dom_event.getRelativePoint)(evt.point, rootEl);
|
|
479
|
+
const percentValue = relativePoint.getPercentValue({
|
|
480
|
+
dir: ctx2.dir,
|
|
481
|
+
orientation: ctx2.orientation
|
|
482
|
+
});
|
|
483
|
+
let pointValue = percentValue * 100;
|
|
469
484
|
ctx2.activeResizeState = {
|
|
470
485
|
isAtMin: pointValue < bounds.min,
|
|
471
486
|
isAtMax: pointValue > bounds.max
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zag-js/splitter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Core logic for the splitter widget implemented as a state machine",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"js",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"repository": "https://github.com/chakra-ui/zag/tree/main/packages/splitter",
|
|
19
19
|
"sideEffects": false,
|
|
20
20
|
"files": [
|
|
21
|
-
"dist
|
|
21
|
+
"dist",
|
|
22
|
+
"src"
|
|
22
23
|
],
|
|
23
24
|
"publishConfig": {
|
|
24
25
|
"access": "public"
|
|
@@ -27,13 +28,13 @@
|
|
|
27
28
|
"url": "https://github.com/chakra-ui/zag/issues"
|
|
28
29
|
},
|
|
29
30
|
"dependencies": {
|
|
30
|
-
"@zag-js/anatomy": "0.
|
|
31
|
-
"@zag-js/core": "0.
|
|
32
|
-
"@zag-js/types": "0.
|
|
33
|
-
"@zag-js/dom-query": "0.
|
|
34
|
-
"@zag-js/dom-event": "0.
|
|
35
|
-
"@zag-js/number-utils": "0.
|
|
36
|
-
"@zag-js/utils": "0.
|
|
31
|
+
"@zag-js/anatomy": "0.10.1",
|
|
32
|
+
"@zag-js/core": "0.10.1",
|
|
33
|
+
"@zag-js/types": "0.10.1",
|
|
34
|
+
"@zag-js/dom-query": "0.10.1",
|
|
35
|
+
"@zag-js/dom-event": "0.10.1",
|
|
36
|
+
"@zag-js/number-utils": "0.10.1",
|
|
37
|
+
"@zag-js/utils": "0.10.1"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"clean-package": "2.2.0"
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { EventKeyMap, getEventKey, getEventStep } from "@zag-js/dom-event"
|
|
2
|
+
import { dataAttr } from "@zag-js/dom-query"
|
|
3
|
+
import type { NormalizeProps, PropTypes } from "@zag-js/types"
|
|
4
|
+
import { parts } from "./splitter.anatomy"
|
|
5
|
+
import { dom } from "./splitter.dom"
|
|
6
|
+
import type { PanelId, PanelProps, ResizeTriggerProps, Send, State } from "./splitter.types"
|
|
7
|
+
import { getHandleBounds } from "./splitter.utils"
|
|
8
|
+
|
|
9
|
+
export function connect<T extends PropTypes>(state: State, send: Send, normalize: NormalizeProps<T>) {
|
|
10
|
+
const isHorizontal = state.context.isHorizontal
|
|
11
|
+
const isFocused = state.hasTag("focus")
|
|
12
|
+
const isDragging = state.matches("dragging")
|
|
13
|
+
const panels = state.context.panels
|
|
14
|
+
|
|
15
|
+
const api = {
|
|
16
|
+
/**
|
|
17
|
+
* Whether the splitter is focused.
|
|
18
|
+
*/
|
|
19
|
+
isFocused,
|
|
20
|
+
/**
|
|
21
|
+
* Whether the splitter is being dragged.
|
|
22
|
+
*/
|
|
23
|
+
isDragging,
|
|
24
|
+
/**
|
|
25
|
+
* The bounds of the currently dragged splitter handle.
|
|
26
|
+
*/
|
|
27
|
+
bounds: getHandleBounds(state.context),
|
|
28
|
+
/**
|
|
29
|
+
* Function to set a panel to its minimum size.
|
|
30
|
+
*/
|
|
31
|
+
setToMinSize(id: PanelId) {
|
|
32
|
+
const panel = panels.find((panel) => panel.id === id)
|
|
33
|
+
send({ type: "SET_SIZE", id, size: panel?.minSize, src: "collapse" })
|
|
34
|
+
},
|
|
35
|
+
/**
|
|
36
|
+
* Function to set a panel to its maximum size.
|
|
37
|
+
*/
|
|
38
|
+
setToMaxSize(id: PanelId) {
|
|
39
|
+
const panel = panels.find((panel) => panel.id === id)
|
|
40
|
+
send({ type: "SET_SIZE", id, size: panel?.maxSize, src: "expand" })
|
|
41
|
+
},
|
|
42
|
+
/**
|
|
43
|
+
* Function to set the size of a panel.
|
|
44
|
+
*/
|
|
45
|
+
setSize(id: PanelId, size: number) {
|
|
46
|
+
send({ type: "SET_SIZE", id, size })
|
|
47
|
+
},
|
|
48
|
+
/**
|
|
49
|
+
* Returns the state details for a resize trigger.
|
|
50
|
+
*/
|
|
51
|
+
getResizeTriggerState(props: ResizeTriggerProps) {
|
|
52
|
+
const { id, disabled } = props
|
|
53
|
+
const ids = id.split(":")
|
|
54
|
+
const panelIds = ids.map((id) => dom.getPanelId(state.context, id))
|
|
55
|
+
const panels = getHandleBounds(state.context, id)
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
isDisabled: !!disabled,
|
|
59
|
+
isFocused: state.context.activeResizeId === id && isFocused,
|
|
60
|
+
panelIds,
|
|
61
|
+
min: panels?.min,
|
|
62
|
+
max: panels?.max,
|
|
63
|
+
value: 0,
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
rootProps: normalize.element({
|
|
68
|
+
...parts.root.attrs,
|
|
69
|
+
"data-orientation": state.context.orientation,
|
|
70
|
+
id: dom.getRootId(state.context),
|
|
71
|
+
dir: state.context.dir,
|
|
72
|
+
style: {
|
|
73
|
+
display: "flex",
|
|
74
|
+
flexDirection: isHorizontal ? "row" : "column",
|
|
75
|
+
height: "100%",
|
|
76
|
+
width: "100%",
|
|
77
|
+
overflow: "hidden",
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
|
|
81
|
+
getPanelProps(props: PanelProps) {
|
|
82
|
+
const { id } = props
|
|
83
|
+
return normalize.element({
|
|
84
|
+
...parts.panel.attrs,
|
|
85
|
+
dir: state.context.dir,
|
|
86
|
+
id: dom.getPanelId(state.context, id),
|
|
87
|
+
"data-ownedby": dom.getRootId(state.context),
|
|
88
|
+
style: dom.getPanelStyle(state.context, id),
|
|
89
|
+
})
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
getResizeTriggerProps(props: ResizeTriggerProps) {
|
|
93
|
+
const { id, disabled, step = 1 } = props
|
|
94
|
+
const triggerState = api.getResizeTriggerState(props)
|
|
95
|
+
|
|
96
|
+
return normalize.element({
|
|
97
|
+
...parts.resizeTrigger.attrs,
|
|
98
|
+
dir: state.context.dir,
|
|
99
|
+
id: dom.getResizeTriggerId(state.context, id),
|
|
100
|
+
role: "separator",
|
|
101
|
+
"data-ownedby": dom.getRootId(state.context),
|
|
102
|
+
tabIndex: disabled ? undefined : 0,
|
|
103
|
+
"aria-valuenow": triggerState.value,
|
|
104
|
+
"aria-valuemin": triggerState.min,
|
|
105
|
+
"aria-valuemax": triggerState.max,
|
|
106
|
+
"data-orientation": state.context.orientation,
|
|
107
|
+
"aria-orientation": state.context.orientation,
|
|
108
|
+
"aria-controls": triggerState.panelIds.join(" "),
|
|
109
|
+
"data-focus": dataAttr(triggerState.isFocused),
|
|
110
|
+
"data-disabled": dataAttr(disabled),
|
|
111
|
+
style: {
|
|
112
|
+
touchAction: "none",
|
|
113
|
+
userSelect: "none",
|
|
114
|
+
flex: "0 0 auto",
|
|
115
|
+
pointerEvents: isDragging && !triggerState.isFocused ? "none" : undefined,
|
|
116
|
+
cursor: isHorizontal ? "col-resize" : "row-resize",
|
|
117
|
+
[isHorizontal ? "minHeight" : "minWidth"]: "0",
|
|
118
|
+
},
|
|
119
|
+
onPointerDown(event) {
|
|
120
|
+
if (disabled) {
|
|
121
|
+
event.preventDefault()
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
send({ type: "POINTER_DOWN", id })
|
|
125
|
+
event.preventDefault()
|
|
126
|
+
event.stopPropagation()
|
|
127
|
+
},
|
|
128
|
+
onPointerOver() {
|
|
129
|
+
if (disabled) return
|
|
130
|
+
send({ type: "POINTER_OVER", id })
|
|
131
|
+
},
|
|
132
|
+
onPointerLeave() {
|
|
133
|
+
if (disabled) return
|
|
134
|
+
send({ type: "POINTER_LEAVE", id })
|
|
135
|
+
},
|
|
136
|
+
onBlur() {
|
|
137
|
+
send("BLUR")
|
|
138
|
+
},
|
|
139
|
+
onFocus() {
|
|
140
|
+
send({ type: "FOCUS", id })
|
|
141
|
+
},
|
|
142
|
+
onDoubleClick() {
|
|
143
|
+
if (disabled) return
|
|
144
|
+
send({ type: "DOUBLE_CLICK", id })
|
|
145
|
+
},
|
|
146
|
+
onKeyDown(event) {
|
|
147
|
+
if (disabled) return
|
|
148
|
+
const moveStep = getEventStep(event) * step
|
|
149
|
+
const keyMap: EventKeyMap = {
|
|
150
|
+
Enter() {
|
|
151
|
+
send("ENTER")
|
|
152
|
+
},
|
|
153
|
+
ArrowUp() {
|
|
154
|
+
send({ type: "ARROW_UP", step: moveStep })
|
|
155
|
+
},
|
|
156
|
+
ArrowDown() {
|
|
157
|
+
send({ type: "ARROW_DOWN", step: moveStep })
|
|
158
|
+
},
|
|
159
|
+
ArrowLeft() {
|
|
160
|
+
send({ type: "ARROW_LEFT", step: moveStep })
|
|
161
|
+
},
|
|
162
|
+
ArrowRight() {
|
|
163
|
+
send({ type: "ARROW_RIGHT", step: moveStep })
|
|
164
|
+
},
|
|
165
|
+
Home() {
|
|
166
|
+
send("HOME")
|
|
167
|
+
},
|
|
168
|
+
End() {
|
|
169
|
+
send("END")
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const key = getEventKey(event, state.context)
|
|
174
|
+
const exec = keyMap[key]
|
|
175
|
+
|
|
176
|
+
if (exec) {
|
|
177
|
+
exec(event)
|
|
178
|
+
event.preventDefault()
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
},
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return api
|
|
186
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createScope, queryAll } from "@zag-js/dom-query"
|
|
2
|
+
import type { JSX, Style } from "@zag-js/types"
|
|
3
|
+
import type { MachineContext as Ctx, PanelId } from "./splitter.types"
|
|
4
|
+
|
|
5
|
+
export const dom = createScope({
|
|
6
|
+
getRootId: (ctx: Ctx) => ctx.ids?.root ?? `splitter:${ctx.id}`,
|
|
7
|
+
getResizeTriggerId: (ctx: Ctx, id: string) => ctx.ids?.resizeTrigger?.(id) ?? `splitter:${ctx.id}:splitter:${id}`,
|
|
8
|
+
getToggleTriggerId: (ctx: Ctx) => ctx.ids?.toggleTrigger?.(ctx.id) ?? `splitter:${ctx.id}:toggle-btn`,
|
|
9
|
+
getLabelId: (ctx: Ctx) => ctx.ids?.label ?? `splitter:${ctx.id}:label`,
|
|
10
|
+
getPanelId: (ctx: Ctx, id: string | number) => ctx.ids?.panel?.(id) ?? `splitter:${ctx.id}:panel:${id}`,
|
|
11
|
+
globalCursorId: (ctx: Ctx) => `splitter:${ctx.id}:global-cursor`,
|
|
12
|
+
|
|
13
|
+
getRootEl: (ctx: Ctx) => dom.queryById(ctx, dom.getRootId(ctx)),
|
|
14
|
+
getResizeTriggerEl: (ctx: Ctx, id: string) => dom.getById(ctx, dom.getResizeTriggerId(ctx, id)),
|
|
15
|
+
getPanelEl: (ctx: Ctx, id: string | number) => dom.getById(ctx, dom.getPanelId(ctx, id)),
|
|
16
|
+
|
|
17
|
+
getCursor(ctx: Ctx) {
|
|
18
|
+
const x = ctx.isHorizontal
|
|
19
|
+
let cursor: Style["cursor"] = x ? "col-resize" : "row-resize"
|
|
20
|
+
if (ctx.activeResizeState.isAtMin) cursor = x ? "e-resize" : "s-resize"
|
|
21
|
+
if (ctx.activeResizeState.isAtMax) cursor = x ? "w-resize" : "n-resize"
|
|
22
|
+
return cursor
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
getPanelStyle(ctx: Ctx, id: PanelId): JSX.CSSProperties {
|
|
26
|
+
const flexGrow = ctx.panels.find((panel) => panel.id === id)?.size ?? "0"
|
|
27
|
+
return {
|
|
28
|
+
flexBasis: 0,
|
|
29
|
+
flexGrow,
|
|
30
|
+
flexShrink: 1,
|
|
31
|
+
overflow: "hidden",
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
getActiveHandleEl(ctx: Ctx) {
|
|
36
|
+
const activeId = ctx.activeResizeId
|
|
37
|
+
if (activeId == null) return
|
|
38
|
+
return dom.getById(ctx, dom.getResizeTriggerId(ctx, activeId))
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
getResizeTriggerEls(ctx: Ctx) {
|
|
42
|
+
const ownerId = CSS.escape(dom.getRootId(ctx))
|
|
43
|
+
return queryAll(dom.getRootEl(ctx), `[role=separator][data-ownedby='${ownerId}']`)
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
setupGlobalCursor(ctx: Ctx) {
|
|
47
|
+
const styleEl = dom.getById(ctx, dom.globalCursorId(ctx))
|
|
48
|
+
const textContent = `* { cursor: ${dom.getCursor(ctx)} !important; }`
|
|
49
|
+
if (styleEl) {
|
|
50
|
+
styleEl.textContent = textContent
|
|
51
|
+
} else {
|
|
52
|
+
const style = dom.getDoc(ctx).createElement("style")
|
|
53
|
+
style.id = dom.globalCursorId(ctx)
|
|
54
|
+
style.textContent = textContent
|
|
55
|
+
dom.getDoc(ctx).head.appendChild(style)
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
removeGlobalCursor(ctx: Ctx) {
|
|
60
|
+
dom.getById(ctx, dom.globalCursorId(ctx))?.remove()
|
|
61
|
+
},
|
|
62
|
+
})
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { createMachine } from "@zag-js/core"
|
|
2
|
+
import { getRelativePoint, trackPointerMove } from "@zag-js/dom-event"
|
|
3
|
+
import { raf } from "@zag-js/dom-query"
|
|
4
|
+
import { compact } from "@zag-js/utils"
|
|
5
|
+
import { dom } from "./splitter.dom"
|
|
6
|
+
import type { MachineContext, MachineState, UserDefinedContext } from "./splitter.types"
|
|
7
|
+
import { clamp, getHandleBounds, getHandlePanels, getNormalizedPanels, getPanelBounds } from "./splitter.utils"
|
|
8
|
+
|
|
9
|
+
export function machine(userContext: UserDefinedContext) {
|
|
10
|
+
const ctx = compact(userContext)
|
|
11
|
+
return createMachine<MachineContext, MachineState>(
|
|
12
|
+
{
|
|
13
|
+
id: "splitter",
|
|
14
|
+
initial: "idle",
|
|
15
|
+
context: {
|
|
16
|
+
orientation: "horizontal",
|
|
17
|
+
activeResizeId: null,
|
|
18
|
+
previousPanels: [],
|
|
19
|
+
size: [],
|
|
20
|
+
initialSize: [],
|
|
21
|
+
activeResizeState: {
|
|
22
|
+
isAtMin: false,
|
|
23
|
+
isAtMax: false,
|
|
24
|
+
},
|
|
25
|
+
...ctx,
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
created: ["setPreviousPanels", "setInitialSize"],
|
|
29
|
+
|
|
30
|
+
watch: {
|
|
31
|
+
size: ["setActiveResizeState"],
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
computed: {
|
|
35
|
+
isHorizontal: (ctx) => ctx.orientation === "horizontal",
|
|
36
|
+
panels: (ctx) => getNormalizedPanels(ctx),
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
on: {
|
|
40
|
+
COLLAPSE: {
|
|
41
|
+
actions: "setStartPanelToMin",
|
|
42
|
+
},
|
|
43
|
+
EXPAND: {
|
|
44
|
+
actions: "setStartPanelToMax",
|
|
45
|
+
},
|
|
46
|
+
TOGGLE: [
|
|
47
|
+
{
|
|
48
|
+
guard: "isStartPanelAtMin",
|
|
49
|
+
actions: "setStartPanelToMax",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
actions: "setStartPanelToMin",
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
SET_SIZE: {
|
|
56
|
+
actions: "setPanelSize",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
states: {
|
|
60
|
+
idle: {
|
|
61
|
+
entry: ["clearActiveHandleId"],
|
|
62
|
+
on: {
|
|
63
|
+
POINTER_OVER: {
|
|
64
|
+
target: "hover:temp",
|
|
65
|
+
actions: ["setActiveHandleId"],
|
|
66
|
+
},
|
|
67
|
+
FOCUS: {
|
|
68
|
+
target: "focused",
|
|
69
|
+
actions: ["setActiveHandleId"],
|
|
70
|
+
},
|
|
71
|
+
DOUBLE_CLICK: {
|
|
72
|
+
actions: ["resetStartPanel", "setPreviousPanels"],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
"hover:temp": {
|
|
78
|
+
after: {
|
|
79
|
+
HOVER_DELAY: "hover",
|
|
80
|
+
},
|
|
81
|
+
on: {
|
|
82
|
+
POINTER_DOWN: {
|
|
83
|
+
target: "dragging",
|
|
84
|
+
actions: ["setActiveHandleId", "invokeOnResizeStart"],
|
|
85
|
+
},
|
|
86
|
+
POINTER_LEAVE: "idle",
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
hover: {
|
|
91
|
+
tags: ["focus"],
|
|
92
|
+
on: {
|
|
93
|
+
POINTER_DOWN: {
|
|
94
|
+
target: "dragging",
|
|
95
|
+
actions: ["invokeOnResizeStart"],
|
|
96
|
+
},
|
|
97
|
+
POINTER_LEAVE: "idle",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
focused: {
|
|
102
|
+
tags: ["focus"],
|
|
103
|
+
on: {
|
|
104
|
+
BLUR: "idle",
|
|
105
|
+
POINTER_DOWN: {
|
|
106
|
+
target: "dragging",
|
|
107
|
+
actions: ["setActiveHandleId", "invokeOnResizeStart"],
|
|
108
|
+
},
|
|
109
|
+
ARROW_LEFT: {
|
|
110
|
+
guard: "isHorizontal",
|
|
111
|
+
actions: ["shrinkStartPanel", "setPreviousPanels"],
|
|
112
|
+
},
|
|
113
|
+
ARROW_RIGHT: {
|
|
114
|
+
guard: "isHorizontal",
|
|
115
|
+
actions: ["expandStartPanel", "setPreviousPanels"],
|
|
116
|
+
},
|
|
117
|
+
ARROW_UP: {
|
|
118
|
+
guard: "isVertical",
|
|
119
|
+
actions: ["shrinkStartPanel", "setPreviousPanels"],
|
|
120
|
+
},
|
|
121
|
+
ARROW_DOWN: {
|
|
122
|
+
guard: "isVertical",
|
|
123
|
+
actions: ["expandStartPanel", "setPreviousPanels"],
|
|
124
|
+
},
|
|
125
|
+
ENTER: [
|
|
126
|
+
{
|
|
127
|
+
guard: "isStartPanelAtMax",
|
|
128
|
+
actions: ["setStartPanelToMin", "setPreviousPanels"],
|
|
129
|
+
},
|
|
130
|
+
{ actions: ["setStartPanelToMax", "setPreviousPanels"] },
|
|
131
|
+
],
|
|
132
|
+
HOME: {
|
|
133
|
+
actions: ["setStartPanelToMin", "setPreviousPanels"],
|
|
134
|
+
},
|
|
135
|
+
END: {
|
|
136
|
+
actions: ["setStartPanelToMax", "setPreviousPanels"],
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
dragging: {
|
|
142
|
+
tags: ["focus"],
|
|
143
|
+
entry: "focusResizeHandle",
|
|
144
|
+
activities: ["trackPointerMove"],
|
|
145
|
+
on: {
|
|
146
|
+
POINTER_MOVE: {
|
|
147
|
+
actions: ["setPointerValue", "setGlobalCursor"],
|
|
148
|
+
},
|
|
149
|
+
POINTER_UP: {
|
|
150
|
+
target: "focused",
|
|
151
|
+
actions: ["invokeOnResizeEnd", "setPreviousPanels", "clearGlobalCursor", "blurResizeHandle"],
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
activities: {
|
|
159
|
+
trackPointerMove: (ctx, _evt, { send }) => {
|
|
160
|
+
const doc = dom.getDoc(ctx)
|
|
161
|
+
return trackPointerMove(doc, {
|
|
162
|
+
onPointerMove(info) {
|
|
163
|
+
send({ type: "POINTER_MOVE", point: info.point })
|
|
164
|
+
},
|
|
165
|
+
onPointerUp() {
|
|
166
|
+
send("POINTER_UP")
|
|
167
|
+
},
|
|
168
|
+
})
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
guards: {
|
|
172
|
+
isStartPanelAtMin: (ctx) => ctx.activeResizeState.isAtMin,
|
|
173
|
+
isStartPanelAtMax: (ctx) => ctx.activeResizeState.isAtMax,
|
|
174
|
+
isHorizontal: (ctx) => ctx.isHorizontal,
|
|
175
|
+
isVertical: (ctx) => !ctx.isHorizontal,
|
|
176
|
+
},
|
|
177
|
+
delays: {
|
|
178
|
+
HOVER_DELAY: 250,
|
|
179
|
+
},
|
|
180
|
+
actions: {
|
|
181
|
+
setGlobalCursor(ctx) {
|
|
182
|
+
dom.setupGlobalCursor(ctx)
|
|
183
|
+
},
|
|
184
|
+
clearGlobalCursor(ctx) {
|
|
185
|
+
dom.removeGlobalCursor(ctx)
|
|
186
|
+
},
|
|
187
|
+
invokeOnResize(ctx) {
|
|
188
|
+
ctx.onResize?.({ size: ctx.size, activeHandleId: ctx.activeResizeId })
|
|
189
|
+
},
|
|
190
|
+
invokeOnResizeStart(ctx) {
|
|
191
|
+
ctx.onResizeStart?.({ size: ctx.size, activeHandleId: ctx.activeResizeId })
|
|
192
|
+
},
|
|
193
|
+
invokeOnResizeEnd(ctx) {
|
|
194
|
+
ctx.onResizeEnd?.({ size: ctx.size, activeHandleId: ctx.activeResizeId })
|
|
195
|
+
},
|
|
196
|
+
setActiveHandleId(ctx, evt) {
|
|
197
|
+
ctx.activeResizeId = evt.id
|
|
198
|
+
},
|
|
199
|
+
clearActiveHandleId(ctx) {
|
|
200
|
+
ctx.activeResizeId = null
|
|
201
|
+
},
|
|
202
|
+
setInitialSize(ctx) {
|
|
203
|
+
ctx.initialSize = ctx.panels.slice().map((panel) => ({
|
|
204
|
+
id: panel.id,
|
|
205
|
+
size: panel.size,
|
|
206
|
+
}))
|
|
207
|
+
},
|
|
208
|
+
setPanelSize(ctx, evt) {
|
|
209
|
+
const { id, size } = evt
|
|
210
|
+
ctx.size = ctx.size.map((panel) => {
|
|
211
|
+
const panelSize = clamp(size, panel.minSize ?? 0, panel.maxSize ?? 100)
|
|
212
|
+
return panel.id === id ? { ...panel, size: panelSize } : panel
|
|
213
|
+
})
|
|
214
|
+
},
|
|
215
|
+
setStartPanelToMin(ctx) {
|
|
216
|
+
const bounds = getPanelBounds(ctx)
|
|
217
|
+
if (!bounds) return
|
|
218
|
+
const { before, after } = bounds
|
|
219
|
+
ctx.size[before.index].size = before.min
|
|
220
|
+
ctx.size[after.index].size = after.min
|
|
221
|
+
},
|
|
222
|
+
setStartPanelToMax(ctx) {
|
|
223
|
+
const bounds = getPanelBounds(ctx)
|
|
224
|
+
if (!bounds) return
|
|
225
|
+
const { before, after } = bounds
|
|
226
|
+
ctx.size[before.index].size = before.max
|
|
227
|
+
ctx.size[after.index].size = after.max
|
|
228
|
+
},
|
|
229
|
+
expandStartPanel(ctx, evt) {
|
|
230
|
+
const bounds = getPanelBounds(ctx)
|
|
231
|
+
if (!bounds) return
|
|
232
|
+
const { before, after } = bounds
|
|
233
|
+
ctx.size[before.index].size = before.up(evt.step)
|
|
234
|
+
ctx.size[after.index].size = after.down(evt.step)
|
|
235
|
+
},
|
|
236
|
+
shrinkStartPanel(ctx, evt) {
|
|
237
|
+
const bounds = getPanelBounds(ctx)
|
|
238
|
+
if (!bounds) return
|
|
239
|
+
const { before, after } = bounds
|
|
240
|
+
ctx.size[before.index].size = before.down(evt.step)
|
|
241
|
+
ctx.size[after.index].size = after.up(evt.step)
|
|
242
|
+
},
|
|
243
|
+
resetStartPanel(ctx, evt) {
|
|
244
|
+
const bounds = getPanelBounds(ctx, evt.id)
|
|
245
|
+
if (!bounds) return
|
|
246
|
+
const { before, after } = bounds
|
|
247
|
+
ctx.size[before.index].size = ctx.initialSize[before.index].size
|
|
248
|
+
ctx.size[after.index].size = ctx.initialSize[after.index].size
|
|
249
|
+
},
|
|
250
|
+
focusResizeHandle(ctx) {
|
|
251
|
+
raf(() => {
|
|
252
|
+
dom.getActiveHandleEl(ctx)?.focus({ preventScroll: true })
|
|
253
|
+
})
|
|
254
|
+
},
|
|
255
|
+
blurResizeHandle(ctx) {
|
|
256
|
+
raf(() => {
|
|
257
|
+
dom.getActiveHandleEl(ctx)?.blur()
|
|
258
|
+
})
|
|
259
|
+
},
|
|
260
|
+
setPreviousPanels(ctx) {
|
|
261
|
+
ctx.previousPanels = ctx.panels.slice()
|
|
262
|
+
},
|
|
263
|
+
setActiveResizeState(ctx) {
|
|
264
|
+
const panels = getPanelBounds(ctx)
|
|
265
|
+
if (!panels) return
|
|
266
|
+
const { before } = panels
|
|
267
|
+
ctx.activeResizeState = {
|
|
268
|
+
isAtMin: before.isAtMin,
|
|
269
|
+
isAtMax: before.isAtMax,
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
setPointerValue(ctx, evt) {
|
|
273
|
+
const panels = getHandlePanels(ctx)
|
|
274
|
+
const bounds = getHandleBounds(ctx)
|
|
275
|
+
|
|
276
|
+
if (!panels || !bounds) return
|
|
277
|
+
|
|
278
|
+
const rootEl = dom.getRootEl(ctx)
|
|
279
|
+
const relativePoint = getRelativePoint(evt.point, rootEl)
|
|
280
|
+
const percentValue = relativePoint.getPercentValue({
|
|
281
|
+
dir: ctx.dir,
|
|
282
|
+
orientation: ctx.orientation,
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
let pointValue = percentValue * 100
|
|
286
|
+
|
|
287
|
+
// update active resize state here because we use `previousPanels` in the calculations
|
|
288
|
+
ctx.activeResizeState = {
|
|
289
|
+
isAtMin: pointValue < bounds.min,
|
|
290
|
+
isAtMax: pointValue > bounds.max,
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
pointValue = clamp(pointValue, bounds.min, bounds.max)
|
|
294
|
+
|
|
295
|
+
const { before, after } = panels
|
|
296
|
+
|
|
297
|
+
const offset = pointValue - before.end
|
|
298
|
+
ctx.size[before.index].size = before.size + offset
|
|
299
|
+
ctx.size[after.index].size = after.size - offset
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
)
|
|
304
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { StateMachine as S } from "@zag-js/core"
|
|
2
|
+
import type { CommonProperties, Context, DirectionProperty, RequiredBy } from "@zag-js/types"
|
|
3
|
+
|
|
4
|
+
export type PanelId = string | number
|
|
5
|
+
|
|
6
|
+
type PanelSizeData = {
|
|
7
|
+
id: PanelId
|
|
8
|
+
size?: number
|
|
9
|
+
minSize?: number
|
|
10
|
+
maxSize?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type ResizeDetails = {
|
|
14
|
+
size: PanelSizeData[]
|
|
15
|
+
activeHandleId: string | null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type ElementIds = Partial<{
|
|
19
|
+
root: string
|
|
20
|
+
resizeTrigger(id: string): string
|
|
21
|
+
toggleTrigger(id: string): string
|
|
22
|
+
label(id: string): string
|
|
23
|
+
panel(id: string | number): string
|
|
24
|
+
}>
|
|
25
|
+
|
|
26
|
+
type PublicContext = DirectionProperty &
|
|
27
|
+
CommonProperties & {
|
|
28
|
+
/**
|
|
29
|
+
* The orientation of the splitter. Can be `horizontal` or `vertical`
|
|
30
|
+
*/
|
|
31
|
+
orientation: "horizontal" | "vertical"
|
|
32
|
+
/**
|
|
33
|
+
* The size data of the panels
|
|
34
|
+
*/
|
|
35
|
+
size: PanelSizeData[]
|
|
36
|
+
/**
|
|
37
|
+
* Function called when the splitter is resized.
|
|
38
|
+
*/
|
|
39
|
+
onResize?: (details: ResizeDetails) => void
|
|
40
|
+
/**
|
|
41
|
+
* Function called when the splitter resize starts.
|
|
42
|
+
*/
|
|
43
|
+
onResizeStart?: (details: ResizeDetails) => void
|
|
44
|
+
/**
|
|
45
|
+
* Function called when the splitter resize ends.
|
|
46
|
+
*/
|
|
47
|
+
onResizeEnd?: (details: ResizeDetails) => void
|
|
48
|
+
/**
|
|
49
|
+
* The ids of the elements in the splitter. Useful for composition.
|
|
50
|
+
*/
|
|
51
|
+
ids?: ElementIds
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type UserDefinedContext = RequiredBy<PublicContext, "id">
|
|
55
|
+
|
|
56
|
+
export type NormalizedPanelData = Array<
|
|
57
|
+
Required<PanelSizeData> & {
|
|
58
|
+
remainingSize: number
|
|
59
|
+
minSize: number
|
|
60
|
+
maxSize: number
|
|
61
|
+
start: number
|
|
62
|
+
end: number
|
|
63
|
+
}
|
|
64
|
+
>
|
|
65
|
+
|
|
66
|
+
type ComputedContext = Readonly<{
|
|
67
|
+
isHorizontal: boolean
|
|
68
|
+
panels: NormalizedPanelData
|
|
69
|
+
activeResizeBounds?: { min: number; max: number }
|
|
70
|
+
activeResizePanels?: { before: PanelSizeData; after: PanelSizeData }
|
|
71
|
+
}>
|
|
72
|
+
|
|
73
|
+
type PrivateContext = Context<{
|
|
74
|
+
activeResizeId: string | null
|
|
75
|
+
previousPanels: NormalizedPanelData
|
|
76
|
+
activeResizeState: { isAtMin: boolean; isAtMax: boolean }
|
|
77
|
+
initialSize: Array<Required<Pick<PanelSizeData, "id" | "size">>>
|
|
78
|
+
}>
|
|
79
|
+
|
|
80
|
+
export type MachineContext = PublicContext & ComputedContext & PrivateContext
|
|
81
|
+
|
|
82
|
+
export type MachineState = {
|
|
83
|
+
value: "idle" | "hover:temp" | "hover" | "dragging" | "focused"
|
|
84
|
+
tags: "focus"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type State = S.State<MachineContext, MachineState>
|
|
88
|
+
|
|
89
|
+
export type Send = S.Send<S.AnyEventObject>
|
|
90
|
+
|
|
91
|
+
export type PanelProps = {
|
|
92
|
+
id: PanelId
|
|
93
|
+
snapSize?: number
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type ResizeTriggerProps = {
|
|
97
|
+
id: `${PanelId}:${PanelId}`
|
|
98
|
+
step?: number
|
|
99
|
+
disabled?: boolean
|
|
100
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { MachineContext as Ctx, NormalizedPanelData } from "./splitter.types"
|
|
2
|
+
|
|
3
|
+
function validateSize(key: string, size: number) {
|
|
4
|
+
if (Math.floor(size) > 100) {
|
|
5
|
+
throw new Error(`Total ${key} of panels cannot be greater than 100`)
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getNormalizedPanels(ctx: Ctx): NormalizedPanelData {
|
|
10
|
+
let numOfPanelsWithoutSize = 0
|
|
11
|
+
let totalSize = 0
|
|
12
|
+
let totalMinSize = 0
|
|
13
|
+
|
|
14
|
+
const panels = ctx.size.map((panel) => {
|
|
15
|
+
const minSize = panel.minSize ?? 10
|
|
16
|
+
const maxSize = panel.maxSize ?? 100
|
|
17
|
+
|
|
18
|
+
totalMinSize += minSize
|
|
19
|
+
|
|
20
|
+
if (panel.size == null) {
|
|
21
|
+
numOfPanelsWithoutSize++
|
|
22
|
+
} else {
|
|
23
|
+
totalSize += panel.size
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
...panel,
|
|
28
|
+
minSize,
|
|
29
|
+
maxSize,
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
validateSize("minSize", totalMinSize)
|
|
34
|
+
validateSize("size", totalSize)
|
|
35
|
+
|
|
36
|
+
let end = 0
|
|
37
|
+
let remainingSize = 0
|
|
38
|
+
|
|
39
|
+
const result = panels.map((panel) => {
|
|
40
|
+
let start = end
|
|
41
|
+
|
|
42
|
+
if (panel.size != null) {
|
|
43
|
+
end += panel.size
|
|
44
|
+
remainingSize = panel.size - panel.minSize
|
|
45
|
+
return {
|
|
46
|
+
...panel,
|
|
47
|
+
start,
|
|
48
|
+
end,
|
|
49
|
+
remainingSize,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const size = (100 - totalSize) / numOfPanelsWithoutSize
|
|
54
|
+
end += size
|
|
55
|
+
remainingSize = size - panel.minSize
|
|
56
|
+
|
|
57
|
+
return { ...panel, size, start, end, remainingSize }
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
return result as NormalizedPanelData
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getHandlePanels(ctx: Ctx, id = ctx.activeResizeId) {
|
|
64
|
+
const [beforeId, afterId] = id?.split(":") ?? []
|
|
65
|
+
if (!beforeId || !afterId) return
|
|
66
|
+
|
|
67
|
+
const beforeIndex = ctx.previousPanels.findIndex((panel) => panel.id === beforeId)
|
|
68
|
+
const afterIndex = ctx.previousPanels.findIndex((panel) => panel.id === afterId)
|
|
69
|
+
if (beforeIndex === -1 || afterIndex === -1) return
|
|
70
|
+
|
|
71
|
+
const before = ctx.previousPanels[beforeIndex]
|
|
72
|
+
const after = ctx.previousPanels[afterIndex]
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
before: {
|
|
76
|
+
...before,
|
|
77
|
+
index: beforeIndex,
|
|
78
|
+
},
|
|
79
|
+
after: {
|
|
80
|
+
...after,
|
|
81
|
+
index: afterIndex,
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getHandleBounds(ctx: Ctx, id = ctx.activeResizeId) {
|
|
87
|
+
const panels = getHandlePanels(ctx, id)
|
|
88
|
+
if (!panels) return
|
|
89
|
+
|
|
90
|
+
const { before, after } = panels
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
min: Math.max(before.start + before.minSize, after.end - after.maxSize),
|
|
94
|
+
max: Math.min(after.end - after.minSize, before.maxSize + before.start),
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function getPanelBounds(ctx: Ctx, id?: string | null) {
|
|
99
|
+
const bounds = getHandleBounds(ctx, id)
|
|
100
|
+
const panels = getHandlePanels(ctx, id)
|
|
101
|
+
|
|
102
|
+
if (!bounds || !panels) return
|
|
103
|
+
const { before, after } = panels
|
|
104
|
+
|
|
105
|
+
const beforeMin = Math.abs(before.start - bounds.min)
|
|
106
|
+
const afterMin = after.size + (before.size - beforeMin)
|
|
107
|
+
|
|
108
|
+
const beforeMax = Math.abs(before.start - bounds.max)
|
|
109
|
+
const afterMax = after.size - (beforeMax - before.size)
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
before: {
|
|
113
|
+
index: before.index,
|
|
114
|
+
min: beforeMin,
|
|
115
|
+
max: beforeMax,
|
|
116
|
+
isAtMin: beforeMin === before.size,
|
|
117
|
+
isAtMax: beforeMax === before.size,
|
|
118
|
+
up(step: number) {
|
|
119
|
+
return Math.min(before.size + step, beforeMax)
|
|
120
|
+
},
|
|
121
|
+
down(step: number) {
|
|
122
|
+
return Math.max(before.size - step, beforeMin)
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
after: {
|
|
126
|
+
index: after.index,
|
|
127
|
+
min: afterMin,
|
|
128
|
+
max: afterMax,
|
|
129
|
+
isAtMin: afterMin === after.size,
|
|
130
|
+
isAtMax: afterMax === after.size,
|
|
131
|
+
up(step: number) {
|
|
132
|
+
return Math.min(after.size + step, afterMin)
|
|
133
|
+
},
|
|
134
|
+
down(step: number) {
|
|
135
|
+
return Math.max(after.size - step, afterMax)
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function clamp(value: number, min: number, max: number) {
|
|
142
|
+
return Math.min(Math.max(value, min), max)
|
|
143
|
+
}
|