@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
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** The drag handle — the caption strip, a selector, or a painted grip — and the press test for it (tile first, step 4b-i: out of the binder). */
|
|
2
|
+
/** A painted grip: the only drag zone, placed along the card's top edge. */
|
|
3
|
+
export interface DragGripOptions {
|
|
4
|
+
grip: true;
|
|
5
|
+
/** Where along the top edge. Default 'left'. */
|
|
6
|
+
position?: 'left' | 'center' | 'right';
|
|
7
|
+
/** In the header band, or a tab above the card. Default 'inside'. */
|
|
8
|
+
placement?: 'inside' | 'outside';
|
|
9
|
+
}
|
|
10
|
+
export type DragHandleOption = boolean | string | DragGripOptions;
|
|
11
|
+
/** The class of the painted grip element (a child of the node host). */
|
|
12
|
+
export declare const GRIP_CLASS = "axdb-grip";
|
|
13
|
+
/** The container class that turns the caption strip's grip dots on. */
|
|
14
|
+
export declare const DRAG_HANDLE_CLASS = "axdb-drag-handle";
|
|
15
|
+
/** The caption strip of a host with no kit header, px from the card's top. */
|
|
16
|
+
export declare const CAPTION_BAND = 28;
|
|
17
|
+
/** A `dragHandle` value with its defaults filled in — the form the handle reports. */
|
|
18
|
+
export declare const normalizeDragHandle: (v: DragHandleOption | undefined) => DragHandleOption;
|
|
19
|
+
/** The selector a `dragHandle` value names — `null` when the whole card is the handle. */
|
|
20
|
+
export declare const dragHandleSelector: (v: DragHandleOption) => string | null;
|
|
21
|
+
/** The grip config of a value, or null when it paints no grip. */
|
|
22
|
+
export declare const gripOf: (v: DragHandleOption) => DragGripOptions | null;
|
|
23
|
+
export declare const sameDragHandle: (a: DragHandleOption, b: DragHandleOption) => boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Paint (or remove) the grip on one host, and stamp the host with the grip's
|
|
26
|
+
* placement so the header can make room for it. `movable` false = no grip.
|
|
27
|
+
*/
|
|
28
|
+
export declare function syncGrip(host: HTMLElement, cfg: DragGripOptions | null, movable: boolean): void;
|
|
29
|
+
/** The member host a press on a painted grip belongs to (the grip may sit OUTSIDE the host's box). */
|
|
30
|
+
export declare function gripHostOf(target: Element | null): HTMLElement | null;
|
|
31
|
+
/**
|
|
32
|
+
* Did a press land on the drag handle? A press INSIDE the handle element
|
|
33
|
+
* always does. The default handle is a CAPTION BAR the DevExpress way: the
|
|
34
|
+
* strip from the card's top edge down to the header's bottom, padding
|
|
35
|
+
* included — so the pointer need not hit the header's text to grab the tile.
|
|
36
|
+
*/
|
|
37
|
+
export declare function pressOnDragHandle(sel: string, target: Element | null, hostEl: HTMLElement | null, clientX: number, clientY: number): boolean;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** The drag handle — the caption strip, a selector, or a painted grip — and the press test for it (tile first, step 4b-i: out of the binder). */
|
|
2
|
+
/** The class of the painted grip element (a child of the node host). */
|
|
3
|
+
export const GRIP_CLASS = 'axdb-grip';
|
|
4
|
+
/** The container class that turns the caption strip's grip dots on. */
|
|
5
|
+
export const DRAG_HANDLE_CLASS = 'axdb-drag-handle';
|
|
6
|
+
/** The caption strip of a host with no kit header, px from the card's top. */
|
|
7
|
+
export const CAPTION_BAND = 28;
|
|
8
|
+
/** A `dragHandle` value with its defaults filled in — the form the handle reports. */
|
|
9
|
+
export const normalizeDragHandle = (v) => {
|
|
10
|
+
var _a, _b;
|
|
11
|
+
return typeof v === 'object' && v !== null && v.grip
|
|
12
|
+
? { grip: true, position: (_a = v.position) !== null && _a !== void 0 ? _a : 'left', placement: (_b = v.placement) !== null && _b !== void 0 ? _b : 'inside' }
|
|
13
|
+
: v === true ? true : typeof v === 'string' && v.length > 0 ? v : false;
|
|
14
|
+
};
|
|
15
|
+
/** The selector a `dragHandle` value names — `null` when the whole card is the handle. */
|
|
16
|
+
export const dragHandleSelector = (v) => v === true ? '.axdb-widget-h' : typeof v === 'string' ? v : typeof v === 'object' ? '.' + GRIP_CLASS : null;
|
|
17
|
+
/** The grip config of a value, or null when it paints no grip. */
|
|
18
|
+
export const gripOf = (v) => (typeof v === 'object' ? v : null);
|
|
19
|
+
export const sameDragHandle = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
20
|
+
/**
|
|
21
|
+
* Paint (or remove) the grip on one host, and stamp the host with the grip's
|
|
22
|
+
* placement so the header can make room for it. `movable` false = no grip.
|
|
23
|
+
*/
|
|
24
|
+
export function syncGrip(host, cfg, movable) {
|
|
25
|
+
var _a, _b, _c, _d, _e;
|
|
26
|
+
const existing = host.querySelector(':scope > .' + GRIP_CLASS);
|
|
27
|
+
host.classList.remove('axdb-gp-inside', 'axdb-gp-outside', 'axdb-gp-left', 'axdb-gp-center', 'axdb-gp-right');
|
|
28
|
+
if (!cfg || !movable) {
|
|
29
|
+
existing === null || existing === void 0 ? void 0 : existing.remove();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const el = (_a = existing) !== null && _a !== void 0 ? _a : host.ownerDocument.createElement('div');
|
|
33
|
+
if (!existing) {
|
|
34
|
+
el.setAttribute('aria-hidden', 'true');
|
|
35
|
+
el.setAttribute('title', 'Drag');
|
|
36
|
+
host.appendChild(el);
|
|
37
|
+
}
|
|
38
|
+
el.className = `${GRIP_CLASS} ${GRIP_CLASS}--${(_b = cfg.position) !== null && _b !== void 0 ? _b : 'left'} ${GRIP_CLASS}--${(_c = cfg.placement) !== null && _c !== void 0 ? _c : 'inside'}`;
|
|
39
|
+
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'}`);
|
|
40
|
+
}
|
|
41
|
+
/** The member host a press on a painted grip belongs to (the grip may sit OUTSIDE the host's box). */
|
|
42
|
+
export function gripHostOf(target) {
|
|
43
|
+
var _a, _b;
|
|
44
|
+
const grip = (_a = target === null || target === void 0 ? void 0 : target.closest) === null || _a === void 0 ? void 0 : _a.call(target, '.' + GRIP_CLASS);
|
|
45
|
+
return (_b = grip === null || grip === void 0 ? void 0 : grip.closest('.grafloria-node-host')) !== null && _b !== void 0 ? _b : null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Did a press land on the drag handle? A press INSIDE the handle element
|
|
49
|
+
* always does. The default handle is a CAPTION BAR the DevExpress way: the
|
|
50
|
+
* strip from the card's top edge down to the header's bottom, padding
|
|
51
|
+
* included — so the pointer need not hit the header's text to grab the tile.
|
|
52
|
+
*/
|
|
53
|
+
export function pressOnDragHandle(sel, target, hostEl, clientX, clientY) {
|
|
54
|
+
var _a, _b;
|
|
55
|
+
const grip = (_b = (_a = target === null || target === void 0 ? void 0 : target.closest) === null || _a === void 0 ? void 0 : _a.call(target, sel)) !== null && _b !== void 0 ? _b : null;
|
|
56
|
+
if (grip && (!hostEl || hostEl.contains(grip)))
|
|
57
|
+
return true;
|
|
58
|
+
if (sel !== '.axdb-widget-h' || !hostEl)
|
|
59
|
+
return false;
|
|
60
|
+
const hr = hostEl.getBoundingClientRect();
|
|
61
|
+
const header = hostEl.querySelector('.axdb-widget-h');
|
|
62
|
+
// A host painting its own content has no kit header: its caption is the top
|
|
63
|
+
// band of the card, so `dragHandle: true` still means something there.
|
|
64
|
+
const bottom = header ? header.getBoundingClientRect().bottom : hr.top + CAPTION_BAND;
|
|
65
|
+
return clientX >= hr.left && clientX <= hr.right && clientY >= hr.top && clientY <= bottom;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=grip.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KEYBOARD OPERATION (tile first, step 4b-i: out of the binder). WCAG 2.1.1,
|
|
3
|
+
* and the non-drag alternative 2.5.7 asks for: on a focused member, arrows
|
|
4
|
+
* move it one cell, Shift+arrows resize it one cell, Home/End jump to the
|
|
5
|
+
* first/last member. Every move and resize is the same programmatic gesture
|
|
6
|
+
* the API uses — one undoable step, reported through onLayoutChange — and
|
|
7
|
+
* every outcome is spoken: the tile's new cell, each neighbour it displaced,
|
|
8
|
+
* or why it was refused. Handled keys stop here so the renderer's own pixel
|
|
9
|
+
* nudge never fights the grid.
|
|
10
|
+
*/
|
|
11
|
+
import type { NodeModel } from '@grafloria/engine';
|
|
12
|
+
import { LiveRegionController } from '@grafloria/renderer';
|
|
13
|
+
import type { CellRect } from './grid-mapping.js';
|
|
14
|
+
import type { BoardCtx } from './board-ctx.js';
|
|
15
|
+
export declare function liveRegionFor(container: HTMLElement): LiveRegionController;
|
|
16
|
+
export declare function directionName(dx: number, dy: number): string;
|
|
17
|
+
/** "column 4, row 2, 3 by 1" — the cell as a person hears it (1-based). */
|
|
18
|
+
export declare function describeCell(c: CellRect): string;
|
|
19
|
+
export interface KeyboardDeps {
|
|
20
|
+
/** The board's handle — its programmatic move, resize and focus; read late, it exists after the binder is built. */
|
|
21
|
+
handle(): {
|
|
22
|
+
moveTo(id: string, x: number, y: number): Promise<boolean>;
|
|
23
|
+
resizeTo(id: string, w: number, h: number): Promise<boolean>;
|
|
24
|
+
focusWidget(id: string): boolean;
|
|
25
|
+
};
|
|
26
|
+
live: LiveRegionController;
|
|
27
|
+
/** The accessible name of a widget: its title, else its kind, else its id. */
|
|
28
|
+
nameOf(node: NodeModel): string;
|
|
29
|
+
focusedId(): string | undefined;
|
|
30
|
+
setFocusedId(id: string): void;
|
|
31
|
+
selectedId(): string | undefined;
|
|
32
|
+
selectWidget(id: string): void;
|
|
33
|
+
syncA11y(): void;
|
|
34
|
+
/** A tile gesture is live: the keys stay out of its way. */
|
|
35
|
+
gestureRunning(): boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface Keyboard {
|
|
38
|
+
onFocusIn(e: FocusEvent): void;
|
|
39
|
+
onKey(e: KeyboardEvent): void;
|
|
40
|
+
}
|
|
41
|
+
export declare function createKeyboard(ctx: BoardCtx, deps: KeyboardDeps): Keyboard;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { __awaiter } from "tslib";
|
|
2
|
+
import { LiveRegionController } from '@grafloria/renderer';
|
|
3
|
+
/**
|
|
4
|
+
* ONE aria-live region per canvas, shared by every board on it — the
|
|
5
|
+
* renderer's own controller (coalescing, de-duplicating), so a dashboard
|
|
6
|
+
* announces through the same channel a diagram does. WeakMap: the region
|
|
7
|
+
* follows the container out of memory.
|
|
8
|
+
*/
|
|
9
|
+
const LIVE_REGIONS = new WeakMap();
|
|
10
|
+
export function liveRegionFor(container) {
|
|
11
|
+
let live = LIVE_REGIONS.get(container);
|
|
12
|
+
if (!live) {
|
|
13
|
+
live = new LiveRegionController(container);
|
|
14
|
+
LIVE_REGIONS.set(container, live);
|
|
15
|
+
}
|
|
16
|
+
return live;
|
|
17
|
+
}
|
|
18
|
+
export function directionName(dx, dy) {
|
|
19
|
+
return dx < 0 ? 'left' : dx > 0 ? 'right' : dy < 0 ? 'up' : 'down';
|
|
20
|
+
}
|
|
21
|
+
/** "column 4, row 2, 3 by 1" — the cell as a person hears it (1-based). */
|
|
22
|
+
export function describeCell(c) {
|
|
23
|
+
return `column ${c.x + 1}, row ${c.y + 1}, ${c.w} by ${c.h}`;
|
|
24
|
+
}
|
|
25
|
+
export function createKeyboard(ctx, deps) {
|
|
26
|
+
const { group, diagram } = ctx;
|
|
27
|
+
const memberHostAt = (target) => {
|
|
28
|
+
var _a, _b, _c;
|
|
29
|
+
const host = (_a = target === null || target === void 0 ? void 0 : target.closest) === null || _a === void 0 ? void 0 : _a.call(target, '.grafloria-node-host');
|
|
30
|
+
if (!host)
|
|
31
|
+
return null;
|
|
32
|
+
const id = (_b = host.getAttribute('data-node-id')) !== null && _b !== void 0 ? _b : '';
|
|
33
|
+
if (!((_c = group.members) !== null && _c !== void 0 ? _c : new Set()).has(id) || !diagram.getNode(id))
|
|
34
|
+
return null;
|
|
35
|
+
return { id, host };
|
|
36
|
+
};
|
|
37
|
+
const onFocusIn = (e) => {
|
|
38
|
+
const hit = memberHostAt(e.target);
|
|
39
|
+
if (!hit || ctx.disposed())
|
|
40
|
+
return;
|
|
41
|
+
if (deps.focusedId() !== hit.id || deps.selectedId() !== hit.id) {
|
|
42
|
+
deps.setFocusedId(hit.id);
|
|
43
|
+
deps.selectWidget(hit.id);
|
|
44
|
+
deps.syncA11y();
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const onKey = (e) => {
|
|
48
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
49
|
+
if (ctx.disposed() || deps.gestureRunning())
|
|
50
|
+
return;
|
|
51
|
+
const handle = deps.handle();
|
|
52
|
+
const hit = memberHostAt(e.target);
|
|
53
|
+
if (!hit) {
|
|
54
|
+
const el = e.target;
|
|
55
|
+
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'));
|
|
56
|
+
if (onRoot && (e.key.startsWith('Arrow') || e.key === 'Enter' || e.key === ' ')) {
|
|
57
|
+
const members = [...((_c = group.members) !== null && _c !== void 0 ? _c : [])].filter((id) => !!diagram.getNode(id) && !!ctx.hostOf(id));
|
|
58
|
+
const focused = deps.focusedId();
|
|
59
|
+
const target = focused && members.includes(focused) ? focused : members[0];
|
|
60
|
+
if (target && handle.focusWidget(target)) {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
e.stopPropagation();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const members = [...((_d = group.members) !== null && _d !== void 0 ? _d : [])].filter((id) => !!diagram.getNode(id) && !!ctx.hostOf(id));
|
|
68
|
+
if (e.key === 'Home' || e.key === 'End') {
|
|
69
|
+
const id = e.key === 'Home' ? members[0] : members[members.length - 1];
|
|
70
|
+
if (id)
|
|
71
|
+
handle.focusWidget(id);
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
e.stopPropagation();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const arrow = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] }[e.key];
|
|
77
|
+
if (!arrow)
|
|
78
|
+
return;
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
e.stopPropagation();
|
|
81
|
+
if (ctx.isStatic())
|
|
82
|
+
return; // readable, not editable
|
|
83
|
+
const engine = ctx.engine();
|
|
84
|
+
const node = diagram.getNode(hit.id);
|
|
85
|
+
const item = engine.getItem(hit.id);
|
|
86
|
+
if (!node || !item)
|
|
87
|
+
return;
|
|
88
|
+
const name = deps.nameOf(node);
|
|
89
|
+
const live = deps.live;
|
|
90
|
+
if (((_e = node.state) === null || _e === void 0 ? void 0 : _e.locked) === true) {
|
|
91
|
+
live.announceError(`${name} is pinned`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const [dx, dy] = arrow;
|
|
95
|
+
const before = new Map(engine.getItems().map((i) => [i.id, { x: i.x, y: i.y, w: i.w, h: i.h }]));
|
|
96
|
+
const resize = e.shiftKey;
|
|
97
|
+
if (resize && ((_f = node.getMetadata) === null || _f === void 0 ? void 0 : _f.call(node, 'widgetResizable')) === false) {
|
|
98
|
+
live.announceError(`${name} cannot be resized`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (!resize && ((_g = node.getMetadata) === null || _g === void 0 ? void 0 : _g.call(node, 'widgetMovable')) === false) {
|
|
102
|
+
live.announceError(`${name} cannot be moved`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const stepOrSwap = () => __awaiter(this, void 0, void 0, function* () {
|
|
106
|
+
if (yield handle.moveTo(hit.id, item.x + dx, item.y + dy))
|
|
107
|
+
return true;
|
|
108
|
+
const probe = { x: item.x + dx, y: item.y + dy, w: item.w, h: item.h };
|
|
109
|
+
const c = engine
|
|
110
|
+
.getItems()
|
|
111
|
+
.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);
|
|
112
|
+
if (!c)
|
|
113
|
+
return false;
|
|
114
|
+
const tx = dx > 0 ? c.x + c.w - item.w : dx < 0 ? c.x : item.x;
|
|
115
|
+
const ty = dy > 0 ? c.y + c.h - item.h : dy < 0 ? c.y : item.y;
|
|
116
|
+
return handle.moveTo(hit.id, tx, ty);
|
|
117
|
+
});
|
|
118
|
+
const op = resize ? handle.resizeTo(hit.id, item.w + dx, item.h + dy) : stepOrSwap();
|
|
119
|
+
void op.then((ok) => {
|
|
120
|
+
var _a, _b;
|
|
121
|
+
if (ctx.disposed())
|
|
122
|
+
return;
|
|
123
|
+
const after = ctx.engine().getItem(hit.id);
|
|
124
|
+
if (!ok || !after) {
|
|
125
|
+
live.announceError(resize ? `Cannot resize ${name} that way` : `Cannot move ${name} ${directionName(dx, dy)}`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const parts = [`${name} ${resize ? 'resized' : 'moved'} to ${describeCell(after)}`];
|
|
129
|
+
for (const other of ctx.engine().getItems()) {
|
|
130
|
+
if (other.id === hit.id)
|
|
131
|
+
continue;
|
|
132
|
+
const was = before.get(other.id);
|
|
133
|
+
if (!was || (was.x === other.x && was.y === other.y))
|
|
134
|
+
continue;
|
|
135
|
+
const o = diagram.getNode(other.id);
|
|
136
|
+
parts.push(`${o ? deps.nameOf(o) : other.id} moved to ${describeCell(other)}`);
|
|
137
|
+
}
|
|
138
|
+
live.announce(parts.join('. '), 'polite', true);
|
|
139
|
+
deps.syncA11y();
|
|
140
|
+
(_b = (_a = ctx.hostOf(hit.id)) === null || _a === void 0 ? void 0 : _a.focus) === null || _b === void 0 ? void 0 : _b.call(_a, { preventScroll: true });
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
return { onFocusIn, onKey };
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=keyboard.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE PROJECTION (tile first, step 4b-i): cells to pixels, and the chrome that
|
|
3
|
+
* rides on the projection — the board's own height, the placeholder under a
|
|
4
|
+
* ghost, the ghost's transition exemption, the glide every pushed tile gets.
|
|
5
|
+
* Moved out of the binder unchanged in behaviour; the binder keeps calling
|
|
6
|
+
* these by the same names.
|
|
7
|
+
*/
|
|
8
|
+
import type { WorldRect } from './grid-mapping.js';
|
|
9
|
+
import type { BoardCtx } from './board-ctx.js';
|
|
10
|
+
export interface Projection {
|
|
11
|
+
/** Write one member's projected rect (derived state → system write). */
|
|
12
|
+
writeRect(id: string, r: WorldRect): void;
|
|
13
|
+
/** Enforce the board-frame height the sizing mode implies. */
|
|
14
|
+
enforceBoardHeight(): void;
|
|
15
|
+
/** Project every member from its engine cells (the ghost is exempt). */
|
|
16
|
+
project(): void;
|
|
17
|
+
/** The placeholder follows the ghost's cell; gone when there is no ghost on this board. */
|
|
18
|
+
syncPlaceholder(): void;
|
|
19
|
+
/** Take the placeholder down now (an accent overlay says which half; the grey cell under it is noise). */
|
|
20
|
+
hidePlaceholder(): void;
|
|
21
|
+
armGlide(): void;
|
|
22
|
+
disarmGlideSoon(): void;
|
|
23
|
+
/** Lift a pending ghost NOW. */
|
|
24
|
+
flushGhost(): void;
|
|
25
|
+
setGhost(id: string, on: boolean): void;
|
|
26
|
+
dispose(): void;
|
|
27
|
+
}
|
|
28
|
+
export declare function createProjection(ctx: BoardCtx, deps: {
|
|
29
|
+
afterProject(): void;
|
|
30
|
+
}): Projection;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { cellToRect } from './grid-mapping.js';
|
|
2
|
+
const GLIDE_OFF_DELAY = 400;
|
|
3
|
+
export function createProjection(ctx, deps) {
|
|
4
|
+
const { api, group, diagram } = ctx;
|
|
5
|
+
let placeholder = null;
|
|
6
|
+
let glideTimer = null;
|
|
7
|
+
let ghostTimer = null;
|
|
8
|
+
/** The host whose ghost class the pending timer will lift. */
|
|
9
|
+
let ghostHost = null;
|
|
10
|
+
const writeRect = (id, r) => {
|
|
11
|
+
const node = diagram.getNode(id);
|
|
12
|
+
if (node) {
|
|
13
|
+
if (Math.abs(node.position.x - r.x) > 0.25 ||
|
|
14
|
+
Math.abs(node.position.y - r.y) > 0.25 ||
|
|
15
|
+
Math.abs(node.size.width - r.width) > 0.25 ||
|
|
16
|
+
Math.abs(node.size.height - r.height) > 0.25) {
|
|
17
|
+
diagram.runSystemWrite(() => {
|
|
18
|
+
var _a;
|
|
19
|
+
node.setPosition(r.x, r.y);
|
|
20
|
+
node.setSize(r.width, r.height, (_a = node.size.depth) !== null && _a !== void 0 ? _a : 0);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const grp = diagram.getGroup(id);
|
|
26
|
+
if (grp) {
|
|
27
|
+
const p = grp.position;
|
|
28
|
+
const s = ctx.sizeOf(grp);
|
|
29
|
+
if (Math.abs(p.x - r.x) > 0.25 ||
|
|
30
|
+
Math.abs(p.y - r.y) > 0.25 ||
|
|
31
|
+
Math.abs(s.width - r.width) > 0.25 ||
|
|
32
|
+
Math.abs(s.height - r.height) > 0.25) {
|
|
33
|
+
diagram.runSystemWrite(() => grp.setFrame(Object.assign({}, r)));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const enforceBoardHeight = () => {
|
|
38
|
+
const designH = ctx.designH();
|
|
39
|
+
if (designH <= 0)
|
|
40
|
+
return;
|
|
41
|
+
const r = ctx.rows();
|
|
42
|
+
// FIT keeps its design height, full stop — the user's rule: "the board
|
|
43
|
+
// stays the same and the widgets change size so all of them fit". Only
|
|
44
|
+
// overflow:'scroll' lets the frame EXTEND to hold the rows at the floor
|
|
45
|
+
// height (and the canvas pan). Grow extends at the base row height.
|
|
46
|
+
const target = ctx.sizing() === 'fit'
|
|
47
|
+
? ctx.overflow === 'scroll'
|
|
48
|
+
? Math.max(designH, 2 * ctx.padding + r * ctx.minRowHeight + (r - 1) * ctx.gap)
|
|
49
|
+
: designH
|
|
50
|
+
: Math.max(designH, 2 * ctx.padding + r * ctx.baseRowHeight + (r - 1) * ctx.gap);
|
|
51
|
+
const f = ctx.frame();
|
|
52
|
+
if (Math.abs(f.height - target) > 0.5) {
|
|
53
|
+
ctx.write(() => diagram.runSystemWrite(() => group.setFrame({ x: f.x, y: f.y, width: f.width, height: target })));
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
/** The placeholder exists ONLY while a gesture is live — so at any moment
|
|
57
|
+
* the DOM holds at most one `.axdb-ph` per active gesture, not one idle
|
|
58
|
+
* div per bound board. */
|
|
59
|
+
const syncPlaceholder = () => {
|
|
60
|
+
const ghostId = ctx.ghostId();
|
|
61
|
+
const item = ghostId ? ctx.engine().getItem(ghostId) : undefined;
|
|
62
|
+
if (!item) {
|
|
63
|
+
placeholder === null || placeholder === void 0 ? void 0 : placeholder.remove();
|
|
64
|
+
placeholder = null;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const layer = ctx.htmlLayer();
|
|
68
|
+
if (!layer)
|
|
69
|
+
return;
|
|
70
|
+
if (!placeholder || placeholder.parentElement !== layer) {
|
|
71
|
+
placeholder === null || placeholder === void 0 ? void 0 : placeholder.remove();
|
|
72
|
+
placeholder = document.createElement('div');
|
|
73
|
+
placeholder.className = 'axdb-ph';
|
|
74
|
+
layer.prepend(placeholder);
|
|
75
|
+
}
|
|
76
|
+
const r = cellToRect(item, ctx.frame(), ctx.geom(), ctx.rows());
|
|
77
|
+
placeholder.style.display = 'block';
|
|
78
|
+
placeholder.style.left = `${r.x}px`;
|
|
79
|
+
placeholder.style.top = `${r.y}px`;
|
|
80
|
+
placeholder.style.width = `${r.width}px`;
|
|
81
|
+
placeholder.style.height = `${r.height}px`;
|
|
82
|
+
};
|
|
83
|
+
const hidePlaceholder = () => {
|
|
84
|
+
placeholder === null || placeholder === void 0 ? void 0 : placeholder.remove();
|
|
85
|
+
placeholder = null;
|
|
86
|
+
};
|
|
87
|
+
const project = () => {
|
|
88
|
+
enforceBoardHeight();
|
|
89
|
+
ctx.write(() => {
|
|
90
|
+
const f = ctx.frame();
|
|
91
|
+
const g = ctx.geom();
|
|
92
|
+
const r = ctx.rows();
|
|
93
|
+
const ghost = ctx.ghostId();
|
|
94
|
+
for (const item of ctx.engine().getItems()) {
|
|
95
|
+
if (item.id === ghost)
|
|
96
|
+
continue; // the ghost — this board's own, or one adopted from another binder
|
|
97
|
+
writeRect(item.id, cellToRect(item, f, g, r));
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
syncPlaceholder();
|
|
101
|
+
// The section overlays are projected chrome like the placeholder: they
|
|
102
|
+
// follow every frame write, not only a rebuild. Painted at bind time only,
|
|
103
|
+
// a section two levels down kept the geometry of its parent's placeholder
|
|
104
|
+
// frame (100 × 34) after the view board had laid the parent out (the kit
|
|
105
|
+
// lab's L47, 2026-09-08).
|
|
106
|
+
deps.afterProject();
|
|
107
|
+
};
|
|
108
|
+
const armGlide = () => {
|
|
109
|
+
var _a;
|
|
110
|
+
(_a = ctx.htmlLayer()) === null || _a === void 0 ? void 0 : _a.classList.add('axdb-glide');
|
|
111
|
+
if (glideTimer)
|
|
112
|
+
clearTimeout(glideTimer);
|
|
113
|
+
};
|
|
114
|
+
const disarmGlideSoon = () => {
|
|
115
|
+
if (glideTimer)
|
|
116
|
+
clearTimeout(glideTimer);
|
|
117
|
+
glideTimer = setTimeout(() => { var _a; return (_a = ctx.htmlLayer()) === null || _a === void 0 ? void 0 : _a.classList.remove('axdb-glide'); }, GLIDE_OFF_DELAY);
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Lift a pending ghost NOW. One timer serves every host, so superseding it
|
|
121
|
+
* (a new gesture within 60 ms of the last drop — ③ then ④ in the
|
|
122
|
+
* nested-containers checks — or a dispose on a rebind) used to clear the
|
|
123
|
+
* timer and leave the previous tile lifted for good: a permanent drop
|
|
124
|
+
* shadow the visual gate finally caught.
|
|
125
|
+
*/
|
|
126
|
+
const flushGhost = () => {
|
|
127
|
+
if (ghostTimer)
|
|
128
|
+
clearTimeout(ghostTimer);
|
|
129
|
+
ghostTimer = null;
|
|
130
|
+
ghostHost === null || ghostHost === void 0 ? void 0 : ghostHost.classList.remove('axdb-ghost', 'axdb-out');
|
|
131
|
+
ghostHost = null;
|
|
132
|
+
};
|
|
133
|
+
const setGhost = (id, on) => {
|
|
134
|
+
const host = ctx.hostOf(id);
|
|
135
|
+
if (!host)
|
|
136
|
+
return;
|
|
137
|
+
if (ghostHost && ghostHost !== host)
|
|
138
|
+
flushGhost();
|
|
139
|
+
if (on) {
|
|
140
|
+
if (ghostTimer)
|
|
141
|
+
clearTimeout(ghostTimer);
|
|
142
|
+
ghostTimer = null;
|
|
143
|
+
host.classList.add('axdb-ghost');
|
|
144
|
+
host.classList.remove('axdb-out');
|
|
145
|
+
ghostHost = host;
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
host.classList.remove('axdb-out');
|
|
149
|
+
// Keep transition-exemption through the drop write so the snap into the
|
|
150
|
+
// placeholder is INSTANT (gridstack-style), then let glides resume.
|
|
151
|
+
if (ghostTimer)
|
|
152
|
+
clearTimeout(ghostTimer);
|
|
153
|
+
ghostHost = host;
|
|
154
|
+
ghostTimer = setTimeout(() => {
|
|
155
|
+
host.classList.remove('axdb-ghost');
|
|
156
|
+
ghostTimer = null;
|
|
157
|
+
if (ghostHost === host)
|
|
158
|
+
ghostHost = null;
|
|
159
|
+
}, 60);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const dispose = () => {
|
|
163
|
+
var _a;
|
|
164
|
+
hidePlaceholder();
|
|
165
|
+
if (glideTimer)
|
|
166
|
+
clearTimeout(glideTimer);
|
|
167
|
+
glideTimer = null;
|
|
168
|
+
// A rebind inside the 60 ms window (a layout switch right after an undo)
|
|
169
|
+
// disposed this binder with the timer pending — the host kept its
|
|
170
|
+
// lifted ghost for good (visual gate, nested-containers ⑤).
|
|
171
|
+
flushGhost();
|
|
172
|
+
(_a = ctx.htmlLayer()) === null || _a === void 0 ? void 0 : _a.classList.remove('axdb-glide');
|
|
173
|
+
void api;
|
|
174
|
+
};
|
|
175
|
+
return { writeRect, enforceBoardHeight, project, syncPlaceholder, hidePlaceholder, armGlide, disarmGlideSoon, flushGhost, setGhost, dispose };
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=project.js.map
|