@grafloria/element 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/package.json +1 -1
- package/src/lib/dashboard-kit/dashboard.d.ts +34 -0
- package/src/lib/dashboard-kit/dashboard.js +225 -43
- package/src/lib/dashboard-kit/grid-binder.js +17 -3
- package/src/lib/load.js +57 -17
package/README.md
CHANGED
|
@@ -44,6 +44,8 @@ Worst case, importing the **entire** public surface of `@grafloria/element`
|
|
|
44
44
|
|
|
45
45
|
A real app importing only what it uses ships less. Reproduce it in two minutes:
|
|
46
46
|
|
|
47
|
+
**Docs:** [JavaScript in 10 minutes](https://grafloria.com/learn/javascript/) · [guides & concepts](https://grafloria.com/learn/) · [111 live demos](https://grafloria.com/demos/)
|
|
48
|
+
|
|
47
49
|
```sh
|
|
48
50
|
npm i -D esbuild @grafloria/element
|
|
49
51
|
echo "export * from '@grafloria/element';" > entry.mjs
|
package/package.json
CHANGED
|
@@ -64,6 +64,30 @@ export interface DashboardWidgetSpec {
|
|
|
64
64
|
data?: Record<string, unknown>;
|
|
65
65
|
/** Optional title used by the built-in fallback renderer. */
|
|
66
66
|
title?: string;
|
|
67
|
+
/**
|
|
68
|
+
* CONTAINMENT. A widget carrying `widgets` is a CONTAINER: it mounts as a
|
|
69
|
+
* member group (a locked slab in its parent's grid, exactly like a view's
|
|
70
|
+
* board one level down) with its own nested pack grid bound on it. Children
|
|
71
|
+
* lay out inside its frame; dragging a tile across the boundary adopts it
|
|
72
|
+
* live in either direction, and one undo restores the whole gesture.
|
|
73
|
+
* Containers may nest — tested to TWO levels; deeper is not exercised by
|
|
74
|
+
* the gates and rides at your own risk. A container renders no card of its
|
|
75
|
+
* own (`kind`/`data` are carried for your bookkeeping and serialization,
|
|
76
|
+
* not painted).
|
|
77
|
+
*/
|
|
78
|
+
widgets?: DashboardWidgetSpec[];
|
|
79
|
+
/**
|
|
80
|
+
* Container only: column count of the INNER grid (default: the parent
|
|
81
|
+
* board's column count).
|
|
82
|
+
*/
|
|
83
|
+
columns?: number;
|
|
84
|
+
/**
|
|
85
|
+
* Container only: the inner grid's designed row count. A child resized past
|
|
86
|
+
* it ESCALATES — the container's slab grows a row in the parent board (the
|
|
87
|
+
* ratchet), instead of the child overflowing the frame. Default: the row
|
|
88
|
+
* extent of the declared children.
|
|
89
|
+
*/
|
|
90
|
+
maxRows?: number;
|
|
67
91
|
}
|
|
68
92
|
/** One board. Multiple views are the tab pattern: only one is on-camera. */
|
|
69
93
|
export interface DashboardViewSpec {
|
|
@@ -316,7 +340,17 @@ export interface DashboardHandleContext {
|
|
|
316
340
|
groups: Map<string, GroupModel>;
|
|
317
341
|
binders: Map<string, DashboardGridHandle>;
|
|
318
342
|
specById: Map<string, DashboardWidgetSpec>;
|
|
343
|
+
/** Widget id → the BOARD that owns it (a view id, or a container id). */
|
|
319
344
|
viewOfWidget: Map<string, string>;
|
|
345
|
+
/** Every board group — the views PLUS every container. Views also live in
|
|
346
|
+
* `groups` (the parking map showView drives); containers deliberately do
|
|
347
|
+
* NOT — parking flings a group to OFFSCREEN_X, and a container must follow
|
|
348
|
+
* its parent, not travel on its own. */
|
|
349
|
+
boardGroups: Map<string, GroupModel>;
|
|
350
|
+
/** Board id → its authored widgets array (views and containers alike). */
|
|
351
|
+
boardWidgets: Map<string, DashboardWidgetSpec[]>;
|
|
352
|
+
/** Board id → the VIEW it belongs to (identity for views). */
|
|
353
|
+
viewOfBoard: Map<string, string>;
|
|
320
354
|
hosts: Map<string, HTMLElement>;
|
|
321
355
|
renderWidget: (widget: DashboardWidgetSpec, host: HTMLElement) => void;
|
|
322
356
|
columns: number;
|
|
@@ -129,6 +129,32 @@ function buildWidgetNode(w, rowHeight) {
|
|
|
129
129
|
node.removePort(p.id);
|
|
130
130
|
return node;
|
|
131
131
|
}
|
|
132
|
+
function cloneWidgets(ws) {
|
|
133
|
+
return ws.map((w) => (Object.assign(Object.assign({}, w), (w.widgets ? { widgets: cloneWidgets(w.widgets) } : {}))));
|
|
134
|
+
}
|
|
135
|
+
/** A container's inner column count: authored, else its own span — inner cells
|
|
136
|
+
* then ride the parent's column rhythm, which is what a section reads as. */
|
|
137
|
+
function innerColumnsOf(w) {
|
|
138
|
+
var _a, _b;
|
|
139
|
+
return Math.max(1, (_b = (_a = w.columns) !== null && _a !== void 0 ? _a : w.span) !== null && _b !== void 0 ? _b : 3);
|
|
140
|
+
}
|
|
141
|
+
/** The row extent of a laid-out widget list (the inner grid's design height). */
|
|
142
|
+
function rowExtentOf(widgets) {
|
|
143
|
+
var _a, _b;
|
|
144
|
+
let max = 1;
|
|
145
|
+
for (const w of widgets)
|
|
146
|
+
max = Math.max(max, ((_a = w.y) !== null && _a !== void 0 ? _a : 0) + ((_b = w.rows) !== null && _b !== void 0 ? _b : 1));
|
|
147
|
+
return max;
|
|
148
|
+
}
|
|
149
|
+
/** Recursive `assignCells`: a container flows in its parent like any widget,
|
|
150
|
+
* and its children flow inside its OWN column count. */
|
|
151
|
+
function assignCellsDeep(widgets, columns) {
|
|
152
|
+
assignCells(widgets, columns);
|
|
153
|
+
for (const w of widgets) {
|
|
154
|
+
if (w.widgets)
|
|
155
|
+
assignCellsDeep(w.widgets, innerColumnsOf(w));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
132
158
|
/** Flow widgets that declared no cell: left-to-right, wrapping at `columns`. */
|
|
133
159
|
function assignCells(widgets, columns) {
|
|
134
160
|
var _a, _b;
|
|
@@ -161,6 +187,38 @@ function assignCells(widgets, columns) {
|
|
|
161
187
|
export function createDashboardHandle(ctx) {
|
|
162
188
|
const { views, groups, binders, specById, viewOfWidget } = ctx;
|
|
163
189
|
const hostOf = (id) => ctx.hosts.get(id);
|
|
190
|
+
/**
|
|
191
|
+
* A board's widgets as a NESTED tree, derived from LIVE membership — not the
|
|
192
|
+
* authored arrays. A cross-boundary drag moves membership through commands
|
|
193
|
+
* (and undo moves it back); the authored arrays do not follow. Deriving from
|
|
194
|
+
* the groups + engines is what makes toJSON() and onLayoutChange report a
|
|
195
|
+
* tile under the container it is actually in, in every one of those states.
|
|
196
|
+
*/
|
|
197
|
+
const treeOf = (boardId) => {
|
|
198
|
+
var _a;
|
|
199
|
+
const g = ctx.boardGroups.get(boardId);
|
|
200
|
+
const b = binders.get(boardId);
|
|
201
|
+
if (!g)
|
|
202
|
+
return [];
|
|
203
|
+
// Cells from the binder's serialisation (largest cached layout — the
|
|
204
|
+
// saving-on-a-phone rule), falling back to the live engine cell.
|
|
205
|
+
const saved = b === null || b === void 0 ? void 0 : b.saveLayout();
|
|
206
|
+
const cellOf = (id) => { var _a; return (_a = saved === null || saved === void 0 ? void 0 : saved.cells.get(id)) !== null && _a !== void 0 ? _a : b === null || b === void 0 ? void 0 : b.cellOf(id); };
|
|
207
|
+
const entries = [];
|
|
208
|
+
for (const memberId of (_a = g.members) !== null && _a !== void 0 ? _a : []) {
|
|
209
|
+
const spec = specById.get(memberId);
|
|
210
|
+
const cell = cellOf(memberId);
|
|
211
|
+
const at = cell ? { x: cell.x, y: cell.y, span: cell.w, rows: cell.h } : {};
|
|
212
|
+
if (ctx.boardGroups.has(memberId)) {
|
|
213
|
+
entries.push(Object.assign(Object.assign(Object.assign({ id: memberId }, (spec !== null && spec !== void 0 ? spec : {})), at), { widgets: treeOf(memberId) }));
|
|
214
|
+
}
|
|
215
|
+
else if (spec) {
|
|
216
|
+
entries.push(Object.assign(Object.assign({}, spec), at));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
entries.sort((p1, p2) => { var _a, _b, _c, _d; return ((_a = p1.y) !== null && _a !== void 0 ? _a : 0) - ((_b = p2.y) !== null && _b !== void 0 ? _b : 0) || ((_c = p1.x) !== null && _c !== void 0 ? _c : 0) - ((_d = p2.x) !== null && _d !== void 0 ? _d : 0); });
|
|
220
|
+
return entries;
|
|
221
|
+
};
|
|
164
222
|
const execCommand = (cmd) => {
|
|
165
223
|
var _a, _b, _c, _d;
|
|
166
224
|
try {
|
|
@@ -237,16 +295,18 @@ export function createDashboardHandle(ctx) {
|
|
|
237
295
|
addWidget(spec, viewId) {
|
|
238
296
|
var _a, _b, _c, _d, _e;
|
|
239
297
|
const vid = viewId !== null && viewId !== void 0 ? viewId : ctx.active;
|
|
240
|
-
|
|
298
|
+
// `vid` may name a view OR a container — both are boards with a group,
|
|
299
|
+
// a binder and an authored array.
|
|
300
|
+
const arr = ctx.boardWidgets.get(vid);
|
|
241
301
|
const model = (_a = ctx.apiRef) === null || _a === void 0 ? void 0 : _a.getModel();
|
|
242
|
-
const group =
|
|
243
|
-
if (!
|
|
302
|
+
const group = ctx.boardGroups.get(vid);
|
|
303
|
+
if (!arr || !model || !group)
|
|
244
304
|
return undefined;
|
|
245
305
|
const w = Object.assign(Object.assign({}, spec), { id: spec.id || `w-${++autoId}`, span: (_b = spec.span) !== null && _b !== void 0 ? _b : 3, rows: (_c = spec.rows) !== null && _c !== void 0 ? _c : 1 });
|
|
246
306
|
// REGISTER FIRST: a custom node mounts exactly once, and the painter
|
|
247
307
|
// returns early for an id the spec does not know — so the widget must be
|
|
248
308
|
// known before the node reaches the model, or it paints blank forever.
|
|
249
|
-
|
|
309
|
+
arr.push(w);
|
|
250
310
|
specById.set(w.id, w);
|
|
251
311
|
viewOfWidget.set(w.id, vid);
|
|
252
312
|
const existing = model.getNode(w.id);
|
|
@@ -292,9 +352,16 @@ export function createDashboardHandle(ctx) {
|
|
|
292
352
|
ids.add(id);
|
|
293
353
|
// Read the SPEC, not the group's member Set: membership is maintained by
|
|
294
354
|
// commands and an in-flight drag can have a widget momentarily reparented.
|
|
295
|
-
// The spec is what the view IS.
|
|
296
|
-
|
|
297
|
-
|
|
355
|
+
// The spec is what the view IS. Containers add themselves AND their
|
|
356
|
+
// subtree — an exported container without its children is an empty frame.
|
|
357
|
+
const walk = (ws) => {
|
|
358
|
+
for (const w of ws) {
|
|
359
|
+
ids.add(w.id);
|
|
360
|
+
if (w.widgets)
|
|
361
|
+
walk(w.widgets);
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
walk((_b = (_a = views.find((v) => v.id === id)) === null || _a === void 0 ? void 0 : _a.widgets) !== null && _b !== void 0 ? _b : []);
|
|
298
365
|
return ids;
|
|
299
366
|
},
|
|
300
367
|
toJSON() {
|
|
@@ -304,13 +371,13 @@ export function createDashboardHandle(ctx) {
|
|
|
304
371
|
// layout its user authored — and the view's `columns` is that count, so
|
|
305
372
|
// feeding this straight back into dashboard() rebuilds the wide board.
|
|
306
373
|
const savedViews = views.map((v) => {
|
|
307
|
-
var _a;
|
|
374
|
+
var _a, _b, _c, _d;
|
|
308
375
|
const saved = (_a = binders.get(v.id)) === null || _a === void 0 ? void 0 : _a.saveLayout();
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}) });
|
|
376
|
+
const live = treeOf(v.id);
|
|
377
|
+
return Object.assign(Object.assign(Object.assign({}, v), (saved ? { columns: saved.columns } : {})), {
|
|
378
|
+
// Live membership when the view is mounted; the authored tree before
|
|
379
|
+
// finalize (a spec serialised without ever rendering keeps its shape).
|
|
380
|
+
widgets: live.length > 0 || ((_d = (_c = (_b = ctx.boardGroups.get(v.id)) === null || _b === void 0 ? void 0 : _b.members) === null || _c === void 0 ? void 0 : _c.size) !== null && _d !== void 0 ? _d : 0) > 0 ? live : v.widgets.map((w) => (Object.assign({}, w))) });
|
|
314
381
|
});
|
|
315
382
|
// Board options come off the LIVE board wherever the handle can see it —
|
|
316
383
|
// `sizing` and `float` are the two a user changes from the toolbar, and
|
|
@@ -319,16 +386,21 @@ export function createDashboardHandle(ctx) {
|
|
|
319
386
|
return Object.assign(Object.assign({}, ctx.optionsBase), { renderWidget: undefined, onLayoutChange: undefined, columns: ctx.columns, gap: ctx.gap, rowHeight: ctx.rowHeight, sizing: handle.getSizing(), float: handle.getFloat(), rtl: handle.getRtl(), views: savedViews });
|
|
320
387
|
},
|
|
321
388
|
dispose() {
|
|
322
|
-
var _a, _b;
|
|
389
|
+
var _a, _b, _c;
|
|
323
390
|
for (const b of binders.values())
|
|
324
391
|
b.dispose();
|
|
325
392
|
binders.clear();
|
|
326
393
|
// The groups finalize() created are ours to clean up — leaving them
|
|
327
394
|
// behind made a rebuild stack a second set of boards on the first.
|
|
328
395
|
const model = (_a = ctx.apiRef) === null || _a === void 0 ? void 0 : _a.getModel();
|
|
329
|
-
|
|
396
|
+
// Deepest first: a container group removed after its parent is an orphan
|
|
397
|
+
// the model never saw inside a board.
|
|
398
|
+
for (const id of [...ctx.boardGroups.keys()].reverse())
|
|
330
399
|
(_b = model === null || model === void 0 ? void 0 : model.removeGroup) === null || _b === void 0 ? void 0 : _b.call(model, id);
|
|
400
|
+
for (const id of groups.keys())
|
|
401
|
+
(_c = model === null || model === void 0 ? void 0 : model.removeGroup) === null || _c === void 0 ? void 0 : _c.call(model, id);
|
|
331
402
|
groups.clear();
|
|
403
|
+
ctx.boardGroups.clear();
|
|
332
404
|
ctx.hosts.clear();
|
|
333
405
|
},
|
|
334
406
|
};
|
|
@@ -407,8 +479,15 @@ export function createDashboardHandle(ctx) {
|
|
|
407
479
|
},
|
|
408
480
|
remove(displaced) {
|
|
409
481
|
var _a, _b;
|
|
482
|
+
if (spec.widgets) {
|
|
483
|
+
// A container is a GROUP, not a node — RemoveNodeCommand would
|
|
484
|
+
// silently no-op and strand the children. Container removal is not
|
|
485
|
+
// supported through the widget handle (yet); say so loudly.
|
|
486
|
+
console.warn('[dashboard] remove() on a container is not supported');
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
410
489
|
const n = node();
|
|
411
|
-
const group =
|
|
490
|
+
const group = ctx.boardGroups.get(viewId);
|
|
412
491
|
const b = binder();
|
|
413
492
|
if (!n || !group || !b)
|
|
414
493
|
return;
|
|
@@ -420,9 +499,12 @@ export function createDashboardHandle(ctx) {
|
|
|
420
499
|
const survivors = (_a = displaced) !== null && _a !== void 0 ? _a : b.planRemoval(id);
|
|
421
500
|
const cmds = [...survivors, new RemoveFromGroupCommand(group.id, id), new RemoveNodeCommand(id)];
|
|
422
501
|
void execCommand(new BatchCommand('Remove widget', cmds));
|
|
423
|
-
const
|
|
424
|
-
if (
|
|
425
|
-
|
|
502
|
+
const arr = ctx.boardWidgets.get(viewId);
|
|
503
|
+
if (arr) {
|
|
504
|
+
const i = arr.findIndex((w) => w.id === id);
|
|
505
|
+
if (i >= 0)
|
|
506
|
+
arr.splice(i, 1);
|
|
507
|
+
}
|
|
426
508
|
specById.delete(id);
|
|
427
509
|
viewOfWidget.delete(id);
|
|
428
510
|
b.sync();
|
|
@@ -440,7 +522,7 @@ export function createDashboardHandle(ctx) {
|
|
|
440
522
|
return handle;
|
|
441
523
|
}
|
|
442
524
|
export function dashboard(options) {
|
|
443
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k
|
|
525
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
444
526
|
ensureDashboardKitStyles();
|
|
445
527
|
const columns = (_a = options.columns) !== null && _a !== void 0 ? _a : DEFAULTS.columns;
|
|
446
528
|
const gap = (_b = options.gap) !== null && _b !== void 0 ? _b : DEFAULTS.gap;
|
|
@@ -448,32 +530,54 @@ export function dashboard(options) {
|
|
|
448
530
|
const boardW = (_d = options.width) !== null && _d !== void 0 ? _d : DEFAULTS.width;
|
|
449
531
|
const boardH = (_e = options.height) !== null && _e !== void 0 ? _e : DEFAULTS.height;
|
|
450
532
|
const views = options.views
|
|
451
|
-
? options.views.map((v) => (Object.assign(Object.assign({}, v), { widgets: v.widgets
|
|
452
|
-
: [{ id: 'main', widgets: ((_f = options.widgets) !== null && _f !== void 0 ? _f : [])
|
|
533
|
+
? options.views.map((v) => (Object.assign(Object.assign({}, v), { widgets: cloneWidgets(v.widgets) })))
|
|
534
|
+
: [{ id: 'main', widgets: cloneWidgets((_f = options.widgets) !== null && _f !== void 0 ? _f : []) }];
|
|
453
535
|
for (const v of views)
|
|
454
|
-
|
|
536
|
+
assignCellsDeep(v.widgets, (_g = v.columns) !== null && _g !== void 0 ? _g : columns);
|
|
455
537
|
// -- the render spec: one custom-HTML node per widget ----------------------
|
|
456
538
|
const nodes = [];
|
|
457
539
|
const specById = new Map();
|
|
458
540
|
const viewOfWidget = new Map();
|
|
459
|
-
|
|
460
|
-
|
|
541
|
+
const boardWidgets = new Map();
|
|
542
|
+
const viewOfBoard = new Map();
|
|
543
|
+
// Recursive walk: a CONTAINER contributes no node (it becomes a group in
|
|
544
|
+
// finalize) but registers like a widget, and its children flatten into the
|
|
545
|
+
// render spec with the container as their board.
|
|
546
|
+
const flatten = (boardId, viewId, widgets) => {
|
|
547
|
+
for (const w of widgets) {
|
|
461
548
|
specById.set(w.id, w);
|
|
462
|
-
viewOfWidget.set(w.id,
|
|
549
|
+
viewOfWidget.set(w.id, boardId);
|
|
550
|
+
if (w.widgets) {
|
|
551
|
+
boardWidgets.set(w.id, w.widgets);
|
|
552
|
+
viewOfBoard.set(w.id, viewId);
|
|
553
|
+
flatten(w.id, viewId, w.widgets);
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
pushWidgetNode(w);
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
const pushWidgetNode = (w) => {
|
|
560
|
+
var _a, _b;
|
|
561
|
+
{
|
|
463
562
|
nodes.push({
|
|
464
563
|
id: w.id,
|
|
465
564
|
type: 'widget',
|
|
466
565
|
position: { x: 0, y: 0 },
|
|
467
566
|
size: { width: 100, height: rowHeight },
|
|
468
567
|
custom: true,
|
|
469
|
-
metadata: Object.assign(Object.assign({ useHTMLLayer: true, widgetKind: (
|
|
568
|
+
metadata: Object.assign(Object.assign({ useHTMLLayer: true, widgetKind: (_a = w.kind) !== null && _a !== void 0 ? _a : 'widget', widgetSpec: (_b = w.data) !== null && _b !== void 0 ? _b : {} }, (w.title !== undefined ? { widgetTitle: w.title } : {})), { columnSpan: w.span, rowSpan: w.rows, gridItem: { columnStart: w.x + 1, columnEnd: w.x + 1 + w.span, rowStart: w.y + 1, rowEnd: w.y + 1 + w.rows } }),
|
|
470
569
|
});
|
|
471
570
|
}
|
|
571
|
+
};
|
|
572
|
+
for (const v of views) {
|
|
573
|
+
boardWidgets.set(v.id, v.widgets);
|
|
574
|
+
viewOfBoard.set(v.id, v.id);
|
|
575
|
+
flatten(v.id, v.id, v.widgets);
|
|
472
576
|
}
|
|
473
577
|
// No renderWidget → the built-in renderers draw the declared `kind` from the
|
|
474
578
|
// developer's own `data` (widgets.ts), unknown kinds landing on the titled
|
|
475
579
|
// frame they always did.
|
|
476
|
-
const renderWidget = (
|
|
580
|
+
const renderWidget = (_h = options.renderWidget) !== null && _h !== void 0 ? _h : defaultWidgetRenderer;
|
|
477
581
|
// -- runtime: ONE shared handle over a boxed context -----------------------
|
|
478
582
|
// The two MUTABLE cells the handle used to close over as free `let`s —
|
|
479
583
|
// `active` (showView reassigns it) and `apiRef` (finalize sets it) — are boxed
|
|
@@ -486,6 +590,9 @@ export function dashboard(options) {
|
|
|
486
590
|
binders: new Map(),
|
|
487
591
|
specById,
|
|
488
592
|
viewOfWidget,
|
|
593
|
+
boardGroups: new Map(),
|
|
594
|
+
boardWidgets,
|
|
595
|
+
viewOfBoard,
|
|
489
596
|
hosts: new Map(),
|
|
490
597
|
renderWidget,
|
|
491
598
|
columns,
|
|
@@ -494,7 +601,7 @@ export function dashboard(options) {
|
|
|
494
601
|
boardW,
|
|
495
602
|
boardH,
|
|
496
603
|
optionsBase: options,
|
|
497
|
-
active: (
|
|
604
|
+
active: (_k = (_j = views[0]) === null || _j === void 0 ? void 0 : _j.id) !== null && _k !== void 0 ? _k : 'main',
|
|
498
605
|
apiRef: null,
|
|
499
606
|
};
|
|
500
607
|
const { binders, groups } = ctx;
|
|
@@ -514,7 +621,7 @@ export function dashboard(options) {
|
|
|
514
621
|
return handle;
|
|
515
622
|
},
|
|
516
623
|
finalize: (api) => {
|
|
517
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p
|
|
624
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
|
|
518
625
|
const a = api;
|
|
519
626
|
if (!a)
|
|
520
627
|
return;
|
|
@@ -545,7 +652,77 @@ export function dashboard(options) {
|
|
|
545
652
|
g.size = { width: (_g = v.width) !== null && _g !== void 0 ? _g : boardW, height: (_h = v.height) !== null && _h !== void 0 ? _h : boardH, depth: 0 };
|
|
546
653
|
g.position = { x: v.id === ctx.active ? 0 : OFFSCREEN_X, y: 0 };
|
|
547
654
|
groups.set(v.id, g);
|
|
548
|
-
|
|
655
|
+
ctx.boardGroups.set(v.id, g);
|
|
656
|
+
mountBoard(v.id, v.id, v.widgets, g);
|
|
657
|
+
binders.set(v.id, bindDashboardGrid(a, g, Object.assign(Object.assign(Object.assign({ columns: (_j = v.columns) !== null && _j !== void 0 ? _j : columns, gap, padding: gap, sizing: (_k = options.sizing) !== null && _k !== void 0 ? _k : 'fit', baseRowHeight: rowHeight, designHeight: (_l = v.height) !== null && _l !== void 0 ? _l : boardH, float: (_m = options.float) !== null && _m !== void 0 ? _m : false, rtl: (_o = options.rtl) !== null && _o !== void 0 ? _o : false }, (options.responsive ? { responsive: options.responsive } : {})), ((_p = options.binder) !== null && _p !== void 0 ? _p : {})), { onGesture: (e) => {
|
|
658
|
+
var _a, _b;
|
|
659
|
+
if (e.type === 'commit')
|
|
660
|
+
reportLayoutChange(v.id);
|
|
661
|
+
(_b = (_a = options.binder) === null || _a === void 0 ? void 0 : _a.onGesture) === null || _b === void 0 ? void 0 : _b.call(_a, e);
|
|
662
|
+
} })));
|
|
663
|
+
}
|
|
664
|
+
handle.showView(ctx.active);
|
|
665
|
+
return;
|
|
666
|
+
/**
|
|
667
|
+
* Mount one board's widgets into its group — and recurse for CONTAINERS.
|
|
668
|
+
* A container is a view's construction one level down: a frameless
|
|
669
|
+
* member group with a slab cell in the PARENT's grid, its own
|
|
670
|
+
* `dashboardBoard` metadata (so `fromDocument()` rebinds it like any
|
|
671
|
+
* board), and a second `bindDashboardGrid` on the same canvas — which
|
|
672
|
+
* registers it as a BinderPeer, so cross-boundary drag, deepest-wins
|
|
673
|
+
* hit-testing and the height-escalation ratchet all apply unchanged.
|
|
674
|
+
*/
|
|
675
|
+
function mountBoard(boardId, viewId, widgets, boardGroup) {
|
|
676
|
+
var _a, _b, _c, _d, _e, _f;
|
|
677
|
+
for (const w of widgets) {
|
|
678
|
+
if (w.widgets) {
|
|
679
|
+
const innerColumns = innerColumnsOf(w);
|
|
680
|
+
const innerRows = (_a = w.maxRows) !== null && _a !== void 0 ? _a : rowExtentOf(w.widgets);
|
|
681
|
+
const cg = new GroupModel({ id: w.id, name: (_b = w.title) !== null && _b !== void 0 ? _b : w.id });
|
|
682
|
+
model.addGroup(cg);
|
|
683
|
+
cg.setMetadata('frameChrome', 'none');
|
|
684
|
+
// Slab cells live in GROUP metadata (groups carry no GridItemConfig).
|
|
685
|
+
cg.setMetadata('gridItem', gridItemFromCell({ x: w.x, y: w.y, w: w.span, h: w.rows }));
|
|
686
|
+
// The container's own spec fields, persisted ON the group — a
|
|
687
|
+
// reloaded document has no authored literal to read them from.
|
|
688
|
+
cg.setMetadata('containerWidget', Object.assign(Object.assign(Object.assign(Object.assign({}, (w.kind !== undefined ? { kind: w.kind } : {})), (w.title !== undefined ? { title: w.title } : {})), { columns: innerColumns, maxRows: innerRows }), (w.data !== undefined ? { data: w.data } : {})));
|
|
689
|
+
cg.setMetadata('dashboardBoard', {
|
|
690
|
+
columns: innerColumns,
|
|
691
|
+
gap,
|
|
692
|
+
padding: 0,
|
|
693
|
+
sizing: 'fit',
|
|
694
|
+
baseRowHeight: rowHeight,
|
|
695
|
+
// The slab's height is the PARENT's business — 0 hands it over,
|
|
696
|
+
// which is what makes escalation grow the slab instead of the
|
|
697
|
+
// container fighting its own frame.
|
|
698
|
+
designHeight: 0,
|
|
699
|
+
maxRows: innerRows,
|
|
700
|
+
float: false,
|
|
701
|
+
rtl: (_c = options.rtl) !== null && _c !== void 0 ? _c : false,
|
|
702
|
+
});
|
|
703
|
+
cg.size = { width: 100, height: rowHeight, depth: 0 };
|
|
704
|
+
boardGroup.addMember(w.id);
|
|
705
|
+
ctx.boardGroups.set(w.id, cg);
|
|
706
|
+
mountBoard(w.id, viewId, w.widgets, cg);
|
|
707
|
+
binders.set(w.id, bindDashboardGrid(a, cg, {
|
|
708
|
+
columns: innerColumns,
|
|
709
|
+
gap,
|
|
710
|
+
padding: 0,
|
|
711
|
+
sizing: 'fit',
|
|
712
|
+
baseRowHeight: rowHeight,
|
|
713
|
+
designHeight: 0,
|
|
714
|
+
maxRows: innerRows,
|
|
715
|
+
float: false,
|
|
716
|
+
rtl: (_d = options.rtl) !== null && _d !== void 0 ? _d : false,
|
|
717
|
+
onGesture: (e) => {
|
|
718
|
+
var _a, _b;
|
|
719
|
+
if (e.type === 'commit')
|
|
720
|
+
reportLayoutChange(viewId);
|
|
721
|
+
(_b = (_a = options.binder) === null || _a === void 0 ? void 0 : _a.onGesture) === null || _b === void 0 ? void 0 : _b.call(_a, e);
|
|
722
|
+
},
|
|
723
|
+
}));
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
549
726
|
const n = model.getNode(w.id);
|
|
550
727
|
if (!n)
|
|
551
728
|
continue;
|
|
@@ -557,7 +734,7 @@ export function dashboard(options) {
|
|
|
557
734
|
// and made toJSON() → dashboard() NOT round-trip (a saved layout
|
|
558
735
|
// rebuilt back into its declaration order rather than its cells).
|
|
559
736
|
if (w.x !== undefined && w.y !== undefined) {
|
|
560
|
-
n.setGridItem(gridItemFromCell({ x: w.x, y: w.y, w: (
|
|
737
|
+
n.setGridItem(gridItemFromCell({ x: w.x, y: w.y, w: (_e = w.span) !== null && _e !== void 0 ? _e : 3, h: (_f = w.rows) !== null && _f !== void 0 ? _f : 1 }));
|
|
561
738
|
}
|
|
562
739
|
if (w.pinned)
|
|
563
740
|
n.setState({ locked: true });
|
|
@@ -571,19 +748,24 @@ export function dashboard(options) {
|
|
|
571
748
|
n.setBehavior({ connectable: false });
|
|
572
749
|
for (const p of [...n.getPorts().values()])
|
|
573
750
|
n.removePort(p.id);
|
|
574
|
-
|
|
751
|
+
boardGroup.addMember(w.id);
|
|
575
752
|
}
|
|
576
|
-
binders.set(v.id, bindDashboardGrid(a, g, Object.assign(Object.assign(Object.assign({ columns: (_l = v.columns) !== null && _l !== void 0 ? _l : columns, gap, padding: gap, sizing: (_m = options.sizing) !== null && _m !== void 0 ? _m : 'fit', baseRowHeight: rowHeight, designHeight: (_o = v.height) !== null && _o !== void 0 ? _o : boardH, float: (_p = options.float) !== null && _p !== void 0 ? _p : false, rtl: (_q = options.rtl) !== null && _q !== void 0 ? _q : false }, (options.responsive ? { responsive: options.responsive } : {})), ((_r = options.binder) !== null && _r !== void 0 ? _r : {})), { onGesture: (e) => {
|
|
577
|
-
var _a, _b;
|
|
578
|
-
if (e.type === 'commit' && options.onLayoutChange) {
|
|
579
|
-
const snapshot = handle.toJSON().views.find((x) => x.id === v.id);
|
|
580
|
-
if (snapshot)
|
|
581
|
-
options.onLayoutChange(v.id, snapshot.widgets);
|
|
582
|
-
}
|
|
583
|
-
(_b = (_a = options.binder) === null || _a === void 0 ? void 0 : _a.onGesture) === null || _b === void 0 ? void 0 : _b.call(_a, e);
|
|
584
|
-
} })));
|
|
585
753
|
}
|
|
586
|
-
|
|
754
|
+
/**
|
|
755
|
+
* One reporter for every binder on a view — the view's own and each
|
|
756
|
+
* container's. The payload is the view's FULL NESTED TREE derived from
|
|
757
|
+
* live membership (handle.toJSON()), so an inner commit reports the same
|
|
758
|
+
* truth an outer one does, and a tile that crossed a boundary shows up
|
|
759
|
+
* under its NEW parent. (The old per-view lookup found nothing for a
|
|
760
|
+
* container binder and silently reported nothing.)
|
|
761
|
+
*/
|
|
762
|
+
function reportLayoutChange(viewId) {
|
|
763
|
+
if (!options.onLayoutChange)
|
|
764
|
+
return;
|
|
765
|
+
const snapshot = handle.toJSON().views.find((x) => x.id === viewId);
|
|
766
|
+
if (snapshot)
|
|
767
|
+
options.onLayoutChange(viewId, snapshot.widgets);
|
|
768
|
+
}
|
|
587
769
|
},
|
|
588
770
|
};
|
|
589
771
|
}
|
|
@@ -1240,13 +1240,27 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
1240
1240
|
id: `dashboard-grid:${group.id}:${++binderSeq}`,
|
|
1241
1241
|
priority: 2, // point-specific claim — outranks mode-style tools (see ext/tools.ts)
|
|
1242
1242
|
hitTest(ev, hit) {
|
|
1243
|
-
var _a;
|
|
1243
|
+
var _a, _b;
|
|
1244
1244
|
if (disposed)
|
|
1245
1245
|
return false;
|
|
1246
1246
|
if (gesture)
|
|
1247
1247
|
return true; // own the rest of an in-flight gesture
|
|
1248
|
-
if (hit.node)
|
|
1249
|
-
|
|
1248
|
+
if (hit.node) {
|
|
1249
|
+
if (((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(hit.node.id))
|
|
1250
|
+
return true;
|
|
1251
|
+
// A press on a tile that belongs to a NESTED board must reach that
|
|
1252
|
+
// board's tool. The dead-zone claim below deadens the slab's EMPTY
|
|
1253
|
+
// band — claiming a peer's tile with it swallowed every resize inside
|
|
1254
|
+
// an API-built container (which binds child-before-parent, so the
|
|
1255
|
+
// parent's tool won the registration-order tie and the child's resize
|
|
1256
|
+
// never armed; grid-options binds parent-first and worked by
|
|
1257
|
+
// accident).
|
|
1258
|
+
for (const p of (_b = BOARD_REGISTRY.get(api.container)) !== null && _b !== void 0 ? _b : []) {
|
|
1259
|
+
if (p !== selfPeer && p.hasItem(hit.node.id))
|
|
1260
|
+
return false;
|
|
1261
|
+
}
|
|
1262
|
+
return insideMemberGroupFrame(ev.world.x, ev.world.y);
|
|
1263
|
+
}
|
|
1250
1264
|
// Claim (and deaden) empty presses inside a member group's frame so the
|
|
1251
1265
|
// built-in group-drag cannot fight the pack layout for the KPI slab.
|
|
1252
1266
|
return insideMemberGroupFrame(ev.world.x, ev.world.y);
|
package/src/lib/load.js
CHANGED
|
@@ -67,6 +67,7 @@ import { bindRowInteractions } from './diagram-kit/rows.js';
|
|
|
67
67
|
import { bindCardEditing } from './diagram-kit/editing.js';
|
|
68
68
|
import { ensureDiagramKitStyles } from './diagram-kit/styles.js';
|
|
69
69
|
import { bindDashboardGrid } from './dashboard-kit/grid-binder.js';
|
|
70
|
+
import { cellFromGridItem } from './dashboard-kit/grid-mapping.js';
|
|
70
71
|
import { ensureDashboardKitStyles } from './dashboard-kit/styles.js';
|
|
71
72
|
import { defaultWidgetRenderer } from './dashboard-kit/widgets.js';
|
|
72
73
|
import { createDashboardHandle, } from './dashboard-kit/dashboard.js';
|
|
@@ -100,7 +101,7 @@ function widgetSpecOf(node) {
|
|
|
100
101
|
* of either.
|
|
101
102
|
*/
|
|
102
103
|
export function fromDocument(document, options = {}) {
|
|
103
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
|
|
104
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
|
|
104
105
|
const parsed = typeof document === 'string' ? parseDocument(document) : document;
|
|
105
106
|
const model = new DiagramSerializer().deserialize(parsed);
|
|
106
107
|
const nodes = model.getNodes();
|
|
@@ -124,40 +125,79 @@ export function fromDocument(document, options = {}) {
|
|
|
124
125
|
const dashGroups = groups.filter((g) => g.getMetadata('dashboardBoard') !== undefined);
|
|
125
126
|
const specById = new Map();
|
|
126
127
|
const viewOfWidget = new Map();
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
const boardWidgets = new Map();
|
|
129
|
+
const viewOfBoard = new Map();
|
|
130
|
+
const boardGroups = new Map(dashGroups.map((g) => [g.id, g]));
|
|
131
|
+
// A CONTAINER is a dashboard group that is itself a MEMBER of another
|
|
132
|
+
// dashboard group; the rest are views. This is the whole nesting test —
|
|
133
|
+
// containment IS membership.
|
|
134
|
+
const containerIds = new Set();
|
|
135
|
+
for (const g of dashGroups) {
|
|
136
|
+
for (const m of (_b = g.members) !== null && _b !== void 0 ? _b : [])
|
|
137
|
+
if (boardGroups.has(m))
|
|
138
|
+
containerIds.add(m);
|
|
139
|
+
}
|
|
140
|
+
const rebuildBoard = (g, viewId) => {
|
|
141
|
+
var _a, _b;
|
|
130
142
|
const widgets = [];
|
|
131
143
|
for (const memberId of (_a = g.members) !== null && _a !== void 0 ? _a : []) {
|
|
144
|
+
const childGroup = boardGroups.get(memberId);
|
|
145
|
+
if (childGroup) {
|
|
146
|
+
// A nested container: its spec fields ride on the group, its cell in
|
|
147
|
+
// the group's slab `gridItem`, its children in its own membership.
|
|
148
|
+
const meta = ((_b = childGroup.getMetadata('containerWidget')) !== null && _b !== void 0 ? _b : {});
|
|
149
|
+
const cell = cellFromGridItem(childGroup.getMetadata('gridItem'));
|
|
150
|
+
const ws = Object.assign(Object.assign(Object.assign({ id: childGroup.id }, meta), (cell ? { x: cell.x, y: cell.y, span: cell.w, rows: cell.h } : {})), { widgets: rebuildBoard(childGroup, viewId) });
|
|
151
|
+
specById.set(ws.id, ws);
|
|
152
|
+
viewOfWidget.set(ws.id, g.id);
|
|
153
|
+
boardWidgets.set(ws.id, ws.widgets);
|
|
154
|
+
viewOfBoard.set(ws.id, viewId);
|
|
155
|
+
widgets.push(ws);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
132
158
|
const node = model.getNode(memberId);
|
|
133
159
|
const ws = node ? widgetSpecOf(node) : null;
|
|
134
160
|
if (!ws)
|
|
135
|
-
continue;
|
|
161
|
+
continue;
|
|
136
162
|
specById.set(ws.id, ws);
|
|
137
163
|
viewOfWidget.set(ws.id, g.id);
|
|
138
164
|
widgets.push(ws);
|
|
139
165
|
}
|
|
140
|
-
return
|
|
166
|
+
return widgets;
|
|
167
|
+
};
|
|
168
|
+
const viewGroups = dashGroups.filter((g) => !containerIds.has(g.id));
|
|
169
|
+
const ctxViews = viewGroups.map((g) => {
|
|
170
|
+
var _a, _b;
|
|
171
|
+
const board = g.getMetadata('dashboardBoard');
|
|
172
|
+
const widgets = rebuildBoard(g, g.id);
|
|
173
|
+
boardWidgets.set(g.id, widgets);
|
|
174
|
+
viewOfBoard.set(g.id, g.id);
|
|
175
|
+
return { id: g.id, name: g.name, widgets, columns: board.columns, width: (_a = g.size) === null || _a === void 0 ? void 0 : _a.width, height: (_b = g.size) === null || _b === void 0 ? void 0 : _b.height };
|
|
141
176
|
});
|
|
142
|
-
const firstBoard = (
|
|
177
|
+
const firstBoard = (_c = viewGroups[0]) === null || _c === void 0 ? void 0 : _c.getMetadata('dashboardBoard');
|
|
143
178
|
// The active view is the one the save left ON camera (x≈0); the others were
|
|
144
179
|
// parked far off-screen by showView. Falls back to the first board when
|
|
145
180
|
// positions are ambiguous (e.g. a single view, or positions not restored).
|
|
146
|
-
const activeGroup = (
|
|
181
|
+
const activeGroup = (_d = viewGroups.find((g) => g.position.x > -1000)) !== null && _d !== void 0 ? _d : viewGroups[0];
|
|
147
182
|
const ctx = {
|
|
148
183
|
views: ctxViews,
|
|
149
|
-
|
|
184
|
+
// PARKING map: views only. A container must follow its parent when a view
|
|
185
|
+
// parks, not travel to OFFSCREEN_X on its own.
|
|
186
|
+
groups: new Map(viewGroups.map((g) => [g.id, g])),
|
|
150
187
|
// The SAME map the LoadedDiagramSpec exposes as `boards` — derived, not a copy.
|
|
151
188
|
binders: boards,
|
|
152
189
|
specById,
|
|
153
190
|
viewOfWidget,
|
|
191
|
+
boardGroups,
|
|
192
|
+
boardWidgets,
|
|
193
|
+
viewOfBoard,
|
|
154
194
|
hosts: new Map(),
|
|
155
195
|
renderWidget: paintWidget,
|
|
156
|
-
columns: (
|
|
157
|
-
gap: (
|
|
158
|
-
rowHeight: (
|
|
159
|
-
boardW: (
|
|
160
|
-
boardH: (
|
|
196
|
+
columns: (_e = firstBoard === null || firstBoard === void 0 ? void 0 : firstBoard.columns) !== null && _e !== void 0 ? _e : 12,
|
|
197
|
+
gap: (_f = firstBoard === null || firstBoard === void 0 ? void 0 : firstBoard.gap) !== null && _f !== void 0 ? _f : 8,
|
|
198
|
+
rowHeight: (_g = firstBoard === null || firstBoard === void 0 ? void 0 : firstBoard.baseRowHeight) !== null && _g !== void 0 ? _g : 130,
|
|
199
|
+
boardW: (_k = (_j = (_h = viewGroups[0]) === null || _h === void 0 ? void 0 : _h.size) === null || _j === void 0 ? void 0 : _j.width) !== null && _k !== void 0 ? _k : 1180,
|
|
200
|
+
boardH: (_o = (_m = (_l = viewGroups[0]) === null || _l === void 0 ? void 0 : _l.size) === null || _m === void 0 ? void 0 : _m.height) !== null && _o !== void 0 ? _o : 660,
|
|
161
201
|
// responsive is NOT in the document (a runtime seam), so it is deliberately
|
|
162
202
|
// absent from the round-trip; width/height/columns/gap/sizing/float/rtl are.
|
|
163
203
|
optionsBase: firstBoard
|
|
@@ -168,11 +208,11 @@ export function fromDocument(document, options = {}) {
|
|
|
168
208
|
sizing: firstBoard.sizing,
|
|
169
209
|
float: firstBoard.float,
|
|
170
210
|
rtl: firstBoard.rtl,
|
|
171
|
-
width: (
|
|
172
|
-
height: (
|
|
211
|
+
width: (_q = (_p = viewGroups[0]) === null || _p === void 0 ? void 0 : _p.size) === null || _q === void 0 ? void 0 : _q.width,
|
|
212
|
+
height: (_s = (_r = viewGroups[0]) === null || _r === void 0 ? void 0 : _r.size) === null || _s === void 0 ? void 0 : _s.height,
|
|
173
213
|
}
|
|
174
214
|
: {},
|
|
175
|
-
active: (
|
|
215
|
+
active: (_t = activeGroup === null || activeGroup === void 0 ? void 0 : activeGroup.id) !== null && _t !== void 0 ? _t : 'main',
|
|
176
216
|
apiRef: null,
|
|
177
217
|
};
|
|
178
218
|
const handle = createDashboardHandle(ctx);
|