@dragcraft/core 0.0.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 +21 -0
- package/dist/index.d.mts +840 -0
- package/dist/index.mjs +2281 -0
- package/package.json +45 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2281 @@
|
|
|
1
|
+
import { readonly, ref, shallowRef } from "vue";
|
|
2
|
+
import { EventEmitter, cloneDeep, generateShortId } from "@dragcraft/utils";
|
|
3
|
+
//#region src/behavior.ts
|
|
4
|
+
/**
|
|
5
|
+
* Resolves a behavior field that may be a static boolean or a predicate function.
|
|
6
|
+
*
|
|
7
|
+
* - `undefined` → returns `defaultValue` (preserves opt-in defaults)
|
|
8
|
+
* - `boolean` → returns the boolean directly
|
|
9
|
+
* - `function` → invokes the function with the provided context
|
|
10
|
+
*
|
|
11
|
+
* @param field - The behavior field from WidgetMeta (may be undefined)
|
|
12
|
+
* @param ctx - The context to pass if field is a function
|
|
13
|
+
* @param defaultValue - Value when field is undefined (default: true)
|
|
14
|
+
*/
|
|
15
|
+
function resolveBehavior(field, ctx, defaultValue = true) {
|
|
16
|
+
if (field === void 0) return defaultValue;
|
|
17
|
+
if (typeof field === "function") return field(ctx);
|
|
18
|
+
return field;
|
|
19
|
+
}
|
|
20
|
+
function normalizeCreatableDecision(value, defaultValue) {
|
|
21
|
+
if (value === void 0) return { allowed: defaultValue };
|
|
22
|
+
if (typeof value === "boolean") return { allowed: value };
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
function resolveCreatable(field, ctx, defaultValue = true) {
|
|
26
|
+
return normalizeCreatableDecision(typeof field === "function" ? field(ctx) : field, defaultValue);
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/constants.ts
|
|
30
|
+
const CommandType = {
|
|
31
|
+
ADD_NODE: "ADD_NODE",
|
|
32
|
+
MOVE_NODE: "MOVE_NODE",
|
|
33
|
+
REMOVE_NODE: "REMOVE_NODE",
|
|
34
|
+
DUPLICATE_NODE: "DUPLICATE_NODE",
|
|
35
|
+
CHANGE_CONTAINER_VARIANT: "CHANGE_CONTAINER_VARIANT",
|
|
36
|
+
UPDATE_PROPS: "UPDATE_PROPS",
|
|
37
|
+
SET_GLOBAL_CONFIG: "SET_GLOBAL_CONFIG"
|
|
38
|
+
};
|
|
39
|
+
const EventName = {
|
|
40
|
+
SCHEMA_CHANGED: "schema:changed",
|
|
41
|
+
SELECTION_CHANGED: "selection:changed",
|
|
42
|
+
DRAG_ENTER: "drag:enter",
|
|
43
|
+
DRAG_OVER: "drag:over",
|
|
44
|
+
DRAG_LEAVE: "drag:leave",
|
|
45
|
+
DRAG_DROP: "drag:drop",
|
|
46
|
+
HISTORY_CHANGED: "history:changed",
|
|
47
|
+
NODE_ADDED: "node:added",
|
|
48
|
+
NODE_REMOVED: "node:removed",
|
|
49
|
+
NODE_DUPLICATED: "node:duplicated",
|
|
50
|
+
NODE_MOVED: "node:moved",
|
|
51
|
+
CONTAINER_VARIANT_CHANGED: "container:variant-changed",
|
|
52
|
+
NODE_UPDATED: "node:updated",
|
|
53
|
+
GLOBAL_CONFIG_CHANGED: "global-config:changed"
|
|
54
|
+
};
|
|
55
|
+
const DEFAULT_SCHEMA_VERSION = "1.0.0";
|
|
56
|
+
const DEFAULT_MAX_HISTORY_SIZE = 50;
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/schema-utils.ts
|
|
59
|
+
function cloneSchema(schema) {
|
|
60
|
+
return cloneDeep(schema);
|
|
61
|
+
}
|
|
62
|
+
function deepFreeze(value) {
|
|
63
|
+
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
64
|
+
Object.freeze(value);
|
|
65
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/history-manager.ts
|
|
71
|
+
const ownedSnapshotPushers = /* @__PURE__ */ new WeakMap();
|
|
72
|
+
function pushOwnedHistorySnapshot(history, label, before) {
|
|
73
|
+
const pushOwned = ownedSnapshotPushers.get(history);
|
|
74
|
+
if (pushOwned) pushOwned(label, before);
|
|
75
|
+
else history.pushSnapshot(label, before);
|
|
76
|
+
}
|
|
77
|
+
function createHistoryManager(store, eventHub, maxSize = 50) {
|
|
78
|
+
const undoStack = [];
|
|
79
|
+
const redoStack = [];
|
|
80
|
+
const state = ref({
|
|
81
|
+
canUndo: false,
|
|
82
|
+
canRedo: false,
|
|
83
|
+
undoCount: 0,
|
|
84
|
+
redoCount: 0
|
|
85
|
+
});
|
|
86
|
+
let inTransaction = false;
|
|
87
|
+
let transactionLabel = "";
|
|
88
|
+
let transactionSnapshot = null;
|
|
89
|
+
function resetTransaction() {
|
|
90
|
+
inTransaction = false;
|
|
91
|
+
transactionLabel = "";
|
|
92
|
+
transactionSnapshot = null;
|
|
93
|
+
}
|
|
94
|
+
function trimUndoStack() {
|
|
95
|
+
if (undoStack.length > maxSize) undoStack.splice(0, undoStack.length - maxSize);
|
|
96
|
+
}
|
|
97
|
+
function pushUndo(label, snapshot) {
|
|
98
|
+
undoStack.push({
|
|
99
|
+
label,
|
|
100
|
+
snapshot
|
|
101
|
+
});
|
|
102
|
+
trimUndoStack();
|
|
103
|
+
redoStack.length = 0;
|
|
104
|
+
}
|
|
105
|
+
function emitChange() {
|
|
106
|
+
const nextState = {
|
|
107
|
+
canUndo: canUndo(),
|
|
108
|
+
canRedo: canRedo(),
|
|
109
|
+
undoCount: undoStack.length,
|
|
110
|
+
redoCount: redoStack.length
|
|
111
|
+
};
|
|
112
|
+
state.value = nextState;
|
|
113
|
+
eventHub.emit(EventName.HISTORY_CHANGED, nextState);
|
|
114
|
+
}
|
|
115
|
+
function pushOwnedSnapshot(label, before) {
|
|
116
|
+
if (inTransaction) return;
|
|
117
|
+
pushUndo(label, before);
|
|
118
|
+
emitChange();
|
|
119
|
+
}
|
|
120
|
+
function pushSnapshot(label, before) {
|
|
121
|
+
if (inTransaction) return;
|
|
122
|
+
pushOwnedSnapshot(label, deepFreeze(cloneSchema(before)));
|
|
123
|
+
}
|
|
124
|
+
function undo() {
|
|
125
|
+
if (!canUndo()) return;
|
|
126
|
+
const entry = undoStack.pop();
|
|
127
|
+
const currentSnapshot = store.getSnapshot();
|
|
128
|
+
redoStack.push({
|
|
129
|
+
label: entry.label,
|
|
130
|
+
snapshot: currentSnapshot
|
|
131
|
+
});
|
|
132
|
+
store.restoreSnapshot(entry.snapshot);
|
|
133
|
+
emitChange();
|
|
134
|
+
eventHub.emit(EventName.SCHEMA_CHANGED, store.getSnapshot());
|
|
135
|
+
}
|
|
136
|
+
function redo() {
|
|
137
|
+
if (!canRedo()) return;
|
|
138
|
+
const entry = redoStack.pop();
|
|
139
|
+
const currentSnapshot = store.getSnapshot();
|
|
140
|
+
undoStack.push({
|
|
141
|
+
label: entry.label,
|
|
142
|
+
snapshot: currentSnapshot
|
|
143
|
+
});
|
|
144
|
+
trimUndoStack();
|
|
145
|
+
store.restoreSnapshot(entry.snapshot);
|
|
146
|
+
emitChange();
|
|
147
|
+
eventHub.emit(EventName.SCHEMA_CHANGED, store.getSnapshot());
|
|
148
|
+
}
|
|
149
|
+
function canUndo() {
|
|
150
|
+
return undoStack.length > 0;
|
|
151
|
+
}
|
|
152
|
+
function canRedo() {
|
|
153
|
+
return redoStack.length > 0;
|
|
154
|
+
}
|
|
155
|
+
function beginTransaction(label) {
|
|
156
|
+
if (inTransaction) {
|
|
157
|
+
console.warn("[dragcraft/core] Transaction already in progress, ignoring beginTransaction");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
inTransaction = true;
|
|
161
|
+
transactionLabel = label ?? "transaction";
|
|
162
|
+
transactionSnapshot = store.getSnapshot();
|
|
163
|
+
}
|
|
164
|
+
function commitTransaction() {
|
|
165
|
+
if (!inTransaction) {
|
|
166
|
+
console.warn("[dragcraft/core] No transaction in progress, ignoring commitTransaction");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (transactionSnapshot && transactionSnapshot !== store.getSnapshot()) pushUndo(transactionLabel, transactionSnapshot);
|
|
170
|
+
resetTransaction();
|
|
171
|
+
emitChange();
|
|
172
|
+
}
|
|
173
|
+
function discardTransaction() {
|
|
174
|
+
if (!inTransaction) {
|
|
175
|
+
console.warn("[dragcraft/core] No transaction in progress, ignoring discardTransaction");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const restored = transactionSnapshot !== null && transactionSnapshot !== store.getSnapshot();
|
|
179
|
+
if (restored) store.restoreSnapshot(transactionSnapshot);
|
|
180
|
+
resetTransaction();
|
|
181
|
+
emitChange();
|
|
182
|
+
if (restored) eventHub.emit(EventName.SCHEMA_CHANGED, store.getSnapshot());
|
|
183
|
+
}
|
|
184
|
+
function isInTransaction() {
|
|
185
|
+
return inTransaction;
|
|
186
|
+
}
|
|
187
|
+
function clear() {
|
|
188
|
+
undoStack.length = 0;
|
|
189
|
+
redoStack.length = 0;
|
|
190
|
+
resetTransaction();
|
|
191
|
+
emitChange();
|
|
192
|
+
}
|
|
193
|
+
const history = {
|
|
194
|
+
state,
|
|
195
|
+
pushSnapshot,
|
|
196
|
+
undo,
|
|
197
|
+
redo,
|
|
198
|
+
canUndo,
|
|
199
|
+
canRedo,
|
|
200
|
+
beginTransaction,
|
|
201
|
+
commitTransaction,
|
|
202
|
+
discardTransaction,
|
|
203
|
+
isInTransaction,
|
|
204
|
+
clear
|
|
205
|
+
};
|
|
206
|
+
ownedSnapshotPushers.set(history, pushOwnedSnapshot);
|
|
207
|
+
return history;
|
|
208
|
+
}
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/command-bus.ts
|
|
211
|
+
const COMMAND_EVENT_MAP = {
|
|
212
|
+
[CommandType.ADD_NODE]: EventName.NODE_ADDED,
|
|
213
|
+
[CommandType.MOVE_NODE]: EventName.NODE_MOVED,
|
|
214
|
+
[CommandType.REMOVE_NODE]: EventName.NODE_REMOVED,
|
|
215
|
+
[CommandType.DUPLICATE_NODE]: EventName.NODE_DUPLICATED,
|
|
216
|
+
[CommandType.CHANGE_CONTAINER_VARIANT]: EventName.CONTAINER_VARIANT_CHANGED,
|
|
217
|
+
[CommandType.UPDATE_PROPS]: EventName.NODE_UPDATED,
|
|
218
|
+
[CommandType.SET_GLOBAL_CONFIG]: EventName.GLOBAL_CONFIG_CHANGED
|
|
219
|
+
};
|
|
220
|
+
function normalizeHandlerResult(result) {
|
|
221
|
+
if (result === false) return {
|
|
222
|
+
ok: false,
|
|
223
|
+
code: "COMMAND_REJECTED"
|
|
224
|
+
};
|
|
225
|
+
if (!result) return {
|
|
226
|
+
ok: true,
|
|
227
|
+
changed: true
|
|
228
|
+
};
|
|
229
|
+
if (!result.ok) return result;
|
|
230
|
+
return {
|
|
231
|
+
...result,
|
|
232
|
+
changed: result.changed ?? true
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function createCommandBus(store, registry, eventHub, history) {
|
|
236
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
237
|
+
let executing = false;
|
|
238
|
+
const interactionStore = {
|
|
239
|
+
selectedNodeId: readonly(store.selectedNodeId),
|
|
240
|
+
hoveredNodeId: readonly(store.hoveredNodeId),
|
|
241
|
+
selectNode: store.selectNode,
|
|
242
|
+
hoverNode: store.hoverNode
|
|
243
|
+
};
|
|
244
|
+
function registerHandler(type, handler) {
|
|
245
|
+
handlers.set(type, handler);
|
|
246
|
+
}
|
|
247
|
+
function execute(command) {
|
|
248
|
+
if (executing) return {
|
|
249
|
+
ok: false,
|
|
250
|
+
code: "COMMAND_REENTRANT"
|
|
251
|
+
};
|
|
252
|
+
executing = true;
|
|
253
|
+
try {
|
|
254
|
+
const handler = handlers.get(command.type);
|
|
255
|
+
if (!handler) {
|
|
256
|
+
console.warn(`[dragcraft/core] No handler registered for command type: "${command.type}"`);
|
|
257
|
+
return {
|
|
258
|
+
ok: false,
|
|
259
|
+
code: "COMMAND_HANDLER_MISSING"
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const beforeSnapshot = store.getSnapshot();
|
|
263
|
+
const draft = cloneSchema(beforeSnapshot);
|
|
264
|
+
const ctx = {
|
|
265
|
+
schema: beforeSnapshot,
|
|
266
|
+
draft,
|
|
267
|
+
store: interactionStore,
|
|
268
|
+
registry
|
|
269
|
+
};
|
|
270
|
+
let result;
|
|
271
|
+
try {
|
|
272
|
+
result = handler(ctx, command.payload);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
console.error(`[dragcraft/core] Command "${command.type}" failed, discarding draft:`, error);
|
|
275
|
+
return {
|
|
276
|
+
ok: false,
|
|
277
|
+
code: "COMMAND_HANDLER_FAILED",
|
|
278
|
+
message: error instanceof Error ? error.message : String(error)
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
const normalized = normalizeHandlerResult(result);
|
|
282
|
+
if (!normalized.ok) return normalized;
|
|
283
|
+
if (!normalized.changed) return normalized;
|
|
284
|
+
const snapshot = store.commitSchema(draft);
|
|
285
|
+
pushOwnedHistorySnapshot(history, command.type, beforeSnapshot);
|
|
286
|
+
const specificEvent = COMMAND_EVENT_MAP[command.type];
|
|
287
|
+
if (specificEvent) eventHub.emit(specificEvent, normalized.eventPayload ?? command.payload);
|
|
288
|
+
eventHub.emit(EventName.SCHEMA_CHANGED, snapshot);
|
|
289
|
+
return normalized;
|
|
290
|
+
} finally {
|
|
291
|
+
executing = false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
execute,
|
|
296
|
+
registerHandler
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
//#endregion
|
|
300
|
+
//#region src/schema-index.ts
|
|
301
|
+
function buildSchemaIndex(schema) {
|
|
302
|
+
const index = /* @__PURE__ */ new Map();
|
|
303
|
+
const diagnostics = [];
|
|
304
|
+
const add = (node, location) => {
|
|
305
|
+
if (node.id === schema.root.id) diagnostics.push({
|
|
306
|
+
code: "SCHEMA_NODE_ID_DUPLICATE",
|
|
307
|
+
severity: "error",
|
|
308
|
+
nodeId: node.id,
|
|
309
|
+
...location.owner === "root" ? {} : {
|
|
310
|
+
ownerId: location.owner,
|
|
311
|
+
regionId: location.regionId
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
if (index.has(node.id)) diagnostics.push({
|
|
315
|
+
code: "SCHEMA_NODE_ID_DUPLICATE",
|
|
316
|
+
severity: "error",
|
|
317
|
+
nodeId: node.id
|
|
318
|
+
});
|
|
319
|
+
else index.set(node.id, {
|
|
320
|
+
node,
|
|
321
|
+
...location
|
|
322
|
+
});
|
|
323
|
+
};
|
|
324
|
+
for (const [rootIndex, node] of (schema.root.children ?? []).entries()) {
|
|
325
|
+
add(node, {
|
|
326
|
+
owner: "root",
|
|
327
|
+
index: rootIndex,
|
|
328
|
+
depth: 1
|
|
329
|
+
});
|
|
330
|
+
for (const [regionId, children] of Object.entries(node.container?.regions ?? {})) for (const [childIndex, child] of children.entries()) {
|
|
331
|
+
add(child, {
|
|
332
|
+
owner: node.id,
|
|
333
|
+
regionId,
|
|
334
|
+
index: childIndex,
|
|
335
|
+
depth: 2
|
|
336
|
+
});
|
|
337
|
+
if (child.container) diagnostics.push({
|
|
338
|
+
code: "SCHEMA_CONTAINER_NESTED",
|
|
339
|
+
severity: "error",
|
|
340
|
+
nodeId: child.id,
|
|
341
|
+
ownerId: node.id,
|
|
342
|
+
regionId
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
index,
|
|
348
|
+
diagnostics
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function findIndexedNode(result, nodeId) {
|
|
352
|
+
return result.index.get(nodeId);
|
|
353
|
+
}
|
|
354
|
+
//#endregion
|
|
355
|
+
//#region src/schema-validation.ts
|
|
356
|
+
function isRecord$2(value) {
|
|
357
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
358
|
+
const prototype = Object.getPrototypeOf(value);
|
|
359
|
+
return prototype === Object.prototype || prototype === null;
|
|
360
|
+
}
|
|
361
|
+
function collectSchemaStructuralDiagnostics(input) {
|
|
362
|
+
const diagnostics = [];
|
|
363
|
+
if (!isRecord$2(input) || typeof input.version !== "string" || input.version.length === 0 || !Object.hasOwn(input, "root") || !input.root) {
|
|
364
|
+
diagnostics.push({
|
|
365
|
+
code: "SCHEMA_ENVELOPE_INVALID",
|
|
366
|
+
severity: "error"
|
|
367
|
+
});
|
|
368
|
+
return diagnostics;
|
|
369
|
+
}
|
|
370
|
+
if (!isRecord$2(input.globalConfig)) {
|
|
371
|
+
diagnostics.push({
|
|
372
|
+
code: "SCHEMA_GLOBAL_CONFIG_INVALID",
|
|
373
|
+
severity: "error",
|
|
374
|
+
path: "globalConfig"
|
|
375
|
+
});
|
|
376
|
+
return diagnostics;
|
|
377
|
+
}
|
|
378
|
+
const root = input.root;
|
|
379
|
+
if (!isRecord$2(root) || typeof root.id !== "string" || typeof root.type !== "string" || !isRecord$2(root.props)) {
|
|
380
|
+
diagnostics.push({
|
|
381
|
+
code: "SCHEMA_ROOT_INVALID",
|
|
382
|
+
severity: "error",
|
|
383
|
+
path: "root"
|
|
384
|
+
});
|
|
385
|
+
return diagnostics;
|
|
386
|
+
}
|
|
387
|
+
const inspectNode = (value, path, ownerId, regionId) => {
|
|
388
|
+
if (!isRecord$2(value) || typeof value.id !== "string" || typeof value.type !== "string" || !isRecord$2(value.props)) {
|
|
389
|
+
diagnostics.push({
|
|
390
|
+
code: "SCHEMA_NODE_INVALID",
|
|
391
|
+
severity: "error",
|
|
392
|
+
path,
|
|
393
|
+
nodeId: isRecord$2(value) && typeof value.id === "string" ? value.id : void 0,
|
|
394
|
+
ownerId,
|
|
395
|
+
regionId
|
|
396
|
+
});
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const nodeId = value.id;
|
|
400
|
+
if (value.container === void 0) return;
|
|
401
|
+
if (!isRecord$2(value.container) || typeof value.container.variant !== "string") {
|
|
402
|
+
diagnostics.push({
|
|
403
|
+
code: "CONTAINER_STATE_INVALID",
|
|
404
|
+
severity: "error",
|
|
405
|
+
nodeId,
|
|
406
|
+
path: `${path}.container`
|
|
407
|
+
});
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (!isRecord$2(value.container.regions)) {
|
|
411
|
+
diagnostics.push({
|
|
412
|
+
code: "CONTAINER_REGIONS_INVALID",
|
|
413
|
+
severity: "error",
|
|
414
|
+
nodeId,
|
|
415
|
+
path: `${path}.container.regions`
|
|
416
|
+
});
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
for (const [childRegionId, children] of Object.entries(value.container.regions)) {
|
|
420
|
+
if (!Array.isArray(children)) {
|
|
421
|
+
diagnostics.push({
|
|
422
|
+
code: "CONTAINER_REGION_CHILDREN_INVALID",
|
|
423
|
+
severity: "error",
|
|
424
|
+
nodeId,
|
|
425
|
+
regionId: childRegionId,
|
|
426
|
+
path: `${path}.container.regions.${childRegionId}`
|
|
427
|
+
});
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
children.forEach((child, index) => inspectNode(child, `${path}.container.regions.${childRegionId}.${index}`, nodeId, childRegionId));
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
if (root.children !== void 0 && !Array.isArray(root.children)) {
|
|
434
|
+
diagnostics.push({
|
|
435
|
+
code: "SCHEMA_CHILDREN_INVALID",
|
|
436
|
+
severity: "error",
|
|
437
|
+
path: "root.children"
|
|
438
|
+
});
|
|
439
|
+
return diagnostics;
|
|
440
|
+
}
|
|
441
|
+
for (const [index, node] of (root.children ?? []).entries()) inspectNode(node, `root.children.${index}`);
|
|
442
|
+
return diagnostics;
|
|
443
|
+
}
|
|
444
|
+
function validateSchema(input, registry) {
|
|
445
|
+
const schema = cloneSchema(input);
|
|
446
|
+
const structuralDiagnostics = collectSchemaStructuralDiagnostics(schema);
|
|
447
|
+
if (structuralDiagnostics.length > 0) return {
|
|
448
|
+
valid: false,
|
|
449
|
+
schema,
|
|
450
|
+
diagnostics: structuralDiagnostics
|
|
451
|
+
};
|
|
452
|
+
const diagnostics = [...buildSchemaIndex(schema).diagnostics];
|
|
453
|
+
for (const node of schema.root.children ?? []) {
|
|
454
|
+
for (const [regionId, children] of Object.entries(node.container?.regions ?? {})) for (const child of children) {
|
|
455
|
+
if (registry.getWidget(child.type)?.container && !child.container) diagnostics.push({
|
|
456
|
+
code: "SCHEMA_CONTAINER_NESTED",
|
|
457
|
+
severity: "error",
|
|
458
|
+
nodeId: child.id,
|
|
459
|
+
ownerId: node.id,
|
|
460
|
+
regionId
|
|
461
|
+
});
|
|
462
|
+
if (child.layout?.placement !== void 0 || child.layout?.order !== void 0) diagnostics.push({
|
|
463
|
+
code: "CONTAINER_CHILD_PAGE_LAYOUT_FORBIDDEN",
|
|
464
|
+
severity: "error",
|
|
465
|
+
nodeId: child.id,
|
|
466
|
+
ownerId: node.id,
|
|
467
|
+
regionId
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
const meta = registry.getWidget(node.type);
|
|
471
|
+
if (node.container && !meta) {
|
|
472
|
+
diagnostics.push({
|
|
473
|
+
code: "UNRESOLVED_CONTAINER_TYPE",
|
|
474
|
+
severity: "warning",
|
|
475
|
+
nodeId: node.id
|
|
476
|
+
});
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (node.container && !meta?.container) {
|
|
480
|
+
diagnostics.push({
|
|
481
|
+
code: "CONTAINER_CAPABILITY_MISMATCH",
|
|
482
|
+
severity: "error",
|
|
483
|
+
nodeId: node.id
|
|
484
|
+
});
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (!node.container && meta?.container) {
|
|
488
|
+
diagnostics.push({
|
|
489
|
+
code: "CONTAINER_STATE_MISSING",
|
|
490
|
+
severity: "error",
|
|
491
|
+
nodeId: node.id
|
|
492
|
+
});
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (!node.container || !meta?.container) continue;
|
|
496
|
+
const variant = meta.container.variants[node.container.variant];
|
|
497
|
+
if (!variant) {
|
|
498
|
+
diagnostics.push({
|
|
499
|
+
code: "CONTAINER_VARIANT_UNKNOWN",
|
|
500
|
+
severity: "error",
|
|
501
|
+
nodeId: node.id
|
|
502
|
+
});
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
const knownRegionIds = new Set(variant.regions.map((region) => region.id));
|
|
506
|
+
for (const regionId of Object.keys(node.container.regions)) if (!knownRegionIds.has(regionId)) diagnostics.push({
|
|
507
|
+
code: "CONTAINER_REGION_UNKNOWN",
|
|
508
|
+
severity: "error",
|
|
509
|
+
nodeId: node.id,
|
|
510
|
+
regionId
|
|
511
|
+
});
|
|
512
|
+
for (const region of variant.regions) node.container.regions[region.id] ??= [];
|
|
513
|
+
for (const region of variant.regions) {
|
|
514
|
+
const children = node.container.regions[region.id];
|
|
515
|
+
const { minItems = 0, maxItems = Number.POSITIVE_INFINITY } = region.constraints ?? {};
|
|
516
|
+
if (children.length < minItems) diagnostics.push({
|
|
517
|
+
code: "CONTAINER_REGION_MIN_ITEMS",
|
|
518
|
+
severity: "error",
|
|
519
|
+
nodeId: node.id,
|
|
520
|
+
regionId: region.id,
|
|
521
|
+
details: {
|
|
522
|
+
actual: children.length,
|
|
523
|
+
minItems
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
if (children.length > maxItems) diagnostics.push({
|
|
527
|
+
code: "CONTAINER_REGION_MAX_ITEMS",
|
|
528
|
+
severity: "error",
|
|
529
|
+
nodeId: node.id,
|
|
530
|
+
regionId: region.id,
|
|
531
|
+
details: {
|
|
532
|
+
actual: children.length,
|
|
533
|
+
maxItems
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
for (const child of children) {
|
|
537
|
+
if (region.constraints?.includeTypes && !region.constraints.includeTypes.includes(child.type)) diagnostics.push({
|
|
538
|
+
code: "CONTAINER_TYPE_NOT_INCLUDED",
|
|
539
|
+
severity: "error",
|
|
540
|
+
nodeId: child.id,
|
|
541
|
+
ownerId: node.id,
|
|
542
|
+
regionId: region.id
|
|
543
|
+
});
|
|
544
|
+
if (region.constraints?.excludeTypes?.includes(child.type)) diagnostics.push({
|
|
545
|
+
code: "CONTAINER_TYPE_EXCLUDED",
|
|
546
|
+
severity: "error",
|
|
547
|
+
nodeId: child.id,
|
|
548
|
+
ownerId: node.id,
|
|
549
|
+
regionId: region.id
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return {
|
|
555
|
+
valid: diagnostics.every((item) => item.severity !== "error"),
|
|
556
|
+
schema,
|
|
557
|
+
diagnostics
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
//#endregion
|
|
561
|
+
//#region src/container-placement.ts
|
|
562
|
+
function placementFailureDetails(ctx) {
|
|
563
|
+
return {
|
|
564
|
+
nodeId: ctx.child.id,
|
|
565
|
+
containerId: ctx.callbackContext.container.id,
|
|
566
|
+
regionId: ctx.region.id
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
function resolvePlacementDecision(ctx) {
|
|
570
|
+
if (ctx.child.container || ctx.childHasContainerCapability) return {
|
|
571
|
+
allowed: false,
|
|
572
|
+
code: "CONTAINER_NESTING_FORBIDDEN"
|
|
573
|
+
};
|
|
574
|
+
const constraints = ctx.region.constraints ?? {};
|
|
575
|
+
if (constraints.includeTypes && !constraints.includeTypes.includes(ctx.child.type)) return {
|
|
576
|
+
allowed: false,
|
|
577
|
+
code: "CONTAINER_TYPE_NOT_INCLUDED"
|
|
578
|
+
};
|
|
579
|
+
if (constraints.excludeTypes?.includes(ctx.child.type)) return {
|
|
580
|
+
allowed: false,
|
|
581
|
+
code: "CONTAINER_TYPE_EXCLUDED"
|
|
582
|
+
};
|
|
583
|
+
if (ctx.targetCount + 1 > (constraints.maxItems ?? Number.POSITIVE_INFINITY)) return {
|
|
584
|
+
allowed: false,
|
|
585
|
+
code: "CONTAINER_REGION_MAX_ITEMS"
|
|
586
|
+
};
|
|
587
|
+
try {
|
|
588
|
+
const decision = cloneDeep(ctx.definition.canPlace?.(cloneDeep(ctx.callbackContext)) ?? { allowed: true });
|
|
589
|
+
if (!decision || typeof decision.allowed !== "boolean") return {
|
|
590
|
+
allowed: false,
|
|
591
|
+
code: "CONTAINER_PLACEMENT_PREDICATE_INVALID",
|
|
592
|
+
details: placementFailureDetails(ctx)
|
|
593
|
+
};
|
|
594
|
+
return decision;
|
|
595
|
+
} catch (error) {
|
|
596
|
+
return {
|
|
597
|
+
allowed: false,
|
|
598
|
+
code: "CONTAINER_PLACEMENT_PREDICATE_FAILED",
|
|
599
|
+
message: error instanceof Error ? error.message : String(error),
|
|
600
|
+
details: placementFailureDetails(ctx)
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function isContainerState$1(value) {
|
|
605
|
+
if (!value || typeof value !== "object") return false;
|
|
606
|
+
const candidate = value;
|
|
607
|
+
return typeof candidate.variant === "string" && Boolean(candidate.regions) && typeof candidate.regions === "object" && !Array.isArray(candidate.regions) && Object.values(candidate.regions).every(Array.isArray);
|
|
608
|
+
}
|
|
609
|
+
function buildCandidateIndex(schema, candidateNode) {
|
|
610
|
+
return buildSchemaIndex({
|
|
611
|
+
version: schema.version,
|
|
612
|
+
globalConfig: {},
|
|
613
|
+
root: {
|
|
614
|
+
id: schema.root.id,
|
|
615
|
+
type: schema.root.type,
|
|
616
|
+
props: {},
|
|
617
|
+
children: [candidateNode]
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
function createContainerState(node, schema, registry, createNode) {
|
|
622
|
+
const definition = registry.getWidget(node.type)?.container;
|
|
623
|
+
if (!definition) return {
|
|
624
|
+
ok: false,
|
|
625
|
+
code: "CONTAINER_DEFINITION_MISSING"
|
|
626
|
+
};
|
|
627
|
+
const variant = definition.variants[definition.defaultVariant];
|
|
628
|
+
if (!variant) return {
|
|
629
|
+
ok: false,
|
|
630
|
+
code: "CONTAINER_DEFAULT_VARIANT_MISSING"
|
|
631
|
+
};
|
|
632
|
+
const emptyState = {
|
|
633
|
+
variant: definition.defaultVariant,
|
|
634
|
+
regions: Object.fromEntries(variant.regions.map((region) => [region.id, []]))
|
|
635
|
+
};
|
|
636
|
+
let state = emptyState;
|
|
637
|
+
try {
|
|
638
|
+
state = cloneDeep(definition.createInitialState ? definition.createInitialState({
|
|
639
|
+
containerNode: cloneDeep(node),
|
|
640
|
+
schema: cloneSchema(schema),
|
|
641
|
+
createNode
|
|
642
|
+
}) : emptyState);
|
|
643
|
+
} catch (error) {
|
|
644
|
+
return {
|
|
645
|
+
ok: false,
|
|
646
|
+
code: "CONTAINER_INITIAL_STATE_FAILED",
|
|
647
|
+
message: error instanceof Error ? error.message : String(error),
|
|
648
|
+
details: {
|
|
649
|
+
nodeId: node.id,
|
|
650
|
+
containerId: node.id
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
if (!isContainerState$1(state)) return {
|
|
655
|
+
ok: false,
|
|
656
|
+
code: "CONTAINER_INITIAL_STATE_INVALID",
|
|
657
|
+
details: {
|
|
658
|
+
nodeId: node.id,
|
|
659
|
+
containerId: node.id,
|
|
660
|
+
diagnostics: []
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
const candidateNode = cloneDeep(node);
|
|
664
|
+
candidateNode.container = state;
|
|
665
|
+
try {
|
|
666
|
+
const candidateIndex = buildCandidateIndex(schema, candidateNode);
|
|
667
|
+
const candidateNodeIds = new Set(candidateIndex.index.keys());
|
|
668
|
+
const candidateSchema = cloneSchema(schema);
|
|
669
|
+
candidateSchema.root.children ??= [];
|
|
670
|
+
candidateSchema.root.children.push(candidateNode);
|
|
671
|
+
const ownDiagnostics = validateSchema(candidateSchema, registry).diagnostics.filter((diagnostic) => diagnostic.nodeId === node.id || diagnostic.ownerId === node.id || diagnostic.nodeId !== void 0 && candidateNodeIds.has(diagnostic.nodeId));
|
|
672
|
+
if (ownDiagnostics.some((diagnostic) => diagnostic.severity === "error")) return {
|
|
673
|
+
ok: false,
|
|
674
|
+
code: "CONTAINER_INITIAL_STATE_INVALID",
|
|
675
|
+
details: {
|
|
676
|
+
nodeId: node.id,
|
|
677
|
+
containerId: node.id,
|
|
678
|
+
diagnostics: ownDiagnostics
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
} catch (error) {
|
|
682
|
+
return {
|
|
683
|
+
ok: false,
|
|
684
|
+
code: "CONTAINER_INITIAL_STATE_INVALID",
|
|
685
|
+
message: error instanceof Error ? error.message : String(error),
|
|
686
|
+
details: {
|
|
687
|
+
nodeId: node.id,
|
|
688
|
+
containerId: node.id,
|
|
689
|
+
diagnostics: []
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
return {
|
|
694
|
+
ok: true,
|
|
695
|
+
state: cloneDeep(state)
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
function createRegisteredNode(registry, createId = generateShortId) {
|
|
699
|
+
return (type, overrides = {}) => {
|
|
700
|
+
const meta = registry.getWidget(type);
|
|
701
|
+
if (!meta) throw new Error(`Cannot initialize unregistered widget type: ${type}`);
|
|
702
|
+
const layout = overrides.layout !== void 0 ? cloneDeep(overrides.layout) : cloneDeep(meta.defaultLayout);
|
|
703
|
+
if (layout) {
|
|
704
|
+
delete layout.placement;
|
|
705
|
+
delete layout.order;
|
|
706
|
+
}
|
|
707
|
+
return {
|
|
708
|
+
id: createId(),
|
|
709
|
+
type,
|
|
710
|
+
props: {
|
|
711
|
+
...cloneDeep(meta.defaultProps),
|
|
712
|
+
...cloneDeep(overrides.props ?? {})
|
|
713
|
+
},
|
|
714
|
+
style: overrides.style !== void 0 ? cloneDeep(overrides.style) : cloneDeep(meta.defaultStyle),
|
|
715
|
+
layout
|
|
716
|
+
};
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
//#endregion
|
|
720
|
+
//#region src/helpers.ts
|
|
721
|
+
function cloneNodeSubtree(node, createId) {
|
|
722
|
+
const clone = cloneDeep(node);
|
|
723
|
+
clone.id = createId();
|
|
724
|
+
for (const children of Object.values(clone.container?.regions ?? {})) for (let index = 0; index < children.length; index++) children[index] = cloneNodeSubtree(children[index], createId);
|
|
725
|
+
return clone;
|
|
726
|
+
}
|
|
727
|
+
function collectSubtreeIds(node) {
|
|
728
|
+
const ids = /* @__PURE__ */ new Set([node.id]);
|
|
729
|
+
for (const children of Object.values(node.container?.regions ?? {})) for (const child of children) for (const id of collectSubtreeIds(child)) ids.add(id);
|
|
730
|
+
return ids;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Find a node by ID in the shallow ownership structure.
|
|
734
|
+
*/
|
|
735
|
+
function findNodeById(root, id) {
|
|
736
|
+
if (root.id === id) return root;
|
|
737
|
+
return buildSchemaIndex({
|
|
738
|
+
version: "",
|
|
739
|
+
globalConfig: {},
|
|
740
|
+
root
|
|
741
|
+
}).index.get(id)?.node ?? null;
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Find the parent node of a given node ID.
|
|
745
|
+
* Returns the owning root or container location, or null if not found.
|
|
746
|
+
*/
|
|
747
|
+
function findParentNode(root, targetId) {
|
|
748
|
+
const indexed = buildSchemaIndex({
|
|
749
|
+
version: "",
|
|
750
|
+
globalConfig: {},
|
|
751
|
+
root
|
|
752
|
+
});
|
|
753
|
+
const location = indexed.index.get(targetId);
|
|
754
|
+
if (!location) return null;
|
|
755
|
+
if (location.owner === "root") return {
|
|
756
|
+
parent: root,
|
|
757
|
+
index: location.index
|
|
758
|
+
};
|
|
759
|
+
const parent = indexed.index.get(location.owner)?.node;
|
|
760
|
+
return parent && location.regionId ? {
|
|
761
|
+
parent,
|
|
762
|
+
regionId: location.regionId,
|
|
763
|
+
index: location.index
|
|
764
|
+
} : null;
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Remove a node from the tree by ID.
|
|
768
|
+
* Returns the removed node or null if not found.
|
|
769
|
+
*/
|
|
770
|
+
function removeNodeFromTree(root, nodeId) {
|
|
771
|
+
const result = findParentNode(root, nodeId);
|
|
772
|
+
if (!result || result.regionId) return null;
|
|
773
|
+
const { parent, index } = result;
|
|
774
|
+
return parent.children.splice(index, 1)[0];
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Insert a node into a parent's children array at a given index.
|
|
778
|
+
* If index is undefined, appends to the end.
|
|
779
|
+
*/
|
|
780
|
+
function insertNodeIntoTree(parent, node, index) {
|
|
781
|
+
if (!parent.children) parent.children = [];
|
|
782
|
+
if (index !== void 0 && index >= 0 && index <= parent.children.length) parent.children.splice(index, 0, node);
|
|
783
|
+
else parent.children.push(node);
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Walk root and its direct children (one level), calling visitor for each node.
|
|
787
|
+
* If visitor returns false, stop traversal.
|
|
788
|
+
*
|
|
789
|
+
* Note: This does NOT recurse into grandchildren — the schema is a flat
|
|
790
|
+
* two-level structure (root → widgets).
|
|
791
|
+
*/
|
|
792
|
+
function walkFlatChildren(root, visitor) {
|
|
793
|
+
if (visitor(root) === false) return;
|
|
794
|
+
if (root.children) {
|
|
795
|
+
for (const child of root.children) if (visitor(child) === false) return;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
//#endregion
|
|
799
|
+
//#region src/layout.ts
|
|
800
|
+
const DEFAULT_LAYOUT_REGION = "content";
|
|
801
|
+
const DEFAULT_SORT_SCOPE = "content";
|
|
802
|
+
const DEFAULT_LAYER = "float";
|
|
803
|
+
function resolveFlowPlacement(placement) {
|
|
804
|
+
const region = placement?.region ?? "content";
|
|
805
|
+
return {
|
|
806
|
+
kind: "flow",
|
|
807
|
+
region,
|
|
808
|
+
sortScope: placement?.sortScope === void 0 ? region === "content" ? DEFAULT_SORT_SCOPE : false : placement.sortScope
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
function resolveChromePlacement(placement) {
|
|
812
|
+
return {
|
|
813
|
+
kind: "chrome",
|
|
814
|
+
edge: placement.edge,
|
|
815
|
+
position: placement.position ?? "fixed",
|
|
816
|
+
reserve: {
|
|
817
|
+
mode: placement.reserve?.mode ?? "measure",
|
|
818
|
+
size: placement.reserve?.size
|
|
819
|
+
},
|
|
820
|
+
avoidContent: placement.avoidContent ?? true
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
function resolveLayerPlacement(placement) {
|
|
824
|
+
const hasAnchor = placement.anchor !== void 0;
|
|
825
|
+
return {
|
|
826
|
+
kind: "layer",
|
|
827
|
+
layer: placement.layer ?? "float",
|
|
828
|
+
mode: placement.mode ?? (hasAnchor ? "framework" : "self"),
|
|
829
|
+
anchor: placement.anchor ?? {
|
|
830
|
+
block: "end",
|
|
831
|
+
inline: "end"
|
|
832
|
+
},
|
|
833
|
+
offset: placement.offset,
|
|
834
|
+
avoid: placement.avoid ?? ["safe-area", "chrome"]
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
function resolvePlacement(placement) {
|
|
838
|
+
if (!placement || placement.kind === "flow") return resolveFlowPlacement(placement);
|
|
839
|
+
if (placement.kind === "chrome") return resolveChromePlacement(placement);
|
|
840
|
+
return resolveLayerPlacement(placement);
|
|
841
|
+
}
|
|
842
|
+
function resolveNodeLayout(node, registry, schema) {
|
|
843
|
+
const layout = {
|
|
844
|
+
...registry.getWidget(node.type)?.defaultLayout ?? {},
|
|
845
|
+
...node.layout ?? {}
|
|
846
|
+
};
|
|
847
|
+
const placement = resolvePlacement(layout.placement);
|
|
848
|
+
const rawVisible = layout.visible ?? true;
|
|
849
|
+
const visible = typeof rawVisible === "function" ? schema !== void 0 && rawVisible({
|
|
850
|
+
node,
|
|
851
|
+
schema
|
|
852
|
+
}) : rawVisible;
|
|
853
|
+
return {
|
|
854
|
+
placement,
|
|
855
|
+
region: placement.kind === "flow" ? placement.region : void 0,
|
|
856
|
+
sortScope: placement.kind === "flow" ? placement.sortScope : false,
|
|
857
|
+
order: layout.order,
|
|
858
|
+
visible
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
function sortEntries(entries) {
|
|
862
|
+
entries.sort((a, b) => {
|
|
863
|
+
const orderA = a.layout.order ?? a.arrayIndex;
|
|
864
|
+
const orderB = b.layout.order ?? b.arrayIndex;
|
|
865
|
+
if (orderA !== orderB) return orderA - orderB;
|
|
866
|
+
return a.arrayIndex - b.arrayIndex;
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
function pushEntry(map, key, entry) {
|
|
870
|
+
const entries = map.get(key);
|
|
871
|
+
if (entries) entries.push(entry);
|
|
872
|
+
else map.set(key, [entry]);
|
|
873
|
+
}
|
|
874
|
+
function edgeOrder(edge) {
|
|
875
|
+
switch (edge) {
|
|
876
|
+
case "block-start": return 0;
|
|
877
|
+
case "inline-start": return 1;
|
|
878
|
+
case "inline-end": return 2;
|
|
879
|
+
case "block-end": return 3;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
function createLayoutPlan(schema, registry) {
|
|
883
|
+
const children = schema.root.children ?? [];
|
|
884
|
+
const entries = [];
|
|
885
|
+
const regions = /* @__PURE__ */ new Map();
|
|
886
|
+
const chrome = [];
|
|
887
|
+
const layers = /* @__PURE__ */ new Map();
|
|
888
|
+
const sortScopes = /* @__PURE__ */ new Map();
|
|
889
|
+
for (let arrayIndex = 0; arrayIndex < children.length; arrayIndex++) {
|
|
890
|
+
const node = children[arrayIndex];
|
|
891
|
+
const layout = resolveNodeLayout(node, registry, schema);
|
|
892
|
+
const entry = {
|
|
893
|
+
node,
|
|
894
|
+
arrayIndex,
|
|
895
|
+
layout
|
|
896
|
+
};
|
|
897
|
+
entries.push(entry);
|
|
898
|
+
const placement = layout.placement;
|
|
899
|
+
if (placement.kind === "flow") {
|
|
900
|
+
pushEntry(regions, placement.region, entry);
|
|
901
|
+
if (placement.sortScope !== false) pushEntry(sortScopes, placement.sortScope, entry);
|
|
902
|
+
} else if (placement.kind === "chrome") chrome.push(entry);
|
|
903
|
+
else pushEntry(layers, placement.layer, entry);
|
|
904
|
+
}
|
|
905
|
+
sortEntries(entries);
|
|
906
|
+
for (const regionEntries of regions.values()) sortEntries(regionEntries);
|
|
907
|
+
for (const scopeEntries of sortScopes.values()) sortEntries(scopeEntries);
|
|
908
|
+
for (const layerEntries of layers.values()) sortEntries(layerEntries);
|
|
909
|
+
chrome.sort((a, b) => {
|
|
910
|
+
const placementA = a.layout.placement;
|
|
911
|
+
const placementB = b.layout.placement;
|
|
912
|
+
const edgeDelta = edgeOrder(placementA.edge) - edgeOrder(placementB.edge);
|
|
913
|
+
if (edgeDelta !== 0) return edgeDelta;
|
|
914
|
+
const orderA = a.layout.order ?? a.arrayIndex;
|
|
915
|
+
const orderB = b.layout.order ?? b.arrayIndex;
|
|
916
|
+
if (orderA !== orderB) return orderA - orderB;
|
|
917
|
+
return a.arrayIndex - b.arrayIndex;
|
|
918
|
+
});
|
|
919
|
+
return {
|
|
920
|
+
entries,
|
|
921
|
+
regions,
|
|
922
|
+
chrome,
|
|
923
|
+
layers,
|
|
924
|
+
sortScopes,
|
|
925
|
+
insets: { contributors: chrome.map((entry) => {
|
|
926
|
+
const placement = entry.layout.placement;
|
|
927
|
+
return placement.avoidContent ? {
|
|
928
|
+
edge: placement.edge,
|
|
929
|
+
sourceNodeId: entry.node.id,
|
|
930
|
+
reserve: placement.reserve
|
|
931
|
+
} : null;
|
|
932
|
+
}).filter((item) => item !== null) }
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
function getLayoutRegionEntries(plan, region = DEFAULT_LAYOUT_REGION) {
|
|
936
|
+
return plan.regions.get(region) ?? [];
|
|
937
|
+
}
|
|
938
|
+
function getSortScopeEntries(plan, sortScope = DEFAULT_SORT_SCOPE) {
|
|
939
|
+
return plan.sortScopes.get(sortScope) ?? [];
|
|
940
|
+
}
|
|
941
|
+
function getSortScopeNodes(plan, sortScope = DEFAULT_SORT_SCOPE) {
|
|
942
|
+
return getSortScopeEntries(plan, sortScope).map((entry) => entry.node);
|
|
943
|
+
}
|
|
944
|
+
function getSortableArrayIndexForInsert(scopeEntries, allChildren, visualIndex) {
|
|
945
|
+
if (scopeEntries.length === 0) return allChildren.length;
|
|
946
|
+
if (visualIndex <= 0) return scopeEntries[0].arrayIndex;
|
|
947
|
+
if (visualIndex >= scopeEntries.length) return scopeEntries[scopeEntries.length - 1].arrayIndex + 1;
|
|
948
|
+
return scopeEntries[visualIndex].arrayIndex;
|
|
949
|
+
}
|
|
950
|
+
function resolveNodeSource(schema, indexed, nodeId) {
|
|
951
|
+
const location = indexed.index.get(nodeId);
|
|
952
|
+
if (!location) return {
|
|
953
|
+
ok: false,
|
|
954
|
+
code: "NODE_NOT_FOUND"
|
|
955
|
+
};
|
|
956
|
+
if (location.owner === "root") return {
|
|
957
|
+
ok: true,
|
|
958
|
+
value: {
|
|
959
|
+
location,
|
|
960
|
+
children: schema.root.children ?? [],
|
|
961
|
+
index: location.index,
|
|
962
|
+
destination: {
|
|
963
|
+
kind: "root",
|
|
964
|
+
index: location.index
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
const owner = indexed.index.get(location.owner)?.node;
|
|
969
|
+
const regionId = location.regionId;
|
|
970
|
+
const children = regionId ? owner?.container?.regions[regionId] : void 0;
|
|
971
|
+
if (!owner || !regionId || !children) return {
|
|
972
|
+
ok: false,
|
|
973
|
+
code: "CONTAINER_OWNER_INVALID"
|
|
974
|
+
};
|
|
975
|
+
return {
|
|
976
|
+
ok: true,
|
|
977
|
+
value: {
|
|
978
|
+
location,
|
|
979
|
+
children,
|
|
980
|
+
index: location.index,
|
|
981
|
+
destination: {
|
|
982
|
+
kind: "container",
|
|
983
|
+
containerId: owner.id,
|
|
984
|
+
regionId,
|
|
985
|
+
index: location.index
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
function resolveDestination(schema, registry, destination) {
|
|
991
|
+
if (destination.kind === "root") {
|
|
992
|
+
schema.root.children ??= [];
|
|
993
|
+
return {
|
|
994
|
+
ok: true,
|
|
995
|
+
value: {
|
|
996
|
+
children: schema.root.children,
|
|
997
|
+
destination
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
const location = buildSchemaIndex(schema).index.get(destination.containerId);
|
|
1002
|
+
const container = location?.owner === "root" ? location.node : void 0;
|
|
1003
|
+
const definition = container && registry.getWidget(container.type)?.container;
|
|
1004
|
+
const variant = container?.container && definition?.variants[container.container.variant];
|
|
1005
|
+
const region = variant?.regions.find((item) => item.id === destination.regionId);
|
|
1006
|
+
const children = container?.container?.regions[destination.regionId];
|
|
1007
|
+
if (!container || !definition || !variant) return {
|
|
1008
|
+
ok: false,
|
|
1009
|
+
code: "CONTAINER_UNRESOLVED"
|
|
1010
|
+
};
|
|
1011
|
+
if (!region || !children) return {
|
|
1012
|
+
ok: false,
|
|
1013
|
+
code: "CONTAINER_REGION_UNKNOWN"
|
|
1014
|
+
};
|
|
1015
|
+
return {
|
|
1016
|
+
ok: true,
|
|
1017
|
+
value: {
|
|
1018
|
+
children,
|
|
1019
|
+
destination,
|
|
1020
|
+
container,
|
|
1021
|
+
definition,
|
|
1022
|
+
variant,
|
|
1023
|
+
region
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
function clampInsertIndex(index, length) {
|
|
1028
|
+
return Math.max(0, Math.min(index ?? length, length));
|
|
1029
|
+
}
|
|
1030
|
+
function stripPageLayout(node) {
|
|
1031
|
+
const clone = cloneDeep(node);
|
|
1032
|
+
if (clone.layout) {
|
|
1033
|
+
delete clone.layout.placement;
|
|
1034
|
+
delete clone.layout.order;
|
|
1035
|
+
}
|
|
1036
|
+
return clone;
|
|
1037
|
+
}
|
|
1038
|
+
//#endregion
|
|
1039
|
+
//#region src/sortable.ts
|
|
1040
|
+
/**
|
|
1041
|
+
* Collect the indices of all widgets whose `sortable` behavior resolves to false.
|
|
1042
|
+
* These widgets must stay at their current array index (absolute index locking).
|
|
1043
|
+
*
|
|
1044
|
+
* @param _children - The flat widget list (`root.children`)
|
|
1045
|
+
* @param registry - Widget meta registry for resolving behavior predicates
|
|
1046
|
+
* @param schema - Current designer schema (for predicate evaluation context)
|
|
1047
|
+
* @param sortScope - Sort scope to evaluate
|
|
1048
|
+
*/
|
|
1049
|
+
function getLockedIndices(_children, registry, schema, sortScope = DEFAULT_SORT_SCOPE) {
|
|
1050
|
+
return getLockedIndicesFromEntries(getSortScopeEntries(createLayoutPlan(schema, registry), sortScope), registry, schema);
|
|
1051
|
+
}
|
|
1052
|
+
function getLockedIndicesFromEntries(scopeEntries, registry, schema) {
|
|
1053
|
+
return getLockedIndicesFromNodes(scopeEntries.map((entry) => entry.node), registry, schema);
|
|
1054
|
+
}
|
|
1055
|
+
function getLockedIndicesFromNodes(nodes, registry, schema) {
|
|
1056
|
+
const locked = /* @__PURE__ */ new Set();
|
|
1057
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1058
|
+
const node = nodes[i];
|
|
1059
|
+
const meta = registry.getWidget(node.type);
|
|
1060
|
+
if (!meta) continue;
|
|
1061
|
+
if (!resolveBehavior(meta.sortable, {
|
|
1062
|
+
node,
|
|
1063
|
+
schema
|
|
1064
|
+
})) locked.add(i);
|
|
1065
|
+
}
|
|
1066
|
+
return locked;
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Can a new node be inserted at `insertIndex` without shifting any locked widget?
|
|
1070
|
+
*
|
|
1071
|
+
* Inserting at index `i` shifts all widgets at index >= `i` rightward by 1.
|
|
1072
|
+
* If any locked widget has index >= `insertIndex`, it would be displaced.
|
|
1073
|
+
*/
|
|
1074
|
+
function isInsertAllowed(insertIndex, lockedIndices) {
|
|
1075
|
+
for (const L of lockedIndices) if (L >= insertIndex) return false;
|
|
1076
|
+
return true;
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Can a node be moved from `srcIdx` to `targetIdx` (post-removal insertion index)
|
|
1080
|
+
* without altering any locked widget's absolute position?
|
|
1081
|
+
*
|
|
1082
|
+
* Mathematical analysis for each locked widget at index L (L !== srcIdx):
|
|
1083
|
+
* - srcIdx < L: Removal shifts L to L-1. Insertion must be at targetIdx <= L-1 to restore it.
|
|
1084
|
+
* - srcIdx > L: L is unaffected by removal. Insertion must satisfy targetIdx > L to keep it at L.
|
|
1085
|
+
*/
|
|
1086
|
+
function isMoveAllowed(srcIdx, targetIdx, lockedIndices) {
|
|
1087
|
+
if (lockedIndices.has(srcIdx)) return false;
|
|
1088
|
+
for (const L of lockedIndices) {
|
|
1089
|
+
if (L === srcIdx) continue;
|
|
1090
|
+
if (srcIdx < L) {
|
|
1091
|
+
if (targetIdx > L - 1) return false;
|
|
1092
|
+
} else if (targetIdx <= L) return false;
|
|
1093
|
+
}
|
|
1094
|
+
return true;
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Can a node at `removeIndex` be removed without shifting any locked widget?
|
|
1098
|
+
*
|
|
1099
|
+
* Removing at index `i` shifts all widgets at index > `i` leftward by 1.
|
|
1100
|
+
* If any locked widget has index > `removeIndex`, it would be displaced.
|
|
1101
|
+
*/
|
|
1102
|
+
function isRemoveAllowed(removeIndex, lockedIndices) {
|
|
1103
|
+
for (const L of lockedIndices) if (L > removeIndex) return false;
|
|
1104
|
+
return true;
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Compute the set of valid visual drop indices (0..children.length) for a drag operation.
|
|
1108
|
+
*
|
|
1109
|
+
* For new-widget drops (sourceNodeId === null): checks isInsertAllowed for each position.
|
|
1110
|
+
* For existing-widget moves: converts visual gap index to post-removal target index
|
|
1111
|
+
* and checks isMoveAllowed.
|
|
1112
|
+
*
|
|
1113
|
+
* @param children - The flat widget list
|
|
1114
|
+
* @param lockedIndices - Pre-computed locked indices from getLockedIndices()
|
|
1115
|
+
* @param sourceNodeId - ID of the dragged widget (null if dragging from material panel)
|
|
1116
|
+
*/
|
|
1117
|
+
function getValidDropIndices(children, lockedIndices, sourceNodeId) {
|
|
1118
|
+
const valid = /* @__PURE__ */ new Set();
|
|
1119
|
+
const n = children.length;
|
|
1120
|
+
if (lockedIndices.size === 0) {
|
|
1121
|
+
for (let i = 0; i <= n; i++) valid.add(i);
|
|
1122
|
+
return valid;
|
|
1123
|
+
}
|
|
1124
|
+
if (sourceNodeId === null) {
|
|
1125
|
+
for (let i = 0; i <= n; i++) if (isInsertAllowed(i, lockedIndices)) valid.add(i);
|
|
1126
|
+
} else {
|
|
1127
|
+
const srcIdx = children.findIndex((c) => "node" in c ? c.node.id === sourceNodeId : c.id === sourceNodeId);
|
|
1128
|
+
if (srcIdx === -1) return valid;
|
|
1129
|
+
for (let visualIdx = 0; visualIdx <= n; visualIdx++) {
|
|
1130
|
+
let targetIdx = visualIdx;
|
|
1131
|
+
if (targetIdx > srcIdx) targetIdx = targetIdx - 1;
|
|
1132
|
+
if (targetIdx === srcIdx) {
|
|
1133
|
+
valid.add(visualIdx);
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
if (isMoveAllowed(srcIdx, targetIdx, lockedIndices)) valid.add(visualIdx);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
return valid;
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Given a raw visual index and a set of valid indices, find the nearest valid one.
|
|
1143
|
+
* Returns null if no valid index exists.
|
|
1144
|
+
*/
|
|
1145
|
+
function findNearestValidIndex(rawIndex, validIndices) {
|
|
1146
|
+
if (validIndices.size === 0) return null;
|
|
1147
|
+
if (validIndices.has(rawIndex)) return rawIndex;
|
|
1148
|
+
let best = null;
|
|
1149
|
+
let bestDist = Infinity;
|
|
1150
|
+
for (const v of validIndices) {
|
|
1151
|
+
const dist = Math.abs(v - rawIndex);
|
|
1152
|
+
if (dist < bestDist) {
|
|
1153
|
+
bestDist = dist;
|
|
1154
|
+
best = v;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
return best;
|
|
1158
|
+
}
|
|
1159
|
+
//#endregion
|
|
1160
|
+
//#region src/commands/add-node.ts
|
|
1161
|
+
function addNodeHandler(ctx, payload) {
|
|
1162
|
+
const { draft: rawSchema, registry } = ctx;
|
|
1163
|
+
const safeSchema = ctx.schema;
|
|
1164
|
+
const meta = registry.getWidget(payload.node.type);
|
|
1165
|
+
const destination = payload.destination ?? { kind: "root" };
|
|
1166
|
+
const createDecision = meta ? resolveCreatable(meta.creatable, {
|
|
1167
|
+
widgetType: payload.node.type,
|
|
1168
|
+
schema: ctx.schema
|
|
1169
|
+
}, true) : { allowed: true };
|
|
1170
|
+
if (!createDecision.allowed) {
|
|
1171
|
+
const reason = createDecision.message ?? createDecision.messageKey ?? createDecision.code;
|
|
1172
|
+
console.warn(`[dragcraft/core] ADD_NODE: blocked by creatable constraint for widget type "${payload.node.type}"${reason ? ` (${reason})` : ""}`);
|
|
1173
|
+
return {
|
|
1174
|
+
ok: false,
|
|
1175
|
+
code: createDecision.code ?? "NODE_NOT_CREATABLE",
|
|
1176
|
+
messageKey: createDecision.messageKey,
|
|
1177
|
+
message: createDecision.message
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
const node = cloneDeep(payload.node);
|
|
1181
|
+
const candidateNodeIds = collectSubtreeIds(node);
|
|
1182
|
+
if (node.container && !meta) return {
|
|
1183
|
+
ok: false,
|
|
1184
|
+
code: "UNRESOLVED_CONTAINER_READ_ONLY"
|
|
1185
|
+
};
|
|
1186
|
+
if (node.container && !meta?.container) return {
|
|
1187
|
+
ok: false,
|
|
1188
|
+
code: "CONTAINER_CAPABILITY_MISMATCH"
|
|
1189
|
+
};
|
|
1190
|
+
if (destination.kind === "container" && meta?.container) return {
|
|
1191
|
+
ok: false,
|
|
1192
|
+
code: "CONTAINER_NESTING_FORBIDDEN"
|
|
1193
|
+
};
|
|
1194
|
+
if (meta?.container && !node.container) {
|
|
1195
|
+
const initialized = createContainerState(node, safeSchema, registry, createRegisteredNode(registry));
|
|
1196
|
+
if (!initialized.ok) return initialized;
|
|
1197
|
+
node.container = initialized.state;
|
|
1198
|
+
}
|
|
1199
|
+
const idCandidate = cloneSchema(safeSchema);
|
|
1200
|
+
idCandidate.root.children ??= [];
|
|
1201
|
+
idCandidate.root.children.push(cloneDeep(node));
|
|
1202
|
+
const idDiagnostics = buildSchemaIndex(idCandidate).diagnostics.filter((diagnostic) => diagnostic.code === "SCHEMA_NODE_ID_DUPLICATE");
|
|
1203
|
+
if (idDiagnostics.length > 0) return {
|
|
1204
|
+
ok: false,
|
|
1205
|
+
code: "SCHEMA_NODE_ID_DUPLICATE",
|
|
1206
|
+
details: { diagnostics: idDiagnostics }
|
|
1207
|
+
};
|
|
1208
|
+
if (node.container) {
|
|
1209
|
+
const candidate = cloneSchema(safeSchema);
|
|
1210
|
+
candidate.root.children ??= [];
|
|
1211
|
+
candidate.root.children.push(cloneDeep(node));
|
|
1212
|
+
const candidateDiagnostics = validateSchema(candidate, registry).diagnostics.filter((diagnostic) => diagnostic.nodeId !== void 0 && candidateNodeIds.has(diagnostic.nodeId) || diagnostic.ownerId !== void 0 && candidateNodeIds.has(diagnostic.ownerId));
|
|
1213
|
+
if (candidateDiagnostics.some((diagnostic) => diagnostic.severity === "error")) return {
|
|
1214
|
+
ok: false,
|
|
1215
|
+
code: "CONTAINER_STATE_INVALID",
|
|
1216
|
+
details: { diagnostics: candidateDiagnostics }
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
if (destination.kind === "container") {
|
|
1220
|
+
const targetResult = resolveDestination(safeSchema, registry, destination);
|
|
1221
|
+
if (!targetResult.ok) return targetResult;
|
|
1222
|
+
const target = targetResult.value;
|
|
1223
|
+
if (!target.container || !target.definition || !target.variant || !target.region) return {
|
|
1224
|
+
ok: false,
|
|
1225
|
+
code: "CONTAINER_DESTINATION_REQUIRED"
|
|
1226
|
+
};
|
|
1227
|
+
const index = clampInsertIndex(destination.index, target.children.length);
|
|
1228
|
+
if (!isInsertAllowed(index, getLockedIndicesFromNodes(target.children, registry, safeSchema))) return {
|
|
1229
|
+
ok: false,
|
|
1230
|
+
code: "SORTABLE_LOCK_VIOLATION"
|
|
1231
|
+
};
|
|
1232
|
+
const decision = resolvePlacementDecision({
|
|
1233
|
+
definition: target.definition,
|
|
1234
|
+
region: target.region,
|
|
1235
|
+
child: node,
|
|
1236
|
+
childHasContainerCapability: Boolean(meta?.container),
|
|
1237
|
+
targetCount: target.children.length,
|
|
1238
|
+
callbackContext: {
|
|
1239
|
+
operation: "add",
|
|
1240
|
+
schema: safeSchema,
|
|
1241
|
+
container: target.container,
|
|
1242
|
+
variant: target.variant,
|
|
1243
|
+
region: target.region,
|
|
1244
|
+
child: node,
|
|
1245
|
+
targetIndex: index
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
if (!decision.allowed) return {
|
|
1249
|
+
ok: false,
|
|
1250
|
+
code: decision.code ?? "CONTAINER_PLACEMENT_DENIED",
|
|
1251
|
+
messageKey: decision.messageKey,
|
|
1252
|
+
message: decision.message,
|
|
1253
|
+
details: decision.details
|
|
1254
|
+
};
|
|
1255
|
+
const candidate = cloneSchema(safeSchema);
|
|
1256
|
+
const candidateTarget = resolveDestination(candidate, registry, destination);
|
|
1257
|
+
if (!candidateTarget.ok) return candidateTarget;
|
|
1258
|
+
candidateTarget.value.children.splice(index, 0, stripPageLayout(cloneDeep(node)));
|
|
1259
|
+
const candidateDiagnostics = validateSchema(candidate, registry).diagnostics.filter((diagnostic) => diagnostic.nodeId !== void 0 && candidateNodeIds.has(diagnostic.nodeId) || diagnostic.ownerId !== void 0 && candidateNodeIds.has(diagnostic.ownerId));
|
|
1260
|
+
if (candidateDiagnostics.some((diagnostic) => diagnostic.severity === "error")) return {
|
|
1261
|
+
ok: false,
|
|
1262
|
+
code: "SCHEMA_CANDIDATE_INVALID",
|
|
1263
|
+
details: { diagnostics: candidateDiagnostics }
|
|
1264
|
+
};
|
|
1265
|
+
const draftTarget = resolveDestination(rawSchema, registry, destination);
|
|
1266
|
+
if (!draftTarget.ok) return draftTarget;
|
|
1267
|
+
draftTarget.value.children.splice(index, 0, stripPageLayout(node));
|
|
1268
|
+
return {
|
|
1269
|
+
ok: true,
|
|
1270
|
+
eventPayload: {
|
|
1271
|
+
nodeId: node.id,
|
|
1272
|
+
destination: {
|
|
1273
|
+
...destination,
|
|
1274
|
+
index
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
rawSchema.root.children ??= [];
|
|
1280
|
+
const rootChildren = rawSchema.root.children;
|
|
1281
|
+
const nodeLayout = resolveNodeLayout(node, registry);
|
|
1282
|
+
const resolvedScope = destination.sortScope ?? (nodeLayout.sortScope === false ? void 0 : nodeLayout.sortScope);
|
|
1283
|
+
let resolvedArrayIndex = rootChildren.length;
|
|
1284
|
+
if (destination.index !== void 0 && resolvedScope !== void 0) {
|
|
1285
|
+
const scopeEntries = getSortScopeEntries(createLayoutPlan(safeSchema, registry), resolvedScope);
|
|
1286
|
+
const lockedIndices = getLockedIndicesFromEntries(scopeEntries, registry, safeSchema);
|
|
1287
|
+
if (!isInsertAllowed(destination.index, lockedIndices)) return {
|
|
1288
|
+
ok: false,
|
|
1289
|
+
code: "SORTABLE_LOCK_VIOLATION"
|
|
1290
|
+
};
|
|
1291
|
+
resolvedArrayIndex = getSortableArrayIndexForInsert(scopeEntries, rootChildren, destination.index);
|
|
1292
|
+
} else if (destination.index !== void 0) resolvedArrayIndex = clampInsertIndex(destination.index, rootChildren.length);
|
|
1293
|
+
const candidate = cloneSchema(safeSchema);
|
|
1294
|
+
candidate.root.children ??= [];
|
|
1295
|
+
candidate.root.children.splice(resolvedArrayIndex, 0, cloneDeep(node));
|
|
1296
|
+
const candidateDiagnostics = validateSchema(candidate, registry).diagnostics.filter((diagnostic) => diagnostic.nodeId !== void 0 && candidateNodeIds.has(diagnostic.nodeId) || diagnostic.ownerId !== void 0 && candidateNodeIds.has(diagnostic.ownerId));
|
|
1297
|
+
if (candidateDiagnostics.some((diagnostic) => diagnostic.severity === "error")) return {
|
|
1298
|
+
ok: false,
|
|
1299
|
+
code: "SCHEMA_CANDIDATE_INVALID",
|
|
1300
|
+
details: { diagnostics: candidateDiagnostics }
|
|
1301
|
+
};
|
|
1302
|
+
rootChildren.splice(resolvedArrayIndex, 0, node);
|
|
1303
|
+
return {
|
|
1304
|
+
ok: true,
|
|
1305
|
+
eventPayload: {
|
|
1306
|
+
nodeId: node.id,
|
|
1307
|
+
destination: {
|
|
1308
|
+
kind: "root",
|
|
1309
|
+
sortScope: resolvedScope,
|
|
1310
|
+
index: resolvedArrayIndex
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
//#endregion
|
|
1316
|
+
//#region src/commands/change-container-variant.ts
|
|
1317
|
+
function sameRegionIds(left, right) {
|
|
1318
|
+
const leftIds = left.regions.map((region) => region.id).sort();
|
|
1319
|
+
const rightIds = right.regions.map((region) => region.id).sort();
|
|
1320
|
+
return leftIds.length === rightIds.length && leftIds.every((id, index) => id === rightIds[index]);
|
|
1321
|
+
}
|
|
1322
|
+
function isPlainRecord$1(value) {
|
|
1323
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1324
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1325
|
+
return prototype === Object.prototype || prototype === null;
|
|
1326
|
+
}
|
|
1327
|
+
function isSchemaNode(value) {
|
|
1328
|
+
return isPlainRecord$1(value) && typeof value.id === "string" && typeof value.type === "string" && isPlainRecord$1(value.props);
|
|
1329
|
+
}
|
|
1330
|
+
function isContainerState(value) {
|
|
1331
|
+
if (!isPlainRecord$1(value) || typeof value.variant !== "string" || !isPlainRecord$1(value.regions)) return false;
|
|
1332
|
+
return Object.values(value.regions).every((children) => Array.isArray(children) && children.every(isSchemaNode));
|
|
1333
|
+
}
|
|
1334
|
+
function isOptionalString(value) {
|
|
1335
|
+
return value === void 0 || typeof value === "string";
|
|
1336
|
+
}
|
|
1337
|
+
function invalidMigrationResult(containerId) {
|
|
1338
|
+
return {
|
|
1339
|
+
allowed: false,
|
|
1340
|
+
code: "CONTAINER_VARIANT_MIGRATION_INVALID"
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
function safelyMigrateVariant(definition, ctx) {
|
|
1344
|
+
if (!definition.migrateVariant) return {
|
|
1345
|
+
allowed: false,
|
|
1346
|
+
code: "CONTAINER_VARIANT_MIGRATION_REQUIRED"
|
|
1347
|
+
};
|
|
1348
|
+
try {
|
|
1349
|
+
const result = cloneDeep(definition.migrateVariant(cloneDeep(ctx)));
|
|
1350
|
+
if (!isPlainRecord$1(result) || typeof result.allowed !== "boolean") return invalidMigrationResult(ctx.container.id);
|
|
1351
|
+
if (result.allowed) {
|
|
1352
|
+
if (!isContainerState(result.state)) return invalidMigrationResult(ctx.container.id);
|
|
1353
|
+
return {
|
|
1354
|
+
allowed: true,
|
|
1355
|
+
state: result.state
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
if (!isOptionalString(result.code) || !isOptionalString(result.messageKey) || !isOptionalString(result.message) || result.details !== void 0 && !isPlainRecord$1(result.details)) return invalidMigrationResult(ctx.container.id);
|
|
1359
|
+
return {
|
|
1360
|
+
allowed: false,
|
|
1361
|
+
code: result.code,
|
|
1362
|
+
messageKey: result.messageKey,
|
|
1363
|
+
message: result.message,
|
|
1364
|
+
details: result.details
|
|
1365
|
+
};
|
|
1366
|
+
} catch (error) {
|
|
1367
|
+
return {
|
|
1368
|
+
allowed: false,
|
|
1369
|
+
code: "CONTAINER_VARIANT_MIGRATION_FAILED",
|
|
1370
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1371
|
+
details: {
|
|
1372
|
+
nodeId: ctx.container.id,
|
|
1373
|
+
containerId: ctx.container.id,
|
|
1374
|
+
fromVariantId: ctx.fromVariantId,
|
|
1375
|
+
toVariantId: ctx.toVariantId
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
function changeContainerVariantHandler(ctx, payload) {
|
|
1381
|
+
const rawSchema = ctx.draft;
|
|
1382
|
+
const indexed = buildSchemaIndex(rawSchema).index.get(payload.containerId);
|
|
1383
|
+
const definition = indexed && ctx.registry.getWidget(indexed.node.type)?.container;
|
|
1384
|
+
if (!indexed?.node.container || !definition) return {
|
|
1385
|
+
ok: false,
|
|
1386
|
+
code: "CONTAINER_UNRESOLVED"
|
|
1387
|
+
};
|
|
1388
|
+
const fromVariantId = indexed.node.container.variant;
|
|
1389
|
+
const from = definition.variants[fromVariantId];
|
|
1390
|
+
const to = definition.variants[payload.variant];
|
|
1391
|
+
if (!from || !to) return {
|
|
1392
|
+
ok: false,
|
|
1393
|
+
code: "CONTAINER_VARIANT_UNKNOWN"
|
|
1394
|
+
};
|
|
1395
|
+
const result = definition.migrateVariant ? safelyMigrateVariant(definition, {
|
|
1396
|
+
schema: rawSchema,
|
|
1397
|
+
container: indexed.node,
|
|
1398
|
+
fromVariantId,
|
|
1399
|
+
toVariantId: payload.variant,
|
|
1400
|
+
fromVariant: from,
|
|
1401
|
+
toVariant: to,
|
|
1402
|
+
state: indexed.node.container
|
|
1403
|
+
}) : sameRegionIds(from, to) ? {
|
|
1404
|
+
allowed: true,
|
|
1405
|
+
state: {
|
|
1406
|
+
...cloneDeep(indexed.node.container),
|
|
1407
|
+
variant: payload.variant
|
|
1408
|
+
}
|
|
1409
|
+
} : safelyMigrateVariant(definition, {
|
|
1410
|
+
schema: rawSchema,
|
|
1411
|
+
container: indexed.node,
|
|
1412
|
+
fromVariantId,
|
|
1413
|
+
toVariantId: payload.variant,
|
|
1414
|
+
fromVariant: from,
|
|
1415
|
+
toVariant: to,
|
|
1416
|
+
state: indexed.node.container
|
|
1417
|
+
});
|
|
1418
|
+
if (!result.allowed) return {
|
|
1419
|
+
ok: false,
|
|
1420
|
+
code: result.code ?? "CONTAINER_VARIANT_MIGRATION_REJECTED",
|
|
1421
|
+
...result.messageKey === void 0 ? {} : { messageKey: result.messageKey },
|
|
1422
|
+
...result.message === void 0 ? {} : { message: result.message },
|
|
1423
|
+
...result.details === void 0 ? {} : { details: result.details }
|
|
1424
|
+
};
|
|
1425
|
+
if (result.state.variant !== payload.variant) return {
|
|
1426
|
+
ok: false,
|
|
1427
|
+
code: "CONTAINER_VARIANT_MIGRATION_TARGET_MISMATCH"
|
|
1428
|
+
};
|
|
1429
|
+
const candidate = cloneSchema(rawSchema);
|
|
1430
|
+
const candidateNode = buildSchemaIndex(candidate).index.get(indexed.node.id)?.node;
|
|
1431
|
+
if (!candidateNode) return {
|
|
1432
|
+
ok: false,
|
|
1433
|
+
code: "CONTAINER_NOT_FOUND"
|
|
1434
|
+
};
|
|
1435
|
+
candidateNode.container = cloneDeep(result.state);
|
|
1436
|
+
const validation = validateSchema(candidate, ctx.registry);
|
|
1437
|
+
if (!validation.valid) return {
|
|
1438
|
+
ok: false,
|
|
1439
|
+
code: "CONTAINER_VARIANT_MIGRATION_INVALID",
|
|
1440
|
+
details: { diagnostics: validation.diagnostics }
|
|
1441
|
+
};
|
|
1442
|
+
const validatedState = buildSchemaIndex(validation.schema).index.get(indexed.node.id)?.node.container;
|
|
1443
|
+
if (!validatedState) return {
|
|
1444
|
+
ok: false,
|
|
1445
|
+
code: "CONTAINER_VARIANT_MIGRATION_INVALID"
|
|
1446
|
+
};
|
|
1447
|
+
indexed.node.container = cloneDeep(validatedState);
|
|
1448
|
+
return {
|
|
1449
|
+
ok: true,
|
|
1450
|
+
eventPayload: {
|
|
1451
|
+
containerId: indexed.node.id,
|
|
1452
|
+
fromVariant: fromVariantId,
|
|
1453
|
+
toVariant: payload.variant
|
|
1454
|
+
}
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
//#endregion
|
|
1458
|
+
//#region src/commands/duplicate-node.ts
|
|
1459
|
+
function duplicateNodeHandler(ctx, payload) {
|
|
1460
|
+
const schema = ctx.schema;
|
|
1461
|
+
const indexed = buildSchemaIndex(schema);
|
|
1462
|
+
const sourceResult = resolveNodeSource(schema, indexed, payload.nodeId);
|
|
1463
|
+
if (!sourceResult.ok) return sourceResult;
|
|
1464
|
+
const source = sourceResult.value;
|
|
1465
|
+
const sourceNode = source.location.node;
|
|
1466
|
+
if (sourceNode.container && !ctx.registry.getWidget(sourceNode.type)?.container) return {
|
|
1467
|
+
ok: false,
|
|
1468
|
+
code: "UNRESOLVED_CONTAINER_READ_ONLY"
|
|
1469
|
+
};
|
|
1470
|
+
if (source.destination.kind === "container") {
|
|
1471
|
+
const sourceDestination = source.destination;
|
|
1472
|
+
const owner = indexed.index.get(sourceDestination.containerId)?.node;
|
|
1473
|
+
const definition = owner && ctx.registry.getWidget(owner.type)?.container;
|
|
1474
|
+
const variant = owner?.container && definition?.variants[owner.container.variant];
|
|
1475
|
+
const region = variant?.regions.find((item) => item.id === sourceDestination.regionId);
|
|
1476
|
+
if (!definition || !variant || !region) return {
|
|
1477
|
+
ok: false,
|
|
1478
|
+
code: "UNRESOLVED_CONTAINER_READ_ONLY"
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
const clone = cloneNodeSubtree(sourceNode, generateShortId);
|
|
1482
|
+
let destination = {
|
|
1483
|
+
...source.destination,
|
|
1484
|
+
index: source.index + 1
|
|
1485
|
+
};
|
|
1486
|
+
if (source.destination.kind === "root") {
|
|
1487
|
+
const sortScope = resolveNodeLayout(sourceNode, ctx.registry).sortScope;
|
|
1488
|
+
if (sortScope !== false) {
|
|
1489
|
+
const scopeIndex = getSortScopeEntries(createLayoutPlan(schema, ctx.registry), sortScope).findIndex((entry) => entry.node.id === sourceNode.id);
|
|
1490
|
+
if (scopeIndex !== -1) destination = {
|
|
1491
|
+
...source.destination,
|
|
1492
|
+
index: scopeIndex + 1
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
const added = addNodeHandler(ctx, {
|
|
1497
|
+
node: clone,
|
|
1498
|
+
destination
|
|
1499
|
+
});
|
|
1500
|
+
if (!added) return {
|
|
1501
|
+
ok: false,
|
|
1502
|
+
code: "DUPLICATE_ADD_REJECTED"
|
|
1503
|
+
};
|
|
1504
|
+
if (!added.ok) return added;
|
|
1505
|
+
const addedEvent = added.eventPayload;
|
|
1506
|
+
return {
|
|
1507
|
+
ok: true,
|
|
1508
|
+
eventPayload: {
|
|
1509
|
+
sourceNodeId: payload.nodeId,
|
|
1510
|
+
nodeId: clone.id,
|
|
1511
|
+
destination: addedEvent?.destination ?? destination
|
|
1512
|
+
}
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
//#endregion
|
|
1516
|
+
//#region src/commands/move-node.ts
|
|
1517
|
+
function moveNodeHandler(ctx, payload) {
|
|
1518
|
+
const { draft: rawSchema, registry } = ctx;
|
|
1519
|
+
const safeSchema = ctx.schema;
|
|
1520
|
+
const indexed = buildSchemaIndex(safeSchema);
|
|
1521
|
+
const sourceResult = resolveNodeSource(safeSchema, indexed, payload.nodeId);
|
|
1522
|
+
if (!sourceResult.ok) return sourceResult;
|
|
1523
|
+
const targetResult = resolveDestination(safeSchema, registry, payload.destination);
|
|
1524
|
+
if (!targetResult.ok) return targetResult;
|
|
1525
|
+
const source = sourceResult.value;
|
|
1526
|
+
const target = targetResult.value;
|
|
1527
|
+
const node = source.location.node;
|
|
1528
|
+
if (node.container && !registry.getWidget(node.type)?.container) return {
|
|
1529
|
+
ok: false,
|
|
1530
|
+
code: "UNRESOLVED_CONTAINER_READ_ONLY"
|
|
1531
|
+
};
|
|
1532
|
+
const sourceMeta = registry.getWidget(node.type);
|
|
1533
|
+
const behaviorContext = {
|
|
1534
|
+
node,
|
|
1535
|
+
schema: safeSchema
|
|
1536
|
+
};
|
|
1537
|
+
if (!resolveBehavior(sourceMeta?.draggable, behaviorContext) || !resolveBehavior(sourceMeta?.sortable, behaviorContext)) return {
|
|
1538
|
+
ok: false,
|
|
1539
|
+
code: "NODE_NOT_MOVABLE"
|
|
1540
|
+
};
|
|
1541
|
+
if (source.destination.kind === "container") {
|
|
1542
|
+
const sourceRegionId = source.destination.regionId;
|
|
1543
|
+
const owner = indexed.index.get(source.destination.containerId)?.node;
|
|
1544
|
+
const definition = owner && registry.getWidget(owner.type)?.container;
|
|
1545
|
+
if (!definition) return {
|
|
1546
|
+
ok: false,
|
|
1547
|
+
code: "UNRESOLVED_CONTAINER_READ_ONLY"
|
|
1548
|
+
};
|
|
1549
|
+
const variant = owner?.container && definition.variants[owner.container.variant];
|
|
1550
|
+
const region = variant?.regions.find((item) => item.id === sourceRegionId);
|
|
1551
|
+
if (!variant || !region) return {
|
|
1552
|
+
ok: false,
|
|
1553
|
+
code: "UNRESOLVED_CONTAINER_READ_ONLY"
|
|
1554
|
+
};
|
|
1555
|
+
const sameRegion = target.children === source.children;
|
|
1556
|
+
const minItems = region.constraints?.minItems ?? 0;
|
|
1557
|
+
if (!sameRegion && source.children.length - 1 < minItems) return {
|
|
1558
|
+
ok: false,
|
|
1559
|
+
code: "CONTAINER_REGION_MIN_ITEMS"
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
let requestedIndex = payload.destination.kind === "container" ? clampInsertIndex(payload.destination.index, target.children.length) : Math.max(0, payload.destination.index ?? Number.MAX_SAFE_INTEGER);
|
|
1563
|
+
const sameRegion = payload.destination.kind === "container" && source.destination.kind === "container" && target.children === source.children;
|
|
1564
|
+
if (sameRegion && source.index < requestedIndex) requestedIndex -= 1;
|
|
1565
|
+
if (sameRegion && source.index === requestedIndex) return {
|
|
1566
|
+
ok: true,
|
|
1567
|
+
changed: false
|
|
1568
|
+
};
|
|
1569
|
+
if (source.destination.kind === "container") {
|
|
1570
|
+
const sourceLocks = getLockedIndicesFromNodes(source.children, registry, safeSchema);
|
|
1571
|
+
if (sameRegion) {
|
|
1572
|
+
if (!isMoveAllowed(source.index, requestedIndex, sourceLocks)) return {
|
|
1573
|
+
ok: false,
|
|
1574
|
+
code: "SORTABLE_LOCK_VIOLATION"
|
|
1575
|
+
};
|
|
1576
|
+
} else if (!isRemoveAllowed(source.index, sourceLocks)) return {
|
|
1577
|
+
ok: false,
|
|
1578
|
+
code: "SORTABLE_LOCK_VIOLATION"
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
if (payload.destination.kind === "container" && !sameRegion) {
|
|
1582
|
+
const targetLocks = getLockedIndicesFromNodes(target.children, registry, safeSchema);
|
|
1583
|
+
if (!isInsertAllowed(requestedIndex, targetLocks)) return {
|
|
1584
|
+
ok: false,
|
|
1585
|
+
code: "SORTABLE_LOCK_VIOLATION"
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
if (target.container && target.definition && target.variant && target.region) {
|
|
1589
|
+
const targetCount = target.children.length - (target.children === source.children ? 1 : 0);
|
|
1590
|
+
const decision = resolvePlacementDecision({
|
|
1591
|
+
definition: target.definition,
|
|
1592
|
+
region: target.region,
|
|
1593
|
+
child: node,
|
|
1594
|
+
childHasContainerCapability: Boolean(sourceMeta?.container),
|
|
1595
|
+
targetCount,
|
|
1596
|
+
callbackContext: {
|
|
1597
|
+
operation: "move",
|
|
1598
|
+
schema: safeSchema,
|
|
1599
|
+
container: target.container,
|
|
1600
|
+
variant: target.variant,
|
|
1601
|
+
region: target.region,
|
|
1602
|
+
child: node,
|
|
1603
|
+
targetIndex: requestedIndex
|
|
1604
|
+
}
|
|
1605
|
+
});
|
|
1606
|
+
if (!decision.allowed) return {
|
|
1607
|
+
ok: false,
|
|
1608
|
+
code: decision.code ?? "CONTAINER_PLACEMENT_DENIED",
|
|
1609
|
+
messageKey: decision.messageKey,
|
|
1610
|
+
message: decision.message,
|
|
1611
|
+
details: decision.details
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
const sourceScope = source.destination.kind === "root" ? resolveNodeLayout(node, registry).sortScope : void 0;
|
|
1615
|
+
const targetScope = payload.destination.kind === "root" ? payload.destination.sortScope ?? sourceScope ?? resolveNodeLayout(node, registry).sortScope : void 0;
|
|
1616
|
+
const targetUsesSortScope = typeof targetScope === "string";
|
|
1617
|
+
const targetEntriesBefore = payload.destination.kind === "root" && targetUsesSortScope ? getSortScopeEntries(createLayoutPlan(safeSchema, registry), targetScope) : [];
|
|
1618
|
+
if (payload.destination.kind === "root") requestedIndex = clampInsertIndex(payload.destination.index, targetUsesSortScope ? targetEntriesBefore.length : target.children.length);
|
|
1619
|
+
if (source.destination.kind === "root") {
|
|
1620
|
+
if (sourceScope === false && payload.destination.kind === "root") return {
|
|
1621
|
+
ok: false,
|
|
1622
|
+
code: "NODE_NOT_SORTABLE"
|
|
1623
|
+
};
|
|
1624
|
+
const sourceEntries = sourceScope === false ? [] : getSortScopeEntries(createLayoutPlan(safeSchema, registry), sourceScope);
|
|
1625
|
+
const sourceScopeIndex = sourceEntries.findIndex((entry) => entry.node.id === node.id);
|
|
1626
|
+
const sourceLocks = getLockedIndicesFromEntries(sourceEntries, registry, safeSchema);
|
|
1627
|
+
if (payload.destination.kind === "root" && targetScope === sourceScope) {
|
|
1628
|
+
if (sourceScopeIndex < requestedIndex) requestedIndex -= 1;
|
|
1629
|
+
if (sourceScopeIndex === requestedIndex) return {
|
|
1630
|
+
ok: true,
|
|
1631
|
+
changed: false
|
|
1632
|
+
};
|
|
1633
|
+
if (!isMoveAllowed(sourceScopeIndex, requestedIndex, sourceLocks)) return {
|
|
1634
|
+
ok: false,
|
|
1635
|
+
code: "SORTABLE_LOCK_VIOLATION"
|
|
1636
|
+
};
|
|
1637
|
+
} else if (sourceScope !== false && !isRemoveAllowed(sourceScopeIndex, sourceLocks)) return {
|
|
1638
|
+
ok: false,
|
|
1639
|
+
code: "SORTABLE_LOCK_VIOLATION"
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
if (payload.destination.kind === "root" && targetUsesSortScope && targetScope !== sourceScope) {
|
|
1643
|
+
const targetLocks = getLockedIndicesFromEntries(targetEntriesBefore, registry, safeSchema);
|
|
1644
|
+
if (!isInsertAllowed(requestedIndex, targetLocks)) return {
|
|
1645
|
+
ok: false,
|
|
1646
|
+
code: "SORTABLE_LOCK_VIOLATION"
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
const draftSourceResult = resolveNodeSource(rawSchema, buildSchemaIndex(rawSchema), payload.nodeId);
|
|
1650
|
+
if (!draftSourceResult.ok) return draftSourceResult;
|
|
1651
|
+
const draftTargetResult = resolveDestination(rawSchema, registry, payload.destination);
|
|
1652
|
+
if (!draftTargetResult.ok) return draftTargetResult;
|
|
1653
|
+
const draftSource = draftSourceResult.value;
|
|
1654
|
+
const draftTarget = draftTargetResult.value;
|
|
1655
|
+
const sameOwnerArray = draftTarget.children === draftSource.children;
|
|
1656
|
+
const sourceBefore = cloneDeep(draftSource.children);
|
|
1657
|
+
const targetBefore = sameOwnerArray ? null : cloneDeep(draftTarget.children);
|
|
1658
|
+
const [removed] = draftSource.children.splice(draftSource.index, 1);
|
|
1659
|
+
const inserted = payload.destination.kind === "container" ? stripPageLayout(removed) : cloneDeep(removed);
|
|
1660
|
+
const insertedIndex = payload.destination.kind === "root" ? targetUsesSortScope ? getSortableArrayIndexForInsert(getSortScopeEntries(createLayoutPlan(deepFreeze(cloneSchema(rawSchema)), registry), targetScope), draftTarget.children, requestedIndex) : clampInsertIndex(requestedIndex, draftTarget.children.length) : clampInsertIndex(requestedIndex, draftTarget.children.length);
|
|
1661
|
+
draftTarget.children.splice(insertedIndex, 0, inserted);
|
|
1662
|
+
const validation = validateSchema(rawSchema, registry);
|
|
1663
|
+
const movedNodeIds = collectSubtreeIds(inserted);
|
|
1664
|
+
const candidateDiagnostics = validation.diagnostics.filter((diagnostic) => diagnostic.nodeId !== void 0 && movedNodeIds.has(diagnostic.nodeId) || diagnostic.ownerId !== void 0 && movedNodeIds.has(diagnostic.ownerId));
|
|
1665
|
+
if (candidateDiagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
|
1666
|
+
draftSource.children.splice(0, draftSource.children.length, ...sourceBefore);
|
|
1667
|
+
if (targetBefore) draftTarget.children.splice(0, draftTarget.children.length, ...targetBefore);
|
|
1668
|
+
return {
|
|
1669
|
+
ok: false,
|
|
1670
|
+
code: "SCHEMA_CANDIDATE_INVALID",
|
|
1671
|
+
details: { diagnostics: candidateDiagnostics }
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
return {
|
|
1675
|
+
ok: true,
|
|
1676
|
+
eventPayload: {
|
|
1677
|
+
nodeId: payload.nodeId,
|
|
1678
|
+
source: source.destination,
|
|
1679
|
+
destination: {
|
|
1680
|
+
...payload.destination,
|
|
1681
|
+
index: insertedIndex
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
//#endregion
|
|
1687
|
+
//#region src/commands/remove-node.ts
|
|
1688
|
+
function removeNodeHandler(ctx, payload) {
|
|
1689
|
+
const { store, registry } = ctx;
|
|
1690
|
+
const rawSchema = ctx.schema;
|
|
1691
|
+
if (payload.nodeId === rawSchema.root.id) {
|
|
1692
|
+
console.warn("[dragcraft/core] REMOVE_NODE: cannot remove root node");
|
|
1693
|
+
return false;
|
|
1694
|
+
}
|
|
1695
|
+
const indexed = buildSchemaIndex(rawSchema);
|
|
1696
|
+
const sourceResult = resolveNodeSource(rawSchema, indexed, payload.nodeId);
|
|
1697
|
+
if (!sourceResult.ok) {
|
|
1698
|
+
console.warn(`[dragcraft/core] REMOVE_NODE: node "${payload.nodeId}" not found`);
|
|
1699
|
+
return sourceResult;
|
|
1700
|
+
}
|
|
1701
|
+
const source = sourceResult.value;
|
|
1702
|
+
const node = source.location.node;
|
|
1703
|
+
const meta = registry.getWidget(node.type);
|
|
1704
|
+
if (!resolveBehavior(meta?.deletable, {
|
|
1705
|
+
node,
|
|
1706
|
+
schema: rawSchema
|
|
1707
|
+
})) return {
|
|
1708
|
+
ok: false,
|
|
1709
|
+
code: "NODE_NOT_DELETABLE"
|
|
1710
|
+
};
|
|
1711
|
+
if (node.container && !meta?.container) return {
|
|
1712
|
+
ok: false,
|
|
1713
|
+
code: "UNRESOLVED_CONTAINER_READ_ONLY"
|
|
1714
|
+
};
|
|
1715
|
+
if (source.destination.kind === "container") {
|
|
1716
|
+
const sourceDestination = source.destination;
|
|
1717
|
+
const owner = indexed.index.get(sourceDestination.containerId)?.node;
|
|
1718
|
+
const definition = owner && registry.getWidget(owner.type)?.container;
|
|
1719
|
+
if (!definition) return {
|
|
1720
|
+
ok: false,
|
|
1721
|
+
code: "UNRESOLVED_CONTAINER_READ_ONLY"
|
|
1722
|
+
};
|
|
1723
|
+
const variant = owner?.container && definition.variants[owner.container.variant];
|
|
1724
|
+
const region = variant?.regions.find((item) => item.id === sourceDestination.regionId);
|
|
1725
|
+
if (!variant || !region) return {
|
|
1726
|
+
ok: false,
|
|
1727
|
+
code: "UNRESOLVED_CONTAINER_READ_ONLY"
|
|
1728
|
+
};
|
|
1729
|
+
if (source.children.length - 1 < (region.constraints?.minItems ?? 0)) return {
|
|
1730
|
+
ok: false,
|
|
1731
|
+
code: "CONTAINER_REGION_MIN_ITEMS"
|
|
1732
|
+
};
|
|
1733
|
+
const lockedIndices = getLockedIndicesFromNodes(source.children, registry, rawSchema);
|
|
1734
|
+
if (!isRemoveAllowed(source.index, lockedIndices)) return {
|
|
1735
|
+
ok: false,
|
|
1736
|
+
code: "SORTABLE_LOCK_VIOLATION"
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
if (source.destination.kind === "root") {
|
|
1740
|
+
const layout = node ? resolveNodeLayout(node, registry) : null;
|
|
1741
|
+
if (node && layout && layout.sortScope !== false) {
|
|
1742
|
+
const scopeEntries = getSortScopeEntries(createLayoutPlan(rawSchema, registry), layout.sortScope);
|
|
1743
|
+
const removeIndex = scopeEntries.findIndex((entry) => entry.node.id === payload.nodeId);
|
|
1744
|
+
if (removeIndex !== -1) {
|
|
1745
|
+
const lockedIndices = getLockedIndicesFromEntries(scopeEntries, registry, rawSchema);
|
|
1746
|
+
if (lockedIndices.size > 0 && !isRemoveAllowed(removeIndex, lockedIndices)) {
|
|
1747
|
+
console.warn(`[dragcraft/core] REMOVE_NODE: blocked by sortable constraint (removing index ${removeIndex} would shift locked widgets)`);
|
|
1748
|
+
return false;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
const removedIds = collectSubtreeIds(node);
|
|
1754
|
+
const draftIndex = buildSchemaIndex(ctx.draft);
|
|
1755
|
+
const draftSource = resolveNodeSource(ctx.draft, draftIndex, payload.nodeId);
|
|
1756
|
+
if (!draftSource.ok) return draftSource;
|
|
1757
|
+
draftSource.value.children.splice(draftSource.value.index, 1);
|
|
1758
|
+
if (store.selectedNodeId.value && removedIds.has(store.selectedNodeId.value)) store.selectNode(null);
|
|
1759
|
+
if (store.hoveredNodeId.value && removedIds.has(store.hoveredNodeId.value)) store.hoverNode(null);
|
|
1760
|
+
return {
|
|
1761
|
+
ok: true,
|
|
1762
|
+
eventPayload: {
|
|
1763
|
+
nodeId: payload.nodeId,
|
|
1764
|
+
source: source.destination
|
|
1765
|
+
}
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
//#endregion
|
|
1769
|
+
//#region src/merge-record.ts
|
|
1770
|
+
const BLOCKED_RECORD_KEYS = /* @__PURE__ */ new Set([
|
|
1771
|
+
"__proto__",
|
|
1772
|
+
"prototype",
|
|
1773
|
+
"constructor"
|
|
1774
|
+
]);
|
|
1775
|
+
function isPlainRecord(value) {
|
|
1776
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
1777
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1778
|
+
return prototype === Object.prototype || prototype === null;
|
|
1779
|
+
}
|
|
1780
|
+
function mergeRecord(target, patch) {
|
|
1781
|
+
let changed = false;
|
|
1782
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
1783
|
+
if (BLOCKED_RECORD_KEYS.has(key)) continue;
|
|
1784
|
+
const current = target[key];
|
|
1785
|
+
if (Object.hasOwn(target, key) && isPlainRecord(current) && isPlainRecord(value)) changed = mergeRecord(current, value) || changed;
|
|
1786
|
+
else if (!Object.hasOwn(target, key) || !Object.is(current, value)) {
|
|
1787
|
+
target[key] = value;
|
|
1788
|
+
changed = true;
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
return changed;
|
|
1792
|
+
}
|
|
1793
|
+
//#endregion
|
|
1794
|
+
//#region src/commands/set-global-config.ts
|
|
1795
|
+
function setGlobalConfigHandler(ctx, payload) {
|
|
1796
|
+
return {
|
|
1797
|
+
ok: true,
|
|
1798
|
+
changed: mergeRecord(ctx.draft.globalConfig, payload.config)
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
//#endregion
|
|
1802
|
+
//#region src/commands/update-props.ts
|
|
1803
|
+
function updatePropsHandler(ctx, payload) {
|
|
1804
|
+
const rawSchema = ctx.draft;
|
|
1805
|
+
const node = findNodeById(rawSchema.root, payload.nodeId);
|
|
1806
|
+
if (!node) {
|
|
1807
|
+
console.warn(`[dragcraft/core] UPDATE_PROPS: node "${payload.nodeId}" not found`);
|
|
1808
|
+
return false;
|
|
1809
|
+
}
|
|
1810
|
+
let changed = mergeRecord(node.props, payload.props);
|
|
1811
|
+
if (payload.style) {
|
|
1812
|
+
const style = node.style ?? {};
|
|
1813
|
+
const styleChanged = mergeRecord(style, payload.style);
|
|
1814
|
+
if (!node.style && styleChanged) node.style = style;
|
|
1815
|
+
changed = styleChanged || changed;
|
|
1816
|
+
}
|
|
1817
|
+
return {
|
|
1818
|
+
ok: true,
|
|
1819
|
+
changed
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
//#endregion
|
|
1823
|
+
//#region src/container-definition.ts
|
|
1824
|
+
const RESERVED_IDS = /* @__PURE__ */ new Set([
|
|
1825
|
+
"__proto__",
|
|
1826
|
+
"prototype",
|
|
1827
|
+
"constructor"
|
|
1828
|
+
]);
|
|
1829
|
+
function isRecord$1(value) {
|
|
1830
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1831
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1832
|
+
return prototype === Object.prototype || prototype === null;
|
|
1833
|
+
}
|
|
1834
|
+
function validateContainerDefinition(definition) {
|
|
1835
|
+
const errors = [];
|
|
1836
|
+
if (!isRecord$1(definition)) return {
|
|
1837
|
+
valid: false,
|
|
1838
|
+
errors: [{
|
|
1839
|
+
code: "CONTAINER_DEFINITION_INVALID",
|
|
1840
|
+
path: ""
|
|
1841
|
+
}]
|
|
1842
|
+
};
|
|
1843
|
+
const variants = definition.variants;
|
|
1844
|
+
if (!isRecord$1(variants)) return {
|
|
1845
|
+
valid: false,
|
|
1846
|
+
errors: [{
|
|
1847
|
+
code: "CONTAINER_VARIANTS_INVALID",
|
|
1848
|
+
path: "variants"
|
|
1849
|
+
}]
|
|
1850
|
+
};
|
|
1851
|
+
if (typeof definition.defaultVariant !== "string" || !Object.hasOwn(variants, definition.defaultVariant)) errors.push({
|
|
1852
|
+
code: "CONTAINER_DEFAULT_VARIANT_MISSING",
|
|
1853
|
+
path: "defaultVariant"
|
|
1854
|
+
});
|
|
1855
|
+
for (const [variantId, variantValue] of Object.entries(variants)) {
|
|
1856
|
+
const variantPath = `variants.${variantId}`;
|
|
1857
|
+
if (!variantId || RESERVED_IDS.has(variantId)) errors.push({
|
|
1858
|
+
code: "CONTAINER_VARIANT_ID_RESERVED",
|
|
1859
|
+
path: variantPath
|
|
1860
|
+
});
|
|
1861
|
+
if (!isRecord$1(variantValue)) {
|
|
1862
|
+
errors.push({
|
|
1863
|
+
code: "CONTAINER_VARIANT_INVALID",
|
|
1864
|
+
path: variantPath
|
|
1865
|
+
});
|
|
1866
|
+
continue;
|
|
1867
|
+
}
|
|
1868
|
+
if (!Array.isArray(variantValue.regions)) {
|
|
1869
|
+
errors.push({
|
|
1870
|
+
code: "CONTAINER_REGIONS_INVALID",
|
|
1871
|
+
path: `${variantPath}.regions`
|
|
1872
|
+
});
|
|
1873
|
+
continue;
|
|
1874
|
+
}
|
|
1875
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1876
|
+
for (const [index, regionValue] of variantValue.regions.entries()) {
|
|
1877
|
+
const path = `${variantPath}.regions.${index}`;
|
|
1878
|
+
if (!isRecord$1(regionValue)) {
|
|
1879
|
+
errors.push({
|
|
1880
|
+
code: "CONTAINER_REGION_INVALID",
|
|
1881
|
+
path
|
|
1882
|
+
});
|
|
1883
|
+
continue;
|
|
1884
|
+
}
|
|
1885
|
+
const regionId = regionValue.id;
|
|
1886
|
+
if (typeof regionId !== "string" || !regionId || RESERVED_IDS.has(regionId)) errors.push({
|
|
1887
|
+
code: "CONTAINER_REGION_ID_RESERVED",
|
|
1888
|
+
path
|
|
1889
|
+
});
|
|
1890
|
+
if (typeof regionId === "string" && seen.has(regionId)) errors.push({
|
|
1891
|
+
code: "CONTAINER_REGION_ID_DUPLICATE",
|
|
1892
|
+
path
|
|
1893
|
+
});
|
|
1894
|
+
if (typeof regionId === "string") seen.add(regionId);
|
|
1895
|
+
const constraints = regionValue.constraints;
|
|
1896
|
+
if (constraints !== void 0 && !isRecord$1(constraints)) {
|
|
1897
|
+
errors.push({
|
|
1898
|
+
code: "CONTAINER_CONSTRAINTS_INVALID",
|
|
1899
|
+
path: `${path}.constraints`
|
|
1900
|
+
});
|
|
1901
|
+
continue;
|
|
1902
|
+
}
|
|
1903
|
+
const minItems = constraints?.minItems ?? 0;
|
|
1904
|
+
const maxItems = constraints?.maxItems ?? Number.POSITIVE_INFINITY;
|
|
1905
|
+
const maxItemsInvalid = constraints?.maxItems !== void 0 && (!Number.isInteger(maxItems) || maxItems < 0);
|
|
1906
|
+
if (!Number.isInteger(minItems) || minItems < 0 || maxItemsInvalid || minItems > maxItems) errors.push({
|
|
1907
|
+
code: "CONTAINER_CARDINALITY_INVALID",
|
|
1908
|
+
path: `${path}.constraints`
|
|
1909
|
+
});
|
|
1910
|
+
for (const listName of ["includeTypes", "excludeTypes"]) {
|
|
1911
|
+
const typeIds = constraints?.[listName];
|
|
1912
|
+
if (typeIds !== void 0 && (!Array.isArray(typeIds) || typeIds.some((typeId) => typeof typeId !== "string" || typeId.length === 0))) errors.push({
|
|
1913
|
+
code: "CONTAINER_TYPE_ID_INVALID",
|
|
1914
|
+
path: `${path}.constraints.${listName}`
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
return {
|
|
1920
|
+
valid: errors.length === 0,
|
|
1921
|
+
errors
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
//#endregion
|
|
1925
|
+
//#region src/container-plan.ts
|
|
1926
|
+
function createContainerPlan(node, registry) {
|
|
1927
|
+
const definition = registry.getWidget(node.type)?.container;
|
|
1928
|
+
if (!node.container || !definition) return {
|
|
1929
|
+
ok: false,
|
|
1930
|
+
code: "CONTAINER_UNRESOLVED",
|
|
1931
|
+
containerId: node.id
|
|
1932
|
+
};
|
|
1933
|
+
const variant = definition.variants[node.container.variant];
|
|
1934
|
+
if (!variant) return {
|
|
1935
|
+
ok: false,
|
|
1936
|
+
code: "CONTAINER_VARIANT_UNKNOWN",
|
|
1937
|
+
containerId: node.id
|
|
1938
|
+
};
|
|
1939
|
+
return {
|
|
1940
|
+
ok: true,
|
|
1941
|
+
plan: {
|
|
1942
|
+
containerId: node.id,
|
|
1943
|
+
variant,
|
|
1944
|
+
regions: variant.regions.map((region) => {
|
|
1945
|
+
const nodes = node.container.regions[region.id] ?? [];
|
|
1946
|
+
return {
|
|
1947
|
+
definition: region,
|
|
1948
|
+
nodes,
|
|
1949
|
+
isEmpty: nodes.length === 0
|
|
1950
|
+
};
|
|
1951
|
+
})
|
|
1952
|
+
}
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1955
|
+
//#endregion
|
|
1956
|
+
//#region src/event-hub.ts
|
|
1957
|
+
var EventHub = class {
|
|
1958
|
+
emitter;
|
|
1959
|
+
constructor() {
|
|
1960
|
+
this.emitter = new EventEmitter();
|
|
1961
|
+
}
|
|
1962
|
+
on(event, listener) {
|
|
1963
|
+
this.emitter.on(event, listener);
|
|
1964
|
+
}
|
|
1965
|
+
off(event, listener) {
|
|
1966
|
+
this.emitter.off(event, listener);
|
|
1967
|
+
}
|
|
1968
|
+
emit(event, ...args) {
|
|
1969
|
+
this.emitter.emit(event, ...args);
|
|
1970
|
+
}
|
|
1971
|
+
once(event, listener) {
|
|
1972
|
+
this.emitter.once(event, listener);
|
|
1973
|
+
}
|
|
1974
|
+
clear() {
|
|
1975
|
+
this.emitter.clear();
|
|
1976
|
+
}
|
|
1977
|
+
};
|
|
1978
|
+
function createEventHub() {
|
|
1979
|
+
return new EventHub();
|
|
1980
|
+
}
|
|
1981
|
+
//#endregion
|
|
1982
|
+
//#region src/registry.ts
|
|
1983
|
+
function isRecord(value) {
|
|
1984
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1985
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1986
|
+
return prototype === Object.prototype || prototype === null;
|
|
1987
|
+
}
|
|
1988
|
+
function createRegistry() {
|
|
1989
|
+
const widgets = /* @__PURE__ */ new Map();
|
|
1990
|
+
let globalConfigSchema;
|
|
1991
|
+
function registerWidget(meta) {
|
|
1992
|
+
if (!isRecord(meta)) {
|
|
1993
|
+
console.warn("[dragcraft/core] registerWidget: widget metadata must be a plain record");
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
if (!meta.type || typeof meta.type !== "string") {
|
|
1997
|
+
console.warn("[dragcraft/core] registerWidget: widget meta must have a non-empty \"type\" string");
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
if (!meta.title || typeof meta.title !== "string") {
|
|
2001
|
+
console.warn(`[dragcraft/core] registerWidget: widget "${meta.type}" must have a non-empty "title" string`);
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
2004
|
+
if (meta.container !== void 0) {
|
|
2005
|
+
const validation = validateContainerDefinition(meta.container);
|
|
2006
|
+
if (!validation.valid) {
|
|
2007
|
+
const codes = validation.errors.map((error) => error.code).join(", ");
|
|
2008
|
+
console.warn(`[dragcraft/core] registerWidget: widget "${meta.type}" has an invalid container definition: ${codes}`);
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
if (widgets.has(meta.type)) console.warn(`[dragcraft/core] Widget type "${meta.type}" is already registered, overwriting.`);
|
|
2013
|
+
widgets.set(meta.type, meta);
|
|
2014
|
+
}
|
|
2015
|
+
function registerGlobalConfigSchema(schema) {
|
|
2016
|
+
globalConfigSchema = schema;
|
|
2017
|
+
}
|
|
2018
|
+
function registerGlobalConfigFormSchema(schema) {
|
|
2019
|
+
globalConfigSchema = schema;
|
|
2020
|
+
}
|
|
2021
|
+
function getWidget(type) {
|
|
2022
|
+
return widgets.get(type);
|
|
2023
|
+
}
|
|
2024
|
+
function getGlobalConfigSchema() {
|
|
2025
|
+
return globalConfigSchema;
|
|
2026
|
+
}
|
|
2027
|
+
function getAllWidgets() {
|
|
2028
|
+
return Array.from(widgets.values());
|
|
2029
|
+
}
|
|
2030
|
+
return {
|
|
2031
|
+
registerWidget,
|
|
2032
|
+
registerGlobalConfigSchema,
|
|
2033
|
+
registerGlobalConfigFormSchema,
|
|
2034
|
+
getWidget,
|
|
2035
|
+
getGlobalConfigSchema,
|
|
2036
|
+
getAllWidgets
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
//#endregion
|
|
2040
|
+
//#region src/schema-store.ts
|
|
2041
|
+
function createDefaultSchema() {
|
|
2042
|
+
return {
|
|
2043
|
+
version: DEFAULT_SCHEMA_VERSION,
|
|
2044
|
+
globalConfig: {},
|
|
2045
|
+
root: {
|
|
2046
|
+
id: "root",
|
|
2047
|
+
type: "root",
|
|
2048
|
+
props: {},
|
|
2049
|
+
children: []
|
|
2050
|
+
}
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
function createSchemaStore(initialSchema, onSelectionChange) {
|
|
2054
|
+
const schema = shallowRef(deepFreeze(initialSchema ? cloneSchema(initialSchema) : createDefaultSchema()));
|
|
2055
|
+
const selectedNodeId = ref(null);
|
|
2056
|
+
const hoveredNodeId = ref(null);
|
|
2057
|
+
const dragTarget = ref(null);
|
|
2058
|
+
let schemaIndex = null;
|
|
2059
|
+
function getSchema() {
|
|
2060
|
+
return cloneSchema(schema.value);
|
|
2061
|
+
}
|
|
2062
|
+
function getSnapshot() {
|
|
2063
|
+
return schema.value;
|
|
2064
|
+
}
|
|
2065
|
+
function replaceSnapshot(snapshot) {
|
|
2066
|
+
schemaIndex = null;
|
|
2067
|
+
schema.value = snapshot;
|
|
2068
|
+
}
|
|
2069
|
+
function setSchema(newSchema) {
|
|
2070
|
+
replaceSnapshot(deepFreeze(cloneSchema(newSchema)));
|
|
2071
|
+
}
|
|
2072
|
+
function commitSchema(newSchema) {
|
|
2073
|
+
const snapshot = deepFreeze(newSchema);
|
|
2074
|
+
replaceSnapshot(snapshot);
|
|
2075
|
+
return snapshot;
|
|
2076
|
+
}
|
|
2077
|
+
function restoreSnapshot(snapshot) {
|
|
2078
|
+
replaceSnapshot(snapshot);
|
|
2079
|
+
}
|
|
2080
|
+
function selectNode(id) {
|
|
2081
|
+
selectedNodeId.value = id;
|
|
2082
|
+
onSelectionChange?.(id);
|
|
2083
|
+
}
|
|
2084
|
+
function hoverNode(id) {
|
|
2085
|
+
hoveredNodeId.value = id;
|
|
2086
|
+
}
|
|
2087
|
+
function setDragTarget(target) {
|
|
2088
|
+
dragTarget.value = target ? { ...target } : null;
|
|
2089
|
+
}
|
|
2090
|
+
function getNodeById(id) {
|
|
2091
|
+
if (schema.value.root.id === id) return schema.value.root;
|
|
2092
|
+
schemaIndex ??= buildSchemaIndex(schema.value);
|
|
2093
|
+
return schemaIndex.index.get(id)?.node ?? null;
|
|
2094
|
+
}
|
|
2095
|
+
return {
|
|
2096
|
+
schema,
|
|
2097
|
+
selectedNodeId,
|
|
2098
|
+
hoveredNodeId,
|
|
2099
|
+
dragTarget,
|
|
2100
|
+
getSchema,
|
|
2101
|
+
getSnapshot,
|
|
2102
|
+
setSchema,
|
|
2103
|
+
commitSchema,
|
|
2104
|
+
restoreSnapshot,
|
|
2105
|
+
selectNode,
|
|
2106
|
+
hoverNode,
|
|
2107
|
+
setDragTarget,
|
|
2108
|
+
getNodeById
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
//#endregion
|
|
2112
|
+
//#region src/engine.ts
|
|
2113
|
+
function createEngine(options) {
|
|
2114
|
+
const maxHistorySize = options?.maxHistorySize ?? 50;
|
|
2115
|
+
const eventHub = createEventHub();
|
|
2116
|
+
const schemaStore = createSchemaStore(void 0, (id) => {
|
|
2117
|
+
eventHub.emit(EventName.SELECTION_CHANGED, id);
|
|
2118
|
+
});
|
|
2119
|
+
const store = {
|
|
2120
|
+
schema: readonly(schemaStore.schema),
|
|
2121
|
+
selectedNodeId: readonly(schemaStore.selectedNodeId),
|
|
2122
|
+
hoveredNodeId: readonly(schemaStore.hoveredNodeId),
|
|
2123
|
+
dragTarget: readonly(schemaStore.dragTarget),
|
|
2124
|
+
selectNode: schemaStore.selectNode,
|
|
2125
|
+
hoverNode: schemaStore.hoverNode,
|
|
2126
|
+
setDragTarget: schemaStore.setDragTarget
|
|
2127
|
+
};
|
|
2128
|
+
const registry = createRegistry();
|
|
2129
|
+
const history = createHistoryManager(schemaStore, eventHub, maxHistorySize);
|
|
2130
|
+
const commandBus = createCommandBus(schemaStore, registry, eventHub, history);
|
|
2131
|
+
const state = {
|
|
2132
|
+
getSchema: schemaStore.getSnapshot,
|
|
2133
|
+
getNodeById: (id) => {
|
|
2134
|
+
return schemaStore.getNodeById(id);
|
|
2135
|
+
},
|
|
2136
|
+
getSelectedNodeId: () => store.selectedNodeId.value,
|
|
2137
|
+
getHoveredNodeId: () => store.hoveredNodeId.value,
|
|
2138
|
+
getDragTarget: () => {
|
|
2139
|
+
const target = schemaStore.dragTarget.value;
|
|
2140
|
+
return target ? { ...target } : null;
|
|
2141
|
+
}
|
|
2142
|
+
};
|
|
2143
|
+
commandBus.registerHandler(CommandType.ADD_NODE, addNodeHandler);
|
|
2144
|
+
commandBus.registerHandler(CommandType.CHANGE_CONTAINER_VARIANT, changeContainerVariantHandler);
|
|
2145
|
+
commandBus.registerHandler(CommandType.DUPLICATE_NODE, duplicateNodeHandler);
|
|
2146
|
+
commandBus.registerHandler(CommandType.MOVE_NODE, moveNodeHandler);
|
|
2147
|
+
commandBus.registerHandler(CommandType.REMOVE_NODE, removeNodeHandler);
|
|
2148
|
+
commandBus.registerHandler(CommandType.UPDATE_PROPS, updatePropsHandler);
|
|
2149
|
+
commandBus.registerHandler(CommandType.SET_GLOBAL_CONFIG, setGlobalConfigHandler);
|
|
2150
|
+
function execute(command) {
|
|
2151
|
+
return commandBus.execute(command);
|
|
2152
|
+
}
|
|
2153
|
+
function registerHandler(type, handler) {
|
|
2154
|
+
commandBus.registerHandler(type, handler);
|
|
2155
|
+
}
|
|
2156
|
+
function registerWidget(meta) {
|
|
2157
|
+
registry.registerWidget(meta);
|
|
2158
|
+
}
|
|
2159
|
+
const migrations = [];
|
|
2160
|
+
function registerMigration(migration) {
|
|
2161
|
+
migrations.push(migration);
|
|
2162
|
+
}
|
|
2163
|
+
function migrateSchema(schema) {
|
|
2164
|
+
let current = schema;
|
|
2165
|
+
const applied = /* @__PURE__ */ new Set();
|
|
2166
|
+
let changed = true;
|
|
2167
|
+
while (changed) {
|
|
2168
|
+
changed = false;
|
|
2169
|
+
for (const m of migrations) {
|
|
2170
|
+
const key = `${m.fromVersion}->${m.toVersion}`;
|
|
2171
|
+
if (current.version === m.fromVersion && !applied.has(key)) {
|
|
2172
|
+
current = m.migrate(current);
|
|
2173
|
+
applied.add(key);
|
|
2174
|
+
changed = true;
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
return current;
|
|
2179
|
+
}
|
|
2180
|
+
function exportSchema() {
|
|
2181
|
+
return schemaStore.getSchema();
|
|
2182
|
+
}
|
|
2183
|
+
function importSchema(schema) {
|
|
2184
|
+
const structuralDiagnostics = collectSchemaStructuralDiagnostics(schema);
|
|
2185
|
+
if (structuralDiagnostics.length > 0) {
|
|
2186
|
+
console.warn("[dragcraft/core] importSchema: invalid schema, missing root or version");
|
|
2187
|
+
return {
|
|
2188
|
+
ok: false,
|
|
2189
|
+
diagnostics: structuralDiagnostics
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
const validation = validateSchema(migrateSchema(cloneSchema(schema)), registry);
|
|
2193
|
+
if (!validation.valid) return {
|
|
2194
|
+
ok: false,
|
|
2195
|
+
diagnostics: validation.diagnostics
|
|
2196
|
+
};
|
|
2197
|
+
schemaStore.setSchema(validation.schema);
|
|
2198
|
+
history.clear();
|
|
2199
|
+
eventHub.emit(EventName.SCHEMA_CHANGED, schemaStore.getSnapshot());
|
|
2200
|
+
return {
|
|
2201
|
+
ok: true,
|
|
2202
|
+
diagnostics: validation.diagnostics
|
|
2203
|
+
};
|
|
2204
|
+
}
|
|
2205
|
+
function dispose() {
|
|
2206
|
+
eventHub.clear();
|
|
2207
|
+
history.clear();
|
|
2208
|
+
schemaStore.selectNode(null);
|
|
2209
|
+
schemaStore.hoverNode(null);
|
|
2210
|
+
schemaStore.setDragTarget(null);
|
|
2211
|
+
}
|
|
2212
|
+
return {
|
|
2213
|
+
store,
|
|
2214
|
+
state,
|
|
2215
|
+
commandBus,
|
|
2216
|
+
history,
|
|
2217
|
+
registry,
|
|
2218
|
+
eventHub,
|
|
2219
|
+
execute,
|
|
2220
|
+
registerHandler,
|
|
2221
|
+
registerWidget,
|
|
2222
|
+
registerMigration,
|
|
2223
|
+
migrateSchema,
|
|
2224
|
+
exportSchema,
|
|
2225
|
+
importSchema,
|
|
2226
|
+
dispose
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
//#endregion
|
|
2230
|
+
//#region src/style.ts
|
|
2231
|
+
const LENGTH_STYLE_KEYS = /* @__PURE__ */ new Set([
|
|
2232
|
+
"bottom",
|
|
2233
|
+
"height",
|
|
2234
|
+
"left",
|
|
2235
|
+
"margin",
|
|
2236
|
+
"marginBlock",
|
|
2237
|
+
"marginBlockEnd",
|
|
2238
|
+
"marginBlockStart",
|
|
2239
|
+
"marginBottom",
|
|
2240
|
+
"marginInline",
|
|
2241
|
+
"marginInlineEnd",
|
|
2242
|
+
"marginInlineStart",
|
|
2243
|
+
"marginLeft",
|
|
2244
|
+
"marginRight",
|
|
2245
|
+
"marginTop",
|
|
2246
|
+
"maxHeight",
|
|
2247
|
+
"maxWidth",
|
|
2248
|
+
"minHeight",
|
|
2249
|
+
"minWidth",
|
|
2250
|
+
"padding",
|
|
2251
|
+
"paddingBlock",
|
|
2252
|
+
"paddingBlockEnd",
|
|
2253
|
+
"paddingBlockStart",
|
|
2254
|
+
"paddingBottom",
|
|
2255
|
+
"paddingInline",
|
|
2256
|
+
"paddingInlineEnd",
|
|
2257
|
+
"paddingInlineStart",
|
|
2258
|
+
"paddingLeft",
|
|
2259
|
+
"paddingRight",
|
|
2260
|
+
"paddingTop",
|
|
2261
|
+
"right",
|
|
2262
|
+
"top",
|
|
2263
|
+
"width"
|
|
2264
|
+
]);
|
|
2265
|
+
function normalizeStyleValueMap(style) {
|
|
2266
|
+
if (!style) return void 0;
|
|
2267
|
+
const normalized = {};
|
|
2268
|
+
for (const [key, value] of Object.entries(style)) normalized[key] = typeof value === "number" && value !== 0 && LENGTH_STYLE_KEYS.has(key) ? `${value}px` : value;
|
|
2269
|
+
return normalized;
|
|
2270
|
+
}
|
|
2271
|
+
//#endregion
|
|
2272
|
+
//#region src/types.ts
|
|
2273
|
+
function commandFailure(code, reason = {}) {
|
|
2274
|
+
return {
|
|
2275
|
+
ok: false,
|
|
2276
|
+
code,
|
|
2277
|
+
...reason
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
//#endregion
|
|
2281
|
+
export { CommandType, DEFAULT_LAYER, DEFAULT_LAYOUT_REGION, DEFAULT_MAX_HISTORY_SIZE, DEFAULT_SCHEMA_VERSION, DEFAULT_SORT_SCOPE, EventHub, EventName, addNodeHandler, buildSchemaIndex, changeContainerVariantHandler, clampInsertIndex, cloneNodeSubtree, collectSubtreeIds, commandFailure, createCommandBus, createContainerPlan, createContainerState, createDefaultSchema, createEngine, createEventHub, createHistoryManager, createLayoutPlan, createRegisteredNode, createRegistry, duplicateNodeHandler, findIndexedNode, findNearestValidIndex, findNodeById, findParentNode, getLayoutRegionEntries, getLockedIndices, getLockedIndicesFromNodes, getSortScopeEntries, getSortScopeNodes, getSortableArrayIndexForInsert, getValidDropIndices, insertNodeIntoTree, isInsertAllowed, isMoveAllowed, isRemoveAllowed, moveNodeHandler, normalizeStyleValueMap, removeNodeFromTree, removeNodeHandler, resolveBehavior, resolveCreatable, resolveDestination, resolveNodeLayout, resolveNodeSource, resolvePlacementDecision, setGlobalConfigHandler, stripPageLayout, updatePropsHandler, validateContainerDefinition, validateSchema, walkFlatChildren };
|