@jxsuite/studio 5.1.0 → 5.3.0
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/dist/chunks/{cf-account-picker-qwkm9zrf.js → cf-account-picker-nwnwdm6p.js} +10 -10
- package/dist/chunks/{cf-settings-71bv5zn9.js → cf-settings-adg74qsk.js} +4 -4
- package/dist/chunks/{components-vj72gqtq.js → components-5y78e05d.js} +4 -4
- package/dist/chunks/content-source-4rzvmp0m.js +46 -0
- package/dist/chunks/{convert-to-repeater-n64h8znc.js → convert-to-repeater-jxsv62v7.js} +22 -22
- package/dist/chunks/entry-commands-1wnsqn5d.js +42 -0
- package/dist/chunks/{extension-sections-3hpcambp.js → extension-sections-2mrptyek.js} +27 -27
- package/dist/chunks/{file-ops-8yhd83xr.js → file-ops-knfqshrm.js} +22 -22
- package/dist/chunks/{files-hnxqxtsg.js → files-y6fr4he9.js} +32 -30
- package/dist/chunks/{format-host-779sfp2n.js → format-host-1k4qrcqd.js} +2 -2
- package/dist/chunks/{fs-events-b88z7g4h.js → fs-events-3y6y0nrm.js} +4 -4
- package/dist/chunks/{git-panel-xwj8fpab.js → git-panel-2rkrsjva.js} +23 -22
- package/dist/chunks/grid-view-1p6hsyjx.css +8406 -0
- package/dist/chunks/grid-view-a7b87n2c.js +15322 -0
- package/dist/chunks/{iframe-host-syr0f9sb.js → iframe-host-scpw01h8.js} +13 -13
- package/dist/chunks/{jx-validate-j0wxk72w.js → jx-validate-epp2z3s3.js} +2 -2
- package/dist/chunks/{layers-ff81wthn.js → layers-jnvdbqva.js} +5 -5
- package/dist/chunks/layers-panel-73gfkcga.js +41 -0
- package/dist/chunks/{listbox-6v2ac08j.js → listbox-b7jxnhq1.js} +19 -3
- package/dist/chunks/{media-picker-cqhhxhhe.js → media-picker-f66nav16.js} +13 -13
- package/dist/chunks/menu-1dfhry4s.js +15 -0
- package/dist/chunks/{monaco-buffer-bhy3hb6m.js → monaco-buffer-pykwbm92.js} +3 -3
- package/dist/chunks/{monaco-setup-8r62fj6y.js → monaco-setup-km6abc6q.js} +2 -2
- package/dist/chunks/{notify-628t0anh.js → notify-42gk8ha1.js} +2 -2
- package/dist/chunks/{parse-0zx14y15.js → parse-reg9eevt.js} +2 -2
- package/dist/chunks/publish-panel-z1zd2da1.js +27 -0
- package/dist/chunks/quick-search-zdv7h8ht.js +47 -0
- package/dist/chunks/{references-qvvxej7h.js → references-h69xp9zz.js} +2 -2
- package/dist/chunks/right-panel-889vbnm5.js +45 -0
- package/dist/chunks/{split-g7nm1rgb.js → split-4cz4b6ef.js} +14 -3
- package/dist/chunks/{studio-vfq8qpjt.js → studio-0j7r7s9g.js} +3 -3
- package/dist/chunks/{studio-b4s9eykd.js → studio-151w0xnh.js} +15 -15
- package/dist/chunks/studio-1tbjfqv4.js +146 -0
- package/dist/chunks/{studio-8scdew0e.js → studio-27q131j3.js} +4 -4
- package/dist/chunks/{studio-7r42c6ca.js → studio-3ntvq6cg.js} +5 -5
- package/dist/chunks/{studio-g3jevyjt.js → studio-4c041r8y.js} +2 -2
- package/dist/chunks/{studio-js8nv53n.js → studio-4f3psrk1.js} +14 -14
- package/dist/chunks/{studio-pxbgypmg.js → studio-50wfwy44.js} +17 -14
- package/dist/chunks/{studio-vw7mzp6z.js → studio-6gryj0sg.js} +5 -5
- package/dist/chunks/{studio-e1pgga3k.js → studio-6kvgrkrp.js} +7 -7
- package/dist/chunks/{studio-wqz6dggm.js → studio-7144epdr.js} +9 -9
- package/dist/chunks/{studio-b0k5tqgp.js → studio-7rrb1esn.js} +32 -28
- package/dist/chunks/{studio-38emantv.js → studio-a5h5j33a.js} +19 -5
- package/dist/chunks/{studio-3773086z.js → studio-a65w20ex.js} +4 -4
- package/dist/chunks/{studio-nzzpvzx9.js → studio-ab87phj1.js} +4 -4
- package/dist/chunks/{studio-r6nvacqs.js → studio-b66rsh78.js} +20 -20
- package/dist/chunks/{studio-jxp12rnj.js → studio-b7xacrwe.js} +6 -6
- package/dist/chunks/{studio-6xa21ej9.js → studio-c04nzbby.js} +2 -2
- package/dist/chunks/{studio-try13h8c.js → studio-c0qgc3tn.js} +18 -18
- package/dist/chunks/{studio-tqgryxwg.js → studio-cgprcdxn.js} +7 -7
- package/dist/chunks/{studio-kkqkcp3d.js → studio-d1n2bykn.js} +40 -13
- package/dist/chunks/{studio-p1dw8wfw.js → studio-d8z5bajd.js} +14 -10
- package/dist/chunks/{studio-3rz5md06.js → studio-dd1qqkry.js} +6 -6
- package/dist/chunks/{studio-1z1kcjv4.js → studio-drg8qj9k.js} +11 -11
- package/dist/chunks/{studio-kcgff4r0.js → studio-fgmfksz8.js} +2 -2
- package/dist/chunks/{studio-qk90fgqm.js → studio-fzjx96t2.js} +7 -7
- package/dist/chunks/{studio-a79k90n9.js → studio-g33v4fe9.js} +17 -16
- package/dist/chunks/{studio-k75n7vhp.js → studio-gk2twyh4.js} +74 -36
- package/dist/chunks/{studio-syg2tadj.js → studio-gtfq1dkz.js} +3 -3
- package/dist/chunks/{studio-1knfqgy7.js → studio-j0w60gft.js} +8 -8
- package/dist/chunks/{studio-aj37eer3.js → studio-jt27ta6x.js} +4 -4
- package/dist/chunks/{studio-gb13095a.js → studio-ke09tn9k.js} +59 -20
- package/dist/chunks/{studio-d0300636.js → studio-ks2zzth2.js} +17 -2
- package/dist/chunks/{studio-6ewqv390.js → studio-m1p9b6c3.js} +5 -5
- package/dist/chunks/{studio-qk4bfsq2.js → studio-msc1ee1m.js} +4 -4
- package/dist/chunks/{studio-93zafjcm.js → studio-nfx354t1.js} +10 -10
- package/dist/chunks/{studio-mhhyamxf.js → studio-nsgw7dbk.js} +6 -6
- package/dist/chunks/{studio-qhhm6hbt.js → studio-rtskkm43.js} +4 -4
- package/dist/chunks/studio-t171hxhe.js +23 -0
- package/dist/chunks/{studio-hhvs4sdg.js → studio-vn5hske5.js} +10 -4
- package/dist/chunks/{studio-q3x06vmj.js → studio-wnsyffg1.js} +2 -2
- package/dist/chunks/{studio-5wt9nvhd.js → studio-xajk70w0.js} +29 -42
- package/dist/chunks/{studio-7xnmgv66.js → studio-ye9drbk8.js} +45 -44
- package/dist/chunks/{studio-y9zedyjn.js → studio-z2smx5ce.js} +2 -2
- package/dist/chunks/{studio-sbjyc8bt.js → studio-ze46gzjh.js} +9 -9
- package/dist/chunks/{studio-zdn2g6dt.js → studio-zjjde71a.js} +2 -2
- package/dist/chunks/{workspace-t55jdjbx.js → workspace-np10mayv.js} +18 -10
- package/dist/iframe-entry.js +9 -5
- package/dist/studio.css +1 -2
- package/dist/studio.js +910 -15705
- package/package.json +10 -10
- package/src/files/files.ts +38 -15
- package/src/grid/grid-lazy.ts +59 -0
- package/src/grid/grid-open.ts +20 -3
- package/src/grid/grid-panel.ts +43 -9
- package/src/media/media-open.ts +14 -2
- package/src/panels/chat-panel.ts +40 -5
- package/src/panels/pane-context.ts +5 -4
- package/src/panels/pane-grid.ts +100 -10
- package/src/panels/tab-drop.ts +220 -0
- package/src/panels/tab-strip.ts +235 -30
- package/src/services/ai-models.ts +13 -0
- package/src/services/module-preloads.ts +67 -0
- package/src/studio.ts +16 -6
- package/src/surfaces/commandbar.json +26 -16
- package/src/surfaces/formula-palette.json +8 -7
- package/src/surfaces/formula-palette.ts +10 -6
- package/src/surfaces/palette.json +8 -7
- package/src/surfaces/pane-grid.json +35 -3
- package/src/surfaces/pane-grid.ts +26 -4
- package/src/surfaces/tab-strip.json +16 -39
- package/src/surfaces/tab-strip.ts +76 -12
- package/src/ui/ai-credentials-form.ts +12 -1
- package/src/ui/panel-resize.ts +3 -23
- package/src/ui/virtual-window.ts +57 -4
- package/src/workspace/pane-derive.ts +7 -5
- package/src/workspace/session.ts +3 -3
- package/src/workspace/workspace.ts +160 -43
- package/styles/shell-frame.css +8 -0
- package/styles/shell-frame.json +4 -0
- package/dist/chunks/content-source-5mex4x1h.js +0 -46
- package/dist/chunks/entry-commands-234hjnqw.js +0 -42
- package/dist/chunks/layers-panel-pnnghw12.js +0 -40
- package/dist/chunks/menu-9p2w46gj.js +0 -15
- package/dist/chunks/publish-panel-5zt840sy.js +0 -27
- package/dist/chunks/quick-search-3748564r.js +0 -45
- package/dist/chunks/right-panel-eccz92qt.js +0 -45
package/src/panels/pane-grid.ts
CHANGED
|
@@ -11,20 +11,22 @@
|
|
|
11
11
|
* that scaffolding was standing in for, and the handover is deleted with it.
|
|
12
12
|
*
|
|
13
13
|
* **The document owns the frame; this module owns the decisions.** `surfaces/pane-grid.json` is the
|
|
14
|
-
* cells, the
|
|
14
|
+
* cells, the five boxes inside each one, the splitter between two of them and every rule that lays
|
|
15
15
|
* them out; `surfaces/pane-grid.ts` is the mount. What is left here is the flow: which panes there
|
|
16
16
|
* are, what a cell's stage is furnished with, in what ORDER a departing cell is taken apart, and
|
|
17
17
|
* what the grid's own tracks are. Everything a cell CONTAINS still arrives through the module that
|
|
18
18
|
* owns it — `canvas/canvas-render.ts` is the render root of the stage and this document puts
|
|
19
19
|
* nothing inside it, `panels/jump-bar.ts` and `panels/pane-context.ts` are handed the jump and
|
|
20
|
-
* chrome boxes,
|
|
20
|
+
* chrome boxes, `panels/tab-strip.ts` is handed the strip, and this module makes the fifth — the
|
|
21
|
+
* right-edge drop zone — a pragmatic target itself.
|
|
21
22
|
*
|
|
22
23
|
* **Three properties survived the conversion, and each is structural rather than remembered:**
|
|
23
24
|
*
|
|
24
25
|
* 1. _A pane is complete before it is published._ The runtime builds a `$map` row inside its own
|
|
25
|
-
* effect scope and inserts it afterwards, so the
|
|
26
|
-
* record, the gestures, the two bars' mounts, the pane-focus listener
|
|
27
|
-
* cell is still detached. There is no frame in which a cell
|
|
26
|
+
* effect scope and inserts it afterwards, so the five `cellPart` calls below — the surface
|
|
27
|
+
* record, the gestures, the two bars' mounts, the pane-focus listener, the drop zone's own
|
|
28
|
+
* registration — all happen while the cell is still detached. There is no frame in which a cell
|
|
29
|
+
* exists with no stage inside it.
|
|
28
30
|
* 2. _A pane's node is never re-parented._ The repeater is keyed on the pane id and the runtime's
|
|
29
31
|
* forward-cursor pass only moves rows that are out of place. `workspace.panes[0]` is always the
|
|
30
32
|
* primary and `MAX_PANES` is 2, so no row ever moves — which matters because re-parenting is not
|
|
@@ -54,6 +56,12 @@
|
|
|
54
56
|
*/
|
|
55
57
|
|
|
56
58
|
import { effect, effectScope } from "../reactivity";
|
|
59
|
+
import {
|
|
60
|
+
dropTargetForElements,
|
|
61
|
+
monitorForElements,
|
|
62
|
+
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
63
|
+
import { isTabDropSource } from "./tab-drop";
|
|
64
|
+
import { rectOf } from "../utils/geometry";
|
|
57
65
|
import {
|
|
58
66
|
DEFAULT_PANE_SPLIT,
|
|
59
67
|
PANE_SPLIT_MAX,
|
|
@@ -74,13 +82,13 @@ import { scheduleCanvasRender } from "../canvas/canvas-render";
|
|
|
74
82
|
import { paneRegion, paneStripRegion } from "../ui/regions";
|
|
75
83
|
import { attachJumpBarHost } from "./jump-bar";
|
|
76
84
|
import { attachPaneChromeHost } from "./pane-context";
|
|
77
|
-
import { focusPane, workspace } from "../workspace/workspace";
|
|
85
|
+
import { MAX_PANES, focusPane, workspace } from "../workspace/workspace";
|
|
78
86
|
import { mountPaneGridSurface } from "../surfaces/pane-grid";
|
|
79
87
|
import type { EffectScope } from "@vue/reactivity";
|
|
80
88
|
import type { CanvasSurface } from "../canvas/canvas-surface";
|
|
81
89
|
import type { PaneCellPart, PaneGridRow, PaneGridSurface } from "../surfaces/pane-grid";
|
|
82
90
|
|
|
83
|
-
/** One drawn pane: its root and the
|
|
91
|
+
/** One drawn pane: its root and the five surfaces inside it. */
|
|
84
92
|
export interface PaneCell {
|
|
85
93
|
paneId: string;
|
|
86
94
|
root: HTMLElement;
|
|
@@ -88,13 +96,16 @@ export interface PaneCell {
|
|
|
88
96
|
jump: HTMLElement;
|
|
89
97
|
chrome: HTMLElement;
|
|
90
98
|
stage: HTMLElement;
|
|
99
|
+
dropZone: HTMLElement;
|
|
91
100
|
surface: CanvasSurface;
|
|
92
101
|
}
|
|
93
102
|
|
|
94
|
-
/** A cell's record, plus the
|
|
103
|
+
/** A cell's record, plus the disposers its stage gestures and its drop zone live behind. */
|
|
95
104
|
interface CellState extends PaneCell {
|
|
96
105
|
/** Stage-gesture disposer, live between the stage's creation and the cell's disposal. */
|
|
97
106
|
releaseGestures: (() => void) | null;
|
|
107
|
+
/** The drop zone's pragmatic registration, live between its creation and the cell's disposal. */
|
|
108
|
+
releaseDropZone: (() => void) | null;
|
|
98
109
|
}
|
|
99
110
|
|
|
100
111
|
const _cells = new Map<string, CellState>();
|
|
@@ -110,6 +121,13 @@ let _drawn: string[] = [];
|
|
|
110
121
|
|
|
111
122
|
let _scope: EffectScope | null = null;
|
|
112
123
|
|
|
124
|
+
/**
|
|
125
|
+
* The monitor that ARMS the right-edge zones — `_surface.drag`'s one writer. It never resolves a
|
|
126
|
+
* drop; `panels/tab-strip.ts`'s own monitor does that, from the same drop targets this one only
|
|
127
|
+
* watches.
|
|
128
|
+
*/
|
|
129
|
+
let _dragMonitor: (() => void) | null = null;
|
|
130
|
+
|
|
113
131
|
/** The cell a pane is drawn in, or null. Test-visible, and the bootstrap's handle on the primary. */
|
|
114
132
|
export function cellForPane(paneId: string): PaneCell | null {
|
|
115
133
|
return _cells.get(paneId) ?? null;
|
|
@@ -120,7 +138,7 @@ export function cellForPane(paneId: string): PaneCell | null {
|
|
|
120
138
|
*
|
|
121
139
|
* The mount is asynchronous — the runtime waits for the kit to be defined and renders one microtask
|
|
122
140
|
* after that — so `mountShell()` starting the grid is not the same event as the grid existing. The
|
|
123
|
-
* bootstrap reads the primary cell's
|
|
141
|
+
* bootstrap reads the primary cell's five boxes on the line after it, exactly as `mountShellTree()`
|
|
124
142
|
* made every caller await the frame for the same reason.
|
|
125
143
|
*/
|
|
126
144
|
export function paneGridReady(): Promise<void> {
|
|
@@ -142,8 +160,10 @@ function cellState(paneId: string): CellState {
|
|
|
142
160
|
}
|
|
143
161
|
const cell: CellState = {
|
|
144
162
|
chrome: null as unknown as HTMLElement,
|
|
163
|
+
dropZone: null as unknown as HTMLElement,
|
|
145
164
|
jump: null as unknown as HTMLElement,
|
|
146
165
|
paneId,
|
|
166
|
+
releaseDropZone: null,
|
|
147
167
|
releaseGestures: null,
|
|
148
168
|
root: null as unknown as HTMLElement,
|
|
149
169
|
stage: null as unknown as HTMLElement,
|
|
@@ -217,7 +237,52 @@ function attachStage(cell: CellState, stage: HTMLElement): void {
|
|
|
217
237
|
}
|
|
218
238
|
|
|
219
239
|
/**
|
|
220
|
-
*
|
|
240
|
+
* The RIGHT BAND of `paneId`'s cell, and only while it is legal to drop there: the grid is still
|
|
241
|
+
* under {@link MAX_PANES}, and `paneId` names the LAST pane — the one a second pane would sit
|
|
242
|
+
* beside. A grid of two panes has no legal edge at all, so the band answers refused everywhere on
|
|
243
|
+
* it.
|
|
244
|
+
*
|
|
245
|
+
* The band itself is `min(120px, half the cell's own width)`: wide enough to aim at, narrow enough
|
|
246
|
+
* that the rest of the stage keeps ordinary canvas gestures — a drag never reaches this at all
|
|
247
|
+
* unless `canDrop` already accepted it, so the band only ever competes with itself.
|
|
248
|
+
*/
|
|
249
|
+
function edgeData(
|
|
250
|
+
paneId: string,
|
|
251
|
+
input: { clientX: number },
|
|
252
|
+
element: HTMLElement,
|
|
253
|
+
): Record<string, unknown> {
|
|
254
|
+
if (workspace.panes.length >= MAX_PANES || workspace.panes.at(-1)?.id !== paneId) {
|
|
255
|
+
return { type: "pane-edge-refused" };
|
|
256
|
+
}
|
|
257
|
+
const rect = rectOf(element);
|
|
258
|
+
const band = Math.min(120, rect.width / 2);
|
|
259
|
+
if (input.clientX < rect.right - band) {
|
|
260
|
+
return { type: "pane-edge-refused" };
|
|
261
|
+
}
|
|
262
|
+
return { paneId, type: "pane-edge" };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Make a cell's drop zone a pragmatic target. `canDrop` alone is not enough to keep the zone out of
|
|
267
|
+
* a two-pane grid's way — {@link edgeData} refuses in the DATA, the same "never fall through" rule
|
|
268
|
+
* `files.ts`'s row targets use, because the zone sits ABOVE the stage (`z-index: 4`) and a refusal
|
|
269
|
+
* that let the drop bubble past it would still have kept the pointer from ever reaching the iframe
|
|
270
|
+
* underneath.
|
|
271
|
+
*/
|
|
272
|
+
function attachDropZone(paneId: string, element: HTMLElement): () => void {
|
|
273
|
+
return dropTargetForElements({
|
|
274
|
+
canDrop({ source }) {
|
|
275
|
+
return isTabDropSource(source.data);
|
|
276
|
+
},
|
|
277
|
+
element,
|
|
278
|
+
getData({ input }) {
|
|
279
|
+
return edgeData(paneId, input, element);
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* One of a cell's six boxes has been created. Fill the record in, and wire what belongs to it.
|
|
221
286
|
*
|
|
222
287
|
* The two bars are HANDED their host rather than resolving a region, the same way
|
|
223
288
|
* `panels/frontmatter-panel.ts` is handed the stage. They cannot resolve one the way the tab strip
|
|
@@ -248,6 +313,11 @@ function cellPart(paneId: string, part: PaneCellPart, element: HTMLElement): voi
|
|
|
248
313
|
attachPaneChromeHost(paneId, element);
|
|
249
314
|
return;
|
|
250
315
|
}
|
|
316
|
+
if (part === "dropZone") {
|
|
317
|
+
cell.dropZone = element;
|
|
318
|
+
cell.releaseDropZone = attachDropZone(paneId, element);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
251
321
|
attachStage(cell, element);
|
|
252
322
|
}
|
|
253
323
|
|
|
@@ -272,6 +342,8 @@ function cellPart(paneId: string, part: PaneCellPart, element: HTMLElement): voi
|
|
|
272
342
|
* the record the hosts resolve through.
|
|
273
343
|
*/
|
|
274
344
|
function disposeCell(cell: CellState): void {
|
|
345
|
+
cell.releaseDropZone?.();
|
|
346
|
+
cell.releaseDropZone = null;
|
|
275
347
|
attachJumpBarHost(cell.paneId, null);
|
|
276
348
|
attachPaneChromeHost(cell.paneId, null);
|
|
277
349
|
cell.releaseGestures?.();
|
|
@@ -396,6 +468,22 @@ export function mount(): void {
|
|
|
396
468
|
{ cellPart, move: setPaneSplit, settle: persistDocks },
|
|
397
469
|
);
|
|
398
470
|
_ready = _surface.ready;
|
|
471
|
+
_dragMonitor = monitorForElements({
|
|
472
|
+
canMonitor({ source }) {
|
|
473
|
+
return isTabDropSource(source.data);
|
|
474
|
+
},
|
|
475
|
+
onDrag({ location }) {
|
|
476
|
+
const [target] = location.current.dropTargets;
|
|
477
|
+
const edgePane = target?.data.type === "pane-edge" ? (target.data.paneId as string) : "";
|
|
478
|
+
_surface?.drag({ armed: true, edgePane });
|
|
479
|
+
},
|
|
480
|
+
onDragStart() {
|
|
481
|
+
_surface?.drag({ armed: true, edgePane: "" });
|
|
482
|
+
},
|
|
483
|
+
onDrop() {
|
|
484
|
+
_surface?.drag({ armed: false, edgePane: "" });
|
|
485
|
+
},
|
|
486
|
+
});
|
|
399
487
|
_scope = effectScope();
|
|
400
488
|
_scope.run(() => {
|
|
401
489
|
effect(() => {
|
|
@@ -412,6 +500,8 @@ export function mount(): void {
|
|
|
412
500
|
export function unmount(): void {
|
|
413
501
|
_scope?.stop();
|
|
414
502
|
_scope = null;
|
|
503
|
+
_dragMonitor?.();
|
|
504
|
+
_dragMonitor = null;
|
|
415
505
|
/* Every cell is taken apart by the same ordered teardown a departing one gets, and BEFORE the
|
|
416
506
|
document goes: doing it afterwards would hand `releaseCanvasHosts` a stage whose frames the
|
|
417
507
|
runtime had already dropped. */
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Resolve a completed pragmatic drag over a tab strip or a pane's right edge into a model write.
|
|
4
|
+
*
|
|
5
|
+
* `panels/tab-strip.ts` registers every chip as a source and every strip and chip as a target,
|
|
6
|
+
* `panels/pane-grid.ts` registers the right-edge zone, and both hand their monitor's `onDrop` here
|
|
7
|
+
* with nothing decided yet — the DATA a target's `getData` produced, and nothing else. This module
|
|
8
|
+
* is the one place that DATA becomes a write, which is what keeps the two registration sites (a
|
|
9
|
+
* strip, a grid cell) from having to agree twice about what a drop means.
|
|
10
|
+
*
|
|
11
|
+
* No import of `tab-strip.ts` or `pane-grid.ts`: this module reaches the model through
|
|
12
|
+
* `workspace/workspace.ts` and `workspace/pane-derive.ts`, and the one opener through
|
|
13
|
+
* `files/files.ts` — the same three a keyboard command would use, because a drag and `⌘\` / "Open
|
|
14
|
+
* to the Side" are the same act by a different gesture (studio-ui-guidelines.md §8.2).
|
|
15
|
+
*
|
|
16
|
+
* @docs studio/interface/tabs
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
|
|
20
|
+
import { getReorderDestinationIndex } from "@atlaskit/pragmatic-drag-and-drop-hitbox/util/get-reorder-destination-index";
|
|
21
|
+
import type { Edge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
|
|
22
|
+
import {
|
|
23
|
+
activateTab,
|
|
24
|
+
closePane,
|
|
25
|
+
moveTab,
|
|
26
|
+
moveTabToPane,
|
|
27
|
+
paneById,
|
|
28
|
+
paneIsEmpty,
|
|
29
|
+
promoteTab,
|
|
30
|
+
receivingPane,
|
|
31
|
+
splitRight,
|
|
32
|
+
workspace,
|
|
33
|
+
} from "../workspace/workspace";
|
|
34
|
+
import { clearPaneDerivation } from "../workspace/pane-derive";
|
|
35
|
+
import { openFileInTab } from "../files/files";
|
|
36
|
+
|
|
37
|
+
/** A tab chip dragged off its own strip. */
|
|
38
|
+
export interface TabDragData extends Record<string, unknown> {
|
|
39
|
+
type: "tab";
|
|
40
|
+
tabId: string;
|
|
41
|
+
/**
|
|
42
|
+
* The pane the chip is being dragged FROM, read at drag start — a host can change which pane it
|
|
43
|
+
* draws, so this is never captured earlier than the gesture itself.
|
|
44
|
+
*/
|
|
45
|
+
paneId?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A file row dragged off the Files tree — `files.ts`'s own source shape, read structurally. */
|
|
49
|
+
export interface FileDragData extends Record<string, unknown> {
|
|
50
|
+
type: "file-tree";
|
|
51
|
+
entryType: string;
|
|
52
|
+
path: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type TabDropSourceData = TabDragData | FileDragData;
|
|
56
|
+
|
|
57
|
+
/** A slot in a strip — a chip's edge, or the strip's own empty tail when `tabId` is absent. */
|
|
58
|
+
export interface TabSlotTarget extends Record<string, unknown> {
|
|
59
|
+
type: "tab-slot";
|
|
60
|
+
paneId: string;
|
|
61
|
+
tabId?: string;
|
|
62
|
+
/** The target's own slot in `paneId`'s order at `getData` time — the tail's length, otherwise. */
|
|
63
|
+
index: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface TabSlotRefusedTarget extends Record<string, unknown> {
|
|
67
|
+
type: "tab-slot-refused";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The right band of the last pane's stage, armed only while the grid is under {@link MAX_PANES}. */
|
|
71
|
+
export interface PaneEdgeTarget extends Record<string, unknown> {
|
|
72
|
+
type: "pane-edge";
|
|
73
|
+
paneId: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface PaneEdgeRefusedTarget extends Record<string, unknown> {
|
|
77
|
+
type: "pane-edge-refused";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type TabDropTarget =
|
|
81
|
+
| TabSlotTarget
|
|
82
|
+
| TabSlotRefusedTarget
|
|
83
|
+
| PaneEdgeTarget
|
|
84
|
+
| PaneEdgeRefusedTarget;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Whether `data` — a drag source's own data, exactly as `getInitialData` produced it — is a source
|
|
88
|
+
* this module knows how to land. A directory row is refused HERE, in the data, rather than by
|
|
89
|
+
* omitting a `dropTargetForElements` registration: pragmatic-dnd bubbles a refusal to the next
|
|
90
|
+
* target OUT, and the next target out of a tab strip is the pane cell, which is not a target at all
|
|
91
|
+
* — so an unhandled refusal would not merely do nothing, it would do nothing silently in a way
|
|
92
|
+
* indistinguishable from a bug. Every caller answers `canDrop` with this, so a directory row (or
|
|
93
|
+
* anything else this module has no branch for) is refused at the SOURCE and never reaches a target
|
|
94
|
+
* closure at all.
|
|
95
|
+
*/
|
|
96
|
+
export function isTabDropSource(data: Record<string, unknown>): data is TabDropSourceData {
|
|
97
|
+
return data.type === "tab" || (data.type === "file-tree" && data.entryType === "file");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Land a tab at a specific slot of `paneId`, releasing any derivation the pane is borrowing FIRST —
|
|
102
|
+
* the same order `receivingPane` uses, and for the same reason: inserting into a `tabOrder` while
|
|
103
|
+
* `pane.derived` is still set is the one-frame D2 violation its own docstring describes, observable
|
|
104
|
+
* to anything that reads the pane between the two writes.
|
|
105
|
+
*/
|
|
106
|
+
function placeTab(tabId: string, paneId: string, index: number): void {
|
|
107
|
+
const pane = paneById(paneId);
|
|
108
|
+
if (pane?.derived) {
|
|
109
|
+
clearPaneDerivation(paneId);
|
|
110
|
+
}
|
|
111
|
+
const landed = moveTabToPane(tabId, paneId, index);
|
|
112
|
+
if (!landed) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// A drag is a commitment, exactly as a double-click promoting a preview tab is one.
|
|
116
|
+
promoteTab(tabId);
|
|
117
|
+
activateTab(tabId);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** {@link placeTab}'s file half: land the file's tab at `index`, opening it first if it is not. */
|
|
121
|
+
async function placeFile(path: string, paneId: string, index: number): Promise<void> {
|
|
122
|
+
const existing = [...workspace.tabs.values()].find((tab) => tab.documentPath === path);
|
|
123
|
+
if (existing) {
|
|
124
|
+
placeTab(existing.id, paneId, index);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
await openFileInTab(path, { focus: true, paneId });
|
|
128
|
+
// Idempotent by construction: `openFileInTab` may already have landed the tab at its default
|
|
129
|
+
// Slot, and this re-finds it by path and moves it to the slot the drop actually asked for.
|
|
130
|
+
const opened = [...workspace.tabs.values()].find((tab) => tab.documentPath === path);
|
|
131
|
+
if (opened) {
|
|
132
|
+
placeTab(opened.id, paneId, index);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The landing index a chip-edge target names, or the tail target's own index when there is none. */
|
|
137
|
+
function edgeIndex(target: TabSlotTarget, edge: Edge | null): number {
|
|
138
|
+
if (target.tabId === undefined) {
|
|
139
|
+
return target.index;
|
|
140
|
+
}
|
|
141
|
+
return edge === "left" ? target.index : target.index + 1;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function resolveTabSlotDrop(source: TabDropSourceData, target: TabSlotTarget): Promise<void> {
|
|
145
|
+
const edge = extractClosestEdge(target as unknown as Record<string | symbol, unknown>);
|
|
146
|
+
if (source.type === "tab") {
|
|
147
|
+
if (source.paneId === target.paneId) {
|
|
148
|
+
const pane = paneById(target.paneId);
|
|
149
|
+
if (!pane) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const startIndex = pane.tabOrder.indexOf(source.tabId);
|
|
153
|
+
if (startIndex === -1) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const to =
|
|
157
|
+
target.tabId === undefined
|
|
158
|
+
? pane.tabOrder.length - 1
|
|
159
|
+
: getReorderDestinationIndex({
|
|
160
|
+
axis: "horizontal",
|
|
161
|
+
closestEdgeOfTarget: edge,
|
|
162
|
+
indexOfTarget: target.index,
|
|
163
|
+
startIndex,
|
|
164
|
+
});
|
|
165
|
+
if (to !== startIndex) {
|
|
166
|
+
moveTab(source.tabId, to);
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
placeTab(source.tabId, target.paneId, edgeIndex(target, edge));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
await placeFile(source.path, target.paneId, edgeIndex(target, edge));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function resolvePaneEdgeDrop(
|
|
177
|
+
source: TabDropSourceData,
|
|
178
|
+
target: PaneEdgeTarget,
|
|
179
|
+
): Promise<void> {
|
|
180
|
+
if (source.type === "tab") {
|
|
181
|
+
// Ignores `target.paneId`: the split moves the tab beside ITS OWN pane, which is the edge zone's
|
|
182
|
+
// Pane by construction — the zone is only armed below `MAX_PANES`, so there is exactly one pane
|
|
183
|
+
// For a dragged tab to have come from.
|
|
184
|
+
const landed = splitRight(source.tabId);
|
|
185
|
+
if (landed) {
|
|
186
|
+
promoteTab(source.tabId);
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const pane = receivingPane(target.paneId);
|
|
191
|
+
await openFileInTab(source.path, { paneId: pane.id });
|
|
192
|
+
const landed = [...workspace.tabs.values()].some((tab) => tab.documentPath === source.path);
|
|
193
|
+
if (!landed && paneIsEmpty(pane)) {
|
|
194
|
+
// A refusal (a missing file, no editor for the format) must not leave a pane this drop minted
|
|
195
|
+
// Standing empty — the same rule `document.openToSide` applies to its own refusal.
|
|
196
|
+
closePane(pane.id);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Turn a completed drag into a write. `target` is the drop's own INNERMOST target data, or
|
|
202
|
+
* `undefined` for a drag cancelled off every target — both are legal and both mean nothing landed.
|
|
203
|
+
*/
|
|
204
|
+
export async function resolveTabDrop(
|
|
205
|
+
source: { data: Record<string, unknown> },
|
|
206
|
+
target: { data: Record<string, unknown> } | undefined,
|
|
207
|
+
): Promise<void> {
|
|
208
|
+
if (!target || !isTabDropSource(source.data)) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const sourceData = source.data;
|
|
212
|
+
if (target.data.type === "tab-slot") {
|
|
213
|
+
await resolveTabSlotDrop(sourceData, target.data as TabSlotTarget);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (target.data.type === "pane-edge") {
|
|
217
|
+
await resolvePaneEdgeDrop(sourceData, target.data as PaneEdgeTarget);
|
|
218
|
+
}
|
|
219
|
+
// `tab-slot-refused` / `pane-edge-refused` / anything else: nothing.
|
|
220
|
+
}
|