@grafloria/element 0.4.53 → 0.4.55
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/board-ctx.d.ts +62 -0
- package/src/lib/dashboard-kit/board-ctx.js +2 -0
- package/src/lib/dashboard-kit/chrome.d.ts +60 -0
- package/src/lib/dashboard-kit/chrome.js +461 -0
- package/src/lib/dashboard-kit/dashboard.d.ts +11 -3
- package/src/lib/dashboard-kit/dashboard.js +11 -6
- package/src/lib/dashboard-kit/edges.d.ts +15 -0
- package/src/lib/dashboard-kit/edges.js +30 -0
- package/src/lib/dashboard-kit/grid-binder.d.ts +4 -43
- package/src/lib/dashboard-kit/grid-binder.js +101 -883
- package/src/lib/dashboard-kit/grip.d.ts +37 -0
- package/src/lib/dashboard-kit/grip.js +67 -0
- package/src/lib/dashboard-kit/keyboard.d.ts +41 -0
- package/src/lib/dashboard-kit/keyboard.js +145 -0
- package/src/lib/dashboard-kit/project.d.ts +30 -0
- package/src/lib/dashboard-kit/project.js +177 -0
package/package.json
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHAT A BOARD'S MODULES SEE OF THE BINDER (tile first, step 4b-i).
|
|
3
|
+
*
|
|
4
|
+
* `bindDashboardGrid` is one closure: the engine, the group, the options and
|
|
5
|
+
* a few dozen live values. The modules that left it — the projection, the
|
|
6
|
+
* chrome, the keyboard — read that state through this context instead of
|
|
7
|
+
* sharing the closure. Everything that can change during the binder's life
|
|
8
|
+
* is a call (`engine()`, `frame()`, `isStatic()`), never a copied value, so a
|
|
9
|
+
* module sees the engine the binder holds NOW, not the one it was created
|
|
10
|
+
* with (the binder rebuilds its engine on every sync).
|
|
11
|
+
*/
|
|
12
|
+
import type { DiagramModel, GridPackEngine, GroupModel, NodeModel } from '@grafloria/engine';
|
|
13
|
+
import type { DashboardGridGeometry, WorldRect } from './grid-mapping.js';
|
|
14
|
+
import type { DashboardGridApi, DashboardGridOptions } from './grid-binder.js';
|
|
15
|
+
export interface BoardCtx {
|
|
16
|
+
readonly api: DashboardGridApi;
|
|
17
|
+
readonly group: GroupModel;
|
|
18
|
+
readonly diagram: DiagramModel;
|
|
19
|
+
readonly options: DashboardGridOptions;
|
|
20
|
+
readonly gap: number;
|
|
21
|
+
readonly padding: number;
|
|
22
|
+
readonly baseRowHeight: number;
|
|
23
|
+
readonly minRowHeight: number;
|
|
24
|
+
readonly overflow: 'bounded' | 'scroll';
|
|
25
|
+
/** The engine the binder holds now (it is rebuilt on every sync). */
|
|
26
|
+
engine(): GridPackEngine;
|
|
27
|
+
frame(): WorldRect;
|
|
28
|
+
geom(): DashboardGridGeometry;
|
|
29
|
+
rows(): number;
|
|
30
|
+
sizing(): 'fit' | 'grow';
|
|
31
|
+
/** The board's design height in px, 0 when it has none. */
|
|
32
|
+
designH(): number;
|
|
33
|
+
rtl(): boolean;
|
|
34
|
+
isStatic(): boolean;
|
|
35
|
+
disposed(): boolean;
|
|
36
|
+
htmlLayer(): HTMLElement | null;
|
|
37
|
+
hostOf(id: string): HTMLElement | null;
|
|
38
|
+
memberEntity(id: string): NodeModel | GroupModel | undefined;
|
|
39
|
+
sizeOf(e: {
|
|
40
|
+
size?: {
|
|
41
|
+
width: number;
|
|
42
|
+
height: number;
|
|
43
|
+
depth?: number;
|
|
44
|
+
};
|
|
45
|
+
}): {
|
|
46
|
+
width: number;
|
|
47
|
+
height: number;
|
|
48
|
+
depth?: number;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* The tile the projection leaves alone — this board's own ghost while its
|
|
52
|
+
* gesture runs, or a ghost another board's gesture placed here through its
|
|
53
|
+
* leg — and the tile the placeholder is drawn for. Null when none.
|
|
54
|
+
*/
|
|
55
|
+
ghostId(): string | null;
|
|
56
|
+
/**
|
|
57
|
+
* A system write of DERIVED state (a projected rect, the board's own
|
|
58
|
+
* height). The binder's `writing` flag guards its bounds handler while it
|
|
59
|
+
* runs, so a frame the projection wrote is not re-projected as a change.
|
|
60
|
+
*/
|
|
61
|
+
write(fn: () => void): void;
|
|
62
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE CHROME (tile first, step 4b-i): everything a board paints that is not a
|
|
3
|
+
* widget — section slabs and their selection ring, a tab container's frame and
|
|
4
|
+
* tinted surface, caption bands, the carried subtree of a moving group, the
|
|
5
|
+
* corner resize handles and the painted grips on member hosts, the edge
|
|
6
|
+
* cursors, the refused cell of a section move, and the static guard that lets
|
|
7
|
+
* content be clicked on a read-only board. Moved out of the binder unchanged
|
|
8
|
+
* in behaviour; the binder keeps calling these by the same names.
|
|
9
|
+
*/
|
|
10
|
+
import type { GroupModel } from '@grafloria/engine';
|
|
11
|
+
import { type ResizeEdges } from './edges.js';
|
|
12
|
+
import { type DragHandleOption } from './grip.js';
|
|
13
|
+
import type { BoardCtx } from './board-ctx.js';
|
|
14
|
+
/**
|
|
15
|
+
* A TAB CONTAINER's frame is its drag handle (0.4.43): its 8-px margin —
|
|
16
|
+
* under the strip, beside the pages — moves the group, so the edge-resize
|
|
17
|
+
* zone shrinks to 3 px there (the corner handle still resizes). A section's
|
|
18
|
+
* edges keep the full grip: its empty band is a drop target, not a handle.
|
|
19
|
+
*/
|
|
20
|
+
export declare const TAB_FRAME_GRIP = 3;
|
|
21
|
+
export declare const isTabsGroup: (grp: GroupModel) => boolean;
|
|
22
|
+
export declare const edgeGripFor: (grp: GroupModel) => number;
|
|
23
|
+
export interface ChromeDeps {
|
|
24
|
+
/** The selected member (a widget or a section), for the slab's ring. */
|
|
25
|
+
selectedId(): string | undefined;
|
|
26
|
+
/** The accessible chrome on every member host — the binder's, since it owns the focus and the selection. */
|
|
27
|
+
syncA11y(only?: ReadonlySet<string>): void;
|
|
28
|
+
/** A section is being moved by hand right now (the refusal cell's cursor, the hover cursor). */
|
|
29
|
+
grabbing(): boolean;
|
|
30
|
+
/** A tile gesture is live (the hover affordance stays out of its way). */
|
|
31
|
+
gestureRunning(): boolean;
|
|
32
|
+
/** The member SECTION whose frame holds the world point, if any. */
|
|
33
|
+
memberGroupAt(x: number, y: number): string | null;
|
|
34
|
+
/** Which of a section frame's edges a world point is within its grip of. */
|
|
35
|
+
slabEdgesNear(grp: GroupModel, x: number, y: number): ResizeEdges;
|
|
36
|
+
dragHandle(): DragHandleOption;
|
|
37
|
+
wantHandles: boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface Chrome {
|
|
40
|
+
/** The section overlays: projected chrome like the placeholder, following every frame write. */
|
|
41
|
+
syncSlabs(): void;
|
|
42
|
+
/** The chrome on every member host: the painted grip (or none) and the corner resize handle. */
|
|
43
|
+
syncHandles(only?: ReadonlySet<string>): void;
|
|
44
|
+
/** Start answering host churn (a mount, a repaint that wiped the handle) with a re-sync of those hosts. */
|
|
45
|
+
observe(layer: HTMLElement): void;
|
|
46
|
+
ensureStaticGuard(): void;
|
|
47
|
+
/** The pointer over the container: edge cursors on hosts, section-edge cursors, `show: 'hover'` captions. */
|
|
48
|
+
onHover(e: PointerEvent): void;
|
|
49
|
+
onHoverLeave(): void;
|
|
50
|
+
/** Everything in a group's subtree — tiles, strips, slabs, the surface — transition-exempt while it is carried. */
|
|
51
|
+
setCarried(id: string, on: boolean): void;
|
|
52
|
+
flushCarried(): void;
|
|
53
|
+
/** The cell a slab move asked for and could not have — painted so the refusal is visible; null clears it. */
|
|
54
|
+
showRefusal(cell: {
|
|
55
|
+
x: number;
|
|
56
|
+
y: number;
|
|
57
|
+
} | null, w: number, h: number): void;
|
|
58
|
+
dispose(): void;
|
|
59
|
+
}
|
|
60
|
+
export declare function createChrome(ctx: BoardCtx, deps: ChromeDeps): Chrome;
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import { cellToRect } from './grid-mapping.js';
|
|
2
|
+
import { captionKey, captionOfGroup, captionPainted, paintCaptionBand, sizeCaptionBand } from './caption.js';
|
|
3
|
+
import { cursorFor, edgesNear, EDGE_GRIP } from './edges.js';
|
|
4
|
+
import { gripOf, syncGrip } from './grip.js';
|
|
5
|
+
/**
|
|
6
|
+
* A TAB CONTAINER's frame is its drag handle (0.4.43): its 8-px margin —
|
|
7
|
+
* under the strip, beside the pages — moves the group, so the edge-resize
|
|
8
|
+
* zone shrinks to 3 px there (the corner handle still resizes). A section's
|
|
9
|
+
* edges keep the full grip: its empty band is a drop target, not a handle.
|
|
10
|
+
*/
|
|
11
|
+
export const TAB_FRAME_GRIP = 3;
|
|
12
|
+
export const isTabsGroup = (grp) => { var _a; return ((_a = grp.getMetadata('containerWidget')) === null || _a === void 0 ? void 0 : _a.layout) === 'tabs'; };
|
|
13
|
+
export const edgeGripFor = (grp) => (isTabsGroup(grp) ? TAB_FRAME_GRIP : EDGE_GRIP);
|
|
14
|
+
export function createChrome(ctx, deps) {
|
|
15
|
+
const { api, group, diagram, options } = ctx;
|
|
16
|
+
// -- section slabs, group frames, caption bands ----------------------------
|
|
17
|
+
/**
|
|
18
|
+
* SECTION CHROME. A section (member group) paints no card of its own, so it
|
|
19
|
+
* had nothing to press: a click on its empty band cleared the selection and
|
|
20
|
+
* its frame had no handle and no edge — a section with many children could
|
|
21
|
+
* not be selected at all, and could only be resized by pulling a child
|
|
22
|
+
* (Quantia, Groups page). Every section gets a pointer-transparent overlay
|
|
23
|
+
* in the HTML layer that wears the selection ring and, while selected, the
|
|
24
|
+
* corner handle; its frame edges answer the resize cursor.
|
|
25
|
+
*/
|
|
26
|
+
const slabEls = new Map();
|
|
27
|
+
/**
|
|
28
|
+
* GROUP FRAME (0.4.43): a TAB CONTAINER wears a frame by default — its slab
|
|
29
|
+
* is bordered and a tinted surface lies under its pages, first in the layer
|
|
30
|
+
* so the tiles paint over it. A page torn out with two widgets under its tab
|
|
31
|
+
* read as a strip floating over two loose cards: nothing said the second
|
|
32
|
+
* card was the tab's. A plain section keeps its invisible slab.
|
|
33
|
+
*/
|
|
34
|
+
const groupBgs = new Map();
|
|
35
|
+
const syncGroupBg = (layer, id, on, x, y, w, h) => {
|
|
36
|
+
var _a;
|
|
37
|
+
let bg = (_a = groupBgs.get(id)) !== null && _a !== void 0 ? _a : null;
|
|
38
|
+
if (!on) {
|
|
39
|
+
bg === null || bg === void 0 ? void 0 : bg.remove();
|
|
40
|
+
groupBgs.delete(id);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (!bg || bg.parentElement !== layer) {
|
|
44
|
+
bg === null || bg === void 0 ? void 0 : bg.remove();
|
|
45
|
+
bg = document.createElement('div');
|
|
46
|
+
bg.className = 'axdb-group-bg';
|
|
47
|
+
bg.setAttribute('data-group-bg', id);
|
|
48
|
+
layer.prepend(bg);
|
|
49
|
+
groupBgs.set(id, bg);
|
|
50
|
+
}
|
|
51
|
+
bg.style.left = `${x}px`;
|
|
52
|
+
bg.style.top = `${y}px`;
|
|
53
|
+
bg.style.width = `${w}px`;
|
|
54
|
+
bg.style.height = `${h}px`;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* A `show: 'hover'` caption cannot ride CSS `:hover`: the slab overlay takes
|
|
58
|
+
* no pointer (by design — it must never steal a press), and while the band
|
|
59
|
+
* is hidden it takes none either, so nothing in the section is ever hovered
|
|
60
|
+
* in CSS terms. The binder already tracks the pointer; it marks the section
|
|
61
|
+
* under it instead.
|
|
62
|
+
*/
|
|
63
|
+
/** Only the sections carrying a `show: 'hover'` band — usually none, so the
|
|
64
|
+
* pointer handler costs nothing on a board that has no hover caption. */
|
|
65
|
+
const hoverSlabs = new Set();
|
|
66
|
+
const markHotSection = (clientX, clientY) => {
|
|
67
|
+
if (!hoverSlabs.size)
|
|
68
|
+
return;
|
|
69
|
+
for (const el of hoverSlabs) {
|
|
70
|
+
const r = el.getBoundingClientRect();
|
|
71
|
+
el.classList.toggle('axdb-slab--hot', clientX >= r.left && clientX <= r.right && clientY >= r.top && clientY <= r.bottom);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const onHoverLeave = () => {
|
|
75
|
+
for (const el of hoverSlabs)
|
|
76
|
+
el.classList.remove('axdb-slab--hot');
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* THE CAPTION BAND of a section, on its slab overlay. Painted from the
|
|
80
|
+
* group's persisted caption; repainted only when its identity changes (the
|
|
81
|
+
* options, RTL, static, the tier) so a custom `renderCaption` is not run
|
|
82
|
+
* per frame. The band takes the pointer (the slab itself does not): a press
|
|
83
|
+
* on it selects the section, an action fires, pass-through reaches content.
|
|
84
|
+
*/
|
|
85
|
+
const syncCaption = (el, id, grp, sectionH) => {
|
|
86
|
+
const cap = captionOfGroup(grp);
|
|
87
|
+
const isStatic = ctx.isStatic();
|
|
88
|
+
let band = el.querySelector(':scope > .axdb-slab-h');
|
|
89
|
+
if (!cap || !captionPainted(cap, isStatic)) {
|
|
90
|
+
band === null || band === void 0 ? void 0 : band.remove();
|
|
91
|
+
hoverSlabs.delete(el);
|
|
92
|
+
el.classList.remove('axdb-slab--hot');
|
|
93
|
+
el.removeAttribute('aria-label');
|
|
94
|
+
el.removeAttribute('role');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (cap.show === 'hover')
|
|
98
|
+
hoverSlabs.add(el);
|
|
99
|
+
else {
|
|
100
|
+
hoverSlabs.delete(el);
|
|
101
|
+
el.classList.remove('axdb-slab--hot');
|
|
102
|
+
}
|
|
103
|
+
const cctx = { rtl: ctx.rtl(), static: isStatic, sectionH };
|
|
104
|
+
const key = captionKey(cap, cctx);
|
|
105
|
+
if (band && band.getAttribute('data-key') === key) {
|
|
106
|
+
sizeCaptionBand(band, cap, sectionH); // the tier follows the live size
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
band === null || band === void 0 ? void 0 : band.remove();
|
|
110
|
+
band = document.createElement('div');
|
|
111
|
+
el.prepend(band);
|
|
112
|
+
const render = options.renderCaption;
|
|
113
|
+
paintCaptionBand(band, cap, Object.assign(Object.assign(Object.assign({}, cctx), (render ? { render: (host) => render(id, host) } : {})), { onAction: (actionId) => { var _a; return (_a = options.onCaptionAction) === null || _a === void 0 ? void 0 : _a.call(options, id, actionId); } }));
|
|
114
|
+
band.setAttribute('data-key', key);
|
|
115
|
+
// A named group, so the caption text is the section's name in the
|
|
116
|
+
// accessibility tree rather than a stray label on an unnamed div. (The
|
|
117
|
+
// band is not a tab stop yet — the actions are deliberately out of the
|
|
118
|
+
// tab order so a board keeps exactly ONE stop; see the plan.)
|
|
119
|
+
if (cap.text) {
|
|
120
|
+
el.setAttribute('role', 'group');
|
|
121
|
+
el.setAttribute('aria-label', cap.text);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
el.removeAttribute('role');
|
|
125
|
+
el.removeAttribute('aria-label');
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
let slabLayer = null;
|
|
129
|
+
const syncSlabs = () => {
|
|
130
|
+
var _a, _b, _c;
|
|
131
|
+
if (ctx.disposed())
|
|
132
|
+
return;
|
|
133
|
+
const layer = (slabLayer === null || slabLayer === void 0 ? void 0 : slabLayer.isConnected) ? slabLayer : (slabLayer = ctx.htmlLayer());
|
|
134
|
+
if (!layer)
|
|
135
|
+
return;
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
const selectedId = deps.selectedId();
|
|
138
|
+
const isStatic = ctx.isStatic();
|
|
139
|
+
const rtl = ctx.rtl();
|
|
140
|
+
for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : []) {
|
|
141
|
+
const grp = diagram.getGroup(id);
|
|
142
|
+
if (!grp || diagram.getNode(id))
|
|
143
|
+
continue;
|
|
144
|
+
seen.add(id);
|
|
145
|
+
let el = slabEls.get(id);
|
|
146
|
+
if (!el || el.parentElement !== layer) {
|
|
147
|
+
el === null || el === void 0 ? void 0 : el.remove();
|
|
148
|
+
el = document.createElement('div');
|
|
149
|
+
el.className = 'axdb-slab';
|
|
150
|
+
el.setAttribute('data-slab-id', id);
|
|
151
|
+
const rs = document.createElement('div');
|
|
152
|
+
rs.className = 'axdb-rs';
|
|
153
|
+
rs.setAttribute('title', 'Resize section');
|
|
154
|
+
el.appendChild(rs);
|
|
155
|
+
layer.appendChild(el);
|
|
156
|
+
slabEls.set(id, el);
|
|
157
|
+
}
|
|
158
|
+
const p = grp.position;
|
|
159
|
+
const sz = ctx.sizeOf(grp);
|
|
160
|
+
el.style.left = `${p.x}px`;
|
|
161
|
+
el.style.top = `${p.y}px`;
|
|
162
|
+
el.style.width = `${sz.width}px`;
|
|
163
|
+
el.style.height = `${sz.height}px`;
|
|
164
|
+
el.classList.toggle('axdb-slab--selected', selectedId === id);
|
|
165
|
+
el.classList.toggle('axdb-slab--static', isStatic);
|
|
166
|
+
(_b = el.querySelector(':scope > .axdb-rs')) === null || _b === void 0 ? void 0 : _b.classList.toggle('axdb-rs--rtl', rtl);
|
|
167
|
+
const tabs = isTabsGroup(grp);
|
|
168
|
+
el.classList.toggle('axdb-slab--tabs', tabs);
|
|
169
|
+
syncGroupBg(layer, id, tabs, p.x, p.y, sz.width, sz.height);
|
|
170
|
+
syncCaption(el, id, grp, sz.height);
|
|
171
|
+
}
|
|
172
|
+
for (const [id, el] of slabEls) {
|
|
173
|
+
if (!seen.has(id)) {
|
|
174
|
+
el.remove();
|
|
175
|
+
hoverSlabs.delete(el);
|
|
176
|
+
slabEls.delete(id);
|
|
177
|
+
(_c = groupBgs.get(id)) === null || _c === void 0 ? void 0 : _c.remove();
|
|
178
|
+
groupBgs.delete(id);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
// -- the carried subtree ---------------------------------------------------
|
|
183
|
+
/**
|
|
184
|
+
* CARRIED (0.4.43): a group dragged by its strip or band moves as ONE thing.
|
|
185
|
+
* The held TILE is transition-exempt (the ghost), but a group has no host of
|
|
186
|
+
* its own: its strip jumped to the pointer while its pages' tiles GLIDED
|
|
187
|
+
* after it — on the live demo the content trailed the strip by up to 140 px
|
|
188
|
+
* at every step. Everything in the group's subtree — tiles, strips, slabs,
|
|
189
|
+
* the surface — is exempt for the gesture and, like the ghost, through the
|
|
190
|
+
* drop write; then the glides resume.
|
|
191
|
+
*/
|
|
192
|
+
const carriedEls = new Set();
|
|
193
|
+
let carriedTimer = null;
|
|
194
|
+
const subtreeIds = (id) => {
|
|
195
|
+
var _a;
|
|
196
|
+
const groups = [];
|
|
197
|
+
const nodes = [];
|
|
198
|
+
const queue = [id];
|
|
199
|
+
const seen = new Set();
|
|
200
|
+
while (queue.length) {
|
|
201
|
+
const cur = queue.shift();
|
|
202
|
+
if (seen.has(cur))
|
|
203
|
+
continue;
|
|
204
|
+
seen.add(cur);
|
|
205
|
+
const grp = diagram.getGroup(cur);
|
|
206
|
+
if (grp) {
|
|
207
|
+
groups.push(cur);
|
|
208
|
+
for (const m of (_a = grp.members) !== null && _a !== void 0 ? _a : [])
|
|
209
|
+
queue.push(m);
|
|
210
|
+
}
|
|
211
|
+
else if (diagram.getNode(cur))
|
|
212
|
+
nodes.push(cur);
|
|
213
|
+
}
|
|
214
|
+
return { groups, nodes };
|
|
215
|
+
};
|
|
216
|
+
const cssId = (id) => (typeof CSS !== 'undefined' && CSS.escape ? CSS.escape(id) : id.replace(/"/g, '\\"'));
|
|
217
|
+
const setCarried = (id, on) => {
|
|
218
|
+
const layer = ctx.htmlLayer();
|
|
219
|
+
if (!layer)
|
|
220
|
+
return;
|
|
221
|
+
if (carriedTimer) {
|
|
222
|
+
clearTimeout(carriedTimer);
|
|
223
|
+
carriedTimer = null;
|
|
224
|
+
}
|
|
225
|
+
if (on) {
|
|
226
|
+
const { groups, nodes } = subtreeIds(id);
|
|
227
|
+
const els = [];
|
|
228
|
+
for (const n of nodes) {
|
|
229
|
+
const h = ctx.hostOf(n);
|
|
230
|
+
if (h)
|
|
231
|
+
els.push(h);
|
|
232
|
+
}
|
|
233
|
+
for (const g of groups) {
|
|
234
|
+
els.push(...Array.from(layer.querySelectorAll(`:scope > .axdb-tabs[data-tabs-id="${cssId(g)}"], :scope > .axdb-slab[data-slab-id="${cssId(g)}"], :scope > .axdb-group-bg[data-group-bg="${cssId(g)}"]`)));
|
|
235
|
+
}
|
|
236
|
+
for (const el of els) {
|
|
237
|
+
el.classList.add('axdb-carried');
|
|
238
|
+
carriedEls.add(el);
|
|
239
|
+
}
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
carriedTimer = setTimeout(() => {
|
|
243
|
+
for (const el of carriedEls)
|
|
244
|
+
el.classList.remove('axdb-carried');
|
|
245
|
+
carriedEls.clear();
|
|
246
|
+
carriedTimer = null;
|
|
247
|
+
}, 60);
|
|
248
|
+
};
|
|
249
|
+
const flushCarried = () => {
|
|
250
|
+
if (carriedTimer)
|
|
251
|
+
clearTimeout(carriedTimer);
|
|
252
|
+
carriedTimer = null;
|
|
253
|
+
for (const el of carriedEls)
|
|
254
|
+
el.classList.remove('axdb-carried');
|
|
255
|
+
carriedEls.clear();
|
|
256
|
+
};
|
|
257
|
+
// -- the refused cell ------------------------------------------------------
|
|
258
|
+
let refusal = null;
|
|
259
|
+
const showRefusal = (cell, w, h) => {
|
|
260
|
+
const layer = ctx.htmlLayer();
|
|
261
|
+
if (!cell || !layer) {
|
|
262
|
+
refusal === null || refusal === void 0 ? void 0 : refusal.remove();
|
|
263
|
+
refusal = null;
|
|
264
|
+
api.container.style.cursor = deps.grabbing() ? 'grabbing' : '';
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (!refusal || refusal.parentElement !== layer) {
|
|
268
|
+
refusal === null || refusal === void 0 ? void 0 : refusal.remove();
|
|
269
|
+
refusal = document.createElement('div');
|
|
270
|
+
refusal.className = 'axdb-ph axdb-ph--no';
|
|
271
|
+
layer.prepend(refusal);
|
|
272
|
+
}
|
|
273
|
+
const r = cellToRect({ x: cell.x, y: cell.y, w, h }, ctx.frame(), ctx.geom(), ctx.rows());
|
|
274
|
+
refusal.style.left = `${r.x}px`;
|
|
275
|
+
refusal.style.top = `${r.y}px`;
|
|
276
|
+
refusal.style.width = `${r.width}px`;
|
|
277
|
+
refusal.style.height = `${r.height}px`;
|
|
278
|
+
api.container.style.cursor = 'not-allowed';
|
|
279
|
+
};
|
|
280
|
+
// -- the static guard ------------------------------------------------------
|
|
281
|
+
/**
|
|
282
|
+
* STATIC BOARDS LET CONTENT BE CLICKED. The renderer prevents the default of
|
|
283
|
+
* every press a tool claims, which cancels the compatibility mouse events —
|
|
284
|
+
* a chart inside a read-only board could not be clicked. So under `static`
|
|
285
|
+
* a press inside a member's CONTENT (not on kit chrome) is stopped on the
|
|
286
|
+
* HTML layer, in the bubble phase: the content has already received it, the
|
|
287
|
+
* renderer never does, nothing is prevented.
|
|
288
|
+
*/
|
|
289
|
+
const staticGuard = (e) => {
|
|
290
|
+
var _a, _b, _c, _d;
|
|
291
|
+
if (!ctx.isStatic() || ctx.disposed())
|
|
292
|
+
return;
|
|
293
|
+
const t = e.target;
|
|
294
|
+
const host = (_a = t === null || t === void 0 ? void 0 : t.closest) === null || _a === void 0 ? void 0 : _a.call(t, '.grafloria-node-host');
|
|
295
|
+
if (!host || !((_b = group.members) !== null && _b !== void 0 ? _b : new Set()).has((_c = host.getAttribute('data-node-id')) !== null && _c !== void 0 ? _c : ''))
|
|
296
|
+
return;
|
|
297
|
+
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'))
|
|
298
|
+
return;
|
|
299
|
+
e.stopPropagation();
|
|
300
|
+
};
|
|
301
|
+
let guardedLayer = null;
|
|
302
|
+
const ensureStaticGuard = () => {
|
|
303
|
+
if (guardedLayer === null || guardedLayer === void 0 ? void 0 : guardedLayer.isConnected)
|
|
304
|
+
return;
|
|
305
|
+
const layer = ctx.htmlLayer();
|
|
306
|
+
if (!layer)
|
|
307
|
+
return;
|
|
308
|
+
guardedLayer === null || guardedLayer === void 0 ? void 0 : guardedLayer.removeEventListener('pointerdown', staticGuard);
|
|
309
|
+
guardedLayer = layer;
|
|
310
|
+
layer.addEventListener('pointerdown', staticGuard);
|
|
311
|
+
};
|
|
312
|
+
// -- handles and grips on member hosts -------------------------------------
|
|
313
|
+
/**
|
|
314
|
+
* The chrome on every member host: the painted grip (or none) and the corner
|
|
315
|
+
* resize handle — ONE host lookup per member, which the host observer's
|
|
316
|
+
* budget counts (a repaint of one host must cost that host's lookup, not a
|
|
317
|
+
* second pass).
|
|
318
|
+
*/
|
|
319
|
+
const syncHandles = (only) => {
|
|
320
|
+
var _a, _b, _c, _d, _e;
|
|
321
|
+
deps.syncA11y(only);
|
|
322
|
+
if (ctx.disposed())
|
|
323
|
+
return;
|
|
324
|
+
ensureStaticGuard();
|
|
325
|
+
syncSlabs();
|
|
326
|
+
const grip = gripOf(deps.dragHandle());
|
|
327
|
+
const isStatic = ctx.isStatic();
|
|
328
|
+
const rtl = ctx.rtl();
|
|
329
|
+
for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : []) {
|
|
330
|
+
if (only && !only.has(id))
|
|
331
|
+
continue;
|
|
332
|
+
const node = diagram.getNode(id);
|
|
333
|
+
if (!node)
|
|
334
|
+
continue;
|
|
335
|
+
const host = ctx.hostOf(id);
|
|
336
|
+
if (!host)
|
|
337
|
+
continue;
|
|
338
|
+
syncGrip(host, grip, ((_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, 'widgetMovable')) !== false);
|
|
339
|
+
if (!deps.wantHandles)
|
|
340
|
+
continue;
|
|
341
|
+
const existing = host.querySelector(':scope > .axdb-rs');
|
|
342
|
+
if (((_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, 'widgetResizable')) === false) {
|
|
343
|
+
existing === null || existing === void 0 ? void 0 : existing.remove();
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const rs = existing !== null && existing !== void 0 ? existing : document.createElement('div');
|
|
347
|
+
if (!existing) {
|
|
348
|
+
rs.className = 'axdb-rs';
|
|
349
|
+
rs.setAttribute('title', 'Resize');
|
|
350
|
+
host.appendChild(rs);
|
|
351
|
+
}
|
|
352
|
+
// The grab corner mirrors with the board: bottom-right LTR, bottom-left
|
|
353
|
+
// RTL — the same corner the tile actually grows from in each direction.
|
|
354
|
+
rs.classList.toggle('axdb-rs--rtl', rtl);
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
/**
|
|
358
|
+
* Only a HOST-LEVEL change matters: a host arriving (a mount) or a host's
|
|
359
|
+
* own children changing (a repaint that wiped the injected handle). A
|
|
360
|
+
* chart's internal churn — most of what a live dashboard mutates — targets
|
|
361
|
+
* deeper nodes and is ignored, and the hosts the records DO name are the
|
|
362
|
+
* only ones re-synced. This was members × repaints `querySelector` calls
|
|
363
|
+
* per wave (9,216 at 96 widgets, review D10); it is now proportional to the
|
|
364
|
+
* hosts that actually changed.
|
|
365
|
+
*/
|
|
366
|
+
const hostObserver = new MutationObserver((records) => {
|
|
367
|
+
const touched = new Set();
|
|
368
|
+
const noteHost = (el) => {
|
|
369
|
+
var _a;
|
|
370
|
+
const e = el;
|
|
371
|
+
if ((_a = e === null || e === void 0 ? void 0 : e.classList) === null || _a === void 0 ? void 0 : _a.contains('grafloria-node-host')) {
|
|
372
|
+
const id = e.getAttribute('data-node-id');
|
|
373
|
+
if (id)
|
|
374
|
+
touched.add(id);
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
for (const r of records) {
|
|
378
|
+
// Our own re-injected handle arriving is not a change to answer — it
|
|
379
|
+
// would echo one more pass per repaint.
|
|
380
|
+
const ownEcho = r.removedNodes.length === 0 &&
|
|
381
|
+
r.addedNodes.length > 0 &&
|
|
382
|
+
Array.from(r.addedNodes).every((n) => { var _a; return (_a = n.classList) === null || _a === void 0 ? void 0 : _a.contains('axdb-rs'); });
|
|
383
|
+
if (ownEcho)
|
|
384
|
+
continue;
|
|
385
|
+
noteHost(r.target);
|
|
386
|
+
r.addedNodes.forEach((n) => noteHost(n));
|
|
387
|
+
}
|
|
388
|
+
if (touched.size)
|
|
389
|
+
syncHandles(touched);
|
|
390
|
+
});
|
|
391
|
+
const observe = (layer) => hostObserver.observe(layer, { childList: true, subtree: true });
|
|
392
|
+
// -- the hover affordance --------------------------------------------------
|
|
393
|
+
/**
|
|
394
|
+
* Edge affordance: the cursor says which border a press would take, the
|
|
395
|
+
* way gridstack's invisible edge handles do. One passive listener on the
|
|
396
|
+
* container; the corner handle keeps its own cursor from the stylesheet.
|
|
397
|
+
*/
|
|
398
|
+
let hoverHost = null;
|
|
399
|
+
const onHover = (e) => {
|
|
400
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
401
|
+
if (ctx.disposed() || deps.gestureRunning())
|
|
402
|
+
return;
|
|
403
|
+
markHotSection(e.clientX, e.clientY);
|
|
404
|
+
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');
|
|
405
|
+
if (!host) {
|
|
406
|
+
for (const id of (_c = group.members) !== null && _c !== void 0 ? _c : []) {
|
|
407
|
+
const h = ctx.hostOf(id);
|
|
408
|
+
if (!h)
|
|
409
|
+
continue;
|
|
410
|
+
const r = h.getBoundingClientRect();
|
|
411
|
+
if (e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom) {
|
|
412
|
+
host = h;
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (hoverHost && hoverHost !== host) {
|
|
418
|
+
hoverHost.style.cursor = '';
|
|
419
|
+
hoverHost.removeAttribute('data-axdb-edge'); // the affordance follows the pointer off a tile
|
|
420
|
+
}
|
|
421
|
+
hoverHost = host;
|
|
422
|
+
const grabbing = deps.grabbing();
|
|
423
|
+
if (!host) {
|
|
424
|
+
const wpt = ((_d = api.viewport) === null || _d === void 0 ? void 0 : _d.clientToWorld) ? api.viewport.clientToWorld(e.clientX, e.clientY, api.container.getBoundingClientRect()) : null;
|
|
425
|
+
const sid = wpt ? deps.memberGroupAt(wpt.x, wpt.y) : null;
|
|
426
|
+
const grp = sid ? diagram.getGroup(sid) : undefined;
|
|
427
|
+
const c = grp && wpt && !ctx.isStatic() ? cursorFor(deps.slabEdgesNear(grp, wpt.x, wpt.y)) : '';
|
|
428
|
+
if (!grabbing)
|
|
429
|
+
api.container.style.cursor = c;
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (!grabbing && api.container.style.cursor)
|
|
433
|
+
api.container.style.cursor = '';
|
|
434
|
+
const id = (_e = host.getAttribute('data-node-id')) !== null && _e !== void 0 ? _e : '';
|
|
435
|
+
if (!((_f = group.members) !== null && _f !== void 0 ? _f : new Set()).has(id))
|
|
436
|
+
return;
|
|
437
|
+
const node = diagram.getNode(id);
|
|
438
|
+
const resizable = !!node && !ctx.isStatic() && ((_g = node.state) === null || _g === void 0 ? void 0 : _g.locked) !== true && ((_h = node.getMetadata) === null || _h === void 0 ? void 0 : _h.call(node, 'widgetResizable')) !== false;
|
|
439
|
+
const cursor = resizable ? cursorFor(edgesNear(host, e.clientX, e.clientY)) : '';
|
|
440
|
+
if (cursor)
|
|
441
|
+
host.setAttribute('data-axdb-edge', cursor);
|
|
442
|
+
else
|
|
443
|
+
host.removeAttribute('data-axdb-edge');
|
|
444
|
+
};
|
|
445
|
+
const dispose = () => {
|
|
446
|
+
guardedLayer === null || guardedLayer === void 0 ? void 0 : guardedLayer.removeEventListener('pointerdown', staticGuard);
|
|
447
|
+
hostObserver.disconnect();
|
|
448
|
+
for (const el of slabEls.values())
|
|
449
|
+
el.remove();
|
|
450
|
+
slabEls.clear();
|
|
451
|
+
hoverSlabs.clear();
|
|
452
|
+
for (const bg of groupBgs.values())
|
|
453
|
+
bg.remove();
|
|
454
|
+
groupBgs.clear();
|
|
455
|
+
refusal === null || refusal === void 0 ? void 0 : refusal.remove();
|
|
456
|
+
refusal = null;
|
|
457
|
+
flushCarried();
|
|
458
|
+
};
|
|
459
|
+
return { syncSlabs, syncHandles, observe, ensureStaticGuard, onHover, onHoverLeave, setCarried, flushCarried, showRefusal, dispose };
|
|
460
|
+
}
|
|
461
|
+
//# sourceMappingURL=chrome.js.map
|
|
@@ -411,9 +411,17 @@ export interface DashboardHandle {
|
|
|
411
411
|
/**
|
|
412
412
|
* Add a widget to a view. CREATES the node (you do not pre-build one), wires
|
|
413
413
|
* its metadata, and commits node + membership as ONE undoable step.
|
|
414
|
-
* Auto-positions when the spec names no cell.
|
|
415
|
-
|
|
416
|
-
|
|
414
|
+
* Auto-positions when the spec names no cell. `opts.displaced`: the
|
|
415
|
+
* commands a palette drop handed `onDropIn` for the tiles the placeholder
|
|
416
|
+
* pushed aside — folded into the same step, so the widget lands on the cell
|
|
417
|
+
* the drop showed and undo puts the pushed tiles back with it. Left out,
|
|
418
|
+
* the board is re-read from the model and the push is forgotten: the new
|
|
419
|
+
* widget then auto-positions into whatever hole is left (Quantia's "lands
|
|
420
|
+
* on the cell it was aimed at", element 0.4.54).
|
|
421
|
+
*/
|
|
422
|
+
addWidget(spec: DashboardWidgetSpec, viewId?: string, opts?: {
|
|
423
|
+
displaced?: Command[];
|
|
424
|
+
}): WidgetHandle | undefined;
|
|
417
425
|
/**
|
|
418
426
|
* Re-read every board from the model — call after undo/redo, or any
|
|
419
427
|
* out-of-band mutation, so the grid and the projection agree again.
|
|
@@ -1575,8 +1575,8 @@ export function createDashboardHandle(ctx) {
|
|
|
1575
1575
|
(_a = ctx.apiRef) === null || _a === void 0 ? void 0 : _a.renderNow();
|
|
1576
1576
|
},
|
|
1577
1577
|
getDragHandle: () => { var _a, _b, _c; return (_b = (_a = binders.get(ctx.active)) === null || _a === void 0 ? void 0 : _a.getDragHandle()) !== null && _b !== void 0 ? _b : ((_c = ctx.optionsBase.dragHandle) !== null && _c !== void 0 ? _c : false); },
|
|
1578
|
-
addWidget(spec, viewId) {
|
|
1579
|
-
var _a, _b, _c, _d, _e, _f;
|
|
1578
|
+
addWidget(spec, viewId, opts) {
|
|
1579
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
1580
1580
|
const vid = viewId !== null && viewId !== void 0 ? viewId : ctx.active;
|
|
1581
1581
|
// `vid` may name a view OR a container — both are boards with a group,
|
|
1582
1582
|
// a binder and an authored array.
|
|
@@ -1601,10 +1601,15 @@ export function createDashboardHandle(ctx) {
|
|
|
1601
1601
|
const node = existing !== null && existing !== void 0 ? existing : buildWidgetNode(w, ctx.rowHeight);
|
|
1602
1602
|
if (w.pinned)
|
|
1603
1603
|
node.setState({ locked: true });
|
|
1604
|
-
// ONE undoable step, registration included (see AddWidgetCommand).
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1604
|
+
// ONE undoable step, registration included (see AddWidgetCommand). The
|
|
1605
|
+
// tiles a drop pushed aside ride in front of it, in a SEQUENCE: a batch
|
|
1606
|
+
// runs its members across awaits, and the board is re-read right after
|
|
1607
|
+
// this call — the pushed cells must be in the model by then.
|
|
1608
|
+
const add = new AddWidgetCommand(node, group.id, registry, !!existing);
|
|
1609
|
+
const displaced = (_e = opts === null || opts === void 0 ? void 0 : opts.displaced) !== null && _e !== void 0 ? _e : [];
|
|
1610
|
+
execCommand(displaced.length > 0 ? new SequenceCommand('Add widget', [...displaced, add]) : add);
|
|
1611
|
+
(_f = binders.get(vid)) === null || _f === void 0 ? void 0 : _f.sync();
|
|
1612
|
+
(_g = ctx.apiRef) === null || _g === void 0 ? void 0 : _g.renderNow();
|
|
1608
1613
|
return makeWidgetHandle(w.id);
|
|
1609
1614
|
},
|
|
1610
1615
|
refresh() {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Which edges of a tile a press takes, and the cursor that says so (tile first, step 4b-i: out of the binder). */
|
|
2
|
+
/** A press this close (CSS px) to a tile's border takes that edge for a resize. */
|
|
3
|
+
export declare const EDGE_GRIP = 7;
|
|
4
|
+
export interface ResizeEdges {
|
|
5
|
+
n: boolean;
|
|
6
|
+
e: boolean;
|
|
7
|
+
s: boolean;
|
|
8
|
+
w: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare const NO_EDGES: ResizeEdges;
|
|
11
|
+
/** Which of a host's edges a client point is within EDGE_GRIP of (none when outside). */
|
|
12
|
+
export declare function edgesNear(host: Element, cx: number, cy: number): ResizeEdges;
|
|
13
|
+
export declare const anyEdge: (E: ResizeEdges) => boolean;
|
|
14
|
+
/** The resize cursor for a set of edges ('' when none). */
|
|
15
|
+
export declare function cursorFor(E: ResizeEdges): string;
|