@jxsuite/studio 0.7.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 +125373 -124927
- package/dist/studio.js.map +87 -73
- package/package.json +2 -2
- 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/editor/insertion-helper.js +301 -0
- package/src/editor/slash-menu.js +111 -37
- package/src/panels/block-action-bar.js +436 -0
- package/src/panels/canvas-dnd.js +165 -0
- package/src/panels/dnd.js +332 -0
- package/src/panels/editors.js +196 -0
- package/src/panels/left-panel.js +3 -2
- package/src/panels/panel-events.js +263 -0
- package/src/panels/preview-render.js +103 -0
- package/src/panels/properties-panel.js +1340 -0
- package/src/panels/pseudo-preview.js +64 -0
- package/src/panels/right-panel.js +2 -1
- package/src/panels/shared.js +74 -0
- package/src/panels/stylebook-panel.js +1052 -0
- package/src/studio.js +121 -4874
- package/src/utils/edit-display.js +197 -0
package/src/panels/left-panel.js
CHANGED
|
@@ -20,6 +20,7 @@ import { ensureLitState } from "./shared.js";
|
|
|
20
20
|
import { renderLayersTemplate } from "./layers-panel.js";
|
|
21
21
|
import { renderStylebookLayersTemplate } from "./stylebook-layers-panel.js";
|
|
22
22
|
import { renderElementsTemplate } from "./elements-panel.js";
|
|
23
|
+
import { selectStylebookTag, stylebookMeta } from "./stylebook-panel.js";
|
|
23
24
|
|
|
24
25
|
/** @type {any} */
|
|
25
26
|
let _ctx = null;
|
|
@@ -65,8 +66,8 @@ function _render() {
|
|
|
65
66
|
content =
|
|
66
67
|
_ctx.getCanvasMode() === "settings"
|
|
67
68
|
? renderStylebookLayersTemplate({
|
|
68
|
-
selectStylebookTag
|
|
69
|
-
stylebookMeta
|
|
69
|
+
selectStylebookTag,
|
|
70
|
+
stylebookMeta,
|
|
70
71
|
})
|
|
71
72
|
: renderLayersTemplate({
|
|
72
73
|
navigateToComponent: _ctx.navigateToComponent,
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Panel events — extracted from studio.js (Phase 4m). Unified event handler system for canvas
|
|
3
|
+
* panels: click-to-select, double-click inline edit, context menu, hover tracking, insertion
|
|
4
|
+
* helper.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
update,
|
|
9
|
+
selectNode,
|
|
10
|
+
hoverNode,
|
|
11
|
+
elToPath,
|
|
12
|
+
pathsEqual,
|
|
13
|
+
insertNode,
|
|
14
|
+
parentElementPath,
|
|
15
|
+
childIndex,
|
|
16
|
+
getNodeAtPath,
|
|
17
|
+
renderOnly,
|
|
18
|
+
} from "../store.js";
|
|
19
|
+
import { view } from "../view.js";
|
|
20
|
+
import { stopEditing, isEditing, isEditableBlock } from "../editor/inline-edit.js";
|
|
21
|
+
import { showContextMenu } from "../editor/context-menu.js";
|
|
22
|
+
import * as insertionHelper from "../editor/insertion-helper.js";
|
|
23
|
+
import { defaultDef } from "../panels/shared.js";
|
|
24
|
+
|
|
25
|
+
/** @type {any} */
|
|
26
|
+
let _ctx = null;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Initialize the panel events module.
|
|
30
|
+
*
|
|
31
|
+
* @param {{
|
|
32
|
+
* getState: () => any;
|
|
33
|
+
* setState: (s: any) => void;
|
|
34
|
+
* getCanvasMode: () => string;
|
|
35
|
+
* bubbleInlinePath: (doc: any, path: any) => any;
|
|
36
|
+
* findCanvasElement: (path: any, canvasEl: any) => any;
|
|
37
|
+
* enterInlineEdit: (el: any, path: any) => void;
|
|
38
|
+
* navigateToComponent: (path: any) => void;
|
|
39
|
+
* effectiveZoom: () => number;
|
|
40
|
+
* }} ctx
|
|
41
|
+
*/
|
|
42
|
+
export function initPanelEvents(ctx) {
|
|
43
|
+
_ctx = ctx;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @param {any} panel */
|
|
47
|
+
export function registerPanelEvents(panel) {
|
|
48
|
+
const { canvas, overlayClk, mediaName } = panel;
|
|
49
|
+
const ac = new AbortController();
|
|
50
|
+
const opts = { signal: ac.signal };
|
|
51
|
+
view.canvasEventCleanups.push(() => ac.abort());
|
|
52
|
+
|
|
53
|
+
/** @param {any} fn */
|
|
54
|
+
function withPanelPointerEvents(fn) {
|
|
55
|
+
const els = canvas.querySelectorAll("*");
|
|
56
|
+
for (const el of els) el.style.pointerEvents = "auto";
|
|
57
|
+
overlayClk.style.display = "none";
|
|
58
|
+
const result = fn();
|
|
59
|
+
overlayClk.style.display = "";
|
|
60
|
+
for (const el of els) el.style.pointerEvents = "none";
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
overlayClk.addEventListener(
|
|
65
|
+
"click",
|
|
66
|
+
(/** @type {any} */ e) => {
|
|
67
|
+
const barInner = view.blockActionBarEl?.firstElementChild;
|
|
68
|
+
if (barInner) {
|
|
69
|
+
const r = barInner.getBoundingClientRect();
|
|
70
|
+
if (
|
|
71
|
+
e.clientX >= r.left &&
|
|
72
|
+
e.clientX <= r.right &&
|
|
73
|
+
e.clientY >= r.top &&
|
|
74
|
+
e.clientY <= r.bottom
|
|
75
|
+
)
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (isEditing()) {
|
|
79
|
+
stopEditing();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const S = _ctx.getState();
|
|
83
|
+
const canvasMode = _ctx.getCanvasMode();
|
|
84
|
+
|
|
85
|
+
const elements = withPanelPointerEvents(() =>
|
|
86
|
+
document.elementsFromPoint(e.clientX, e.clientY),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
for (const el of elements) {
|
|
90
|
+
if (canvas.contains(el) && el !== canvas) {
|
|
91
|
+
const originalPath = elToPath.get(el);
|
|
92
|
+
if (originalPath) {
|
|
93
|
+
let path = _ctx.bubbleInlinePath(S.document, originalPath);
|
|
94
|
+
const newMedia = mediaName === "base" ? null : (mediaName ?? null);
|
|
95
|
+
const withMedia = { ...S, ui: { ...S.ui, activeMedia: newMedia } };
|
|
96
|
+
|
|
97
|
+
const resolvedEl =
|
|
98
|
+
path === originalPath ? el : _ctx.findCanvasElement(path, canvas) || el;
|
|
99
|
+
|
|
100
|
+
if (
|
|
101
|
+
pathsEqual(path, S.selection) &&
|
|
102
|
+
isEditableBlock(resolvedEl) &&
|
|
103
|
+
(canvasMode === "edit" || S.mode === "content")
|
|
104
|
+
) {
|
|
105
|
+
_ctx.setState(withMedia);
|
|
106
|
+
_ctx.enterInlineEdit(resolvedEl, path);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (canvasMode === "design" && S.mode !== "content") {
|
|
111
|
+
view.pendingInlineEdit = { path, mediaName };
|
|
112
|
+
update(selectNode(withMedia, path));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
update(selectNode(withMedia, path));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
update(selectNode(S, null));
|
|
122
|
+
},
|
|
123
|
+
opts,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
overlayClk.addEventListener(
|
|
127
|
+
"dblclick",
|
|
128
|
+
(/** @type {any} */ e) => {
|
|
129
|
+
const barInner = view.blockActionBarEl?.firstElementChild;
|
|
130
|
+
if (barInner) {
|
|
131
|
+
const r = barInner.getBoundingClientRect();
|
|
132
|
+
if (
|
|
133
|
+
e.clientX >= r.left &&
|
|
134
|
+
e.clientX <= r.right &&
|
|
135
|
+
e.clientY >= r.top &&
|
|
136
|
+
e.clientY <= r.bottom
|
|
137
|
+
)
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const canvasMode = _ctx.getCanvasMode();
|
|
141
|
+
if (canvasMode !== "edit" && canvasMode !== "design") return;
|
|
142
|
+
|
|
143
|
+
const S = _ctx.getState();
|
|
144
|
+
const elements = withPanelPointerEvents(() =>
|
|
145
|
+
document.elementsFromPoint(e.clientX, e.clientY),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
for (const el of elements) {
|
|
149
|
+
if (canvas.contains(el) && el !== canvas) {
|
|
150
|
+
const originalPath = elToPath.get(el);
|
|
151
|
+
if (originalPath) {
|
|
152
|
+
const path = _ctx.bubbleInlinePath(S.document, originalPath);
|
|
153
|
+
const resolvedEl =
|
|
154
|
+
path === originalPath ? el : _ctx.findCanvasElement(path, canvas) || el;
|
|
155
|
+
if (isEditableBlock(resolvedEl)) {
|
|
156
|
+
const newMedia = mediaName === "base" ? null : (mediaName ?? null);
|
|
157
|
+
const withMedia = { ...S, ui: { ...S.ui, activeMedia: newMedia } };
|
|
158
|
+
update(selectNode(withMedia, path));
|
|
159
|
+
_ctx.enterInlineEdit(resolvedEl, path);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
opts,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
overlayClk.addEventListener(
|
|
170
|
+
"contextmenu",
|
|
171
|
+
(/** @type {any} */ e) => {
|
|
172
|
+
const barInner = view.blockActionBarEl?.firstElementChild;
|
|
173
|
+
if (barInner) {
|
|
174
|
+
const r = barInner.getBoundingClientRect();
|
|
175
|
+
if (
|
|
176
|
+
e.clientX >= r.left &&
|
|
177
|
+
e.clientX <= r.right &&
|
|
178
|
+
e.clientY >= r.top &&
|
|
179
|
+
e.clientY <= r.bottom
|
|
180
|
+
)
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const S = _ctx.getState();
|
|
184
|
+
const elements = withPanelPointerEvents(() =>
|
|
185
|
+
document.elementsFromPoint(e.clientX, e.clientY),
|
|
186
|
+
);
|
|
187
|
+
for (const el of elements) {
|
|
188
|
+
if (canvas.contains(el) && el !== canvas) {
|
|
189
|
+
let path = elToPath.get(el);
|
|
190
|
+
if (path) {
|
|
191
|
+
path = _ctx.bubbleInlinePath(S.document, path);
|
|
192
|
+
showContextMenu(e, path, S, { onEditComponent: _ctx.navigateToComponent });
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
e.preventDefault();
|
|
198
|
+
},
|
|
199
|
+
opts,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
overlayClk.addEventListener(
|
|
203
|
+
"mousemove",
|
|
204
|
+
(/** @type {any} */ e) => {
|
|
205
|
+
const barInner = view.blockActionBarEl?.firstElementChild;
|
|
206
|
+
if (barInner) {
|
|
207
|
+
const r = barInner.getBoundingClientRect();
|
|
208
|
+
if (
|
|
209
|
+
e.clientX >= r.left &&
|
|
210
|
+
e.clientX <= r.right &&
|
|
211
|
+
e.clientY >= r.top &&
|
|
212
|
+
e.clientY <= r.bottom
|
|
213
|
+
)
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
let S = _ctx.getState();
|
|
217
|
+
const el = withPanelPointerEvents(() => document.elementFromPoint(e.clientX, e.clientY));
|
|
218
|
+
if (el && canvas.contains(el) && el !== canvas) {
|
|
219
|
+
let path = elToPath.get(el);
|
|
220
|
+
if (path) {
|
|
221
|
+
path = _ctx.bubbleInlinePath(S.document, path);
|
|
222
|
+
if (!pathsEqual(path, S.hover)) {
|
|
223
|
+
_ctx.setState(hoverNode(S, path));
|
|
224
|
+
renderOnly("overlays");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} else if (S.hover) {
|
|
228
|
+
_ctx.setState(hoverNode(S, null));
|
|
229
|
+
renderOnly("overlays");
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
opts,
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
overlayClk.addEventListener(
|
|
236
|
+
"mouseleave",
|
|
237
|
+
() => {
|
|
238
|
+
const S = _ctx.getState();
|
|
239
|
+
if (S.hover) {
|
|
240
|
+
_ctx.setState(hoverNode(S, null));
|
|
241
|
+
renderOnly("overlays");
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
opts,
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
insertionHelper.mount({
|
|
248
|
+
getState: _ctx.getState,
|
|
249
|
+
update,
|
|
250
|
+
getCanvasMode: _ctx.getCanvasMode,
|
|
251
|
+
withPanelPointerEvents,
|
|
252
|
+
effectiveZoom: _ctx.effectiveZoom,
|
|
253
|
+
defaultDef,
|
|
254
|
+
insertNode,
|
|
255
|
+
selectNode,
|
|
256
|
+
parentElementPath,
|
|
257
|
+
childIndex,
|
|
258
|
+
getNodeAtPath,
|
|
259
|
+
elToPath,
|
|
260
|
+
panel,
|
|
261
|
+
});
|
|
262
|
+
view.canvasEventCleanups.push(() => insertionHelper.unmount());
|
|
263
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preview render — extracted from studio.js (Phase 4m). Structural preview renderer that creates
|
|
3
|
+
* DOM from Jx node trees as a fallback when runtime rendering fails.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getState, elToPath } from "../store.js";
|
|
7
|
+
import { applyCanvasStyle } from "../utils/canvas-media.js";
|
|
8
|
+
import { resolveDefaultForCanvas } from "../panels/signals-panel.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Recursively render a Jx node to the canvas DOM. Media-aware: applies base styles + active
|
|
12
|
+
* breakpoint/feature overrides.
|
|
13
|
+
*
|
|
14
|
+
* @param {any} node
|
|
15
|
+
* @param {any} path
|
|
16
|
+
* @param {any} parent
|
|
17
|
+
* @param {any} activeBreakpoints
|
|
18
|
+
* @param {any} featureToggles
|
|
19
|
+
*/
|
|
20
|
+
export function renderCanvasNode(node, path, parent, activeBreakpoints, featureToggles) {
|
|
21
|
+
if (typeof node === "string" || typeof node === "number" || typeof node === "boolean") {
|
|
22
|
+
parent.appendChild(document.createTextNode(String(node)));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (!node || typeof node !== "object") return;
|
|
26
|
+
|
|
27
|
+
const tag = node.tagName || "div";
|
|
28
|
+
const el = document.createElement(tag);
|
|
29
|
+
|
|
30
|
+
elToPath.set(el, path);
|
|
31
|
+
|
|
32
|
+
if (typeof node.textContent === "string") {
|
|
33
|
+
el.textContent = node.textContent;
|
|
34
|
+
} else if (typeof node.textContent === "object" && node.textContent?.$ref) {
|
|
35
|
+
const resolved = resolveDefaultForCanvas(node.textContent, getState().document.state);
|
|
36
|
+
el.textContent = resolved;
|
|
37
|
+
el.style.opacity = "0.7";
|
|
38
|
+
el.style.fontStyle = "italic";
|
|
39
|
+
el.title = `Bound: ${node.textContent.$ref}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (node.id) el.id = node.id;
|
|
43
|
+
if (node.className) el.className = node.className;
|
|
44
|
+
|
|
45
|
+
applyCanvasStyle(el, node.style, activeBreakpoints, featureToggles);
|
|
46
|
+
|
|
47
|
+
if (node.attributes && typeof node.attributes === "object") {
|
|
48
|
+
for (const [attr, val] of Object.entries(node.attributes)) {
|
|
49
|
+
try {
|
|
50
|
+
if (typeof val === "object" && val?.$ref) {
|
|
51
|
+
const resolved = resolveDefaultForCanvas(val, getState().document.state);
|
|
52
|
+
el.setAttribute(attr, resolved);
|
|
53
|
+
} else {
|
|
54
|
+
el.setAttribute(attr, val);
|
|
55
|
+
}
|
|
56
|
+
} catch {}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (Array.isArray(node.children)) {
|
|
61
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
62
|
+
renderCanvasNode(
|
|
63
|
+
node.children[i],
|
|
64
|
+
[...path, "children", i],
|
|
65
|
+
el,
|
|
66
|
+
activeBreakpoints,
|
|
67
|
+
featureToggles,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
} else if (
|
|
71
|
+
node.children &&
|
|
72
|
+
typeof node.children === "object" &&
|
|
73
|
+
node.children.$prototype === "Array"
|
|
74
|
+
) {
|
|
75
|
+
const template = node.children.map;
|
|
76
|
+
if (template && typeof template === "object") {
|
|
77
|
+
const wrapper = document.createElement("div");
|
|
78
|
+
wrapper.className = "repeater-perimeter";
|
|
79
|
+
elToPath.set(wrapper, [...path, "children"]);
|
|
80
|
+
renderCanvasNode(
|
|
81
|
+
template,
|
|
82
|
+
[...path, "children", "map"],
|
|
83
|
+
wrapper,
|
|
84
|
+
activeBreakpoints,
|
|
85
|
+
featureToggles,
|
|
86
|
+
);
|
|
87
|
+
el.appendChild(wrapper);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (node.$switch && node.cases && typeof node.cases === "object") {
|
|
92
|
+
const keys = Object.keys(node.cases);
|
|
93
|
+
const placeholder = document.createElement("div");
|
|
94
|
+
placeholder.textContent = `[$switch: ${keys.join(" | ")}]`;
|
|
95
|
+
placeholder.style.cssText =
|
|
96
|
+
"font-family:monospace;font-size:11px;padding:6px 10px;background:color-mix(in srgb, var(--danger) 8%, transparent);border:1px dashed color-mix(in srgb, var(--danger) 40%, transparent);border-radius:4px;color:var(--danger);font-style:italic";
|
|
97
|
+
el.appendChild(placeholder);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
el.style.pointerEvents = "none";
|
|
101
|
+
parent.appendChild(el);
|
|
102
|
+
return el;
|
|
103
|
+
}
|