@jxsuite/studio 0.10.2 → 0.13.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/README.md +82 -0
- package/dist/studio.js +5114 -3424
- package/dist/studio.js.map +65 -49
- package/package.json +6 -3
- package/src/browse/browse.js +27 -3
- package/src/canvas/canvas-diff.js +184 -0
- package/src/canvas/canvas-helpers.js +10 -14
- package/src/canvas/canvas-live-render.js +136 -17
- package/src/canvas/canvas-render.js +154 -21
- package/src/canvas/canvas-utils.js +21 -23
- package/src/editor/component-inline-edit.js +54 -41
- package/src/editor/content-inline-edit.js +46 -47
- package/src/editor/context-menu.js +63 -39
- package/src/editor/convert-to-component.js +11 -14
- package/src/editor/insertion-helper.js +8 -10
- package/src/editor/shortcuts.js +69 -39
- package/src/files/components.js +15 -4
- package/src/files/files.js +72 -24
- package/src/panels/activity-bar.js +29 -7
- package/src/panels/block-action-bar.js +104 -80
- package/src/panels/canvas-dnd.js +132 -50
- package/src/panels/dnd.js +32 -28
- package/src/panels/editors.js +7 -14
- package/src/panels/elements-panel.js +9 -3
- package/src/panels/events-panel.js +44 -39
- package/src/panels/git-panel.js +45 -3
- package/src/panels/layers-panel.js +16 -11
- package/src/panels/left-panel.js +108 -43
- package/src/panels/overlays.js +97 -35
- package/src/panels/panel-events.js +18 -11
- package/src/panels/properties-panel.js +467 -98
- package/src/panels/right-panel.js +85 -37
- package/src/panels/shared.js +0 -22
- package/src/panels/signals-panel.js +125 -54
- package/src/panels/statusbar.js +23 -8
- package/src/panels/style-inputs.js +4 -2
- package/src/panels/style-panel.js +128 -105
- package/src/panels/stylebook-panel.js +42 -17
- package/src/panels/tab-strip.js +124 -0
- package/src/panels/toolbar.js +39 -9
- package/src/reactivity.js +28 -0
- package/src/settings/content-types-editor.js +78 -8
- package/src/settings/defs-editor.js +56 -7
- package/src/settings/schema-field-ui.js +99 -11
- package/src/site-context.js +105 -0
- package/src/state.js +0 -456
- package/src/store.js +105 -124
- package/src/studio.js +112 -121
- package/src/tabs/tab.js +153 -0
- package/src/tabs/transact.js +406 -0
- package/src/ui/spectrum.js +2 -0
- package/src/view.js +3 -0
- package/src/workspace/workspace.js +89 -0
|
@@ -6,18 +6,9 @@
|
|
|
6
6
|
import { html, render as litRender, nothing } from "lit-html";
|
|
7
7
|
import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
updateSession,
|
|
13
|
-
renderOnly,
|
|
14
|
-
selectNode,
|
|
15
|
-
moveNode,
|
|
16
|
-
getNodeAtPath,
|
|
17
|
-
nodeLabel,
|
|
18
|
-
parentElementPath,
|
|
19
|
-
childIndex,
|
|
20
|
-
} from "../store.js";
|
|
9
|
+
import { getNodeAtPath, nodeLabel, parentElementPath, childIndex } from "../store.js";
|
|
10
|
+
import { activeTab } from "../workspace/workspace.js";
|
|
11
|
+
import { transactDoc, mutateMoveNode } from "../tabs/transact.js";
|
|
21
12
|
import { view } from "../view.js";
|
|
22
13
|
import { isEditing, getActiveElement, getInlineActions } from "../editor/inline-edit.js";
|
|
23
14
|
import { toggleInlineFormat, isTagActiveInSelection } from "../editor/inline-format.js";
|
|
@@ -25,7 +16,21 @@ import { componentRegistry } from "../files/components.js";
|
|
|
25
16
|
import { convertToComponent } from "../editor/convert-to-component.js";
|
|
26
17
|
import { findCanvasElement, getActivePanel } from "../canvas/canvas-helpers.js";
|
|
27
18
|
|
|
28
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {import("../state.js").StudioState} StudioState
|
|
21
|
+
*
|
|
22
|
+
* @typedef {import("../state.js").JxPath} JxPath
|
|
23
|
+
*
|
|
24
|
+
* @typedef {{ command: string; tag: string; label: string; icon: string; shortcut?: string }} InlineAction
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @type {{
|
|
29
|
+
* getCanvasMode: () => string;
|
|
30
|
+
* navigateToComponent: (path: string) => void;
|
|
31
|
+
* createFloatingContainer: () => HTMLElement;
|
|
32
|
+
* } | null}
|
|
33
|
+
*/
|
|
29
34
|
let _ctx = null;
|
|
30
35
|
|
|
31
36
|
/**
|
|
@@ -33,8 +38,8 @@ let _ctx = null;
|
|
|
33
38
|
*
|
|
34
39
|
* @param {{
|
|
35
40
|
* getCanvasMode: () => string;
|
|
36
|
-
* navigateToComponent:
|
|
37
|
-
* createFloatingContainer:
|
|
41
|
+
* navigateToComponent: (path: string) => void;
|
|
42
|
+
* createFloatingContainer: () => HTMLElement;
|
|
38
43
|
* }} ctx
|
|
39
44
|
*/
|
|
40
45
|
export function initBlockActionBar(ctx) {
|
|
@@ -45,7 +50,7 @@ export function initBlockActionBar(ctx) {
|
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
/** Pre-built icon templates for inline format buttons (avoids unsafeStatic) */
|
|
48
|
-
const formatIconMap = /** @type {Record<string,
|
|
53
|
+
const formatIconMap = /** @type {Record<string, import("lit-html").TemplateResult>} */ ({
|
|
49
54
|
"sp-icon-text-bold": html`<sp-icon-text-bold slot="icon"></sp-icon-text-bold>`,
|
|
50
55
|
"sp-icon-text-italic": html`<sp-icon-text-italic slot="icon"></sp-icon-text-italic>`,
|
|
51
56
|
"sp-icon-text-underline": html`<sp-icon-text-underline slot="icon"></sp-icon-text-underline>`,
|
|
@@ -63,11 +68,11 @@ const formatIconMap = /** @type {Record<string, any>} */ ({
|
|
|
63
68
|
/**
|
|
64
69
|
* Prevent the bar from stealing focus from contenteditable
|
|
65
70
|
*
|
|
66
|
-
* @param {
|
|
71
|
+
* @param {MouseEvent} e
|
|
67
72
|
*/
|
|
68
73
|
function onBarMousedown(e) {
|
|
69
|
-
if (e.target.closest("sp-textfield")) return;
|
|
70
|
-
if (e.target.closest(".bar-drag-handle")) return;
|
|
74
|
+
if (/** @type {HTMLElement} */ (e.target).closest("sp-textfield")) return;
|
|
75
|
+
if (/** @type {HTMLElement} */ (e.target).closest(".bar-drag-handle")) return;
|
|
71
76
|
e.preventDefault();
|
|
72
77
|
}
|
|
73
78
|
|
|
@@ -78,42 +83,48 @@ function captureSelectionRange() {
|
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
/**
|
|
81
|
-
* @param {
|
|
82
|
-
* @param {
|
|
86
|
+
* @param {MouseEvent} e
|
|
87
|
+
* @param {InlineAction} action
|
|
83
88
|
*/
|
|
84
89
|
function onFormatClick(e, action) {
|
|
85
90
|
e.stopPropagation();
|
|
86
91
|
if (action.command === "link") {
|
|
87
|
-
showLinkPopover(
|
|
92
|
+
showLinkPopover(
|
|
93
|
+
/** @type {HTMLElement} */ (
|
|
94
|
+
/** @type {HTMLElement} */ (e.target).closest("sp-action-button")
|
|
95
|
+
),
|
|
96
|
+
);
|
|
88
97
|
} else if (view.savedRange) {
|
|
89
|
-
const sel =
|
|
98
|
+
const sel = window.getSelection();
|
|
90
99
|
const anchor = view.savedRange.startContainer;
|
|
91
100
|
const editableRoot = (
|
|
92
|
-
anchor?.nodeType === Node.ELEMENT_NODE
|
|
101
|
+
anchor?.nodeType === Node.ELEMENT_NODE
|
|
102
|
+
? /** @type {Element} */ (anchor)
|
|
103
|
+
: anchor?.parentElement
|
|
93
104
|
)?.closest("[contenteditable]");
|
|
94
105
|
if (editableRoot) {
|
|
95
|
-
editableRoot.focus();
|
|
96
|
-
sel
|
|
97
|
-
sel
|
|
106
|
+
/** @type {HTMLElement} */ (editableRoot).focus();
|
|
107
|
+
sel?.removeAllRanges();
|
|
108
|
+
sel?.addRange(view.savedRange);
|
|
98
109
|
applyInlineFormat(action);
|
|
99
110
|
}
|
|
100
111
|
}
|
|
101
112
|
}
|
|
102
113
|
|
|
103
114
|
function renderParentSelector() {
|
|
104
|
-
const
|
|
105
|
-
|
|
115
|
+
const tab = activeTab.value;
|
|
116
|
+
if (!tab?.session.selection) return nothing;
|
|
117
|
+
const pPath = parentElementPath(tab.session.selection);
|
|
106
118
|
if (!pPath) return nothing;
|
|
107
|
-
const parentNode = getNodeAtPath(
|
|
119
|
+
const parentNode = getNodeAtPath(tab.doc.document, pPath);
|
|
108
120
|
return html`
|
|
109
121
|
<sp-action-button
|
|
110
122
|
size="xs"
|
|
111
123
|
quiet
|
|
112
124
|
title="Select parent: ${nodeLabel(parentNode)}"
|
|
113
|
-
@click=${(/** @type {
|
|
125
|
+
@click=${(/** @type {MouseEvent} */ e) => {
|
|
114
126
|
e.stopPropagation();
|
|
115
|
-
|
|
116
|
-
update(selectNode(S, pPath));
|
|
127
|
+
activeTab.value.session.selection = pPath;
|
|
117
128
|
}}
|
|
118
129
|
>
|
|
119
130
|
<sp-icon-back slot="icon"></sp-icon-back>
|
|
@@ -122,10 +133,12 @@ function renderParentSelector() {
|
|
|
122
133
|
}
|
|
123
134
|
|
|
124
135
|
function renderMoveArrows() {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
const
|
|
136
|
+
const tab = activeTab.value;
|
|
137
|
+
if (!tab?.session.selection) return nothing;
|
|
138
|
+
const sel = tab.session.selection;
|
|
139
|
+
const idx = /** @type {number} */ (childIndex(sel));
|
|
140
|
+
const pPath = parentElementPath(sel);
|
|
141
|
+
const parentNode = getNodeAtPath(tab.doc.document, /** @type {JxPath} */ (pPath));
|
|
129
142
|
const siblings = parentNode?.children;
|
|
130
143
|
return html`
|
|
131
144
|
<sp-action-button
|
|
@@ -133,7 +146,7 @@ function renderMoveArrows() {
|
|
|
133
146
|
quiet
|
|
134
147
|
title="Move up"
|
|
135
148
|
?disabled=${idx <= 0}
|
|
136
|
-
@click=${(/** @type {
|
|
149
|
+
@click=${(/** @type {MouseEvent} */ e) => {
|
|
137
150
|
e.stopPropagation();
|
|
138
151
|
moveSelectionUp();
|
|
139
152
|
}}
|
|
@@ -145,7 +158,7 @@ function renderMoveArrows() {
|
|
|
145
158
|
quiet
|
|
146
159
|
title="Move down"
|
|
147
160
|
?disabled=${!siblings || idx >= siblings.length - 1}
|
|
148
|
-
@click=${(/** @type {
|
|
161
|
+
@click=${(/** @type {MouseEvent} */ e) => {
|
|
149
162
|
e.stopPropagation();
|
|
150
163
|
moveSelectionDown();
|
|
151
164
|
}}
|
|
@@ -158,10 +171,10 @@ function renderMoveArrows() {
|
|
|
158
171
|
/**
|
|
159
172
|
* Apply an inline format action.
|
|
160
173
|
*
|
|
161
|
-
* @param {
|
|
174
|
+
* @param {InlineAction} action
|
|
162
175
|
*/
|
|
163
176
|
function applyInlineFormat(action) {
|
|
164
|
-
/** @type {Record<string,
|
|
177
|
+
/** @type {Record<string, string>} */
|
|
165
178
|
const cmdToTag = {
|
|
166
179
|
bold: "strong",
|
|
167
180
|
italic: "em",
|
|
@@ -190,19 +203,22 @@ export function dismissBlockActionBar() {
|
|
|
190
203
|
if (view.blockActionBarEl) litRender(nothing, view.blockActionBarEl);
|
|
191
204
|
}
|
|
192
205
|
|
|
193
|
-
/** @param {
|
|
206
|
+
/** @param {HTMLElement} anchorBtn */
|
|
194
207
|
function showLinkPopover(anchorBtn) {
|
|
195
208
|
litRender(nothing, view.linkPopoverHost);
|
|
196
209
|
|
|
197
210
|
const sel = window.getSelection();
|
|
198
|
-
/** @type {
|
|
211
|
+
/** @type {HTMLAnchorElement | null} */
|
|
199
212
|
let existingLink = null;
|
|
200
213
|
if (sel?.rangeCount) {
|
|
201
|
-
/** @type {
|
|
214
|
+
/** @type {Node | null} */
|
|
202
215
|
let node = sel.anchorNode;
|
|
203
216
|
while (node && node !== document.body) {
|
|
204
|
-
if (
|
|
205
|
-
|
|
217
|
+
if (
|
|
218
|
+
node.nodeType === Node.ELEMENT_NODE &&
|
|
219
|
+
/** @type {Element} */ (node).tagName.toLowerCase() === "a"
|
|
220
|
+
) {
|
|
221
|
+
existingLink = /** @type {HTMLAnchorElement} */ (node);
|
|
206
222
|
break;
|
|
207
223
|
}
|
|
208
224
|
node = node.parentNode;
|
|
@@ -212,8 +228,10 @@ function showLinkPopover(anchorBtn) {
|
|
|
212
228
|
const rect = anchorBtn.getBoundingClientRect();
|
|
213
229
|
|
|
214
230
|
const onApply = () => {
|
|
215
|
-
const field =
|
|
216
|
-
|
|
231
|
+
const field = /** @type {HTMLInputElement | null} */ (
|
|
232
|
+
view.linkPopoverHost.querySelector("sp-textfield")
|
|
233
|
+
);
|
|
234
|
+
const url = field?.value || "";
|
|
217
235
|
if (existingLink) {
|
|
218
236
|
existingLink.setAttribute("href", url);
|
|
219
237
|
} else if (url) {
|
|
@@ -224,6 +242,7 @@ function showLinkPopover(anchorBtn) {
|
|
|
224
242
|
};
|
|
225
243
|
|
|
226
244
|
const onRemove = () => {
|
|
245
|
+
if (!existingLink?.parentNode) return;
|
|
227
246
|
const frag = document.createDocumentFragment();
|
|
228
247
|
while (existingLink.firstChild) frag.appendChild(existingLink.firstChild);
|
|
229
248
|
existingLink.parentNode.replaceChild(frag, existingLink);
|
|
@@ -231,7 +250,7 @@ function showLinkPopover(anchorBtn) {
|
|
|
231
250
|
renderBlockActionBar();
|
|
232
251
|
};
|
|
233
252
|
|
|
234
|
-
const onKeydown = (/** @type {
|
|
253
|
+
const onKeydown = (/** @type {KeyboardEvent} */ e) => {
|
|
235
254
|
if (e.key === "Enter") onApply();
|
|
236
255
|
else if (e.key === "Escape") {
|
|
237
256
|
litRender(nothing, view.linkPopoverHost);
|
|
@@ -273,32 +292,33 @@ function showLinkPopover(anchorBtn) {
|
|
|
273
292
|
|
|
274
293
|
/** Move the selected node up (swap with previous sibling). */
|
|
275
294
|
function moveSelectionUp() {
|
|
276
|
-
const
|
|
277
|
-
if (!
|
|
278
|
-
const
|
|
295
|
+
const tab = activeTab.value;
|
|
296
|
+
if (!tab?.session.selection || tab.session.selection.length < 2) return;
|
|
297
|
+
const sel = tab.session.selection;
|
|
298
|
+
const idx = /** @type {number} */ (childIndex(sel));
|
|
279
299
|
if (idx <= 0) return;
|
|
280
|
-
const pPath = /** @type {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
renderOnly("overlays");
|
|
300
|
+
const pPath = /** @type {JxPath} */ (parentElementPath(sel));
|
|
301
|
+
transactDoc(tab, (t) => mutateMoveNode(t, sel, pPath, idx - 1));
|
|
302
|
+
tab.session.selection = [...pPath, "children", idx - 1];
|
|
284
303
|
}
|
|
285
304
|
|
|
286
305
|
/** Move the selected node down (swap with next sibling). */
|
|
287
306
|
function moveSelectionDown() {
|
|
288
|
-
const
|
|
289
|
-
if (!
|
|
290
|
-
const
|
|
291
|
-
const
|
|
292
|
-
const
|
|
307
|
+
const tab = activeTab.value;
|
|
308
|
+
if (!tab?.session.selection || tab.session.selection.length < 2) return;
|
|
309
|
+
const sel = tab.session.selection;
|
|
310
|
+
const idx = /** @type {number} */ (childIndex(sel));
|
|
311
|
+
const pPath = /** @type {JxPath} */ (parentElementPath(sel));
|
|
312
|
+
const parentNode = getNodeAtPath(tab.doc.document, pPath);
|
|
293
313
|
const siblings = parentNode?.children;
|
|
294
314
|
if (!siblings || idx >= siblings.length - 1) return;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
renderOnly("overlays");
|
|
315
|
+
transactDoc(tab, (t) => mutateMoveNode(t, sel, pPath, idx + 2));
|
|
316
|
+
tab.session.selection = [...pPath, "children", idx + 1];
|
|
298
317
|
}
|
|
299
318
|
|
|
300
319
|
/** Render the unified block action bar above the selected element. */
|
|
301
320
|
export function renderBlockActionBar() {
|
|
321
|
+
if (!_ctx) return;
|
|
302
322
|
if (!view.blockActionBarEl) {
|
|
303
323
|
view.blockActionBarEl = _ctx.createFloatingContainer();
|
|
304
324
|
}
|
|
@@ -308,21 +328,22 @@ export function renderBlockActionBar() {
|
|
|
308
328
|
view.selDragCleanup = null;
|
|
309
329
|
}
|
|
310
330
|
|
|
311
|
-
const
|
|
331
|
+
const tab = activeTab.value;
|
|
312
332
|
const canvasMode = _ctx.getCanvasMode();
|
|
313
333
|
|
|
314
|
-
if (!
|
|
334
|
+
if (!tab?.session.selection || (canvasMode !== "design" && canvasMode !== "edit")) {
|
|
315
335
|
litRender(nothing, view.blockActionBarEl);
|
|
316
336
|
return;
|
|
317
337
|
}
|
|
318
338
|
|
|
339
|
+
const selection = tab.session.selection;
|
|
319
340
|
const activePanel = getActivePanel();
|
|
320
341
|
if (!activePanel) {
|
|
321
342
|
litRender(nothing, view.blockActionBarEl);
|
|
322
343
|
return;
|
|
323
344
|
}
|
|
324
|
-
const el = findCanvasElement(
|
|
325
|
-
const node = el && getNodeAtPath(
|
|
345
|
+
const el = findCanvasElement(selection, activePanel.canvas);
|
|
346
|
+
const node = el && getNodeAtPath(tab.doc.document, selection);
|
|
326
347
|
if (!el || !node) {
|
|
327
348
|
litRender(nothing, view.blockActionBarEl);
|
|
328
349
|
return;
|
|
@@ -347,28 +368,31 @@ export function renderBlockActionBar() {
|
|
|
347
368
|
style="left:${elRect.left}px; top:${topPos}px"
|
|
348
369
|
@mousedown=${onBarMousedown}
|
|
349
370
|
>
|
|
350
|
-
${
|
|
371
|
+
${selection.length >= 2 ? renderParentSelector() : nothing}
|
|
351
372
|
|
|
352
373
|
<span class="bar-tag">${node.$id || (node.tagName ?? "div")}</span>
|
|
353
374
|
|
|
354
|
-
${
|
|
375
|
+
${selection.length >= 2
|
|
355
376
|
? html`<span class="bar-drag-handle" title="Drag to reorder">⡇</span>`
|
|
356
377
|
: nothing}
|
|
357
|
-
${
|
|
358
|
-
${
|
|
378
|
+
${selection.length >= 2 ? renderMoveArrows() : nothing}
|
|
379
|
+
${selection.length >= 2 && node.tagName
|
|
359
380
|
? (() => {
|
|
360
381
|
const isComp =
|
|
361
382
|
node.tagName.includes("-") &&
|
|
362
|
-
componentRegistry.some(
|
|
383
|
+
componentRegistry.some(
|
|
384
|
+
(/** @type {{ tagName: string }} */ c) => c.tagName === node.tagName,
|
|
385
|
+
);
|
|
363
386
|
if (isComp) {
|
|
364
387
|
const comp = componentRegistry.find(
|
|
365
|
-
(/** @type {
|
|
388
|
+
(/** @type {{ tagName: string; path: string }} */ c) =>
|
|
389
|
+
c.tagName === node.tagName,
|
|
366
390
|
);
|
|
367
391
|
return html`<sp-action-button
|
|
368
392
|
size="xs"
|
|
369
393
|
quiet
|
|
370
394
|
title="Edit Component"
|
|
371
|
-
@click=${() => _ctx
|
|
395
|
+
@click=${() => _ctx?.navigateToComponent(/** @type {string} */ (comp?.path))}
|
|
372
396
|
><sp-icon-edit slot="icon" size="xs"></sp-icon-edit
|
|
373
397
|
></sp-action-button>`;
|
|
374
398
|
}
|
|
@@ -376,7 +400,7 @@ export function renderBlockActionBar() {
|
|
|
376
400
|
size="xs"
|
|
377
401
|
quiet
|
|
378
402
|
title="Convert to Component"
|
|
379
|
-
@click=${() => convertToComponent(
|
|
403
|
+
@click=${() => convertToComponent()}
|
|
380
404
|
><sp-icon-box slot="icon" size="xs"></sp-icon-box
|
|
381
405
|
></sp-action-button>`;
|
|
382
406
|
})()
|
|
@@ -398,7 +422,7 @@ export function renderBlockActionBar() {
|
|
|
398
422
|
value=${action.tag}
|
|
399
423
|
title="${action.label}${action.shortcut ? ` (${action.shortcut})` : ""}"
|
|
400
424
|
@mousedown=${captureSelectionRange}
|
|
401
|
-
@click=${(/** @type {
|
|
425
|
+
@click=${(/** @type {MouseEvent} */ e) => onFormatClick(e, action)}
|
|
402
426
|
>
|
|
403
427
|
${formatIconMap[action.icon] ?? nothing}
|
|
404
428
|
</sp-action-button>
|
|
@@ -422,8 +446,8 @@ export function renderBlockActionBar() {
|
|
|
422
446
|
bar.style.left = `${Math.max(0, window.innerWidth - barRect.width)}px`;
|
|
423
447
|
}
|
|
424
448
|
// Attach drag handle
|
|
425
|
-
const
|
|
426
|
-
if (
|
|
449
|
+
const currentTab = activeTab.value;
|
|
450
|
+
if (currentTab?.session.selection && currentTab.session.selection.length >= 2) {
|
|
427
451
|
const handle = bar.querySelector(".bar-drag-handle");
|
|
428
452
|
if (handle) {
|
|
429
453
|
if (view.selDragCleanup) {
|
|
@@ -432,7 +456,7 @@ export function renderBlockActionBar() {
|
|
|
432
456
|
}
|
|
433
457
|
view.selDragCleanup = draggable({
|
|
434
458
|
element: handle,
|
|
435
|
-
getInitialData: () => ({ type: "tree-node", path:
|
|
459
|
+
getInitialData: () => ({ type: "tree-node", path: activeTab.value?.session.selection }),
|
|
436
460
|
});
|
|
437
461
|
}
|
|
438
462
|
}
|
package/src/panels/canvas-dnd.js
CHANGED
|
@@ -20,19 +20,39 @@ import { view } from "../view.js";
|
|
|
20
20
|
import { applyDropInstruction } from "../panels/dnd.js";
|
|
21
21
|
import { effectiveZoom } from "../canvas/canvas-helpers.js";
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {{
|
|
25
|
+
* canvas: HTMLElement;
|
|
26
|
+
* overlayClk: HTMLElement;
|
|
27
|
+
* overlay: HTMLElement;
|
|
28
|
+
* viewport: HTMLElement;
|
|
29
|
+
* dropLine: HTMLElement;
|
|
30
|
+
* }} CanvasPanel
|
|
31
|
+
*
|
|
32
|
+
* @typedef {(string | number)[]} JxPath
|
|
33
|
+
*
|
|
34
|
+
* @typedef {{ type: "reorder-above" | "reorder-below" | "make-child" }} DropInstruction
|
|
35
|
+
*
|
|
36
|
+
* @typedef {{ instruction: DropInstruction; referenceEl: HTMLElement; targetPath: JxPath }} DropResult
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/** @type {HTMLElement | null} */
|
|
40
|
+
let _activeDropEl = null;
|
|
41
|
+
|
|
23
42
|
/**
|
|
24
43
|
* Register all canvas elements in a panel as DnD drop targets.
|
|
25
44
|
*
|
|
26
|
-
* @param {
|
|
45
|
+
* @param {CanvasPanel} panel
|
|
27
46
|
*/
|
|
28
47
|
export function registerPanelDnD(panel) {
|
|
29
48
|
const { canvas, dropLine } = panel;
|
|
30
49
|
const allEls = canvas.querySelectorAll("*");
|
|
31
50
|
|
|
32
51
|
const monitorCleanup = monitorForElements({
|
|
33
|
-
onDragStart() {
|
|
52
|
+
onDragStart({ location }) {
|
|
53
|
+
view.lastDragInput = location.current.input;
|
|
34
54
|
for (const el of canvas.querySelectorAll("*")) {
|
|
35
|
-
/** @type {
|
|
55
|
+
/** @type {HTMLElement} */ (el).style.pointerEvents = "auto";
|
|
36
56
|
}
|
|
37
57
|
for (const p of canvasPanels) p.overlayClk.style.pointerEvents = "none";
|
|
38
58
|
},
|
|
@@ -40,10 +60,12 @@ export function registerPanelDnD(panel) {
|
|
|
40
60
|
view.lastDragInput = location.current.input;
|
|
41
61
|
},
|
|
42
62
|
onDrop() {
|
|
63
|
+
_activeDropEl?.classList.remove("canvas-drop-target");
|
|
64
|
+
_activeDropEl = null;
|
|
43
65
|
for (const p of canvasPanels) p.dropLine.style.display = "none";
|
|
44
66
|
view.lastDragInput = null;
|
|
45
67
|
for (const el of canvas.querySelectorAll("*")) {
|
|
46
|
-
/** @type {
|
|
68
|
+
/** @type {HTMLElement} */ (el).style.pointerEvents = "none";
|
|
47
69
|
}
|
|
48
70
|
for (const p of canvasPanels) p.overlayClk.style.pointerEvents = "";
|
|
49
71
|
},
|
|
@@ -56,34 +78,45 @@ export function registerPanelDnD(panel) {
|
|
|
56
78
|
if (!elPath) continue;
|
|
57
79
|
|
|
58
80
|
const node = getNodeAtPath(S.document, elPath);
|
|
59
|
-
const
|
|
81
|
+
const tag = (node?.tagName || "div").toLowerCase();
|
|
82
|
+
const hasElementChildren = node?.children?.some(
|
|
83
|
+
(/** @type {unknown} */ c) => c != null && typeof c === "object",
|
|
84
|
+
);
|
|
85
|
+
const isLeaf = VOID_ELEMENTS.has(tag) || !hasElementChildren;
|
|
60
86
|
|
|
61
87
|
const cleanup = dropTargetForElements({
|
|
62
|
-
element: el,
|
|
88
|
+
element: /** @type {HTMLElement} */ (el),
|
|
63
89
|
canDrop({ source }) {
|
|
64
|
-
const srcPath = source.data.path;
|
|
65
|
-
if (srcPath && isAncestor(
|
|
90
|
+
const srcPath = /** @type {JxPath | undefined} */ (source.data.path);
|
|
91
|
+
if (srcPath && isAncestor(srcPath, elPath)) return false;
|
|
66
92
|
return true;
|
|
67
93
|
},
|
|
68
94
|
getData() {
|
|
69
|
-
return { path: elPath, _isVoid:
|
|
70
|
-
},
|
|
71
|
-
onDragEnter() {
|
|
72
|
-
showCanvasDropIndicator(el, elPath, isVoid, panel);
|
|
95
|
+
return { path: elPath, _isVoid: isLeaf };
|
|
73
96
|
},
|
|
74
|
-
|
|
75
|
-
|
|
97
|
+
onDragEnter({ location }) {
|
|
98
|
+
view.lastDragInput = location.current.input;
|
|
99
|
+
if (_activeDropEl && _activeDropEl !== el) {
|
|
100
|
+
_activeDropEl.classList.remove("canvas-drop-target");
|
|
101
|
+
}
|
|
102
|
+
_activeDropEl = /** @type {HTMLElement} */ (el);
|
|
103
|
+
showCanvasDropIndicator(/** @type {HTMLElement} */ (el), elPath, isLeaf, panel);
|
|
76
104
|
},
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
el
|
|
105
|
+
onDrag({ location }) {
|
|
106
|
+
view.lastDragInput = location.current.input;
|
|
107
|
+
showCanvasDropIndicator(/** @type {HTMLElement} */ (el), elPath, isLeaf, panel);
|
|
80
108
|
},
|
|
109
|
+
onDragLeave() {},
|
|
81
110
|
onDrop({ source }) {
|
|
82
111
|
dropLine.style.display = "none";
|
|
83
|
-
el.classList.remove("canvas-drop-target");
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
112
|
+
/** @type {HTMLElement} */ (el).classList.remove("canvas-drop-target");
|
|
113
|
+
_activeDropEl = null;
|
|
114
|
+
const { instruction, targetPath } = getCanvasDropResult(
|
|
115
|
+
/** @type {HTMLElement} */ (el),
|
|
116
|
+
elPath,
|
|
117
|
+
isLeaf,
|
|
118
|
+
);
|
|
119
|
+
applyDropInstruction(instruction, source.data, targetPath);
|
|
87
120
|
},
|
|
88
121
|
});
|
|
89
122
|
view.canvasDndCleanups.push(cleanup);
|
|
@@ -91,49 +124,98 @@ export function registerPanelDnD(panel) {
|
|
|
91
124
|
}
|
|
92
125
|
|
|
93
126
|
/**
|
|
94
|
-
* @param {
|
|
95
|
-
* @param {
|
|
96
|
-
* @param {
|
|
127
|
+
* @param {HTMLElement} el
|
|
128
|
+
* @param {JxPath} elPath
|
|
129
|
+
* @param {boolean} isLeaf
|
|
130
|
+
* @returns {DropResult}
|
|
97
131
|
*/
|
|
98
|
-
function
|
|
99
|
-
|
|
100
|
-
|
|
132
|
+
function getCanvasDropResult(el, elPath, isLeaf) {
|
|
133
|
+
if (!view.lastDragInput)
|
|
134
|
+
return { instruction: { type: "make-child" }, referenceEl: el, targetPath: elPath };
|
|
101
135
|
const y = view.lastDragInput.clientY;
|
|
136
|
+
|
|
137
|
+
if (elPath.length === 0) {
|
|
138
|
+
const children = /** @type {HTMLElement[]} */ (Array.from(el.children));
|
|
139
|
+
if (children.length === 0)
|
|
140
|
+
return { instruction: { type: "make-child" }, referenceEl: el, targetPath: elPath };
|
|
141
|
+
return nearestChildEdge(children, y, elPath);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const rect = el.getBoundingClientRect();
|
|
102
145
|
const relY = (y - rect.top) / rect.height;
|
|
103
146
|
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
147
|
+
if (isLeaf) {
|
|
148
|
+
const instruction =
|
|
149
|
+
relY < 0.5
|
|
150
|
+
? { type: /** @type {const} */ ("reorder-above") }
|
|
151
|
+
: { type: /** @type {const} */ ("reorder-below") };
|
|
152
|
+
return { instruction, referenceEl: el, targetPath: elPath };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (relY < 0.25)
|
|
156
|
+
return { instruction: { type: "reorder-above" }, referenceEl: el, targetPath: elPath };
|
|
157
|
+
if (relY > 0.75)
|
|
158
|
+
return { instruction: { type: "reorder-below" }, referenceEl: el, targetPath: elPath };
|
|
159
|
+
return { instruction: { type: "make-child" }, referenceEl: el, targetPath: elPath };
|
|
109
160
|
}
|
|
110
161
|
|
|
111
162
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* @param {
|
|
163
|
+
* Find the nearest child edge to the cursor and return the appropriate instruction along with the
|
|
164
|
+
* reference child element and its path.
|
|
165
|
+
*
|
|
166
|
+
* @param {HTMLElement[]} children
|
|
167
|
+
* @param {number} cursorY
|
|
168
|
+
* @param {JxPath} parentPath
|
|
169
|
+
* @returns {DropResult}
|
|
116
170
|
*/
|
|
117
|
-
function
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
171
|
+
function nearestChildEdge(children, cursorY, parentPath) {
|
|
172
|
+
let closestDist = Infinity;
|
|
173
|
+
let instruction = /** @type {DropInstruction} */ ({ type: "reorder-below" });
|
|
174
|
+
let closestIdx = children.length - 1;
|
|
175
|
+
|
|
176
|
+
for (let i = 0; i < children.length; i++) {
|
|
177
|
+
const rect = children[i].getBoundingClientRect();
|
|
178
|
+
const topDist = Math.abs(cursorY - rect.top);
|
|
179
|
+
const bottomDist = Math.abs(cursorY - rect.bottom);
|
|
180
|
+
|
|
181
|
+
if (topDist < closestDist) {
|
|
182
|
+
closestDist = topDist;
|
|
183
|
+
instruction = { type: "reorder-above" };
|
|
184
|
+
closestIdx = i;
|
|
185
|
+
}
|
|
186
|
+
if (bottomDist < closestDist) {
|
|
187
|
+
closestDist = bottomDist;
|
|
188
|
+
instruction = { type: "reorder-below" };
|
|
189
|
+
closestIdx = i;
|
|
190
|
+
}
|
|
123
191
|
}
|
|
124
192
|
|
|
193
|
+
const childPath = [...parentPath, "children", closestIdx];
|
|
194
|
+
return { instruction, referenceEl: children[closestIdx], targetPath: childPath };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* @param {HTMLElement} el
|
|
199
|
+
* @param {JxPath} elPath
|
|
200
|
+
* @param {boolean} isLeaf
|
|
201
|
+
* @param {CanvasPanel} panel
|
|
202
|
+
*/
|
|
203
|
+
function showCanvasDropIndicator(el, elPath, isLeaf, panel) {
|
|
204
|
+
const { instruction, referenceEl } = getCanvasDropResult(el, elPath, isLeaf);
|
|
205
|
+
const { dropLine, viewport } = panel;
|
|
206
|
+
|
|
125
207
|
const scale = effectiveZoom();
|
|
126
208
|
const wrapRect = viewport.getBoundingClientRect();
|
|
127
|
-
const
|
|
128
|
-
const left = (
|
|
129
|
-
const width =
|
|
209
|
+
const refRect = referenceEl.getBoundingClientRect();
|
|
210
|
+
const left = (refRect.left - wrapRect.left + viewport.scrollLeft) / scale;
|
|
211
|
+
const width = refRect.width / scale;
|
|
130
212
|
|
|
131
213
|
if (instruction.type === "make-child") {
|
|
132
214
|
dropLine.style.display = "block";
|
|
133
|
-
dropLine.style.top = `${(
|
|
215
|
+
dropLine.style.top = `${(refRect.top - wrapRect.top + viewport.scrollTop) / scale}px`;
|
|
134
216
|
dropLine.style.left = `${left}px`;
|
|
135
217
|
dropLine.style.width = `${width}px`;
|
|
136
|
-
dropLine.style.height = `${
|
|
218
|
+
dropLine.style.height = `${refRect.height / scale}px`;
|
|
137
219
|
dropLine.className = "canvas-drop-indicator inside";
|
|
138
220
|
el.classList.add("canvas-drop-target");
|
|
139
221
|
return;
|
|
@@ -142,13 +224,13 @@ function showCanvasDropIndicator(el, elPath, isVoid, panel) {
|
|
|
142
224
|
el.classList.remove("canvas-drop-target");
|
|
143
225
|
const top =
|
|
144
226
|
instruction.type === "reorder-above"
|
|
145
|
-
? (
|
|
146
|
-
: (
|
|
227
|
+
? (refRect.top - wrapRect.top + viewport.scrollTop) / scale
|
|
228
|
+
: (refRect.bottom - wrapRect.top + viewport.scrollTop) / scale;
|
|
147
229
|
|
|
148
230
|
dropLine.style.display = "block";
|
|
149
231
|
dropLine.style.top = `${top}px`;
|
|
150
232
|
dropLine.style.left = `${left}px`;
|
|
151
233
|
dropLine.style.width = `${width}px`;
|
|
152
|
-
dropLine.style.height = "
|
|
234
|
+
dropLine.style.height = "";
|
|
153
235
|
dropLine.className = "canvas-drop-indicator line";
|
|
154
236
|
}
|