@jxsuite/studio 0.8.0 → 0.9.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 +120246 -120127
- package/dist/studio.js.map +71 -63
- package/package.json +1 -1
- package/src/canvas/canvas-utils.js +313 -0
- package/src/editor/component-inline-edit.js +316 -0
- package/src/editor/content-inline-edit.js +220 -0
- package/src/panels/canvas-dnd.js +165 -0
- package/src/panels/panel-events.js +263 -0
- package/src/panels/preview-render.js +103 -0
- package/src/panels/pseudo-preview.js +64 -0
- package/src/studio.js +59 -1536
- package/src/utils/edit-display.js +197 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edit-mode display transforms — extracted from studio.js (Phase 4i). Pure stateless functions that
|
|
3
|
+
* convert document trees for visual editing (template expressions, $map, $switch, empty
|
|
4
|
+
* placeholders).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Convert a template string to a displayable expression for edit mode. Replaces ${expr} with ❮ expr
|
|
9
|
+
* ❯ so the runtime renders it as literal text.
|
|
10
|
+
*
|
|
11
|
+
* @param {any} str
|
|
12
|
+
*/
|
|
13
|
+
export function templateToEditDisplay(str) {
|
|
14
|
+
return str.replace(/\$\{([^}]+)\}/g, "\u276A $1 \u276B");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Reverse templateToEditDisplay: walk all text nodes in `el` and replace ❪ expr ❫ back to ${expr}
|
|
19
|
+
* so the user edits raw template syntax.
|
|
20
|
+
*
|
|
21
|
+
* @param {any} el
|
|
22
|
+
*/
|
|
23
|
+
export function restoreTemplateExpressions(el) {
|
|
24
|
+
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
|
25
|
+
while (walker.nextNode()) {
|
|
26
|
+
const node = /** @type {any} */ (walker.currentNode);
|
|
27
|
+
if (node.textContent.includes("\u276A")) {
|
|
28
|
+
node.textContent = node.textContent.replace(/\u276A\s*(.*?)\s*\u276B/g, "${$1}");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Prepare a document for edit-mode rendering. Replaces template strings with readable literal text,
|
|
35
|
+
* $prototype:Array with placeholders, and $ref bindings with display labels. Preserves state so the
|
|
36
|
+
* runtime can still initialise scope.
|
|
37
|
+
*
|
|
38
|
+
* @param {any} node
|
|
39
|
+
* @returns {any}
|
|
40
|
+
*/
|
|
41
|
+
export function prepareForEditMode(node) {
|
|
42
|
+
if (!node || typeof node !== "object") return node;
|
|
43
|
+
if (Array.isArray(node)) return node.map(prepareForEditMode);
|
|
44
|
+
|
|
45
|
+
/** @type {Record<string, any>} */
|
|
46
|
+
const out = {};
|
|
47
|
+
for (const [k, v] of Object.entries(node)) {
|
|
48
|
+
if (k === "state" || k === "$media" || k === "$props" || k === "$elements") {
|
|
49
|
+
out[k] = v; // preserve as-is for runtime resolution
|
|
50
|
+
} else if (k === "children") {
|
|
51
|
+
if (Array.isArray(v)) {
|
|
52
|
+
out.children = v.map(prepareForEditMode);
|
|
53
|
+
} else if (v && typeof v === "object" && v.$prototype === "Array") {
|
|
54
|
+
// Wrap the map template in a visual repeater perimeter
|
|
55
|
+
const template = v.map;
|
|
56
|
+
if (template && typeof template === "object") {
|
|
57
|
+
out.children = [
|
|
58
|
+
{
|
|
59
|
+
tagName: "div",
|
|
60
|
+
className: "repeater-perimeter",
|
|
61
|
+
state: {
|
|
62
|
+
$map: { item: {}, index: 0 },
|
|
63
|
+
"$map/item": {},
|
|
64
|
+
"$map/index": 0,
|
|
65
|
+
},
|
|
66
|
+
children: [prepareForEditMode(template)],
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
} else {
|
|
70
|
+
out.children = [];
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
out.children = prepareForEditMode(v);
|
|
74
|
+
}
|
|
75
|
+
} else if (k === "cases" && node.$switch && v && typeof v === "object") {
|
|
76
|
+
// Replace $switch cases with a placeholder showing the first case or a label
|
|
77
|
+
const caseKeys = Object.keys(v);
|
|
78
|
+
if (caseKeys.length > 0) {
|
|
79
|
+
const firstCase = v[caseKeys[0]];
|
|
80
|
+
if (firstCase && typeof firstCase === "object" && !firstCase.$ref) {
|
|
81
|
+
out.children = [prepareForEditMode(firstCase)];
|
|
82
|
+
} else {
|
|
83
|
+
out.children = [
|
|
84
|
+
{
|
|
85
|
+
tagName: "div",
|
|
86
|
+
textContent: `[$switch: ${caseKeys.join(" | ")}]`,
|
|
87
|
+
style: {
|
|
88
|
+
fontFamily: "'SF Mono', 'Fira Code', monospace",
|
|
89
|
+
fontSize: "11px",
|
|
90
|
+
padding: "6px 10px",
|
|
91
|
+
background: "color-mix(in srgb, var(--danger) 8%, transparent)",
|
|
92
|
+
border: "1px dashed color-mix(in srgb, var(--danger) 40%, transparent)",
|
|
93
|
+
borderRadius: "4px",
|
|
94
|
+
color: "var(--danger)",
|
|
95
|
+
fontStyle: "italic",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} else if (k === "style") {
|
|
102
|
+
// Replace template strings in style values with empty strings
|
|
103
|
+
if (v && typeof v === "object") {
|
|
104
|
+
/** @type {Record<string, any>} */
|
|
105
|
+
const s = {};
|
|
106
|
+
for (const [sk, sv] of Object.entries(v)) {
|
|
107
|
+
s[sk] = typeof sv === "string" && sv.includes("${") ? "" : sv;
|
|
108
|
+
}
|
|
109
|
+
out.style = s;
|
|
110
|
+
} else {
|
|
111
|
+
out.style = v;
|
|
112
|
+
}
|
|
113
|
+
} else if (typeof v === "string" && v.includes("${")) {
|
|
114
|
+
// Template string in a display property → show raw expression
|
|
115
|
+
out[k] = templateToEditDisplay(v);
|
|
116
|
+
} else if (v && typeof v === "object" && v.$ref) {
|
|
117
|
+
// $ref binding → show ref path as literal text
|
|
118
|
+
const ref = v.$ref;
|
|
119
|
+
const label = ref.startsWith("#/state/") ? ref.slice(8) : ref;
|
|
120
|
+
out[k] = `{${label}}`;
|
|
121
|
+
} else {
|
|
122
|
+
out[k] = prepareForEditMode(v);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Mark empty elements with placeholder classes for design-mode visibility
|
|
127
|
+
if (out.tagName && !out.textContent && !out.innerHTML) {
|
|
128
|
+
const hasChildren = Array.isArray(out.children) && out.children.length > 0;
|
|
129
|
+
if (!hasChildren) {
|
|
130
|
+
const tag = out.tagName;
|
|
131
|
+
const textTags = new Set([
|
|
132
|
+
"p",
|
|
133
|
+
"h1",
|
|
134
|
+
"h2",
|
|
135
|
+
"h3",
|
|
136
|
+
"h4",
|
|
137
|
+
"h5",
|
|
138
|
+
"h6",
|
|
139
|
+
"blockquote",
|
|
140
|
+
"li",
|
|
141
|
+
"dt",
|
|
142
|
+
"dd",
|
|
143
|
+
"th",
|
|
144
|
+
"td",
|
|
145
|
+
"span",
|
|
146
|
+
"strong",
|
|
147
|
+
"em",
|
|
148
|
+
"small",
|
|
149
|
+
"mark",
|
|
150
|
+
"code",
|
|
151
|
+
"abbr",
|
|
152
|
+
"q",
|
|
153
|
+
"sub",
|
|
154
|
+
"sup",
|
|
155
|
+
"time",
|
|
156
|
+
"a",
|
|
157
|
+
"button",
|
|
158
|
+
"label",
|
|
159
|
+
"legend",
|
|
160
|
+
"caption",
|
|
161
|
+
"summary",
|
|
162
|
+
"pre",
|
|
163
|
+
"option",
|
|
164
|
+
]);
|
|
165
|
+
const containerTags = new Set([
|
|
166
|
+
"div",
|
|
167
|
+
"section",
|
|
168
|
+
"article",
|
|
169
|
+
"aside",
|
|
170
|
+
"header",
|
|
171
|
+
"footer",
|
|
172
|
+
"main",
|
|
173
|
+
"nav",
|
|
174
|
+
"figure",
|
|
175
|
+
"figcaption",
|
|
176
|
+
"details",
|
|
177
|
+
"fieldset",
|
|
178
|
+
"form",
|
|
179
|
+
"ul",
|
|
180
|
+
"ol",
|
|
181
|
+
"dl",
|
|
182
|
+
"table",
|
|
183
|
+
]);
|
|
184
|
+
if (textTags.has(tag)) {
|
|
185
|
+
out.className = out.className
|
|
186
|
+
? out.className + " empty-text-placeholder"
|
|
187
|
+
: "empty-text-placeholder";
|
|
188
|
+
} else if (containerTags.has(tag)) {
|
|
189
|
+
out.className = out.className
|
|
190
|
+
? out.className + " empty-container-placeholder"
|
|
191
|
+
: "empty-container-placeholder";
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return out;
|
|
197
|
+
}
|