@grafloria/element 0.4.2 → 0.4.4
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/package.json +1 -1
- package/src/lib/dashboard-kit/dashboard.d.ts +147 -1
- package/src/lib/dashboard-kit/dashboard.js +524 -113
- package/src/lib/dashboard-kit/grid-binder.d.ts +43 -0
- package/src/lib/dashboard-kit/grid-binder.js +719 -106
- package/src/lib/dashboard-kit/index.d.ts +2 -0
- package/src/lib/dashboard-kit/index.js +2 -0
- package/src/lib/dashboard-kit/split-binder.d.ts +69 -0
- package/src/lib/dashboard-kit/split-binder.js +1078 -0
- package/src/lib/dashboard-kit/split-layout.d.ts +128 -0
- package/src/lib/dashboard-kit/split-layout.js +425 -0
- package/src/lib/dashboard-kit/styles.js +137 -20
- package/src/lib/dashboard-kit/widgets.d.ts +16 -1
- package/src/lib/dashboard-kit/widgets.js +146 -33
- package/src/lib/diagram-kit/card.d.ts +46 -0
- package/src/lib/diagram-kit/card.js +149 -0
- package/src/lib/diagram-kit/er.js +5 -3
- package/src/lib/diagram-kit/styles.js +40 -5
- package/src/lib/diagram-kit/uml.js +3 -4
- package/src/lib/diagram-kit/update.js +7 -3
- package/src/lib/grafloria.js +4 -4
- package/src/lib/load.d.ts +5 -0
- package/src/lib/load.js +72 -8
|
@@ -51,10 +51,38 @@
|
|
|
51
51
|
*/
|
|
52
52
|
import { __awaiter } from "tslib";
|
|
53
53
|
import { AddToGroupCommand, BatchCommand, Command, GridPackEngine, RemoveFromGroupCommand, } from '@grafloria/engine';
|
|
54
|
-
import { registerTool } from '@grafloria/renderer';
|
|
54
|
+
import { LiveRegionController, registerTool } from '@grafloria/renderer';
|
|
55
55
|
import { buildCommitCommands, cellFromGridItem, cellToRect, columnUnitFor, gridItemFromCell, pointToCell, rowHeightFor, sizeToSpan, } from './grid-mapping.js';
|
|
56
56
|
import { ensureDashboardKitStyles } from './styles.js';
|
|
57
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Binders on the same canvas find each other here. A WeakMap: a canvas that
|
|
59
|
+
* is gone takes its (empty) peer set with it — the strong Map it replaced kept
|
|
60
|
+
* every container element a dashboard had ever mounted for the life of the
|
|
61
|
+
* page (review D12).
|
|
62
|
+
*/
|
|
63
|
+
const BOARD_REGISTRY = new WeakMap();
|
|
64
|
+
/**
|
|
65
|
+
* ONE aria-live region per canvas, shared by every board on it — the
|
|
66
|
+
* renderer's own controller (coalescing, de-duplicating), so a dashboard
|
|
67
|
+
* announces through the same channel a diagram does. WeakMap: the region
|
|
68
|
+
* follows the container out of memory.
|
|
69
|
+
*/
|
|
70
|
+
const LIVE_REGIONS = new WeakMap();
|
|
71
|
+
function liveRegionFor(container) {
|
|
72
|
+
let live = LIVE_REGIONS.get(container);
|
|
73
|
+
if (!live) {
|
|
74
|
+
live = new LiveRegionController(container);
|
|
75
|
+
LIVE_REGIONS.set(container, live);
|
|
76
|
+
}
|
|
77
|
+
return live;
|
|
78
|
+
}
|
|
79
|
+
function directionName(dx, dy) {
|
|
80
|
+
return dx < 0 ? 'left' : dx > 0 ? 'right' : dy < 0 ? 'up' : 'down';
|
|
81
|
+
}
|
|
82
|
+
/** "column 4, row 2, 3 by 1" — the cell as a person hears it (1-based). */
|
|
83
|
+
function describeCell(c) {
|
|
84
|
+
return `column ${c.x + 1}, row ${c.y + 1}, ${c.w} by ${c.h}`;
|
|
85
|
+
}
|
|
58
86
|
/**
|
|
59
87
|
* Undoable cell+frame write for a GROUP member (the strip's slab). The engine
|
|
60
88
|
* has Move/Resize commands for nodes but none for a group's frame, and slab
|
|
@@ -101,9 +129,37 @@ class SetGroupCellCommand extends Command {
|
|
|
101
129
|
}
|
|
102
130
|
const DRAG_THRESHOLD = 4;
|
|
103
131
|
const GLIDE_OFF_DELAY = 400;
|
|
132
|
+
/** A press this close (CSS px) to a tile's border takes that edge for a resize. */
|
|
133
|
+
const EDGE_GRIP = 7;
|
|
134
|
+
const NO_EDGES = { n: false, e: false, s: false, w: false };
|
|
135
|
+
/** Which of a host's edges a client point is within EDGE_GRIP of (none when outside). */
|
|
136
|
+
function edgesNear(host, cx, cy) {
|
|
137
|
+
const r = host.getBoundingClientRect();
|
|
138
|
+
if (cx < r.left - 2 || cx > r.right + 2 || cy < r.top - 2 || cy > r.bottom + 2)
|
|
139
|
+
return NO_EDGES;
|
|
140
|
+
return {
|
|
141
|
+
n: cy - r.top <= EDGE_GRIP,
|
|
142
|
+
s: r.bottom - cy <= EDGE_GRIP,
|
|
143
|
+
w: cx - r.left <= EDGE_GRIP,
|
|
144
|
+
e: r.right - cx <= EDGE_GRIP,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const anyEdge = (E) => E.n || E.e || E.s || E.w;
|
|
148
|
+
/** The resize cursor for a set of edges ('' when none). */
|
|
149
|
+
function cursorFor(E) {
|
|
150
|
+
const v = E.n || E.s;
|
|
151
|
+
const h = E.e || E.w;
|
|
152
|
+
if (v && h)
|
|
153
|
+
return (E.n && E.w) || (E.s && E.e) ? 'nwse-resize' : 'nesw-resize';
|
|
154
|
+
if (v)
|
|
155
|
+
return 'ns-resize';
|
|
156
|
+
if (h)
|
|
157
|
+
return 'ew-resize';
|
|
158
|
+
return '';
|
|
159
|
+
}
|
|
104
160
|
let binderSeq = 0;
|
|
105
161
|
export function bindDashboardGrid(api, group, options = {}) {
|
|
106
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
162
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
107
163
|
ensureDashboardKitStyles();
|
|
108
164
|
const diagram = api.getModel();
|
|
109
165
|
/** The DECLARED count — the board's authored width, and the responsive cap. */
|
|
@@ -119,12 +175,118 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
119
175
|
const baseRowHeight = (_d = options.baseRowHeight) !== null && _d !== void 0 ? _d : 110;
|
|
120
176
|
const minRowHeight = (_e = options.minRowHeight) !== null && _e !== void 0 ? _e : 28;
|
|
121
177
|
let float = (_f = options.float) !== null && _f !== void 0 ? _f : false;
|
|
178
|
+
/** The AUTHORED bound — a nested strip's design (row-first push, escalation). */
|
|
122
179
|
const maxRows = options.maxRows;
|
|
123
180
|
const dragOut = (_g = options.dragOut) !== null && _g !== void 0 ? _g : 'cancel';
|
|
124
181
|
const wantHandles = options.resizeHandles !== false;
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
let
|
|
182
|
+
const fluid = options.fluid === true;
|
|
183
|
+
const overflow = (_h = options.overflow) !== null && _h !== void 0 ? _h : 'bounded';
|
|
184
|
+
let isStatic = options.static === true;
|
|
185
|
+
/** The member the ROVING TABINDEX rests on (one tab stop per board). */
|
|
186
|
+
let focusedId;
|
|
187
|
+
const live = liveRegionFor(api.container);
|
|
188
|
+
/** The design height. Fluid boards re-read it from the container. */
|
|
189
|
+
let designH = (_l = (_j = options.designHeight) !== null && _j !== void 0 ? _j : (_k = group.size) === null || _k === void 0 ? void 0 : _k.height) !== null && _l !== void 0 ? _l : 0;
|
|
190
|
+
/** Fit-mode CAPACITY in rows (see `overflow`). Undefined = unbounded. */
|
|
191
|
+
let capacity;
|
|
192
|
+
let sizing = (_m = options.sizing) !== null && _m !== void 0 ? _m : 'fit';
|
|
193
|
+
/**
|
|
194
|
+
* A fresh engine holding `items` VERBATIM. The constructor add()s each item
|
|
195
|
+
* and settles after every one, so in gravity mode a legal layout with a gap
|
|
196
|
+
* — a tile dropped below free space, exactly what the placeholder promised —
|
|
197
|
+
* was re-packed on every rebuild: refresh(), undo, and (since the kit follows
|
|
198
|
+
* the history) every commit moved a tile the user had just placed. The
|
|
199
|
+
* persisted cells are the truth; a rebuild must not edit them. Float while
|
|
200
|
+
* constructing, then restore the real mode for everything that follows.
|
|
201
|
+
*
|
|
202
|
+
* `pack` is for the two moments gravity IS wanted: the first adoption of the
|
|
203
|
+
* authored spec (a declared cell hovering over an empty row settles — the
|
|
204
|
+
* kit's documented boot contract) and turning float off.
|
|
205
|
+
*/
|
|
206
|
+
const engineFrom = (items, pack = false, at = columns) => {
|
|
207
|
+
const e = new GridPackEngine(items, { columns: at, float: pack ? float : true, maxRows, capacity });
|
|
208
|
+
e.float = float;
|
|
209
|
+
return e;
|
|
210
|
+
};
|
|
211
|
+
/** The effective row bound a gesture must respect: the strip's, else the fit capacity. */
|
|
212
|
+
const bound = () => maxRows !== null && maxRows !== void 0 ? maxRows : capacity;
|
|
213
|
+
/**
|
|
214
|
+
* The rows the design height can hold at the row floor — what 'bounded' fit
|
|
215
|
+
* enforces. A board is never bounded BELOW what it already holds: a document
|
|
216
|
+
* loaded with more rows than fit keeps every tile (the frame extends, see
|
|
217
|
+
* enforceBoardHeight) and only further growth is refused.
|
|
218
|
+
*/
|
|
219
|
+
const fitCapacity = () => {
|
|
220
|
+
if (maxRows !== undefined || sizing !== 'fit' || overflow === 'scroll' || designH <= 0)
|
|
221
|
+
return undefined;
|
|
222
|
+
const rowsThatFit = Math.floor((designH - 2 * padding + gap) / (minRowHeight + gap));
|
|
223
|
+
return Math.max(1, rowsThatFit, engine.rows());
|
|
224
|
+
};
|
|
225
|
+
/** Re-derive the capacity; true when the engine must be rebuilt to carry it. */
|
|
226
|
+
const refreshCapacity = () => {
|
|
227
|
+
const next = fitCapacity();
|
|
228
|
+
if (next === capacity)
|
|
229
|
+
return false;
|
|
230
|
+
capacity = next;
|
|
231
|
+
return true;
|
|
232
|
+
};
|
|
233
|
+
/** The canvas container's CSS-pixel box (0 when unmeasurable, e.g. jsdom). */
|
|
234
|
+
const containerBox = () => ({
|
|
235
|
+
w: api.container.clientWidth || 0,
|
|
236
|
+
h: api.container.clientHeight || 0,
|
|
237
|
+
});
|
|
238
|
+
/**
|
|
239
|
+
* FLUID: make the group's frame the container's box. Width always; the
|
|
240
|
+
* design height follows too, so fit capacity follows the viewport (a shorter
|
|
241
|
+
* window holds fewer rows) and grow never shrinks below the visible area.
|
|
242
|
+
* Returns true when the frame changed. A container that cannot be measured
|
|
243
|
+
* (width 0) leaves the authored frame alone.
|
|
244
|
+
*/
|
|
245
|
+
const applyFluidFrame = () => {
|
|
246
|
+
if (!fluid || disposed)
|
|
247
|
+
return false;
|
|
248
|
+
const box = containerBox();
|
|
249
|
+
if (box.w <= 0)
|
|
250
|
+
return false;
|
|
251
|
+
if (box.h > 0)
|
|
252
|
+
designH = box.h;
|
|
253
|
+
const f = frame();
|
|
254
|
+
const height = sizing === 'fit' && box.h > 0 ? box.h : f.height;
|
|
255
|
+
if (Math.abs(f.width - box.w) < 0.5 && Math.abs(f.height - height) < 0.5)
|
|
256
|
+
return false;
|
|
257
|
+
writing = true;
|
|
258
|
+
try {
|
|
259
|
+
diagram.runSystemWrite(() => group.setFrame({ x: f.x, y: f.y, width: box.w, height }));
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
writing = false;
|
|
263
|
+
}
|
|
264
|
+
return true;
|
|
265
|
+
};
|
|
266
|
+
/**
|
|
267
|
+
* THE PERSISTED COLUMN CACHE (D4). `toJSON()` always saved the wide layout —
|
|
268
|
+
* it reads the engine's widest cached count. The DOCUMENT path read the
|
|
269
|
+
* node's GridItemConfig, and a responsive column change writes the live,
|
|
270
|
+
* NARROW cells into exactly that field: a board saved on a phone reloaded as
|
|
271
|
+
* 1-wide tiles crammed into the left of a 12-column board, because the cache
|
|
272
|
+
* that knew better lived only in this closure. It now rides on the group as
|
|
273
|
+
* `dashboardLayouts` — the cache as plain data plus the live count the
|
|
274
|
+
* GridItemConfigs belong to — and `rebuild()` reads it back on a fresh bind.
|
|
275
|
+
* Written only when there is a cache, so a board that never changed column
|
|
276
|
+
* count serialises exactly as before.
|
|
277
|
+
*/
|
|
278
|
+
const persistLayouts = () => {
|
|
279
|
+
if (disposed || engine.cachedColumns().length === 0)
|
|
280
|
+
return;
|
|
281
|
+
writing = true;
|
|
282
|
+
try {
|
|
283
|
+
diagram.runSystemWrite(() => group.setMetadata('dashboardLayouts', { columns: engine.columns, layouts: engine.getLayouts() }));
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
writing = false;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
let engine = engineFrom([]);
|
|
128
290
|
let gesture = null;
|
|
129
291
|
let disposed = false;
|
|
130
292
|
/** Reentrancy guard: our own derived frame writes must not re-project. */
|
|
@@ -154,7 +316,10 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
154
316
|
padding,
|
|
155
317
|
sizing,
|
|
156
318
|
baseRowHeight,
|
|
157
|
-
|
|
319
|
+
// BOUNDED FIT NEVER SCROLLS: the floor is a capacity for growth, not a
|
|
320
|
+
// reason to paint past the frame. A board already holding more rows than
|
|
321
|
+
// fit (a Grow→Fit switch, a loaded document) squeezes below it instead.
|
|
322
|
+
minRowHeight: sizing === 'fit' && overflow !== 'scroll' ? 1 : minRowHeight,
|
|
158
323
|
designHeight: sizing === 'fit' ? frame().height : designH,
|
|
159
324
|
rtl,
|
|
160
325
|
});
|
|
@@ -171,18 +336,22 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
171
336
|
* group metadata; spans falling back to metadata columnSpan/rowSpan; last
|
|
172
337
|
* resort: adopt from the current pixel geometry). */
|
|
173
338
|
const itemFor = (id) => {
|
|
174
|
-
var _a, _b, _c, _d, _e;
|
|
339
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
175
340
|
const node = diagram.getNode(id);
|
|
176
341
|
if (node) {
|
|
177
342
|
const spanMeta = Number((_a = node.getMetadata) === null || _a === void 0 ? void 0 : _a.call(node, 'columnSpan')) || 0;
|
|
178
343
|
const rowsMeta = Number((_b = node.getMetadata) === null || _b === void 0 ? void 0 : _b.call(node, 'rowSpan')) || 0;
|
|
179
344
|
const locked = ((_c = node.state) === null || _c === void 0 ? void 0 : _c.locked) === true;
|
|
180
|
-
|
|
345
|
+
// Per-widget size limits ride on the node as `widgetLimits` and reach the
|
|
346
|
+
// engine as gridstack's minW/maxW/minH/maxH.
|
|
347
|
+
const lim = ((_e = (_d = node.getMetadata) === null || _d === void 0 ? void 0 : _d.call(node, 'widgetLimits')) !== null && _e !== void 0 ? _e : {});
|
|
348
|
+
const limits = Object.assign(Object.assign(Object.assign(Object.assign({}, (lim.minSpan !== undefined ? { minW: lim.minSpan } : {})), (lim.maxSpan !== undefined ? { maxW: lim.maxSpan } : {})), (lim.minRows !== undefined ? { minH: lim.minRows } : {})), (lim.maxRows !== undefined ? { maxH: lim.maxRows } : {}));
|
|
349
|
+
const cell = cellFromGridItem((_f = node.getGridItem) === null || _f === void 0 ? void 0 : _f.call(node), {
|
|
181
350
|
w: spanMeta || 1,
|
|
182
351
|
h: rowsMeta || 1,
|
|
183
352
|
});
|
|
184
353
|
if (cell)
|
|
185
|
-
return Object.assign(Object.assign({ id }, cell), { locked });
|
|
354
|
+
return Object.assign(Object.assign(Object.assign({ id }, cell), { locked }), limits);
|
|
186
355
|
const f = frame();
|
|
187
356
|
const g = geom();
|
|
188
357
|
if (node.position && (node.position.x !== 0 || node.position.y !== 0)) {
|
|
@@ -191,20 +360,13 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
191
360
|
// left edge into a cell.
|
|
192
361
|
const s = sizeToSpan(node.size.width, node.size.height, f, g, rows());
|
|
193
362
|
const p = pointToCell(node.position.x, node.position.y, f, g, rows(), spanMeta || s.w);
|
|
194
|
-
return {
|
|
195
|
-
id,
|
|
196
|
-
x: Math.max(0, p.x),
|
|
197
|
-
y: Math.max(0, p.y),
|
|
198
|
-
w: spanMeta || s.w,
|
|
199
|
-
h: rowsMeta || s.h,
|
|
200
|
-
locked,
|
|
201
|
-
};
|
|
363
|
+
return Object.assign({ id, x: Math.max(0, p.x), y: Math.max(0, p.y), w: spanMeta || s.w, h: rowsMeta || s.h, locked }, limits);
|
|
202
364
|
}
|
|
203
|
-
return { id, x: 0, y: 0, w: spanMeta || 1, h: rowsMeta || 1, locked, autoPosition: true };
|
|
365
|
+
return Object.assign({ id, x: 0, y: 0, w: spanMeta || 1, h: rowsMeta || 1, locked, autoPosition: true }, limits);
|
|
204
366
|
}
|
|
205
367
|
const grp = diagram.getGroup(id);
|
|
206
368
|
const cell = grp
|
|
207
|
-
? cellFromGridItem((
|
|
369
|
+
? cellFromGridItem((_g = grp.getMetadata) === null || _g === void 0 ? void 0 : _g.call(grp, 'gridItem'))
|
|
208
370
|
: null;
|
|
209
371
|
// Member groups are LOCKED slabs (see the module doc).
|
|
210
372
|
if (cell)
|
|
@@ -260,8 +422,14 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
260
422
|
if (designH <= 0)
|
|
261
423
|
return;
|
|
262
424
|
const r = rows();
|
|
425
|
+
// FIT keeps its design height, full stop — the user's rule: "the board
|
|
426
|
+
// stays the same and the widgets change size so all of them fit". Only
|
|
427
|
+
// overflow:'scroll' lets the frame EXTEND to hold the rows at the floor
|
|
428
|
+
// height (and the canvas pan). Grow extends at the base row height.
|
|
263
429
|
const target = sizing === 'fit'
|
|
264
|
-
?
|
|
430
|
+
? overflow === 'scroll'
|
|
431
|
+
? Math.max(designH, 2 * padding + r * minRowHeight + (r - 1) * gap)
|
|
432
|
+
: designH
|
|
265
433
|
: Math.max(designH, 2 * padding + r * baseRowHeight + (r - 1) * gap);
|
|
266
434
|
const f = frame();
|
|
267
435
|
if (Math.abs(f.height - target) > 0.5) {
|
|
@@ -355,11 +523,61 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
355
523
|
}
|
|
356
524
|
};
|
|
357
525
|
// -- resize handles ---------------------------------------------------------
|
|
358
|
-
|
|
526
|
+
/** The accessible name of a widget: its title, else its kind, else its id. */
|
|
527
|
+
const nameOf = (node) => {
|
|
528
|
+
var _a, _b, _c, _d;
|
|
529
|
+
const title = (_a = node.getMetadata) === null || _a === void 0 ? void 0 : _a.call(node, 'widgetTitle');
|
|
530
|
+
if (typeof title === 'string' && title)
|
|
531
|
+
return title;
|
|
532
|
+
// The visible header falls back to a data label (a KPI's `label`) before
|
|
533
|
+
// the kind — the spoken name must agree with what is painted.
|
|
534
|
+
const label = (_c = (_b = node.getMetadata) === null || _b === void 0 ? void 0 : _b.call(node, 'widgetSpec')) === null || _c === void 0 ? void 0 : _c.label;
|
|
535
|
+
if (typeof label === 'string' && label)
|
|
536
|
+
return label;
|
|
537
|
+
const kind = (_d = node.getMetadata) === null || _d === void 0 ? void 0 : _d.call(node, 'widgetKind');
|
|
538
|
+
return typeof kind === 'string' && kind && kind !== 'widget' ? `${kind} widget` : node.id;
|
|
539
|
+
};
|
|
540
|
+
/**
|
|
541
|
+
* ACCESSIBLE CHROME on every member host (WCAG 4.1.2 name/role/value): a
|
|
542
|
+
* group role, a widget role description, a label that carries the cell,
|
|
543
|
+
* and the ROVING TABINDEX — exactly one member per board is a tab stop.
|
|
544
|
+
* Runs with the handles, so a repainted host gets it back too.
|
|
545
|
+
*/
|
|
546
|
+
const syncA11y = (only) => {
|
|
359
547
|
var _a, _b;
|
|
548
|
+
if (disposed)
|
|
549
|
+
return;
|
|
550
|
+
const members = [...((_a = group.members) !== null && _a !== void 0 ? _a : [])].filter((id) => !!diagram.getNode(id));
|
|
551
|
+
if (focusedId && !members.includes(focusedId))
|
|
552
|
+
focusedId = undefined;
|
|
553
|
+
const stop = focusedId !== null && focusedId !== void 0 ? focusedId : members[0];
|
|
554
|
+
for (const id of members) {
|
|
555
|
+
if (only && !only.has(id))
|
|
556
|
+
continue;
|
|
557
|
+
const node = diagram.getNode(id);
|
|
558
|
+
const host = hostOf(id);
|
|
559
|
+
if (!node || !host)
|
|
560
|
+
continue;
|
|
561
|
+
const cell = engine.getItem(id);
|
|
562
|
+
const bits = [nameOf(node)];
|
|
563
|
+
if (cell)
|
|
564
|
+
bits.push(describeCell(cell));
|
|
565
|
+
if (((_b = node.state) === null || _b === void 0 ? void 0 : _b.locked) === true)
|
|
566
|
+
bits.push('pinned');
|
|
567
|
+
host.setAttribute('role', 'group');
|
|
568
|
+
host.setAttribute('aria-roledescription', 'dashboard widget');
|
|
569
|
+
host.setAttribute('aria-label', bits.join(', '));
|
|
570
|
+
host.setAttribute('tabindex', id === stop ? '0' : '-1');
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
const syncHandles = (only) => {
|
|
574
|
+
var _a, _b, _c;
|
|
575
|
+
syncA11y(only);
|
|
360
576
|
if (!wantHandles || disposed)
|
|
361
577
|
return;
|
|
362
578
|
for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : []) {
|
|
579
|
+
if (only && !only.has(id))
|
|
580
|
+
continue;
|
|
363
581
|
const node = diagram.getNode(id);
|
|
364
582
|
if (!node)
|
|
365
583
|
continue;
|
|
@@ -367,7 +585,7 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
367
585
|
if (!host)
|
|
368
586
|
continue;
|
|
369
587
|
const existing = host.querySelector(':scope > .axdb-rs');
|
|
370
|
-
if (((_b = node.state) === null || _b === void 0 ? void 0 : _b.locked) === true) {
|
|
588
|
+
if (((_b = node.state) === null || _b === void 0 ? void 0 : _b.locked) === true || isStatic || ((_c = node.getMetadata) === null || _c === void 0 ? void 0 : _c.call(node, 'widgetResizable')) === false) {
|
|
371
589
|
existing === null || existing === void 0 ? void 0 : existing.remove();
|
|
372
590
|
continue;
|
|
373
591
|
}
|
|
@@ -382,7 +600,40 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
382
600
|
rs.classList.toggle('axdb-rs--rtl', rtl);
|
|
383
601
|
}
|
|
384
602
|
};
|
|
385
|
-
|
|
603
|
+
/**
|
|
604
|
+
* Only a HOST-LEVEL change matters: a host arriving (a mount) or a host's
|
|
605
|
+
* own children changing (a repaint that wiped the injected handle). A
|
|
606
|
+
* chart's internal churn — most of what a live dashboard mutates — targets
|
|
607
|
+
* deeper nodes and is ignored, and the hosts the records DO name are the
|
|
608
|
+
* only ones re-synced. This was members × repaints `querySelector` calls
|
|
609
|
+
* per wave (9,216 at 96 widgets, review D10); it is now proportional to the
|
|
610
|
+
* hosts that actually changed.
|
|
611
|
+
*/
|
|
612
|
+
const hostObserver = new MutationObserver((records) => {
|
|
613
|
+
const touched = new Set();
|
|
614
|
+
const noteHost = (el) => {
|
|
615
|
+
var _a;
|
|
616
|
+
const e = el;
|
|
617
|
+
if ((_a = e === null || e === void 0 ? void 0 : e.classList) === null || _a === void 0 ? void 0 : _a.contains('grafloria-node-host')) {
|
|
618
|
+
const id = e.getAttribute('data-node-id');
|
|
619
|
+
if (id)
|
|
620
|
+
touched.add(id);
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
for (const r of records) {
|
|
624
|
+
// Our own re-injected handle arriving is not a change to answer — it
|
|
625
|
+
// would echo one more pass per repaint.
|
|
626
|
+
const ownEcho = r.removedNodes.length === 0 &&
|
|
627
|
+
r.addedNodes.length > 0 &&
|
|
628
|
+
Array.from(r.addedNodes).every((n) => { var _a; return (_a = n.classList) === null || _a === void 0 ? void 0 : _a.contains('axdb-rs'); });
|
|
629
|
+
if (ownEcho)
|
|
630
|
+
continue;
|
|
631
|
+
noteHost(r.target);
|
|
632
|
+
r.addedNodes.forEach((n) => noteHost(n));
|
|
633
|
+
}
|
|
634
|
+
if (touched.size)
|
|
635
|
+
syncHandles(touched);
|
|
636
|
+
});
|
|
386
637
|
// -- gesture snapshot / commit ---------------------------------------------
|
|
387
638
|
const snapshotAll = () => {
|
|
388
639
|
const cells = new Map();
|
|
@@ -432,11 +683,21 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
432
683
|
};
|
|
433
684
|
// -- membership + bounds sync ----------------------------------------------
|
|
434
685
|
const onMemberAdded = (id) => {
|
|
686
|
+
var _a;
|
|
435
687
|
if (disposed)
|
|
436
688
|
return;
|
|
437
689
|
if (!engine.getItem(id)) {
|
|
438
690
|
const item = itemFor(id);
|
|
439
|
-
|
|
691
|
+
let placed = engine.add(item);
|
|
692
|
+
if (!placed && capacity !== undefined) {
|
|
693
|
+
// Membership is a document fact (an undo just restored it, say); a
|
|
694
|
+
// bounded board must not strand the node invisible. Lift the capacity
|
|
695
|
+
// for this adoption — it floors at the content on the next refresh.
|
|
696
|
+
capacity = undefined;
|
|
697
|
+
engine = engineFrom([...engine.getItems().map((i) => (Object.assign({}, i))), item]);
|
|
698
|
+
placed = (_a = engine.getItem(id)) !== null && _a !== void 0 ? _a : null;
|
|
699
|
+
refreshCapacity();
|
|
700
|
+
}
|
|
440
701
|
if (placed)
|
|
441
702
|
persistAdoptedCell(id, placed);
|
|
442
703
|
}
|
|
@@ -534,6 +795,7 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
534
795
|
return false;
|
|
535
796
|
columns = engine.columns;
|
|
536
797
|
persistLiveCells();
|
|
798
|
+
persistLayouts();
|
|
537
799
|
armGlide();
|
|
538
800
|
project();
|
|
539
801
|
syncPlaceholder();
|
|
@@ -548,10 +810,17 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
548
810
|
* turn, and a page that does not still wants the check to run. The binder
|
|
549
811
|
* owns this — pages should never have to wire a ResizeObserver for it.
|
|
550
812
|
*/
|
|
551
|
-
const containerObserver = responsive && typeof ResizeObserver !== 'undefined'
|
|
813
|
+
const containerObserver = (responsive || fluid) && typeof ResizeObserver !== 'undefined'
|
|
552
814
|
? new ResizeObserver(() => {
|
|
553
815
|
if (disposed)
|
|
554
816
|
return;
|
|
817
|
+
if (applyFluidFrame()) {
|
|
818
|
+
if (refreshCapacity())
|
|
819
|
+
rebuild(false);
|
|
820
|
+
else
|
|
821
|
+
project();
|
|
822
|
+
api.renderNow();
|
|
823
|
+
}
|
|
555
824
|
evaluateResponsive();
|
|
556
825
|
})
|
|
557
826
|
: null;
|
|
@@ -695,6 +964,13 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
695
964
|
commands.push(new SetGroupCellCommand(group.id, g.esc.cellBefore, g.esc.cellAfter, g.esc.frameBefore, g.esc.frameAfter));
|
|
696
965
|
}
|
|
697
966
|
const changed = execute(g.kind === 'resize' ? 'Resize widget' : 'Move widget', commands);
|
|
967
|
+
persistLayouts(); // an edit at a narrow count propagated into the wide cache
|
|
968
|
+
if (changed) {
|
|
969
|
+
const it = engine.getItem(g.id);
|
|
970
|
+
if (it)
|
|
971
|
+
live.announce(`${nameOf(g.node)} ${g.kind === 'resize' ? 'resized' : 'moved'} to ${describeCell(it)}`);
|
|
972
|
+
}
|
|
973
|
+
syncA11y();
|
|
698
974
|
api.renderNow();
|
|
699
975
|
(_a = options.onGesture) === null || _a === void 0 ? void 0 : _a.call(options, { type: 'commit', kind: g.kind, nodeId: g.id, changed });
|
|
700
976
|
};
|
|
@@ -723,7 +999,7 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
723
999
|
const lockedNode = ((_b = (_a = diagram.getNode(id)) === null || _a === void 0 ? void 0 : _a.state) === null || _b === void 0 ? void 0 : _b.locked) === true;
|
|
724
1000
|
items.push(Object.assign(Object.assign({ id }, c), { locked: lockedNode || isGroupMember(id) }));
|
|
725
1001
|
}
|
|
726
|
-
engine =
|
|
1002
|
+
engine = engineFrom(items);
|
|
727
1003
|
}
|
|
728
1004
|
else {
|
|
729
1005
|
engine.cancelGesture();
|
|
@@ -886,23 +1162,40 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
886
1162
|
}
|
|
887
1163
|
// resize: fluid pixel preview on the ghost, cell-stepped live push.
|
|
888
1164
|
//
|
|
889
|
-
//
|
|
890
|
-
//
|
|
891
|
-
//
|
|
892
|
-
//
|
|
893
|
-
//
|
|
894
|
-
|
|
895
|
-
|
|
1165
|
+
// ANCHORED ON THE OPPOSITE EDGE. The gesture carries which edges the press
|
|
1166
|
+
// took (the corner handle is s+e — s+w on an RTL board, whose corner sits
|
|
1167
|
+
// bottom-left — and a press near any border takes that edge), and the
|
|
1168
|
+
// tile's rect is rebuilt from its start rect with only those edges moved.
|
|
1169
|
+
// A pull from the west or north keeps the right/bottom edge pinned, in
|
|
1170
|
+
// cells too: the target cell is anchored, and the engine is driven MOVE
|
|
1171
|
+
// then RESIZE when growing (so the push happens on the side that grows)
|
|
1172
|
+
// and RESIZE then MOVE when shrinking. The s+e case reproduces the old
|
|
1173
|
+
// corner maths exactly, which the scenario battery pins.
|
|
1174
|
+
const dx = ev.world.x - g.downWorld.x;
|
|
1175
|
+
const dy = ev.world.y - g.downWorld.y;
|
|
1176
|
+
const E = g.edges;
|
|
1177
|
+
let left = g.startPos.x;
|
|
1178
|
+
let right = g.startPos.x + g.startSize.width;
|
|
1179
|
+
let top = g.startPos.y;
|
|
1180
|
+
let bottom = g.startPos.y + g.startSize.height;
|
|
1181
|
+
if (E.e)
|
|
1182
|
+
right += dx;
|
|
1183
|
+
if (E.w)
|
|
1184
|
+
left += dx;
|
|
1185
|
+
if (E.s)
|
|
1186
|
+
bottom += dy;
|
|
1187
|
+
if (E.n)
|
|
1188
|
+
top += dy;
|
|
896
1189
|
const f = frame();
|
|
897
1190
|
const gg = geom();
|
|
898
1191
|
const minW = Math.max(8, columnUnitFor(gg, f.width));
|
|
899
|
-
let w = Math.max(minW,
|
|
1192
|
+
let w = Math.max(minW, right - left);
|
|
900
1193
|
// The height min-clamp is applied AFTER the escalation logic below: on a
|
|
901
1194
|
// grown strip "one current row" IS the whole strip, and clamping the
|
|
902
1195
|
// fluid pull to it made the de-escalation threshold unreachable — the
|
|
903
1196
|
// ratchet's second disguise (grow committed; a fresh shrink gesture could
|
|
904
1197
|
// never pull low enough to ask the parent for a row back).
|
|
905
|
-
let h =
|
|
1198
|
+
let h = bottom - top;
|
|
906
1199
|
// NESTED HEIGHT ESCALATION (live report: "i cant increase height"). A
|
|
907
1200
|
// bounded strip cannot grow a tile taller than itself — so pulling
|
|
908
1201
|
// clearly past its bottom GROWS THE STRIP: the slab gains a row in the
|
|
@@ -950,38 +1243,71 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
950
1243
|
// The fluid preview must not outrun what the board can accept: on a
|
|
951
1244
|
// bounded strip an unclamped ghost ballooned to 273px while the engine
|
|
952
1245
|
// (rightly) refused every cell — visually indistinguishable from the
|
|
953
|
-
// squeeze bug it replaced. Clamp to the tile's maximum legal rect
|
|
1246
|
+
// squeeze bug it replaced. Clamp to the tile's maximum legal rect: the
|
|
1247
|
+
// growing side can reach the board edge on ITS side — column 0 / row 0
|
|
1248
|
+
// for a west / north pull, the last column / the bound otherwise.
|
|
954
1249
|
// (Escalation above may have just grown OR shrunk the board — re-read.)
|
|
955
1250
|
const fNow = frame();
|
|
956
1251
|
const ggNow = geom();
|
|
957
1252
|
h = Math.max(Math.max(8, rowHeightFor(ggNow, rows())), h);
|
|
958
1253
|
const itemNow = engine.getItem(g.id);
|
|
1254
|
+
const pullsX = E.e || E.w;
|
|
1255
|
+
const pullsY = E.n || E.s;
|
|
959
1256
|
if (itemNow) {
|
|
960
1257
|
const cuNow = columnUnitFor(ggNow, fNow.width);
|
|
961
1258
|
const rhNow = rowHeightFor(ggNow, rows());
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1259
|
+
const wCells = E.w ? itemNow.x + itemNow.w : columns - itemNow.x;
|
|
1260
|
+
w = Math.min(w, wCells * (cuNow + gap) - gap);
|
|
1261
|
+
const b = bound();
|
|
1262
|
+
const hCells = E.n ? itemNow.y + itemNow.h : b !== undefined ? Math.max(1, b - itemNow.y) : Infinity;
|
|
1263
|
+
if (hCells !== Infinity)
|
|
1264
|
+
h = Math.min(h, hCells * (rhNow + gap) - gap);
|
|
1265
|
+
// AN AXIS NOBODY PULLED KEEPS ITS CELLS. In fit mode a push during the
|
|
1266
|
+
// gesture reflows the board and the row height shrinks, so a pixel
|
|
1267
|
+
// height that never changed re-quantises to MORE rows (measured: a west
|
|
1268
|
+
// pull took a 3-row donut to 5). The un-pulled axis follows the live
|
|
1269
|
+
// projection of its cell span instead of the start-of-gesture pixels.
|
|
1270
|
+
if (!pullsX)
|
|
1271
|
+
w = itemNow.w * (cuNow + gap) - gap;
|
|
1272
|
+
if (!pullsY)
|
|
1273
|
+
h = itemNow.h * (rhNow + gap) - gap;
|
|
966
1274
|
}
|
|
1275
|
+
// Anchor: the pulled edges follow w/h, the opposite ones stay put — on
|
|
1276
|
+
// the LIVE projection of the tile's cell, not the start-of-gesture pixels.
|
|
1277
|
+
// In fit mode a push during the gesture reflows every row, so the row the
|
|
1278
|
+
// tile sits in moves; anchoring on the start rect drew the ghost a row or
|
|
1279
|
+
// two below its own placeholder (visual audit, west pull on the donut).
|
|
1280
|
+
const liveRect = itemNow ? cellToRect(itemNow, fNow, ggNow, rows()) : null;
|
|
1281
|
+
const anchorLeft = liveRect && !E.w && !E.e ? liveRect.x : left;
|
|
1282
|
+
const anchorRight = liveRect ? liveRect.x + liveRect.width : right;
|
|
1283
|
+
const anchorTop = liveRect && !E.n && !E.s ? liveRect.y : top;
|
|
1284
|
+
const anchorBottom = liveRect ? liveRect.y + liveRect.height : bottom;
|
|
1285
|
+
const px = E.w ? anchorRight - w : anchorLeft;
|
|
1286
|
+
const py = E.n ? anchorBottom - h : anchorTop;
|
|
967
1287
|
g.node.setSize(w, h, (_j = g.node.size.depth) !== null && _j !== void 0 ? _j : 0);
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
const startLeft = g.downWorld.x - g.grab.dx;
|
|
973
|
-
const right = startLeft + g.startSize.width;
|
|
974
|
-
g.node.setPosition(right - w, g.node.position.y);
|
|
975
|
-
ghostStyleFastPath(g, { x: right - w, width: w, height: h });
|
|
976
|
-
}
|
|
977
|
-
else {
|
|
978
|
-
ghostStyleFastPath(g, { width: w, height: h });
|
|
979
|
-
}
|
|
980
|
-
const spanF = maxRows !== undefined ? frame() : f;
|
|
981
|
-
const spanG = maxRows !== undefined ? geom() : gg;
|
|
1288
|
+
g.node.setPosition(px, py);
|
|
1289
|
+
ghostStyleFastPath(g, { x: px, y: py, width: w, height: h });
|
|
1290
|
+
const spanF = bound() !== undefined ? frame() : f;
|
|
1291
|
+
const spanG = bound() !== undefined ? geom() : gg;
|
|
982
1292
|
const span = sizeToSpan(w, h, spanF, spanG, rows());
|
|
983
|
-
if (
|
|
984
|
-
|
|
1293
|
+
if (itemNow) {
|
|
1294
|
+
if (!pullsX)
|
|
1295
|
+
span.w = itemNow.w;
|
|
1296
|
+
if (!pullsY)
|
|
1297
|
+
span.h = itemNow.h;
|
|
1298
|
+
const tx = E.w ? itemNow.x + itemNow.w - span.w : itemNow.x;
|
|
1299
|
+
const ty = E.n ? itemNow.y + itemNow.h - span.h : itemNow.y;
|
|
1300
|
+
const moves = tx !== itemNow.x || ty !== itemNow.y;
|
|
1301
|
+
const growing = span.w > itemNow.w || span.h > itemNow.h;
|
|
1302
|
+
let changed = false;
|
|
1303
|
+
if (moves && growing)
|
|
1304
|
+
changed = engine.moveCheck(g.id, tx, ty, { gate: false }).changed || changed;
|
|
1305
|
+
changed = engine.resizeCheck(g.id, span.w, span.h).changed || changed;
|
|
1306
|
+
if (moves && !growing)
|
|
1307
|
+
changed = engine.moveCheck(g.id, tx, ty, { gate: false }).changed || changed;
|
|
1308
|
+
if (changed)
|
|
1309
|
+
project();
|
|
1310
|
+
}
|
|
985
1311
|
syncPlaceholder();
|
|
986
1312
|
};
|
|
987
1313
|
const onToolUp = () => {
|
|
@@ -1039,6 +1365,7 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
1039
1365
|
cleanupGestureVisuals(g);
|
|
1040
1366
|
gesture = null;
|
|
1041
1367
|
enforceBoardHeight();
|
|
1368
|
+
persistLayouts();
|
|
1042
1369
|
api.renderNow();
|
|
1043
1370
|
(_c = options.onGesture) === null || _c === void 0 ? void 0 : _c.call(options, { type: 'commit', kind: g.kind, nodeId: g.id, changed: true });
|
|
1044
1371
|
return;
|
|
@@ -1128,8 +1455,9 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
1128
1455
|
// Clamp to the TARGET board's shape: a tall tile entering a one-row strip
|
|
1129
1456
|
// arrives as a strip-height tile, not a refusal.
|
|
1130
1457
|
span.w = Math.max(1, Math.min(columns, span.w));
|
|
1131
|
-
|
|
1132
|
-
|
|
1458
|
+
const b = bound();
|
|
1459
|
+
if (b !== undefined)
|
|
1460
|
+
span.h = Math.max(1, Math.min(b, span.h));
|
|
1133
1461
|
engine.beginGesture(); // pre-entry snapshot — abort() restores it
|
|
1134
1462
|
const entered = engine.add({ id: node.id, x: 0, y: engine.rows(), w: span.w, h: span.h });
|
|
1135
1463
|
if (!entered) {
|
|
@@ -1262,16 +1590,55 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
1262
1590
|
return insideMemberGroupFrame(ev.world.x, ev.world.y);
|
|
1263
1591
|
}
|
|
1264
1592
|
// Claim (and deaden) empty presses inside a member group's frame so the
|
|
1265
|
-
// built-in group-drag cannot fight the pack layout for the KPI slab
|
|
1266
|
-
|
|
1593
|
+
// built-in group-drag cannot fight the pack layout for the KPI slab —
|
|
1594
|
+
// and empty presses on the BOARD itself: its group is a layout
|
|
1595
|
+
// container, not a thing to drag around the canvas, and on a fluid
|
|
1596
|
+
// board there is no void outside it, so the space between tiles IS the
|
|
1597
|
+
// void (a click there clears the selection, see onPointerDown).
|
|
1598
|
+
return insideMemberGroupFrame(ev.world.x, ev.world.y) || worldInsideBoard(ev.world.x, ev.world.y);
|
|
1267
1599
|
},
|
|
1268
1600
|
onPointerDown(ev, hit) {
|
|
1269
|
-
var _a, _b, _c, _d, _e, _f;
|
|
1270
|
-
if (gesture
|
|
1271
|
-
return; //
|
|
1601
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
1602
|
+
if (gesture)
|
|
1603
|
+
return; // mid-palette
|
|
1604
|
+
if (!hit.node) {
|
|
1605
|
+
// The board's own empty area: a void click. Nothing to drag, and the
|
|
1606
|
+
// selection clears exactly as a click outside any board would.
|
|
1607
|
+
(_b = (_a = diagram).clearSelection) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
1608
|
+
api.render();
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1272
1611
|
const node = diagram.getNode(hit.node.id);
|
|
1273
|
-
if (!node || ((
|
|
1612
|
+
if (!node || ((_c = node.state) === null || _c === void 0 ? void 0 : _c.locked) === true)
|
|
1274
1613
|
return; // pinned: refuse; click still focuses
|
|
1614
|
+
if (isStatic)
|
|
1615
|
+
return; // a static board: claimed and deadened, click still focuses
|
|
1616
|
+
// Which edges did the press take? The corner handle names its own (s+e,
|
|
1617
|
+
// or s+w on RTL); a bare press within EDGE_GRIP of the tile's border
|
|
1618
|
+
// takes that border; anywhere else is a move.
|
|
1619
|
+
const target = ((_e = (_d = ev.source) === null || _d === void 0 ? void 0 : _d.target) !== null && _e !== void 0 ? _e : null);
|
|
1620
|
+
const onHandle = !!((_f = target === null || target === void 0 ? void 0 : target.closest) === null || _f === void 0 ? void 0 : _f.call(target, '.axdb-rs'));
|
|
1621
|
+
const hostEl = hostOf(node.id);
|
|
1622
|
+
const resizable = ((_g = node.getMetadata) === null || _g === void 0 ? void 0 : _g.call(node, 'widgetResizable')) !== false;
|
|
1623
|
+
const movable = ((_h = node.getMetadata) === null || _h === void 0 ? void 0 : _h.call(node, 'widgetMovable')) !== false;
|
|
1624
|
+
let edges = NO_EDGES;
|
|
1625
|
+
if (resizable) {
|
|
1626
|
+
if (onHandle)
|
|
1627
|
+
edges = rtl ? { n: false, e: false, s: true, w: true } : { n: false, e: true, s: true, w: false };
|
|
1628
|
+
else if (hostEl) {
|
|
1629
|
+
// `ev.screen` is ELEMENT-LOCAL px; the host rect is in client px.
|
|
1630
|
+
// Compare like with like — the source event's clientX/Y when there
|
|
1631
|
+
// is one, else the container's origin plus the local offset.
|
|
1632
|
+
const src = ev.source;
|
|
1633
|
+
const cr = api.container.getBoundingClientRect();
|
|
1634
|
+
const cx = typeof (src === null || src === void 0 ? void 0 : src.clientX) === 'number' ? src.clientX : cr.left + ev.screen.x;
|
|
1635
|
+
const cy = typeof (src === null || src === void 0 ? void 0 : src.clientY) === 'number' ? src.clientY : cr.top + ev.screen.y;
|
|
1636
|
+
edges = edgesNear(hostEl, cx, cy);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
const isResize = anyEdge(edges);
|
|
1640
|
+
if (!isResize && !movable)
|
|
1641
|
+
return; // a fixed tile: refuse the drag, click still focuses
|
|
1275
1642
|
// Arm the glide class NOW, a full task before any displacement can
|
|
1276
1643
|
// happen: a transition defined in the same style recalc as the first
|
|
1277
1644
|
// left/top write does not run (CSS transitions fire only when the
|
|
@@ -1279,8 +1646,6 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
1279
1646
|
// style) — the first pushed neighbour of every gesture TELEPORTED
|
|
1280
1647
|
// (measured 109px in one frame) while later ones glided.
|
|
1281
1648
|
armGlide();
|
|
1282
|
-
const target = ((_c = (_b = ev.source) === null || _b === void 0 ? void 0 : _b.target) !== null && _c !== void 0 ? _c : null);
|
|
1283
|
-
const isResize = !!((_d = target === null || target === void 0 ? void 0 : target.closest) === null || _d === void 0 ? void 0 : _d.call(target, '.axdb-rs'));
|
|
1284
1649
|
const it = engine.getItem(node.id);
|
|
1285
1650
|
gesture = {
|
|
1286
1651
|
kind: isResize ? 'resize' : 'move',
|
|
@@ -1296,7 +1661,9 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
1296
1661
|
startCells: new Map(),
|
|
1297
1662
|
startGeom: new Map(),
|
|
1298
1663
|
startSize: { width: node.size.width, height: node.size.height },
|
|
1299
|
-
|
|
1664
|
+
startPos: { x: node.position.x, y: node.position.y },
|
|
1665
|
+
edges,
|
|
1666
|
+
spans: { w: (_j = it === null || it === void 0 ? void 0 : it.w) !== null && _j !== void 0 ? _j : 1, h: (_k = it === null || it === void 0 ? void 0 : it.h) !== null && _k !== void 0 ? _k : 1 },
|
|
1300
1667
|
removedFromBoard: false,
|
|
1301
1668
|
leg: null,
|
|
1302
1669
|
lastWorld: null,
|
|
@@ -1317,10 +1684,179 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
1317
1684
|
},
|
|
1318
1685
|
};
|
|
1319
1686
|
const unregisterTool = registerTool(tool);
|
|
1687
|
+
/**
|
|
1688
|
+
* Edge affordance: the cursor says which border a press would take, the
|
|
1689
|
+
* way gridstack's invisible edge handles do. One passive listener on the
|
|
1690
|
+
* container; the corner handle keeps its own cursor from the stylesheet.
|
|
1691
|
+
*/
|
|
1692
|
+
let hoverHost = null;
|
|
1693
|
+
const onHover = (e) => {
|
|
1694
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
1695
|
+
if (disposed || gesture)
|
|
1696
|
+
return;
|
|
1697
|
+
// The event may target the host, its content, or (when a host's content
|
|
1698
|
+
// is pointer-transparent) the canvas under it — find the member host by
|
|
1699
|
+
// the pointer's position in that case.
|
|
1700
|
+
let host = (_b = (_a = e.target) === null || _a === void 0 ? void 0 : _a.closest) === null || _b === void 0 ? void 0 : _b.call(_a, '.grafloria-node-host');
|
|
1701
|
+
if (!host) {
|
|
1702
|
+
for (const id of (_c = group.members) !== null && _c !== void 0 ? _c : []) {
|
|
1703
|
+
const h = hostOf(id);
|
|
1704
|
+
if (!h)
|
|
1705
|
+
continue;
|
|
1706
|
+
const r = h.getBoundingClientRect();
|
|
1707
|
+
if (e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom) {
|
|
1708
|
+
host = h;
|
|
1709
|
+
break;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
if (hoverHost && hoverHost !== host)
|
|
1714
|
+
hoverHost.style.cursor = '';
|
|
1715
|
+
hoverHost = host;
|
|
1716
|
+
if (!host)
|
|
1717
|
+
return;
|
|
1718
|
+
const id = (_d = host.getAttribute('data-node-id')) !== null && _d !== void 0 ? _d : '';
|
|
1719
|
+
if (!((_e = group.members) !== null && _e !== void 0 ? _e : new Set()).has(id))
|
|
1720
|
+
return;
|
|
1721
|
+
const node = diagram.getNode(id);
|
|
1722
|
+
const resizable = !!node && !isStatic && ((_f = node.state) === null || _f === void 0 ? void 0 : _f.locked) !== true && ((_g = node.getMetadata) === null || _g === void 0 ? void 0 : _g.call(node, 'widgetResizable')) !== false;
|
|
1723
|
+
host.style.cursor = resizable ? cursorFor(edgesNear(host, e.clientX, e.clientY)) : '';
|
|
1724
|
+
};
|
|
1725
|
+
api.container.addEventListener('pointermove', onHover, { passive: true });
|
|
1726
|
+
/**
|
|
1727
|
+
* KEYBOARD OPERATION (WCAG 2.1.1, and the non-drag alternative 2.5.7 asks
|
|
1728
|
+
* for): on a focused member, arrows move it one cell, Shift+arrows resize it
|
|
1729
|
+
* one cell, Home/End jump to the first/last member. Every move and resize is
|
|
1730
|
+
* the same programmatic gesture the API uses — one undoable step, reported
|
|
1731
|
+
* through onLayoutChange — and every outcome is spoken: the tile's new
|
|
1732
|
+
* cell, each neighbour it displaced, or why it was refused. Handled keys
|
|
1733
|
+
* stop here so the renderer's own pixel nudge never fights the grid.
|
|
1734
|
+
*/
|
|
1735
|
+
const memberHostAt = (target) => {
|
|
1736
|
+
var _a, _b, _c;
|
|
1737
|
+
const host = (_a = target === null || target === void 0 ? void 0 : target.closest) === null || _a === void 0 ? void 0 : _a.call(target, '.grafloria-node-host');
|
|
1738
|
+
if (!host)
|
|
1739
|
+
return null;
|
|
1740
|
+
const id = (_b = host.getAttribute('data-node-id')) !== null && _b !== void 0 ? _b : '';
|
|
1741
|
+
if (!((_c = group.members) !== null && _c !== void 0 ? _c : new Set()).has(id) || !diagram.getNode(id))
|
|
1742
|
+
return null;
|
|
1743
|
+
return { id, host };
|
|
1744
|
+
};
|
|
1745
|
+
const onFocusIn = (e) => {
|
|
1746
|
+
const hit = memberHostAt(e.target);
|
|
1747
|
+
if (!hit || disposed)
|
|
1748
|
+
return;
|
|
1749
|
+
if (focusedId !== hit.id) {
|
|
1750
|
+
focusedId = hit.id;
|
|
1751
|
+
syncA11y();
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
1754
|
+
const onKey = (e) => {
|
|
1755
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
1756
|
+
if (disposed || gesture)
|
|
1757
|
+
return;
|
|
1758
|
+
const hit = memberHostAt(e.target);
|
|
1759
|
+
if (!hit) {
|
|
1760
|
+
// Tab reaches the diagram's own root (the svg) before any widget. An
|
|
1761
|
+
// arrow or Enter there hands focus to the board's tab stop, so a
|
|
1762
|
+
// keyboard user is never parked on "Diagram, 8 nodes" with nowhere to go.
|
|
1763
|
+
const el = e.target;
|
|
1764
|
+
const onRoot = !!el && ((_a = el.tagName) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'svg' && ((_b = el.classList) === null || _b === void 0 ? void 0 : _b.contains('grafloria-diagram'));
|
|
1765
|
+
if (onRoot && (e.key.startsWith('Arrow') || e.key === 'Enter' || e.key === ' ')) {
|
|
1766
|
+
const members = [...((_c = group.members) !== null && _c !== void 0 ? _c : [])].filter((id) => !!diagram.getNode(id) && !!hostOf(id));
|
|
1767
|
+
const target = focusedId && members.includes(focusedId) ? focusedId : members[0];
|
|
1768
|
+
if (target && handle.focusWidget(target)) {
|
|
1769
|
+
e.preventDefault();
|
|
1770
|
+
e.stopPropagation();
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
const members = [...((_d = group.members) !== null && _d !== void 0 ? _d : [])].filter((id) => !!diagram.getNode(id) && !!hostOf(id));
|
|
1776
|
+
if (e.key === 'Home' || e.key === 'End') {
|
|
1777
|
+
const id = e.key === 'Home' ? members[0] : members[members.length - 1];
|
|
1778
|
+
if (id)
|
|
1779
|
+
handle.focusWidget(id);
|
|
1780
|
+
e.preventDefault();
|
|
1781
|
+
e.stopPropagation();
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
const arrow = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] }[e.key];
|
|
1785
|
+
if (!arrow)
|
|
1786
|
+
return;
|
|
1787
|
+
e.preventDefault();
|
|
1788
|
+
e.stopPropagation();
|
|
1789
|
+
if (isStatic)
|
|
1790
|
+
return; // readable, not editable
|
|
1791
|
+
const node = diagram.getNode(hit.id);
|
|
1792
|
+
const item = engine.getItem(hit.id);
|
|
1793
|
+
if (!node || !item)
|
|
1794
|
+
return;
|
|
1795
|
+
const name = nameOf(node);
|
|
1796
|
+
if (((_e = node.state) === null || _e === void 0 ? void 0 : _e.locked) === true) {
|
|
1797
|
+
live.announceError(`${name} is pinned`);
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
const [dx, dy] = arrow;
|
|
1801
|
+
const before = new Map(engine.getItems().map((i) => [i.id, { x: i.x, y: i.y, w: i.w, h: i.h }]));
|
|
1802
|
+
const resize = e.shiftKey;
|
|
1803
|
+
if (resize && ((_f = node.getMetadata) === null || _f === void 0 ? void 0 : _f.call(node, 'widgetResizable')) === false) {
|
|
1804
|
+
live.announceError(`${name} cannot be resized`);
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
if (!resize && ((_g = node.getMetadata) === null || _g === void 0 ? void 0 : _g.call(node, 'widgetMovable')) === false) {
|
|
1808
|
+
live.announceError(`${name} cannot be moved`);
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
// A one-cell keyboard step onto a neighbour would be refused by the
|
|
1812
|
+
// pointer's anti-jitter gate (a 3-wide covering a third of its neighbour
|
|
1813
|
+
// is not "more than half"). A key press is deliberate, so when the step
|
|
1814
|
+
// is refused and a neighbour sits there, aim at the neighbour's far edge —
|
|
1815
|
+
// the same swap a full drag lands on.
|
|
1816
|
+
const stepOrSwap = () => __awaiter(this, void 0, void 0, function* () {
|
|
1817
|
+
if (yield handle.moveTo(hit.id, item.x + dx, item.y + dy))
|
|
1818
|
+
return true;
|
|
1819
|
+
const probe = { x: item.x + dx, y: item.y + dy, w: item.w, h: item.h };
|
|
1820
|
+
const c = engine
|
|
1821
|
+
.getItems()
|
|
1822
|
+
.find((o) => o.id !== hit.id && probe.x < o.x + o.w && o.x < probe.x + probe.w && probe.y < o.y + o.h && o.y < probe.y + probe.h);
|
|
1823
|
+
if (!c)
|
|
1824
|
+
return false;
|
|
1825
|
+
const tx = dx > 0 ? c.x + c.w - item.w : dx < 0 ? c.x : item.x;
|
|
1826
|
+
const ty = dy > 0 ? c.y + c.h - item.h : dy < 0 ? c.y : item.y;
|
|
1827
|
+
return handle.moveTo(hit.id, tx, ty);
|
|
1828
|
+
});
|
|
1829
|
+
const op = resize ? handle.resizeTo(hit.id, item.w + dx, item.h + dy) : stepOrSwap();
|
|
1830
|
+
void op.then((ok) => {
|
|
1831
|
+
var _a, _b;
|
|
1832
|
+
if (disposed)
|
|
1833
|
+
return;
|
|
1834
|
+
const after = engine.getItem(hit.id);
|
|
1835
|
+
if (!ok || !after) {
|
|
1836
|
+
live.announceError(resize ? `Cannot resize ${name} that way` : `Cannot move ${name} ${directionName(dx, dy)}`);
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
const parts = [`${name} ${resize ? 'resized' : 'moved'} to ${describeCell(after)}`];
|
|
1840
|
+
for (const other of engine.getItems()) {
|
|
1841
|
+
if (other.id === hit.id)
|
|
1842
|
+
continue;
|
|
1843
|
+
const was = before.get(other.id);
|
|
1844
|
+
if (!was || (was.x === other.x && was.y === other.y))
|
|
1845
|
+
continue;
|
|
1846
|
+
const o = diagram.getNode(other.id);
|
|
1847
|
+
parts.push(`${o ? nameOf(o) : other.id} moved to ${describeCell(other)}`);
|
|
1848
|
+
}
|
|
1849
|
+
live.announce(parts.join('. '), 'polite', true);
|
|
1850
|
+
syncA11y();
|
|
1851
|
+
(_b = (_a = hostOf(hit.id)) === null || _a === void 0 ? void 0 : _a.focus) === null || _b === void 0 ? void 0 : _b.call(_a, { preventScroll: true });
|
|
1852
|
+
});
|
|
1853
|
+
};
|
|
1854
|
+
api.container.addEventListener('focusin', onFocusIn);
|
|
1855
|
+
api.container.addEventListener('keydown', onKey);
|
|
1320
1856
|
// -- palette drag-in --------------------------------------------------------
|
|
1321
1857
|
const beginPaletteDrag = (node, spec, event) => {
|
|
1322
1858
|
var _a, _b;
|
|
1323
|
-
if (disposed || gesture)
|
|
1859
|
+
if (disposed || gesture || isStatic)
|
|
1324
1860
|
return;
|
|
1325
1861
|
const chip = (_a = spec.chip) !== null && _a !== void 0 ? _a : null;
|
|
1326
1862
|
if (chip) {
|
|
@@ -1341,6 +1877,8 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
1341
1877
|
startCells: new Map(),
|
|
1342
1878
|
startGeom: new Map(),
|
|
1343
1879
|
startSize: { width: 0, height: 0 },
|
|
1880
|
+
startPos: { x: 0, y: 0 },
|
|
1881
|
+
edges: NO_EDGES,
|
|
1344
1882
|
spans: { w: Math.max(1, spec.w), h: Math.max(1, spec.h) },
|
|
1345
1883
|
removedFromBoard: true,
|
|
1346
1884
|
leg: null,
|
|
@@ -1386,8 +1924,12 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
1386
1924
|
g.removedFromBoard = false;
|
|
1387
1925
|
// Enter at the bottom edge (collision-free), then take the cursor
|
|
1388
1926
|
// cell GATELESSLY — gridstack's drag-in skips the gate on entry.
|
|
1389
|
-
|
|
1390
|
-
|
|
1927
|
+
// A bounded board with no room refuses the entry: the chip dims to
|
|
1928
|
+
// say so, and the release will snap it home.
|
|
1929
|
+
const entered = engine.add({ id: g.id, x: 0, y: engine.rows(), w: g.spans.w, h: g.spans.h });
|
|
1930
|
+
chip === null || chip === void 0 ? void 0 : chip.classList.toggle('axdb-out', !entered);
|
|
1931
|
+
if (entered)
|
|
1932
|
+
engine.moveCheck(g.id, cell.x, cell.y, { gate: false });
|
|
1391
1933
|
project();
|
|
1392
1934
|
}
|
|
1393
1935
|
else if (engine.moveCheck(g.id, cell.x, cell.y).changed) {
|
|
@@ -1396,6 +1938,7 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
1396
1938
|
}
|
|
1397
1939
|
else if (!g.removedFromBoard) {
|
|
1398
1940
|
g.removedFromBoard = true;
|
|
1941
|
+
chip === null || chip === void 0 ? void 0 : chip.classList.remove('axdb-out');
|
|
1399
1942
|
engine.remove(g.id); // displaced tiles come home (gesture memory)
|
|
1400
1943
|
project();
|
|
1401
1944
|
}
|
|
@@ -1423,6 +1966,7 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
1423
1966
|
cleanupGestureVisuals(g);
|
|
1424
1967
|
gesture = null;
|
|
1425
1968
|
void ((_a = options.onDropIn) === null || _a === void 0 ? void 0 : _a.call(options, node, cell, displaced));
|
|
1969
|
+
persistLayouts();
|
|
1426
1970
|
(_b = options.onGesture) === null || _b === void 0 ? void 0 : _b.call(options, { type: 'drop-in', kind: 'palette', nodeId: g.id, changed: true });
|
|
1427
1971
|
api.renderNow();
|
|
1428
1972
|
return;
|
|
@@ -1473,35 +2017,63 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
1473
2017
|
if (commands.length > 0) {
|
|
1474
2018
|
yield api.getEngine().commandManager.execute(new BatchCommand(name, commands));
|
|
1475
2019
|
}
|
|
2020
|
+
persistLayouts();
|
|
1476
2021
|
api.renderNow();
|
|
1477
2022
|
return true;
|
|
1478
2023
|
});
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
2024
|
+
/** Rebuild the engine from the members' persisted cells. `pack` runs gravity
|
|
2025
|
+
* over the result (boot, float→off); a plain sync keeps the cells verbatim. */
|
|
2026
|
+
const rebuild = (pack) => {
|
|
2027
|
+
var _a, _b, _c, _d;
|
|
2028
|
+
if (disposed)
|
|
2029
|
+
return;
|
|
2030
|
+
if (gesture)
|
|
2031
|
+
cancelActiveGesture(false);
|
|
2032
|
+
applyFluidFrame();
|
|
2033
|
+
const items = [];
|
|
2034
|
+
for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : []) {
|
|
2035
|
+
if (!memberEntity(id))
|
|
2036
|
+
continue;
|
|
2037
|
+
items.push(itemFor(id));
|
|
2038
|
+
}
|
|
2039
|
+
// CARRY THE PER-COLUMN CACHE ACROSS THE REBUILD. sync() runs on every
|
|
2040
|
+
// undo, member add and refresh; without this handoff a responsive board
|
|
2041
|
+
// would silently lose its wide layouts the first time anything else
|
|
2042
|
+
// happened, and growing back would re-derive instead of restoring. On a
|
|
2043
|
+
// FRESH bind (a loaded document) the cache comes from the group instead.
|
|
2044
|
+
const persisted = (_b = group.getMetadata) === null || _b === void 0 ? void 0 : _b.call(group, 'dashboardLayouts');
|
|
2045
|
+
const carried = engine.cachedColumns().length > 0 ? engine.getLayouts() : ((_c = persisted === null || persisted === void 0 ? void 0 : persisted.layouts) !== null && _c !== void 0 ? _c : {});
|
|
2046
|
+
// The document's cells belong to the count it was SAVED at. When that is
|
|
2047
|
+
// not the count this board is bound at, build the engine at the saved
|
|
2048
|
+
// count — where the cells are legal — hand it the cache, and let its own
|
|
2049
|
+
// column change bring the board to the bound count: known items come back
|
|
2050
|
+
// from the cache exactly, the rest scale. (Only a fresh bind can see a
|
|
2051
|
+
// mismatch; every later rebuild finds cells the binder itself wrote.)
|
|
2052
|
+
const savedAt = engine.cachedColumns().length === 0 && typeof (persisted === null || persisted === void 0 ? void 0 : persisted.columns) === 'number' ? persisted.columns : columns;
|
|
2053
|
+
engine = engineFrom(items, pack, savedAt);
|
|
2054
|
+
engine.setLayouts(carried);
|
|
2055
|
+
const converted = savedAt !== columns && engine.setColumns(columns, (_d = responsive === null || responsive === void 0 ? void 0 : responsive.layout) !== null && _d !== void 0 ? _d : 'moveScale');
|
|
2056
|
+
// A PACKED or CONVERTED rebuild may have moved cells; write them all back
|
|
2057
|
+
// so the next verbatim sync reads the settled board, not the pre-pack one.
|
|
2058
|
+
// A verbatim rebuild only fills in cells that were never persisted.
|
|
2059
|
+
if (pack || converted)
|
|
2060
|
+
persistLiveCells();
|
|
2061
|
+
else
|
|
1499
2062
|
for (const item of engine.getItems())
|
|
1500
2063
|
persistAdoptedCell(item.id, item);
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
2064
|
+
// The capacity is derived from the design height AND floors at the content
|
|
2065
|
+
// just rebuilt, so it is computed here and carried into the engine.
|
|
2066
|
+
if (refreshCapacity())
|
|
2067
|
+
engine = engineFrom(engine.getItems().map((i) => (Object.assign({}, i))), false);
|
|
2068
|
+
persistLayouts();
|
|
2069
|
+
project();
|
|
2070
|
+
syncHandles();
|
|
2071
|
+
api.renderNow();
|
|
2072
|
+
evaluateResponsive();
|
|
2073
|
+
};
|
|
2074
|
+
const handle = {
|
|
2075
|
+
sync() {
|
|
2076
|
+
rebuild(false);
|
|
1505
2077
|
},
|
|
1506
2078
|
setColumns(n, layout, opts) {
|
|
1507
2079
|
var _a;
|
|
@@ -1522,6 +2094,26 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
1522
2094
|
api.renderNow();
|
|
1523
2095
|
},
|
|
1524
2096
|
getRtl: () => rtl,
|
|
2097
|
+
focusWidget(id) {
|
|
2098
|
+
var _a, _b, _c;
|
|
2099
|
+
if (disposed || !((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(id) || !diagram.getNode(id))
|
|
2100
|
+
return false;
|
|
2101
|
+
focusedId = id;
|
|
2102
|
+
syncA11y();
|
|
2103
|
+
(_c = (_b = hostOf(id)) === null || _b === void 0 ? void 0 : _b.focus) === null || _c === void 0 ? void 0 : _c.call(_b, { preventScroll: true });
|
|
2104
|
+
return true;
|
|
2105
|
+
},
|
|
2106
|
+
getFocusedWidget: () => focusedId,
|
|
2107
|
+
setStatic(on) {
|
|
2108
|
+
if (on === isStatic)
|
|
2109
|
+
return;
|
|
2110
|
+
isStatic = on;
|
|
2111
|
+
if (gesture)
|
|
2112
|
+
cancelActiveGesture(false);
|
|
2113
|
+
syncHandles();
|
|
2114
|
+
api.renderNow();
|
|
2115
|
+
},
|
|
2116
|
+
getStatic: () => isStatic,
|
|
1525
2117
|
saveLayout() {
|
|
1526
2118
|
const saved = engine.saveLayout();
|
|
1527
2119
|
return {
|
|
@@ -1533,7 +2125,11 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
1533
2125
|
if (mode === sizing)
|
|
1534
2126
|
return;
|
|
1535
2127
|
sizing = mode;
|
|
1536
|
-
|
|
2128
|
+
applyFluidFrame();
|
|
2129
|
+
if (refreshCapacity())
|
|
2130
|
+
rebuild(false);
|
|
2131
|
+
else
|
|
2132
|
+
project();
|
|
1537
2133
|
api.renderNow();
|
|
1538
2134
|
},
|
|
1539
2135
|
getSizing: () => sizing,
|
|
@@ -1541,10 +2137,9 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
1541
2137
|
if (on === float)
|
|
1542
2138
|
return;
|
|
1543
2139
|
float = on;
|
|
1544
|
-
// Rebuild from persisted cells under the new mode:
|
|
1545
|
-
//
|
|
1546
|
-
|
|
1547
|
-
this.sync();
|
|
2140
|
+
// Rebuild from persisted cells under the new mode, PACKED: gravity (when
|
|
2141
|
+
// float turns off) applies immediately through the rebuild's settle.
|
|
2142
|
+
rebuild(true);
|
|
1548
2143
|
api.renderNow();
|
|
1549
2144
|
},
|
|
1550
2145
|
getFloat: () => float,
|
|
@@ -1557,6 +2152,9 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
1557
2152
|
maxColumns,
|
|
1558
2153
|
rtl,
|
|
1559
2154
|
responsive: !!responsive && !responsivePinned,
|
|
2155
|
+
fluid,
|
|
2156
|
+
static: isStatic,
|
|
2157
|
+
capacity,
|
|
1560
2158
|
gap,
|
|
1561
2159
|
padding,
|
|
1562
2160
|
sizing,
|
|
@@ -1567,6 +2165,12 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
1567
2165
|
frame: f,
|
|
1568
2166
|
};
|
|
1569
2167
|
},
|
|
2168
|
+
willItFit(w, h) {
|
|
2169
|
+
if (bound() === undefined)
|
|
2170
|
+
return true;
|
|
2171
|
+
const probe = engineFrom(engine.getItems().map((i) => (Object.assign({}, i))));
|
|
2172
|
+
return probe.add({ id: '\u0000probe', x: 0, y: 0, w: Math.max(1, w), h: Math.max(1, h), autoPosition: true }) !== null;
|
|
2173
|
+
},
|
|
1570
2174
|
cellOf(id) {
|
|
1571
2175
|
const it = engine.getItem(id);
|
|
1572
2176
|
return it ? { x: it.x, y: it.y, w: it.w, h: it.h } : undefined;
|
|
@@ -1613,31 +2217,40 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
1613
2217
|
},
|
|
1614
2218
|
beginPaletteDrag,
|
|
1615
2219
|
dispose() {
|
|
1616
|
-
var _a;
|
|
2220
|
+
var _a, _b, _c, _d;
|
|
1617
2221
|
if (disposed)
|
|
1618
2222
|
return;
|
|
1619
2223
|
cancelActiveGesture(false);
|
|
1620
2224
|
disposed = true;
|
|
1621
2225
|
peersOnCanvas().delete(selfPeer);
|
|
1622
2226
|
unregisterTool();
|
|
2227
|
+
api.container.removeEventListener('pointermove', onHover);
|
|
2228
|
+
api.container.removeEventListener('focusin', onFocusIn);
|
|
2229
|
+
api.container.removeEventListener('keydown', onKey);
|
|
1623
2230
|
hostObserver.disconnect();
|
|
1624
2231
|
containerObserver === null || containerObserver === void 0 ? void 0 : containerObserver.disconnect();
|
|
1625
2232
|
for (const off of subs)
|
|
1626
2233
|
off();
|
|
2234
|
+
// The corner handles are THIS binder's affordance: a board re-bound as a
|
|
2235
|
+
// split layout must not keep showing a resize corner it cannot act on.
|
|
2236
|
+
for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : [])
|
|
2237
|
+
(_c = (_b = hostOf(id)) === null || _b === void 0 ? void 0 : _b.querySelector(':scope > .axdb-rs')) === null || _c === void 0 ? void 0 : _c.remove();
|
|
1627
2238
|
placeholder === null || placeholder === void 0 ? void 0 : placeholder.remove();
|
|
1628
2239
|
placeholder = null;
|
|
1629
2240
|
if (glideTimer)
|
|
1630
2241
|
clearTimeout(glideTimer);
|
|
1631
2242
|
if (ghostTimer)
|
|
1632
2243
|
clearTimeout(ghostTimer);
|
|
1633
|
-
(
|
|
2244
|
+
(_d = htmlLayer()) === null || _d === void 0 ? void 0 : _d.classList.remove('axdb-glide');
|
|
1634
2245
|
api.container.style.cursor = '';
|
|
1635
2246
|
},
|
|
1636
2247
|
};
|
|
1637
|
-
// Boot: adopt the current members
|
|
1638
|
-
//
|
|
1639
|
-
//
|
|
1640
|
-
|
|
2248
|
+
// Boot: adopt the current members (PACKED — a declared cell hovering over an
|
|
2249
|
+
// empty row settles, the documented contract), observe host churn for handle
|
|
2250
|
+
// re-injection, and let a responsive board settle on the count its width asks
|
|
2251
|
+
// for before the first frame (a 400px board declared at 12 columns must not
|
|
2252
|
+
// flash at 12).
|
|
2253
|
+
rebuild(true);
|
|
1641
2254
|
const layer = htmlLayer();
|
|
1642
2255
|
if (layer)
|
|
1643
2256
|
hostObserver.observe(layer, { childList: true, subtree: true });
|