@dashfoo/core 0.5.2 → 1.1.0
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/README.md +19 -12
- package/dist/index.d.ts +317 -113
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +373 -267
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { assign, emit, setup } from "xstate";
|
|
3
3
|
|
|
4
|
+
//#region src/lib/clamp-selected.ts
|
|
5
|
+
/** In `lib/` because both the parse boundary and `normalize` need it, and
|
|
6
|
+
* importing it from either would close a module cycle. */
|
|
7
|
+
const clampSelected = (length, selected) => {
|
|
8
|
+
if (length === 0) return 0;
|
|
9
|
+
const base = Number.isFinite(selected) ? Math.trunc(selected) : 0;
|
|
10
|
+
if (base < 0) return 0;
|
|
11
|
+
if (base > length - 1) return length - 1;
|
|
12
|
+
return base;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
4
16
|
//#region src/model/schema.ts
|
|
17
|
+
/** Weights are relative shares of a row, not percentages. */
|
|
18
|
+
const DEFAULT_WEIGHT = 1;
|
|
5
19
|
const jsonValueSchema = z.lazy(() => z.union([
|
|
6
20
|
z.string(),
|
|
7
21
|
z.number(),
|
|
@@ -44,7 +58,11 @@ const tabNodeSchema = z.object({
|
|
|
44
58
|
name: z.string(),
|
|
45
59
|
type: z.literal("tab")
|
|
46
60
|
});
|
|
47
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Kept pre-transform so `node-attrs` can `.pick()` from it: a pipe has no
|
|
63
|
+
* `.pick`, and `.partial()` over a defaulted field still fills the default in.
|
|
64
|
+
*/
|
|
65
|
+
const tabsetNodeObjectSchema = z.object({
|
|
48
66
|
children: z.array(tabNodeSchema),
|
|
49
67
|
enableClose: z.boolean().optional(),
|
|
50
68
|
enableMaximize: z.boolean().optional(),
|
|
@@ -54,18 +72,25 @@ const tabsetNodeSchema = z.object({
|
|
|
54
72
|
name: z.string().optional(),
|
|
55
73
|
selected: z.number().int(),
|
|
56
74
|
type: z.literal("tabset"),
|
|
57
|
-
weight: z.number().
|
|
75
|
+
weight: z.number().default(1)
|
|
58
76
|
});
|
|
59
|
-
const
|
|
60
|
-
|
|
77
|
+
const tabsetNodeSchema = tabsetNodeObjectSchema.transform((tabset) => ({
|
|
78
|
+
...tabset,
|
|
79
|
+
selected: clampSelected(tabset.children.length, tabset.selected)
|
|
80
|
+
}));
|
|
81
|
+
/** The getter's explicit return type is required: without it, TS7023. */
|
|
82
|
+
const rowNodeSchema = z.object({
|
|
83
|
+
get children() {
|
|
84
|
+
return z.array(z.union([rowNodeSchema, tabsetNodeSchema]));
|
|
85
|
+
},
|
|
61
86
|
id: z.string(),
|
|
62
87
|
max: dimensionSchema.optional(),
|
|
63
88
|
min: dimensionSchema.optional(),
|
|
64
89
|
orientation: orientationSchema,
|
|
65
90
|
snap: snapSchema.optional(),
|
|
66
91
|
type: z.literal("row"),
|
|
67
|
-
weight: z.number().
|
|
68
|
-
})
|
|
92
|
+
weight: z.number().default(1)
|
|
93
|
+
});
|
|
69
94
|
const geometrySchema = z.object({
|
|
70
95
|
height: z.number(),
|
|
71
96
|
left: z.number(),
|
|
@@ -95,7 +120,7 @@ const globalAttributesSchema = z.object({
|
|
|
95
120
|
});
|
|
96
121
|
const dashfooSchema = z.object({
|
|
97
122
|
activeTabsetId: z.string().optional(),
|
|
98
|
-
floats: z.array(floatNodeSchema).
|
|
123
|
+
floats: z.array(floatNodeSchema).default(() => []),
|
|
99
124
|
global: globalAttributesSchema,
|
|
100
125
|
layout: rowNodeSchema,
|
|
101
126
|
maximizedTabsetId: z.string().optional(),
|
|
@@ -103,7 +128,7 @@ const dashfooSchema = z.object({
|
|
|
103
128
|
});
|
|
104
129
|
|
|
105
130
|
//#endregion
|
|
106
|
-
//#region src/state/
|
|
131
|
+
//#region src/state/node-attrs.ts
|
|
107
132
|
const mutableTabAttrsSchema = tabNodeSchema.pick({
|
|
108
133
|
config: true,
|
|
109
134
|
enableClose: true,
|
|
@@ -111,14 +136,14 @@ const mutableTabAttrsSchema = tabNodeSchema.pick({
|
|
|
111
136
|
enableRename: true,
|
|
112
137
|
name: true
|
|
113
138
|
}).partial();
|
|
114
|
-
|
|
139
|
+
/** `weight` is re-declared, not picked: `.partial()` over a defaulted field still injects the default. */
|
|
140
|
+
const mutableTabsetAttrsSchema = tabsetNodeObjectSchema.pick({
|
|
115
141
|
enableClose: true,
|
|
116
142
|
enableMaximize: true,
|
|
117
143
|
max: true,
|
|
118
144
|
min: true,
|
|
119
|
-
selected: true
|
|
120
|
-
|
|
121
|
-
}).partial();
|
|
145
|
+
selected: true
|
|
146
|
+
}).extend({ weight: z.number() }).partial();
|
|
122
147
|
const mutableRowAttrsSchema = z.object({
|
|
123
148
|
max: dimensionSchema.optional(),
|
|
124
149
|
min: dimensionSchema.optional(),
|
|
@@ -126,11 +151,14 @@ const mutableRowAttrsSchema = z.object({
|
|
|
126
151
|
snap: snapSchema.optional(),
|
|
127
152
|
weight: z.number()
|
|
128
153
|
}).partial();
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Merged, not a union: a union of all-optional members matches its first member
|
|
156
|
+
* and strips the rest. The reducer enforces which keys a given node accepts.
|
|
157
|
+
*/
|
|
158
|
+
const mutableNodeAttrsSchema = mutableTabAttrsSchema.extend(mutableTabsetAttrsSchema.shape).extend(mutableRowAttrsSchema.shape);
|
|
159
|
+
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region src/state/actions.ts
|
|
134
162
|
const dockLocationSchema = z.enum([
|
|
135
163
|
"center",
|
|
136
164
|
"split-bottom",
|
|
@@ -155,7 +183,6 @@ const actionSchema = z.discriminatedUnion("type", [
|
|
|
155
183
|
type: z.literal("moveNode")
|
|
156
184
|
}),
|
|
157
185
|
z.object({
|
|
158
|
-
index: z.number().int().optional(),
|
|
159
186
|
location: dockLocationSchema,
|
|
160
187
|
sourceId: z.string(),
|
|
161
188
|
targetId: z.string(),
|
|
@@ -243,19 +270,12 @@ const createTabId = () => createNodeId("tab");
|
|
|
243
270
|
|
|
244
271
|
//#endregion
|
|
245
272
|
//#region src/model/tree.ts
|
|
246
|
-
const collectRoots = (model) => [model.layout, ...
|
|
247
|
-
const findFloat = (model, floatId) =>
|
|
273
|
+
const collectRoots = (model) => [model.layout, ...model.floats.map((float) => float.layout)];
|
|
274
|
+
const findFloat = (model, floatId) => model.floats.find((float) => float.id === floatId);
|
|
248
275
|
/**
|
|
249
|
-
* Depth-first pre-order
|
|
250
|
-
* order
|
|
251
|
-
*
|
|
252
|
-
* traversal serves both find-first and collect-all.
|
|
253
|
-
*
|
|
254
|
-
* Every query below used to carry its own copy of this recursion, and they had
|
|
255
|
-
* drifted in child order: some scanned a row's own tabsets before descending,
|
|
256
|
-
* others interleaved. With unique ids all orders agree, so they now share this
|
|
257
|
-
* one; a model with duplicate ids (which the builder and the parser both warn
|
|
258
|
-
* about) can see a different duplicate win than it did before.
|
|
276
|
+
* Depth-first pre-order; a defined `visit` result stops the walk. With unique
|
|
277
|
+
* ids the order is immaterial, but a duplicate-id model (which the builder and
|
|
278
|
+
* the parser both warn about) resolves to whichever copy this reaches first.
|
|
259
279
|
*/
|
|
260
280
|
const walk = (row, visit, parent, index = -1) => {
|
|
261
281
|
const atRoot = visit({
|
|
@@ -286,7 +306,20 @@ const walk = (row, visit, parent, index = -1) => {
|
|
|
286
306
|
}
|
|
287
307
|
}
|
|
288
308
|
};
|
|
289
|
-
const
|
|
309
|
+
const isRow = (node) => node.type === "row";
|
|
310
|
+
const isTab = (node) => node.type === "tab";
|
|
311
|
+
const isTabset = (node) => node.type === "tabset";
|
|
312
|
+
/** Predicate-first, so an id shared across node kinds resolves the same way here as anywhere else. */
|
|
313
|
+
const locate = (model, id, is) => {
|
|
314
|
+
for (const root of collectRoots(model)) {
|
|
315
|
+
const found = walk(root, ({ index, node, parent }) => node.id === id && is(node) ? {
|
|
316
|
+
index,
|
|
317
|
+
node,
|
|
318
|
+
parent
|
|
319
|
+
} : void 0);
|
|
320
|
+
if (found) return found;
|
|
321
|
+
}
|
|
322
|
+
};
|
|
290
323
|
const collectTabsetsInRow = (row, acc) => {
|
|
291
324
|
walk(row, ({ node }) => {
|
|
292
325
|
if (node.type === "tabset") acc.push(node);
|
|
@@ -298,41 +331,28 @@ const collectTabsets = (model) => {
|
|
|
298
331
|
return acc;
|
|
299
332
|
};
|
|
300
333
|
const getFirstTabset = (model) => collectTabsets(model)[0];
|
|
301
|
-
const findTabset = (model, tabsetId) =>
|
|
302
|
-
const findTabInContainer = (container, tabId) => {
|
|
303
|
-
const index = container.children.findIndex((tab) => tab.id === tabId);
|
|
304
|
-
if (index === -1) return;
|
|
305
|
-
const tab = container.children.at(index);
|
|
306
|
-
if (!tab) return;
|
|
307
|
-
return {
|
|
308
|
-
container,
|
|
309
|
-
index,
|
|
310
|
-
tab
|
|
311
|
-
};
|
|
312
|
-
};
|
|
334
|
+
const findTabset = (model, tabsetId) => locate(model, tabsetId, isTabset)?.node;
|
|
313
335
|
const findTab = (model, tabId) => {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const found = locate(row, id)?.node;
|
|
321
|
-
return found?.type === "row" ? found : void 0;
|
|
336
|
+
const found = locate(model, tabId, isTab);
|
|
337
|
+
return found?.parent?.type === "tabset" ? {
|
|
338
|
+
container: found.parent,
|
|
339
|
+
index: found.index,
|
|
340
|
+
tab: found.node
|
|
341
|
+
} : void 0;
|
|
322
342
|
};
|
|
323
|
-
const
|
|
343
|
+
const findRow = (model, id) => locate(model, id, isRow)?.node;
|
|
324
344
|
const findAttributedNode = (model, id) => {
|
|
325
345
|
for (const root of collectRoots(model)) {
|
|
326
|
-
const found =
|
|
346
|
+
const found = walk(root, ({ node }) => node.id === id ? node : void 0);
|
|
327
347
|
if (found) return found;
|
|
328
348
|
}
|
|
329
349
|
};
|
|
330
350
|
const findRootContaining = (model, nodeId) => {
|
|
331
|
-
for (const root of collectRoots(model)) if (
|
|
351
|
+
for (const root of collectRoots(model)) if (walk(root, ({ node }) => node.id === nodeId ? true : void 0)) return root;
|
|
332
352
|
};
|
|
333
|
-
const findTabsetParent = (
|
|
334
|
-
const found = locate(
|
|
335
|
-
return found?.
|
|
353
|
+
const findTabsetParent = (model, tabsetId) => {
|
|
354
|
+
const found = locate(model, tabsetId, isTabset);
|
|
355
|
+
return found?.parent?.type === "row" ? {
|
|
336
356
|
index: found.index,
|
|
337
357
|
parent: found.parent
|
|
338
358
|
} : void 0;
|
|
@@ -344,7 +364,7 @@ const collectIdsInRow = (row, acc) => {
|
|
|
344
364
|
};
|
|
345
365
|
const findDuplicateIds = (model) => {
|
|
346
366
|
const ids = [];
|
|
347
|
-
for (const float of model.floats
|
|
367
|
+
for (const float of model.floats) ids.push(float.id);
|
|
348
368
|
for (const root of collectRoots(model)) collectIdsInRow(root, ids);
|
|
349
369
|
const seen = /* @__PURE__ */ new Set();
|
|
350
370
|
const duplicates = /* @__PURE__ */ new Set();
|
|
@@ -377,7 +397,8 @@ const tabset = (children, options = {}) => {
|
|
|
377
397
|
...max === void 0 ? {} : { max: toDimension(max) },
|
|
378
398
|
...min === void 0 ? {} : { min: toDimension(min) },
|
|
379
399
|
id: options.id ?? createNodeId("tabset"),
|
|
380
|
-
selected: options.selected ?? 0
|
|
400
|
+
selected: clampSelected(children.length, options.selected ?? 0),
|
|
401
|
+
weight: options.weight ?? 1
|
|
381
402
|
};
|
|
382
403
|
};
|
|
383
404
|
const row = (children, options = {}) => {
|
|
@@ -389,7 +410,8 @@ const row = (children, options = {}) => {
|
|
|
389
410
|
...max === void 0 ? {} : { max: toDimension(max) },
|
|
390
411
|
...min === void 0 ? {} : { min: toDimension(min) },
|
|
391
412
|
id: options.id ?? createNodeId("row"),
|
|
392
|
-
orientation: options.orientation ?? "row"
|
|
413
|
+
orientation: options.orientation ?? "row",
|
|
414
|
+
weight: options.weight ?? 1
|
|
393
415
|
};
|
|
394
416
|
};
|
|
395
417
|
const floatNode = (layout, geometry, options = {}) => ({
|
|
@@ -404,6 +426,7 @@ const model = (layout, options = {}) => {
|
|
|
404
426
|
layout,
|
|
405
427
|
version: 1,
|
|
406
428
|
...options,
|
|
429
|
+
floats: options.floats ?? [],
|
|
407
430
|
global: options.global ?? {}
|
|
408
431
|
};
|
|
409
432
|
const duplicates = findDuplicateIds(built);
|
|
@@ -411,6 +434,12 @@ const model = (layout, options = {}) => {
|
|
|
411
434
|
return built;
|
|
412
435
|
};
|
|
413
436
|
|
|
437
|
+
//#endregion
|
|
438
|
+
//#region src/lib/assert-never.ts
|
|
439
|
+
const assertNever = (value) => {
|
|
440
|
+
throw new Error(`Unhandled variant: ${JSON.stringify(value)}`);
|
|
441
|
+
};
|
|
442
|
+
|
|
414
443
|
//#endregion
|
|
415
444
|
//#region src/geometry/geometry.ts
|
|
416
445
|
const DEFAULT_BAND_FRACTION = .22;
|
|
@@ -431,15 +460,22 @@ const closestEdge = (d) => {
|
|
|
431
460
|
if (min === d.top) return "top";
|
|
432
461
|
return "bottom";
|
|
433
462
|
};
|
|
463
|
+
const splitEdge = (location) => {
|
|
464
|
+
switch (location) {
|
|
465
|
+
case "center": return;
|
|
466
|
+
case "split-bottom": return "bottom";
|
|
467
|
+
case "split-left": return "left";
|
|
468
|
+
case "split-right": return "right";
|
|
469
|
+
case "split-top": return "top";
|
|
470
|
+
default: return assertNever(location);
|
|
471
|
+
}
|
|
472
|
+
};
|
|
434
473
|
const resolveDockTarget = (pointer, rect, opts) => {
|
|
435
474
|
const band = opts?.bandFraction ?? DEFAULT_BAND_FRACTION;
|
|
436
|
-
if (rect.width <= 0 || rect.height <= 0) return
|
|
475
|
+
if (rect.width <= 0 || rect.height <= 0) return "center";
|
|
437
476
|
const distances = edgeDistances(pointer, rect);
|
|
438
|
-
if (Math.min(distances.left, distances.right, distances.top, distances.bottom) > band) return
|
|
439
|
-
return {
|
|
440
|
-
edge: closestEdge(distances),
|
|
441
|
-
kind: "split"
|
|
442
|
-
};
|
|
477
|
+
if (Math.min(distances.left, distances.right, distances.top, distances.bottom) > band) return "center";
|
|
478
|
+
return `split-${closestEdge(distances)}`;
|
|
443
479
|
};
|
|
444
480
|
const dockZonePolygons = (rect, opts) => {
|
|
445
481
|
const { height: h, width: w, x, y } = rect;
|
|
@@ -564,7 +600,7 @@ const zoneRect = (rect, location) => {
|
|
|
564
600
|
y: rect.y
|
|
565
601
|
};
|
|
566
602
|
case "center": return rect;
|
|
567
|
-
default: return
|
|
603
|
+
default: return assertNever(location);
|
|
568
604
|
}
|
|
569
605
|
};
|
|
570
606
|
|
|
@@ -588,21 +624,20 @@ const resolveSnapTargets = (config, panelCount) => {
|
|
|
588
624
|
}
|
|
589
625
|
return [...targets].toSorted((a, b) => a - b);
|
|
590
626
|
};
|
|
591
|
-
const
|
|
627
|
+
const resolveSnapGrid = (config, panelCount) => ({
|
|
628
|
+
targets: config === null ? [] : resolveSnapTargets(config, panelCount),
|
|
629
|
+
threshold: config?.threshold ?? 4
|
|
630
|
+
});
|
|
631
|
+
const snapEnabled = (grid) => grid.targets.length > 0;
|
|
592
632
|
const nearestTarget = (position, targets) => targets.reduce((best, target) => {
|
|
593
633
|
if (best === void 0 || Math.abs(target - position) < Math.abs(best - position)) return target;
|
|
594
634
|
return best;
|
|
595
635
|
}, void 0);
|
|
596
|
-
const snapSizes = (sizes, boundaryIndex,
|
|
597
|
-
const
|
|
636
|
+
const snapSizes = (sizes, boundaryIndex, grid) => {
|
|
637
|
+
const { targets, threshold } = grid;
|
|
598
638
|
const left = sizes[boundaryIndex];
|
|
599
639
|
const right = sizes[boundaryIndex + 1];
|
|
600
|
-
if (boundaryIndex < 0 || left === void 0 || right === void 0) return {
|
|
601
|
-
sizes,
|
|
602
|
-
snapped: false
|
|
603
|
-
};
|
|
604
|
-
const targets = resolveSnapTargets(config, sizes.length);
|
|
605
|
-
if (targets.length === 0) return {
|
|
640
|
+
if (boundaryIndex < 0 || left === void 0 || right === void 0 || targets.length === 0) return {
|
|
606
641
|
sizes,
|
|
607
642
|
snapped: false
|
|
608
643
|
};
|
|
@@ -626,32 +661,30 @@ const snapSizes = (sizes, boundaryIndex, config) => {
|
|
|
626
661
|
snapped: true
|
|
627
662
|
};
|
|
628
663
|
};
|
|
664
|
+
const decideSnap = (sizes, boundaryIndex, grid) => {
|
|
665
|
+
if (boundaryIndex === null || !snapEnabled(grid)) return { kind: "inactive" };
|
|
666
|
+
const { sizes: snappedSizes, snapped } = snapSizes(sizes, boundaryIndex, grid);
|
|
667
|
+
return snapped ? {
|
|
668
|
+
kind: "engage",
|
|
669
|
+
sizes: snappedSizes
|
|
670
|
+
} : { kind: "clear" };
|
|
671
|
+
};
|
|
672
|
+
const settleSnap = (sizes, boundaryIndex, grid) => {
|
|
673
|
+
const decision = decideSnap(sizes, boundaryIndex, grid);
|
|
674
|
+
return decision.kind === "engage" ? decision.sizes : sizes;
|
|
675
|
+
};
|
|
629
676
|
|
|
630
677
|
//#endregion
|
|
631
678
|
//#region src/model/invariants.ts
|
|
632
|
-
const clampSelected = (length, selected) => {
|
|
633
|
-
if (length === 0) return 0;
|
|
634
|
-
const base = Number.isFinite(selected) ? Math.trunc(selected) : 0;
|
|
635
|
-
if (base < 0) return 0;
|
|
636
|
-
if (base > length - 1) return length - 1;
|
|
637
|
-
return base;
|
|
638
|
-
};
|
|
639
|
-
const normalizeTabset = (tabset) => ({
|
|
640
|
-
...tabset,
|
|
641
|
-
selected: clampSelected(tabset.children.length, tabset.selected)
|
|
642
|
-
});
|
|
643
679
|
/**
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
* load and on every persist, so the loss is permanent.
|
|
648
|
-
*
|
|
649
|
-
* `snap` only exists on rows, so it is carried only to a row survivor.
|
|
680
|
+
* The survivor takes over the collapsed row's slot, so it has to take the slot's
|
|
681
|
+
* sizing too: dropping it widens a pinned sidebar, permanently, since this runs
|
|
682
|
+
* on every persist. `snap` is row-only, so it travels only to a row survivor.
|
|
650
683
|
*/
|
|
651
684
|
const inheritSlot = (row, survivor) => {
|
|
652
685
|
const sized = {
|
|
653
686
|
...survivor,
|
|
654
|
-
|
|
687
|
+
weight: row.weight,
|
|
655
688
|
...row.min === void 0 ? {} : { min: row.min },
|
|
656
689
|
...row.max === void 0 ? {} : { max: row.max }
|
|
657
690
|
};
|
|
@@ -661,14 +694,18 @@ const inheritSlot = (row, survivor) => {
|
|
|
661
694
|
};
|
|
662
695
|
return sized;
|
|
663
696
|
};
|
|
664
|
-
|
|
697
|
+
/** Records the id of every tabset it keeps, in document order. */
|
|
698
|
+
const normalizeRowChildren = (children, keptTabsetIds) => {
|
|
665
699
|
const out = [];
|
|
666
700
|
for (const child of children) {
|
|
667
701
|
if (child.type === "tabset") {
|
|
668
|
-
if (child.children.length > 0)
|
|
702
|
+
if (child.children.length > 0) {
|
|
703
|
+
out.push(child);
|
|
704
|
+
keptTabsetIds.push(child.id);
|
|
705
|
+
}
|
|
669
706
|
continue;
|
|
670
707
|
}
|
|
671
|
-
const grandchildren = normalizeRowChildren(child.children);
|
|
708
|
+
const grandchildren = normalizeRowChildren(child.children, keptTabsetIds);
|
|
672
709
|
if (grandchildren.length === 0) continue;
|
|
673
710
|
if (grandchildren.length === 1) {
|
|
674
711
|
const only = grandchildren[0];
|
|
@@ -682,9 +719,8 @@ const normalizeRowChildren = (children) => {
|
|
|
682
719
|
}
|
|
683
720
|
return out;
|
|
684
721
|
};
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
let children = normalizeRowChildren(root.children);
|
|
722
|
+
const normalizeLayout = (root, keptTabsetIds) => {
|
|
723
|
+
let children = normalizeRowChildren(root.children, keptTabsetIds);
|
|
688
724
|
let orientation = root.orientation;
|
|
689
725
|
let inner = children[0];
|
|
690
726
|
while (children.length === 1 && inner?.type === "row") {
|
|
@@ -699,87 +735,111 @@ const normalizeLayout = (root) => {
|
|
|
699
735
|
};
|
|
700
736
|
};
|
|
701
737
|
const normalize = (model) => {
|
|
702
|
-
const
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
738
|
+
const mainTabsetIds = [];
|
|
739
|
+
const layout = normalizeLayout(model.layout, mainTabsetIds);
|
|
740
|
+
const floats = [];
|
|
741
|
+
const floatTabsetIds = [];
|
|
742
|
+
for (const float of model.floats) {
|
|
743
|
+
const ownTabsetIds = [];
|
|
744
|
+
const floatLayout = normalizeLayout(float.layout, ownTabsetIds);
|
|
745
|
+
if (floatLayout.children.length > 0) {
|
|
746
|
+
floats.push({
|
|
747
|
+
...float,
|
|
748
|
+
layout: floatLayout
|
|
749
|
+
});
|
|
750
|
+
floatTabsetIds.push(...ownTabsetIds);
|
|
751
|
+
}
|
|
710
752
|
}
|
|
711
|
-
const
|
|
712
|
-
const
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
};
|
|
717
|
-
const tabsetIds = new Set(collectTabsets(withRoots).map((tabset) => tabset.id));
|
|
718
|
-
const firstTabsetId = collectTabsets(withRoots)[0]?.id;
|
|
719
|
-
const activeTabsetId = model.activeTabsetId !== void 0 && tabsetIds.has(model.activeTabsetId) ? model.activeTabsetId : firstTabsetId;
|
|
720
|
-
const maximizedTabsetId = model.maximizedTabsetId !== void 0 && tabsetIds.has(model.maximizedTabsetId) ? model.maximizedTabsetId : void 0;
|
|
753
|
+
const mainIds = new Set(mainTabsetIds);
|
|
754
|
+
const liveIds = /* @__PURE__ */ new Set([...mainTabsetIds, ...floatTabsetIds]);
|
|
755
|
+
const firstTabsetId = mainTabsetIds[0] ?? floatTabsetIds[0];
|
|
756
|
+
const activeTabsetId = model.activeTabsetId !== void 0 && liveIds.has(model.activeTabsetId) ? model.activeTabsetId : firstTabsetId;
|
|
757
|
+
const maximizedTabsetId = model.maximizedTabsetId !== void 0 && mainIds.has(model.maximizedTabsetId) ? model.maximizedTabsetId : void 0;
|
|
721
758
|
return {
|
|
722
|
-
...
|
|
759
|
+
...model,
|
|
723
760
|
activeTabsetId,
|
|
761
|
+
floats,
|
|
762
|
+
layout,
|
|
724
763
|
maximizedTabsetId
|
|
725
764
|
};
|
|
726
765
|
};
|
|
727
766
|
|
|
728
767
|
//#endregion
|
|
729
768
|
//#region src/state/surgery.ts
|
|
730
|
-
const removeTabsetReturning = (
|
|
731
|
-
const found = findTabsetParent(
|
|
769
|
+
const removeTabsetReturning = (model, tabsetId) => {
|
|
770
|
+
const found = findTabsetParent(model, tabsetId);
|
|
732
771
|
if (!found) return;
|
|
733
772
|
const [removed] = found.parent.children.splice(found.index, 1);
|
|
734
773
|
return removed?.type === "tabset" ? removed : void 0;
|
|
735
774
|
};
|
|
736
|
-
|
|
737
|
-
const
|
|
738
|
-
|
|
775
|
+
/** Total, so a sixth `DockLocation` is a compile error here rather than a wrong default. */
|
|
776
|
+
const SPLIT_PLACEMENT = {
|
|
777
|
+
"split-bottom": {
|
|
778
|
+
before: false,
|
|
779
|
+
orientation: "column"
|
|
780
|
+
},
|
|
781
|
+
"split-left": {
|
|
782
|
+
before: true,
|
|
783
|
+
orientation: "row"
|
|
784
|
+
},
|
|
785
|
+
"split-right": {
|
|
786
|
+
before: false,
|
|
787
|
+
orientation: "row"
|
|
788
|
+
},
|
|
789
|
+
"split-top": {
|
|
790
|
+
before: true,
|
|
791
|
+
orientation: "column"
|
|
792
|
+
}
|
|
793
|
+
};
|
|
739
794
|
const placeBesideTarget = (draft, placed, targetId, location) => {
|
|
740
795
|
const targetTabset = findTabset(draft, targetId);
|
|
741
|
-
const found = findTabsetParent(
|
|
742
|
-
if (!targetTabset || !found) return;
|
|
743
|
-
const orientation =
|
|
744
|
-
const before = splitsBefore(location);
|
|
796
|
+
const found = findTabsetParent(draft, targetId);
|
|
797
|
+
if (!targetTabset || !found) return false;
|
|
798
|
+
const { before, orientation } = SPLIT_PLACEMENT[location];
|
|
745
799
|
if (found.parent.orientation === orientation) {
|
|
746
|
-
const
|
|
747
|
-
targetTabset.weight =
|
|
748
|
-
placed.weight =
|
|
800
|
+
const half = targetTabset.weight / 2;
|
|
801
|
+
targetTabset.weight = half;
|
|
802
|
+
placed.weight = half;
|
|
749
803
|
found.parent.children.splice(before ? found.index : found.index + 1, 0, placed);
|
|
750
804
|
} else {
|
|
751
|
-
const targetWeight = targetTabset.weight;
|
|
752
|
-
targetTabset.weight = 50;
|
|
753
|
-
placed.weight = 50;
|
|
754
805
|
const newRow = {
|
|
755
806
|
children: before ? [placed, targetTabset] : [targetTabset, placed],
|
|
756
807
|
id: createNodeId("row"),
|
|
757
808
|
orientation,
|
|
758
|
-
type: "row"
|
|
809
|
+
type: "row",
|
|
810
|
+
weight: targetTabset.weight
|
|
759
811
|
};
|
|
760
|
-
|
|
812
|
+
targetTabset.weight = 50;
|
|
813
|
+
placed.weight = 50;
|
|
761
814
|
found.parent.children.splice(found.index, 1, newRow);
|
|
762
815
|
}
|
|
763
816
|
draft.activeTabsetId = placed.id;
|
|
817
|
+
return true;
|
|
764
818
|
};
|
|
765
819
|
const insertTab = (draft, tab, target) => {
|
|
766
820
|
const { id: targetId, index, location } = target;
|
|
767
821
|
const targetTabset = findTabset(draft, targetId);
|
|
768
|
-
if (!targetTabset) return;
|
|
822
|
+
if (!targetTabset) return false;
|
|
769
823
|
if (location === "center") {
|
|
770
|
-
const at = index ?? targetTabset.children.length;
|
|
824
|
+
const at = Math.min(Math.max(index ?? targetTabset.children.length, 0), targetTabset.children.length);
|
|
771
825
|
targetTabset.children.splice(at, 0, tab);
|
|
772
826
|
targetTabset.selected = at;
|
|
773
|
-
return;
|
|
827
|
+
return true;
|
|
774
828
|
}
|
|
775
829
|
const newTabset = {
|
|
776
830
|
children: [tab],
|
|
777
831
|
id: createNodeId("tabset"),
|
|
778
832
|
selected: 0,
|
|
779
833
|
type: "tabset",
|
|
780
|
-
weight:
|
|
834
|
+
weight: 1
|
|
781
835
|
};
|
|
782
|
-
placeBesideTarget(draft, newTabset, targetId, location);
|
|
836
|
+
return placeBesideTarget(draft, newTabset, targetId, location);
|
|
837
|
+
};
|
|
838
|
+
/** Re-clamps here rather than leaving the whole tree to be re-walked afterwards. */
|
|
839
|
+
const detachTab = (container, index) => {
|
|
840
|
+
const removed = container.children.splice(index, 1).at(0);
|
|
841
|
+
container.selected = clampSelected(container.children.length, container.selected);
|
|
842
|
+
return removed;
|
|
783
843
|
};
|
|
784
844
|
const mergeTabsInto = (target, tabs, selectedOffset) => {
|
|
785
845
|
const mergeStart = target.children.length;
|
|
@@ -791,17 +851,20 @@ const wrapTabInLayout = (tab) => ({
|
|
|
791
851
|
children: [tab],
|
|
792
852
|
id: createNodeId("tabset"),
|
|
793
853
|
selected: 0,
|
|
794
|
-
type: "tabset"
|
|
854
|
+
type: "tabset",
|
|
855
|
+
weight: 1
|
|
795
856
|
}],
|
|
796
857
|
id: createNodeId("row"),
|
|
797
858
|
orientation: "row",
|
|
798
|
-
type: "row"
|
|
859
|
+
type: "row",
|
|
860
|
+
weight: 1
|
|
799
861
|
});
|
|
800
862
|
const wrapTabsetInLayout = (tabset) => ({
|
|
801
863
|
children: [tabset],
|
|
802
864
|
id: createNodeId("row"),
|
|
803
865
|
orientation: "row",
|
|
804
|
-
type: "row"
|
|
866
|
+
type: "row",
|
|
867
|
+
weight: 1
|
|
805
868
|
});
|
|
806
869
|
|
|
807
870
|
//#endregion
|
|
@@ -824,20 +887,20 @@ const pushFloat = (draft, layout, geometry, floatId) => {
|
|
|
824
887
|
geometry: geometry ?? DEFAULT_FLOAT_GEOMETRY,
|
|
825
888
|
id: floatId ?? createNodeId("float"),
|
|
826
889
|
layout,
|
|
827
|
-
name: uniqueFloatName("Panel", draft.floats
|
|
890
|
+
name: uniqueFloatName("Panel", draft.floats),
|
|
828
891
|
type: "float"
|
|
829
892
|
};
|
|
830
|
-
draft.floats = [...draft.floats
|
|
893
|
+
draft.floats = [...draft.floats, float];
|
|
831
894
|
const tabsets = [];
|
|
832
895
|
collectTabsetsInRow(layout, tabsets);
|
|
833
896
|
const first = tabsets.at(0);
|
|
834
897
|
if (first) draft.activeTabsetId = first.id;
|
|
835
898
|
};
|
|
836
899
|
const floatTabsetById = (draft, tabsetId, geometry, floatId) => {
|
|
837
|
-
const
|
|
838
|
-
if (!
|
|
839
|
-
|
|
840
|
-
|
|
900
|
+
const detached = removeTabsetReturning(draft, tabsetId);
|
|
901
|
+
if (!detached) return false;
|
|
902
|
+
pushFloat(draft, wrapTabsetInLayout(detached), geometry, floatId);
|
|
903
|
+
return true;
|
|
841
904
|
};
|
|
842
905
|
const resolveMainTarget = (draft, targetId) => {
|
|
843
906
|
const mainTabsets = [];
|
|
@@ -853,168 +916,185 @@ const resolveMainTarget = (draft, targetId) => {
|
|
|
853
916
|
return mainTabsets[0];
|
|
854
917
|
};
|
|
855
918
|
const dockFloat = (draft, floatId, targetId, location) => {
|
|
856
|
-
const
|
|
857
|
-
const
|
|
858
|
-
if (index === -1) return;
|
|
859
|
-
const float = floats.splice(index, 1).at(0);
|
|
860
|
-
draft.floats = floats;
|
|
861
|
-
if (!float) return;
|
|
919
|
+
const index = draft.floats.findIndex((float) => float.id === floatId);
|
|
920
|
+
const float = draft.floats.at(index);
|
|
921
|
+
if (index === -1 || !float) return false;
|
|
862
922
|
const tabsets = [];
|
|
863
923
|
collectTabsetsInRow(float.layout, tabsets);
|
|
864
924
|
const tabs = tabsets.flatMap((tabset) => tabset.children);
|
|
865
|
-
if (tabs.length === 0) return;
|
|
925
|
+
if (tabs.length === 0) return false;
|
|
866
926
|
const leadTabset = tabsets.at(0);
|
|
867
927
|
const selectedOffset = leadTabset ? clampSelected(leadTabset.children.length, leadTabset.selected) : 0;
|
|
868
928
|
const target = resolveMainTarget(draft, targetId);
|
|
929
|
+
draft.floats.splice(index, 1);
|
|
869
930
|
if (!target) {
|
|
870
931
|
draft.layout = float.layout;
|
|
871
932
|
draft.activeTabsetId = leadTabset?.id;
|
|
872
|
-
return;
|
|
933
|
+
return true;
|
|
873
934
|
}
|
|
874
935
|
if (location === "center") {
|
|
875
936
|
mergeTabsInto(target, tabs, selectedOffset);
|
|
876
937
|
draft.activeTabsetId = target.id;
|
|
877
|
-
return;
|
|
938
|
+
return true;
|
|
878
939
|
}
|
|
879
|
-
const where = location ?? "split-right";
|
|
880
940
|
const placed = tabsets.length === 1 && leadTabset ? leadTabset : {
|
|
881
941
|
children: tabs,
|
|
882
942
|
id: createNodeId("tabset"),
|
|
883
943
|
selected: selectedOffset,
|
|
884
|
-
type: "tabset"
|
|
944
|
+
type: "tabset",
|
|
945
|
+
weight: 1
|
|
885
946
|
};
|
|
886
|
-
placed.
|
|
887
|
-
placeBesideTarget(draft, placed, target.id, where);
|
|
947
|
+
return placeBesideTarget(draft, placed, target.id, location ?? "split-right");
|
|
888
948
|
};
|
|
889
949
|
|
|
890
950
|
//#endregion
|
|
891
951
|
//#region src/state/reducer.ts
|
|
892
|
-
const
|
|
893
|
-
|
|
952
|
+
const parseNodeAttrs = (node, attrs) => {
|
|
953
|
+
switch (node.type) {
|
|
954
|
+
case "row": return mutableRowAttrsSchema.parse(attrs);
|
|
955
|
+
case "tab": return mutableTabAttrsSchema.parse(attrs);
|
|
956
|
+
case "tabset": {
|
|
957
|
+
const parsed = mutableTabsetAttrsSchema.parse(attrs);
|
|
958
|
+
return parsed.selected === void 0 ? parsed : {
|
|
959
|
+
...parsed,
|
|
960
|
+
selected: clampSelected(node.children.length, parsed.selected)
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
default: return assertNever(node);
|
|
964
|
+
}
|
|
894
965
|
};
|
|
966
|
+
const sameGeometry = (a, b) => a.height === b.height && a.left === b.left && a.top === b.top && a.width === b.width;
|
|
967
|
+
const alreadyApplied = (target, attrs) => {
|
|
968
|
+
const current = new Map(Object.entries(target));
|
|
969
|
+
return Object.entries(attrs).every(([key, value]) => Object.is(current.get(key), value));
|
|
970
|
+
};
|
|
971
|
+
/** Reports whether it changed anything, so `history.dispatch` can skip a no-op. */
|
|
895
972
|
const applyAction = (draft, action) => {
|
|
896
973
|
switch (action.type) {
|
|
897
|
-
case "addNode":
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
});
|
|
903
|
-
return;
|
|
974
|
+
case "addNode": return insertTab(draft, action.tab, {
|
|
975
|
+
id: action.targetId,
|
|
976
|
+
index: action.index,
|
|
977
|
+
location: action.location
|
|
978
|
+
});
|
|
904
979
|
case "adjustSplit": {
|
|
905
|
-
const row = findRow(
|
|
906
|
-
if (!row) return;
|
|
980
|
+
const row = findRow(draft, action.rowId);
|
|
981
|
+
if (!row) return false;
|
|
982
|
+
let changed = false;
|
|
907
983
|
for (let index = 0; index < action.weights.length; index++) {
|
|
908
984
|
const child = row.children.at(index);
|
|
909
985
|
const weight = action.weights[index];
|
|
910
|
-
if (!child || weight === void 0) continue;
|
|
986
|
+
if (!child || weight === void 0 || child.weight === weight) continue;
|
|
911
987
|
child.weight = weight;
|
|
988
|
+
changed = true;
|
|
912
989
|
}
|
|
913
|
-
return;
|
|
990
|
+
return changed;
|
|
914
991
|
}
|
|
915
992
|
case "deleteTab": {
|
|
916
993
|
const location = findTab(draft, action.tabId);
|
|
917
|
-
if (location)
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
case "deleteTabset": {
|
|
921
|
-
const root = findRootContaining(draft, action.tabsetId);
|
|
922
|
-
if (root) removeTabset(root, action.tabsetId);
|
|
923
|
-
return;
|
|
994
|
+
if (!location) return false;
|
|
995
|
+
detachTab(location.container, location.index);
|
|
996
|
+
return true;
|
|
924
997
|
}
|
|
925
|
-
case "
|
|
926
|
-
|
|
927
|
-
return;
|
|
998
|
+
case "deleteTabset": return removeTabsetReturning(draft, action.tabsetId) !== void 0;
|
|
999
|
+
case "dockFloat": return dockFloat(draft, action.floatId, action.targetId, action.location);
|
|
928
1000
|
case "floatTab": {
|
|
929
1001
|
const location = findTab(draft, action.tabId);
|
|
930
|
-
if (!location) return;
|
|
931
|
-
const removed = location.container
|
|
932
|
-
if (removed)
|
|
933
|
-
|
|
1002
|
+
if (!location) return false;
|
|
1003
|
+
const removed = detachTab(location.container, location.index);
|
|
1004
|
+
if (!removed) return false;
|
|
1005
|
+
pushFloat(draft, wrapTabInLayout(removed), action.geometry, action.floatId);
|
|
1006
|
+
return true;
|
|
934
1007
|
}
|
|
935
|
-
case "floatTabset":
|
|
936
|
-
floatTabsetById(draft, action.tabsetId, action.geometry, action.floatId);
|
|
937
|
-
return;
|
|
1008
|
+
case "floatTabset": return floatTabsetById(draft, action.tabsetId, action.geometry, action.floatId);
|
|
938
1009
|
case "moveFloat": {
|
|
939
1010
|
const float = findFloat(draft, action.floatId);
|
|
940
|
-
if (float
|
|
941
|
-
|
|
1011
|
+
if (!float || sameGeometry(float.geometry, action.geometry)) return false;
|
|
1012
|
+
float.geometry = action.geometry;
|
|
1013
|
+
return true;
|
|
942
1014
|
}
|
|
943
1015
|
case "moveNode": {
|
|
944
1016
|
const source = findTab(draft, action.sourceId);
|
|
945
|
-
if (!source) return;
|
|
946
|
-
const removed = source.container
|
|
947
|
-
if (!removed) return;
|
|
948
|
-
insertTab(draft, removed, {
|
|
1017
|
+
if (!source || !findTabset(draft, action.targetId)) return false;
|
|
1018
|
+
const removed = detachTab(source.container, source.index);
|
|
1019
|
+
if (!removed) return false;
|
|
1020
|
+
return insertTab(draft, removed, {
|
|
949
1021
|
id: action.targetId,
|
|
950
1022
|
index: action.index,
|
|
951
1023
|
location: action.location
|
|
952
1024
|
});
|
|
953
|
-
return;
|
|
954
1025
|
}
|
|
955
1026
|
case "moveTabset": {
|
|
956
|
-
if (action.sourceId === action.targetId) return;
|
|
1027
|
+
if (action.sourceId === action.targetId) return false;
|
|
957
1028
|
const source = findTabset(draft, action.sourceId);
|
|
958
|
-
|
|
1029
|
+
const target = findTabset(draft, action.targetId);
|
|
1030
|
+
if (!source || !target) return false;
|
|
959
1031
|
if (action.location === "center") {
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
const sourceRoot = findRootContaining(draft, action.sourceId);
|
|
964
|
-
if (sourceRoot) removeTabset(sourceRoot, action.sourceId);
|
|
965
|
-
}
|
|
966
|
-
return;
|
|
1032
|
+
mergeTabsInto(target, source.children, clampSelected(source.children.length, source.selected));
|
|
1033
|
+
removeTabsetReturning(draft, action.sourceId);
|
|
1034
|
+
return true;
|
|
967
1035
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
const detached = sourceRoot ? removeTabsetReturning(sourceRoot, action.sourceId) : void 0;
|
|
971
|
-
if (detached) placeBesideTarget(draft, detached, action.targetId, action.location);
|
|
972
|
-
}
|
|
973
|
-
return;
|
|
1036
|
+
const detached = removeTabsetReturning(draft, action.sourceId);
|
|
1037
|
+
return detached ? placeBesideTarget(draft, detached, action.targetId, action.location) : false;
|
|
974
1038
|
}
|
|
975
1039
|
case "renameFloat": {
|
|
976
1040
|
const float = findFloat(draft, action.floatId);
|
|
977
|
-
if (float)
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1041
|
+
if (!float) return false;
|
|
1042
|
+
const others = draft.floats.filter((other) => other.id !== action.floatId);
|
|
1043
|
+
const name = uniqueFloatName(action.name, others);
|
|
1044
|
+
if (float.name === name) return false;
|
|
1045
|
+
float.name = name;
|
|
1046
|
+
return true;
|
|
982
1047
|
}
|
|
983
1048
|
case "renameTab": {
|
|
984
1049
|
const location = findTab(draft, action.tabId);
|
|
985
|
-
if (location
|
|
986
|
-
|
|
1050
|
+
if (!location || location.tab.name === action.name) return false;
|
|
1051
|
+
location.tab.name = action.name;
|
|
1052
|
+
return true;
|
|
987
1053
|
}
|
|
988
1054
|
case "selectTab": {
|
|
989
1055
|
const tabset = findTabset(draft, action.tabsetId);
|
|
990
|
-
if (tabset)
|
|
991
|
-
|
|
1056
|
+
if (!tabset) return false;
|
|
1057
|
+
const selected = clampSelected(tabset.children.length, action.index);
|
|
1058
|
+
if (tabset.selected === selected) return false;
|
|
1059
|
+
tabset.selected = selected;
|
|
1060
|
+
return true;
|
|
992
1061
|
}
|
|
993
1062
|
case "setActiveTabset":
|
|
994
|
-
if (findTabset(draft, action.tabsetId))
|
|
995
|
-
|
|
1063
|
+
if (draft.activeTabsetId === action.tabsetId || !findTabset(draft, action.tabsetId)) return false;
|
|
1064
|
+
draft.activeTabsetId = action.tabsetId;
|
|
1065
|
+
return true;
|
|
996
1066
|
case "setFloatMinimized": {
|
|
997
1067
|
const float = findFloat(draft, action.floatId);
|
|
998
|
-
if (float
|
|
999
|
-
|
|
1068
|
+
if (!float || (float.minimized ?? false) === action.minimized) return false;
|
|
1069
|
+
float.minimized = action.minimized;
|
|
1070
|
+
return true;
|
|
1071
|
+
}
|
|
1072
|
+
case "setMaximizedTabset": {
|
|
1073
|
+
const next = action.tabsetId ?? void 0;
|
|
1074
|
+
if (draft.maximizedTabsetId === next) return false;
|
|
1075
|
+
draft.maximizedTabsetId = next;
|
|
1076
|
+
return true;
|
|
1000
1077
|
}
|
|
1001
|
-
case "setMaximizedTabset":
|
|
1002
|
-
draft.maximizedTabsetId = action.tabsetId ?? void 0;
|
|
1003
|
-
return;
|
|
1004
1078
|
case "updateGlobalAttributes":
|
|
1079
|
+
if (alreadyApplied(draft.global, action.attrs)) return false;
|
|
1005
1080
|
Object.assign(draft.global, action.attrs);
|
|
1006
|
-
return;
|
|
1081
|
+
return true;
|
|
1007
1082
|
case "updateNodeAttributes": {
|
|
1008
1083
|
const node = findAttributedNode(draft, action.nodeId);
|
|
1009
|
-
if (node)
|
|
1010
|
-
|
|
1084
|
+
if (!node) return false;
|
|
1085
|
+
const parsed = parseNodeAttrs(node, action.attrs);
|
|
1086
|
+
if (alreadyApplied(node, parsed)) return false;
|
|
1087
|
+
Object.assign(node, parsed);
|
|
1088
|
+
return true;
|
|
1011
1089
|
}
|
|
1012
|
-
default: assertNever(action);
|
|
1090
|
+
default: return assertNever(action);
|
|
1013
1091
|
}
|
|
1014
1092
|
};
|
|
1093
|
+
/** Identity-equal output means "no edit". A rejected action skips `normalize`, so
|
|
1094
|
+
* pass a model that has already been through it. */
|
|
1015
1095
|
const reducer = (model, action) => {
|
|
1016
1096
|
const draft = structuredClone(model);
|
|
1017
|
-
applyAction(draft, action);
|
|
1097
|
+
if (!applyAction(draft, action)) return model;
|
|
1018
1098
|
return normalize(draft);
|
|
1019
1099
|
};
|
|
1020
1100
|
|
|
@@ -1028,8 +1108,14 @@ const createHistory = (present) => ({
|
|
|
1028
1108
|
});
|
|
1029
1109
|
const canUndo = (history) => history.past.length > 0;
|
|
1030
1110
|
const canRedo = (history) => history.future.length > 0;
|
|
1111
|
+
/**
|
|
1112
|
+
* An action that changed nothing is not an edit: recording it would spend an
|
|
1113
|
+
* undo slot and clear a redo stack the user just built. Not coalescing (ADR
|
|
1114
|
+
* 0002): two distinct edits are still two entries.
|
|
1115
|
+
*/
|
|
1031
1116
|
const dispatch = (history, action) => {
|
|
1032
1117
|
const present = reducer(history.present, action);
|
|
1118
|
+
if (Object.is(present, history.present)) return history;
|
|
1033
1119
|
return {
|
|
1034
1120
|
future: [],
|
|
1035
1121
|
past: [...history.past, history.present].slice(-100),
|
|
@@ -1075,25 +1161,50 @@ const dashfooMachine = setup({ types: {
|
|
|
1075
1161
|
|
|
1076
1162
|
//#endregion
|
|
1077
1163
|
//#region src/machines/drag-dock-machine.ts
|
|
1078
|
-
|
|
1079
|
-
|
|
1164
|
+
/** One definition of both "is this drop valid" and "what does it do", so the guard and the emit agree. */
|
|
1165
|
+
const dropAction = (drag) => {
|
|
1166
|
+
if (drag.kind !== "dragging" || drag.drop === null) return null;
|
|
1167
|
+
const { intent, scope } = drag.drop;
|
|
1168
|
+
const { subject } = drag;
|
|
1169
|
+
if (subject.kind === "external") return {
|
|
1170
|
+
action: {
|
|
1171
|
+
index: intent.index,
|
|
1172
|
+
location: intent.location,
|
|
1173
|
+
tab: subject.tab,
|
|
1174
|
+
targetId: intent.targetId,
|
|
1175
|
+
type: "addNode"
|
|
1176
|
+
},
|
|
1177
|
+
scope
|
|
1178
|
+
};
|
|
1179
|
+
if (subject.kind === "tabset") return {
|
|
1180
|
+
action: {
|
|
1181
|
+
location: intent.location,
|
|
1182
|
+
sourceId: subject.id,
|
|
1183
|
+
targetId: intent.targetId,
|
|
1184
|
+
type: "moveTabset"
|
|
1185
|
+
},
|
|
1186
|
+
scope
|
|
1187
|
+
};
|
|
1080
1188
|
return {
|
|
1081
|
-
|
|
1082
|
-
|
|
1189
|
+
action: {
|
|
1190
|
+
index: intent.index,
|
|
1191
|
+
location: intent.location,
|
|
1192
|
+
sourceId: subject.id,
|
|
1193
|
+
targetId: intent.targetId,
|
|
1194
|
+
type: "moveNode"
|
|
1195
|
+
},
|
|
1196
|
+
scope
|
|
1083
1197
|
};
|
|
1084
1198
|
};
|
|
1085
1199
|
const dragDockMachine = setup({
|
|
1086
|
-
guards: { hasValidDrop: ({ context }) => context.
|
|
1200
|
+
guards: { hasValidDrop: ({ context }) => dropAction(context.drag) !== null },
|
|
1087
1201
|
types: {
|
|
1088
1202
|
context: {},
|
|
1089
1203
|
emitted: {},
|
|
1090
1204
|
events: {}
|
|
1091
1205
|
}
|
|
1092
1206
|
}).createMachine({
|
|
1093
|
-
context: {
|
|
1094
|
-
intent: null,
|
|
1095
|
-
subject: null
|
|
1096
|
-
},
|
|
1207
|
+
context: { drag: { kind: "idle" } },
|
|
1097
1208
|
id: "dragDock",
|
|
1098
1209
|
initial: "idle",
|
|
1099
1210
|
states: {
|
|
@@ -1101,36 +1212,30 @@ const dragDockMachine = setup({
|
|
|
1101
1212
|
CANCEL: { target: "idle" },
|
|
1102
1213
|
DROP: [{
|
|
1103
1214
|
actions: emit(({ context }) => {
|
|
1104
|
-
const
|
|
1215
|
+
const resolved = dropAction(context.drag);
|
|
1216
|
+
if (resolved === null) throw new Error("drop requires a subject and a resolved drop target");
|
|
1105
1217
|
return {
|
|
1106
|
-
action:
|
|
1107
|
-
|
|
1108
|
-
location: intent.location,
|
|
1109
|
-
tab: subject.tab,
|
|
1110
|
-
targetId: intent.targetId,
|
|
1111
|
-
type: "addNode"
|
|
1112
|
-
} : {
|
|
1113
|
-
index: intent.index,
|
|
1114
|
-
location: intent.location,
|
|
1115
|
-
sourceId: subject.id,
|
|
1116
|
-
targetId: intent.targetId,
|
|
1117
|
-
type: subject.kind === "tabset" ? "moveTabset" : "moveNode"
|
|
1118
|
-
},
|
|
1218
|
+
action: resolved.action,
|
|
1219
|
+
scope: resolved.scope,
|
|
1119
1220
|
type: "COMMIT"
|
|
1120
1221
|
};
|
|
1121
1222
|
}),
|
|
1122
1223
|
guard: "hasValidDrop",
|
|
1123
1224
|
target: "idle"
|
|
1124
1225
|
}, { target: "idle" }],
|
|
1125
|
-
OVER: { actions: assign({
|
|
1226
|
+
OVER: { actions: assign({ drag: ({ context, event }) => context.drag.kind === "dragging" ? {
|
|
1227
|
+
...context.drag,
|
|
1228
|
+
drop: event.drop
|
|
1229
|
+
} : context.drag }) }
|
|
1126
1230
|
} },
|
|
1127
1231
|
idle: {
|
|
1128
|
-
entry: assign({
|
|
1129
|
-
intent: null,
|
|
1130
|
-
subject: null
|
|
1131
|
-
}),
|
|
1232
|
+
entry: assign({ drag: { kind: "idle" } }),
|
|
1132
1233
|
on: { START: {
|
|
1133
|
-
actions: assign({
|
|
1234
|
+
actions: assign({ drag: ({ event }) => ({
|
|
1235
|
+
drop: null,
|
|
1236
|
+
kind: "dragging",
|
|
1237
|
+
subject: event.subject
|
|
1238
|
+
}) }),
|
|
1134
1239
|
target: "dragging"
|
|
1135
1240
|
} }
|
|
1136
1241
|
}
|
|
@@ -1164,12 +1269,13 @@ const stackModel = (model, orientation = "column") => {
|
|
|
1164
1269
|
children,
|
|
1165
1270
|
id: model.layout.id,
|
|
1166
1271
|
orientation,
|
|
1167
|
-
type: "row"
|
|
1272
|
+
type: "row",
|
|
1273
|
+
weight: model.layout.weight
|
|
1168
1274
|
},
|
|
1169
1275
|
maximizedTabsetId: void 0
|
|
1170
1276
|
});
|
|
1171
1277
|
};
|
|
1172
1278
|
|
|
1173
1279
|
//#endregion
|
|
1174
|
-
export { DEFAULT_SNAP_THRESHOLD, HISTORY_LIMIT, actionSchema, canRedo, canUndo, collectRoots, collectTabsets, collectTabsetsInRow, createHistory, createNodeId, createTabId, dashfooMachine, dashfooSchema, dimensionSchema, dispatch, dockLocationSchema, dockZonePolygons, dragDockMachine, edgeSchema, findAttributedNode, findDuplicateIds, findFloat, findRootContaining, findRow, findTab, findTabset, findTabsetParent, floatNode, floatNodeSchema, fromJSON, geometrySchema, getFirstTabset, globalAttributesSchema, jsonValueSchema, model, mutableNodeAttrsSchema, normalize, orientationSchema, parseModel, redo, reducer, resolveDockTarget, resolveSnapTargets, row, rowNodeSchema, snapEnabled, snapSchema, snapSizes, stackModel, tab, tabNodeSchema, tabset, tabsetNodeSchema, toJSON, undo, unitSchema, zoneRect };
|
|
1280
|
+
export { DEFAULT_SNAP_THRESHOLD, DEFAULT_WEIGHT, HISTORY_LIMIT, actionSchema, canRedo, canUndo, collectRoots, collectTabsets, collectTabsetsInRow, createHistory, createNodeId, createTabId, dashfooMachine, dashfooSchema, decideSnap, dimensionSchema, dispatch, dockLocationSchema, dockZonePolygons, dragDockMachine, dropAction, edgeSchema, findAttributedNode, findDuplicateIds, findFloat, findRootContaining, findRow, findTab, findTabset, findTabsetParent, floatNode, floatNodeSchema, fromJSON, geometrySchema, getFirstTabset, globalAttributesSchema, jsonValueSchema, model, mutableNodeAttrsSchema, normalize, orientationSchema, parseModel, redo, reducer, resolveDockTarget, resolveSnapGrid, resolveSnapTargets, row, rowNodeSchema, settleSnap, snapEnabled, snapSchema, snapSizes, splitEdge, stackModel, tab, tabNodeSchema, tabset, tabsetNodeObjectSchema, tabsetNodeSchema, toJSON, undo, unitSchema, zoneRect };
|
|
1175
1281
|
//# sourceMappingURL=index.js.map
|