@grafloria/element 0.4.28 → 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 +1 -1
- package/src/index.d.ts +1 -1
- package/src/index.js +1 -1
- package/src/lib/dashboard-kit/dashboard.d.ts +11 -1
- package/src/lib/dashboard-kit/dashboard.js +266 -17
- package/src/lib/dashboard-kit/grid-binder.d.ts +61 -2
- package/src/lib/dashboard-kit/grid-binder.js +259 -10
- package/src/lib/dashboard-kit/index.d.ts +1 -1
- package/src/lib/dashboard-kit/index.js +1 -1
- package/src/lib/dashboard-kit/split-binder.js +3 -0
- package/src/lib/dashboard-kit/styles.js +21 -0
- package/src/lib/dashboard-kit/tabs.d.ts +14 -1
- package/src/lib/dashboard-kit/tabs.js +51 -1
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -79,7 +79,7 @@ export type { JoinColumn, JoinEnd, MatchTier, JoinGuidanceApi, JoinGuidanceOptio
|
|
|
79
79
|
export type { ErColumn, ErEntitySpec, ErRelationshipSpec, ErCardinality, ErSide, ErDiagramOptions, UmlClassSpec, UmlRelationshipSpec, UmlRelationKind, UmlSide, UmlDiagramOptions, ErEntityDelta, UmlClassDelta, CardEditingHandle, } from './lib/diagram-kit/index.js';
|
|
80
80
|
export { erTable, umlClass, erTables, umlClasses, CardHandle, ErTable, ErField, ErColumnList, UmlClass, UmlMemberList, } from './lib/diagram-kit/index.js';
|
|
81
81
|
export type { HandleApi } from './lib/diagram-kit/index.js';
|
|
82
|
-
export { bindDashboardGrid, paintTabStrip, tabStripReserve, tabStripKey, TAB_STRIP_HEIGHT, normalizeCaption, captionReserve, captionBandHeight, paintCaptionBand, CAPTION_HEIGHT, CAPTION_HEIGHT_SUBTITLE, CAPTION_HEIGHT_TIGHT, CAPTION_PASS_THROUGH, rowHeightFor, boardHeightFor, columnUnitFor, cellToRect, pointToCell, sizeToSpan, gridItemFromCell, cellFromGridItem, buildCommitCommands, ensureDashboardKitStyles, DASHBOARD_KIT_STYLE_ID, dashboard, defaultWidgetRenderer, renderKpiWidget, renderLineWidget, renderBarWidget, renderDonutWidget, renderFunnelWidget, renderTableWidget, BUILT_IN_WIDGET_KINDS, } from './lib/dashboard-kit/index.js';
|
|
82
|
+
export { bindDashboardGrid, paintTabStrip, tabStripReserve, tabStripKey, TAB_STRIP_HEIGHT, TAB_DRAG_THRESHOLD, normalizeCaption, captionReserve, captionBandHeight, paintCaptionBand, CAPTION_HEIGHT, CAPTION_HEIGHT_SUBTITLE, CAPTION_HEIGHT_TIGHT, CAPTION_PASS_THROUGH, rowHeightFor, boardHeightFor, columnUnitFor, cellToRect, pointToCell, sizeToSpan, gridItemFromCell, cellFromGridItem, buildCommitCommands, ensureDashboardKitStyles, DASHBOARD_KIT_STYLE_ID, dashboard, defaultWidgetRenderer, renderKpiWidget, renderLineWidget, renderBarWidget, renderDonutWidget, renderFunnelWidget, renderTableWidget, BUILT_IN_WIDGET_KINDS, } from './lib/dashboard-kit/index.js';
|
|
83
83
|
export type { DashboardGridApi, DashboardGridOptions, DashboardGridHandle, CellRect, WorldRect, DashboardGridGeometry, TileDelta, DashboardOptions, DashboardSpec, DashboardSnapshot, DragHandleOption, DragGripOptions, DashboardHandle, DashboardViewSpec, DashboardWidgetSpec, SectionCaption, SectionCaptionOptions, TabsOptions, TabPage, SectionCaptionAction, SectionCaptionFont, WidgetHandle, WidgetRenderer, KpiWidgetData, LineWidgetData, LineSeries, BarWidgetData, DonutWidgetData, FunnelWidgetData, TableWidgetData, } from './lib/dashboard-kit/index.js';
|
|
84
84
|
export { bindStencilPalette, bindShapeDataPanel, ensureStencilKitStyles } from './lib/stencil-kit/index.js';
|
|
85
85
|
export type { StencilPaletteApi, StencilPaletteOptions, StencilPaletteHandle, ShapeDataPanelApi, ShapeDataPanelOptions, ShapeDataPanelHandle, } from './lib/stencil-kit/index.js';
|
package/src/index.js
CHANGED
|
@@ -230,7 +230,7 @@ export { erTable, umlClass, erTables, umlClasses, CardHandle, ErTable, ErField,
|
|
|
230
230
|
// GridItemConfig. Element-local code, so the Phase-0 wholesale re-export
|
|
231
231
|
// below does NOT cover it — this curated block is its only door.
|
|
232
232
|
// ===========================================================================
|
|
233
|
-
export { bindDashboardGrid, paintTabStrip, tabStripReserve, tabStripKey, TAB_STRIP_HEIGHT, normalizeCaption, captionReserve, captionBandHeight, paintCaptionBand, CAPTION_HEIGHT, CAPTION_HEIGHT_SUBTITLE, CAPTION_HEIGHT_TIGHT, CAPTION_PASS_THROUGH, rowHeightFor, boardHeightFor, columnUnitFor, cellToRect, pointToCell, sizeToSpan, gridItemFromCell, cellFromGridItem, buildCommitCommands, ensureDashboardKitStyles, DASHBOARD_KIT_STYLE_ID,
|
|
233
|
+
export { bindDashboardGrid, paintTabStrip, tabStripReserve, tabStripKey, TAB_STRIP_HEIGHT, TAB_DRAG_THRESHOLD, normalizeCaption, captionReserve, captionBandHeight, paintCaptionBand, CAPTION_HEIGHT, CAPTION_HEIGHT_SUBTITLE, CAPTION_HEIGHT_TIGHT, CAPTION_PASS_THROUGH, rowHeightFor, boardHeightFor, columnUnitFor, cellToRect, pointToCell, sizeToSpan, gridItemFromCell, cellFromGridItem, buildCommitCommands, ensureDashboardKitStyles, DASHBOARD_KIT_STYLE_ID,
|
|
234
234
|
// The DATA-FIRST authoring API — the erDiagram()/umlDiagram() equivalent.
|
|
235
235
|
dashboard,
|
|
236
236
|
// …and the built-in renderers behind `kind`, so a dashboard is useful with
|
|
@@ -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,8 +43,8 @@
|
|
|
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';
|
|
47
|
-
import { bindDashboardGrid, } from './grid-binder.js';
|
|
46
|
+
import { BatchCommand, BringNodeToFrontCommand, Command, GroupModel, NodeModel, AddGroupCommand, AddToGroupCommand, RemoveFromGroupCommand, RemoveGroupCommand, RemoveNodeCommand, SendNodeToBackCommand, } from '@grafloria/engine';
|
|
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';
|
|
50
50
|
import { ensureDashboardKitStyles } from './styles.js';
|
|
@@ -341,13 +341,47 @@ 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
|
+
// Pages come from LIVE membership, in the authored order, with anything that
|
|
346
|
+
// arrived later on the end. Reading the authored spec instead meant a page
|
|
347
|
+
// that LEFT the container kept its tab — a tab pointing at nothing, which is
|
|
348
|
+
// exactly the "the content is taken and the tab remains" report — and a page
|
|
349
|
+
// that arrived never got one.
|
|
345
350
|
const pagesOf = (id) => {
|
|
346
|
-
var _a;
|
|
347
|
-
const
|
|
348
|
-
|
|
351
|
+
var _a, _b, _c, _d;
|
|
352
|
+
const cg = (_a = ctx.boardGroups.get(id)) !== null && _a !== void 0 ? _a : model.getGroup(id);
|
|
353
|
+
const authored = ((_c = (_b = ctx.specById.get(id)) === null || _b === void 0 ? void 0 : _b.widgets) !== null && _c !== void 0 ? _c : []).map((c) => c.id);
|
|
354
|
+
const live = [...((_d = cg === null || cg === void 0 ? void 0 : cg.members) !== null && _d !== void 0 ? _d : [])].filter((m) => !!model.getGroup(m));
|
|
355
|
+
const rank = (pid) => {
|
|
356
|
+
// A plain child is wrapped as `<id>__page`, so the authored order is
|
|
357
|
+
// keyed on the id BEFORE the wrap as well as after it.
|
|
358
|
+
const i = authored.indexOf(pid);
|
|
359
|
+
const j = i < 0 ? authored.indexOf(pid.replace(/__page$/, '')) : i;
|
|
360
|
+
return j < 0 ? Number.MAX_SAFE_INTEGER : j;
|
|
361
|
+
};
|
|
362
|
+
live.sort((a, b) => rank(a) - rank(b));
|
|
363
|
+
return live.map((pid) => { var _a, _b; return ({ id: pid, label: (_b = (_a = ctx.specById.get(pid)) === null || _a === void 0 ? void 0 : _a.title) !== null && _b !== void 0 ? _b : pid }); });
|
|
349
364
|
};
|
|
350
365
|
const isTabs = (id) => { var _a, _b; return ((_a = ctx.layoutOf.get(id)) !== null && _a !== void 0 ? _a : (_b = ctx.specById.get(id)) === null || _b === void 0 ? void 0 : _b.layout) === 'tabs' && pagesOf(id).length > 0; };
|
|
366
|
+
/**
|
|
367
|
+
* A tab dragged off its strip tears its page out onto the board that owns
|
|
368
|
+
* the container — VS Code's "drag a tab out and it becomes a group of its
|
|
369
|
+
* own". Only the board below can place it, so the gesture is handed to that
|
|
370
|
+
* binder; a container with a single page keeps it, because a tab container
|
|
371
|
+
* with no pages has nothing to show.
|
|
372
|
+
*/
|
|
373
|
+
const tearOut = (containerId, pageId, ev) => {
|
|
374
|
+
var _a;
|
|
375
|
+
if (!container)
|
|
376
|
+
return false;
|
|
377
|
+
if (!pagesOf(containerId).some((p) => p.id === pageId))
|
|
378
|
+
return false;
|
|
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);
|
|
384
|
+
};
|
|
351
385
|
const paintStrip = (id, f, h, pages, active) => {
|
|
352
386
|
var _a, _b, _c;
|
|
353
387
|
const layer = container === null || container === void 0 ? void 0 : container.querySelector('.grafloria-html-layer');
|
|
@@ -370,7 +404,7 @@ export function attachTabsRuntime(ctx, model, container, handle) {
|
|
|
370
404
|
const key = tabStripKey(pages, active, ctx.tabsOf.get(id), rtl);
|
|
371
405
|
if (el.getAttribute('data-key') === key)
|
|
372
406
|
return;
|
|
373
|
-
paintTabStrip(el, pages, active, ctx.tabsOf.get(id), rtl, (pid) => handle.activateTab(id, pid), () => handle.selectWidget(id));
|
|
407
|
+
paintTabStrip(el, pages, active, ctx.tabsOf.get(id), rtl, (pid) => handle.activateTab(id, pid), () => handle.selectWidget(id), (pid, ev) => tearOut(id, pid, ev));
|
|
374
408
|
el.setAttribute('data-key', key);
|
|
375
409
|
};
|
|
376
410
|
const sync = (id) => {
|
|
@@ -379,7 +413,12 @@ export function attachTabsRuntime(ctx, model, container, handle) {
|
|
|
379
413
|
if (!cg || !isTabs(id))
|
|
380
414
|
return;
|
|
381
415
|
const pages = pagesOf(id);
|
|
382
|
-
|
|
416
|
+
// The page that was showing may have just been dragged out.
|
|
417
|
+
let active = (_b = ctx.activeTab.get(id)) !== null && _b !== void 0 ? _b : pages[0].id;
|
|
418
|
+
if (!pages.some((p) => p.id === active)) {
|
|
419
|
+
active = pages[0].id;
|
|
420
|
+
ctx.activeTab.set(id, active);
|
|
421
|
+
}
|
|
383
422
|
const strip = tabStripReserve(ctx.tabsOf.get(id), pages.length);
|
|
384
423
|
const f = { x: cg.position.x, y: cg.position.y, width: (_d = (_c = cg.size) === null || _c === void 0 ? void 0 : _c.width) !== null && _d !== void 0 ? _d : 0, height: (_f = (_e = cg.size) === null || _e === void 0 ? void 0 : _e.height) !== null && _f !== void 0 ? _f : 0 };
|
|
385
424
|
const inner = { width: f.width, height: Math.max(0, f.height - strip) };
|
|
@@ -419,22 +458,78 @@ export function attachTabsRuntime(ctx, model, container, handle) {
|
|
|
419
458
|
};
|
|
420
459
|
ctx.syncTabs = sync;
|
|
421
460
|
ctx.subscriptions = (_a = ctx.subscriptions) !== null && _a !== void 0 ? _a : [];
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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;
|
|
425
469
|
const pages = pagesOf(id);
|
|
426
|
-
if (!ctx.activeTab.has(id)) {
|
|
470
|
+
if (!ctx.activeTab.has(id) && pages.length > 0) {
|
|
427
471
|
const meta = cg.getMetadata('containerWidget');
|
|
428
|
-
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;
|
|
429
473
|
ctx.activeTab.set(id, want && pages.some((p) => p.id === want) ? want : pages[0].id);
|
|
430
474
|
if ((meta === null || meta === void 0 ? void 0 : meta.tabs) && !ctx.tabsOf.has(id))
|
|
431
475
|
ctx.tabsOf.set(id, meta.tabs);
|
|
432
476
|
}
|
|
433
|
-
// Re-lay the pages whenever the parent moves or resizes the container
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
477
|
+
// Re-lay the pages whenever the parent moves or resizes the container —
|
|
478
|
+
// and whenever a page joins or leaves, so the strip always shows the pages
|
|
479
|
+
// that are actually there.
|
|
480
|
+
const offs = [];
|
|
481
|
+
for (const ev of ['bounds:changed', 'member:added', 'member:removed']) {
|
|
482
|
+
const off = cg.on(ev, (() => sync(id)));
|
|
483
|
+
if (typeof off === 'function')
|
|
484
|
+
offs.push(off);
|
|
485
|
+
}
|
|
486
|
+
subsOf.set(id, offs);
|
|
487
|
+
(_f = ctx.subscriptions) === null || _f === void 0 ? void 0 : _f.push(() => detach(id));
|
|
437
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()) } };
|
|
438
533
|
}
|
|
439
534
|
}
|
|
440
535
|
/** `setCaption` as one history step: execute re-applies the value, undo the previous one. */
|
|
@@ -564,6 +659,160 @@ export function createDashboardHandle(ctx) {
|
|
|
564
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); });
|
|
565
660
|
return entries;
|
|
566
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
|
+
};
|
|
567
816
|
/** Bookkeeping closures for one widget, shared by the add and remove
|
|
568
817
|
* commands (and applied synchronously by the handle — idempotent). */
|
|
569
818
|
const registryOf = (id, boardId, spec) => {
|
|
@@ -485,13 +485,72 @@ interface BinderPeer {
|
|
|
485
485
|
*/
|
|
486
486
|
containsWorldExtended(x: number, y: number): boolean;
|
|
487
487
|
frameArea(): number;
|
|
488
|
-
adopt(node:
|
|
488
|
+
adopt(node: {
|
|
489
|
+
id: string;
|
|
490
|
+
}, world: {
|
|
489
491
|
x: number;
|
|
490
492
|
y: number;
|
|
491
493
|
}, pxSize: {
|
|
492
494
|
width: number;
|
|
493
495
|
height: number;
|
|
494
|
-
}): AdoptedLeg | null;
|
|
496
|
+
}, opts?: AdoptOptions): AdoptedLeg | null;
|
|
497
|
+
/**
|
|
498
|
+
* Tear a tab page out of its container and onto THIS board, driven by the
|
|
499
|
+
* press that started on its tab. Answers false when the board will not take
|
|
500
|
+
* it, so the strip can leave the press alone.
|
|
501
|
+
*/
|
|
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';
|
|
495
554
|
}
|
|
496
555
|
interface AdoptedLeg {
|
|
497
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
|
|
@@ -448,6 +450,8 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
448
450
|
let placeholder = null;
|
|
449
451
|
/** Foreign tile currently adopted from another binder's gesture. */
|
|
450
452
|
let adoptedGhostId = null;
|
|
453
|
+
/** The tab page this board is currently tearing out of a container, if any. */
|
|
454
|
+
let tearing = null;
|
|
451
455
|
let glideTimer = null;
|
|
452
456
|
let ghostTimer = null;
|
|
453
457
|
/**
|
|
@@ -1091,6 +1095,11 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
1091
1095
|
* the plan prototype cannot leave its board at all (cells clamp at the
|
|
1092
1096
|
* edges), so a 60px slip past the frame must not dim or delete. */
|
|
1093
1097
|
const EDGE_GRACE = 60;
|
|
1098
|
+
/** A point inside a member group's own frame (its strip and its pages). */
|
|
1099
|
+
const worldInsideGroup = (g, x, y) => {
|
|
1100
|
+
const s = sizeOf(g);
|
|
1101
|
+
return x >= g.position.x && x <= g.position.x + s.width && y >= g.position.y && y <= g.position.y + s.height;
|
|
1102
|
+
};
|
|
1094
1103
|
const worldInsideBoardGrace = (x, y) => {
|
|
1095
1104
|
if (worldInsideBoardExtended(x, y))
|
|
1096
1105
|
return true;
|
|
@@ -2014,6 +2023,14 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
2014
2023
|
const geomBefore = g.startGeom.get(g.id);
|
|
2015
2024
|
const crossing = [
|
|
2016
2025
|
...sourceDisplaced,
|
|
2026
|
+
// …and the TARGET board's own displaced tiles. Dropping onto an
|
|
2027
|
+
// occupied row pushes that row down, and the ghost draws the pushed
|
|
2028
|
+
// layout — but these commands were computed and then dropped on the
|
|
2029
|
+
// floor, so nothing persisted the push. The next rebuild read the
|
|
2030
|
+
// pre-drop cells, found the arriving tile overlapping them, and pushed
|
|
2031
|
+
// IT to the bottom instead: the drop landed a whole row away from the
|
|
2032
|
+
// ghost that promised it ("it goes in as an extra row").
|
|
2033
|
+
...fin.commands,
|
|
2017
2034
|
new RemoveFromGroupCommand(group.id, g.id),
|
|
2018
2035
|
new AddToGroupCommand(targetGroupId, g.id),
|
|
2019
2036
|
...buildCommitCommands([
|
|
@@ -2115,6 +2132,8 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
2115
2132
|
}
|
|
2116
2133
|
return best;
|
|
2117
2134
|
};
|
|
2135
|
+
/** `handle.planRemoval` for local use before the handle object exists. */
|
|
2136
|
+
const planRemovalOf = (id) => handle.planRemoval(id);
|
|
2118
2137
|
/** This binder's side of an adoption: enter gateless, then live-push. */
|
|
2119
2138
|
/**
|
|
2120
2139
|
* Put an ARRIVING tile as close to the pointer as the board allows. The
|
|
@@ -2124,21 +2143,115 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
2124
2143
|
* I put it". Slide along the row instead, nearest first, so a tile that
|
|
2125
2144
|
* cannot take the cell under the pointer still lands beside it.
|
|
2126
2145
|
*/
|
|
2127
|
-
|
|
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) => {
|
|
2158
|
+
const i = engine.getItem(id);
|
|
2159
|
+
return !!i && i.x === cx && i.y === y;
|
|
2160
|
+
};
|
|
2161
|
+
const maxX = Math.max(0, columns - w);
|
|
2162
|
+
if (at(x))
|
|
2163
|
+
return true;
|
|
2128
2164
|
if (engine.moveCheck(id, x, y, { gate: false }).changed)
|
|
2129
2165
|
return true;
|
|
2130
|
-
const maxX = Math.max(0, columns - w);
|
|
2131
2166
|
for (let d = 1; d <= columns; d++) {
|
|
2132
2167
|
for (const cx of [x - d, x + d]) {
|
|
2133
2168
|
if (cx < 0 || cx > maxX)
|
|
2134
2169
|
continue;
|
|
2170
|
+
if (at(cx))
|
|
2171
|
+
return true;
|
|
2135
2172
|
if (engine.moveCheck(id, cx, y, { gate: false }).changed)
|
|
2136
2173
|
return true;
|
|
2137
2174
|
}
|
|
2138
2175
|
}
|
|
2139
2176
|
return false;
|
|
2140
2177
|
};
|
|
2141
|
-
const
|
|
2178
|
+
const placeNear = (id, x, y, w) => {
|
|
2179
|
+
if (placeOnRow(id, x, y, w))
|
|
2180
|
+
return true;
|
|
2181
|
+
// EVERY column on that row refused — which is what a locked section
|
|
2182
|
+
// spanning the full width does, and there are plenty of those. Sliding
|
|
2183
|
+
// sideways can never clear it, so try the rows either side, nearest first.
|
|
2184
|
+
// Without this the drop either fell to the bottom of the board or, when
|
|
2185
|
+
// the board had no room down there either, did nothing at all.
|
|
2186
|
+
const reach = Math.max(1, rows()) + 2;
|
|
2187
|
+
for (let d = 1; d <= reach; d++) {
|
|
2188
|
+
for (const cy of [y - d, y + d]) {
|
|
2189
|
+
if (cy < 0)
|
|
2190
|
+
continue;
|
|
2191
|
+
if (placeOnRow(id, x, cy, w))
|
|
2192
|
+
return true;
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
return false;
|
|
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
|
+
};
|
|
2252
|
+
const adopt = (
|
|
2253
|
+
// Only the id is used: a tab page arriving here is a GROUP, not a node.
|
|
2254
|
+
node, world, pxSize, opts = {}) => {
|
|
2142
2255
|
if (disposed)
|
|
2143
2256
|
return null;
|
|
2144
2257
|
const f = frame();
|
|
@@ -2173,9 +2286,16 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
2173
2286
|
}
|
|
2174
2287
|
}
|
|
2175
2288
|
adoptedGhostId = node.id;
|
|
2176
|
-
const
|
|
2177
|
-
const
|
|
2178
|
-
|
|
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);
|
|
2179
2299
|
armGlide();
|
|
2180
2300
|
project();
|
|
2181
2301
|
syncPlaceholder();
|
|
@@ -2185,9 +2305,12 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
2185
2305
|
const item = engine.getItem(node.id);
|
|
2186
2306
|
if (!item)
|
|
2187
2307
|
return;
|
|
2188
|
-
const
|
|
2189
|
-
|
|
2190
|
-
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))
|
|
2191
2314
|
project();
|
|
2192
2315
|
syncPlaceholder();
|
|
2193
2316
|
},
|
|
@@ -2220,6 +2343,7 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
2220
2343
|
};
|
|
2221
2344
|
const selfPeer = {
|
|
2222
2345
|
group,
|
|
2346
|
+
tearOutMember: (pageId, fromGroupId, ev, plan) => beginTearOut(pageId, fromGroupId, ev, plan),
|
|
2223
2347
|
clearSelection: () => {
|
|
2224
2348
|
var _a;
|
|
2225
2349
|
if (selectedId === undefined)
|
|
@@ -2757,6 +2881,119 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
2757
2881
|
};
|
|
2758
2882
|
api.container.addEventListener('focusin', onFocusIn);
|
|
2759
2883
|
api.container.addEventListener('keydown', onKey);
|
|
2884
|
+
// -- tab tear-out -----------------------------------------------------------
|
|
2885
|
+
/**
|
|
2886
|
+
* A press that TRAVELLED on a tab drags the whole page onto this board, the
|
|
2887
|
+
* way dragging a VS Code tab out of its group makes a group of its own. The
|
|
2888
|
+
* page is a GROUP, so it rides the same `adopt` leg a widget uses — entry at
|
|
2889
|
+
* the bottom, `placeNear` under the pointer, the dashed placeholder drawn
|
|
2890
|
+
* from the live cell — and the commit is one batch: the cell and frame it
|
|
2891
|
+
* landed on, out of the container, into this board, plus every tile this
|
|
2892
|
+
* board had to push aside.
|
|
2893
|
+
*
|
|
2894
|
+
* Like VS Code, the page itself does not follow the pointer; a chip carrying
|
|
2895
|
+
* the tab's label does, and the placeholder shows where the drop will land.
|
|
2896
|
+
*/
|
|
2897
|
+
const beginTearOut = (pageId, fromGroupId, ev, plan) => {
|
|
2898
|
+
var _a;
|
|
2899
|
+
if (disposed || gesture || slabGesture || isStatic || tearing)
|
|
2900
|
+
return false;
|
|
2901
|
+
const from = diagram.getGroup(fromGroupId);
|
|
2902
|
+
if (!from || !diagram.getGroup(pageId) || !engine.getItem(fromGroupId))
|
|
2903
|
+
return false;
|
|
2904
|
+
const toWorld = (cx, cy) => {
|
|
2905
|
+
var _a;
|
|
2906
|
+
const rect = api.container.getBoundingClientRect();
|
|
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 };
|
|
2908
|
+
};
|
|
2909
|
+
const first = toWorld(ev.clientX, ev.clientY);
|
|
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' });
|
|
2915
|
+
if (!leg)
|
|
2916
|
+
return false;
|
|
2917
|
+
tearing = pageId;
|
|
2918
|
+
const doc = (_a = api.container.ownerDocument) !== null && _a !== void 0 ? _a : document;
|
|
2919
|
+
const chip = doc.createElement('div');
|
|
2920
|
+
chip.className = 'axdb-drag-chip axdb-tab-chip';
|
|
2921
|
+
chip.textContent = plan.label;
|
|
2922
|
+
doc.body.appendChild(chip);
|
|
2923
|
+
const moveChip = (cx, cy) => {
|
|
2924
|
+
chip.style.left = `${cx + 6}px`;
|
|
2925
|
+
chip.style.top = `${cy + 6}px`;
|
|
2926
|
+
};
|
|
2927
|
+
moveChip(ev.clientX, ev.clientY);
|
|
2928
|
+
armGlide();
|
|
2929
|
+
api.render();
|
|
2930
|
+
let last = { x: ev.clientX, y: ev.clientY };
|
|
2931
|
+
const detach = () => {
|
|
2932
|
+
window.removeEventListener('pointermove', onMove, true);
|
|
2933
|
+
window.removeEventListener('pointerup', onUp, true);
|
|
2934
|
+
window.removeEventListener('pointercancel', onCancel, true);
|
|
2935
|
+
window.removeEventListener('keydown', onKey, true);
|
|
2936
|
+
};
|
|
2937
|
+
const onMove = (e) => {
|
|
2938
|
+
if (disposed)
|
|
2939
|
+
return detach();
|
|
2940
|
+
last = { x: e.clientX, y: e.clientY };
|
|
2941
|
+
moveChip(e.clientX, e.clientY);
|
|
2942
|
+
const world = toWorld(e.clientX, e.clientY);
|
|
2943
|
+
const home = worldInsideGroup(from, world.x, world.y);
|
|
2944
|
+
// Over its own container it goes home: a tab dragged around its own
|
|
2945
|
+
// strip must not tear itself out.
|
|
2946
|
+
chip.classList.toggle('axdb-out', home || !worldInsideBoardGrace(world.x, world.y));
|
|
2947
|
+
if (!home)
|
|
2948
|
+
leg.move(world);
|
|
2949
|
+
api.render();
|
|
2950
|
+
};
|
|
2951
|
+
const finish = (commit) => {
|
|
2952
|
+
var _a, _b;
|
|
2953
|
+
detach();
|
|
2954
|
+
chip.remove();
|
|
2955
|
+
tearing = null;
|
|
2956
|
+
if (disposed)
|
|
2957
|
+
return;
|
|
2958
|
+
const world = toWorld(last.x, last.y);
|
|
2959
|
+
const home = worldInsideGroup(from, world.x, world.y);
|
|
2960
|
+
if (!commit || home || !worldInsideBoardGrace(world.x, world.y)) {
|
|
2961
|
+
leg.abort();
|
|
2962
|
+
disarmGlideSoon();
|
|
2963
|
+
api.renderNow();
|
|
2964
|
+
(_a = options.onGesture) === null || _a === void 0 ? void 0 : _a.call(options, { type: 'cancel', kind: 'move', nodeId: pageId, changed: false });
|
|
2965
|
+
return;
|
|
2966
|
+
}
|
|
2967
|
+
const fin = leg.finalize();
|
|
2968
|
+
if (!fin) {
|
|
2969
|
+
disarmGlideSoon();
|
|
2970
|
+
api.renderNow();
|
|
2971
|
+
return;
|
|
2972
|
+
}
|
|
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]);
|
|
2979
|
+
disarmGlideSoon();
|
|
2980
|
+
enforceBoardHeight();
|
|
2981
|
+
persistLayouts();
|
|
2982
|
+
api.renderNow();
|
|
2983
|
+
(_b = options.onGesture) === null || _b === void 0 ? void 0 : _b.call(options, { type: 'commit', kind: 'move', nodeId: pageId, changed });
|
|
2984
|
+
};
|
|
2985
|
+
const onUp = () => finish(true);
|
|
2986
|
+
const onCancel = () => finish(false);
|
|
2987
|
+
const onKey = (e) => {
|
|
2988
|
+
if (e.key === 'Escape')
|
|
2989
|
+
finish(false);
|
|
2990
|
+
};
|
|
2991
|
+
window.addEventListener('pointermove', onMove, true);
|
|
2992
|
+
window.addEventListener('pointerup', onUp, true);
|
|
2993
|
+
window.addEventListener('pointercancel', onCancel, true);
|
|
2994
|
+
window.addEventListener('keydown', onKey, true);
|
|
2995
|
+
return true;
|
|
2996
|
+
};
|
|
2760
2997
|
// -- palette drag-in --------------------------------------------------------
|
|
2761
2998
|
const beginPaletteDrag = (node, spec, event) => {
|
|
2762
2999
|
var _a, _b;
|
|
@@ -3126,7 +3363,18 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
3126
3363
|
const it = engine.getItem(id);
|
|
3127
3364
|
if (!it)
|
|
3128
3365
|
return [];
|
|
3129
|
-
|
|
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 });
|
|
3130
3378
|
clone.remove(id);
|
|
3131
3379
|
const f = frame();
|
|
3132
3380
|
const g = geom();
|
|
@@ -3175,6 +3423,7 @@ export function bindDashboardGrid(api, group, options = {}) {
|
|
|
3175
3423
|
api.container.removeEventListener('keydown', onKey);
|
|
3176
3424
|
hostObserver.disconnect();
|
|
3177
3425
|
containerObserver === null || containerObserver === void 0 ? void 0 : containerObserver.disconnect();
|
|
3426
|
+
tearing = null;
|
|
3178
3427
|
for (const off of subs)
|
|
3179
3428
|
off();
|
|
3180
3429
|
// The corner handles are THIS binder's affordance: a board re-bound as a
|
|
@@ -3,7 +3,7 @@ export { bindDashboardSplit, SetSplitTreeCommand, SPLIT_TREE_KEY, type Dashboard
|
|
|
3
3
|
export { projectSplit, dividersOf, addSplitLeaf, removeSplitLeaf, insertSplitLeaf, moveSplitDivider, splitFromCells, cellsFromSplit, splitLeaves, type SplitNode, type SplitGroup, type SplitLeaf, type SplitDir, type SplitSide, type SplitDivider, } from './split-layout.js';
|
|
4
4
|
export { rowHeightFor, boardHeightFor, columnUnitFor, cellToRect, pointToCell, sizeToSpan, spanWidthPx, gridItemFromCell, cellFromGridItem, buildCommitCommands, type CellRect, type WorldRect, type DashboardGridGeometry, type TileDelta, } from './grid-mapping.js';
|
|
5
5
|
export { ensureDashboardKitStyles, DASHBOARD_KIT_STYLE_ID } from './styles.js';
|
|
6
|
-
export { paintTabStrip, tabStripReserve, tabStripKey, TAB_STRIP_HEIGHT, type TabsOptions, type TabPage } from './tabs.js';
|
|
6
|
+
export { paintTabStrip, tabStripReserve, tabStripKey, TAB_STRIP_HEIGHT, TAB_DRAG_THRESHOLD, type TabsOptions, type TabPage } from './tabs.js';
|
|
7
7
|
export { normalizeCaption, captionReserve, captionBandHeight, paintCaptionBand, CAPTION_HEIGHT, CAPTION_HEIGHT_SUBTITLE, CAPTION_HEIGHT_TIGHT, CAPTION_PASS_THROUGH, type SectionCaption, type SectionCaptionOptions, type SectionCaptionAction, type SectionCaptionFont, } from './caption.js';
|
|
8
8
|
export { dashboard, type DashboardOptions, type DashboardSpec, type DashboardSnapshot, type DashboardHandle, type DashboardViewSpec, type DashboardWidgetSpec, type WidgetHandle, } from './dashboard.js';
|
|
9
9
|
export { defaultWidgetRenderer, renderKpiWidget, renderLineWidget, renderBarWidget, renderDonutWidget, renderFunnelWidget, renderTableWidget, BUILT_IN_WIDGET_KINDS, type WidgetRenderer, type KpiWidgetData, type LineWidgetData, type LineSeries, type BarWidgetData, type DonutWidgetData, type FunnelWidgetData, type TableWidgetData, } from './widgets.js';
|
|
@@ -3,7 +3,7 @@ export { bindDashboardSplit, SetSplitTreeCommand, SPLIT_TREE_KEY, } from './spli
|
|
|
3
3
|
export { projectSplit, dividersOf, addSplitLeaf, removeSplitLeaf, insertSplitLeaf, moveSplitDivider, splitFromCells, cellsFromSplit, splitLeaves, } from './split-layout.js';
|
|
4
4
|
export { rowHeightFor, boardHeightFor, columnUnitFor, cellToRect, pointToCell, sizeToSpan, spanWidthPx, gridItemFromCell, cellFromGridItem, buildCommitCommands, } from './grid-mapping.js';
|
|
5
5
|
export { ensureDashboardKitStyles, DASHBOARD_KIT_STYLE_ID } from './styles.js';
|
|
6
|
-
export { paintTabStrip, tabStripReserve, tabStripKey, TAB_STRIP_HEIGHT } from './tabs.js';
|
|
6
|
+
export { paintTabStrip, tabStripReserve, tabStripKey, TAB_STRIP_HEIGHT, TAB_DRAG_THRESHOLD } from './tabs.js';
|
|
7
7
|
export { normalizeCaption, captionReserve, captionBandHeight, paintCaptionBand, CAPTION_HEIGHT, CAPTION_HEIGHT_SUBTITLE, CAPTION_HEIGHT_TIGHT, CAPTION_PASS_THROUGH, } from './caption.js';
|
|
8
8
|
// The DATA-FIRST authoring API (the erDiagram/umlDiagram equivalent).
|
|
9
9
|
export { dashboard, } from './dashboard.js';
|
|
@@ -1251,6 +1251,9 @@ export function bindDashboardSplit(api, group, options = {}) {
|
|
|
1251
1251
|
hasItem: (id) => { var _a; return ((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(id); },
|
|
1252
1252
|
memberCell: (id) => handle.cellOf(id),
|
|
1253
1253
|
resizeMemberBy: () => ({ changed: false }),
|
|
1254
|
+
// A split board has no cells to drop a torn-out page into: it refuses,
|
|
1255
|
+
// and the press stays a plain tab click.
|
|
1256
|
+
tearOutMember: (_pageId, _fromGroupId, _ev, _plan) => false,
|
|
1254
1257
|
containsWorld: (x, y) => worldInsideBoard(x, y),
|
|
1255
1258
|
containsWorldExtended: (x, y) => worldInsideBoard(x, y),
|
|
1256
1259
|
frameArea: () => {
|
|
@@ -111,6 +111,27 @@ const CSS = `
|
|
|
111
111
|
(the same signal a tile ghost gives outside the board). Without this rule
|
|
112
112
|
the class was set and nothing showed — a refusal a user could not see. */
|
|
113
113
|
.axdb-drag-chip.axdb-out { opacity: .35; filter: grayscale(.7) drop-shadow(0 8px 14px rgba(16, 24, 40, .2)); }
|
|
114
|
+
/* A tab dragged off its strip: VS Code carries the TAB, not the editor, so
|
|
115
|
+
what follows the pointer is a chip wearing the tab's own label. */
|
|
116
|
+
.axdb-tab-chip {
|
|
117
|
+
padding: 5px 12px;
|
|
118
|
+
max-width: 220px;
|
|
119
|
+
overflow: hidden;
|
|
120
|
+
text-overflow: ellipsis;
|
|
121
|
+
white-space: nowrap;
|
|
122
|
+
font: 500 12px/1.4 var(--axdb-font, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
|
|
123
|
+
color: var(--axdb-tabs-on-fg, #1f2430);
|
|
124
|
+
background: var(--axdb-tabs-on-bg, #fff);
|
|
125
|
+
border-radius: var(--axdb-rs-radius, 3px);
|
|
126
|
+
box-shadow: 0 0 0 1px rgba(31, 36, 48, .12);
|
|
127
|
+
}
|
|
128
|
+
@media (prefers-color-scheme: dark) {
|
|
129
|
+
.axdb-tab-chip {
|
|
130
|
+
color: var(--axdb-tabs-on-fg, #eceef4);
|
|
131
|
+
background: var(--axdb-tabs-on-bg, #1a1d25);
|
|
132
|
+
box-shadow: 0 0 0 1px rgba(236, 238, 244, .16);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
114
135
|
|
|
115
136
|
/* ===========================================================================
|
|
116
137
|
BUILT-IN WIDGET CARDS — what widgets.ts paints when a page writes no
|
|
@@ -25,6 +25,12 @@ export interface TabPage {
|
|
|
25
25
|
label: string;
|
|
26
26
|
}
|
|
27
27
|
export declare const TAB_STRIP_HEIGHT = 30;
|
|
28
|
+
/**
|
|
29
|
+
* Travel, in px, that turns a press on a tab from a click into a drag. VS Code
|
|
30
|
+
* uses the platform drag threshold; 4 px is the same order and keeps a shaky
|
|
31
|
+
* click a click.
|
|
32
|
+
*/
|
|
33
|
+
export declare const TAB_DRAG_THRESHOLD = 4;
|
|
28
34
|
/** Pixels the pages give up at the top of the container. */
|
|
29
35
|
export declare function tabStripReserve(o: TabsOptions | undefined, pageCount: number): number;
|
|
30
36
|
/**
|
|
@@ -33,4 +39,11 @@ export declare function tabStripReserve(o: TabsOptions | undefined, pageCount: n
|
|
|
33
39
|
* rebuild — and so a `click` listener survives between switches.
|
|
34
40
|
*/
|
|
35
41
|
export declare function tabStripKey(pages: TabPage[], activeId: string, o: TabsOptions | undefined, rtl: boolean): string;
|
|
36
|
-
export declare function paintTabStrip(strip: HTMLElement, pages: TabPage[], activeId: string, o: TabsOptions | undefined, rtl: boolean, onPick: (id: string) => void, onSelectContainer?: () => void
|
|
42
|
+
export declare function paintTabStrip(strip: HTMLElement, pages: TabPage[], activeId: string, o: TabsOptions | undefined, rtl: boolean, onPick: (id: string) => void, onSelectContainer?: () => void,
|
|
43
|
+
/**
|
|
44
|
+
* The tab is the PAGE's drag handle, as it is in VS Code: press one and
|
|
45
|
+
* travel, and the whole page leaves — not the widget under the pointer,
|
|
46
|
+
* which used to be the only way to drag anything out and left the tab
|
|
47
|
+
* behind pointing at an empty page.
|
|
48
|
+
*/
|
|
49
|
+
onDrag?: (pageId: string, ev: PointerEvent) => boolean): void;
|
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
* binder's.
|
|
10
10
|
*/
|
|
11
11
|
export const TAB_STRIP_HEIGHT = 30;
|
|
12
|
+
/**
|
|
13
|
+
* Travel, in px, that turns a press on a tab from a click into a drag. VS Code
|
|
14
|
+
* uses the platform drag threshold; 4 px is the same order and keeps a shaky
|
|
15
|
+
* click a click.
|
|
16
|
+
*/
|
|
17
|
+
export const TAB_DRAG_THRESHOLD = 4;
|
|
12
18
|
/** Pixels the pages give up at the top of the container. */
|
|
13
19
|
export function tabStripReserve(o, pageCount) {
|
|
14
20
|
var _a;
|
|
@@ -24,8 +30,19 @@ export function tabStripReserve(o, pageCount) {
|
|
|
24
30
|
export function tabStripKey(pages, activeId, o, rtl) {
|
|
25
31
|
return JSON.stringify([pages, activeId, o !== null && o !== void 0 ? o : null, rtl]);
|
|
26
32
|
}
|
|
27
|
-
export function paintTabStrip(strip, pages, activeId, o, rtl, onPick, onSelectContainer
|
|
33
|
+
export function paintTabStrip(strip, pages, activeId, o, rtl, onPick, onSelectContainer,
|
|
34
|
+
/**
|
|
35
|
+
* The tab is the PAGE's drag handle, as it is in VS Code: press one and
|
|
36
|
+
* travel, and the whole page leaves — not the widget under the pointer,
|
|
37
|
+
* which used to be the only way to drag anything out and left the tab
|
|
38
|
+
* behind pointing at an empty page.
|
|
39
|
+
*/
|
|
40
|
+
onDrag) {
|
|
28
41
|
const doc = strip.ownerDocument;
|
|
42
|
+
// Set by a tab drag, read by the click that trails it. Strip-level so it
|
|
43
|
+
// survives between the two listeners, cleared on the next press so an
|
|
44
|
+
// aborted drag never eats a later click.
|
|
45
|
+
let suppressClick = false;
|
|
29
46
|
strip.className = 'axdb-tabs';
|
|
30
47
|
if (o === null || o === void 0 ? void 0 : o.className)
|
|
31
48
|
for (const c of o.className.split(/\s+/).filter(Boolean))
|
|
@@ -60,8 +77,41 @@ export function paintTabStrip(strip, pages, activeId, o, rtl, onPick, onSelectCo
|
|
|
60
77
|
b.tabIndex = p.id === activeId ? 0 : -1;
|
|
61
78
|
b.textContent = p.label;
|
|
62
79
|
b.title = p.label;
|
|
80
|
+
b.addEventListener('pointerdown', (e) => {
|
|
81
|
+
const pe = e;
|
|
82
|
+
if (pe.button !== undefined && pe.button > 0)
|
|
83
|
+
return;
|
|
84
|
+
suppressClick = false;
|
|
85
|
+
if (!onDrag)
|
|
86
|
+
return;
|
|
87
|
+
const from = { x: pe.clientX, y: pe.clientY };
|
|
88
|
+
const stop = () => {
|
|
89
|
+
doc.removeEventListener('pointermove', move, true);
|
|
90
|
+
doc.removeEventListener('pointerup', stop, true);
|
|
91
|
+
doc.removeEventListener('pointercancel', stop, true);
|
|
92
|
+
};
|
|
93
|
+
const move = (m) => {
|
|
94
|
+
const pm = m;
|
|
95
|
+
if (Math.abs(pm.clientX - from.x) < TAB_DRAG_THRESHOLD &&
|
|
96
|
+
Math.abs(pm.clientY - from.y) < TAB_DRAG_THRESHOLD)
|
|
97
|
+
return;
|
|
98
|
+
stop();
|
|
99
|
+
// A press that TRAVELLED is a drag: the click that may follow it must
|
|
100
|
+
// not also switch the page — but only if the drag was actually TAKEN.
|
|
101
|
+
// A board that cannot place the page (a split pane, a container down
|
|
102
|
+
// to its last page) refuses, and then the press is still a click.
|
|
103
|
+
suppressClick = onDrag(p.id, pm) === true;
|
|
104
|
+
};
|
|
105
|
+
doc.addEventListener('pointermove', move, true);
|
|
106
|
+
doc.addEventListener('pointerup', stop, true);
|
|
107
|
+
doc.addEventListener('pointercancel', stop, true);
|
|
108
|
+
});
|
|
63
109
|
b.addEventListener('click', (e) => {
|
|
64
110
|
e.stopPropagation();
|
|
111
|
+
if (suppressClick) {
|
|
112
|
+
suppressClick = false;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
65
115
|
onPick(p.id);
|
|
66
116
|
});
|
|
67
117
|
b.addEventListener('keydown', (e) => {
|