@jxsuite/studio 0.33.0 → 0.35.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 +6238 -0
- package/dist/iframe-entry.js.map +35 -0
- package/dist/studio.js +30972 -17222
- package/dist/studio.js.map +328 -83
- package/package.json +9 -9
- 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 +5 -1
- package/src/new-project/design-fields.ts +260 -0
- package/src/new-project/import-tab.ts +322 -0
- package/src/new-project/new-project-modal.ts +472 -89
- package/src/new-project/templates.ts +61 -0
- package/src/packages/ensure-deps.ts +6 -0
- package/src/packages/pull-package-sync.ts +339 -0
- package/src/page-params.ts +383 -0
- package/src/panels/ai-chat/attached-context.ts +49 -0
- package/src/panels/ai-chat/chat-markdown.ts +38 -0
- package/src/panels/ai-chat/chat-view.ts +250 -0
- package/src/panels/ai-chat/composer.ts +315 -0
- package/src/panels/ai-chat/sessions-view.ts +98 -0
- package/src/panels/ai-panel.ts +189 -457
- 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/git-panel.ts +16 -1
- package/src/panels/overlays.ts +10 -125
- package/src/panels/properties-panel.ts +210 -0
- package/src/panels/right-panel.ts +21 -7
- 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 +45 -1
- package/src/services/agent-seed.ts +91 -0
- package/src/services/ai-models.ts +64 -0
- package/src/services/ai-session-store.ts +250 -0
- package/src/services/ai-settings.ts +5 -0
- package/src/services/automation.ts +140 -0
- package/src/services/document-assistant.ts +98 -38
- package/src/services/import-client.ts +134 -0
- package/src/services/monaco-setup.ts +12 -0
- package/src/services/render-critic.ts +9 -9
- package/src/services/settings-store.ts +93 -0
- package/src/settings/css-vars-editor.ts +2 -2
- package/src/store.ts +4 -62
- package/src/studio.ts +115 -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 +2 -74
- package/src/types.ts +98 -18
- package/src/ui/ai-credentials-form.ts +205 -0
- package/src/ui/jx-theme.ts +63 -0
- package/src/ui/media-picker.ts +6 -4
- package/src/ui/spectrum.ts +13 -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
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
/// <reference lib="dom" />
|
|
2
|
-
/**
|
|
3
|
-
* Insertion-helper.js — Single floating "+" button for element insertion on the canvas.
|
|
4
|
-
*
|
|
5
|
-
* Uses CSS Anchor Positioning to attach to sibling boundaries and empty containers. Uses Native
|
|
6
|
-
* Observables (Chrome 135+) for declarative event handling.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { showSlashMenu } from "./slash-menu";
|
|
10
|
-
import { activeTab } from "../workspace/workspace";
|
|
11
|
-
import { mutateInsertNode, transactDoc } from "../tabs/transact";
|
|
12
|
-
import type { CanvasPanel } from "../types";
|
|
13
|
-
import type { JxPath } from "../state";
|
|
14
|
-
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
15
|
-
|
|
16
|
-
interface ObservableSubscription {
|
|
17
|
-
subscribe: ((observer: { next: (e: Event) => void }) => void) &
|
|
18
|
-
((observer: { next: (e: MouseEvent) => void }) => void);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface ObservableElement {
|
|
22
|
-
on: (event: string, options?: Record<string, unknown>) => ObservableSubscription;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface InsertionHelperContext {
|
|
26
|
-
getCanvasMode: () => string; // Returns the active canvas mode.
|
|
27
|
-
withPanelPointerEvents: (fn: () => unknown) => unknown; // Executes fn with pointer-events
|
|
28
|
-
effectiveZoom: () => number; // Returns the current zoom scale factor.
|
|
29
|
-
defaultDef: (tag: string) => JxMutableNode; // Creates a default element definition for a tag.
|
|
30
|
-
parentElementPath: (path: JxPath) => JxPath | null; // Returns the parent element path,
|
|
31
|
-
childIndex: (path: JxPath) => string | number; // Returns the child index within the
|
|
32
|
-
getNodeAtPath: (doc: JxMutableNode, path: JxPath) => JxMutableNode | null; // Retrieves
|
|
33
|
-
elToPath: WeakMap<Element, JxPath>; // Maps rendered DOM elements to their document
|
|
34
|
-
panel: CanvasPanel; // The active canvas panel.
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
38
|
-
|
|
39
|
-
// ─── State ───────────────────────────────────────────────────────────────────
|
|
40
|
-
|
|
41
|
-
let _ctx: InsertionHelperContext | null = null;
|
|
42
|
-
|
|
43
|
-
let _helper: HTMLElement | null = null;
|
|
44
|
-
|
|
45
|
-
let _currentAnchor: HTMLElement | null = null;
|
|
46
|
-
|
|
47
|
-
let _insertionPoint: {
|
|
48
|
-
edge: string;
|
|
49
|
-
path: JxPath;
|
|
50
|
-
parentPath: JxPath;
|
|
51
|
-
idx: number;
|
|
52
|
-
} | null = null;
|
|
53
|
-
|
|
54
|
-
let _abort: AbortController | null = null;
|
|
55
|
-
|
|
56
|
-
let _hideTimer: ReturnType<typeof setTimeout> | null = null;
|
|
57
|
-
|
|
58
|
-
// Edge detection threshold in pixels
|
|
59
|
-
const EDGE_THRESHOLD = 14;
|
|
60
|
-
|
|
61
|
-
// Delay before hiding to allow cursor to reach the button
|
|
62
|
-
const HIDE_DELAY = 300;
|
|
63
|
-
|
|
64
|
-
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Mount the insertion helper system.
|
|
68
|
-
*
|
|
69
|
-
* @param {InsertionHelperContext} ctx
|
|
70
|
-
*/
|
|
71
|
-
export function mount(ctx: InsertionHelperContext) {
|
|
72
|
-
_ctx = ctx;
|
|
73
|
-
const { panel } = ctx;
|
|
74
|
-
|
|
75
|
-
_helper = document.createElement("button");
|
|
76
|
-
_helper.className = "insertion-helper";
|
|
77
|
-
_helper.textContent = "+";
|
|
78
|
-
_helper.addEventListener("click", onHelperClick);
|
|
79
|
-
_helper.addEventListener("mouseenter", () => {
|
|
80
|
-
cancelHide();
|
|
81
|
-
});
|
|
82
|
-
_helper.addEventListener("mouseleave", () => {
|
|
83
|
-
scheduleHide();
|
|
84
|
-
});
|
|
85
|
-
panel.viewport.append(_helper);
|
|
86
|
-
|
|
87
|
-
_abort = new AbortController();
|
|
88
|
-
|
|
89
|
-
// Listen on viewport — overlayClk gets pointer-events:none during editing/selection
|
|
90
|
-
const viewport = panel.viewport as HTMLElement & ObservableElement;
|
|
91
|
-
if (typeof viewport.on === "function") {
|
|
92
|
-
viewport.on("mousemove", { signal: _abort.signal }).subscribe({ next: onMouseMove });
|
|
93
|
-
viewport.on("mouseleave", { signal: _abort.signal }).subscribe({ next: hide });
|
|
94
|
-
} else {
|
|
95
|
-
panel.viewport.addEventListener("mousemove", onMouseMove, {
|
|
96
|
-
signal: _abort.signal,
|
|
97
|
-
});
|
|
98
|
-
panel.viewport.addEventListener("mouseleave", hide, {
|
|
99
|
-
signal: _abort.signal,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function unmount() {
|
|
105
|
-
_abort?.abort();
|
|
106
|
-
_abort = null;
|
|
107
|
-
cancelHide();
|
|
108
|
-
if (_helper?.parentElement) {
|
|
109
|
-
_helper.remove();
|
|
110
|
-
}
|
|
111
|
-
clearAnchor();
|
|
112
|
-
_helper = null;
|
|
113
|
-
_ctx = null;
|
|
114
|
-
_insertionPoint = null;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ─── Detection ───────────────────────────────────────────────────────────────
|
|
118
|
-
|
|
119
|
-
/** @param {MouseEvent} e */
|
|
120
|
-
function onMouseMove(e: MouseEvent) {
|
|
121
|
-
if (!_ctx || !_helper) {
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const { getCanvasMode } = _ctx;
|
|
126
|
-
const mode = getCanvasMode();
|
|
127
|
-
if (mode !== "design" && mode !== "edit") {
|
|
128
|
-
hide();
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const { panel, withPanelPointerEvents, elToPath } = _ctx;
|
|
133
|
-
const el = withPanelPointerEvents(() =>
|
|
134
|
-
document.elementFromPoint(e.clientX, e.clientY),
|
|
135
|
-
) as HTMLElement | null;
|
|
136
|
-
|
|
137
|
-
if (!el || !panel.canvas.contains(el)) {
|
|
138
|
-
hide();
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const path = elToPath.get(el);
|
|
143
|
-
if (!path) {
|
|
144
|
-
hide();
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Empty container: show centered "+"
|
|
149
|
-
if (el.classList.contains("empty-container-placeholder")) {
|
|
150
|
-
showAt(el, "center", path, path, 0);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Root element — can't insert siblings above/below root
|
|
155
|
-
if (path.length === 0) {
|
|
156
|
-
hide();
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Determine layout direction of parent container
|
|
161
|
-
const parent = el.parentElement;
|
|
162
|
-
if (!parent) {
|
|
163
|
-
hide();
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const parentStyle = getComputedStyle(parent);
|
|
168
|
-
const { display } = parentStyle;
|
|
169
|
-
const isFlex = display === "flex" || display === "inline-flex";
|
|
170
|
-
const isGrid = display === "grid" || display === "inline-grid";
|
|
171
|
-
const isRow =
|
|
172
|
-
(isFlex && parentStyle.flexDirection.startsWith("row")) ||
|
|
173
|
-
(isGrid && parentStyle.gridAutoFlow?.startsWith("column"));
|
|
174
|
-
|
|
175
|
-
// Calculate relative position within element
|
|
176
|
-
const rect = el.getBoundingClientRect();
|
|
177
|
-
const parentPath = _ctx.parentElementPath(path);
|
|
178
|
-
if (!parentPath) {
|
|
179
|
-
hide();
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
const childIdx = _ctx.childIndex(path) as number;
|
|
183
|
-
|
|
184
|
-
if (isRow) {
|
|
185
|
-
const relX = e.clientX - rect.left;
|
|
186
|
-
if (relX < EDGE_THRESHOLD) {
|
|
187
|
-
showAt(el, "left", path, parentPath, childIdx);
|
|
188
|
-
} else if (rect.width - relX < EDGE_THRESHOLD) {
|
|
189
|
-
showAt(el, "right", path, parentPath, childIdx + 1);
|
|
190
|
-
} else {
|
|
191
|
-
hide();
|
|
192
|
-
}
|
|
193
|
-
} else {
|
|
194
|
-
const relY = e.clientY - rect.top;
|
|
195
|
-
if (relY < EDGE_THRESHOLD) {
|
|
196
|
-
showAt(el, "top", path, parentPath, childIdx);
|
|
197
|
-
} else if (rect.height - relY < EDGE_THRESHOLD) {
|
|
198
|
-
showAt(el, "bottom", path, parentPath, childIdx + 1);
|
|
199
|
-
} else {
|
|
200
|
-
hide();
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// ─── Show / Hide ─────────────────────────────────────────────────────────────
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* @param {HTMLElement} el
|
|
209
|
-
* @param {string} edge
|
|
210
|
-
* @param {JxPath} path
|
|
211
|
-
* @param {JxPath} parentPath
|
|
212
|
-
* @param {number} idx
|
|
213
|
-
*/
|
|
214
|
-
function showAt(el: HTMLElement, edge: string, path: JxPath, parentPath: JxPath, idx: number) {
|
|
215
|
-
if (!_helper) {
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Set CSS anchor on target element
|
|
220
|
-
if (_currentAnchor !== el) {
|
|
221
|
-
clearAnchor();
|
|
222
|
-
(el.style as CSSStyleDeclaration & Record<string, string>).anchorName = "--jx-insert";
|
|
223
|
-
_currentAnchor = el;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
_helper.dataset.edge = edge;
|
|
227
|
-
_helper.classList.add("visible");
|
|
228
|
-
_insertionPoint = { edge, idx, parentPath, path };
|
|
229
|
-
cancelHide();
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function scheduleHide() {
|
|
233
|
-
cancelHide();
|
|
234
|
-
_hideTimer = setTimeout(hideNow, HIDE_DELAY);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function cancelHide() {
|
|
238
|
-
if (_hideTimer !== null) {
|
|
239
|
-
clearTimeout(_hideTimer);
|
|
240
|
-
_hideTimer = null;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function hide() {
|
|
245
|
-
scheduleHide();
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function hideNow() {
|
|
249
|
-
_hideTimer = null;
|
|
250
|
-
if (!_helper) {
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
_helper.classList.remove("visible");
|
|
254
|
-
clearAnchor();
|
|
255
|
-
_insertionPoint = null;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function clearAnchor() {
|
|
259
|
-
if (_currentAnchor) {
|
|
260
|
-
(_currentAnchor.style as CSSStyleDeclaration & Record<string, string>).anchorName = "";
|
|
261
|
-
_currentAnchor = null;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// ─── Insertion ───────────────────────────────────────────────────────────────
|
|
266
|
-
|
|
267
|
-
function onHelperClick(e: MouseEvent) {
|
|
268
|
-
e.stopPropagation();
|
|
269
|
-
e.preventDefault();
|
|
270
|
-
|
|
271
|
-
if (!_ctx || !_helper || !_insertionPoint) {
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const captured = _insertionPoint;
|
|
276
|
-
showSlashMenu(_helper, "", {
|
|
277
|
-
onSelect: (cmd) => onSlashSelect(cmd, captured),
|
|
278
|
-
showFilter: true,
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* @param {{ label: string; tag: string; description?: string }} cmd
|
|
284
|
-
* @param {{ edge: string; path: JxPath; parentPath: JxPath; idx: number }} point
|
|
285
|
-
*/
|
|
286
|
-
function onSlashSelect(
|
|
287
|
-
cmd: { label: string; tag: string; description?: string },
|
|
288
|
-
point: { edge: string; path: JxPath; parentPath: JxPath; idx: number },
|
|
289
|
-
) {
|
|
290
|
-
if (!_ctx) {
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const { defaultDef } = _ctx;
|
|
295
|
-
const { parentPath, idx, edge } = point;
|
|
296
|
-
|
|
297
|
-
const newDef = defaultDef(cmd.tag);
|
|
298
|
-
const insertPath = edge === "center" ? point.path : parentPath;
|
|
299
|
-
const insertIdx = edge === "center" ? 0 : idx;
|
|
300
|
-
const newPath = [...insertPath, "children", insertIdx];
|
|
301
|
-
|
|
302
|
-
transactDoc(activeTab.value, (t) => {
|
|
303
|
-
mutateInsertNode(t, insertPath, insertIdx, newDef);
|
|
304
|
-
t.session.selection = newPath;
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
hide();
|
|
308
|
-
}
|
package/src/panels/canvas-dnd.ts
DELETED
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
/// <reference lib="dom" />
|
|
2
|
-
/// <reference lib="dom.iterable" />
|
|
3
|
-
/**
|
|
4
|
-
* Canvas DnD — extracted from studio.js (Phase 4m). Registers canvas elements as drag-and-drop
|
|
5
|
-
* targets using @atlaskit/pragmatic-drag-and-drop.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
dropTargetForElements,
|
|
10
|
-
monitorForElements,
|
|
11
|
-
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
12
|
-
|
|
13
|
-
import { VOID_ELEMENTS, canvasPanels, elToPath, getNodeAtPath, isAncestor } from "../store";
|
|
14
|
-
import { activeTab } from "../workspace/workspace";
|
|
15
|
-
import { view } from "../view";
|
|
16
|
-
import { applyDropInstruction } from "../panels/dnd";
|
|
17
|
-
import { effectiveZoom } from "../canvas/canvas-helpers";
|
|
18
|
-
|
|
19
|
-
import type { CanvasPanel } from "../types";
|
|
20
|
-
|
|
21
|
-
import type { JxPath } from "../state";
|
|
22
|
-
|
|
23
|
-
export type { CanvasPanel } from "../types";
|
|
24
|
-
|
|
25
|
-
interface DropInstruction {
|
|
26
|
-
type: "reorder-above" | "reorder-below" | "make-child";
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface DropResult {
|
|
30
|
-
instruction: DropInstruction;
|
|
31
|
-
referenceEl: HTMLElement;
|
|
32
|
-
targetPath: JxPath;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
let _activeDropEl: HTMLElement | null = null;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Register all canvas elements in a panel as DnD drop targets.
|
|
39
|
-
*
|
|
40
|
-
* @param {CanvasPanel} panel
|
|
41
|
-
*/
|
|
42
|
-
export function registerPanelDnD(panel: CanvasPanel) {
|
|
43
|
-
const { canvas, dropLine } = panel;
|
|
44
|
-
const allEls = canvas.querySelectorAll("*");
|
|
45
|
-
|
|
46
|
-
// Drop-target callbacks fire on EVERY target in the stack (innermost → outermost),
|
|
47
|
-
// And every canvas element is a drop target — so the indicator and the drop are
|
|
48
|
-
// Driven from the monitor using only the innermost target.
|
|
49
|
-
/** Innermost drop target if it belongs to this panel's canvas, else null */
|
|
50
|
-
const innermostCanvasTarget = (location: {
|
|
51
|
-
current: {
|
|
52
|
-
dropTargets: {
|
|
53
|
-
data: Record<string | symbol, unknown>;
|
|
54
|
-
element: Element;
|
|
55
|
-
}[];
|
|
56
|
-
};
|
|
57
|
-
}) => {
|
|
58
|
-
const [target] = location.current.dropTargets;
|
|
59
|
-
if (!target) {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
const tEl = target.element as HTMLElement;
|
|
63
|
-
const tPath = target.data.path;
|
|
64
|
-
if (!canvas.contains(tEl) || !Array.isArray(tPath)) {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
return { el: tEl, isLeaf: Boolean(target.data._isVoid), path: tPath as JxPath };
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const monitorCleanup = monitorForElements({
|
|
71
|
-
onDrag({ location }) {
|
|
72
|
-
view.lastDragInput = location.current.input;
|
|
73
|
-
const target = innermostCanvasTarget(location);
|
|
74
|
-
if (target) {
|
|
75
|
-
if (_activeDropEl && _activeDropEl !== target.el) {
|
|
76
|
-
_activeDropEl.classList.remove("canvas-drop-target");
|
|
77
|
-
}
|
|
78
|
-
_activeDropEl = target.el;
|
|
79
|
-
showCanvasDropIndicator(target.el, target.path, target.isLeaf, panel);
|
|
80
|
-
} else if (location.current.dropTargets.length > 0) {
|
|
81
|
-
// Pointer is over a non-canvas target (e.g. a layer row) — hide this panel's
|
|
82
|
-
// Indicator. When over dead space (no targets at all) keep the last indicator
|
|
83
|
-
// Visible so it persists for the whole drag.
|
|
84
|
-
if (_activeDropEl && canvas.contains(_activeDropEl)) {
|
|
85
|
-
_activeDropEl.classList.remove("canvas-drop-target");
|
|
86
|
-
_activeDropEl = null;
|
|
87
|
-
}
|
|
88
|
-
dropLine.style.display = "none";
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
onDragStart({ location }) {
|
|
92
|
-
view.lastDragInput = location.current.input;
|
|
93
|
-
for (const el of canvas.querySelectorAll("*")) {
|
|
94
|
-
(el as HTMLElement).style.pointerEvents = "auto";
|
|
95
|
-
}
|
|
96
|
-
for (const p of canvasPanels) {
|
|
97
|
-
p.overlayClk.style.pointerEvents = "none";
|
|
98
|
-
}
|
|
99
|
-
},
|
|
100
|
-
onDrop({ source, location }) {
|
|
101
|
-
const target = innermostCanvasTarget(location);
|
|
102
|
-
if (target) {
|
|
103
|
-
const { instruction, targetPath } = getCanvasDropResult(
|
|
104
|
-
target.el,
|
|
105
|
-
target.path,
|
|
106
|
-
target.isLeaf,
|
|
107
|
-
);
|
|
108
|
-
applyDropInstruction(instruction, source.data, targetPath);
|
|
109
|
-
}
|
|
110
|
-
_activeDropEl?.classList.remove("canvas-drop-target");
|
|
111
|
-
_activeDropEl = null;
|
|
112
|
-
for (const p of canvasPanels) {
|
|
113
|
-
if (p.dropLine) {
|
|
114
|
-
p.dropLine.style.display = "none";
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
view.lastDragInput = null;
|
|
118
|
-
for (const el of canvas.querySelectorAll("*")) {
|
|
119
|
-
(el as HTMLElement).style.pointerEvents = "none";
|
|
120
|
-
}
|
|
121
|
-
for (const p of canvasPanels) {
|
|
122
|
-
p.overlayClk.style.pointerEvents = "";
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
});
|
|
126
|
-
view.canvasDndCleanups.push(monitorCleanup);
|
|
127
|
-
|
|
128
|
-
for (const el of allEls) {
|
|
129
|
-
if (!elToPath.get(el)) {
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
registerElementDropTarget(el);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Register one canvas element as a drop target. Path and leaf-ness are read live from elToPath and
|
|
138
|
-
* the current document at drag time, so surgical patches that remap sibling paths never leave stale
|
|
139
|
-
* closures behind.
|
|
140
|
-
*
|
|
141
|
-
* @param {Element} el
|
|
142
|
-
*/
|
|
143
|
-
function registerElementDropTarget(el: Element) {
|
|
144
|
-
const cleanup = dropTargetForElements({
|
|
145
|
-
canDrop({ source }) {
|
|
146
|
-
const elPath = elToPath.get(el);
|
|
147
|
-
if (!elPath) {
|
|
148
|
-
return false;
|
|
149
|
-
}
|
|
150
|
-
const srcPath = source.data.path as JxPath | undefined;
|
|
151
|
-
if (srcPath && isAncestor(srcPath, elPath)) {
|
|
152
|
-
return false;
|
|
153
|
-
}
|
|
154
|
-
return true;
|
|
155
|
-
},
|
|
156
|
-
element: /** @type {HTMLElement} */ el,
|
|
157
|
-
getData() {
|
|
158
|
-
const elPath = elToPath.get(el) ?? [];
|
|
159
|
-
const document = activeTab.value?.doc.document;
|
|
160
|
-
const node = document ? getNodeAtPath(document, elPath) : undefined;
|
|
161
|
-
const tag = (node?.tagName || "div").toLowerCase();
|
|
162
|
-
const hasElementChildren =
|
|
163
|
-
Array.isArray(node?.children) &&
|
|
164
|
-
node.children.some((c: unknown) => c != null && typeof c === "object");
|
|
165
|
-
return { _isVoid: VOID_ELEMENTS.has(tag) || !hasElementChildren, path: elPath };
|
|
166
|
-
},
|
|
167
|
-
});
|
|
168
|
-
view.canvasDndCleanups.push(cleanup);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Register drop targets for a freshly patched-in subtree (root plus descendants). Cleanups join
|
|
173
|
-
* view.canvasDndCleanups and are released by the next full render like all canvas DnD handlers.
|
|
174
|
-
*
|
|
175
|
-
* @param {HTMLElement} rootEl
|
|
176
|
-
*/
|
|
177
|
-
export function registerSubtreeDnD(rootEl: HTMLElement) {
|
|
178
|
-
if (elToPath.get(rootEl)) {
|
|
179
|
-
registerElementDropTarget(rootEl);
|
|
180
|
-
}
|
|
181
|
-
for (const el of rootEl.querySelectorAll("*")) {
|
|
182
|
-
if (elToPath.get(el)) {
|
|
183
|
-
registerElementDropTarget(el);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* @param {HTMLElement} el
|
|
190
|
-
* @param {JxPath} elPath
|
|
191
|
-
* @param {boolean} isLeaf
|
|
192
|
-
* @returns {DropResult}
|
|
193
|
-
*/
|
|
194
|
-
function getCanvasDropResult(el: HTMLElement, elPath: JxPath, isLeaf: boolean): DropResult {
|
|
195
|
-
if (!view.lastDragInput) {
|
|
196
|
-
return {
|
|
197
|
-
instruction: { type: "make-child" },
|
|
198
|
-
referenceEl: el,
|
|
199
|
-
targetPath: elPath,
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
const y = view.lastDragInput.clientY;
|
|
203
|
-
|
|
204
|
-
if (elPath.length === 0) {
|
|
205
|
-
const children = [...el.children] as HTMLElement[];
|
|
206
|
-
if (children.length === 0) {
|
|
207
|
-
return {
|
|
208
|
-
instruction: { type: "make-child" },
|
|
209
|
-
referenceEl: el,
|
|
210
|
-
targetPath: elPath,
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
return nearestChildEdge(children, y, elPath);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const rect = el.getBoundingClientRect();
|
|
217
|
-
const relY = (y - rect.top) / rect.height;
|
|
218
|
-
|
|
219
|
-
if (isLeaf) {
|
|
220
|
-
const instruction =
|
|
221
|
-
relY < 0.5 ? { type: "reorder-above" as const } : { type: "reorder-below" as const };
|
|
222
|
-
return { instruction, referenceEl: el, targetPath: elPath };
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (relY < 0.25) {
|
|
226
|
-
return {
|
|
227
|
-
instruction: { type: "reorder-above" },
|
|
228
|
-
referenceEl: el,
|
|
229
|
-
targetPath: elPath,
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
if (relY > 0.75) {
|
|
233
|
-
return {
|
|
234
|
-
instruction: { type: "reorder-below" },
|
|
235
|
-
referenceEl: el,
|
|
236
|
-
targetPath: elPath,
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
return {
|
|
240
|
-
instruction: { type: "make-child" },
|
|
241
|
-
referenceEl: el,
|
|
242
|
-
targetPath: elPath,
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Find the nearest child edge to the cursor and return the appropriate instruction along with the
|
|
248
|
-
* reference child element and its path.
|
|
249
|
-
*
|
|
250
|
-
* @param {HTMLElement[]} children
|
|
251
|
-
* @param {number} cursorY
|
|
252
|
-
* @param {JxPath} parentPath
|
|
253
|
-
* @returns {DropResult}
|
|
254
|
-
*/
|
|
255
|
-
function nearestChildEdge(children: HTMLElement[], cursorY: number, parentPath: JxPath) {
|
|
256
|
-
let closestDist = Infinity;
|
|
257
|
-
let instruction = { type: "reorder-below" } as DropInstruction;
|
|
258
|
-
let closestIdx = children.length - 1;
|
|
259
|
-
|
|
260
|
-
for (let i = 0; i < children.length; i++) {
|
|
261
|
-
const rect = children[i]!.getBoundingClientRect();
|
|
262
|
-
const topDist = Math.abs(cursorY - rect.top);
|
|
263
|
-
const bottomDist = Math.abs(cursorY - rect.bottom);
|
|
264
|
-
|
|
265
|
-
if (topDist < closestDist) {
|
|
266
|
-
closestDist = topDist;
|
|
267
|
-
instruction = { type: "reorder-above" };
|
|
268
|
-
closestIdx = i;
|
|
269
|
-
}
|
|
270
|
-
if (bottomDist < closestDist) {
|
|
271
|
-
closestDist = bottomDist;
|
|
272
|
-
instruction = { type: "reorder-below" };
|
|
273
|
-
closestIdx = i;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const childPath = [...parentPath, "children", closestIdx];
|
|
278
|
-
return {
|
|
279
|
-
instruction,
|
|
280
|
-
referenceEl: children[closestIdx]!,
|
|
281
|
-
targetPath: childPath,
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* @param {HTMLElement} el
|
|
287
|
-
* @param {JxPath} elPath
|
|
288
|
-
* @param {boolean} isLeaf
|
|
289
|
-
* @param {CanvasPanel} panel
|
|
290
|
-
*/
|
|
291
|
-
function showCanvasDropIndicator(
|
|
292
|
-
el: HTMLElement,
|
|
293
|
-
elPath: JxPath,
|
|
294
|
-
isLeaf: boolean,
|
|
295
|
-
panel: CanvasPanel,
|
|
296
|
-
) {
|
|
297
|
-
const { instruction, referenceEl } = getCanvasDropResult(el, elPath, isLeaf);
|
|
298
|
-
const { dropLine, viewport } = panel;
|
|
299
|
-
|
|
300
|
-
const scale = effectiveZoom();
|
|
301
|
-
const wrapRect = viewport.getBoundingClientRect();
|
|
302
|
-
const refRect = referenceEl.getBoundingClientRect();
|
|
303
|
-
const left = (refRect.left - wrapRect.left + viewport.scrollLeft) / scale;
|
|
304
|
-
const width = refRect.width / scale;
|
|
305
|
-
|
|
306
|
-
if (instruction.type === "make-child") {
|
|
307
|
-
dropLine.style.display = "block";
|
|
308
|
-
dropLine.style.top = `${(refRect.top - wrapRect.top + viewport.scrollTop) / scale}px`;
|
|
309
|
-
dropLine.style.left = `${left}px`;
|
|
310
|
-
dropLine.style.width = `${width}px`;
|
|
311
|
-
dropLine.style.height = `${refRect.height / scale}px`;
|
|
312
|
-
dropLine.className = "canvas-drop-indicator inside";
|
|
313
|
-
el.classList.add("canvas-drop-target");
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
el.classList.remove("canvas-drop-target");
|
|
318
|
-
const top =
|
|
319
|
-
instruction.type === "reorder-above"
|
|
320
|
-
? (refRect.top - wrapRect.top + viewport.scrollTop) / scale
|
|
321
|
-
: (refRect.bottom - wrapRect.top + viewport.scrollTop) / scale;
|
|
322
|
-
|
|
323
|
-
dropLine.style.display = "block";
|
|
324
|
-
dropLine.style.top = `${top}px`;
|
|
325
|
-
dropLine.style.left = `${left}px`;
|
|
326
|
-
dropLine.style.width = `${width}px`;
|
|
327
|
-
dropLine.style.height = "";
|
|
328
|
-
dropLine.className = "canvas-drop-indicator line";
|
|
329
|
-
}
|