@jxsuite/studio 0.32.0 → 0.34.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/iframe-entry.js +6230 -0
- package/dist/iframe-entry.js.map +35 -0
- package/dist/studio.js +22791 -13709
- package/dist/studio.js.map +181 -74
- package/package.json +12 -7
- package/src/browse/browse.ts +11 -4
- package/src/canvas/canvas-helpers.ts +2 -56
- package/src/canvas/canvas-live-render.ts +102 -435
- package/src/canvas/canvas-origin.ts +66 -0
- package/src/canvas/canvas-patcher.ts +63 -403
- package/src/canvas/canvas-render.ts +70 -212
- package/src/canvas/canvas-utils.ts +37 -65
- package/src/canvas/iframe-channel.ts +154 -0
- package/src/canvas/iframe-drop.ts +484 -0
- package/src/canvas/iframe-entry.ts +600 -0
- package/src/canvas/iframe-host.ts +1373 -0
- package/src/canvas/iframe-inline-edit.ts +367 -0
- package/src/canvas/iframe-insert.ts +164 -0
- package/src/canvas/iframe-interaction.ts +176 -0
- package/src/canvas/iframe-keys.ts +85 -0
- package/src/canvas/iframe-overlay.ts +218 -0
- package/src/canvas/iframe-patch.ts +363 -0
- package/src/canvas/iframe-protocol.ts +361 -0
- package/src/canvas/iframe-render.ts +458 -0
- package/src/canvas/iframe-slash.ts +114 -0
- package/src/canvas/iframe-subtree.ts +113 -0
- package/src/canvas/path-mapping.ts +86 -0
- package/src/canvas/serialize-scope.ts +65 -0
- package/src/editor/canvas-context-menu.ts +40 -0
- package/src/editor/canvas-slash-bridge.ts +21 -0
- package/src/editor/context-menu.ts +2 -1
- package/src/editor/inline-edit-apply.ts +183 -0
- package/src/editor/inline-edit.ts +99 -21
- package/src/editor/inline-link.ts +89 -0
- package/src/editor/insert-zone-action.ts +35 -0
- package/src/editor/merge-tags.ts +26 -2
- package/src/editor/repeater-scope.ts +144 -0
- package/src/editor/shortcuts.ts +14 -28
- package/src/editor/slash-menu.ts +73 -42
- package/src/files/files.ts +2 -1
- package/src/page-params.ts +383 -0
- package/src/panels/ai-panel.ts +386 -328
- package/src/panels/block-action-bar.ts +296 -138
- package/src/panels/canvas-dnd-bridge.ts +397 -0
- package/src/panels/component-preview.ts +56 -0
- package/src/panels/dnd.ts +41 -17
- package/src/panels/drag-ghost.ts +62 -0
- package/src/panels/editors.ts +1 -1
- package/src/panels/overlays.ts +10 -125
- package/src/panels/properties-panel.ts +210 -0
- package/src/panels/right-panel.ts +0 -2
- package/src/panels/signals-panel.ts +136 -22
- package/src/panels/stylebook-doc.ts +373 -0
- package/src/panels/stylebook-panel.ts +46 -689
- package/src/panels/tab-bar.ts +159 -13
- package/src/panels/toolbar.ts +3 -2
- package/src/platforms/devserver.ts +18 -47
- package/src/services/ai-settings.ts +107 -0
- package/src/services/ai-system-prompt.ts +617 -0
- package/src/services/ai-tools.ts +854 -0
- package/src/services/context-manager.ts +200 -0
- package/src/services/document-assistant.ts +183 -0
- package/src/services/jx-validate.ts +65 -0
- package/src/services/monaco-setup.ts +12 -0
- package/src/services/render-critic.ts +75 -0
- package/src/services/token-lint.ts +140 -0
- package/src/services/tool-executor.ts +156 -0
- package/src/settings/css-vars-editor.ts +2 -2
- package/src/state.ts +29 -0
- package/src/store.ts +4 -62
- package/src/studio.ts +90 -40
- package/src/tabs/doc-op-apply.ts +89 -0
- package/src/tabs/patch-ops.ts +6 -2
- package/src/tabs/tab.ts +23 -4
- package/src/tabs/transact.ts +33 -69
- package/src/types.ts +16 -24
- package/src/ui/jx-theme.ts +63 -0
- package/src/ui/media-picker.ts +6 -4
- package/src/ui/spectrum.ts +5 -0
- package/src/utils/canvas-media.ts +0 -137
- package/src/utils/edit-display.ts +23 -3
- package/src/utils/geometry.ts +43 -0
- package/src/utils/link-target.ts +93 -0
- package/src/utils/strip-events.ts +54 -0
- package/src/view.ts +0 -23
- package/src/workspace/workspace.ts +14 -1
- package/src/canvas/canvas-subtree-render.ts +0 -113
- package/src/editor/component-inline-edit.ts +0 -349
- package/src/editor/content-inline-edit.ts +0 -207
- package/src/editor/insertion-helper.ts +0 -308
- package/src/panels/canvas-dnd.ts +0 -329
- package/src/panels/panel-events.ts +0 -306
- package/src/panels/preview-render.ts +0 -132
- package/src/panels/pseudo-preview.ts +0 -75
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Cross-frame canvas DnD coordinator (Phase 4c spike). The canvas is an iframe, and pragmatic-dnd
|
|
4
|
+
* is per-realm, so the parent keeps ONE `monitorForElements` for its drag SOURCES and drives the
|
|
5
|
+
* cross-frame leg as a pointer/message stream: it resolves the target iframe host by hit-testing
|
|
6
|
+
* the pointer against each live host's rect, posts `dragStart`/`dragMove`/`drop`, and the iframe
|
|
7
|
+
* replies with `dragOver`/`dropResult` (handled in iframe-host). The applied mutation runs through
|
|
8
|
+
* the realm-agnostic `applyDropInstruction` with PARENT-retained source data (never crossing the
|
|
9
|
+
* wire).
|
|
10
|
+
*
|
|
11
|
+
* Sources: palette/element/component cards ({type:'block'}, flow 1) and layer rows + the
|
|
12
|
+
* block-action-bar ⠿ handle ({type:'tree-node', path}, flows 2 & 4). Flow 3 (grab-anywhere) enters
|
|
13
|
+
* the SAME coordinator via the iframe's `dragOriginate` ({@link startIframeOriginatedDrag}). The
|
|
14
|
+
* iframe's drop math rejects a tree-node dropped onto its own ancestor/self (canDrop), so no
|
|
15
|
+
* coordinator-side guard is needed.
|
|
16
|
+
*
|
|
17
|
+
* The coordinator is PARENT-authoritative for boundary + host selection + cancel (D-4): each move
|
|
18
|
+
* re-resolves the host by cursor hit-test ({@link liveDragHostAt}) and migrates the session across
|
|
19
|
+
* panels (dragEnd old + dragStart new); a cancel ({@link isCancelDrop}) tears down without
|
|
20
|
+
* applying; a drop with no reply within {@link DROP_RESULT_TIMEOUT} clears the affordances. The
|
|
21
|
+
* drag ghost (D-3) follows the raw pointer 1:1; the drop indicator is drawn host-side from the
|
|
22
|
+
* iframe's `dragOver` preview.
|
|
23
|
+
*
|
|
24
|
+
* The coordinate math is factored into the PURE {@link buildDragMessages} (injected scale + iframe
|
|
25
|
+
* rect) so the cursor conversion is unit-tested without a live iframe or transform.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
29
|
+
import {
|
|
30
|
+
adoptDragSession,
|
|
31
|
+
beginDragSession,
|
|
32
|
+
clearDropIndicator,
|
|
33
|
+
currentDragSession,
|
|
34
|
+
endDragSession,
|
|
35
|
+
hostDragGeometry,
|
|
36
|
+
liveDragHostAt,
|
|
37
|
+
postDragMessage,
|
|
38
|
+
sawIframeDragOver,
|
|
39
|
+
setIframeOriginateHandler,
|
|
40
|
+
setNativeDragEnterHandler,
|
|
41
|
+
} from "../canvas/iframe-host";
|
|
42
|
+
import { parentCursorToIframe } from "../canvas/iframe-overlay";
|
|
43
|
+
import { clearDragGhost, moveDragGhost, setDragGhost } from "./drag-ghost";
|
|
44
|
+
import { getNodeAtPath } from "../store";
|
|
45
|
+
import { activeTab } from "../workspace/workspace";
|
|
46
|
+
import type { DragHost } from "../canvas/iframe-host";
|
|
47
|
+
import type { DragSrcKind, ParentToIframe } from "../canvas/iframe-protocol";
|
|
48
|
+
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
49
|
+
|
|
50
|
+
/** A parent-viewport pointer position (pragmatic's `location.current.input`). */
|
|
51
|
+
interface Cursor {
|
|
52
|
+
x: number;
|
|
53
|
+
y: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface MonitorDragArgs {
|
|
57
|
+
source: { data: Record<string, unknown> };
|
|
58
|
+
location: {
|
|
59
|
+
current: { input: { clientX: number; clientY: number } };
|
|
60
|
+
// Pragmatic resets `current` back to `initial` when a drag is CANCELLED (Escape / dropped on no
|
|
61
|
+
// Valid target). The canvas isn't a parent pragmatic drop target, so we detect cancel by that
|
|
62
|
+
// Snap-back rather than an explicit flag (single-sourced through pragmatic, per the spec).
|
|
63
|
+
initial?: { input: { clientX: number; clientY: number } };
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* PURE: build the `dragMove` + `drop` parent→iframe messages for a cursor, given the session id and
|
|
69
|
+
* the target iframe's EMPIRICAL `scale` + `rect` (both injected so this is testable without a DOM).
|
|
70
|
+
* The cursor is converted to iframe-viewport coords via {@link parentCursorToIframe} (divide by
|
|
71
|
+
* scale; the iframe rect's left/top cancel the pan). Both leg messages share the one conversion so
|
|
72
|
+
* the move-preview and the authoritative drop never use a divergent coordinate transform.
|
|
73
|
+
*/
|
|
74
|
+
export function buildDragMessages(
|
|
75
|
+
cursor: Cursor,
|
|
76
|
+
dragSeq: number,
|
|
77
|
+
scale: number,
|
|
78
|
+
iframeRect: { left: number; top: number },
|
|
79
|
+
): { move: ParentToIframe; drop: ParentToIframe } {
|
|
80
|
+
const local = parentCursorToIframe(cursor, iframeRect, scale);
|
|
81
|
+
return {
|
|
82
|
+
drop: { cursor: local, dragSeq, kind: "drop" },
|
|
83
|
+
move: { cursor: local, dragSeq, kind: "dragMove" },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The cursor of a monitor event in parent-viewport coords. */
|
|
88
|
+
function cursorOf(location: MonitorDragArgs["location"]): Cursor {
|
|
89
|
+
return { x: location.current.input.clientX, y: location.current.input.clientY };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Map a pragmatic source's `data` to a realm-agnostic {@link DragSrcKind}, or null when this source
|
|
94
|
+
* doesn't drive the canvas coordinator. Palette/element/component cards are `{type:'block'}` (the
|
|
95
|
+
* fragment is retained parent-side); layer rows + the block-action-bar ⠿ handle are
|
|
96
|
+
* `{type:'tree-node', path}` (a move — the source's existing document path crosses as the kind, and
|
|
97
|
+
* the iframe's `canDrop` rejects ancestor/self targets). Anything else (e.g. a row missing a path)
|
|
98
|
+
* is ignored.
|
|
99
|
+
*/
|
|
100
|
+
function dragSrcOf(data: Record<string, unknown>): DragSrcKind | null {
|
|
101
|
+
if (data.type === "block") {
|
|
102
|
+
return { type: "block" };
|
|
103
|
+
}
|
|
104
|
+
if (data.type === "tree-node" && Array.isArray(data.path)) {
|
|
105
|
+
return { path: data.path as (string | number)[], type: "tree-node" };
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The ghost label for a drag (Phase 4c). A block source uses its fragment's tag (the card label); a
|
|
112
|
+
* tree-node source uses the tag of the node at its path in the live doc (a node chip). Falls back
|
|
113
|
+
* to a generic label when the tag is unknown.
|
|
114
|
+
*/
|
|
115
|
+
export function ghostLabel(src: DragSrcKind, data: Record<string, unknown>): string {
|
|
116
|
+
if (src.type === "block") {
|
|
117
|
+
const fragment = data.fragment as JxMutableNode | undefined;
|
|
118
|
+
return fragment?.tagName ?? "block";
|
|
119
|
+
}
|
|
120
|
+
const doc = activeTab.value?.doc.document as JxMutableNode | undefined;
|
|
121
|
+
const node = doc ? (getNodeAtPath(doc, src.path) as JxMutableNode | undefined) : undefined;
|
|
122
|
+
return node?.tagName ?? "node";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Adopt a flow-3 (iframe-ORIGINATED + iframe-DRIVEN) drag session. A drag that begins inside the
|
|
127
|
+
* canvas iframe gets its held-button pointermoves in the IFRAME document (implicit pointer
|
|
128
|
+
* capture), so the parent would never receive them — the iframe therefore drives the whole gesture
|
|
129
|
+
* itself, computing the preview/drop locally and posting `dragOver`/`dropResult` directly.
|
|
130
|
+
*
|
|
131
|
+
* The parent's only job here is to ADOPT the iframe's `seq` (so those replies pass the host's seq
|
|
132
|
+
* gate, see {@link file://../canvas/iframe-host.ts}'s `adoptDragSession`) and SHOW the ghost; the
|
|
133
|
+
* host's `dragOver` handler then positions the ghost from the `cursor` the iframe posts. The parent
|
|
134
|
+
* attaches NO document pointer listeners — it has no pointer of its own during this drag. `path` is
|
|
135
|
+
* the grabbed node's document path; `seq` is the iframe's pre-allocated session id.
|
|
136
|
+
*/
|
|
137
|
+
export function startIframeOriginatedDrag(
|
|
138
|
+
host: DragHost,
|
|
139
|
+
path: (string | number)[],
|
|
140
|
+
seq: number,
|
|
141
|
+
): void {
|
|
142
|
+
const src: DragSrcKind = { path: [...path], type: "tree-node" };
|
|
143
|
+
const srcData = { path: [...path], type: "tree-node" };
|
|
144
|
+
// Adopt the iframe's seq + retain the (path-only) source data; do NOT post dragStart (the iframe
|
|
145
|
+
// Is driving) and do NOT attach parent-document listeners (the iframe owns the pointer it started).
|
|
146
|
+
adoptDragSession(host, src, srcData, seq);
|
|
147
|
+
// Show the ghost; the host's dragOver handler moves it from the iframe-posted cursor each move.
|
|
148
|
+
setDragGhost(ghostLabel(src, srcData), 0, 0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Timeout (ms) after a `drop` post before the parent gives up waiting for a `dropResult` and clears
|
|
153
|
+
* the ghost/indicator (the iframe reloaded mid-drag, so no reply will come — treat as cancel).
|
|
154
|
+
*/
|
|
155
|
+
const DROP_RESULT_TIMEOUT = 250;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* A live coordinator session. PARENT-authoritative for boundary + host selection (D-4): `host` is
|
|
159
|
+
* the panel the session is currently bound to (its iframe got the latest `dragStart`); `null` while
|
|
160
|
+
* the pointer is off every canvas. `seq` is the session id the bound host's iframe replies with.
|
|
161
|
+
*/
|
|
162
|
+
interface CoordSession {
|
|
163
|
+
src: DragSrcKind;
|
|
164
|
+
srcData: Record<string, unknown>;
|
|
165
|
+
/** The host whose iframe currently holds the session, or null while off-canvas. */
|
|
166
|
+
host: DragHost | null;
|
|
167
|
+
/** The session id the bound host was started with (for move/drop tagging). */
|
|
168
|
+
seq: number;
|
|
169
|
+
/** The drag ghost's label (computed once at start; the dragged thing doesn't change). */
|
|
170
|
+
label: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Begin a session bound to `host`: bump+post dragStart, show the ghost, record the seq. */
|
|
174
|
+
function startSession(
|
|
175
|
+
host: DragHost,
|
|
176
|
+
src: DragSrcKind,
|
|
177
|
+
srcData: Record<string, unknown>,
|
|
178
|
+
): CoordSession {
|
|
179
|
+
const seq = beginDragSession(host, src, srcData);
|
|
180
|
+
return { host, label: ghostLabel(src, srcData), seq, src, srcData };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Advance a session for a new cursor (parent-authoritative boundary, D-4). Moves the ghost 1:1,
|
|
185
|
+
* then resolves the host under the cursor and reconciles:
|
|
186
|
+
*
|
|
187
|
+
* - Same host → post dragMove.
|
|
188
|
+
* - Different host (cross-panel) → MIGRATE: dragEnd the old, dragStart the new (a fresh seq), so the
|
|
189
|
+
* new panel's iframe drives the rest.
|
|
190
|
+
* - No host (inside→outside) → dragEnd the old + unbind; the ghost keeps following off-canvas.
|
|
191
|
+
*/
|
|
192
|
+
function sessionDrag(session: CoordSession, cursor: Cursor): void {
|
|
193
|
+
moveDragGhost(cursor.x, cursor.y);
|
|
194
|
+
const target = liveDragHostAt(cursor);
|
|
195
|
+
if (target === session.host) {
|
|
196
|
+
if (target) {
|
|
197
|
+
const { rect, scale } = hostDragGeometry(target);
|
|
198
|
+
postDragMessage(target, buildDragMessages(cursor, session.seq, scale, rect).move);
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
// The bound host changed (migrate) or the pointer left every canvas (unbind).
|
|
203
|
+
if (session.host) {
|
|
204
|
+
postDragMessage(session.host, { dragSeq: session.seq, kind: "dragEnd" });
|
|
205
|
+
}
|
|
206
|
+
if (!target) {
|
|
207
|
+
session.host = null;
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
// Migrate the session to the new panel: a fresh dragStart (new seq) the new iframe replies with.
|
|
211
|
+
session.host = target;
|
|
212
|
+
session.seq = beginDragSession(target, session.src, session.srcData);
|
|
213
|
+
const { rect, scale } = hostDragGeometry(target);
|
|
214
|
+
postDragMessage(target, buildDragMessages(cursor, session.seq, scale, rect).move);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Finish a session at the drop cursor. Posts `drop` to the host under the cursor (the pointer's
|
|
219
|
+
* panel owns the drop, not the active panel); the iframe computes the drop FRESH and posts
|
|
220
|
+
* `dropResult`, which iframe-host applies + clears the ghost/indicator. A timeout fallback clears
|
|
221
|
+
* the affordances if no reply arrives (iframe reloaded mid-drag). Drops off-canvas release retained
|
|
222
|
+
* data.
|
|
223
|
+
*/
|
|
224
|
+
function sessionDrop(session: CoordSession, cursor: Cursor): void {
|
|
225
|
+
const target = liveDragHostAt(cursor);
|
|
226
|
+
// Migrate first if the drop landed on a different panel than the last move bound.
|
|
227
|
+
if (target && target !== session.host) {
|
|
228
|
+
if (session.host) {
|
|
229
|
+
postDragMessage(session.host, { dragSeq: session.seq, kind: "dragEnd" });
|
|
230
|
+
}
|
|
231
|
+
session.host = target;
|
|
232
|
+
session.seq = beginDragSession(target, session.src, session.srcData);
|
|
233
|
+
}
|
|
234
|
+
if (!target) {
|
|
235
|
+
if (session.host) {
|
|
236
|
+
postDragMessage(session.host, { dragSeq: session.seq, kind: "dragEnd" });
|
|
237
|
+
}
|
|
238
|
+
endDragSession(session.seq);
|
|
239
|
+
clearDragGhost();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const { rect, scale } = hostDragGeometry(target);
|
|
243
|
+
postDragMessage(target, buildDragMessages(cursor, session.seq, scale, rect).drop);
|
|
244
|
+
// The iframe-host's dropResult handler clears the ghost/indicator on a reply; guard against a
|
|
245
|
+
// Reload that never replies by clearing after a short timeout (treated as a cancel).
|
|
246
|
+
const { seq } = session;
|
|
247
|
+
setTimeout(() => {
|
|
248
|
+
if (currentDragSession() === seq) {
|
|
249
|
+
clearDragGhost();
|
|
250
|
+
clearDropIndicator(target);
|
|
251
|
+
}
|
|
252
|
+
}, DROP_RESULT_TIMEOUT);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Tear down a session WITHOUT applying a drop (Escape/abort): dragEnd the bound host so it clears
|
|
257
|
+
* its indicator + stops auto-scroll, release the retained source data, hide the ghost.
|
|
258
|
+
*/
|
|
259
|
+
function sessionCancel(session: CoordSession): void {
|
|
260
|
+
if (session.host) {
|
|
261
|
+
postDragMessage(session.host, { dragSeq: session.seq, kind: "dragEnd" });
|
|
262
|
+
clearDropIndicator(session.host);
|
|
263
|
+
}
|
|
264
|
+
endDragSession(session.seq);
|
|
265
|
+
clearDragGhost();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Whether a pragmatic `onDrop` represents a CANCEL: pragmatic snaps `current` back to `initial` on
|
|
270
|
+
* Escape/abort, so equal input coords (with an `initial` present) signal a cancel rather than a
|
|
271
|
+
* real release. PURE.
|
|
272
|
+
*/
|
|
273
|
+
export function isCancelDrop(location: MonitorDragArgs["location"]): boolean {
|
|
274
|
+
const { initial } = location;
|
|
275
|
+
if (!initial) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
return (
|
|
279
|
+
initial.input.clientX === location.current.input.clientX &&
|
|
280
|
+
initial.input.clientY === location.current.input.clientY
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Register the single coordinator monitor. Returns a cleanup. Wired once from studio init; the
|
|
286
|
+
* monitor stays for the app's lifetime (pragmatic monitors are global, not per-render).
|
|
287
|
+
*/
|
|
288
|
+
export function registerCanvasDndBridge(): () => void {
|
|
289
|
+
// The active parent-source (pragmatic) session, or null between drags / before it binds a host.
|
|
290
|
+
let session: CoordSession | null = null;
|
|
291
|
+
// The in-flight pragmatic source (set in onDragStart, cleared in onDrop) so a nativeDragEnter can
|
|
292
|
+
// Bind a session for a drag whose cursor entered an iframe before the parent ever bound a host.
|
|
293
|
+
let pending: { src: DragSrcKind; srcData: Record<string, unknown> } | null = null;
|
|
294
|
+
|
|
295
|
+
// Install the flow-3 handler so the iframe-host's `dragOriginate` case enters this coordinator.
|
|
296
|
+
// The iframe drives the gesture; the parent only adopts the seq + shows the ghost (no listeners).
|
|
297
|
+
setIframeOriginateHandler((host, path, seq) => startIframeOriginatedDrag(host, path, seq));
|
|
298
|
+
|
|
299
|
+
// Bind/migrate the live pragmatic session when its NATIVE stream enters an unclaimed iframe:
|
|
300
|
+
// Chromium delivers dragover to the frame under the cursor, so a drag from the palette/layers
|
|
301
|
+
// Crosses onto the canvas without this monitor ever seeing a cursor inside the iframe rect — the
|
|
302
|
+
// Iframe announces the crossing instead (the `nativeDragEnter` message).
|
|
303
|
+
setNativeDragEnterHandler((host) => {
|
|
304
|
+
if (!pending || session?.host === host) {
|
|
305
|
+
// No parent drag in flight (e.g. an OS file drag), or already bound here — nothing to do.
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (session) {
|
|
309
|
+
// Migrate: the session was bound elsewhere (or unbound after leaving a canvas).
|
|
310
|
+
if (session.host) {
|
|
311
|
+
postDragMessage(session.host, { dragSeq: session.seq, kind: "dragEnd" });
|
|
312
|
+
}
|
|
313
|
+
session.host = host;
|
|
314
|
+
session.seq = beginDragSession(host, session.src, session.srcData);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
// First canvas contact for this drag — the ghost is already up (set in onDragStart).
|
|
318
|
+
session = startSession(host, pending.src, pending.srcData);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
return monitorForElements({
|
|
322
|
+
onDrag({ source, location }: MonitorDragArgs) {
|
|
323
|
+
const src = dragSrcOf(source.data);
|
|
324
|
+
if (!src) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const cursor = cursorOf(location);
|
|
328
|
+
if (session) {
|
|
329
|
+
sessionDrag(session, cursor);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
// The drag started off-canvas (no host bound yet); bind lazily when the pointer enters one.
|
|
333
|
+
const host = liveDragHostAt(cursor);
|
|
334
|
+
if (host) {
|
|
335
|
+
session = startSession(host, src, source.data);
|
|
336
|
+
setDragGhost(session.label, cursor.x, cursor.y);
|
|
337
|
+
sessionDrag(session, cursor);
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
onDragStart({ source, location }: MonitorDragArgs) {
|
|
341
|
+
const src = dragSrcOf(source.data);
|
|
342
|
+
if (!src) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
// Retain the source for the drag's lifetime so a nativeDragEnter can bind late (see above).
|
|
346
|
+
pending = { src, srcData: source.data };
|
|
347
|
+
const cursor = cursorOf(location);
|
|
348
|
+
const host = liveDragHostAt(cursor);
|
|
349
|
+
// Bind the session to the host under the cursor (if any). The ghost shows immediately and
|
|
350
|
+
// Follows the pointer even before it enters a canvas (lazy bind happens in onDrag otherwise).
|
|
351
|
+
session = host ? startSession(host, src, source.data) : null;
|
|
352
|
+
setDragGhost(ghostLabel(src, source.data), cursor.x, cursor.y);
|
|
353
|
+
},
|
|
354
|
+
onDrop({ source, location }: MonitorDragArgs) {
|
|
355
|
+
const src = dragSrcOf(source.data);
|
|
356
|
+
if (!src) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
pending = null;
|
|
360
|
+
const cursor = location.current.input ? cursorOf(location) : { x: 0, y: 0 };
|
|
361
|
+
if (!session) {
|
|
362
|
+
// Never bound a host (dropped off every canvas) — just hide the ghost; nothing to apply.
|
|
363
|
+
clearDragGhost();
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
// While the cursor is over the canvas, the native over/drop stream goes to the IFRAME — this
|
|
367
|
+
// Monitor sees nothing, so pragmatic's location is stale: its cancel heuristic false-positives
|
|
368
|
+
// (current never advanced past initial) and its coords are frame-local. When the iframe
|
|
369
|
+
// Recently drove the session, its native-drop dropResult is the authoritative outcome and is
|
|
370
|
+
// In flight: keep the retained srcData for it and only schedule the no-reply fallback teardown
|
|
371
|
+
// (which also covers an Escape-cancel over the iframe — no dropResult ever arrives).
|
|
372
|
+
if (sawIframeDragOver(session.seq)) {
|
|
373
|
+
const { host, seq } = session;
|
|
374
|
+
session = null;
|
|
375
|
+
setTimeout(() => {
|
|
376
|
+
if (currentDragSession() !== seq) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (host) {
|
|
380
|
+
postDragMessage(host, { dragSeq: seq, kind: "dragEnd" });
|
|
381
|
+
clearDropIndicator(host);
|
|
382
|
+
}
|
|
383
|
+
endDragSession(seq);
|
|
384
|
+
clearDragGhost();
|
|
385
|
+
}, DROP_RESULT_TIMEOUT);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
// A cancel (Escape/abort) snaps back to the initial cursor — tear down without applying.
|
|
389
|
+
if (isCancelDrop(location)) {
|
|
390
|
+
sessionCancel(session);
|
|
391
|
+
} else {
|
|
392
|
+
sessionDrop(session, cursor);
|
|
393
|
+
}
|
|
394
|
+
session = null;
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Parent-realm live component preview — registers a project component's custom element and
|
|
4
|
+
* instantiates it in the editor document. Used by the browse grid and the components-palette DnD
|
|
5
|
+
* cards. (The stylebook renders components inside the canvas iframe instead, via `$elements` refs
|
|
6
|
+
* in its generated specimen doc.)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { defineElement, setSkipServerFunctions } from "@jxsuite/runtime";
|
|
10
|
+
import { projectState } from "../store";
|
|
11
|
+
import type { ComponentEntry } from "../files/components";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Render a live component preview by registering its custom element and instantiating it.
|
|
15
|
+
*
|
|
16
|
+
* @param {ComponentEntry} comp
|
|
17
|
+
* @returns {Promise<HTMLElement>}
|
|
18
|
+
*/
|
|
19
|
+
export async function renderComponentPreview(comp: ComponentEntry) {
|
|
20
|
+
setSkipServerFunctions(true);
|
|
21
|
+
try {
|
|
22
|
+
if (comp.source === "npm") {
|
|
23
|
+
if (!customElements.get(comp.tagName)) {
|
|
24
|
+
return _componentFallback(comp.tagName);
|
|
25
|
+
}
|
|
26
|
+
} else {
|
|
27
|
+
if (comp.path && !comp.path.endsWith(".json")) {
|
|
28
|
+
// Format-class component sources (e.g. markdown) can't be imported as modules
|
|
29
|
+
return _componentFallback(comp.tagName);
|
|
30
|
+
}
|
|
31
|
+
const root = projectState?.projectRoot;
|
|
32
|
+
const url = `${location.origin}/${root ? `${root}/` : ""}${comp.path}`;
|
|
33
|
+
await defineElement(url);
|
|
34
|
+
}
|
|
35
|
+
const el = document.createElement(comp.tagName);
|
|
36
|
+
for (const p of comp.props || []) {
|
|
37
|
+
if (p.default !== undefined && p.default !== "false" && p.default !== "''") {
|
|
38
|
+
const val = String(p.default).replaceAll(/^'|'$/g, "");
|
|
39
|
+
el.setAttribute(p.name, val);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return el;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.warn("Component preview failed:", comp.tagName, error);
|
|
45
|
+
return _componentFallback(comp.tagName);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** @param {string} tagName */
|
|
50
|
+
function _componentFallback(tagName: string) {
|
|
51
|
+
const fallback = document.createElement("div");
|
|
52
|
+
fallback.style.cssText =
|
|
53
|
+
"padding:12px;border:1px dashed var(--border);border-radius:var(--radius);color:var(--fg-dim)";
|
|
54
|
+
fallback.textContent = `<${tagName}>`;
|
|
55
|
+
return fallback;
|
|
56
|
+
}
|
package/src/panels/dnd.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
monitorForElements,
|
|
11
11
|
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
12
12
|
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
|
13
|
+
import { disableNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/disable-native-drag-preview";
|
|
13
14
|
import {
|
|
14
15
|
attachInstruction,
|
|
15
16
|
extractInstruction,
|
|
@@ -32,9 +33,11 @@ import {
|
|
|
32
33
|
componentRegistry,
|
|
33
34
|
computeRelativePath,
|
|
34
35
|
} from "../files/components";
|
|
35
|
-
import { renderComponentPreview } from "./
|
|
36
|
+
import { renderComponentPreview } from "./component-preview";
|
|
36
37
|
import { defaultDef, unsafeTags } from "./shared";
|
|
38
|
+
import { elementAtPoint } from "../utils/geometry";
|
|
37
39
|
import type { JxPath } from "../state";
|
|
40
|
+
import type { Tab } from "../tabs/tab";
|
|
38
41
|
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
39
42
|
import type { ComponentEntry } from "../files/components.js";
|
|
40
43
|
|
|
@@ -43,6 +46,11 @@ interface DragCanDragArgs {
|
|
|
43
46
|
input: { clientX: number; clientY: number };
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
/** The `onGenerateDragPreview` argument subset we forward to {@link disableNativeDragPreview}. */
|
|
50
|
+
interface DragPreviewArgs {
|
|
51
|
+
nativeSetDragImage: ((image: Element, x: number, y: number) => void) | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
interface DragDropSourceArgs {
|
|
47
55
|
source: { data: Record<string, unknown>; element: HTMLElement };
|
|
48
56
|
}
|
|
@@ -75,7 +83,7 @@ export function registerLayersDnD() {
|
|
|
75
83
|
const cleanup = combine(
|
|
76
84
|
draggable({
|
|
77
85
|
canDrag({ element: _el, input }: DragCanDragArgs) {
|
|
78
|
-
const target =
|
|
86
|
+
const target = elementAtPoint(input.clientX, input.clientY) as HTMLElement;
|
|
79
87
|
if (target?.closest(".layer-actions")) {
|
|
80
88
|
return false;
|
|
81
89
|
}
|
|
@@ -85,6 +93,11 @@ export function registerLayersDnD() {
|
|
|
85
93
|
getInitialData() {
|
|
86
94
|
return { path: rowPath, type: "tree-node" };
|
|
87
95
|
},
|
|
96
|
+
onGenerateDragPreview({ nativeSetDragImage }: DragPreviewArgs) {
|
|
97
|
+
// Suppress the browser's native drag image — the cross-frame ghost (Phase 4c) is the
|
|
98
|
+
// Only drag affordance, so a duplicate native preview would double up.
|
|
99
|
+
disableNativeDragPreview({ nativeSetDragImage });
|
|
100
|
+
},
|
|
88
101
|
onDragStart() {
|
|
89
102
|
row.classList.add("dragging");
|
|
90
103
|
view.layerDragSourceHeight = row.offsetHeight;
|
|
@@ -162,7 +175,8 @@ export function registerLayersDnD() {
|
|
|
162
175
|
const srcRow = srcData.type === "tree-node" && source.element;
|
|
163
176
|
const wasExpanded = srcRow && Object.hasOwn(srcRow.dataset, "dndExpanded");
|
|
164
177
|
|
|
165
|
-
|
|
178
|
+
// Parent-originated layer drops legitimately target the active tab.
|
|
179
|
+
applyDropInstruction(activeTab.value, instruction, srcData, targetPath);
|
|
166
180
|
|
|
167
181
|
if (wasExpanded) {
|
|
168
182
|
const tab = activeTab.value;
|
|
@@ -221,6 +235,9 @@ export function registerComponentsDnD() {
|
|
|
221
235
|
getInitialData() {
|
|
222
236
|
return { fragment: structuredClone(instanceDef), type: "block" };
|
|
223
237
|
},
|
|
238
|
+
onGenerateDragPreview({ nativeSetDragImage }: DragPreviewArgs) {
|
|
239
|
+
disableNativeDragPreview({ nativeSetDragImage });
|
|
240
|
+
},
|
|
224
241
|
});
|
|
225
242
|
view.dndCleanups.push(cleanup);
|
|
226
243
|
}
|
|
@@ -248,6 +265,9 @@ export function registerElementsDnD() {
|
|
|
248
265
|
getInitialData() {
|
|
249
266
|
return { fragment: structuredClone(def), type: "block" };
|
|
250
267
|
},
|
|
268
|
+
onGenerateDragPreview({ nativeSetDragImage }: DragPreviewArgs) {
|
|
269
|
+
disableNativeDragPreview({ nativeSetDragImage });
|
|
270
|
+
},
|
|
251
271
|
});
|
|
252
272
|
view.dndCleanups.push(cleanup);
|
|
253
273
|
}
|
|
@@ -337,18 +357,25 @@ export function clearLayerDropGap(container: HTMLElement) {
|
|
|
337
357
|
}
|
|
338
358
|
|
|
339
359
|
/**
|
|
340
|
-
* Apply a DnD instruction to the
|
|
360
|
+
* Apply a DnD instruction to `tab`'s document. `tab` is the tab whose canvas the drop resolved in
|
|
361
|
+
* (host-routed for iframe drops — never the active tab at message time, which may have changed
|
|
362
|
+
* while the dropResult was in flight); a null tab is a no-op.
|
|
341
363
|
*
|
|
364
|
+
* @param {Tab | null} tab
|
|
342
365
|
* @param {{ type: string }} instruction
|
|
343
366
|
* @param {Record<string, unknown>} srcData
|
|
344
367
|
* @param {JxPath} targetPath
|
|
345
368
|
*/
|
|
346
369
|
export function applyDropInstruction(
|
|
370
|
+
tab: Tab | null,
|
|
347
371
|
instruction: { type: string },
|
|
348
372
|
srcData: Record<string, unknown>,
|
|
349
373
|
targetPath: JxPath,
|
|
350
374
|
) {
|
|
351
|
-
|
|
375
|
+
if (!tab) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const doc = tab.doc.document as JxMutableNode;
|
|
352
379
|
if (srcData.type === "tree-node") {
|
|
353
380
|
const fromPath = srcData.path as JxPath;
|
|
354
381
|
const targetParent = parentElementPath(targetPath) as JxPath;
|
|
@@ -361,19 +388,17 @@ export function applyDropInstruction(
|
|
|
361
388
|
|
|
362
389
|
switch (instruction.type) {
|
|
363
390
|
case "reorder-above": {
|
|
364
|
-
transactDoc(
|
|
391
|
+
transactDoc(tab, (t) => mutateMoveNode(t, fromPath, targetParent, targetIdx));
|
|
365
392
|
break;
|
|
366
393
|
}
|
|
367
394
|
case "reorder-below": {
|
|
368
|
-
transactDoc(
|
|
369
|
-
mutateMoveNode(t, fromPath, targetParent, targetIdx + 1),
|
|
370
|
-
);
|
|
395
|
+
transactDoc(tab, (t) => mutateMoveNode(t, fromPath, targetParent, targetIdx + 1));
|
|
371
396
|
break;
|
|
372
397
|
}
|
|
373
398
|
case "make-child": {
|
|
374
399
|
const target = getNodeAtPath(doc, targetPath);
|
|
375
400
|
const len = childList(target).length;
|
|
376
|
-
transactDoc(
|
|
401
|
+
transactDoc(tab, (t) => mutateMoveNode(t, fromPath, targetPath, len));
|
|
377
402
|
break;
|
|
378
403
|
}
|
|
379
404
|
default: {
|
|
@@ -389,7 +414,7 @@ export function applyDropInstruction(
|
|
|
389
414
|
|
|
390
415
|
switch (instruction.type) {
|
|
391
416
|
case "reorder-above": {
|
|
392
|
-
transactDoc(
|
|
417
|
+
transactDoc(tab, (t) =>
|
|
393
418
|
mutateInsertNode(
|
|
394
419
|
t,
|
|
395
420
|
targetParent,
|
|
@@ -400,7 +425,7 @@ export function applyDropInstruction(
|
|
|
400
425
|
break;
|
|
401
426
|
}
|
|
402
427
|
case "reorder-below": {
|
|
403
|
-
transactDoc(
|
|
428
|
+
transactDoc(tab, (t) =>
|
|
404
429
|
mutateInsertNode(
|
|
405
430
|
t,
|
|
406
431
|
targetParent,
|
|
@@ -413,7 +438,7 @@ export function applyDropInstruction(
|
|
|
413
438
|
case "make-child": {
|
|
414
439
|
const target = getNodeAtPath(doc, targetPath);
|
|
415
440
|
const len = childList(target).length;
|
|
416
|
-
transactDoc(
|
|
441
|
+
transactDoc(tab, (t) =>
|
|
417
442
|
mutateInsertNode(t, targetPath, len, structuredClone(srcData.fragment as JxMutableNode)),
|
|
418
443
|
);
|
|
419
444
|
break;
|
|
@@ -429,8 +454,7 @@ export function applyDropInstruction(
|
|
|
429
454
|
if (tag && tag.includes("-")) {
|
|
430
455
|
const comp = componentRegistry.find((c: ComponentEntry) => c.tagName === tag);
|
|
431
456
|
if (comp) {
|
|
432
|
-
const
|
|
433
|
-
const elements = tab?.doc.document?.$elements || [];
|
|
457
|
+
const elements = tab.doc.document?.$elements || [];
|
|
434
458
|
if (comp.source === "npm") {
|
|
435
459
|
const specifier = comp.modulePath ? `${comp.package}/${comp.modulePath}` : comp.package;
|
|
436
460
|
if (!specifier) {
|
|
@@ -440,7 +464,7 @@ export function applyDropInstruction(
|
|
|
440
464
|
(e: JxMutableNode | string | { $ref: string }) => e === specifier || e === comp.package,
|
|
441
465
|
);
|
|
442
466
|
if (!alreadyImported) {
|
|
443
|
-
transact(
|
|
467
|
+
transact(tab, (d: JxMutableNode) => {
|
|
444
468
|
if (!d.$elements) {
|
|
445
469
|
d.$elements = [];
|
|
446
470
|
}
|
|
@@ -459,7 +483,7 @@ export function applyDropInstruction(
|
|
|
459
483
|
});
|
|
460
484
|
if (!alreadyImported && comp.path) {
|
|
461
485
|
const relPath = computeRelativePath(tab?.documentPath ?? null, comp.path);
|
|
462
|
-
transact(
|
|
486
|
+
transact(tab, (d: JxMutableNode) => {
|
|
463
487
|
if (!d.$elements) {
|
|
464
488
|
d.$elements = [];
|
|
465
489
|
}
|