@vuu-ui/grid-layout 4.0.0-alpha.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/LICENSE +201 -0
- package/README.md +141 -0
- package/package.json +40 -0
- package/src/GridCommand.js +395 -0
- package/src/GridComponentSettings.js +163 -0
- package/src/GridController.js +359 -0
- package/src/GridDragCoordinator.js +293 -0
- package/src/GridGeometry.js +1298 -0
- package/src/GridLayout.css.js +119 -0
- package/src/GridLayout.js +120 -0
- package/src/GridLayoutContext.js +58 -0
- package/src/GridLayoutDocument.js +307 -0
- package/src/GridLayoutDocumentIds.js +110 -0
- package/src/GridLayoutItem.css.js +46 -0
- package/src/GridLayoutItem.js +147 -0
- package/src/GridLayoutLegacyCompatibility.js +66 -0
- package/src/GridLayoutModel.js +138 -0
- package/src/GridLayoutProvider.js +263 -0
- package/src/GridLayoutStackedItem.css.js +26 -0
- package/src/GridLayoutStackedtem.js +140 -0
- package/src/GridModel.js +1289 -0
- package/src/GridPlaceholder.css.js +9 -0
- package/src/GridPlaceholder.js +24 -0
- package/src/GridSnapshot.js +11 -0
- package/src/GridSplitter.css.js +62 -0
- package/src/GridSplitter.js +26 -0
- package/src/GridStack.js +277 -0
- package/src/Icon.css.js +8 -0
- package/src/Icon.js +26 -0
- package/src/IconButton.css.js +9 -0
- package/src/IconButton.js +28 -0
- package/src/TabDialog.css.js +10 -0
- package/src/TabDialog.js +77 -0
- package/src/TabMenu.css.js +8 -0
- package/src/TabMenu.js +78 -0
- package/src/componentToJson.js +41 -0
- package/src/drag-drop-next/DragContextNext.js +173 -0
- package/src/drag-drop-next/DragDropProviderNext.css.js +72 -0
- package/src/drag-drop-next/DragDropProviderNext.js +91 -0
- package/src/drag-drop-next/SpaceMan.js +256 -0
- package/src/drag-drop-next/TemplateDragSession.js +36 -0
- package/src/drag-drop-next/tabstrip-drag-drop.js +251 -0
- package/src/grid-dom-utils.js +57 -0
- package/src/grid-layout-utils.js +39 -0
- package/src/grid-snapshot-adapters.js +435 -0
- package/src/index.js +24 -0
- package/src/json-value.js +56 -0
- package/src/layoutFromJson.js +15 -0
- package/src/layoutToJson.js +5 -0
- package/src/propUtils.js +15 -0
- package/src/react-element-utils.js +15 -0
- package/src/typeOf.js +15 -0
- package/src/useAsDropTarget.js +229 -0
- package/src/useDraggable.js +46 -0
- package/src/useEditTabName.js +70 -0
- package/src/useGridChildProps.js +47 -0
- package/src/useGridControllerSnapshot.js +21 -0
- package/src/useGridLayout.js +727 -0
- package/src/useGridSplitterResizing.js +262 -0
- package/src/useNotDropTarget.js +2 -0
- package/types/AddTabDialog.d.ts +5 -0
- package/types/GridCommand.d.ts +146 -0
- package/types/GridComponentSettings.d.ts +71 -0
- package/types/GridController.d.ts +50 -0
- package/types/GridDragCoordinator.d.ts +80 -0
- package/types/GridGeometry.d.ts +331 -0
- package/types/GridLayout.d.ts +20 -0
- package/types/GridLayoutContext.d.ts +141 -0
- package/types/GridLayoutDocument.d.ts +61 -0
- package/types/GridLayoutDocumentIds.d.ts +15 -0
- package/types/GridLayoutItem.d.ts +28 -0
- package/types/GridLayoutLegacyCompatibility.d.ts +57 -0
- package/types/GridLayoutModel.d.ts +72 -0
- package/types/GridLayoutProvider.d.ts +59 -0
- package/types/GridLayoutStackedtem.d.ts +10 -0
- package/types/GridModel.d.ts +504 -0
- package/types/GridPlaceholder.d.ts +5 -0
- package/types/GridSnapshot.d.ts +49 -0
- package/types/GridSplitter.d.ts +10 -0
- package/types/GridStack.d.ts +160 -0
- package/types/Icon.d.ts +6 -0
- package/types/IconButton.d.ts +6 -0
- package/types/TabDialog.d.ts +9 -0
- package/types/TabMenu.d.ts +7 -0
- package/types/componentToJson.d.ts +38 -0
- package/types/drag-drop-next/DragContextNext.d.ts +92 -0
- package/types/drag-drop-next/DragDropProviderNext.d.ts +23 -0
- package/types/drag-drop-next/SpaceMan.d.ts +35 -0
- package/types/drag-drop-next/TemplateDragSession.d.ts +21 -0
- package/types/drag-drop-next/tabstrip-drag-drop.d.ts +3 -0
- package/types/grid-dom-utils.d.ts +17 -0
- package/types/grid-layout-utils.d.ts +18 -0
- package/types/grid-matrix.d.ts +5 -0
- package/types/grid-snapshot-adapters.d.ts +22 -0
- package/types/index.d.ts +24 -0
- package/types/json-value.d.ts +23 -0
- package/types/layoutFromJson.d.ts +4 -0
- package/types/layoutToJson.d.ts +3 -0
- package/types/propUtils.d.ts +5 -0
- package/types/react-element-utils.d.ts +3 -0
- package/types/src/AddTabDialog.d.ts +5 -0
- package/types/src/GridLayout.d.ts +14 -0
- package/types/src/GridLayoutContext.d.ts +88 -0
- package/types/src/GridLayoutItem.d.ts +28 -0
- package/types/src/GridLayoutModel.d.ts +59 -0
- package/types/src/GridLayoutProvider.d.ts +37 -0
- package/types/src/GridLayoutStackedtem.d.ts +10 -0
- package/types/src/GridModel.d.ts +350 -0
- package/types/src/GridPlaceholder.d.ts +5 -0
- package/types/src/GridSplitter.d.ts +9 -0
- package/types/src/Icon.d.ts +6 -0
- package/types/src/IconButton.d.ts +6 -0
- package/types/src/TabMenu.d.ts +6 -0
- package/types/src/componentToJson.d.ts +32 -0
- package/types/src/drag-drop-next/DragContextNext.d.ts +73 -0
- package/types/src/drag-drop-next/DragDropProviderNext.d.ts +20 -0
- package/types/src/drag-drop-next/SpaceMan.d.ts +35 -0
- package/types/src/drag-drop-next/tabstrip-drag-drop.d.ts +3 -0
- package/types/src/grid-dom-utils.d.ts +17 -0
- package/types/src/grid-layout-utils.d.ts +80 -0
- package/types/src/grid-matrix.d.ts +5 -0
- package/types/src/index.d.ts +10 -0
- package/types/src/layoutFromJson.d.ts +3 -0
- package/types/src/layoutToJson.d.ts +2 -0
- package/types/src/propUtils.d.ts +5 -0
- package/types/src/react-element-utils.d.ts +3 -0
- package/types/src/typeOf.d.ts +6 -0
- package/types/src/useAsDropTarget.d.ts +7 -0
- package/types/src/useDraggable.d.ts +23 -0
- package/types/src/useGridChildProps.d.ts +13 -0
- package/types/src/useGridLayout.d.ts +43 -0
- package/types/src/useGridSplitterResizing.d.ts +12 -0
- package/types/src/useNotDropTarget.d.ts +1 -0
- package/types/typeOf.d.ts +6 -0
- package/types/useAsDropTarget.d.ts +7 -0
- package/types/useDraggable.d.ts +24 -0
- package/types/useEditTabName.d.ts +11 -0
- package/types/useGridChildProps.d.ts +13 -0
- package/types/useGridControllerSnapshot.d.ts +7 -0
- package/types/useGridLayout.d.ts +53 -0
- package/types/useGridSplitterResizing.d.ts +14 -0
- package/types/useNotDropTarget.d.ts +1 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { uuid } from "@vuu-ui/vuu-utils";
|
|
2
|
+
import { regeneratePlaceholders, removeGridItem, replaceGridItem, resizeTrackTo, resizeTracksAdjacent, resizeTracksProportionally, splitGridItem } from "./GridGeometry.js";
|
|
3
|
+
import { GridLayoutModel } from "./GridLayoutModel.js";
|
|
4
|
+
import { GridModelChildItem } from "./GridModel.js";
|
|
5
|
+
import { gridStackItemIds } from "./GridStack.js";
|
|
6
|
+
class GridCommandExecutionError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
constructor({ code, message }){
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "GridCommandExecutionError";
|
|
11
|
+
this.code = code;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const assertNever = (value)=>{
|
|
15
|
+
throw Error(`Unhandled GridCommand: ${JSON.stringify(value)}`);
|
|
16
|
+
};
|
|
17
|
+
const success = (command)=>({
|
|
18
|
+
command: command.type,
|
|
19
|
+
ok: true
|
|
20
|
+
});
|
|
21
|
+
const failure = (command, code, message)=>({
|
|
22
|
+
command: command.type,
|
|
23
|
+
error: {
|
|
24
|
+
code,
|
|
25
|
+
message
|
|
26
|
+
},
|
|
27
|
+
ok: false
|
|
28
|
+
});
|
|
29
|
+
const geometryFailure = (command, { code, message })=>{
|
|
30
|
+
switch(code){
|
|
31
|
+
case "GEOMETRY_ERROR":
|
|
32
|
+
throw Error(message);
|
|
33
|
+
case "INVALID_TARGET":
|
|
34
|
+
return failure(command, "INVALID_TARGET", message);
|
|
35
|
+
case "ITEM_NOT_FOUND":
|
|
36
|
+
return failure(command, "ITEM_NOT_FOUND", message);
|
|
37
|
+
case "MEASUREMENT_REQUIRED":
|
|
38
|
+
return failure(command, "MEASUREMENT_REQUIRED", message);
|
|
39
|
+
case "NON_RESIZABLE":
|
|
40
|
+
return failure(command, "NON_RESIZABLE", message);
|
|
41
|
+
case "TRACK_NOT_FOUND":
|
|
42
|
+
return failure(command, "TRACK_NOT_FOUND", message);
|
|
43
|
+
default:
|
|
44
|
+
return failure(command, "INVALID_ITEM", message);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const stackFailure = (command, { code, message })=>failure(command, code, message);
|
|
48
|
+
const toPosition = ({ span, start })=>({
|
|
49
|
+
end: start + span,
|
|
50
|
+
start
|
|
51
|
+
});
|
|
52
|
+
const toChildItem = (item, stackId)=>new GridModelChildItem({
|
|
53
|
+
...item,
|
|
54
|
+
column: toPosition(item.column),
|
|
55
|
+
row: toPosition(item.row),
|
|
56
|
+
stackId
|
|
57
|
+
});
|
|
58
|
+
const toConstraints = (constraints)=>constraints?.map(({ minimum, trackIndices })=>({
|
|
59
|
+
minimum,
|
|
60
|
+
trackIndices: [
|
|
61
|
+
...trackIndices
|
|
62
|
+
]
|
|
63
|
+
})) ?? [];
|
|
64
|
+
class LegacyGridCommandExecutor {
|
|
65
|
+
gridModel;
|
|
66
|
+
gridLayoutModel;
|
|
67
|
+
constructor(gridModel, gridLayoutModel = new GridLayoutModel(gridModel)){
|
|
68
|
+
this.gridModel = gridModel;
|
|
69
|
+
this.gridLayoutModel = gridLayoutModel;
|
|
70
|
+
}
|
|
71
|
+
execute(command) {
|
|
72
|
+
switch(command.type){
|
|
73
|
+
case "add-item":
|
|
74
|
+
{
|
|
75
|
+
const invalid = this.validateNewItem(command, command.item);
|
|
76
|
+
if (invalid) return invalid;
|
|
77
|
+
this.gridModel.addChildItem(toChildItem(command.item));
|
|
78
|
+
return success(command);
|
|
79
|
+
}
|
|
80
|
+
case "move-item":
|
|
81
|
+
{
|
|
82
|
+
const invalid = this.validatePair(command, command.itemId, command.targetId);
|
|
83
|
+
if (invalid) return invalid;
|
|
84
|
+
const stackedMember = this.rejectStackMember(command, command.itemId, command.targetId);
|
|
85
|
+
if (stackedMember) return stackedMember;
|
|
86
|
+
const geometry = this.gridModel.toGeometry();
|
|
87
|
+
const split = this.gridModel.runGeometry((measurements)=>splitGridItem(geometry, {
|
|
88
|
+
droppedItemId: command.itemId,
|
|
89
|
+
splitDirection: command.position,
|
|
90
|
+
targetItemId: command.targetId
|
|
91
|
+
}, measurements));
|
|
92
|
+
if (!split.ok) return geometryFailure(command, split.error);
|
|
93
|
+
this.gridLayoutModel.applySplitTransition(split.value);
|
|
94
|
+
this.gridModel.getChildItem(command.itemId, true).dragging = false;
|
|
95
|
+
return success(command);
|
|
96
|
+
}
|
|
97
|
+
case "replace-item":
|
|
98
|
+
{
|
|
99
|
+
const invalid = this.validatePair(command, command.itemId, command.targetId);
|
|
100
|
+
if (invalid) return invalid;
|
|
101
|
+
const stackedMember = this.rejectStackMember(command, command.itemId, command.targetId);
|
|
102
|
+
if (stackedMember) return stackedMember;
|
|
103
|
+
const replaced = replaceGridItem(this.gridModel.toGeometry(), {
|
|
104
|
+
droppedItemId: command.itemId,
|
|
105
|
+
targetItemId: command.targetId
|
|
106
|
+
});
|
|
107
|
+
if (!replaced.ok) return geometryFailure(command, replaced.error);
|
|
108
|
+
this.gridLayoutModel.applyReplaceTransition(replaced.value);
|
|
109
|
+
this.gridModel.getChildItem(command.itemId, true).dragging = false;
|
|
110
|
+
return success(command);
|
|
111
|
+
}
|
|
112
|
+
case "replace-stack-item":
|
|
113
|
+
{
|
|
114
|
+
const invalid = this.requireTab(command, command.stackId, command.itemId) ?? this.requireItem(command, command.targetId);
|
|
115
|
+
if (invalid) return invalid;
|
|
116
|
+
const target = this.gridModel.getChildItem(command.targetId, true);
|
|
117
|
+
if (target.stackId || "stacked-content" === target.type) return failure(command, "INVALID_TARGET", "A stack member can only replace a standalone grid item");
|
|
118
|
+
const removed = this.gridModel.removeStackItem(command.stackId, command.itemId);
|
|
119
|
+
if (!removed.ok) return stackFailure(command, removed.error);
|
|
120
|
+
const item = this.gridModel.getChildItem(command.itemId, true);
|
|
121
|
+
item.stackId = void 0;
|
|
122
|
+
item.contentDetached = void 0;
|
|
123
|
+
item.contentVisible = true;
|
|
124
|
+
item.dragging = false;
|
|
125
|
+
const replaced = replaceGridItem(this.gridModel.toGeometry(), {
|
|
126
|
+
droppedItemId: command.itemId,
|
|
127
|
+
targetItemId: command.targetId
|
|
128
|
+
});
|
|
129
|
+
if (!replaced.ok) return geometryFailure(command, replaced.error);
|
|
130
|
+
this.gridLayoutModel.applyReplaceTransition(replaced.value);
|
|
131
|
+
return success(command);
|
|
132
|
+
}
|
|
133
|
+
case "remove-item":
|
|
134
|
+
{
|
|
135
|
+
const invalid = this.requireItem(command, command.itemId);
|
|
136
|
+
if (invalid) return invalid;
|
|
137
|
+
return this.removeItem(command, command.itemId, command.reason);
|
|
138
|
+
}
|
|
139
|
+
case "resize-track":
|
|
140
|
+
{
|
|
141
|
+
const invalid = this.requireTracks(command, command.track, [
|
|
142
|
+
command.index
|
|
143
|
+
]);
|
|
144
|
+
if (invalid) return invalid;
|
|
145
|
+
const resized = this.gridModel.tracks.runTrackGeometry(command.track, (tracks)=>resizeTrackTo(tracks, command.index, command.size, command.track));
|
|
146
|
+
if (!resized.ok) return geometryFailure(command, resized.error);
|
|
147
|
+
this.gridModel.tracks.applyTrackTransition(command.track, resized.value);
|
|
148
|
+
return success(command);
|
|
149
|
+
}
|
|
150
|
+
case "resize-tracks":
|
|
151
|
+
{
|
|
152
|
+
if (!Number.isFinite(command.delta)) return failure(command, "INVALID_ITEM", "Grid track resize delta must be finite");
|
|
153
|
+
if ("adjacent" === command.distribution && command.resizedTrackIndex === command.contraTrackIndex) return failure(command, "INVALID_TARGET", "Adjacent track resize requires two distinct tracks");
|
|
154
|
+
const indices = "adjacent" === command.distribution ? [
|
|
155
|
+
command.resizedTrackIndex,
|
|
156
|
+
command.contraTrackIndex
|
|
157
|
+
] : [
|
|
158
|
+
...command.beforeTrackIndices,
|
|
159
|
+
...command.afterTrackIndices,
|
|
160
|
+
...(command.beforeConstraints ?? []).flatMap(({ trackIndices })=>trackIndices),
|
|
161
|
+
...(command.afterConstraints ?? []).flatMap(({ trackIndices })=>trackIndices)
|
|
162
|
+
];
|
|
163
|
+
if ("proportional" === command.distribution && command.initialSizes && (command.initialSizes.length !== this.gridModel.tracks.getTracks(command.track).length || command.initialSizes.some((size)=>!Number.isFinite(size) || size < 0))) return failure(command, "INVALID_ITEM", `Initial ${command.track} sizes must contain one non-negative value per track`);
|
|
164
|
+
const invalid = this.requireTracks(command, command.track, indices) ?? this.validateResizeCommand(command) ?? this.applyMeasurements(command, command.track, indices, command.measuredSizes);
|
|
165
|
+
if (invalid) return invalid;
|
|
166
|
+
const resizedTracks = this.gridModel.tracks.runTrackGeometry(command.track, (tracks)=>"adjacent" === command.distribution ? resizeTracksAdjacent(tracks, {
|
|
167
|
+
contraTrackIndex: command.contraTrackIndex,
|
|
168
|
+
delta: command.delta,
|
|
169
|
+
resizedTrackIndex: command.resizedTrackIndex
|
|
170
|
+
}, command.track) : resizeTracksProportionally(tracks, {
|
|
171
|
+
afterConstraints: toConstraints(command.afterConstraints),
|
|
172
|
+
afterTrackIndices: command.afterTrackIndices,
|
|
173
|
+
beforeConstraints: toConstraints(command.beforeConstraints),
|
|
174
|
+
beforeTrackIndices: command.beforeTrackIndices,
|
|
175
|
+
delta: command.delta,
|
|
176
|
+
initialSizes: command.initialSizes
|
|
177
|
+
}, command.track));
|
|
178
|
+
if (!resizedTracks.ok) return geometryFailure(command, resizedTracks.error);
|
|
179
|
+
this.gridModel.tracks.applyTrackTransition(command.track, resizedTracks.value);
|
|
180
|
+
return success(command);
|
|
181
|
+
}
|
|
182
|
+
case "create-stack":
|
|
183
|
+
{
|
|
184
|
+
const invalid = this.validatePair(command, command.itemId, command.targetId);
|
|
185
|
+
if (invalid) return invalid;
|
|
186
|
+
if (this.gridModel.getChildItem(command.itemId, true).stackId || this.gridModel.getChildItem(command.targetId, true).stackId) return failure(command, "INVALID_TARGET", "A new stack requires two unstacked items");
|
|
187
|
+
const created = this.gridModel.createStack(command.targetId, command.itemId);
|
|
188
|
+
if (!created.ok) return stackFailure(command, created.error);
|
|
189
|
+
this.gridModel.getChildItem(command.itemId, true).dragging = false;
|
|
190
|
+
if (command.selectedItemId) {
|
|
191
|
+
const selected = this.gridModel.selectStackItem(created.value.stackId, command.selectedItemId);
|
|
192
|
+
if (!selected.ok) return stackFailure(command, selected.error);
|
|
193
|
+
}
|
|
194
|
+
return success(command);
|
|
195
|
+
}
|
|
196
|
+
case "add-stack-item":
|
|
197
|
+
{
|
|
198
|
+
const invalid = this.requireStack(command, command.stackId) ?? this.validateNewItem(command, command.item);
|
|
199
|
+
if (invalid) return invalid;
|
|
200
|
+
if (void 0 === command.position !== (void 0 === command.targetItemId)) return failure(command, "INVALID_TARGET", "A stack placement requires both position and targetItemId");
|
|
201
|
+
const stack = this.gridModel.getChildItem(command.stackId, true);
|
|
202
|
+
const added = this.gridModel.addStackItem(command.stackId, toChildItem({
|
|
203
|
+
...command.item,
|
|
204
|
+
column: {
|
|
205
|
+
span: stack.column.end - stack.column.start,
|
|
206
|
+
start: stack.column.start
|
|
207
|
+
},
|
|
208
|
+
row: {
|
|
209
|
+
span: stack.row.end - stack.row.start,
|
|
210
|
+
start: stack.row.start
|
|
211
|
+
}
|
|
212
|
+
}, command.stackId), command.position && command.targetItemId ? {
|
|
213
|
+
position: command.position,
|
|
214
|
+
target: command.targetItemId
|
|
215
|
+
} : void 0);
|
|
216
|
+
if (!added.ok) return stackFailure(command, added.error);
|
|
217
|
+
return success(command);
|
|
218
|
+
}
|
|
219
|
+
case "remove-stack-item":
|
|
220
|
+
{
|
|
221
|
+
const invalid = this.requireTab(command, command.stackId, command.itemId);
|
|
222
|
+
if (invalid) return invalid;
|
|
223
|
+
return this.removeItem(command, command.itemId, "close");
|
|
224
|
+
}
|
|
225
|
+
case "move-item-to-stack":
|
|
226
|
+
{
|
|
227
|
+
const invalid = this.requireItem(command, command.itemId) ?? this.requireStack(command, command.stackId);
|
|
228
|
+
if (invalid) return invalid;
|
|
229
|
+
if (void 0 === command.position !== (void 0 === command.targetItemId)) return failure(command, "INVALID_TARGET", "A stack placement requires both position and targetItemId");
|
|
230
|
+
if (command.fromStackId) {
|
|
231
|
+
const tabFailure = this.requireTab(command, command.fromStackId, command.itemId);
|
|
232
|
+
if (tabFailure) return tabFailure;
|
|
233
|
+
const removed = this.gridModel.removeStackItem(command.fromStackId, command.itemId);
|
|
234
|
+
if (!removed.ok) return stackFailure(command, removed.error);
|
|
235
|
+
} else if (!this.gridModel.getChildItem(command.itemId, true).dragging) {
|
|
236
|
+
const removed = this.removeItem(command, command.itemId, "drag");
|
|
237
|
+
if (!removed.ok) return removed;
|
|
238
|
+
}
|
|
239
|
+
const item = this.gridModel.getChildItem(command.itemId, true);
|
|
240
|
+
item.stackId = void 0;
|
|
241
|
+
item.contentDetached = void 0;
|
|
242
|
+
item.dragging = false;
|
|
243
|
+
const added = this.gridModel.addStackMember(command.stackId, command.itemId, command.position && command.targetItemId ? {
|
|
244
|
+
position: command.position,
|
|
245
|
+
target: command.targetItemId
|
|
246
|
+
} : void 0);
|
|
247
|
+
if (!added.ok) return stackFailure(command, added.error);
|
|
248
|
+
const selected = this.gridModel.selectStackItem(command.stackId, command.itemId);
|
|
249
|
+
return selected.ok ? success(command) : stackFailure(command, selected.error);
|
|
250
|
+
}
|
|
251
|
+
case "move-stack-item-to-grid":
|
|
252
|
+
{
|
|
253
|
+
const invalid = this.requireTab(command, command.stackId, command.itemId) ?? this.requireItem(command, command.targetId);
|
|
254
|
+
if (invalid) return invalid;
|
|
255
|
+
const stackItemIds = gridStackItemIds(this.gridModel.getStackState(command.stackId));
|
|
256
|
+
const targetId = command.targetId === command.stackId && 2 === stackItemIds.length ? stackItemIds.find((itemId)=>itemId !== command.itemId) : command.targetId;
|
|
257
|
+
if (!targetId) return failure(command, "INVALID_TARGET", `Grid stack #${command.stackId} has no remaining split target`);
|
|
258
|
+
const removed = this.gridModel.removeStackItem(command.stackId, command.itemId);
|
|
259
|
+
if (!removed.ok) return stackFailure(command, removed.error);
|
|
260
|
+
const item = this.gridModel.getChildItem(command.itemId, true);
|
|
261
|
+
item.stackId = void 0;
|
|
262
|
+
item.contentDetached = void 0;
|
|
263
|
+
item.contentVisible = true;
|
|
264
|
+
item.dragging = false;
|
|
265
|
+
const split = this.gridModel.runGeometry((measurements)=>splitGridItem(this.gridModel.toGeometry(), {
|
|
266
|
+
droppedItemId: command.itemId,
|
|
267
|
+
splitDirection: command.position,
|
|
268
|
+
targetItemId: targetId
|
|
269
|
+
}, measurements));
|
|
270
|
+
if (!split.ok) return geometryFailure(command, split.error);
|
|
271
|
+
this.gridLayoutModel.applySplitTransition(split.value);
|
|
272
|
+
return success(command);
|
|
273
|
+
}
|
|
274
|
+
case "select-stack-item":
|
|
275
|
+
{
|
|
276
|
+
const invalid = this.requireTab(command, command.stackId, command.itemId);
|
|
277
|
+
if (invalid) return invalid;
|
|
278
|
+
const selected = this.gridModel.selectStackItem(command.stackId, command.itemId);
|
|
279
|
+
if (!selected.ok) return stackFailure(command, selected.error);
|
|
280
|
+
return success(command);
|
|
281
|
+
}
|
|
282
|
+
case "reorder-stack-item":
|
|
283
|
+
{
|
|
284
|
+
const invalid = this.requireTab(command, command.stackId, command.itemId) ?? this.requireTab(command, command.stackId, command.targetItemId);
|
|
285
|
+
if (invalid) return invalid;
|
|
286
|
+
const reordered = this.gridModel.reorderStackItem(command.stackId, {
|
|
287
|
+
activate: command.activate ?? false,
|
|
288
|
+
itemId: command.itemId,
|
|
289
|
+
placement: command.position,
|
|
290
|
+
targetItemId: command.targetItemId
|
|
291
|
+
});
|
|
292
|
+
if (!reordered.ok) return stackFailure(command, reordered.error);
|
|
293
|
+
return success(command);
|
|
294
|
+
}
|
|
295
|
+
case "rename-item":
|
|
296
|
+
{
|
|
297
|
+
const invalid = this.requireItem(command, command.itemId);
|
|
298
|
+
if (invalid) return invalid;
|
|
299
|
+
this.gridModel.updateChildTitle(command.itemId, command.title);
|
|
300
|
+
return success(command);
|
|
301
|
+
}
|
|
302
|
+
case "regenerate-placeholders":
|
|
303
|
+
this.gridModel.applyPlaceholderTransition(regeneratePlaceholders(this.gridModel.toGeometry(), uuid));
|
|
304
|
+
return success(command);
|
|
305
|
+
default:
|
|
306
|
+
return assertNever(command);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
removeItem(command, itemId, reason) {
|
|
310
|
+
const geometry = this.gridModel.toGeometry();
|
|
311
|
+
const removed = this.gridModel.runGeometry((measurements)=>removeGridItem(geometry, {
|
|
312
|
+
itemId,
|
|
313
|
+
reason
|
|
314
|
+
}, {
|
|
315
|
+
createPlaceholderId: uuid,
|
|
316
|
+
measurements
|
|
317
|
+
}));
|
|
318
|
+
if (!removed.ok) return geometryFailure(command, removed.error);
|
|
319
|
+
this.gridLayoutModel.applyRemoveTransition(removed.value);
|
|
320
|
+
return success(command);
|
|
321
|
+
}
|
|
322
|
+
applyMeasurements(command, trackType, indices, measuredSizes) {
|
|
323
|
+
const tracks = this.gridModel.tracks.getTracks(trackType);
|
|
324
|
+
if (measuredSizes) {
|
|
325
|
+
if (measuredSizes.length !== tracks.length || measuredSizes.some((size)=>!Number.isFinite(size) || size < 0)) return failure(command, "INVALID_ITEM", `Measured ${trackType} sizes must contain one non-negative value per track`);
|
|
326
|
+
measuredSizes.forEach((size, index)=>{
|
|
327
|
+
tracks[index].measuredValue = size;
|
|
328
|
+
});
|
|
329
|
+
indices.forEach((index)=>{
|
|
330
|
+
if (tracks[index].isFraction) tracks[index].convertUnitsToPixels();
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
if (indices.some((index)=>tracks[index].isFraction && !tracks[index].isMeasured)) return failure(command, "MEASUREMENT_REQUIRED", `Fractional ${trackType} resize requires measuredSizes`);
|
|
334
|
+
}
|
|
335
|
+
validateResizeCommand(command) {
|
|
336
|
+
const tracks = this.gridModel.tracks.getTracks(command.track);
|
|
337
|
+
if ("adjacent" === command.distribution) {
|
|
338
|
+
const currentSize = (index)=>command.measuredSizes?.[index] ?? (tracks[index].isFraction ? void 0 : tracks[index].numericValue);
|
|
339
|
+
const resizedSize = currentSize(command.resizedTrackIndex);
|
|
340
|
+
const contraSize = currentSize(command.contraTrackIndex);
|
|
341
|
+
if (void 0 !== resizedSize && resizedSize + command.delta < 0 || void 0 !== contraSize && contraSize - command.delta < 0) return failure(command, "INVALID_ITEM", "Adjacent track resize cannot produce a negative track size");
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const before = new Set(command.beforeTrackIndices);
|
|
345
|
+
const after = new Set(command.afterTrackIndices);
|
|
346
|
+
if (before.size !== command.beforeTrackIndices.length || after.size !== command.afterTrackIndices.length || [
|
|
347
|
+
...before
|
|
348
|
+
].some((index)=>after.has(index))) return failure(command, "INVALID_TARGET", "Proportional track resize groups must contain unique, disjoint tracks");
|
|
349
|
+
for (const [constraints, group] of [
|
|
350
|
+
[
|
|
351
|
+
command.beforeConstraints ?? [],
|
|
352
|
+
before
|
|
353
|
+
],
|
|
354
|
+
[
|
|
355
|
+
command.afterConstraints ?? [],
|
|
356
|
+
after
|
|
357
|
+
]
|
|
358
|
+
])for (const { minimum, trackIndices } of constraints)if (!Number.isFinite(minimum) || minimum < 0 || trackIndices.some((index)=>!group.has(index))) return failure(command, "INVALID_ITEM", "Grid track resize constraints must reference their own group and have non-negative minimums");
|
|
359
|
+
}
|
|
360
|
+
requireItem(command, itemId) {
|
|
361
|
+
if (!this.gridModel.getChildItem(itemId)) return failure(command, "ITEM_NOT_FOUND", `[GridModel] GridItem #${itemId} not found`);
|
|
362
|
+
}
|
|
363
|
+
requireStack(command, stackId) {
|
|
364
|
+
const stack = this.gridModel.getChildItem(stackId);
|
|
365
|
+
if (!stack || "stacked-content" !== stack.type) return failure(command, "STACK_NOT_FOUND", `Grid stack #${stackId} not found`);
|
|
366
|
+
}
|
|
367
|
+
requireTab(command, stackId, itemId) {
|
|
368
|
+
const invalidStack = this.requireStack(command, stackId);
|
|
369
|
+
if (invalidStack) return invalidStack;
|
|
370
|
+
if (!gridStackItemIds(this.gridModel.getStackState(stackId)).includes(itemId)) return failure(command, "TAB_NOT_FOUND", `Grid tab #${itemId} not found in stack #${stackId}`);
|
|
371
|
+
}
|
|
372
|
+
requireTracks(command, trackType, indices) {
|
|
373
|
+
const trackCount = this.gridModel.tracks.getTracks(trackType).length;
|
|
374
|
+
const missing = indices.find((index)=>!Number.isInteger(index) || index < 0 || index >= trackCount);
|
|
375
|
+
if (void 0 !== missing) return failure(command, "TRACK_NOT_FOUND", `Grid ${trackType} track #${missing} not found`);
|
|
376
|
+
}
|
|
377
|
+
validateNewItem(command, item) {
|
|
378
|
+
if (this.gridModel.getChildItem(item.id)) return failure(command, "DUPLICATE_ITEM_ID", `Grid item #${item.id} already exists`);
|
|
379
|
+
const { colCount, rowCount } = this.gridModel.tracks;
|
|
380
|
+
if (!item.id || !Number.isInteger(item.column.start) || !Number.isInteger(item.column.span) || !Number.isInteger(item.row.start) || !Number.isInteger(item.row.span) || item.column.start < 1 || item.row.start < 1 || item.column.span < 1 || item.row.span < 1 || item.column.start + item.column.span > colCount + 1 || item.row.start + item.row.span > rowCount + 1) return failure(command, "INVALID_ITEM", `Grid item #${item.id} has invalid grid coordinates`);
|
|
381
|
+
}
|
|
382
|
+
validatePair(command, itemId, targetId) {
|
|
383
|
+
const invalid = this.requireItem(command, itemId) ?? this.requireItem(command, targetId);
|
|
384
|
+
if (invalid) return invalid;
|
|
385
|
+
if (itemId === targetId) return failure(command, "INVALID_TARGET", `Grid item #${itemId} cannot target itself`);
|
|
386
|
+
}
|
|
387
|
+
rejectStackMember(command, ...itemIds) {
|
|
388
|
+
const stackedMember = itemIds.map((itemId)=>this.gridModel.getChildItem(itemId, true)).find(({ stackId })=>void 0 !== stackId);
|
|
389
|
+
if (stackedMember) return failure(command, "INVALID_TARGET", `Stack member #${stackedMember.id} must be moved through a stack command`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const throwForGridCommandFailure = (result)=>{
|
|
393
|
+
if (!result.ok) throw new GridCommandExecutionError(result.error);
|
|
394
|
+
};
|
|
395
|
+
export { GridCommandExecutionError, LegacyGridCommandExecutor, throwForGridCommandFailure };
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { toJsonValue } from "./json-value.js";
|
|
2
|
+
const jsonIssue = (issue, component, code, version)=>({
|
|
3
|
+
code,
|
|
4
|
+
componentId: component.id,
|
|
5
|
+
componentType: component.type,
|
|
6
|
+
message: issue.message,
|
|
7
|
+
path: issue.path,
|
|
8
|
+
version
|
|
9
|
+
});
|
|
10
|
+
const codecError = (issue, component, code, version)=>({
|
|
11
|
+
code,
|
|
12
|
+
componentId: component.id,
|
|
13
|
+
componentType: component.type,
|
|
14
|
+
message: issue.message,
|
|
15
|
+
path: issue.path,
|
|
16
|
+
version
|
|
17
|
+
});
|
|
18
|
+
class GridComponentSettingsRegistry {
|
|
19
|
+
#codecs = new Map();
|
|
20
|
+
register(type, codec) {
|
|
21
|
+
if (!type) throw new Error("Grid component type must not be empty");
|
|
22
|
+
if (!Number.isInteger(codec.version) || codec.version < 1) throw new Error(`Grid component codec "${type}" has an invalid version`);
|
|
23
|
+
if (this.#codecs.has(type)) throw new Error(`Grid component codec "${type}" is already registered`);
|
|
24
|
+
this.#codecs.set(type, {
|
|
25
|
+
decode: codec.decode,
|
|
26
|
+
encode: (settings)=>codec.isSettings(settings) ? codec.encode(settings) : {
|
|
27
|
+
error: {
|
|
28
|
+
code: "INVALID_SETTINGS_TYPE",
|
|
29
|
+
message: `settings do not match component type "${type}"`,
|
|
30
|
+
path: "$"
|
|
31
|
+
},
|
|
32
|
+
ok: false
|
|
33
|
+
},
|
|
34
|
+
migrations: codec.migrations ?? {},
|
|
35
|
+
version: codec.version
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
encode(component) {
|
|
39
|
+
const codec = this.#codecs.get(component.type);
|
|
40
|
+
if (!codec) return {
|
|
41
|
+
error: {
|
|
42
|
+
code: "UNKNOWN_COMPONENT_TYPE",
|
|
43
|
+
componentId: component.id,
|
|
44
|
+
componentType: component.type,
|
|
45
|
+
message: `component type "${component.type}" is not registered`,
|
|
46
|
+
path: "$.type"
|
|
47
|
+
},
|
|
48
|
+
ok: false
|
|
49
|
+
};
|
|
50
|
+
const encoded = codec.encode(component.settings);
|
|
51
|
+
if (!encoded.ok) return {
|
|
52
|
+
error: codecError(encoded.error, component, "COMPONENT_ENCODE_FAILED", codec.version),
|
|
53
|
+
ok: false
|
|
54
|
+
};
|
|
55
|
+
const json = toJsonValue(encoded.value, "$.settings");
|
|
56
|
+
if (!json.ok) return {
|
|
57
|
+
error: jsonIssue(json.error, component, "COMPONENT_ENCODE_FAILED", codec.version),
|
|
58
|
+
ok: false
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
ok: true,
|
|
62
|
+
value: {
|
|
63
|
+
id: component.id,
|
|
64
|
+
settings: json.value,
|
|
65
|
+
type: component.type,
|
|
66
|
+
version: codec.version
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
decode(component) {
|
|
71
|
+
const codec = this.#codecs.get(component.type);
|
|
72
|
+
if (!codec) return {
|
|
73
|
+
error: {
|
|
74
|
+
code: "UNKNOWN_COMPONENT_TYPE",
|
|
75
|
+
componentId: component.id,
|
|
76
|
+
componentType: component.type,
|
|
77
|
+
message: `component type "${component.type}" is not registered`,
|
|
78
|
+
path: "$.type",
|
|
79
|
+
version: component.version
|
|
80
|
+
},
|
|
81
|
+
ok: false
|
|
82
|
+
};
|
|
83
|
+
if (component.version > codec.version || component.version < 1) return {
|
|
84
|
+
error: {
|
|
85
|
+
code: "UNSUPPORTED_COMPONENT_VERSION",
|
|
86
|
+
componentId: component.id,
|
|
87
|
+
componentType: component.type,
|
|
88
|
+
message: `component settings version ${component.version} is not supported; current version is ${codec.version}`,
|
|
89
|
+
path: "$.version",
|
|
90
|
+
version: component.version
|
|
91
|
+
},
|
|
92
|
+
ok: false
|
|
93
|
+
};
|
|
94
|
+
let value = component.settings;
|
|
95
|
+
for(let version = component.version; version < codec.version; version += 1){
|
|
96
|
+
const migrate = codec.migrations[version];
|
|
97
|
+
if (!migrate) return {
|
|
98
|
+
error: {
|
|
99
|
+
code: "UNSUPPORTED_COMPONENT_VERSION",
|
|
100
|
+
componentId: component.id,
|
|
101
|
+
componentType: component.type,
|
|
102
|
+
message: `no migration from component settings version ${version}`,
|
|
103
|
+
path: "$.version",
|
|
104
|
+
version
|
|
105
|
+
},
|
|
106
|
+
ok: false
|
|
107
|
+
};
|
|
108
|
+
const migrated = migrate(value);
|
|
109
|
+
if (!migrated.ok) return {
|
|
110
|
+
error: codecError(migrated.error, component, "COMPONENT_MIGRATION_FAILED", version),
|
|
111
|
+
ok: false
|
|
112
|
+
};
|
|
113
|
+
const json = toJsonValue(migrated.value, "$.settings");
|
|
114
|
+
if (!json.ok) return {
|
|
115
|
+
error: jsonIssue(json.error, component, "COMPONENT_MIGRATION_FAILED", version),
|
|
116
|
+
ok: false
|
|
117
|
+
};
|
|
118
|
+
value = json.value;
|
|
119
|
+
}
|
|
120
|
+
const decoded = codec.decode(value);
|
|
121
|
+
return decoded.ok ? {
|
|
122
|
+
ok: true,
|
|
123
|
+
value: {
|
|
124
|
+
...component,
|
|
125
|
+
settings: decoded.value,
|
|
126
|
+
version: codec.version
|
|
127
|
+
}
|
|
128
|
+
} : {
|
|
129
|
+
error: codecError(decoded.error, component, "COMPONENT_DECODE_FAILED", component.version),
|
|
130
|
+
ok: false
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
class GridComponentRendererRegistry {
|
|
135
|
+
#renderers = new Map();
|
|
136
|
+
register(type, isSettings, render) {
|
|
137
|
+
if (this.#renderers.has(type)) throw new Error(`Grid component renderer "${type}" is already registered`);
|
|
138
|
+
this.#renderers.set(type, {
|
|
139
|
+
isSettings,
|
|
140
|
+
render: (settings, componentId)=>{
|
|
141
|
+
if (!isSettings(settings)) throw new Error(`Decoded settings do not match renderer "${type}"`);
|
|
142
|
+
return render(settings, componentId);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
render(component) {
|
|
147
|
+
const renderer = this.#renderers.get(component.type);
|
|
148
|
+
if (!renderer) throw new Error(`Grid component renderer "${component.type}" is not registered`);
|
|
149
|
+
return renderer.render(component.settings, component.id);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
class GridLayoutContentRegistry {
|
|
153
|
+
#entries = new Map();
|
|
154
|
+
entries() {
|
|
155
|
+
return new Map(this.#entries);
|
|
156
|
+
}
|
|
157
|
+
replace(components, resolve) {
|
|
158
|
+
const next = new Map();
|
|
159
|
+
for (const component of components)next.set(component.id, resolve(component));
|
|
160
|
+
this.#entries = next;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
export { GridComponentRendererRegistry, GridComponentSettingsRegistry, GridLayoutContentRegistry };
|