@jxsuite/studio 0.14.0 → 0.15.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/studio.js +40 -9
- package/dist/studio.js.map +5 -5
- package/package.json +1 -1
- package/src/editor/context-menu.js +47 -10
- package/src/tabs/transact.js +19 -1
- package/src/workspace/workspace.js +4 -0
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// ─── Clipboard & Context Menu ─────────────────────────────────────────────────
|
|
2
2
|
import { html, render as litRender } from "lit-html";
|
|
3
3
|
import { getNodeAtPath, parentElementPath, childIndex } from "../store.js";
|
|
4
|
-
import { activeTab } from "../workspace/workspace.js";
|
|
4
|
+
import { activeTab, workspace } from "../workspace/workspace.js";
|
|
5
5
|
import {
|
|
6
6
|
transactDoc,
|
|
7
7
|
mutateInsertNode,
|
|
8
8
|
mutateRemoveNode,
|
|
9
9
|
mutateDuplicateNode,
|
|
10
10
|
mutateWrapNode,
|
|
11
|
+
mutateReplaceStyle,
|
|
11
12
|
} from "../tabs/transact.js";
|
|
12
13
|
import { statusMessage } from "../panels/statusbar.js";
|
|
13
14
|
import { convertToComponent } from "./convert-to-component.js";
|
|
@@ -21,9 +22,6 @@ import { componentRegistry } from "../files/components.js";
|
|
|
21
22
|
* @typedef {import("../state.js").JxNode} JxNode
|
|
22
23
|
*/
|
|
23
24
|
|
|
24
|
-
/** @type {JxNode | null} */
|
|
25
|
-
let clipboard = null;
|
|
26
|
-
|
|
27
25
|
// ─── Clipboard ────────────────────────────────────────────────────────────────
|
|
28
26
|
|
|
29
27
|
export function copyNode() {
|
|
@@ -31,7 +29,7 @@ export function copyNode() {
|
|
|
31
29
|
if (!tab?.session.selection) return;
|
|
32
30
|
const node = getNodeAtPath(tab.doc.document, tab.session.selection);
|
|
33
31
|
if (!node) return;
|
|
34
|
-
clipboard = structuredClone(node);
|
|
32
|
+
workspace.clipboard = structuredClone(node);
|
|
35
33
|
statusMessage("Copied");
|
|
36
34
|
}
|
|
37
35
|
|
|
@@ -41,16 +39,16 @@ export function cutNode() {
|
|
|
41
39
|
const sel = tab.session.selection;
|
|
42
40
|
const node = getNodeAtPath(tab.doc.document, sel);
|
|
43
41
|
if (!node) return;
|
|
44
|
-
clipboard = structuredClone(node);
|
|
42
|
+
workspace.clipboard = structuredClone(node);
|
|
45
43
|
transactDoc(tab, (t) => mutateRemoveNode(t, sel));
|
|
46
44
|
statusMessage("Cut");
|
|
47
45
|
}
|
|
48
46
|
|
|
49
47
|
export function pasteNode() {
|
|
50
|
-
if (!clipboard) return;
|
|
48
|
+
if (!workspace.clipboard) return;
|
|
51
49
|
const tab = activeTab.value;
|
|
52
50
|
if (!tab) return;
|
|
53
|
-
const clip = clipboard;
|
|
51
|
+
const clip = workspace.clipboard;
|
|
54
52
|
const pPath = tab.session.selection || [];
|
|
55
53
|
const parent = getNodeAtPath(tab.doc.document, pPath);
|
|
56
54
|
if (!parent) return;
|
|
@@ -66,6 +64,25 @@ export function pasteNode() {
|
|
|
66
64
|
statusMessage("Pasted");
|
|
67
65
|
}
|
|
68
66
|
|
|
67
|
+
export function copyStyles() {
|
|
68
|
+
const tab = activeTab.value;
|
|
69
|
+
if (!tab?.session.selection) return;
|
|
70
|
+
const node = getNodeAtPath(tab.doc.document, tab.session.selection);
|
|
71
|
+
if (!node?.style) return;
|
|
72
|
+
workspace.styleClipboard = JSON.parse(JSON.stringify(node.style));
|
|
73
|
+
statusMessage("Styles copied");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function pasteStyles() {
|
|
77
|
+
if (!workspace.styleClipboard) return;
|
|
78
|
+
const tab = activeTab.value;
|
|
79
|
+
if (!tab?.session.selection) return;
|
|
80
|
+
const style = JSON.parse(JSON.stringify(workspace.styleClipboard));
|
|
81
|
+
const sel = /** @type {JxPath} */ (tab.session.selection);
|
|
82
|
+
transactDoc(tab, (t) => mutateReplaceStyle(t, sel, style));
|
|
83
|
+
statusMessage("Styles pasted");
|
|
84
|
+
}
|
|
85
|
+
|
|
69
86
|
// ─── Context menu ─────────────────────────────────────────────────────────────
|
|
70
87
|
|
|
71
88
|
const ctxMenu = document.createElement("sp-popover");
|
|
@@ -109,6 +126,26 @@ export function showContextMenu(e, path, opts = {}) {
|
|
|
109
126
|
label: "Duplicate",
|
|
110
127
|
action: () => transactDoc(activeTab.value, (t) => mutateDuplicateNode(t, path)),
|
|
111
128
|
});
|
|
129
|
+
if (node.style) {
|
|
130
|
+
items.push({
|
|
131
|
+
label: "Copy styles",
|
|
132
|
+
action: () => {
|
|
133
|
+
workspace.styleClipboard = JSON.parse(JSON.stringify(node.style));
|
|
134
|
+
statusMessage("Styles copied");
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if (workspace.styleClipboard) {
|
|
139
|
+
items.push({
|
|
140
|
+
label: "Paste styles",
|
|
141
|
+
action: () => {
|
|
142
|
+
if (!workspace.styleClipboard) return;
|
|
143
|
+
const style = JSON.parse(JSON.stringify(workspace.styleClipboard));
|
|
144
|
+
transactDoc(activeTab.value, (t) => mutateReplaceStyle(t, path, style));
|
|
145
|
+
statusMessage("Styles pasted");
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
112
149
|
items.push({ label: "—" }); // separator
|
|
113
150
|
items.push({
|
|
114
151
|
label: "Insert before",
|
|
@@ -162,8 +199,8 @@ export function showContextMenu(e, path, opts = {}) {
|
|
|
162
199
|
danger: true,
|
|
163
200
|
});
|
|
164
201
|
}
|
|
165
|
-
if (clipboard) {
|
|
166
|
-
const clip = clipboard;
|
|
202
|
+
if (workspace.clipboard) {
|
|
203
|
+
const clip = workspace.clipboard;
|
|
167
204
|
items.push({ label: "—" });
|
|
168
205
|
items.push({
|
|
169
206
|
label: "Paste inside",
|
package/src/tabs/transact.js
CHANGED
|
@@ -26,8 +26,12 @@ const HISTORY_LIMIT = 100;
|
|
|
26
26
|
export function transactDoc(tab, mutationFn, { skipHistory = false } = {}) {
|
|
27
27
|
mutationFn(tab);
|
|
28
28
|
|
|
29
|
+
// Replace the document root reference so effects tracking tab.doc.document re-trigger.
|
|
30
|
+
// Nested objects are shared — the mutation already modified them in place.
|
|
31
|
+
const raw = toRaw(tab.doc.document);
|
|
32
|
+
tab.doc.document = { ...raw };
|
|
33
|
+
|
|
29
34
|
if (!skipHistory) {
|
|
30
|
-
const raw = toRaw(tab.doc.document);
|
|
31
35
|
const snapshot = {
|
|
32
36
|
document: structuredClone(raw),
|
|
33
37
|
selection: tab.session.selection ? [...tab.session.selection] : null,
|
|
@@ -270,6 +274,20 @@ export function mutateUpdateMediaNestedStyle(tab, path, mediaName, selector, pro
|
|
|
270
274
|
if (Object.keys(node.style).length === 0) delete node.style;
|
|
271
275
|
}
|
|
272
276
|
|
|
277
|
+
/**
|
|
278
|
+
* @param {Tab} tab
|
|
279
|
+
* @param {JxPath} path
|
|
280
|
+
* @param {Record<string, any> | undefined} style
|
|
281
|
+
*/
|
|
282
|
+
export function mutateReplaceStyle(tab, path, style) {
|
|
283
|
+
const node = getNodeAtPath(tab.doc.document, path);
|
|
284
|
+
if (style && Object.keys(style).length > 0) {
|
|
285
|
+
node.style = style;
|
|
286
|
+
} else {
|
|
287
|
+
delete node.style;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
273
291
|
/**
|
|
274
292
|
* @param {Tab} tab
|
|
275
293
|
* @param {string} name
|
|
@@ -16,6 +16,10 @@ export const workspace = reactive({
|
|
|
16
16
|
projectConfig: null,
|
|
17
17
|
/** @type {ComponentEntry[]} */
|
|
18
18
|
componentRegistry: [],
|
|
19
|
+
/** @type {import("../state.js").JxNode | null} */
|
|
20
|
+
clipboard: null,
|
|
21
|
+
/** @type {Record<string, any> | null} */
|
|
22
|
+
styleClipboard: null,
|
|
19
23
|
fileTree: {
|
|
20
24
|
/** @type {Map<string, FileEntry[]>} */
|
|
21
25
|
dirs: new Map(),
|