@jxsuite/studio 0.28.2 → 0.28.4
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/studio.js +86144 -83971
- package/dist/studio.js.map +229 -219
- package/package.json +5 -5
- package/src/browse/browse-modal.ts +9 -3
- package/src/browse/browse.ts +139 -82
- package/src/canvas/canvas-diff.ts +35 -18
- package/src/canvas/canvas-helpers.ts +55 -21
- package/src/canvas/canvas-live-render.ts +140 -89
- package/src/canvas/canvas-render.ts +127 -82
- package/src/canvas/canvas-utils.ts +68 -42
- package/src/canvas/nested-site-style.ts +15 -7
- package/src/editor/component-inline-edit.ts +90 -51
- package/src/editor/content-inline-edit.ts +75 -72
- package/src/editor/context-menu.ts +125 -71
- package/src/editor/convert-targets.ts +17 -13
- package/src/editor/convert-to-component.ts +51 -28
- package/src/editor/convert-to-repeater.ts +44 -19
- package/src/editor/inline-edit.ts +107 -52
- package/src/editor/inline-format.ts +128 -60
- package/src/editor/insertion-helper.ts +26 -14
- package/src/editor/shortcuts.ts +89 -43
- package/src/editor/slash-menu.ts +41 -26
- package/src/files/components.ts +9 -7
- package/src/files/file-ops.ts +53 -33
- package/src/files/files.ts +246 -143
- package/src/format/constraints.ts +25 -15
- package/src/format/format-host.ts +33 -10
- package/src/github/github-auth.ts +24 -14
- package/src/github/github-publish.ts +20 -14
- package/src/new-project/new-project-modal.ts +27 -18
- package/src/panels/activity-bar.ts +27 -26
- package/src/panels/ai-panel.ts +100 -44
- package/src/panels/block-action-bar.ts +84 -52
- package/src/panels/canvas-dnd.ts +47 -28
- package/src/panels/data-explorer.ts +24 -11
- package/src/panels/dnd.ts +148 -100
- package/src/panels/editors.ts +54 -37
- package/src/panels/elements-panel.ts +23 -13
- package/src/panels/events-panel.ts +48 -38
- package/src/panels/git-panel.ts +95 -60
- package/src/panels/head-panel.ts +138 -84
- package/src/panels/imports-panel.ts +66 -29
- package/src/panels/layers-panel.ts +80 -36
- package/src/panels/left-panel.ts +86 -53
- package/src/panels/overlays.ts +36 -19
- package/src/panels/panel-events.ts +48 -27
- package/src/panels/panel-scheduler.ts +41 -23
- package/src/panels/preview-render.ts +26 -26
- package/src/panels/properties-panel.ts +155 -118
- package/src/panels/pseudo-preview.ts +31 -15
- package/src/panels/quick-search.ts +28 -13
- package/src/panels/right-panel.ts +35 -24
- package/src/panels/shared.ts +46 -29
- package/src/panels/signals-panel.ts +303 -186
- package/src/panels/statusbar.ts +27 -12
- package/src/panels/style-inputs.ts +30 -24
- package/src/panels/style-panel.ts +130 -90
- package/src/panels/style-utils.ts +63 -23
- package/src/panels/stylebook-layers-panel.ts +27 -30
- package/src/panels/stylebook-panel.ts +140 -72
- package/src/panels/tab-strip.ts +21 -10
- package/src/panels/toolbar.ts +52 -40
- package/src/panels/welcome-screen.ts +1 -1
- package/src/platform.ts +2 -1
- package/src/platforms/devserver.ts +221 -105
- package/src/recent-projects.ts +15 -7
- package/src/resize-edges.ts +7 -5
- package/src/services/cem-export.ts +73 -57
- package/src/services/code-services.ts +52 -25
- package/src/services/monaco-setup.ts +5 -5
- package/src/settings/content-types-editor.ts +133 -62
- package/src/settings/css-vars-editor.ts +31 -14
- package/src/settings/defs-editor.ts +125 -63
- package/src/settings/general-settings.ts +11 -6
- package/src/settings/head-editor.ts +30 -15
- package/src/settings/schema-field-ui.ts +75 -35
- package/src/settings/settings-modal.ts +35 -17
- package/src/site-context.ts +80 -47
- package/src/state.ts +98 -65
- package/src/store.ts +42 -23
- package/src/studio.ts +165 -144
- package/src/tabs/tab.ts +51 -45
- package/src/tabs/transact.ts +230 -109
- package/src/types.ts +60 -51
- package/src/ui/button-group.ts +3 -2
- package/src/ui/color-selector.ts +33 -15
- package/src/ui/expression-editor.ts +139 -86
- package/src/ui/field-input.ts +35 -18
- package/src/ui/field-row.ts +1 -1
- package/src/ui/icons.ts +5 -6
- package/src/ui/layers.ts +33 -26
- package/src/ui/media-picker.ts +13 -7
- package/src/ui/panel-resize.ts +39 -20
- package/src/ui/spectrum.ts +4 -2
- package/src/ui/unit-selector.ts +20 -11
- package/src/ui/value-selector.ts +13 -7
- package/src/ui/widgets.ts +34 -18
- package/src/utils/canvas-media.ts +66 -33
- package/src/utils/edit-display.ts +43 -44
- package/src/utils/google-fonts.ts +10 -6
- package/src/utils/inherited-style.ts +22 -8
- package/src/utils/studio-utils.ts +88 -45
- package/src/view.ts +9 -5
- package/src/workspace/workspace.ts +49 -29
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
|
-
import {
|
|
2
|
+
import { canvasPanels, childIndex, getNodeAtPath, parentElementPath, renderOnly } from "../store";
|
|
3
3
|
import { activeTab } from "../workspace/workspace";
|
|
4
|
-
import {
|
|
4
|
+
import { mutateInsertNode, mutateUpdateProperty, transactDoc } from "../tabs/transact";
|
|
5
5
|
import { view } from "../view";
|
|
6
|
-
import {
|
|
6
|
+
import { isEditableBlock, startEditing } from "./inline-edit";
|
|
7
7
|
import { restoreTemplateExpressions } from "../utils/edit-display";
|
|
8
8
|
import { renderBlockActionBar } from "../panels/block-action-bar";
|
|
9
9
|
import { defaultDef } from "../panels/shared";
|
|
10
10
|
import { findCanvasElement, getActivePanel } from "../canvas/canvas-helpers";
|
|
11
11
|
|
|
12
|
-
import type { JxContentResult } from "./inline-edit";
|
|
13
|
-
import type { SlashCommand } from "./inline-edit";
|
|
12
|
+
import type { JxContentResult, SlashCommand } from "./inline-edit";
|
|
14
13
|
import type { JxPath } from "../state";
|
|
15
14
|
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
16
15
|
|
|
@@ -22,8 +21,8 @@ import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
|
22
21
|
*/
|
|
23
22
|
export function enterInlineEdit(el: HTMLElement, path: JxPath) {
|
|
24
23
|
// Restore raw template expressions before editing.
|
|
25
|
-
//
|
|
26
|
-
//
|
|
24
|
+
// PrepareForEditMode renders ${expr} as ❪ expr ❫ for display;
|
|
25
|
+
// Revert so the user edits the real syntax and commits it back intact.
|
|
27
26
|
restoreTemplateExpressions(el);
|
|
28
27
|
|
|
29
28
|
// Hide overlays while editing
|
|
@@ -40,66 +39,34 @@ export function enterInlineEdit(el: HTMLElement, path: JxPath) {
|
|
|
40
39
|
) {
|
|
41
40
|
const node = getNodeAtPath(activeTab.value!.doc.document, commitPath);
|
|
42
41
|
if (children) {
|
|
43
|
-
if (node && JSON.stringify(node.children) === JSON.stringify(children))
|
|
42
|
+
if (node && JSON.stringify(node.children) === JSON.stringify(children)) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
44
45
|
transactDoc(activeTab.value, (t) => {
|
|
45
|
-
mutateUpdateProperty(t, commitPath, "textContent"
|
|
46
|
+
mutateUpdateProperty(t, commitPath, "textContent");
|
|
46
47
|
mutateUpdateProperty(t, commitPath, "children", children);
|
|
47
48
|
});
|
|
48
49
|
} else if (textContent != null) {
|
|
49
|
-
if (node && node.textContent === textContent && !node.children)
|
|
50
|
+
if (node && node.textContent === textContent && !node.children) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
50
53
|
transactDoc(activeTab.value, (t) => {
|
|
51
|
-
mutateUpdateProperty(t, commitPath, "children"
|
|
54
|
+
mutateUpdateProperty(t, commitPath, "children");
|
|
52
55
|
mutateUpdateProperty(t, commitPath, "textContent", textContent);
|
|
53
56
|
});
|
|
54
57
|
}
|
|
55
58
|
},
|
|
56
59
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const parentPath = parentElementPath(splitPath) as JxPath;
|
|
62
|
-
const idx = childIndex(splitPath) as number;
|
|
63
|
-
const newNode: JxMutableNode = { tagName: tag };
|
|
64
|
-
if (after.textContent != null) {
|
|
65
|
-
newNode.textContent = after.textContent;
|
|
66
|
-
} else if (after.children) {
|
|
67
|
-
newNode.children = after.children;
|
|
68
|
-
} else {
|
|
69
|
-
newNode.textContent = "";
|
|
60
|
+
onEnd() {
|
|
61
|
+
if (view.inlineEditCleanup) {
|
|
62
|
+
view.inlineEditCleanup();
|
|
63
|
+
view.inlineEditCleanup = null;
|
|
70
64
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
mutateUpdateProperty(t, splitPath, "children", undefined);
|
|
77
|
-
mutateUpdateProperty(t, splitPath, "textContent", before.textContent);
|
|
78
|
-
} else if (before.children) {
|
|
79
|
-
mutateUpdateProperty(t, splitPath, "textContent", undefined);
|
|
80
|
-
mutateUpdateProperty(t, splitPath, "children", before.children);
|
|
81
|
-
}
|
|
82
|
-
mutateInsertNode(t, parentPath, idx + 1, newNode);
|
|
83
|
-
t.session.selection = newPath;
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// Re-enter editing on the new element after render
|
|
87
|
-
requestAnimationFrame(() => {
|
|
88
|
-
const activePanel = getActivePanel();
|
|
89
|
-
if (activePanel) {
|
|
90
|
-
const newEl = findCanvasElement(newPath, activePanel.canvas);
|
|
91
|
-
if (newEl && isEditableBlock(newEl)) {
|
|
92
|
-
enterInlineEdit(newEl, newPath);
|
|
93
|
-
// Place cursor at start of new element
|
|
94
|
-
const sel = window.getSelection();
|
|
95
|
-
const range = document.createRange();
|
|
96
|
-
range.selectNodeContents(newEl);
|
|
97
|
-
range.collapse(true);
|
|
98
|
-
sel?.removeAllRanges();
|
|
99
|
-
sel?.addRange(range);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
});
|
|
65
|
+
for (const p of canvasPanels) {
|
|
66
|
+
p.overlay.style.display = "";
|
|
67
|
+
p.overlayClk.style.pointerEvents = "";
|
|
68
|
+
}
|
|
69
|
+
renderOnly("overlays");
|
|
103
70
|
},
|
|
104
71
|
|
|
105
72
|
onInsert(afterPath: JxPath, cmd: SlashCommand, commitData: JxContentResult | undefined) {
|
|
@@ -119,12 +86,12 @@ export function enterInlineEdit(el: HTMLElement, path: JxPath) {
|
|
|
119
86
|
if (isEmpty) {
|
|
120
87
|
transactDoc(activeTab.value, (t) => {
|
|
121
88
|
mutateUpdateProperty(t, afterPath, "tagName", cmd.tag);
|
|
122
|
-
mutateUpdateProperty(t, afterPath, "children"
|
|
89
|
+
mutateUpdateProperty(t, afterPath, "children");
|
|
123
90
|
const def = defaultDef(cmd.tag);
|
|
124
91
|
if (def.textContent && def.textContent !== "Paragraph text") {
|
|
125
92
|
mutateUpdateProperty(t, afterPath, "textContent", def.textContent);
|
|
126
93
|
} else {
|
|
127
|
-
mutateUpdateProperty(t, afterPath, "textContent"
|
|
94
|
+
mutateUpdateProperty(t, afterPath, "textContent");
|
|
128
95
|
}
|
|
129
96
|
t.session.selection = afterPath;
|
|
130
97
|
});
|
|
@@ -132,9 +99,9 @@ export function enterInlineEdit(el: HTMLElement, path: JxPath) {
|
|
|
132
99
|
requestAnimationFrame(() => {
|
|
133
100
|
const activePanel = getActivePanel();
|
|
134
101
|
if (activePanel) {
|
|
135
|
-
const
|
|
136
|
-
if (
|
|
137
|
-
enterInlineEdit(
|
|
102
|
+
const nextEl = findCanvasElement(afterPath, activePanel.canvas);
|
|
103
|
+
if (nextEl && isEditableBlock(nextEl)) {
|
|
104
|
+
enterInlineEdit(nextEl, afterPath);
|
|
138
105
|
}
|
|
139
106
|
}
|
|
140
107
|
});
|
|
@@ -150,10 +117,10 @@ export function enterInlineEdit(el: HTMLElement, path: JxPath) {
|
|
|
150
117
|
transactDoc(activeTab.value, (t) => {
|
|
151
118
|
if (commitData) {
|
|
152
119
|
if (commitData.children) {
|
|
153
|
-
mutateUpdateProperty(t, afterPath, "textContent"
|
|
120
|
+
mutateUpdateProperty(t, afterPath, "textContent");
|
|
154
121
|
mutateUpdateProperty(t, afterPath, "children", commitData.children);
|
|
155
122
|
} else if (commitData.textContent != null) {
|
|
156
|
-
mutateUpdateProperty(t, afterPath, "children"
|
|
123
|
+
mutateUpdateProperty(t, afterPath, "children");
|
|
157
124
|
mutateUpdateProperty(t, afterPath, "textContent", commitData.textContent);
|
|
158
125
|
}
|
|
159
126
|
}
|
|
@@ -173,16 +140,52 @@ export function enterInlineEdit(el: HTMLElement, path: JxPath) {
|
|
|
173
140
|
});
|
|
174
141
|
},
|
|
175
142
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
143
|
+
onSplit(splitPath: JxPath, before: JxContentResult, after: JxContentResult) {
|
|
144
|
+
const tag = "p";
|
|
145
|
+
|
|
146
|
+
// Insert new element after with "after" content
|
|
147
|
+
const parentPath = parentElementPath(splitPath) as JxPath;
|
|
148
|
+
const idx = childIndex(splitPath) as number;
|
|
149
|
+
const newNode: JxMutableNode = { tagName: tag };
|
|
150
|
+
if (after.textContent != null) {
|
|
151
|
+
newNode.textContent = after.textContent;
|
|
152
|
+
} else if (after.children) {
|
|
153
|
+
newNode.children = after.children;
|
|
154
|
+
} else {
|
|
155
|
+
newNode.textContent = "";
|
|
184
156
|
}
|
|
185
|
-
|
|
157
|
+
|
|
158
|
+
const newPath = [...parentPath, "children", idx + 1];
|
|
159
|
+
|
|
160
|
+
transactDoc(activeTab.value, (t) => {
|
|
161
|
+
if (before.textContent != null) {
|
|
162
|
+
mutateUpdateProperty(t, splitPath, "children");
|
|
163
|
+
mutateUpdateProperty(t, splitPath, "textContent", before.textContent);
|
|
164
|
+
} else if (before.children) {
|
|
165
|
+
mutateUpdateProperty(t, splitPath, "textContent");
|
|
166
|
+
mutateUpdateProperty(t, splitPath, "children", before.children);
|
|
167
|
+
}
|
|
168
|
+
mutateInsertNode(t, parentPath, idx + 1, newNode);
|
|
169
|
+
t.session.selection = newPath;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Re-enter editing on the new element after render
|
|
173
|
+
requestAnimationFrame(() => {
|
|
174
|
+
const activePanel = getActivePanel();
|
|
175
|
+
if (activePanel) {
|
|
176
|
+
const newEl = findCanvasElement(newPath, activePanel.canvas);
|
|
177
|
+
if (newEl && isEditableBlock(newEl)) {
|
|
178
|
+
enterInlineEdit(newEl, newPath);
|
|
179
|
+
// Place cursor at start of new element
|
|
180
|
+
const sel = window.getSelection();
|
|
181
|
+
const range = document.createRange();
|
|
182
|
+
range.selectNodeContents(newEl);
|
|
183
|
+
range.collapse(true);
|
|
184
|
+
sel?.removeAllRanges();
|
|
185
|
+
sel?.addRange(range);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
});
|
|
186
189
|
},
|
|
187
190
|
});
|
|
188
191
|
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
// ─── Clipboard & Context Menu ─────────────────────────────────────────────────
|
|
3
3
|
import { html } from "lit-html";
|
|
4
|
+
import { jsonClone } from "../utils/studio-utils";
|
|
4
5
|
import { ref } from "lit-html/directives/ref.js";
|
|
5
6
|
import { htmlToJx } from "@jxsuite/parser/html-to-jx";
|
|
6
|
-
import { getNodeAtPath, parentElementPath
|
|
7
|
+
import { childIndex, getNodeAtPath, parentElementPath } from "../store";
|
|
7
8
|
import { activeTab, workspace } from "../workspace/workspace";
|
|
8
9
|
import {
|
|
9
|
-
|
|
10
|
+
mutateDuplicateNode,
|
|
10
11
|
mutateInsertNode,
|
|
11
12
|
mutateRemoveNode,
|
|
12
|
-
mutateDuplicateNode,
|
|
13
|
-
mutateWrapNode,
|
|
14
13
|
mutateReplaceStyle,
|
|
14
|
+
mutateWrapNode,
|
|
15
|
+
transactDoc,
|
|
15
16
|
} from "../tabs/transact";
|
|
16
17
|
import { statusMessage } from "../panels/statusbar";
|
|
17
18
|
import { convertToComponent } from "./convert-to-component";
|
|
18
19
|
import { convertToRepeater } from "./convert-to-repeater";
|
|
19
20
|
import { componentRegistry } from "../files/components";
|
|
20
21
|
import { renderPopover } from "../ui/layers";
|
|
21
|
-
import { startLayerTitleEdit } from "../panels/layers-panel";
|
|
22
22
|
|
|
23
23
|
import type { JxPath } from "../state";
|
|
24
24
|
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
@@ -30,25 +30,33 @@ type JxNode = JxMutableNode;
|
|
|
30
30
|
const JX_MIME = "web application/jx+json";
|
|
31
31
|
|
|
32
32
|
/** @param {JxNode | string} node */
|
|
33
|
-
function nodeToHtml(node: JxNode | string) {
|
|
34
|
-
if (typeof node === "string")
|
|
33
|
+
function nodeToHtml(node: JxNode | string): string {
|
|
34
|
+
if (typeof node === "string") {
|
|
35
|
+
return node;
|
|
36
|
+
}
|
|
35
37
|
const tag = node.tagName || "div";
|
|
36
38
|
let attrs = "";
|
|
37
39
|
if (node.attributes) {
|
|
38
40
|
for (const [k, v] of Object.entries(node.attributes)) {
|
|
39
|
-
|
|
41
|
+
// Bound ($ref) attribute values have no static HTML representation — skip.
|
|
42
|
+
if (typeof v === "object") {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
attrs += v === "" ? ` ${k}` : ` ${k}="${String(v).replaceAll('"', """)}"`;
|
|
40
46
|
}
|
|
41
47
|
}
|
|
42
48
|
if (node.style) {
|
|
43
49
|
const css = Object.entries(node.style)
|
|
44
50
|
.map(([k, v]) => `${k}:${v}`)
|
|
45
51
|
.join(";");
|
|
46
|
-
if (css)
|
|
52
|
+
if (css) {
|
|
53
|
+
attrs += ` style="${css.replaceAll('"', """)}"`;
|
|
54
|
+
}
|
|
47
55
|
}
|
|
48
56
|
let inner = "";
|
|
49
|
-
if (node.textContent) {
|
|
57
|
+
if (typeof node.textContent === "string") {
|
|
50
58
|
inner = node.textContent;
|
|
51
|
-
} else if (node.children) {
|
|
59
|
+
} else if (Array.isArray(node.children)) {
|
|
52
60
|
inner = node.children.map((c) => nodeToHtml(c)).join("");
|
|
53
61
|
}
|
|
54
62
|
return `<${tag}${attrs}>${inner}</${tag}>`;
|
|
@@ -73,7 +81,7 @@ async function writeToClipboard(json: Record<string, unknown>) {
|
|
|
73
81
|
try {
|
|
74
82
|
await navigator.clipboard.writeText(JSON.stringify(json));
|
|
75
83
|
} catch {
|
|
76
|
-
//
|
|
84
|
+
// Clipboard API unavailable — workspace.clipboard is the fallback
|
|
77
85
|
}
|
|
78
86
|
}
|
|
79
87
|
}
|
|
@@ -99,7 +107,9 @@ async function readFromClipboard() {
|
|
|
99
107
|
const jxNodes = nodes.map((n) =>
|
|
100
108
|
typeof n === "string" ? { tagName: "p", textContent: n } : n,
|
|
101
109
|
) as JxNode[];
|
|
102
|
-
if (jxNodes.length > 0)
|
|
110
|
+
if (jxNodes.length > 0) {
|
|
111
|
+
return jxNodes;
|
|
112
|
+
}
|
|
103
113
|
}
|
|
104
114
|
if (item.types.includes("text/plain")) {
|
|
105
115
|
const blob = await item.getType("text/plain");
|
|
@@ -107,17 +117,21 @@ async function readFromClipboard() {
|
|
|
107
117
|
// Try parsing as Jx JSON
|
|
108
118
|
try {
|
|
109
119
|
const parsed = JSON.parse(text);
|
|
110
|
-
if (parsed && parsed.tagName)
|
|
120
|
+
if (parsed && parsed.tagName) {
|
|
121
|
+
return [parsed];
|
|
122
|
+
}
|
|
111
123
|
} catch {
|
|
112
|
-
//
|
|
124
|
+
// Plain text → paragraph node
|
|
125
|
+
}
|
|
126
|
+
if (text.trim()) {
|
|
127
|
+
return [{ tagName: "p", textContent: text.trim() }];
|
|
113
128
|
}
|
|
114
|
-
if (text.trim()) return [{ tagName: "p", textContent: text.trim() }];
|
|
115
129
|
}
|
|
116
130
|
}
|
|
117
131
|
} catch {
|
|
118
|
-
//
|
|
132
|
+
// Clipboard API unavailable — use workspace fallback
|
|
119
133
|
if (workspace.clipboard) {
|
|
120
|
-
return [
|
|
134
|
+
return [jsonClone(workspace.clipboard)];
|
|
121
135
|
}
|
|
122
136
|
}
|
|
123
137
|
return null;
|
|
@@ -127,21 +141,29 @@ async function readFromClipboard() {
|
|
|
127
141
|
|
|
128
142
|
export async function copyNode() {
|
|
129
143
|
const tab = activeTab.value;
|
|
130
|
-
if (!tab?.session.selection)
|
|
144
|
+
if (!tab?.session.selection) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
131
147
|
const node = getNodeAtPath(tab.doc.document, tab.session.selection);
|
|
132
|
-
if (!node)
|
|
133
|
-
|
|
148
|
+
if (!node) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const json = jsonClone(node);
|
|
134
152
|
await writeToClipboard(json);
|
|
135
153
|
statusMessage("Copied");
|
|
136
154
|
}
|
|
137
155
|
|
|
138
156
|
export async function cutNode() {
|
|
139
157
|
const tab = activeTab.value;
|
|
140
|
-
if (!tab?.session.selection || tab.session.selection.length < 2)
|
|
158
|
+
if (!tab?.session.selection || tab.session.selection.length < 2) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
141
161
|
const sel = tab.session.selection;
|
|
142
162
|
const node = getNodeAtPath(tab.doc.document, sel);
|
|
143
|
-
if (!node)
|
|
144
|
-
|
|
163
|
+
if (!node) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const json = jsonClone(node);
|
|
145
167
|
await writeToClipboard(json);
|
|
146
168
|
transactDoc(tab, (t) => mutateRemoveNode(t, sel));
|
|
147
169
|
statusMessage("Cut");
|
|
@@ -149,14 +171,20 @@ export async function cutNode() {
|
|
|
149
171
|
|
|
150
172
|
export async function pasteNode() {
|
|
151
173
|
const tab = activeTab.value;
|
|
152
|
-
if (!tab)
|
|
174
|
+
if (!tab) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
153
177
|
|
|
154
178
|
const nodes = await readFromClipboard();
|
|
155
|
-
if (!nodes || nodes.length === 0)
|
|
179
|
+
if (!nodes || nodes.length === 0) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
156
182
|
|
|
157
183
|
const pPath = tab.session.selection || [];
|
|
158
184
|
const parent = getNodeAtPath(tab.doc.document, pPath);
|
|
159
|
-
if (!parent)
|
|
185
|
+
if (!parent) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
160
188
|
|
|
161
189
|
if (tab.session.selection && tab.session.selection.length >= 2) {
|
|
162
190
|
const pp = parentElementPath(tab.session.selection) as JxPath;
|
|
@@ -167,7 +195,7 @@ export async function pasteNode() {
|
|
|
167
195
|
}
|
|
168
196
|
});
|
|
169
197
|
} else {
|
|
170
|
-
const idx = parent.children ? parent.children.length : 0;
|
|
198
|
+
const idx = Array.isArray(parent.children) ? parent.children.length : 0;
|
|
171
199
|
transactDoc(tab, (t) => {
|
|
172
200
|
for (let i = 0; i < nodes.length; i++) {
|
|
173
201
|
mutateInsertNode(t, pPath, idx + i, nodes[i]);
|
|
@@ -179,18 +207,26 @@ export async function pasteNode() {
|
|
|
179
207
|
|
|
180
208
|
export function copyStyles() {
|
|
181
209
|
const tab = activeTab.value;
|
|
182
|
-
if (!tab?.session.selection)
|
|
210
|
+
if (!tab?.session.selection) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
183
213
|
const node = getNodeAtPath(tab.doc.document, tab.session.selection);
|
|
184
|
-
if (!node?.style)
|
|
185
|
-
|
|
214
|
+
if (!node?.style) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
workspace.styleClipboard = jsonClone(node.style);
|
|
186
218
|
statusMessage("Styles copied");
|
|
187
219
|
}
|
|
188
220
|
|
|
189
221
|
export function pasteStyles() {
|
|
190
|
-
if (!workspace.styleClipboard)
|
|
222
|
+
if (!workspace.styleClipboard) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
191
225
|
const tab = activeTab.value;
|
|
192
|
-
if (!tab?.session.selection)
|
|
193
|
-
|
|
226
|
+
if (!tab?.session.selection) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const style = jsonClone(workspace.styleClipboard);
|
|
194
230
|
const sel = tab.session.selection as JxPath;
|
|
195
231
|
transactDoc(tab, (t) => mutateReplaceStyle(t, sel, style));
|
|
196
232
|
statusMessage("Styles pasted");
|
|
@@ -225,82 +261,91 @@ export function showContextMenu(
|
|
|
225
261
|
dismissContextMenu();
|
|
226
262
|
|
|
227
263
|
const tab = activeTab.value;
|
|
228
|
-
if (!tab)
|
|
264
|
+
if (!tab) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
229
267
|
const node = getNodeAtPath(tab.doc.document, path);
|
|
230
|
-
if (!node)
|
|
268
|
+
if (!node) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
231
271
|
|
|
232
272
|
// Select the node
|
|
233
273
|
tab.session.selection = path;
|
|
234
274
|
|
|
235
|
-
|
|
236
|
-
|
|
275
|
+
const items: { label: string; action?: () => void | Promise<void>; danger?: boolean }[] = [
|
|
276
|
+
{ action: () => copyNode(), label: "Copy" },
|
|
277
|
+
];
|
|
237
278
|
|
|
238
|
-
items.push({ label: "Copy", action: () => copyNode() });
|
|
239
279
|
if (path.length >= 2) {
|
|
240
|
-
items.push({
|
|
280
|
+
items.push({ action: () => cutNode(), label: "Cut" });
|
|
241
281
|
items.push({
|
|
242
|
-
label: "Duplicate",
|
|
243
282
|
action: () => transactDoc(activeTab.value, (t) => mutateDuplicateNode(t, path)),
|
|
283
|
+
label: "Duplicate",
|
|
244
284
|
});
|
|
245
285
|
if (node.style) {
|
|
286
|
+
const nodeStyle = node.style;
|
|
246
287
|
items.push({
|
|
247
|
-
label: "Copy styles",
|
|
248
288
|
action: () => {
|
|
249
|
-
workspace.styleClipboard =
|
|
289
|
+
workspace.styleClipboard = jsonClone(nodeStyle);
|
|
250
290
|
statusMessage("Styles copied");
|
|
251
291
|
},
|
|
292
|
+
label: "Copy styles",
|
|
252
293
|
});
|
|
253
294
|
}
|
|
254
295
|
if (workspace.styleClipboard) {
|
|
255
296
|
items.push({
|
|
256
|
-
label: "Paste styles",
|
|
257
297
|
action: () => {
|
|
258
|
-
if (!workspace.styleClipboard)
|
|
259
|
-
|
|
298
|
+
if (!workspace.styleClipboard) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const style = jsonClone(workspace.styleClipboard);
|
|
260
302
|
transactDoc(activeTab.value, (t) => mutateReplaceStyle(t, path, style));
|
|
261
303
|
statusMessage("Styles pasted");
|
|
262
304
|
},
|
|
305
|
+
label: "Paste styles",
|
|
263
306
|
});
|
|
264
307
|
}
|
|
265
|
-
items.push({ label: "—" }); //
|
|
308
|
+
items.push({ label: "—" }); // Separator
|
|
266
309
|
items.push({
|
|
267
|
-
label: "Insert before",
|
|
268
310
|
action: () => {
|
|
269
311
|
const pp = parentElementPath(path) as JxPath;
|
|
270
312
|
const idx = childIndex(path) as number;
|
|
271
313
|
transactDoc(activeTab.value, (t) =>
|
|
272
|
-
mutateInsertNode(t, pp, idx, { tagName: "p"
|
|
314
|
+
mutateInsertNode(t, pp, idx, { children: [], tagName: "p" }),
|
|
273
315
|
);
|
|
274
316
|
},
|
|
317
|
+
label: "Insert before",
|
|
275
318
|
});
|
|
276
319
|
items.push({
|
|
277
|
-
label: "Insert after",
|
|
278
320
|
action: () => {
|
|
279
321
|
const pp = parentElementPath(path) as JxPath;
|
|
280
322
|
const idx = childIndex(path) as number;
|
|
281
323
|
transactDoc(activeTab.value, (t) =>
|
|
282
|
-
mutateInsertNode(t, pp, idx + 1, { tagName: "p"
|
|
324
|
+
mutateInsertNode(t, pp, idx + 1, { children: [], tagName: "p" }),
|
|
283
325
|
);
|
|
284
326
|
},
|
|
327
|
+
label: "Insert after",
|
|
285
328
|
});
|
|
286
329
|
items.push({
|
|
287
|
-
label: "Wrap in Div",
|
|
288
330
|
action: () => transactDoc(activeTab.value, (t) => mutateWrapNode(t, path)),
|
|
331
|
+
label: "Wrap in Div",
|
|
289
332
|
});
|
|
290
333
|
// Don't show Repeat if already inside a repeater (path ends with "children", "map")
|
|
291
|
-
if (
|
|
292
|
-
!(path.length >= 2 && path[path.length - 2] === "children" && path[path.length - 1] === "map")
|
|
293
|
-
) {
|
|
334
|
+
if (!(path.length >= 2 && path.at(-2) === "children" && path.at(-1) === "map")) {
|
|
294
335
|
items.push({
|
|
295
|
-
label: "Repeat...",
|
|
296
336
|
action: () => convertToRepeater(),
|
|
337
|
+
label: "Repeat...",
|
|
297
338
|
});
|
|
298
339
|
}
|
|
299
340
|
items.push({
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
341
|
+
action: async () => {
|
|
342
|
+
if (opts.rerender) {
|
|
343
|
+
// Lazy import breaks the context-menu ↔ layers-panel module cycle
|
|
344
|
+
const { startLayerTitleEdit } = await import("../panels/layers-panel");
|
|
345
|
+
startLayerTitleEdit(path, opts.rerender);
|
|
346
|
+
}
|
|
303
347
|
},
|
|
348
|
+
label: "Set Title",
|
|
304
349
|
});
|
|
305
350
|
if (node.tagName) {
|
|
306
351
|
const isComponent =
|
|
@@ -313,31 +358,32 @@ export function showContextMenu(
|
|
|
313
358
|
(/** @type {{ tagName: string; path: string }} */ c) => c.tagName === node.tagName,
|
|
314
359
|
);
|
|
315
360
|
items.push({
|
|
316
|
-
label: "Edit Component",
|
|
317
361
|
action: () => opts.onEditComponent?.(comp?.path as string),
|
|
362
|
+
label: "Edit Component",
|
|
318
363
|
});
|
|
319
364
|
} else if (!isComponent) {
|
|
320
365
|
items.push({
|
|
321
|
-
label: "Convert to Component",
|
|
322
366
|
action: () => convertToComponent(),
|
|
367
|
+
label: "Convert to Component",
|
|
323
368
|
});
|
|
324
369
|
}
|
|
325
370
|
}
|
|
326
|
-
items.push({ label: "—" }); //
|
|
371
|
+
items.push({ label: "—" }); // Separator
|
|
327
372
|
items.push({
|
|
328
|
-
label: "Delete",
|
|
329
373
|
action: () => transactDoc(activeTab.value, (t) => mutateRemoveNode(t, path)),
|
|
330
374
|
danger: true,
|
|
375
|
+
label: "Delete",
|
|
331
376
|
});
|
|
332
377
|
}
|
|
333
378
|
if (path.length >= 2) {
|
|
334
379
|
items.push({ label: "—" });
|
|
335
380
|
items.push({
|
|
336
|
-
label: "Paste inside",
|
|
337
381
|
action: async () => {
|
|
338
382
|
const nodes = await readFromClipboard();
|
|
339
|
-
if (!nodes || nodes.length === 0)
|
|
340
|
-
|
|
383
|
+
if (!nodes || nodes.length === 0) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const idx = Array.isArray(node.children) ? node.children.length : 0;
|
|
341
387
|
transactDoc(activeTab.value, (t) => {
|
|
342
388
|
for (let i = 0; i < nodes.length; i++) {
|
|
343
389
|
mutateInsertNode(t, path, idx + i, nodes[i]);
|
|
@@ -345,12 +391,14 @@ export function showContextMenu(
|
|
|
345
391
|
});
|
|
346
392
|
statusMessage("Pasted");
|
|
347
393
|
},
|
|
394
|
+
label: "Paste inside",
|
|
348
395
|
});
|
|
349
396
|
items.push({
|
|
350
|
-
label: "Paste after",
|
|
351
397
|
action: async () => {
|
|
352
398
|
const nodes = await readFromClipboard();
|
|
353
|
-
if (!nodes || nodes.length === 0)
|
|
399
|
+
if (!nodes || nodes.length === 0) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
354
402
|
const pp = parentElementPath(path) as JxPath;
|
|
355
403
|
const idx = childIndex(path) as number;
|
|
356
404
|
transactDoc(activeTab.value, (t) => {
|
|
@@ -360,6 +408,7 @@ export function showContextMenu(
|
|
|
360
408
|
});
|
|
361
409
|
statusMessage("Pasted");
|
|
362
410
|
},
|
|
411
|
+
label: "Paste after",
|
|
363
412
|
});
|
|
364
413
|
}
|
|
365
414
|
|
|
@@ -371,13 +420,18 @@ export function showContextMenu(
|
|
|
371
420
|
open
|
|
372
421
|
style="position:fixed;z-index:10000;left:${x}px;top:${y}px"
|
|
373
422
|
${ref((el) => {
|
|
374
|
-
if (!el)
|
|
423
|
+
if (!el) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
375
426
|
requestAnimationFrame(() => {
|
|
376
427
|
const popover = el as HTMLElement;
|
|
377
428
|
const menuRect = popover.getBoundingClientRect();
|
|
378
|
-
if (x + menuRect.width > window.innerWidth)
|
|
379
|
-
|
|
429
|
+
if (x + menuRect.width > window.innerWidth) {
|
|
430
|
+
x = window.innerWidth - menuRect.width - 4;
|
|
431
|
+
}
|
|
432
|
+
if (y + menuRect.height > window.innerHeight) {
|
|
380
433
|
y = window.innerHeight - menuRect.height - 4;
|
|
434
|
+
}
|
|
381
435
|
popover.style.left = `${x}px`;
|
|
382
436
|
popover.style.top = `${y}px`;
|
|
383
437
|
});
|