@grafloria/element 0.4.11 → 0.4.13
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 +16 -0
- package/src/lib/dashboard-kit/dashboard.js +45 -19
- package/src/lib/dashboard-kit/grid-binder.d.ts +40 -0
- package/src/lib/dashboard-kit/grid-binder.js +90 -8
- package/src/lib/dashboard-kit/split-binder.d.ts +1 -1
- package/src/lib/dashboard-kit/split-binder.js +43 -5
- package/src/lib/dashboard-kit/styles.js +19 -4
package/package.json
CHANGED
|
@@ -199,6 +199,14 @@ export interface DashboardOptions {
|
|
|
199
199
|
* interactive. Live: `handle.setDragHandle()`.
|
|
200
200
|
*/
|
|
201
201
|
dragHandle?: DragHandleOption;
|
|
202
|
+
/**
|
|
203
|
+
* FIT AND THE ROW FLOOR. `true` (default): a bounded fit board squeezes its
|
|
204
|
+
* rows toward `minRowHeight` before refusing growth. `false`: rows freeze at
|
|
205
|
+
* the height they have now — a gesture that needs a row the frame does not
|
|
206
|
+
* hold is refused outright and no other tile shrinks. See the grid binder's
|
|
207
|
+
* `squeeze` for the exact rule.
|
|
208
|
+
*/
|
|
209
|
+
squeeze?: boolean;
|
|
202
210
|
/**
|
|
203
211
|
* RIGHT-TO-LEFT boards: column x=0 renders at the RIGHT edge and columns run
|
|
204
212
|
* leftwards. Cells are untouched — the same `widgets` array describes the
|
|
@@ -274,6 +282,14 @@ export interface DashboardHandle {
|
|
|
274
282
|
* and a quiet ring; a void click clears. False when the id is unknown.
|
|
275
283
|
*/
|
|
276
284
|
focusWidget(id: string): boolean;
|
|
285
|
+
/**
|
|
286
|
+
* SELECT a widget WITHOUT moving keyboard focus — the ring and the grip,
|
|
287
|
+
* nothing else; what a mouse press does. `undefined` clears the selection
|
|
288
|
+
* on every view. False when the id is unknown.
|
|
289
|
+
*/
|
|
290
|
+
selectWidget(id: string | undefined): boolean;
|
|
291
|
+
/** The selected widget, if any (across views: only the on-camera one can be). */
|
|
292
|
+
getSelectedWidget(): string | undefined;
|
|
277
293
|
/** Every widget handle of a view (default: the active one). */
|
|
278
294
|
widgetsOf(viewId?: string): WidgetHandle[];
|
|
279
295
|
/**
|
|
@@ -505,9 +505,38 @@ export function createDashboardHandle(ctx) {
|
|
|
505
505
|
return makeWidgetHandle(id);
|
|
506
506
|
},
|
|
507
507
|
focusWidget(id) {
|
|
508
|
-
var _a
|
|
508
|
+
var _a;
|
|
509
509
|
const b = binders.get((_a = viewOfWidget.get(id)) !== null && _a !== void 0 ? _a : '');
|
|
510
|
-
|
|
510
|
+
if (!b)
|
|
511
|
+
return false;
|
|
512
|
+
if (b.focusWidget(id))
|
|
513
|
+
return true;
|
|
514
|
+
// A widget added in this same tick is not a member yet (adds commit
|
|
515
|
+
// asynchronously): the spec knows it, so select it once the commit lands.
|
|
516
|
+
if (!specById.has(id))
|
|
517
|
+
return false;
|
|
518
|
+
const retry = (n) => { if (!b.focusWidget(id) && n > 0)
|
|
519
|
+
setTimeout(() => retry(n - 1), 16); };
|
|
520
|
+
queueMicrotask(() => retry(4));
|
|
521
|
+
return true;
|
|
522
|
+
},
|
|
523
|
+
selectWidget(id) {
|
|
524
|
+
var _a;
|
|
525
|
+
if (id === undefined) {
|
|
526
|
+
for (const b of binders.values())
|
|
527
|
+
b.selectWidget(undefined);
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
const b = binders.get((_a = viewOfWidget.get(id)) !== null && _a !== void 0 ? _a : '');
|
|
531
|
+
return !!b && b.selectWidget(id);
|
|
532
|
+
},
|
|
533
|
+
getSelectedWidget() {
|
|
534
|
+
for (const b of binders.values()) {
|
|
535
|
+
const s = b.getSelectedWidget();
|
|
536
|
+
if (s)
|
|
537
|
+
return s;
|
|
538
|
+
}
|
|
539
|
+
return undefined;
|
|
511
540
|
},
|
|
512
541
|
widgetsOf(viewId) {
|
|
513
542
|
var _a;
|
|
@@ -1032,7 +1061,7 @@ export function dashboard(options) {
|
|
|
1032
1061
|
// its tree from exactly those, so any stale tree is cleared first;
|
|
1033
1062
|
// split → grid rebuilds from them, so the column cache goes too.
|
|
1034
1063
|
ctx.rebindView = (viewId, next) => {
|
|
1035
|
-
var _a;
|
|
1064
|
+
var _a, _b, _c;
|
|
1036
1065
|
const v = views.find((x) => x.id === viewId);
|
|
1037
1066
|
const g = groups.get(viewId);
|
|
1038
1067
|
const b = binders.get(viewId);
|
|
@@ -1040,6 +1069,8 @@ export function dashboard(options) {
|
|
|
1040
1069
|
return;
|
|
1041
1070
|
const cells = b.saveLayout().cells;
|
|
1042
1071
|
const live = { rtl: b.getRtl(), static: b.getStatic(), dragHandle: b.getDragHandle() };
|
|
1072
|
+
const focused = b.getFocusedWidget();
|
|
1073
|
+
const selected = b.getSelectedWidget();
|
|
1043
1074
|
b.dispose();
|
|
1044
1075
|
const write = (fn) => (model.runSystemWrite ? model.runSystemWrite(fn) : fn());
|
|
1045
1076
|
write(() => {
|
|
@@ -1059,6 +1090,14 @@ export function dashboard(options) {
|
|
|
1059
1090
|
ctx.layoutOf.set(viewId, next);
|
|
1060
1091
|
binders.set(viewId, bindView(v, g, next, live));
|
|
1061
1092
|
(_a = binders.get(viewId)) === null || _a === void 0 ? void 0 : _a.sync();
|
|
1093
|
+
// The selected widget (and its grip) survives the switch, as it does in
|
|
1094
|
+
// the DevExpress designer — the host used to have to restate it. A
|
|
1095
|
+
// MOUSE selection is not a focus (the press never focuses the host),
|
|
1096
|
+
// so it is carried on its own: the kit lab's L21 lost it (2026-09-08).
|
|
1097
|
+
if (focused)
|
|
1098
|
+
(_b = binders.get(viewId)) === null || _b === void 0 ? void 0 : _b.focusWidget(focused);
|
|
1099
|
+
else if (selected)
|
|
1100
|
+
(_c = binders.get(viewId)) === null || _c === void 0 ? void 0 : _c.selectWidget(selected);
|
|
1062
1101
|
};
|
|
1063
1102
|
handle.showView(ctx.active);
|
|
1064
1103
|
ctx.rebindContainer = (id) => {
|
|
@@ -1153,7 +1192,7 @@ export function dashboard(options) {
|
|
|
1153
1192
|
*/
|
|
1154
1193
|
function bindView(v, g, viewLayout, live) {
|
|
1155
1194
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
1156
|
-
const common = Object.assign(Object.assign({ gap, padding: gap, rtl: (_b = (_a = live === null || live === void 0 ? void 0 : live.rtl) !== null && _a !== void 0 ? _a : options.rtl) !== null && _b !== void 0 ? _b : false, fluid: mode === 'fluid', static: (_d = (_c = live === null || live === void 0 ? void 0 : live.static) !== null && _c !== void 0 ? _c : options.static) !== null && _d !== void 0 ? _d : false, dragHandle: (_f = (_e = live === null || live === void 0 ? void 0 : live.dragHandle) !== null && _e !== void 0 ? _e : options.dragHandle) !== null && _f !== void 0 ? _f : false }, ((_g = options.binder) !== null && _g !== void 0 ? _g : {})), { onGesture: (e) => {
|
|
1195
|
+
const common = Object.assign(Object.assign(Object.assign({ gap, padding: gap, rtl: (_b = (_a = live === null || live === void 0 ? void 0 : live.rtl) !== null && _a !== void 0 ? _a : options.rtl) !== null && _b !== void 0 ? _b : false, fluid: mode === 'fluid', static: (_d = (_c = live === null || live === void 0 ? void 0 : live.static) !== null && _c !== void 0 ? _c : options.static) !== null && _d !== void 0 ? _d : false, dragHandle: (_f = (_e = live === null || live === void 0 ? void 0 : live.dragHandle) !== null && _e !== void 0 ? _e : options.dragHandle) !== null && _f !== void 0 ? _f : false }, (options.squeeze !== undefined ? { squeeze: options.squeeze } : {})), ((_g = options.binder) !== null && _g !== void 0 ? _g : {})), { onGesture: (e) => {
|
|
1157
1196
|
var _a, _b;
|
|
1158
1197
|
if (e.type === 'commit')
|
|
1159
1198
|
reportChanged();
|
|
@@ -1167,25 +1206,12 @@ export function dashboard(options) {
|
|
|
1167
1206
|
/** Bind (or re-bind) a container's inner grid on its group. */
|
|
1168
1207
|
function bindContainer(cg, w, viewId) {
|
|
1169
1208
|
var _a, _b, _c, _d, _e;
|
|
1170
|
-
binders.set(w.id, bindDashboardGrid(a, cg, {
|
|
1171
|
-
columns: innerColumnsOf(w),
|
|
1172
|
-
gap,
|
|
1173
|
-
padding: 0,
|
|
1174
|
-
sizing: 'fit',
|
|
1175
|
-
baseRowHeight: rowHeight,
|
|
1176
|
-
designHeight: 0,
|
|
1177
|
-
maxRows: (_a = w.maxRows) !== null && _a !== void 0 ? _a : rowExtentOf((_b = w.widgets) !== null && _b !== void 0 ? _b : []),
|
|
1178
|
-
float: false,
|
|
1179
|
-
rtl: (_c = options.rtl) !== null && _c !== void 0 ? _c : false,
|
|
1180
|
-
static: (_d = options.static) !== null && _d !== void 0 ? _d : false,
|
|
1181
|
-
dragHandle: (_e = options.dragHandle) !== null && _e !== void 0 ? _e : false,
|
|
1182
|
-
onGesture: (e) => {
|
|
1209
|
+
binders.set(w.id, bindDashboardGrid(a, cg, Object.assign(Object.assign({ columns: innerColumnsOf(w), gap, padding: 0, sizing: 'fit', baseRowHeight: rowHeight, designHeight: 0, maxRows: (_a = w.maxRows) !== null && _a !== void 0 ? _a : rowExtentOf((_b = w.widgets) !== null && _b !== void 0 ? _b : []), float: false, rtl: (_c = options.rtl) !== null && _c !== void 0 ? _c : false, static: (_d = options.static) !== null && _d !== void 0 ? _d : false, dragHandle: (_e = options.dragHandle) !== null && _e !== void 0 ? _e : false }, (options.squeeze !== undefined ? { squeeze: options.squeeze } : {})), { onGesture: (e) => {
|
|
1183
1210
|
var _a, _b;
|
|
1184
1211
|
if (e.type === 'commit')
|
|
1185
1212
|
reportChanged();
|
|
1186
1213
|
(_b = (_a = options.binder) === null || _a === void 0 ? void 0 : _a.onGesture) === null || _b === void 0 ? void 0 : _b.call(_a, e);
|
|
1187
|
-
}
|
|
1188
|
-
}));
|
|
1214
|
+
} })));
|
|
1189
1215
|
}
|
|
1190
1216
|
/**
|
|
1191
1217
|
* One reporter for every binder on a view — the view's own and each
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
* second `bindDashboardGrid` on the section for a nested pack grid.
|
|
51
51
|
*/
|
|
52
52
|
import { Command, type DiagramModel, type GridColumnLayout, type GroupModel, type NodeModel } from '@grafloria/engine';
|
|
53
|
+
import { type ToolPointerEvent } from '@grafloria/renderer';
|
|
53
54
|
import { type CellRect, type WorldRect } from './grid-mapping.js';
|
|
54
55
|
/** The slice of a DiagramInstance the binder needs (structural, test-friendly). */
|
|
55
56
|
export interface DashboardGridApi {
|
|
@@ -223,6 +224,18 @@ export interface DashboardGridOptions {
|
|
|
223
224
|
* interactive (scroll a table, click a legend). Live: `setDragHandle`.
|
|
224
225
|
*/
|
|
225
226
|
dragHandle?: DragHandleOption;
|
|
227
|
+
/**
|
|
228
|
+
* FIT AND THE ROW FLOOR. `true` (default): a bounded fit board holds as many
|
|
229
|
+
* rows as fit at `minRowHeight` — a gesture that needs another row SQUEEZES
|
|
230
|
+
* every row toward the floor, and is refused only when even that is not
|
|
231
|
+
* enough. `false`: rows FREEZE at the height they have now — the capacity is
|
|
232
|
+
* what the frame holds at the current row height, so a resize, move or add
|
|
233
|
+
* that needs a row the frame does not have is refused outright (placeholder
|
|
234
|
+
* stays, `onGesture` reports `changed: false`) and no other tile shrinks. A
|
|
235
|
+
* board loaded with more rows than fit still squeezes to the frame (fit never
|
|
236
|
+
* scrolls); it just cannot be pushed further by a gesture.
|
|
237
|
+
*/
|
|
238
|
+
squeeze?: boolean;
|
|
226
239
|
}
|
|
227
240
|
/** A painted grip: the only drag zone, placed along the card's top edge. */
|
|
228
241
|
export interface DragGripOptions {
|
|
@@ -249,6 +262,23 @@ export declare const gripOf: (v: DragHandleOption) => DragGripOptions | null;
|
|
|
249
262
|
*/
|
|
250
263
|
export declare function syncGrip(host: HTMLElement, cfg: DragGripOptions | null, movable: boolean): void;
|
|
251
264
|
/** The member host a press on a painted grip belongs to (the grip may sit OUTSIDE the host's box). */
|
|
265
|
+
/**
|
|
266
|
+
* Is this press OURS? The renderer's tool registry is PAGE-GLOBAL: every
|
|
267
|
+
* registered tool is asked about every press on every canvas, ties going to
|
|
268
|
+
* the first registered. Two boards on one page sharing widget ids — a designer
|
|
269
|
+
* beside its preview, a page of examples — had the FIRST board's tool claim
|
|
270
|
+
* the second's presses: it selected its own tile and moved nothing (kit lab,
|
|
271
|
+
* 2026-09-08: 16 of 23 boards dead to the mouse). A tool claims only a press
|
|
272
|
+
* whose DOM target sits in its own container and whose hit node is its own
|
|
273
|
+
* diagram's object, not a namesake from another model.
|
|
274
|
+
*/
|
|
275
|
+
export declare function ownsPress(container: HTMLElement, diagram: {
|
|
276
|
+
getNode(id: string): unknown;
|
|
277
|
+
}, ev: ToolPointerEvent, hit: {
|
|
278
|
+
node?: {
|
|
279
|
+
id: string;
|
|
280
|
+
};
|
|
281
|
+
}): boolean;
|
|
252
282
|
export declare function gripHostOf(target: Element | null): HTMLElement | null;
|
|
253
283
|
/**
|
|
254
284
|
* Did a press land on the drag handle? A press INSIDE the handle element
|
|
@@ -257,6 +287,8 @@ export declare function gripHostOf(target: Element | null): HTMLElement | null;
|
|
|
257
287
|
* included — so the pointer need not hit the header's text to grab the tile.
|
|
258
288
|
*/
|
|
259
289
|
export declare function pressOnDragHandle(sel: string, target: Element | null, hostEl: HTMLElement | null, clientX: number, clientY: number): boolean;
|
|
290
|
+
/** The caption strip of a host with no kit header, px from the card's top. */
|
|
291
|
+
export declare const CAPTION_BAND = 28;
|
|
260
292
|
export interface DashboardGridHandle {
|
|
261
293
|
/** Rebuild the engine from the group's members + their cells, re-project pixels. */
|
|
262
294
|
sync(): void;
|
|
@@ -299,6 +331,14 @@ export interface DashboardGridHandle {
|
|
|
299
331
|
focusWidget(id: string): boolean;
|
|
300
332
|
/** The member the roving tabindex currently rests on. */
|
|
301
333
|
getFocusedWidget(): string | undefined;
|
|
334
|
+
/**
|
|
335
|
+
* SELECT a member without moving keyboard focus — what a mouse press does
|
|
336
|
+
* (the renderer cancels the press's default, so a click never focuses the
|
|
337
|
+
* host). `undefined` clears. False for a non-member.
|
|
338
|
+
*/
|
|
339
|
+
selectWidget(id: string | undefined): boolean;
|
|
340
|
+
/** The selected member: the one wearing the ring and, in grip mode, the grip. */
|
|
341
|
+
getSelectedWidget(): string | undefined;
|
|
302
342
|
/**
|
|
303
343
|
* The layout to PERSIST — from the engine's LARGEST cached column count, so
|
|
304
344
|
* saving while the board is narrow still saves the wide layout the user
|
|
@@ -92,6 +92,25 @@ export function syncGrip(host, cfg, movable) {
|
|
|
92
92
|
host.classList.add(`axdb-gp-${(_d = cfg.placement) !== null && _d !== void 0 ? _d : 'inside'}`, `axdb-gp-${(_e = cfg.position) !== null && _e !== void 0 ? _e : 'left'}`);
|
|
93
93
|
}
|
|
94
94
|
/** The member host a press on a painted grip belongs to (the grip may sit OUTSIDE the host's box). */
|
|
95
|
+
/**
|
|
96
|
+
* Is this press OURS? The renderer's tool registry is PAGE-GLOBAL: every
|
|
97
|
+
* registered tool is asked about every press on every canvas, ties going to
|
|
98
|
+
* the first registered. Two boards on one page sharing widget ids — a designer
|
|
99
|
+
* beside its preview, a page of examples — had the FIRST board's tool claim
|
|
100
|
+
* the second's presses: it selected its own tile and moved nothing (kit lab,
|
|
101
|
+
* 2026-09-08: 16 of 23 boards dead to the mouse). A tool claims only a press
|
|
102
|
+
* whose DOM target sits in its own container and whose hit node is its own
|
|
103
|
+
* diagram's object, not a namesake from another model.
|
|
104
|
+
*/
|
|
105
|
+
export function ownsPress(container, diagram, ev, hit) {
|
|
106
|
+
var _a;
|
|
107
|
+
const t = (_a = ev.source) === null || _a === void 0 ? void 0 : _a.target;
|
|
108
|
+
if (typeof Node !== 'undefined' && t instanceof Node && !container.contains(t))
|
|
109
|
+
return false;
|
|
110
|
+
if (hit.node && diagram.getNode(hit.node.id) !== hit.node)
|
|
111
|
+
return false;
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
95
114
|
export function gripHostOf(target) {
|
|
96
115
|
var _a, _b;
|
|
97
116
|
const grip = (_a = target === null || target === void 0 ? void 0 : target.closest) === null || _a === void 0 ? void 0 : _a.call(target, '.' + GRIP_CLASS);
|
|
@@ -110,13 +129,15 @@ export function pressOnDragHandle(sel, target, hostEl, clientX, clientY) {
|
|
|
110
129
|
return true;
|
|
111
130
|
if (sel !== '.axdb-widget-h' || !hostEl)
|
|
112
131
|
return false;
|
|
113
|
-
const header = hostEl.querySelector('.axdb-widget-h');
|
|
114
|
-
if (!header)
|
|
115
|
-
return false;
|
|
116
132
|
const hr = hostEl.getBoundingClientRect();
|
|
117
|
-
const
|
|
118
|
-
|
|
133
|
+
const header = hostEl.querySelector('.axdb-widget-h');
|
|
134
|
+
// A host painting its own content has no kit header: its caption is the top
|
|
135
|
+
// band of the card, so `dragHandle: true` still means something there.
|
|
136
|
+
const bottom = header ? header.getBoundingClientRect().bottom : hr.top + CAPTION_BAND;
|
|
137
|
+
return clientX >= hr.left && clientX <= hr.right && clientY >= hr.top && clientY <= bottom;
|
|
119
138
|
}
|
|
139
|
+
/** The caption strip of a host with no kit header, px from the card's top. */
|
|
140
|
+
export const CAPTION_BAND = 28;
|
|
120
141
|
/**
|
|
121
142
|
* Binders on the same canvas find each other here. A WeakMap: a canvas that
|
|
122
143
|
* is gone takes its (empty) peer set with it — the strong Map it replaced kept
|
|
@@ -237,6 +258,7 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
237
258
|
const padding = (_c = options.padding) !== null && _c !== void 0 ? _c : gap;
|
|
238
259
|
const baseRowHeight = (_d = options.baseRowHeight) !== null && _d !== void 0 ? _d : 110;
|
|
239
260
|
const minRowHeight = (_e = options.minRowHeight) !== null && _e !== void 0 ? _e : 28;
|
|
261
|
+
const squeeze = options.squeeze !== false;
|
|
240
262
|
let float = (_f = options.float) !== null && _f !== void 0 ? _f : false;
|
|
241
263
|
/** The AUTHORED bound — a nested strip's design (row-first push, escalation). */
|
|
242
264
|
const maxRows = options.maxRows;
|
|
@@ -285,7 +307,10 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
285
307
|
const fitCapacity = () => {
|
|
286
308
|
if (maxRows !== undefined || sizing !== 'fit' || overflow === 'scroll' || designH <= 0)
|
|
287
309
|
return undefined;
|
|
288
|
-
|
|
310
|
+
// Elastic (default): rows at the floor. Frozen (`squeeze: false`): rows at
|
|
311
|
+
// the height they have NOW, so a gesture never shrinks a neighbour.
|
|
312
|
+
const floor = squeeze ? minRowHeight : Math.max(minRowHeight, rowHeightFor(geom(), rows()));
|
|
313
|
+
const rowsThatFit = Math.floor((designH - 2 * padding + gap) / (floor + gap));
|
|
289
314
|
return Math.max(1, rowsThatFit, engine.rows());
|
|
290
315
|
};
|
|
291
316
|
/** Re-derive the capacity; true when the engine must be rebuilt to carry it. */
|
|
@@ -664,6 +689,7 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
664
689
|
syncA11y(only);
|
|
665
690
|
if (disposed)
|
|
666
691
|
return;
|
|
692
|
+
ensureStaticGuard();
|
|
667
693
|
const grip = gripOf(dragHandle);
|
|
668
694
|
for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : []) {
|
|
669
695
|
if (only && !only.has(id))
|
|
@@ -1666,6 +1692,8 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
1666
1692
|
return false;
|
|
1667
1693
|
if (gesture)
|
|
1668
1694
|
return true; // own the rest of an in-flight gesture
|
|
1695
|
+
if (!ownsPress(api.container, diagram, ev, hit))
|
|
1696
|
+
return false;
|
|
1669
1697
|
if (hit.node) {
|
|
1670
1698
|
if (((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(hit.node.id))
|
|
1671
1699
|
return true;
|
|
@@ -1818,8 +1846,10 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
1818
1846
|
}
|
|
1819
1847
|
}
|
|
1820
1848
|
}
|
|
1821
|
-
if (hoverHost && hoverHost !== host)
|
|
1849
|
+
if (hoverHost && hoverHost !== host) {
|
|
1822
1850
|
hoverHost.style.cursor = '';
|
|
1851
|
+
hoverHost.removeAttribute('data-axdb-edge'); // the affordance follows the pointer off a tile
|
|
1852
|
+
}
|
|
1823
1853
|
hoverHost = host;
|
|
1824
1854
|
if (!host)
|
|
1825
1855
|
return;
|
|
@@ -1828,9 +1858,50 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
1828
1858
|
return;
|
|
1829
1859
|
const node = diagram.getNode(id);
|
|
1830
1860
|
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;
|
|
1831
|
-
|
|
1861
|
+
const cursor = resizable ? cursorFor(edgesNear(host, e.clientX, e.clientY)) : '';
|
|
1862
|
+
// The affordance rides on an ATTRIBUTE, not the inline style: a repaint
|
|
1863
|
+
// rewrites the host's style and used to clear the cursor mid-hover, and
|
|
1864
|
+
// content that sets its own cursor (a chart canvas) hid it — the
|
|
1865
|
+
// stylesheet applies the attribute's cursor to the host and everything in it.
|
|
1866
|
+
if (cursor)
|
|
1867
|
+
host.setAttribute('data-axdb-edge', cursor);
|
|
1868
|
+
else
|
|
1869
|
+
host.removeAttribute('data-axdb-edge');
|
|
1832
1870
|
};
|
|
1833
1871
|
api.container.addEventListener('pointermove', onHover, { passive: true });
|
|
1872
|
+
/**
|
|
1873
|
+
* STATIC BOARDS LET CONTENT BE CLICKED. The renderer prevents the default of
|
|
1874
|
+
* every press a tool claims, which cancels the compatibility mouse events —
|
|
1875
|
+
* a chart inside a read-only board could not be clicked. So under `static`
|
|
1876
|
+
* a press inside a member's CONTENT (not on kit chrome) is stopped on the
|
|
1877
|
+
* HTML layer, in the bubble phase: the content has already received it, the
|
|
1878
|
+
* renderer never does, nothing is prevented.
|
|
1879
|
+
*/
|
|
1880
|
+
const staticGuard = (e) => {
|
|
1881
|
+
var _a, _b, _c, _d;
|
|
1882
|
+
if (!isStatic || disposed)
|
|
1883
|
+
return;
|
|
1884
|
+
const t = e.target;
|
|
1885
|
+
const host = (_a = t === null || t === void 0 ? void 0 : t.closest) === null || _a === void 0 ? void 0 : _a.call(t, '.grafloria-node-host');
|
|
1886
|
+
if (!host || !((_b = group.members) !== null && _b !== void 0 ? _b : new Set()).has((_c = host.getAttribute('data-node-id')) !== null && _c !== void 0 ? _c : ''))
|
|
1887
|
+
return;
|
|
1888
|
+
if ((_d = t === null || t === void 0 ? void 0 : t.closest) === null || _d === void 0 ? void 0 : _d.call(t, '.axdb-rs, .axdb-grip, .axdb-div'))
|
|
1889
|
+
return;
|
|
1890
|
+
e.stopPropagation();
|
|
1891
|
+
};
|
|
1892
|
+
let guardedLayer = null;
|
|
1893
|
+
const ensureStaticGuard = () => {
|
|
1894
|
+
// One lookup, ever: the host observer budgets container lookups per
|
|
1895
|
+
// repaint, and the layer element lives as long as the instance.
|
|
1896
|
+
if (guardedLayer === null || guardedLayer === void 0 ? void 0 : guardedLayer.isConnected)
|
|
1897
|
+
return;
|
|
1898
|
+
const layer = htmlLayer();
|
|
1899
|
+
if (!layer)
|
|
1900
|
+
return;
|
|
1901
|
+
guardedLayer === null || guardedLayer === void 0 ? void 0 : guardedLayer.removeEventListener('pointerdown', staticGuard);
|
|
1902
|
+
guardedLayer = layer;
|
|
1903
|
+
layer.addEventListener('pointerdown', staticGuard);
|
|
1904
|
+
};
|
|
1834
1905
|
/**
|
|
1835
1906
|
* KEYBOARD OPERATION (WCAG 2.1.1, and the non-drag alternative 2.5.7 asks
|
|
1836
1907
|
* for): on a focused member, arrows move it one cell, Shift+arrows resize it
|
|
@@ -2214,6 +2285,16 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
2214
2285
|
return true;
|
|
2215
2286
|
},
|
|
2216
2287
|
getFocusedWidget: () => focusedId,
|
|
2288
|
+
selectWidget(id) {
|
|
2289
|
+
var _a;
|
|
2290
|
+
if (disposed)
|
|
2291
|
+
return false;
|
|
2292
|
+
if (id !== undefined && (!((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(id) || !diagram.getNode(id)))
|
|
2293
|
+
return false;
|
|
2294
|
+
selectWidget(id);
|
|
2295
|
+
return true;
|
|
2296
|
+
},
|
|
2297
|
+
getSelectedWidget: () => selectedId,
|
|
2217
2298
|
setStatic(on) {
|
|
2218
2299
|
if (on === isStatic)
|
|
2219
2300
|
return;
|
|
@@ -2349,6 +2430,7 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
2349
2430
|
peersOnCanvas().delete(selfPeer);
|
|
2350
2431
|
unregisterTool();
|
|
2351
2432
|
api.container.removeEventListener('pointermove', onHover);
|
|
2433
|
+
guardedLayer === null || guardedLayer === void 0 ? void 0 : guardedLayer.removeEventListener('pointerdown', staticGuard);
|
|
2352
2434
|
api.container.removeEventListener('focusin', onFocusIn);
|
|
2353
2435
|
api.container.removeEventListener('keydown', onKey);
|
|
2354
2436
|
hostObserver.disconnect();
|
|
@@ -27,7 +27,7 @@ import type { DashboardGridApi, DashboardGridHandle, DashboardGridOptions } from
|
|
|
27
27
|
import { type SplitNode } from './split-layout.js';
|
|
28
28
|
/** Group metadata key the tree persists under. */
|
|
29
29
|
export declare const SPLIT_TREE_KEY = "dashboardTree";
|
|
30
|
-
export interface DashboardSplitOptions extends Pick<DashboardGridOptions, 'columns' | 'gap' | 'padding' | 'rtl' | 'fluid' | 'static' | 'dragHandle' | 'designHeight' | 'baseRowHeight' | 'dragOut' | 'removeZone' | 'onRemoveRequest' | 'onDropIn' | 'onGesture'> {
|
|
30
|
+
export interface DashboardSplitOptions extends Pick<DashboardGridOptions, 'columns' | 'gap' | 'padding' | 'rtl' | 'fluid' | 'static' | 'dragHandle' | 'squeeze' | 'designHeight' | 'baseRowHeight' | 'dragOut' | 'removeZone' | 'onRemoveRequest' | 'onDropIn' | 'onGesture'> {
|
|
31
31
|
/** An authored tree. Default: the persisted one, else derived from the members' cells. */
|
|
32
32
|
tree?: SplitNode | null;
|
|
33
33
|
}
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import { __awaiter } from "tslib";
|
|
26
26
|
import { Command } from '@grafloria/engine';
|
|
27
27
|
import { LiveRegionController, registerTool } from '@grafloria/renderer';
|
|
28
|
-
import { dragHandleSelector, gripHostOf, gripOf, normalizeDragHandle, pressOnDragHandle, syncGrip, DRAG_HANDLE_CLASS } from './grid-binder.js';
|
|
28
|
+
import { dragHandleSelector, gripHostOf, gripOf, normalizeDragHandle, ownsPress, pressOnDragHandle, syncGrip, DRAG_HANDLE_CLASS } from './grid-binder.js';
|
|
29
29
|
import { cellFromGridItem } from './grid-mapping.js';
|
|
30
30
|
import { addSplitLeaf, cellsFromSplit, cloneSplit, dividersOf, groupRectsOf, insertSplitLeaf, moveSplitDivider, normalizeSplit, pathToLeaf, projectSplit, removeSplitLeaf, splitFromCells, splitLeaves, } from './split-layout.js';
|
|
31
31
|
import { ensureDashboardKitStyles } from './styles.js';
|
|
@@ -377,23 +377,48 @@ export function bindDashboardSplit(api, group, options = {}) {
|
|
|
377
377
|
selectedId = id;
|
|
378
378
|
syncA11y();
|
|
379
379
|
};
|
|
380
|
-
|
|
380
|
+
// Static boards let content be clicked — see grid-binder's staticGuard.
|
|
381
|
+
const staticGuard = (e) => {
|
|
381
382
|
var _a, _b, _c, _d;
|
|
383
|
+
if (!isStatic || disposed)
|
|
384
|
+
return;
|
|
385
|
+
const t = e.target;
|
|
386
|
+
const host = (_a = t === null || t === void 0 ? void 0 : t.closest) === null || _a === void 0 ? void 0 : _a.call(t, '.grafloria-node-host');
|
|
387
|
+
if (!host || !((_b = group.members) !== null && _b !== void 0 ? _b : new Set()).has((_c = host.getAttribute('data-node-id')) !== null && _c !== void 0 ? _c : ''))
|
|
388
|
+
return;
|
|
389
|
+
if ((_d = t === null || t === void 0 ? void 0 : t.closest) === null || _d === void 0 ? void 0 : _d.call(t, '.axdb-rs, .axdb-grip, .axdb-div'))
|
|
390
|
+
return;
|
|
391
|
+
e.stopPropagation();
|
|
392
|
+
};
|
|
393
|
+
let guardedLayer = null;
|
|
394
|
+
const ensureStaticGuard = () => {
|
|
395
|
+
if (guardedLayer === null || guardedLayer === void 0 ? void 0 : guardedLayer.isConnected)
|
|
396
|
+
return;
|
|
397
|
+
const layer = api.container.querySelector('.grafloria-html-layer');
|
|
398
|
+
if (!layer)
|
|
399
|
+
return;
|
|
400
|
+
guardedLayer === null || guardedLayer === void 0 ? void 0 : guardedLayer.removeEventListener('pointerdown', staticGuard);
|
|
401
|
+
guardedLayer = layer;
|
|
402
|
+
layer.addEventListener('pointerdown', staticGuard);
|
|
403
|
+
};
|
|
404
|
+
const syncA11y = () => {
|
|
405
|
+
var _a, _b, _c, _d, _e;
|
|
382
406
|
if (disposed)
|
|
383
407
|
return;
|
|
408
|
+
ensureStaticGuard();
|
|
384
409
|
const order = splitLeaves(paintedTree()).filter((id) => !!diagram.getNode(id));
|
|
385
410
|
if (selectedId && !order.includes(selectedId))
|
|
386
411
|
selectedId = undefined;
|
|
387
412
|
// No corner handles on a split board: size comes from the dividers. A host
|
|
388
413
|
// that carried the grid's handle (a board switched live) sheds it here.
|
|
389
|
-
for (const id of
|
|
390
|
-
(
|
|
414
|
+
for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : [])
|
|
415
|
+
(_c = (_b = hostOf(id)) === null || _b === void 0 ? void 0 : _b.querySelector(':scope > .axdb-rs')) === null || _c === void 0 ? void 0 : _c.remove();
|
|
391
416
|
// The painted grip (or none), on every leaf host.
|
|
392
417
|
for (const id of order) {
|
|
393
418
|
const host = hostOf(id);
|
|
394
419
|
const node = diagram.getNode(id);
|
|
395
420
|
if (host && node)
|
|
396
|
-
syncGrip(host, gripOf(dragHandle), ((
|
|
421
|
+
syncGrip(host, gripOf(dragHandle), ((_d = node.state) === null || _d === void 0 ? void 0 : _d.locked) !== true && !isStatic && ((_e = node.getMetadata) === null || _e === void 0 ? void 0 : _e.call(node, 'widgetMovable')) !== false);
|
|
397
422
|
}
|
|
398
423
|
if (focusedId && !order.includes(focusedId))
|
|
399
424
|
focusedId = undefined;
|
|
@@ -606,6 +631,8 @@ export function bindDashboardSplit(api, group, options = {}) {
|
|
|
606
631
|
return false;
|
|
607
632
|
if (gesture)
|
|
608
633
|
return true;
|
|
634
|
+
if (!ownsPress(api.container, diagram, ev, hit))
|
|
635
|
+
return false;
|
|
609
636
|
if (hit.node)
|
|
610
637
|
return ((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(hit.node.id);
|
|
611
638
|
return worldInsideBoard(ev.world.x, ev.world.y);
|
|
@@ -1000,6 +1027,16 @@ export function bindDashboardSplit(api, group, options = {}) {
|
|
|
1000
1027
|
return true;
|
|
1001
1028
|
},
|
|
1002
1029
|
getFocusedWidget: () => focusedId,
|
|
1030
|
+
selectWidget(id) {
|
|
1031
|
+
var _a;
|
|
1032
|
+
if (disposed)
|
|
1033
|
+
return false;
|
|
1034
|
+
if (id !== undefined && (!((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(id) || !diagram.getNode(id)))
|
|
1035
|
+
return false;
|
|
1036
|
+
selectWidget(id);
|
|
1037
|
+
return true;
|
|
1038
|
+
},
|
|
1039
|
+
getSelectedWidget: () => selectedId,
|
|
1003
1040
|
saveLayout() {
|
|
1004
1041
|
return { columns, cells: cellsFromSplit(readTree(), columns, rowsGuess()) };
|
|
1005
1042
|
},
|
|
@@ -1103,6 +1140,7 @@ export function bindDashboardSplit(api, group, options = {}) {
|
|
|
1103
1140
|
});
|
|
1104
1141
|
},
|
|
1105
1142
|
dispose() {
|
|
1143
|
+
guardedLayer === null || guardedLayer === void 0 ? void 0 : guardedLayer.removeEventListener('pointerdown', staticGuard);
|
|
1106
1144
|
if (disposed)
|
|
1107
1145
|
return;
|
|
1108
1146
|
cancelActiveGesture();
|
|
@@ -35,7 +35,7 @@ const CSS = `
|
|
|
35
35
|
|
|
36
36
|
/* ===== keyboard focus: the roving tab stop shows where it is (WCAG 2.4.7) ===== */
|
|
37
37
|
.grafloria-html-layer > .grafloria-node-host:focus-visible {
|
|
38
|
-
outline: 2px solid #3b52d9;
|
|
38
|
+
outline: 2px solid var(--axdb-accent, #3b52d9);
|
|
39
39
|
outline-offset: 2px;
|
|
40
40
|
border-radius: var(--axdb-rs-radius, 3px);
|
|
41
41
|
}
|
|
@@ -203,9 +203,16 @@ const CSS = `
|
|
|
203
203
|
content: ''; position: absolute; left: 5px; top: 2px; width: 12px; height: 6px;
|
|
204
204
|
background: radial-gradient(circle, currentColor 1px, transparent 1.4px) 0 0 / 4px 3px;
|
|
205
205
|
}
|
|
206
|
-
.grafloria-node-host > .axdb-grip:hover { border-color: #3b52d9; color: #3b52d9; }
|
|
207
|
-
/* The selected card says so, quietly.
|
|
208
|
-
|
|
206
|
+
.grafloria-node-host > .axdb-grip:hover { border-color: var(--axdb-accent, #3b52d9); color: var(--axdb-accent, #3b52d9); }
|
|
207
|
+
/* The selected card says so, quietly. --axdb-accent themes the grip's hover, the
|
|
208
|
+
ring and the focus outline together; --axdb-accent-ring the ring alone. */
|
|
209
|
+
.grafloria-node-host.axdb-selected > .axdb-widget { box-shadow: 0 0 0 1.5px var(--axdb-accent-ring, rgba(59, 82, 217, .55)), 0 1px 2px rgba(16, 24, 40, .05), 0 1px 3px rgba(16, 24, 40, .05); }
|
|
210
|
+
/* While a resize edge is near, the host and everything in it (a chart canvas
|
|
211
|
+
with its own cursor included) show the edge's cursor. */
|
|
212
|
+
.grafloria-node-host[data-axdb-edge="ns-resize"], .grafloria-node-host[data-axdb-edge="ns-resize"] * { cursor: ns-resize !important; }
|
|
213
|
+
.grafloria-node-host[data-axdb-edge="ew-resize"], .grafloria-node-host[data-axdb-edge="ew-resize"] * { cursor: ew-resize !important; }
|
|
214
|
+
.grafloria-node-host[data-axdb-edge="nwse-resize"], .grafloria-node-host[data-axdb-edge="nwse-resize"] * { cursor: nwse-resize !important; }
|
|
215
|
+
.grafloria-node-host[data-axdb-edge="nesw-resize"], .grafloria-node-host[data-axdb-edge="nesw-resize"] * { cursor: nesw-resize !important; }
|
|
209
216
|
/* INSIDE: centred on the header's text line and flush with the card padding,
|
|
210
217
|
per size tier (padding 13/15, then 8/14, 4/12, 2/12). */
|
|
211
218
|
.grafloria-node-host > .axdb-grip--inside { top: 14px; }
|
|
@@ -237,6 +244,11 @@ const CSS = `
|
|
|
237
244
|
.grafloria-node-host.axdb-gp-inside.axdb-gp-right .axdb-widget > .axdb-widget-h { padding-right: 32px; }
|
|
238
245
|
.grafloria-node-host.axdb-gp-inside.axdb-gp-center .axdb-widget > .axdb-widget-h { padding-top: 12px; }
|
|
239
246
|
.axdb-widget-b { flex: 1; min-height: 0; position: relative; }
|
|
247
|
+
/* A drag across a STATIC board (nothing prevents the press's default there)
|
|
248
|
+
used to select every label on it; kit cards are not prose. Tables stay
|
|
249
|
+
copyable — a figure in a grid is the one thing a viewer selects. */
|
|
250
|
+
.axdb-widget { user-select: none; -webkit-user-select: none; }
|
|
251
|
+
.axdb-widget .axdb-table { user-select: text; -webkit-user-select: text; }
|
|
240
252
|
.axdb-widget-b > svg { display: block; width: 100%; height: 100%; }
|
|
241
253
|
.axdb-widget-b.axdb-scroll { overflow: auto; }
|
|
242
254
|
/* A chart WITH a legend under it: the plot yields height, the legend keeps its
|
|
@@ -325,6 +337,9 @@ const CSS = `
|
|
|
325
337
|
tile gets a bigger ring, not dead card), square, capped so its centre figure
|
|
326
338
|
stays a figure and not a headline. */
|
|
327
339
|
.axdb-widget-b.axdb-donut { display: flex; align-items: center; gap: 10px; }
|
|
340
|
+
/* A legend taller than a SHORT body (a 2-row donut on a squeezed board) is
|
|
341
|
+
clipped at its own foot, not centred over the title above it. */
|
|
342
|
+
.axdb-widget-b.axdb-donut > .axdb-lg--col { max-height: 100%; min-height: 0; overflow: hidden; }
|
|
328
343
|
.axdb-widget-b.axdb-donut > svg {
|
|
329
344
|
flex: 0 0 auto; width: auto; height: 100%; max-height: 260px; max-width: 60%; aspect-ratio: 1 / 1;
|
|
330
345
|
}
|