@grafloria/element 0.4.32 → 0.4.33
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
CHANGED
|
@@ -1099,6 +1099,7 @@ export function createDashboardHandle(ctx) {
|
|
|
1099
1099
|
return { move, collapse: remaining.length > 0 ? [] : collapseOf(boardId) };
|
|
1100
1100
|
},
|
|
1101
1101
|
joinTargets: [...ctx.layoutOf.keys()].filter((id) => { var _a, _b; return id !== containerId && id !== pageId && ((_a = ctx.layoutOf.get(id)) !== null && _a !== void 0 ? _a : (_b = specById.get(id)) === null || _b === void 0 ? void 0 : _b.layout) === 'tabs' && !!model.getGroup(id) && !insidePage(id); }),
|
|
1102
|
+
stripHeight: (targetId) => tabStripReserve(ctx.tabsOf.get(targetId), Math.max(1, liveCount(targetId))),
|
|
1102
1103
|
stripIndex: (targetId, cx, cy) => {
|
|
1103
1104
|
const strip = ctx.tabStrips.get(targetId);
|
|
1104
1105
|
if (!strip)
|
|
@@ -567,6 +567,8 @@ export interface TearOutPlan {
|
|
|
567
567
|
markDrop(targetId: string | null, index: number | null): void;
|
|
568
568
|
/** The tab index a CLIENT point means along `targetId`'s strip — null when the point is not over that strip. */
|
|
569
569
|
stripIndex(targetId: string, clientX: number, clientY: number): number | null;
|
|
570
|
+
/** The strip's height on `targetId`, px — the band above its body. */
|
|
571
|
+
stripHeight(targetId: string): number;
|
|
570
572
|
/** The commands that REORDER the page to `index` along its own strip. Empty when nothing changes. */
|
|
571
573
|
reorder(index: number): Command[];
|
|
572
574
|
/**
|
|
@@ -650,17 +652,34 @@ interface AdoptedLeg {
|
|
|
650
652
|
x: number;
|
|
651
653
|
y: number;
|
|
652
654
|
}): void;
|
|
655
|
+
/** Put the tile at a PRESCRIBED cell (a split's half, a docking band): sized to it, unlocked tiles pushed. Answers whether it sits there. */
|
|
656
|
+
place(cell: {
|
|
657
|
+
x: number;
|
|
658
|
+
y: number;
|
|
659
|
+
w: number;
|
|
660
|
+
h: number;
|
|
661
|
+
}): boolean;
|
|
662
|
+
/** Every other tile's cell before the ghost entered — the layout a row insert translates. */
|
|
663
|
+
baseline(): Map<string, CellRect>;
|
|
653
664
|
/** Undo the adoption: target board back to its pre-entry layout. */
|
|
654
665
|
abort(): void;
|
|
655
666
|
/**
|
|
656
667
|
* Close the leg for commit: returns the target-side displaced commands, the
|
|
657
|
-
* tile's final cell and its projected rect
|
|
658
|
-
*
|
|
668
|
+
* tile's final cell and its projected rect — and the member GROUPS the
|
|
669
|
+
* adoption moved (the node commands skip groups; a dock that pushed sections
|
|
670
|
+
* commits them through these). Null when the tile is somehow gone.
|
|
659
671
|
*/
|
|
660
672
|
finalize(): {
|
|
661
673
|
commands: Command[];
|
|
662
674
|
cell: CellRect;
|
|
663
675
|
rect: WorldRect;
|
|
676
|
+
groups: Array<{
|
|
677
|
+
id: string;
|
|
678
|
+
cellBefore: CellRect;
|
|
679
|
+
cellAfter: CellRect;
|
|
680
|
+
frameBefore: WorldRect;
|
|
681
|
+
frameAfter: WorldRect;
|
|
682
|
+
}>;
|
|
664
683
|
} | null;
|
|
665
684
|
}
|
|
666
685
|
/**
|
|
@@ -2453,6 +2453,34 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
2453
2453
|
syncPlaceholder();
|
|
2454
2454
|
return {
|
|
2455
2455
|
groupId: group.id,
|
|
2456
|
+
baseline: () => startCells,
|
|
2457
|
+
place: (cell) => {
|
|
2458
|
+
if (!engine.getItem(node.id) && !engine.add({ id: node.id, x: 0, y: engine.rows(), w: cell.w, h: cell.h }))
|
|
2459
|
+
return false;
|
|
2460
|
+
const it = engine.getItem(node.id);
|
|
2461
|
+
if (!it)
|
|
2462
|
+
return false;
|
|
2463
|
+
// Shrink, move, then grow. A resize is clamped at the board's edge
|
|
2464
|
+
// where the ghost SITS, so a cell wider than fits there (a full-width
|
|
2465
|
+
// dock from a ghost parked mid-board) is reached by moving first —
|
|
2466
|
+
// and a cell narrower than the ghost by shrinking first, so the move
|
|
2467
|
+
// itself fits.
|
|
2468
|
+
const w0 = Math.min(it.w, cell.w);
|
|
2469
|
+
const h0 = Math.min(it.h, cell.h);
|
|
2470
|
+
if (w0 !== it.w || h0 !== it.h)
|
|
2471
|
+
engine.resizeCheck(node.id, w0, h0);
|
|
2472
|
+
const moved = engine.getItem(node.id);
|
|
2473
|
+
if (moved && (moved.x !== cell.x || moved.y !== cell.y))
|
|
2474
|
+
engine.moveCheck(node.id, cell.x, cell.y, { gate: false });
|
|
2475
|
+
const now = engine.getItem(node.id);
|
|
2476
|
+
if (now && (now.w !== cell.w || now.h !== cell.h))
|
|
2477
|
+
engine.resizeCheck(node.id, cell.w, cell.h);
|
|
2478
|
+
lastWant = null;
|
|
2479
|
+
project();
|
|
2480
|
+
syncPlaceholder();
|
|
2481
|
+
const at = engine.getItem(node.id);
|
|
2482
|
+
return !!at && at.x === cell.x && at.y === cell.y && at.w === cell.w && at.h === cell.h;
|
|
2483
|
+
},
|
|
2456
2484
|
move: (w) => {
|
|
2457
2485
|
const item = engine.getItem(node.id);
|
|
2458
2486
|
if (!item)
|
|
@@ -2506,11 +2534,29 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
2506
2534
|
const cell = { x: item.x, y: item.y, w: item.w, h: item.h };
|
|
2507
2535
|
const rect = cellToRect(item, frame(), geom(), rows());
|
|
2508
2536
|
const commands = buildCommitCommands(deltasSince(startCells, startGeom, node.id));
|
|
2537
|
+
const groups = [];
|
|
2538
|
+
for (const [id, before] of startCells) {
|
|
2539
|
+
if (!isGroupMember(id))
|
|
2540
|
+
continue;
|
|
2541
|
+
const it = engine.getItem(id);
|
|
2542
|
+
const g0 = startGeom.get(id);
|
|
2543
|
+
if (!it || !g0)
|
|
2544
|
+
continue;
|
|
2545
|
+
if (it.x === before.x && it.y === before.y && it.w === before.w && it.h === before.h)
|
|
2546
|
+
continue;
|
|
2547
|
+
groups.push({
|
|
2548
|
+
id,
|
|
2549
|
+
cellBefore: before,
|
|
2550
|
+
cellAfter: { x: it.x, y: it.y, w: it.w, h: it.h },
|
|
2551
|
+
frameBefore: { x: g0.pos.x, y: g0.pos.y, width: g0.size.width, height: g0.size.height },
|
|
2552
|
+
frameAfter: cellToRect(it, frame(), geom(), rows()),
|
|
2553
|
+
});
|
|
2554
|
+
}
|
|
2509
2555
|
engine.endGesture();
|
|
2510
2556
|
adoptedGhostId = null;
|
|
2511
2557
|
disarmGlideSoon();
|
|
2512
2558
|
syncPlaceholder();
|
|
2513
|
-
return { commands, cell, rect };
|
|
2559
|
+
return { commands, cell, rect, groups };
|
|
2514
2560
|
},
|
|
2515
2561
|
};
|
|
2516
2562
|
};
|
|
@@ -3136,69 +3182,374 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
3136
3182
|
const rect = api.container.getBoundingClientRect();
|
|
3137
3183
|
return ((_a = api.viewport) === null || _a === void 0 ? void 0 : _a.clientToWorld) ? api.viewport.clientToWorld(cx, cy, rect) : { x: cx - rect.left, y: cy - rect.top };
|
|
3138
3184
|
};
|
|
3139
|
-
const first = toWorld(ev.clientX, ev.clientY);
|
|
3140
3185
|
// The board holds the NEW group, not the bare page: it is adopted under the
|
|
3141
3186
|
// group's id, sized for the page plus its strip, taking the room under the
|
|
3142
3187
|
// pointer with its top edge at the pointer — the tab is what the pointer
|
|
3143
|
-
// holds, the page hangs below it.
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3188
|
+
// holds, the page hangs below it. NOT at the press: a ghost in the engine
|
|
3189
|
+
// costs rows, and on a fit board every tile squeezes the moment it enters,
|
|
3190
|
+
// so the ghost enters only when the pointer is over free board space, and
|
|
3191
|
+
// leaves whenever it is over a strip, a group or an edge. A full board that
|
|
3192
|
+
// refuses the ghost still lets the page join, reorder or split.
|
|
3193
|
+
let leg = null;
|
|
3194
|
+
const ensureLeg = (world) => {
|
|
3195
|
+
if (!leg)
|
|
3196
|
+
leg = adopt({ id: plan.arrivingId }, world, plan.size, { fit: 'shrink', anchor: 'top' });
|
|
3197
|
+
return leg;
|
|
3198
|
+
};
|
|
3199
|
+
const naturalSpan = (() => {
|
|
3200
|
+
const sp = sizeToSpan(plan.size.width, plan.size.height, frame(), geom(), rows());
|
|
3201
|
+
return { w: Math.max(1, Math.min(columns, sp.w)), h: Math.max(TEAR_OUT_MIN_ROWS, sp.h) };
|
|
3202
|
+
})();
|
|
3203
|
+
// A DOCKED group takes at most HALF the board — VS Code's edge drop splits
|
|
3204
|
+
// the area in two. A page as tall as the container it left would
|
|
3205
|
+
// otherwise shove the whole dashboard out of view. Half of the board as
|
|
3206
|
+
// it stood at the press, and of what the canvas shows, whichever is less.
|
|
3207
|
+
const dockSpan = (() => {
|
|
3208
|
+
const rows0 = rows();
|
|
3209
|
+
const rect = api.container.getBoundingClientRect();
|
|
3210
|
+
const rh = rowHeightFor(geom(), rows0) + gap;
|
|
3211
|
+
const visible = rect.height > 0 && rh > 0 ? Math.max(1, Math.floor((rect.height + gap) / rh)) : rows0;
|
|
3212
|
+
const halfRows = Math.max(TEAR_OUT_MIN_ROWS, Math.ceil(Math.min(rows0, visible) / 2));
|
|
3213
|
+
const halfCols = Math.max(1, Math.ceil(columns / 2));
|
|
3214
|
+
return { w: Math.min(naturalSpan.w, halfCols), h: Math.min(naturalSpan.h, halfRows) };
|
|
3215
|
+
})();
|
|
3147
3216
|
tearing = pageId;
|
|
3148
3217
|
const doc = (_a = api.container.ownerDocument) !== null && _a !== void 0 ? _a : document;
|
|
3149
3218
|
const chip = doc.createElement('div');
|
|
3150
3219
|
chip.className = 'axdb-drag-chip axdb-tab-chip';
|
|
3151
3220
|
chip.textContent = plan.label;
|
|
3152
3221
|
doc.body.appendChild(chip);
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3222
|
+
const moveChip = (cx, cy) => {
|
|
3223
|
+
chip.style.left = `${cx + 6}px`;
|
|
3224
|
+
chip.style.top = `${cy + 6}px`;
|
|
3225
|
+
};
|
|
3226
|
+
moveChip(ev.clientX, ev.clientY);
|
|
3227
|
+
armGlide();
|
|
3228
|
+
api.render();
|
|
3156
3229
|
const layer = htmlLayer();
|
|
3230
|
+
const area = (g) => {
|
|
3231
|
+
const sz = sizeOf(g);
|
|
3232
|
+
return sz.width * sz.height;
|
|
3233
|
+
};
|
|
3157
3234
|
const targets = plan.joinTargets
|
|
3158
3235
|
.map((id) => diagram.getGroup(id))
|
|
3159
|
-
.filter((g) => !!g && g.id !== fromGroupId)
|
|
3160
|
-
|
|
3161
|
-
|
|
3236
|
+
.filter((g) => !!g && g.id !== fromGroupId)
|
|
3237
|
+
.sort((a, b) => area(a) - area(b));
|
|
3238
|
+
// GEOMETRY MUST NOT MOVE WITH THE GHOST. On a fit board the rows squeeze
|
|
3239
|
+
// while the ghost occupies some and relax when it leaves, so a target's
|
|
3240
|
+
// frame changes with the very decision being made about it: a join makes
|
|
3241
|
+
// the ghost leave, the frame grows, and the same pointer now reads as a
|
|
3242
|
+
// split. The target and its frame are therefore anchored when the pointer
|
|
3243
|
+
// enters it and kept until it leaves; the board frame is the one at press.
|
|
3244
|
+
let anchor = null;
|
|
3245
|
+
const inRect = (r, wx, wy) => wx >= r.x && wx <= r.x + r.width && wy >= r.y && wy <= r.y + r.height;
|
|
3246
|
+
const targetAt = (wx, wy) => {
|
|
3247
|
+
var _a;
|
|
3248
|
+
if (anchor && inRect(anchor.frame, wx, wy))
|
|
3249
|
+
return anchor.g;
|
|
3250
|
+
const t = (_a = targets.find((x) => worldInsideGroup(x, wx, wy))) !== null && _a !== void 0 ? _a : null;
|
|
3251
|
+
if (t)
|
|
3252
|
+
leg === null || leg === void 0 ? void 0 : leg.leave(); // the ghost leaves first, so the frame anchored is the relaxed one
|
|
3253
|
+
anchor = t ? { g: t, frame: frameOfGroup(t), stripH: plan.stripHeight(t.id) } : null;
|
|
3254
|
+
return t;
|
|
3255
|
+
};
|
|
3256
|
+
// THE BANDS ARE ON THE SCREEN. Dockview measures its container edges on
|
|
3257
|
+
// the element, and so must we: the camera moves during the drag (the
|
|
3258
|
+
// board glides, the ghost adds rows a bounded scroll answers to), so a
|
|
3259
|
+
// band fixed in world space at the press drifts away from the edge the
|
|
3260
|
+
// user sees. The frame is mapped to client space through the live
|
|
3261
|
+
// camera and clipped to the canvas, so a scrolled board's bands sit at
|
|
3262
|
+
// its VISIBLE edges.
|
|
3263
|
+
const ROOT_TOP = 20;
|
|
3264
|
+
const ROOT_BOTTOM = 20;
|
|
3265
|
+
const ROOT_SIDE = 40;
|
|
3266
|
+
const visibleFrame = () => {
|
|
3267
|
+
const rect = api.container.getBoundingClientRect();
|
|
3268
|
+
const o = toWorld(rect.left, rect.top);
|
|
3269
|
+
const u = toWorld(rect.left + 100, rect.top + 100);
|
|
3270
|
+
const sx = 100 / (u.x - o.x || 100);
|
|
3271
|
+
const sy = 100 / (u.y - o.y || 100);
|
|
3272
|
+
const f = frame();
|
|
3273
|
+
const left = rect.left + (f.x - o.x) * sx;
|
|
3274
|
+
const top = rect.top + (f.y - o.y) * sy;
|
|
3275
|
+
const right = left + f.width * sx;
|
|
3276
|
+
const bottom = top + f.height * sy;
|
|
3277
|
+
if (rect.width <= 0 || rect.height <= 0)
|
|
3278
|
+
return { left, top, right, bottom }; // unlaid-out: nothing to clip to
|
|
3279
|
+
return { left: Math.max(left, rect.left), top: Math.max(top, rect.top), right: Math.min(right, rect.right), bottom: Math.min(bottom, rect.bottom) };
|
|
3280
|
+
};
|
|
3281
|
+
const rootAt = (cx, cy) => {
|
|
3282
|
+
// The bands reach a little OUTSIDE the frame too: a pointer that
|
|
3283
|
+
// overshoots the board's top edge by a few pixels means "the top".
|
|
3284
|
+
const v = visibleFrame();
|
|
3285
|
+
if (cx < v.left - ROOT_SIDE || cx > v.right + ROOT_SIDE || cy < v.top - ROOT_TOP || cy > v.bottom + ROOT_BOTTOM)
|
|
3286
|
+
return null;
|
|
3287
|
+
const rows = Math.max(TEAR_OUT_MIN_ROWS, rowsWithout(plan.arrivingId));
|
|
3288
|
+
const w = dockSpan.w;
|
|
3289
|
+
const h = dockSpan.h;
|
|
3290
|
+
if (cy - v.top <= ROOT_TOP)
|
|
3291
|
+
return { kind: 'root', side: 'top', cell: { x: 0, y: 0, w: columns, h } };
|
|
3292
|
+
if (cx - v.left <= ROOT_SIDE)
|
|
3293
|
+
return { kind: 'root', side: 'left', cell: { x: 0, y: 0, w, h: rows } };
|
|
3294
|
+
if (v.right - cx <= ROOT_SIDE)
|
|
3295
|
+
return { kind: 'root', side: 'right', cell: { x: Math.max(0, columns - w), y: 0, w, h: rows } };
|
|
3296
|
+
if (v.bottom - cy <= ROOT_BOTTOM)
|
|
3297
|
+
return { kind: 'root', side: 'bottom', cell: { x: 0, y: rows, w: columns, h } };
|
|
3298
|
+
return null;
|
|
3299
|
+
};
|
|
3300
|
+
const rowsWithout = (id) => {
|
|
3301
|
+
let r = 0;
|
|
3302
|
+
for (const it of engine.getItems())
|
|
3303
|
+
if (it.id !== id)
|
|
3304
|
+
r = Math.max(r, it.y + it.h);
|
|
3305
|
+
return r;
|
|
3306
|
+
};
|
|
3307
|
+
/** A split being previewed: the target shrunk to its half, to be restored when the pointer leaves. */
|
|
3308
|
+
let split = null;
|
|
3309
|
+
const halves = (target, side) => {
|
|
3310
|
+
// The cell to halve is the target's own — not the half it is already
|
|
3311
|
+
// shrunk to while a split is being previewed.
|
|
3312
|
+
const live = engine.getItem(target.id);
|
|
3313
|
+
const it = split && split.id === target.id ? split.before : live;
|
|
3314
|
+
if (!it || !live)
|
|
3315
|
+
return null; // a group on another board: no cell of ours to halve
|
|
3316
|
+
if (side === 'left' || side === 'right') {
|
|
3317
|
+
if (it.w < 2)
|
|
3318
|
+
return null;
|
|
3319
|
+
const a = Math.ceil(it.w / 2);
|
|
3320
|
+
const b = it.w - a;
|
|
3321
|
+
return side === 'right'
|
|
3322
|
+
? { keep: { x: it.x, y: it.y, w: a, h: it.h }, born: { x: it.x + a, y: it.y, w: b, h: it.h } }
|
|
3323
|
+
: { keep: { x: it.x + b, y: it.y, w: a, h: it.h }, born: { x: it.x, y: it.y, w: b, h: it.h } };
|
|
3324
|
+
}
|
|
3325
|
+
if (it.h < 2 * TEAR_OUT_MIN_ROWS)
|
|
3326
|
+
return null;
|
|
3327
|
+
const a = Math.ceil(it.h / 2);
|
|
3328
|
+
const b = it.h - a;
|
|
3329
|
+
return side === 'bottom'
|
|
3330
|
+
? { keep: { x: it.x, y: it.y, w: it.w, h: a }, born: { x: it.x, y: it.y + a, w: it.w, h: b } }
|
|
3331
|
+
: { keep: { x: it.x, y: it.y + b, w: it.w, h: a }, born: { x: it.x, y: it.y, w: it.w, h: b } };
|
|
3332
|
+
};
|
|
3333
|
+
const zoneAt = (cx, cy, world) => {
|
|
3334
|
+
const target = targetAt(world.x, world.y);
|
|
3335
|
+
const home = !target && worldInsideGroup(from, world.x, world.y);
|
|
3336
|
+
// A strip is the most precise target there is: anyone's wins outright.
|
|
3337
|
+
if (target) {
|
|
3338
|
+
const idx = plan.stripIndex(target.id, cx, cy);
|
|
3339
|
+
if (idx !== null)
|
|
3340
|
+
return { kind: 'strip', target, index: idx };
|
|
3341
|
+
}
|
|
3342
|
+
else if (home) {
|
|
3343
|
+
const idx = plan.stripIndex(fromGroupId, cx, cy);
|
|
3344
|
+
if (idx !== null)
|
|
3345
|
+
return { kind: 'reorder', index: idx };
|
|
3346
|
+
}
|
|
3347
|
+
// The board's own edges beat whatever sits against them — Dockview's
|
|
3348
|
+
// container edges over its groups — so a group at the top of the board
|
|
3349
|
+
// still leaves the top band to the board.
|
|
3350
|
+
const root = rootAt(cx, cy);
|
|
3351
|
+
if (root)
|
|
3352
|
+
return root;
|
|
3353
|
+
if (target) {
|
|
3354
|
+
const f = anchor && anchor.g === target ? anchor.frame : frameOfGroup(target);
|
|
3355
|
+
const stripH = anchor && anchor.g === target ? anchor.stripH : plan.stripHeight(target.id);
|
|
3356
|
+
const bodyH = Math.max(1, f.height - stripH);
|
|
3357
|
+
const rx = Math.min(1, Math.max(0, (world.x - f.x) / Math.max(1, f.width)));
|
|
3358
|
+
const ry = Math.min(1, Math.max(0, (world.y - f.y - stripH) / bodyH));
|
|
3359
|
+
if (rx >= 1 / 3 && rx <= 2 / 3 && ry >= 1 / 3 && ry <= 2 / 3)
|
|
3360
|
+
return { kind: 'join', target };
|
|
3361
|
+
const d = [['left', rx], ['right', 1 - rx], ['top', ry], ['bottom', 1 - ry]];
|
|
3362
|
+
d.sort((p, q) => p[1] - q[1]);
|
|
3363
|
+
const h = halves(target, d[0][0]);
|
|
3364
|
+
return h ? Object.assign({ kind: 'split', target, side: d[0][0] }, h) : { kind: 'join', target };
|
|
3365
|
+
}
|
|
3366
|
+
if (home)
|
|
3367
|
+
return { kind: 'home' };
|
|
3368
|
+
return { kind: 'board' };
|
|
3369
|
+
};
|
|
3370
|
+
const zoneKey = (z) => JSON.stringify(z, (k, v) => (k === 'target' ? v.id : v));
|
|
3371
|
+
// DOCKING PUSHES SECTIONS. A section is a locked tile so that a widget
|
|
3372
|
+
// never pushes it; a group docked against the board's edge is the one
|
|
3373
|
+
// gesture that must — everything below the top band moves down. For the
|
|
3374
|
+
// preview every member group is unlocked; they are relocked when the
|
|
3375
|
+
// pointer leaves the band, and their moved cells are committed with the
|
|
3376
|
+
// dock.
|
|
3377
|
+
let unlockedGroups = [];
|
|
3378
|
+
const unlockGroups = () => {
|
|
3379
|
+
if (unlockedGroups.length > 0)
|
|
3380
|
+
return;
|
|
3381
|
+
for (const it of engine.getItems()) {
|
|
3382
|
+
if (it.id !== plan.arrivingId && it.locked && isGroupMember(it.id)) {
|
|
3383
|
+
it.locked = false;
|
|
3384
|
+
unlockedGroups.push(it.id);
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
};
|
|
3388
|
+
const relockGroups = () => {
|
|
3389
|
+
for (const id of unlockedGroups) {
|
|
3390
|
+
const it = engine.getItem(id);
|
|
3391
|
+
if (it)
|
|
3392
|
+
it.locked = true;
|
|
3393
|
+
}
|
|
3394
|
+
unlockedGroups = [];
|
|
3395
|
+
};
|
|
3396
|
+
const groupCommands = (fin, except) => fin.groups.filter((g) => g.id !== except).map((g) => new SetGroupCellCommand(g.id, g.cellBefore, g.cellAfter, g.frameBefore, g.frameAfter));
|
|
3397
|
+
// A TOP DOCK INSERTS ROWS. The engine's push cascade resolves the band's
|
|
3398
|
+
// collisions one tile at a time and scrambles what stood beneath it (the
|
|
3399
|
+
// demo's KPI row came apart, two of its tiles under the chart). VS Code
|
|
3400
|
+
// shoves the whole area down intact, so after the ghost takes the band
|
|
3401
|
+
// every tile that stood at or below it is put back at its own column,
|
|
3402
|
+
// exactly the band's height lower — a translation of a layout without
|
|
3403
|
+
// overlaps has none — and comes back the same way when the pointer leaves.
|
|
3404
|
+
// The layout translated is the one from BEFORE the ghost entered (the
|
|
3405
|
+
// leg's baseline), not the engine's mid-gesture state: the adoption
|
|
3406
|
+
// itself pushes tiles about, and a snapshot taken after it would carry
|
|
3407
|
+
// that cascade into the insert.
|
|
3408
|
+
let inserted = null;
|
|
3409
|
+
const insertRows = (cell, l) => {
|
|
3410
|
+
if (cell.w < columns)
|
|
3411
|
+
return; // a side dock has no whole rows to insert
|
|
3412
|
+
inserted = l.baseline();
|
|
3413
|
+
for (const [id, c] of inserted) {
|
|
3414
|
+
const it = engine.getItem(id);
|
|
3415
|
+
if (!it)
|
|
3416
|
+
continue;
|
|
3417
|
+
it.x = c.x;
|
|
3418
|
+
it.y = c.y >= cell.y ? c.y + cell.h : c.y;
|
|
3419
|
+
}
|
|
3420
|
+
};
|
|
3421
|
+
const undoInsertRows = () => {
|
|
3422
|
+
if (!inserted)
|
|
3423
|
+
return;
|
|
3424
|
+
leg === null || leg === void 0 ? void 0 : leg.leave(); // the ghost frees its rows first
|
|
3425
|
+
for (const [id, c] of inserted) {
|
|
3426
|
+
const it = engine.getItem(id);
|
|
3427
|
+
if (it) {
|
|
3428
|
+
it.x = c.x;
|
|
3429
|
+
it.y = c.y;
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
inserted = null;
|
|
3433
|
+
project();
|
|
3434
|
+
};
|
|
3162
3435
|
let joinEl = null;
|
|
3163
|
-
const
|
|
3164
|
-
if (!layer
|
|
3165
|
-
return;
|
|
3436
|
+
const showOverlay = (r) => {
|
|
3437
|
+
if (!layer)
|
|
3438
|
+
return;
|
|
3166
3439
|
if (!joinEl) {
|
|
3167
3440
|
joinEl = doc.createElement('div');
|
|
3168
3441
|
joinEl.className = 'axdb-join';
|
|
3169
3442
|
layer.prepend(joinEl);
|
|
3170
3443
|
}
|
|
3171
|
-
|
|
3172
|
-
joinEl.style.
|
|
3173
|
-
joinEl.style.
|
|
3174
|
-
joinEl.style.
|
|
3175
|
-
joinEl.style.height = `${sz.height}px`;
|
|
3444
|
+
joinEl.style.left = `${r.x}px`;
|
|
3445
|
+
joinEl.style.top = `${r.y}px`;
|
|
3446
|
+
joinEl.style.width = `${r.width}px`;
|
|
3447
|
+
joinEl.style.height = `${r.height}px`;
|
|
3176
3448
|
};
|
|
3177
|
-
const
|
|
3449
|
+
const hideOverlay = () => {
|
|
3178
3450
|
joinEl === null || joinEl === void 0 ? void 0 : joinEl.remove();
|
|
3179
3451
|
joinEl = null;
|
|
3180
|
-
plan.markDrop(null, null);
|
|
3181
3452
|
};
|
|
3182
|
-
const
|
|
3183
|
-
if (
|
|
3453
|
+
const undoSplitPreview = () => {
|
|
3454
|
+
if (!split)
|
|
3184
3455
|
return;
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3456
|
+
const it = engine.getItem(split.id);
|
|
3457
|
+
leg === null || leg === void 0 ? void 0 : leg.leave(); // the born half must be free before the target grows back into it
|
|
3458
|
+
if (it) {
|
|
3459
|
+
if (it.x !== split.before.x || it.y !== split.before.y)
|
|
3460
|
+
engine.moveCheck(split.id, split.before.x, split.before.y, { gate: false });
|
|
3461
|
+
if (it.w !== split.before.w || it.h !== split.before.h)
|
|
3462
|
+
engine.resizeCheck(split.id, split.before.w, split.before.h);
|
|
3463
|
+
it.locked = true;
|
|
3193
3464
|
}
|
|
3465
|
+
split = null;
|
|
3466
|
+
project();
|
|
3194
3467
|
};
|
|
3195
|
-
const
|
|
3196
|
-
|
|
3197
|
-
|
|
3468
|
+
const previewSplit = (z, world) => {
|
|
3469
|
+
const it = engine.getItem(z.target.id);
|
|
3470
|
+
const l = ensureLeg(world);
|
|
3471
|
+
if (!it || !l)
|
|
3472
|
+
return false;
|
|
3473
|
+
split = { id: z.target.id, before: { x: it.x, y: it.y, w: it.w, h: it.h }, frameBefore: frameOfGroup(z.target), keep: z.keep };
|
|
3474
|
+
it.locked = false; // its own gesture for the moment: it may shrink and shift
|
|
3475
|
+
if (it.w !== z.keep.w || it.h !== z.keep.h)
|
|
3476
|
+
engine.resizeCheck(z.target.id, z.keep.w, z.keep.h);
|
|
3477
|
+
const now = engine.getItem(z.target.id);
|
|
3478
|
+
if (now && (now.x !== z.keep.x || now.y !== z.keep.y))
|
|
3479
|
+
engine.moveCheck(z.target.id, z.keep.x, z.keep.y, { gate: false });
|
|
3480
|
+
const ok = l.place(z.born);
|
|
3481
|
+
project();
|
|
3482
|
+
placeholder === null || placeholder === void 0 ? void 0 : placeholder.remove(); // the accent overlay says which half; the grey placeholder under it is noise
|
|
3483
|
+
placeholder = null;
|
|
3484
|
+
return ok;
|
|
3485
|
+
};
|
|
3486
|
+
let zone = { kind: 'board' };
|
|
3487
|
+
let key = zoneKey(zone);
|
|
3488
|
+
const applyZone = (z, world) => {
|
|
3489
|
+
var _a;
|
|
3490
|
+
const k = zoneKey(z);
|
|
3491
|
+
const same = k === key;
|
|
3492
|
+
key = k;
|
|
3493
|
+
zone = z;
|
|
3494
|
+
if (same) {
|
|
3495
|
+
if (z.kind === 'board')
|
|
3496
|
+
(_a = ensureLeg(world)) === null || _a === void 0 ? void 0 : _a.move(world);
|
|
3497
|
+
return;
|
|
3498
|
+
}
|
|
3499
|
+
undoSplitPreview();
|
|
3500
|
+
undoInsertRows();
|
|
3501
|
+
if (z.kind !== 'root')
|
|
3502
|
+
relockGroups();
|
|
3503
|
+
plan.markDrop(null, null);
|
|
3504
|
+
hideOverlay();
|
|
3505
|
+
switch (z.kind) {
|
|
3506
|
+
case 'strip':
|
|
3507
|
+
leg === null || leg === void 0 ? void 0 : leg.leave();
|
|
3508
|
+
showOverlay(frameOfGroup(z.target));
|
|
3509
|
+
plan.markDrop(z.target.id, z.index);
|
|
3510
|
+
break;
|
|
3511
|
+
case 'join':
|
|
3512
|
+
leg === null || leg === void 0 ? void 0 : leg.leave();
|
|
3513
|
+
showOverlay(frameOfGroup(z.target));
|
|
3514
|
+
break;
|
|
3515
|
+
case 'split': {
|
|
3516
|
+
if (!previewSplit(z, world)) {
|
|
3517
|
+
zone = { kind: 'join', target: z.target };
|
|
3518
|
+
key = zoneKey(zone);
|
|
3519
|
+
leg === null || leg === void 0 ? void 0 : leg.leave();
|
|
3520
|
+
showOverlay(frameOfGroup(z.target));
|
|
3521
|
+
break;
|
|
3522
|
+
}
|
|
3523
|
+
showOverlay(cellToRect(z.born, frame(), geom(), rows()));
|
|
3524
|
+
break;
|
|
3525
|
+
}
|
|
3526
|
+
case 'root': {
|
|
3527
|
+
const l = ensureLeg(world);
|
|
3528
|
+
if (l) {
|
|
3529
|
+
unlockGroups();
|
|
3530
|
+
l.place(z.cell);
|
|
3531
|
+
insertRows(z.cell, l);
|
|
3532
|
+
project();
|
|
3533
|
+
placeholder === null || placeholder === void 0 ? void 0 : placeholder.remove();
|
|
3534
|
+
placeholder = null;
|
|
3535
|
+
showOverlay(cellToRect(z.cell, frame(), geom(), rows()));
|
|
3536
|
+
}
|
|
3537
|
+
break;
|
|
3538
|
+
}
|
|
3539
|
+
case 'reorder':
|
|
3540
|
+
leg === null || leg === void 0 ? void 0 : leg.leave();
|
|
3541
|
+
plan.markDrop(fromGroupId, z.index);
|
|
3542
|
+
break;
|
|
3543
|
+
case 'home':
|
|
3544
|
+
leg === null || leg === void 0 ? void 0 : leg.leave();
|
|
3545
|
+
break;
|
|
3546
|
+
case 'board': {
|
|
3547
|
+
const l = ensureLeg(world);
|
|
3548
|
+
l === null || l === void 0 ? void 0 : l.enter(world);
|
|
3549
|
+
break;
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3198
3552
|
};
|
|
3199
|
-
moveChip(ev.clientX, ev.clientY);
|
|
3200
|
-
armGlide();
|
|
3201
|
-
api.render();
|
|
3202
3553
|
let last = { x: ev.clientX, y: ev.clientY };
|
|
3203
3554
|
const detach = () => {
|
|
3204
3555
|
window.removeEventListener('pointermove', onMove, true);
|
|
@@ -3206,85 +3557,104 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
3206
3557
|
window.removeEventListener('pointercancel', onCancel, true);
|
|
3207
3558
|
window.removeEventListener('keydown', onKey, true);
|
|
3208
3559
|
};
|
|
3209
|
-
let homeIndex = null;
|
|
3210
3560
|
const onMove = (e) => {
|
|
3211
3561
|
if (disposed)
|
|
3212
3562
|
return detach();
|
|
3213
3563
|
last = { x: e.clientX, y: e.clientY };
|
|
3214
3564
|
moveChip(e.clientX, e.clientY);
|
|
3215
3565
|
const world = toWorld(e.clientX, e.clientY);
|
|
3216
|
-
const
|
|
3217
|
-
|
|
3218
|
-
//
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
setOver(target !== null && target !== void 0 ? target : (homeIndex !== null ? from : null), world);
|
|
3222
|
-
chip.classList.toggle('axdb-out', (home && homeIndex === null) || (!target && !home && !worldInsideBoardGrace(world.x, world.y)));
|
|
3223
|
-
if (target)
|
|
3224
|
-
plan.markDrop(target.id, plan.dropIndex(target.id, e.clientX, e.clientY));
|
|
3225
|
-
else if (homeIndex !== null)
|
|
3226
|
-
plan.markDrop(fromGroupId, homeIndex);
|
|
3227
|
-
else if (!home)
|
|
3228
|
-
leg.move(world);
|
|
3566
|
+
const z = zoneAt(e.clientX, e.clientY, world);
|
|
3567
|
+
applyZone(z, world);
|
|
3568
|
+
// Dimmed = a release here does nothing: home, off the board, or a board
|
|
3569
|
+
// that refused the ghost (bounded and full).
|
|
3570
|
+
chip.classList.toggle('axdb-out', zone.kind === 'home' || (zone.kind === 'root' && !leg) || (zone.kind === 'board' && (!leg || !worldInsideBoardGrace(world.x, world.y))));
|
|
3229
3571
|
api.render();
|
|
3230
3572
|
};
|
|
3573
|
+
const done = (changed, kind) => {
|
|
3574
|
+
var _a;
|
|
3575
|
+
disarmGlideSoon();
|
|
3576
|
+
enforceBoardHeight();
|
|
3577
|
+
persistLayouts();
|
|
3578
|
+
api.renderNow();
|
|
3579
|
+
(_a = options.onGesture) === null || _a === void 0 ? void 0 : _a.call(options, { type: kind, kind: 'move', nodeId: pageId, changed });
|
|
3580
|
+
};
|
|
3231
3581
|
const finish = (commit) => {
|
|
3232
|
-
var _a, _b
|
|
3582
|
+
var _a, _b;
|
|
3233
3583
|
detach();
|
|
3234
3584
|
chip.remove();
|
|
3235
|
-
|
|
3585
|
+
hideOverlay();
|
|
3586
|
+
plan.markDrop(null, null);
|
|
3236
3587
|
tearing = null;
|
|
3237
3588
|
if (disposed)
|
|
3238
3589
|
return;
|
|
3239
3590
|
const world = toWorld(last.x, last.y);
|
|
3240
|
-
const
|
|
3241
|
-
if (
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3591
|
+
const z = commit ? zoneAt(last.x, last.y, world) : { kind: 'home' };
|
|
3592
|
+
if (z.kind !== 'root')
|
|
3593
|
+
undoInsertRows();
|
|
3594
|
+
if (!commit || z.kind === 'home' || (z.kind === 'root' && !ensureLeg(world)) || (z.kind === 'board' && (!ensureLeg(world) || !worldInsideBoardGrace(world.x, world.y)))) {
|
|
3595
|
+
undoSplitPreview();
|
|
3596
|
+
leg === null || leg === void 0 ? void 0 : leg.abort();
|
|
3597
|
+
relockGroups();
|
|
3598
|
+
done(false, 'cancel');
|
|
3599
|
+
return;
|
|
3600
|
+
}
|
|
3601
|
+
if (z.kind === 'reorder') {
|
|
3602
|
+
undoSplitPreview();
|
|
3603
|
+
leg === null || leg === void 0 ? void 0 : leg.abort();
|
|
3604
|
+
const cmds = plan.reorder(z.index);
|
|
3245
3605
|
const changed = cmds.length > 0 ? execute('Reorder tab', cmds) : false;
|
|
3246
|
-
|
|
3247
|
-
api.renderNow();
|
|
3248
|
-
(_a = options.onGesture) === null || _a === void 0 ? void 0 : _a.call(options, { type: changed ? 'commit' : 'cancel', kind: 'move', nodeId: pageId, changed });
|
|
3606
|
+
done(changed, changed ? 'commit' : 'cancel');
|
|
3249
3607
|
return;
|
|
3250
3608
|
}
|
|
3251
|
-
|
|
3252
|
-
if (target) {
|
|
3609
|
+
if (z.kind === 'strip' || z.kind === 'join') {
|
|
3253
3610
|
// JOIN: no cell on this board — the leg is abandoned (its displaced
|
|
3254
3611
|
// tiles are already home) and the page becomes the target's tab.
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
const
|
|
3259
|
-
|
|
3260
|
-
enforceBoardHeight();
|
|
3261
|
-
persistLayouts();
|
|
3262
|
-
api.renderNow();
|
|
3263
|
-
(_b = options.onGesture) === null || _b === void 0 ? void 0 : _b.call(options, { type: 'commit', kind: 'move', nodeId: pageId, changed });
|
|
3612
|
+
undoSplitPreview();
|
|
3613
|
+
const index = z.kind === 'strip' ? z.index : plan.dropIndex(z.target.id, last.x, last.y);
|
|
3614
|
+
leg === null || leg === void 0 ? void 0 : leg.abort();
|
|
3615
|
+
const planned = plan.join(z.target.id, index, group.id);
|
|
3616
|
+
done(execute('Move tab', [...planned.move, ...planned.collapse]), 'commit');
|
|
3264
3617
|
return;
|
|
3265
3618
|
}
|
|
3266
|
-
if (
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3619
|
+
if (z.kind === 'split') {
|
|
3620
|
+
// SPLIT: the target keeps one half of its cell, the page's new group the
|
|
3621
|
+
// other. The preview already holds both in the engine; commit them.
|
|
3622
|
+
if (!split || split.id !== z.target.id || zoneKey(zone) !== zoneKey(z))
|
|
3623
|
+
applyZone(z, world);
|
|
3624
|
+
const fin = (_a = leg === null || leg === void 0 ? void 0 : leg.finalize()) !== null && _a !== void 0 ? _a : null;
|
|
3625
|
+
const it = engine.getItem(z.target.id);
|
|
3626
|
+
const before = split;
|
|
3627
|
+
if (it)
|
|
3628
|
+
it.locked = true;
|
|
3629
|
+
split = null;
|
|
3630
|
+
if (!fin || !before || !it) {
|
|
3631
|
+
leg === null || leg === void 0 ? void 0 : leg.abort();
|
|
3632
|
+
done(false, 'cancel');
|
|
3633
|
+
return;
|
|
3634
|
+
}
|
|
3635
|
+
const keep = { x: it.x, y: it.y, w: it.w, h: it.h };
|
|
3636
|
+
const planned = plan.commands(fin.cell, fin.rect, group.id);
|
|
3637
|
+
const changed = execute('Split group', [
|
|
3638
|
+
...fin.commands,
|
|
3639
|
+
...groupCommands(fin, z.target.id),
|
|
3640
|
+
new SetGroupCellCommand(z.target.id, before.before, keep, before.frameBefore, cellToRect(keep, frame(), geom(), rows())),
|
|
3641
|
+
...planned.move,
|
|
3642
|
+
...planned.collapse,
|
|
3643
|
+
]);
|
|
3644
|
+
done(changed, 'commit');
|
|
3271
3645
|
return;
|
|
3272
3646
|
}
|
|
3273
|
-
|
|
3647
|
+
// ROOT DOCK or a free cell on the board: the leg sits where it will land.
|
|
3648
|
+
if (zoneKey(zone) !== zoneKey(z))
|
|
3649
|
+
applyZone(z, world);
|
|
3650
|
+
const fin = (_b = leg === null || leg === void 0 ? void 0 : leg.finalize()) !== null && _b !== void 0 ? _b : null;
|
|
3651
|
+
relockGroups();
|
|
3274
3652
|
if (!fin) {
|
|
3275
|
-
|
|
3276
|
-
api.renderNow();
|
|
3653
|
+
done(false, 'cancel');
|
|
3277
3654
|
return;
|
|
3278
3655
|
}
|
|
3279
3656
|
const planned = plan.commands(fin.cell, fin.rect, group.id);
|
|
3280
|
-
|
|
3281
|
-
// goes too (the plan carries the removal's settle).
|
|
3282
|
-
const changed = execute('Move tab out', [...fin.commands, ...planned.move, ...planned.collapse]);
|
|
3283
|
-
disarmGlideSoon();
|
|
3284
|
-
enforceBoardHeight();
|
|
3285
|
-
persistLayouts();
|
|
3286
|
-
api.renderNow();
|
|
3287
|
-
(_d = options.onGesture) === null || _d === void 0 ? void 0 : _d.call(options, { type: 'commit', kind: 'move', nodeId: pageId, changed });
|
|
3657
|
+
done(execute(z.kind === 'root' ? 'Dock tab' : 'Move tab out', [...fin.commands, ...groupCommands(fin), ...planned.move, ...planned.collapse]), 'commit');
|
|
3288
3658
|
};
|
|
3289
3659
|
const onUp = () => finish(true);
|
|
3290
3660
|
const onCancel = () => finish(false);
|