@grafloria/element 0.4.29 → 0.4.30
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
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
* round-trips with no extra work — same as every other kit.
|
|
44
44
|
*/
|
|
45
45
|
import { GroupModel, NodeModel, type GridColumnLayout } from '@grafloria/engine';
|
|
46
|
-
import { type DashboardGridHandle, type DashboardGridOptions, type DashboardResponsiveOptions } from './grid-binder.js';
|
|
46
|
+
import { type DashboardGridHandle, type TearOutPlan, type DashboardGridOptions, type DashboardResponsiveOptions } from './grid-binder.js';
|
|
47
47
|
import type { DragHandleOption } from './grid-binder.js';
|
|
48
48
|
import type { SplitNode } from './split-layout.js';
|
|
49
49
|
import type { SectionCaption } from './caption.js';
|
|
@@ -587,6 +587,16 @@ export interface DashboardHandleContext {
|
|
|
587
587
|
tabStrips: Map<string, HTMLElement>;
|
|
588
588
|
/** Set by finalize: re-lay a tab container's pages and repaint its strip. */
|
|
589
589
|
syncTabs?: (containerId: string) => void;
|
|
590
|
+
/**
|
|
591
|
+
* Tab-container runtime hooks (attachTabsRuntime): a container created or
|
|
592
|
+
* removed AFTER boot — a page torn out into a group of its own, a container
|
|
593
|
+
* closed because its last page left — registers and unregisters through
|
|
594
|
+
* these, on execute and on undo alike.
|
|
595
|
+
*/
|
|
596
|
+
attachTabsContainer?: (containerId: string) => void;
|
|
597
|
+
detachTabsContainer?: (containerId: string) => void;
|
|
598
|
+
/** The plan that makes a torn-out page a one-tab group of its own (the handle builder owns the specs and the registry). */
|
|
599
|
+
tearOutPlan?: (containerId: string, pageId: string) => TearOutPlan | null;
|
|
590
600
|
/** Set by finalize: the user's `onTabChange`, so the handle can fire it. */
|
|
591
601
|
onTabChange?: (containerId: string, pageId: string, viewId: string) => void;
|
|
592
602
|
/** Set by finalize: re-bind a VIEW's board under the given layout (setLayout). */
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
* round-trips with no extra work — same as every other kit.
|
|
44
44
|
*/
|
|
45
45
|
import { __awaiter, __rest } from "tslib";
|
|
46
|
-
import { BatchCommand, BringNodeToFrontCommand, Command, GroupModel, NodeModel, RemoveFromGroupCommand, RemoveGroupCommand, RemoveNodeCommand, SendNodeToBackCommand, } from '@grafloria/engine';
|
|
46
|
+
import { BatchCommand, BringNodeToFrontCommand, Command, GroupModel, NodeModel, AddGroupCommand, AddToGroupCommand, RemoveFromGroupCommand, RemoveGroupCommand, RemoveNodeCommand, SendNodeToBackCommand, } from '@grafloria/engine';
|
|
47
47
|
import { bindDashboardGrid, parentPeerOf, } from './grid-binder.js';
|
|
48
48
|
import { bindDashboardSplit, SPLIT_TREE_KEY } from './split-binder.js';
|
|
49
49
|
import { gridItemFromCell } from './grid-mapping.js';
|
|
@@ -341,7 +341,7 @@ function assignCells(widgets, columns) {
|
|
|
341
341
|
*/
|
|
342
342
|
const cssEscape = (v) => typeof CSS !== 'undefined' && CSS.escape ? CSS.escape(v) : v.replace(/"/g, '\\"');
|
|
343
343
|
export function attachTabsRuntime(ctx, model, container, handle) {
|
|
344
|
-
var _a
|
|
344
|
+
var _a;
|
|
345
345
|
// Pages come from LIVE membership, in the authored order, with anything that
|
|
346
346
|
// arrived later on the end. Reading the authored spec instead meant a page
|
|
347
347
|
// that LEFT the container kept its tab — a tab pointing at nothing, which is
|
|
@@ -371,14 +371,16 @@ export function attachTabsRuntime(ctx, model, container, handle) {
|
|
|
371
371
|
* with no pages has nothing to show.
|
|
372
372
|
*/
|
|
373
373
|
const tearOut = (containerId, pageId, ev) => {
|
|
374
|
-
var _a
|
|
374
|
+
var _a;
|
|
375
375
|
if (!container)
|
|
376
376
|
return false;
|
|
377
|
-
|
|
378
|
-
if (pages.length <= 1)
|
|
377
|
+
if (!pagesOf(containerId).some((p) => p.id === pageId))
|
|
379
378
|
return false;
|
|
380
|
-
const
|
|
381
|
-
|
|
379
|
+
const peer = parentPeerOf(container, containerId);
|
|
380
|
+
const plan = (_a = ctx.tearOutPlan) === null || _a === void 0 ? void 0 : _a.call(ctx, containerId, pageId);
|
|
381
|
+
if (!peer || !plan)
|
|
382
|
+
return false;
|
|
383
|
+
return peer.tearOutMember(pageId, containerId, ev, plan);
|
|
382
384
|
};
|
|
383
385
|
const paintStrip = (id, f, h, pages, active) => {
|
|
384
386
|
var _a, _b, _c;
|
|
@@ -456,13 +458,18 @@ export function attachTabsRuntime(ctx, model, container, handle) {
|
|
|
456
458
|
};
|
|
457
459
|
ctx.syncTabs = sync;
|
|
458
460
|
ctx.subscriptions = (_a = ctx.subscriptions) !== null && _a !== void 0 ? _a : [];
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
461
|
+
const subsOf = new Map();
|
|
462
|
+
const attach = (id) => {
|
|
463
|
+
var _a, _b, _c, _d, _e, _f;
|
|
464
|
+
const cg = (_a = ctx.boardGroups.get(id)) !== null && _a !== void 0 ? _a : model.getGroup(id);
|
|
465
|
+
if (!cg || subsOf.has(id))
|
|
466
|
+
return;
|
|
467
|
+
if (((_b = ctx.layoutOf.get(id)) !== null && _b !== void 0 ? _b : (_c = ctx.specById.get(id)) === null || _c === void 0 ? void 0 : _c.layout) !== 'tabs')
|
|
468
|
+
return;
|
|
462
469
|
const pages = pagesOf(id);
|
|
463
|
-
if (!ctx.activeTab.has(id)) {
|
|
470
|
+
if (!ctx.activeTab.has(id) && pages.length > 0) {
|
|
464
471
|
const meta = cg.getMetadata('containerWidget');
|
|
465
|
-
const want = (
|
|
472
|
+
const want = (_e = (_d = ctx.specById.get(id)) === null || _d === void 0 ? void 0 : _d.active) !== null && _e !== void 0 ? _e : meta === null || meta === void 0 ? void 0 : meta.active;
|
|
466
473
|
ctx.activeTab.set(id, want && pages.some((p) => p.id === want) ? want : pages[0].id);
|
|
467
474
|
if ((meta === null || meta === void 0 ? void 0 : meta.tabs) && !ctx.tabsOf.has(id))
|
|
468
475
|
ctx.tabsOf.set(id, meta.tabs);
|
|
@@ -470,12 +477,59 @@ export function attachTabsRuntime(ctx, model, container, handle) {
|
|
|
470
477
|
// Re-lay the pages whenever the parent moves or resizes the container —
|
|
471
478
|
// and whenever a page joins or leaves, so the strip always shows the pages
|
|
472
479
|
// that are actually there.
|
|
480
|
+
const offs = [];
|
|
473
481
|
for (const ev of ['bounds:changed', 'member:added', 'member:removed']) {
|
|
474
482
|
const off = cg.on(ev, (() => sync(id)));
|
|
475
483
|
if (typeof off === 'function')
|
|
476
|
-
|
|
484
|
+
offs.push(off);
|
|
477
485
|
}
|
|
486
|
+
subsOf.set(id, offs);
|
|
487
|
+
(_f = ctx.subscriptions) === null || _f === void 0 ? void 0 : _f.push(() => detach(id));
|
|
478
488
|
sync(id);
|
|
489
|
+
};
|
|
490
|
+
const detach = (id) => {
|
|
491
|
+
var _a, _b;
|
|
492
|
+
for (const off of (_a = subsOf.get(id)) !== null && _a !== void 0 ? _a : [])
|
|
493
|
+
off();
|
|
494
|
+
subsOf.delete(id);
|
|
495
|
+
(_b = ctx.tabStrips.get(id)) === null || _b === void 0 ? void 0 : _b.remove();
|
|
496
|
+
ctx.tabStrips.delete(id);
|
|
497
|
+
};
|
|
498
|
+
ctx.attachTabsContainer = attach;
|
|
499
|
+
ctx.detachTabsContainer = detach;
|
|
500
|
+
for (const id of ctx.boardGroups.keys())
|
|
501
|
+
if (isTabs(id))
|
|
502
|
+
attach(id);
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Steps that depend on each other's RESULT — create a group, then move a page
|
|
506
|
+
* into it, then place the group on the board — run as one history step. A
|
|
507
|
+
* batch checks every member's `canExecute` before running any of them, and
|
|
508
|
+
* the second step's precondition (the group exists) is what the first step
|
|
509
|
+
* creates, so as a batch the chain is refused. This runs the chain in order
|
|
510
|
+
* and reverses it on undo, judging nothing up front.
|
|
511
|
+
*/
|
|
512
|
+
class SequenceCommand extends Command {
|
|
513
|
+
constructor(name, steps) {
|
|
514
|
+
super(name);
|
|
515
|
+
this.steps = steps;
|
|
516
|
+
}
|
|
517
|
+
execute(context) {
|
|
518
|
+
for (const c of this.steps)
|
|
519
|
+
c.execute(context);
|
|
520
|
+
}
|
|
521
|
+
undo(context) {
|
|
522
|
+
for (let i = this.steps.length - 1; i >= 0; i--)
|
|
523
|
+
this.steps[i].undo(context);
|
|
524
|
+
}
|
|
525
|
+
canExecute() {
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
canUndo() {
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
serialize() {
|
|
532
|
+
return { id: this.id, name: this.name, timestamp: this.timestamp, data: { steps: this.steps.map((c) => c.serialize()) } };
|
|
479
533
|
}
|
|
480
534
|
}
|
|
481
535
|
/** `setCaption` as one history step: execute re-applies the value, undo the previous one. */
|
|
@@ -605,6 +659,160 @@ export function createDashboardHandle(ctx) {
|
|
|
605
659
|
entries.sort((p1, p2) => { var _a, _b, _c, _d; return ((_a = p1.y) !== null && _a !== void 0 ? _a : 0) - ((_b = p2.y) !== null && _b !== void 0 ? _b : 0) || ((_c = p1.x) !== null && _c !== void 0 ? _c : 0) - ((_d = p2.x) !== null && _d !== void 0 ? _d : 0); });
|
|
606
660
|
return entries;
|
|
607
661
|
};
|
|
662
|
+
/**
|
|
663
|
+
* TEARING A TAB OUT — what the page becomes. VS Code: a tab dragged out of
|
|
664
|
+
* its group makes a group of its own, still wearing its tab. So the board
|
|
665
|
+
* receives a NEW one-page tab container, `<page>__group`, built exactly as
|
|
666
|
+
* mountBoard() builds an authored one (slab cell in `gridItem`, spec fields
|
|
667
|
+
* in `containerWidget`, board metadata so fromDocument() rebinds it), the
|
|
668
|
+
* page moves into it, and the handle's own registry follows — on execute and
|
|
669
|
+
* on undo. A container left with no page closes, as VS Code closes a group
|
|
670
|
+
* whose last editor leaves; undo reopens it. The board that lands it adds
|
|
671
|
+
* its own displaced-tile commands around these.
|
|
672
|
+
*/
|
|
673
|
+
ctx.tearOutPlan = (containerId, pageId) => {
|
|
674
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
675
|
+
const model = (_a = ctx.apiRef) === null || _a === void 0 ? void 0 : _a.getModel();
|
|
676
|
+
const from = ctx.boardGroups.get(containerId);
|
|
677
|
+
const pg = (_b = ctx.boardGroups.get(pageId)) !== null && _b !== void 0 ? _b : model === null || model === void 0 ? void 0 : model.getGroup(pageId);
|
|
678
|
+
const fromSpec = specById.get(containerId);
|
|
679
|
+
const pageSpec = specById.get(pageId);
|
|
680
|
+
if (!model || !from || !pg || !pageSpec)
|
|
681
|
+
return null;
|
|
682
|
+
const viewId = (_c = ctx.viewOfBoard.get(containerId)) !== null && _c !== void 0 ? _c : ctx.active;
|
|
683
|
+
const W = `${pageId}__group`;
|
|
684
|
+
const tabsOpts = (_d = ctx.tabsOf.get(containerId)) !== null && _d !== void 0 ? _d : {};
|
|
685
|
+
const label = (_e = pageSpec.title) !== null && _e !== void 0 ? _e : pageId;
|
|
686
|
+
const size = { width: (_g = (_f = pg.size) === null || _f === void 0 ? void 0 : _f.width) !== null && _g !== void 0 ? _g : 0, height: ((_j = (_h = pg.size) === null || _h === void 0 ? void 0 : _h.height) !== null && _j !== void 0 ? _j : 0) + tabStripReserve(tabsOpts, 1) };
|
|
687
|
+
const remaining = [...((_k = from.members) !== null && _k !== void 0 ? _k : [])].filter((m) => m !== pageId && !!model.getGroup(m));
|
|
688
|
+
return {
|
|
689
|
+
arrivingId: W,
|
|
690
|
+
label,
|
|
691
|
+
size,
|
|
692
|
+
commands: (cell, rect, boardId) => {
|
|
693
|
+
var _a, _b, _c, _d, _e;
|
|
694
|
+
const fromMeta = ((_a = from.getMetadata('containerWidget')) !== null && _a !== void 0 ? _a : {});
|
|
695
|
+
const fromBoard = ((_b = from.getMetadata('dashboardBoard')) !== null && _b !== void 0 ? _b : {});
|
|
696
|
+
const g = new GroupModel({ id: W, name: label });
|
|
697
|
+
g.setMetadata('frameChrome', 'none');
|
|
698
|
+
g.setMetadata('gridItem', gridItemFromCell(cell));
|
|
699
|
+
g.setMetadata('containerWidget', Object.assign({ title: label, columns: (_c = fromMeta.columns) !== null && _c !== void 0 ? _c : cell.w, maxRows: (_d = fromMeta.maxRows) !== null && _d !== void 0 ? _d : cell.h, layout: 'tabs', active: pageId }, (Object.keys(tabsOpts).length ? { tabs: tabsOpts } : {})));
|
|
700
|
+
g.setMetadata('dashboardBoard', Object.assign(Object.assign({}, fromBoard), { layout: 'tabs' }));
|
|
701
|
+
g.position = { x: rect.x, y: rect.y };
|
|
702
|
+
g.size = { width: rect.width, height: rect.height, depth: 0 };
|
|
703
|
+
const wSpec = Object.assign(Object.assign({ id: W, title: label, layout: 'tabs', active: pageId }, (Object.keys(tabsOpts).length ? { tabs: tabsOpts } : {})), { columns: (_e = fromMeta.columns) !== null && _e !== void 0 ? _e : cell.w, widgets: [pageSpec] });
|
|
704
|
+
let slot = -1;
|
|
705
|
+
const born = {
|
|
706
|
+
register: () => {
|
|
707
|
+
var _a;
|
|
708
|
+
const live = model.getGroup(W);
|
|
709
|
+
if (!live)
|
|
710
|
+
return;
|
|
711
|
+
ctx.boardGroups.set(W, live);
|
|
712
|
+
ctx.layoutOf.set(W, 'tabs');
|
|
713
|
+
ctx.activeTab.set(W, pageId);
|
|
714
|
+
ctx.tabsOf.set(W, tabsOpts);
|
|
715
|
+
specById.set(W, wSpec);
|
|
716
|
+
ctx.viewOfBoard.set(W, viewId);
|
|
717
|
+
viewOfWidget.set(W, boardId);
|
|
718
|
+
viewOfWidget.set(pageId, W);
|
|
719
|
+
if (fromSpec === null || fromSpec === void 0 ? void 0 : fromSpec.widgets) {
|
|
720
|
+
const i = fromSpec.widgets.findIndex((p) => p.id === pageId);
|
|
721
|
+
if (i >= 0) {
|
|
722
|
+
slot = i;
|
|
723
|
+
fromSpec.widgets.splice(i, 1);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
const arr = ctx.boardWidgets.get(boardId);
|
|
727
|
+
if (arr && !arr.some((w) => w.id === W))
|
|
728
|
+
arr.push(wSpec);
|
|
729
|
+
ctx.boardWidgets.set(W, wSpec.widgets);
|
|
730
|
+
(_a = ctx.attachTabsContainer) === null || _a === void 0 ? void 0 : _a.call(ctx, W);
|
|
731
|
+
},
|
|
732
|
+
unregister: () => {
|
|
733
|
+
var _a;
|
|
734
|
+
(_a = ctx.detachTabsContainer) === null || _a === void 0 ? void 0 : _a.call(ctx, W);
|
|
735
|
+
ctx.boardGroups.delete(W);
|
|
736
|
+
ctx.layoutOf.delete(W);
|
|
737
|
+
ctx.activeTab.delete(W);
|
|
738
|
+
ctx.tabsOf.delete(W);
|
|
739
|
+
specById.delete(W);
|
|
740
|
+
ctx.viewOfBoard.delete(W);
|
|
741
|
+
viewOfWidget.delete(W);
|
|
742
|
+
viewOfWidget.set(pageId, containerId);
|
|
743
|
+
if ((fromSpec === null || fromSpec === void 0 ? void 0 : fromSpec.widgets) && !fromSpec.widgets.some((p) => p.id === pageId)) {
|
|
744
|
+
fromSpec.widgets.splice(slot < 0 ? fromSpec.widgets.length : Math.min(slot, fromSpec.widgets.length), 0, pageSpec);
|
|
745
|
+
}
|
|
746
|
+
const arr = ctx.boardWidgets.get(boardId);
|
|
747
|
+
if (arr) {
|
|
748
|
+
const i = arr.findIndex((w) => w.id === W);
|
|
749
|
+
if (i >= 0)
|
|
750
|
+
arr.splice(i, 1);
|
|
751
|
+
}
|
|
752
|
+
ctx.boardWidgets.delete(W);
|
|
753
|
+
},
|
|
754
|
+
};
|
|
755
|
+
const move = [
|
|
756
|
+
new SequenceCommand('Move tab out', [
|
|
757
|
+
new AddGroupCommand(g),
|
|
758
|
+
new RemoveFromGroupCommand(containerId, pageId),
|
|
759
|
+
new AddToGroupCommand(W, pageId),
|
|
760
|
+
new AddToGroupCommand(boardId, W),
|
|
761
|
+
new RegisterWidgetCommand(born, 'register'),
|
|
762
|
+
]),
|
|
763
|
+
];
|
|
764
|
+
if (remaining.length > 0)
|
|
765
|
+
return { move, collapse: [] };
|
|
766
|
+
// The last page left: the container closes. Its registry goes FIRST
|
|
767
|
+
// so that on undo it comes back AFTER the group and its slab do.
|
|
768
|
+
let fromSlot = -1;
|
|
769
|
+
const gone = {
|
|
770
|
+
register: () => {
|
|
771
|
+
var _a;
|
|
772
|
+
const live = model.getGroup(containerId);
|
|
773
|
+
if (!live)
|
|
774
|
+
return;
|
|
775
|
+
ctx.boardGroups.set(containerId, live);
|
|
776
|
+
ctx.layoutOf.set(containerId, 'tabs');
|
|
777
|
+
ctx.viewOfBoard.set(containerId, viewId);
|
|
778
|
+
if (fromSpec)
|
|
779
|
+
specById.set(containerId, fromSpec);
|
|
780
|
+
const arr = ctx.boardWidgets.get(boardId);
|
|
781
|
+
if (arr && fromSpec && !arr.some((w) => w.id === containerId)) {
|
|
782
|
+
arr.splice(fromSlot < 0 ? arr.length : Math.min(fromSlot, arr.length), 0, fromSpec);
|
|
783
|
+
}
|
|
784
|
+
(_a = ctx.attachTabsContainer) === null || _a === void 0 ? void 0 : _a.call(ctx, containerId);
|
|
785
|
+
},
|
|
786
|
+
unregister: () => {
|
|
787
|
+
var _a;
|
|
788
|
+
(_a = ctx.detachTabsContainer) === null || _a === void 0 ? void 0 : _a.call(ctx, containerId);
|
|
789
|
+
ctx.boardGroups.delete(containerId);
|
|
790
|
+
ctx.layoutOf.delete(containerId);
|
|
791
|
+
ctx.viewOfBoard.delete(containerId);
|
|
792
|
+
specById.delete(containerId);
|
|
793
|
+
const arr = ctx.boardWidgets.get(boardId);
|
|
794
|
+
if (arr) {
|
|
795
|
+
const i = arr.findIndex((w) => w.id === containerId);
|
|
796
|
+
if (i >= 0) {
|
|
797
|
+
fromSlot = i;
|
|
798
|
+
arr.splice(i, 1);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
},
|
|
802
|
+
};
|
|
803
|
+
return {
|
|
804
|
+
move,
|
|
805
|
+
collapse: [
|
|
806
|
+
new SequenceCommand('Close empty tab container', [
|
|
807
|
+
new RegisterWidgetCommand(gone, 'unregister'),
|
|
808
|
+
new RemoveFromGroupCommand(boardId, containerId),
|
|
809
|
+
new RemoveGroupCommand(containerId),
|
|
810
|
+
]),
|
|
811
|
+
],
|
|
812
|
+
};
|
|
813
|
+
},
|
|
814
|
+
};
|
|
815
|
+
};
|
|
608
816
|
/** Bookkeeping closures for one widget, shared by the add and remove
|
|
609
817
|
* commands (and applied synchronously by the handle — idempotent). */
|
|
610
818
|
const registryOf = (id, boardId, spec) => {
|
|
@@ -493,13 +493,64 @@ interface BinderPeer {
|
|
|
493
493
|
}, pxSize: {
|
|
494
494
|
width: number;
|
|
495
495
|
height: number;
|
|
496
|
-
}): AdoptedLeg | null;
|
|
496
|
+
}, opts?: AdoptOptions): AdoptedLeg | null;
|
|
497
497
|
/**
|
|
498
498
|
* Tear a tab page out of its container and onto THIS board, driven by the
|
|
499
499
|
* press that started on its tab. Answers false when the board will not take
|
|
500
500
|
* it, so the strip can leave the press alone.
|
|
501
501
|
*/
|
|
502
|
-
tearOutMember(pageId: string, fromGroupId: string,
|
|
502
|
+
tearOutMember(pageId: string, fromGroupId: string, ev: PointerEvent, plan: TearOutPlan): boolean;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* What a torn-out page becomes, decided by the dashboard handle (which owns
|
|
506
|
+
* the specs and the registry) and carried out by the board it lands on. VS
|
|
507
|
+
* Code's rule: a tab dragged out of its group makes a GROUP of its own — so
|
|
508
|
+
* the board receives a new one-page tab container, not the bare page, and the
|
|
509
|
+
* page keeps its tab.
|
|
510
|
+
*/
|
|
511
|
+
export interface TearOutPlan {
|
|
512
|
+
/** The id the board will hold: the new one-page tab container around the page. */
|
|
513
|
+
arrivingId: string;
|
|
514
|
+
/** What the chip that follows the pointer says. */
|
|
515
|
+
label: string;
|
|
516
|
+
/** Pixel size to arrive at — the page plus the strip the new group needs. */
|
|
517
|
+
size: {
|
|
518
|
+
width: number;
|
|
519
|
+
height: number;
|
|
520
|
+
};
|
|
521
|
+
/**
|
|
522
|
+
* The model and registry commands for a landing at `cell`/`rect` on
|
|
523
|
+
* `boardId`. `move` creates the group, moves the page in and registers it;
|
|
524
|
+
* `collapse` (possibly empty) removes the container the page left when it
|
|
525
|
+
* was its last page — VS Code closes an empty group.
|
|
526
|
+
*/
|
|
527
|
+
commands(cell: {
|
|
528
|
+
x: number;
|
|
529
|
+
y: number;
|
|
530
|
+
w: number;
|
|
531
|
+
h: number;
|
|
532
|
+
}, rect: {
|
|
533
|
+
x: number;
|
|
534
|
+
y: number;
|
|
535
|
+
width: number;
|
|
536
|
+
height: number;
|
|
537
|
+
}, boardId: string): {
|
|
538
|
+
move: Command[];
|
|
539
|
+
collapse: Command[];
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
/** A torn-out page never arrives shorter than this: a strip with no room under it is not a group. */
|
|
543
|
+
export declare const TEAR_OUT_MIN_ROWS = 2;
|
|
544
|
+
interface AdoptOptions {
|
|
545
|
+
/**
|
|
546
|
+
* 'shrink': take the cell under the pointer at the height that FITS there
|
|
547
|
+
* (down to TEAR_OUT_MIN_ROWS) before looking for another row — a torn-out
|
|
548
|
+
* page takes the room where it lands, the way a torn-out editor does. The
|
|
549
|
+
* default keeps the tile's size and finds the nearest cell that takes it.
|
|
550
|
+
*/
|
|
551
|
+
fit?: 'shrink';
|
|
552
|
+
/** Anchor the tile's TOP edge at the pointer instead of centring it (the pointer holds a tab, the page hangs below it). */
|
|
553
|
+
anchor?: 'top';
|
|
503
554
|
}
|
|
504
555
|
interface AdoptedLeg {
|
|
505
556
|
groupId: string;
|
|
@@ -154,6 +154,8 @@ export function pressOnDragHandle(sel, target, hostEl, clientX, clientY) {
|
|
|
154
154
|
}
|
|
155
155
|
/** The caption strip of a host with no kit header, px from the card's top. */
|
|
156
156
|
export const CAPTION_BAND = 28;
|
|
157
|
+
/** A torn-out page never arrives shorter than this: a strip with no room under it is not a group. */
|
|
158
|
+
export const TEAR_OUT_MIN_ROWS = 2;
|
|
157
159
|
/**
|
|
158
160
|
* Binders on the same canvas find each other here. A WeakMap: a canvas that
|
|
159
161
|
* is gone takes its (empty) peer set with it — the strong Map it replaced kept
|
|
@@ -2130,6 +2132,8 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
2130
2132
|
}
|
|
2131
2133
|
return best;
|
|
2132
2134
|
};
|
|
2135
|
+
/** `handle.planRemoval` for local use before the handle object exists. */
|
|
2136
|
+
const planRemovalOf = (id) => handle.planRemoval(id);
|
|
2133
2137
|
/** This binder's side of an adoption: enter gateless, then live-push. */
|
|
2134
2138
|
/**
|
|
2135
2139
|
* Put an ARRIVING tile as close to the pointer as the board allows. The
|
|
@@ -2139,37 +2143,40 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
2139
2143
|
* I put it". Slide along the row instead, nearest first, so a tile that
|
|
2140
2144
|
* cannot take the cell under the pointer still lands beside it.
|
|
2141
2145
|
*/
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2146
|
+
/**
|
|
2147
|
+
* Slide along ONE row, nearest column first. `changed:false` means BOTH
|
|
2148
|
+
* "refused" and "it is already there", and reading the second as the first
|
|
2149
|
+
* made the search walk straight past the right answer: parked on the nearest
|
|
2150
|
+
* legal cell, the next pointer move re-tried the wanted cell (refused),
|
|
2151
|
+
* re-tried the cell it was ON (no change, read as refused) and then took a
|
|
2152
|
+
* FARTHER one — so the tile oscillated for the whole drag and settled
|
|
2153
|
+
* wherever the last swing left it. Ask where the tile IS, not whether the
|
|
2154
|
+
* call moved it.
|
|
2155
|
+
*/
|
|
2156
|
+
const placeOnRow = (id, x, y, w) => {
|
|
2157
|
+
const at = (cx) => {
|
|
2151
2158
|
const i = engine.getItem(id);
|
|
2152
|
-
return !!i && i.x === cx && i.y ===
|
|
2159
|
+
return !!i && i.x === cx && i.y === y;
|
|
2153
2160
|
};
|
|
2154
2161
|
const maxX = Math.max(0, columns - w);
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
for (
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
return true;
|
|
2168
|
-
}
|
|
2162
|
+
if (at(x))
|
|
2163
|
+
return true;
|
|
2164
|
+
if (engine.moveCheck(id, x, y, { gate: false }).changed)
|
|
2165
|
+
return true;
|
|
2166
|
+
for (let d = 1; d <= columns; d++) {
|
|
2167
|
+
for (const cx of [x - d, x + d]) {
|
|
2168
|
+
if (cx < 0 || cx > maxX)
|
|
2169
|
+
continue;
|
|
2170
|
+
if (at(cx))
|
|
2171
|
+
return true;
|
|
2172
|
+
if (engine.moveCheck(id, cx, y, { gate: false }).changed)
|
|
2173
|
+
return true;
|
|
2169
2174
|
}
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2175
|
+
}
|
|
2176
|
+
return false;
|
|
2177
|
+
};
|
|
2178
|
+
const placeNear = (id, x, y, w) => {
|
|
2179
|
+
if (placeOnRow(id, x, y, w))
|
|
2173
2180
|
return true;
|
|
2174
2181
|
// EVERY column on that row refused — which is what a locked section
|
|
2175
2182
|
// spanning the full width does, and there are plenty of those. Sliding
|
|
@@ -2181,15 +2188,70 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
2181
2188
|
for (const cy of [y - d, y + d]) {
|
|
2182
2189
|
if (cy < 0)
|
|
2183
2190
|
continue;
|
|
2184
|
-
if (
|
|
2191
|
+
if (placeOnRow(id, x, cy, w))
|
|
2185
2192
|
return true;
|
|
2186
2193
|
}
|
|
2187
2194
|
}
|
|
2188
2195
|
return false;
|
|
2189
2196
|
};
|
|
2197
|
+
/**
|
|
2198
|
+
* Rows free at (x, y) across `w` columns before the nearest LOCKED tile
|
|
2199
|
+
* below — a section is a locked tile nothing can push, so that is the hard
|
|
2200
|
+
* ceiling on what can land there — capped at the natural height. 0 when the
|
|
2201
|
+
* row itself lies under a locked tile.
|
|
2202
|
+
*/
|
|
2203
|
+
const fitHeightAt = (id, x, y, w, hNatural) => {
|
|
2204
|
+
let limit = hNatural;
|
|
2205
|
+
for (const it of engine.getItems()) {
|
|
2206
|
+
if (it.id === id || !it.locked)
|
|
2207
|
+
continue;
|
|
2208
|
+
if (!(it.x < x + w && x < it.x + it.w))
|
|
2209
|
+
continue;
|
|
2210
|
+
if (it.y <= y && it.y + it.h > y)
|
|
2211
|
+
return 0;
|
|
2212
|
+
if (it.y > y)
|
|
2213
|
+
limit = Math.min(limit, it.y - y);
|
|
2214
|
+
}
|
|
2215
|
+
const b = bound();
|
|
2216
|
+
if (b !== undefined)
|
|
2217
|
+
limit = Math.min(limit, Math.max(0, b - y));
|
|
2218
|
+
return limit;
|
|
2219
|
+
};
|
|
2220
|
+
/**
|
|
2221
|
+
* A TEAR-OUT takes the cell under the pointer at the height that fits there
|
|
2222
|
+
* — a page dragged out of a full-height panel onto a busy board would
|
|
2223
|
+
* otherwise have exactly one legal cell, below the lowest section, and the
|
|
2224
|
+
* drop would land a screen away from the pointer with its placeholder out of
|
|
2225
|
+
* sight. Only when nothing fits there does it fall back to the nearest row
|
|
2226
|
+
* at its natural height.
|
|
2227
|
+
*/
|
|
2228
|
+
const placeFitting = (id, x, y, w, hNatural) => {
|
|
2229
|
+
var _a, _b;
|
|
2230
|
+
const it = engine.getItem(id);
|
|
2231
|
+
if (!it)
|
|
2232
|
+
return false;
|
|
2233
|
+
const hFit = fitHeightAt(id, x, y, w, hNatural);
|
|
2234
|
+
if (hFit >= TEAR_OUT_MIN_ROWS) {
|
|
2235
|
+
if (it.h !== hFit)
|
|
2236
|
+
engine.resizeCheck(id, w, hFit);
|
|
2237
|
+
if (((_a = engine.getItem(id)) === null || _a === void 0 ? void 0 : _a.h) === hFit && placeOnRow(id, x, y, w))
|
|
2238
|
+
return true;
|
|
2239
|
+
}
|
|
2240
|
+
const now = engine.getItem(id);
|
|
2241
|
+
if (now && now.h !== hNatural) {
|
|
2242
|
+
// Growing back where it sits may be refused; the entry row at the bottom
|
|
2243
|
+
// always has room, so grow there and let placeNear bring it up.
|
|
2244
|
+
engine.resizeCheck(id, w, hNatural);
|
|
2245
|
+
if (((_b = engine.getItem(id)) === null || _b === void 0 ? void 0 : _b.h) !== hNatural) {
|
|
2246
|
+
engine.moveCheck(id, 0, engine.rows(), { gate: false });
|
|
2247
|
+
engine.resizeCheck(id, w, hNatural);
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
return placeNear(id, x, y, w);
|
|
2251
|
+
};
|
|
2190
2252
|
const adopt = (
|
|
2191
2253
|
// Only the id is used: a tab page arriving here is a GROUP, not a node.
|
|
2192
|
-
node, world, pxSize) => {
|
|
2254
|
+
node, world, pxSize, opts = {}) => {
|
|
2193
2255
|
if (disposed)
|
|
2194
2256
|
return null;
|
|
2195
2257
|
const f = frame();
|
|
@@ -2224,9 +2286,16 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
2224
2286
|
}
|
|
2225
2287
|
}
|
|
2226
2288
|
adoptedGhostId = node.id;
|
|
2227
|
-
const
|
|
2228
|
-
const
|
|
2229
|
-
|
|
2289
|
+
const hNatural = span.h;
|
|
2290
|
+
const wantedCell = (wx, wy, itemW, itemH) => {
|
|
2291
|
+
const tl = centredTopLeft(wx, wy, { w: itemW, h: opts.anchor === 'top' ? 0 : itemH });
|
|
2292
|
+
return pointToCell(tl.x, tl.y, frame(), geom(), rows(), itemW);
|
|
2293
|
+
};
|
|
2294
|
+
const place = (cell, itemW) => opts.fit === 'shrink' ? placeFitting(node.id, cell.x, cell.y, itemW, hNatural) : placeNear(node.id, cell.x, cell.y, itemW);
|
|
2295
|
+
let lastWant = null;
|
|
2296
|
+
const cell0 = wantedCell(world.x, world.y, span.w, span.h);
|
|
2297
|
+
lastWant = cell0;
|
|
2298
|
+
place(cell0, span.w);
|
|
2230
2299
|
armGlide();
|
|
2231
2300
|
project();
|
|
2232
2301
|
syncPlaceholder();
|
|
@@ -2236,9 +2305,12 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
2236
2305
|
const item = engine.getItem(node.id);
|
|
2237
2306
|
if (!item)
|
|
2238
2307
|
return;
|
|
2239
|
-
const
|
|
2240
|
-
|
|
2241
|
-
if (
|
|
2308
|
+
const cell = wantedCell(w.x, w.y, item.w, opts.fit === 'shrink' ? hNatural : item.h);
|
|
2309
|
+
// The search is worth running once per wanted cell, not per pixel.
|
|
2310
|
+
if (lastWant && lastWant.x === cell.x && lastWant.y === cell.y)
|
|
2311
|
+
return;
|
|
2312
|
+
lastWant = cell;
|
|
2313
|
+
if (place(cell, item.w))
|
|
2242
2314
|
project();
|
|
2243
2315
|
syncPlaceholder();
|
|
2244
2316
|
},
|
|
@@ -2271,7 +2343,7 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
2271
2343
|
};
|
|
2272
2344
|
const selfPeer = {
|
|
2273
2345
|
group,
|
|
2274
|
-
tearOutMember: (pageId, fromGroupId,
|
|
2346
|
+
tearOutMember: (pageId, fromGroupId, ev, plan) => beginTearOut(pageId, fromGroupId, ev, plan),
|
|
2275
2347
|
clearSelection: () => {
|
|
2276
2348
|
var _a;
|
|
2277
2349
|
if (selectedId === undefined)
|
|
@@ -2822,36 +2894,31 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
2822
2894
|
* Like VS Code, the page itself does not follow the pointer; a chip carrying
|
|
2823
2895
|
* the tab's label does, and the placeholder shows where the drop will land.
|
|
2824
2896
|
*/
|
|
2825
|
-
const beginTearOut = (pageId, fromGroupId,
|
|
2826
|
-
var _a
|
|
2897
|
+
const beginTearOut = (pageId, fromGroupId, ev, plan) => {
|
|
2898
|
+
var _a;
|
|
2827
2899
|
if (disposed || gesture || slabGesture || isStatic || tearing)
|
|
2828
2900
|
return false;
|
|
2829
|
-
const pg = diagram.getGroup(pageId);
|
|
2830
2901
|
const from = diagram.getGroup(fromGroupId);
|
|
2831
|
-
if (!
|
|
2902
|
+
if (!from || !diagram.getGroup(pageId) || !engine.getItem(fromGroupId))
|
|
2832
2903
|
return false;
|
|
2833
|
-
const size = sizeOf(pg);
|
|
2834
|
-
const frameBefore = frameOfGroup(pg);
|
|
2835
|
-
const cellBefore = (_b = cellFromGridItem((_a = pg.getMetadata) === null || _a === void 0 ? void 0 : _a.call(pg, 'gridItem'))) !== null && _b !== void 0 ? _b : {
|
|
2836
|
-
x: 0,
|
|
2837
|
-
y: 0,
|
|
2838
|
-
w: 1,
|
|
2839
|
-
h: 1,
|
|
2840
|
-
};
|
|
2841
2904
|
const toWorld = (cx, cy) => {
|
|
2842
2905
|
var _a;
|
|
2843
2906
|
const rect = api.container.getBoundingClientRect();
|
|
2844
2907
|
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 };
|
|
2845
2908
|
};
|
|
2846
2909
|
const first = toWorld(ev.clientX, ev.clientY);
|
|
2847
|
-
|
|
2910
|
+
// The board holds the NEW group, not the bare page: it is adopted under the
|
|
2911
|
+
// group's id, sized for the page plus its strip, taking the room under the
|
|
2912
|
+
// pointer with its top edge at the pointer — the tab is what the pointer
|
|
2913
|
+
// holds, the page hangs below it.
|
|
2914
|
+
const leg = adopt({ id: plan.arrivingId }, first, plan.size, { fit: 'shrink', anchor: 'top' });
|
|
2848
2915
|
if (!leg)
|
|
2849
2916
|
return false;
|
|
2850
2917
|
tearing = pageId;
|
|
2851
|
-
const doc = (
|
|
2918
|
+
const doc = (_a = api.container.ownerDocument) !== null && _a !== void 0 ? _a : document;
|
|
2852
2919
|
const chip = doc.createElement('div');
|
|
2853
2920
|
chip.className = 'axdb-drag-chip axdb-tab-chip';
|
|
2854
|
-
chip.textContent = label;
|
|
2921
|
+
chip.textContent = plan.label;
|
|
2855
2922
|
doc.body.appendChild(chip);
|
|
2856
2923
|
const moveChip = (cx, cy) => {
|
|
2857
2924
|
chip.style.left = `${cx + 6}px`;
|
|
@@ -2903,12 +2970,12 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
2903
2970
|
api.renderNow();
|
|
2904
2971
|
return;
|
|
2905
2972
|
}
|
|
2906
|
-
const
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
]);
|
|
2973
|
+
const planned = plan.commands(fin.cell, fin.rect, group.id);
|
|
2974
|
+
// The container the page left may have been holding its last page: it
|
|
2975
|
+
// goes too, and the tiles around its slab settle the way a removal
|
|
2976
|
+
// settles them (VS Code closes a group whose last editor leaves).
|
|
2977
|
+
const collapse = planned.collapse.length > 0 ? [...planRemovalOf(fromGroupId), ...planned.collapse] : [];
|
|
2978
|
+
const changed = execute('Move tab out', [...fin.commands, ...planned.move, ...collapse]);
|
|
2912
2979
|
disarmGlideSoon();
|
|
2913
2980
|
enforceBoardHeight();
|
|
2914
2981
|
persistLayouts();
|
|
@@ -3296,7 +3363,18 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
3296
3363
|
const it = engine.getItem(id);
|
|
3297
3364
|
if (!it)
|
|
3298
3365
|
return [];
|
|
3299
|
-
|
|
3366
|
+
// A float:false engine PACKS as it is built, in ARRAY order — so a tile
|
|
3367
|
+
// that entered the engine last (one just adopted by a drop) is packed
|
|
3368
|
+
// last, and everything above it in the array floats up through the room
|
|
3369
|
+
// it occupies. The plan then "moved" tiles the removal never touched and
|
|
3370
|
+
// undid the drop's own push (L64: B back onto the cell the torn-out
|
|
3371
|
+
// group had just taken, and the group auto-placed into the hole the
|
|
3372
|
+
// closed container left). Packing in row-then-column order is gravity,
|
|
3373
|
+
// whatever order the tiles arrived in.
|
|
3374
|
+
const clone = new GridPackEngine(engine
|
|
3375
|
+
.getItems()
|
|
3376
|
+
.map((i) => (Object.assign({}, i)))
|
|
3377
|
+
.sort((a, b) => a.y - b.y || a.x - b.x), { columns, float });
|
|
3300
3378
|
clone.remove(id);
|
|
3301
3379
|
const f = frame();
|
|
3302
3380
|
const g = geom();
|
|
@@ -1253,7 +1253,7 @@ export function bindDashboardSplit(api, group, options = {}) {
|
|
|
1253
1253
|
resizeMemberBy: () => ({ changed: false }),
|
|
1254
1254
|
// A split board has no cells to drop a torn-out page into: it refuses,
|
|
1255
1255
|
// and the press stays a plain tab click.
|
|
1256
|
-
tearOutMember: () => false,
|
|
1256
|
+
tearOutMember: (_pageId, _fromGroupId, _ev, _plan) => false,
|
|
1257
1257
|
containsWorld: (x, y) => worldInsideBoard(x, y),
|
|
1258
1258
|
containsWorldExtended: (x, y) => worldInsideBoard(x, y),
|
|
1259
1259
|
frameArea: () => {
|