@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Jx Studio — visual builder for Jx documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@atlaskit/pragmatic-drag-and-drop": "^1.8.1",
|
|
32
32
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
|
33
|
-
"@jxsuite/runtime": "^0.
|
|
33
|
+
"@jxsuite/runtime": "^0.12.0",
|
|
34
34
|
"@spectrum-web-components/accordion": "^1.12.1",
|
|
35
35
|
"@spectrum-web-components/action-bar": "1.12.1",
|
|
36
36
|
"@spectrum-web-components/action-button": "^1.12.1",
|
|
@@ -58,7 +58,9 @@
|
|
|
58
58
|
"@spectrum-web-components/tabs": "^1.12.1",
|
|
59
59
|
"@spectrum-web-components/textfield": "^1.12.1",
|
|
60
60
|
"@spectrum-web-components/theme": "^1.12.1",
|
|
61
|
+
"@spectrum-web-components/toast": "^1.12.1",
|
|
61
62
|
"@spectrum-web-components/tooltip": "^1.12.1",
|
|
63
|
+
"@vue/reactivity": "3.5.34",
|
|
62
64
|
"lit-html": "^3.3.3",
|
|
63
65
|
"monaco-editor": "^0.55.1",
|
|
64
66
|
"remark-directive": "^4.0.0",
|
|
@@ -74,5 +76,6 @@
|
|
|
74
76
|
"@webref/css": "^8.5.6",
|
|
75
77
|
"@webref/elements": "^2.7.1",
|
|
76
78
|
"@webref/idl": "^3.78.0"
|
|
77
|
-
}
|
|
79
|
+
},
|
|
80
|
+
"//vue-reactivity": "Exact pin required — must match @jxsuite/runtime to avoid cross-boundary proxy bugs"
|
|
78
81
|
}
|
package/src/browse/browse.js
CHANGED
|
@@ -95,11 +95,14 @@ async function collectFiles(dir, platform) {
|
|
|
95
95
|
results.push(...sub);
|
|
96
96
|
} else {
|
|
97
97
|
const ext = extOf(entry.name);
|
|
98
|
+
const category = categoryFor(entry.path, ext);
|
|
99
|
+
const type =
|
|
100
|
+
category === "Content" ? contentTypeFor(entry.path) || ext || "file" : ext || "file";
|
|
98
101
|
results.push({
|
|
99
102
|
name: entry.name,
|
|
100
103
|
path: entry.path,
|
|
101
|
-
type
|
|
102
|
-
category
|
|
104
|
+
type,
|
|
105
|
+
category,
|
|
103
106
|
ext,
|
|
104
107
|
});
|
|
105
108
|
}
|
|
@@ -110,6 +113,27 @@ async function collectFiles(dir, platform) {
|
|
|
110
113
|
return results;
|
|
111
114
|
}
|
|
112
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Match a file path against project contentTypes source globs to find its content type name.
|
|
118
|
+
* Returns the content type name (capitalized) or null if no match.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} filePath
|
|
121
|
+
* @returns {string | null}
|
|
122
|
+
*/
|
|
123
|
+
function contentTypeFor(filePath) {
|
|
124
|
+
const config = projectState?.projectConfig;
|
|
125
|
+
if (!config?.contentTypes) return null;
|
|
126
|
+
for (const [name, def] of Object.entries(config.contentTypes)) {
|
|
127
|
+
const d = /** @type {any} */ (def);
|
|
128
|
+
if (!d.source) continue;
|
|
129
|
+
const prefix = d.source.replace(/^\.\//, "").split("/**")[0].split("/*")[0];
|
|
130
|
+
if (filePath.startsWith(prefix + "/") || filePath === prefix) {
|
|
131
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
113
137
|
// ─── Data loading ────────────────────────────────────────────────────────────
|
|
114
138
|
|
|
115
139
|
async function loadFiles() {
|
|
@@ -376,7 +400,7 @@ export async function renderBrowse(container, ctx) {
|
|
|
376
400
|
>
|
|
377
401
|
<sp-table-cell class="browse-name-cell">${f.name}</sp-table-cell>
|
|
378
402
|
<sp-table-cell>${f.category}</sp-table-cell>
|
|
379
|
-
<sp-table-cell>${f.
|
|
403
|
+
<sp-table-cell>${f.type}</sp-table-cell>
|
|
380
404
|
<sp-table-cell class="browse-path-cell">${f.path}</sp-table-cell>
|
|
381
405
|
</sp-table-row>
|
|
382
406
|
`,
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas Diff — computes visual diff between original and current Jx documents.
|
|
3
|
+
*
|
|
4
|
+
* Compares two document trees recursively, marking nodes as added/removed/modified, and propagates
|
|
5
|
+
* "modified" status up the tree for structural changes (children added/removed/reordered).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {Record<string, any>} JxNode
|
|
10
|
+
*
|
|
11
|
+
* @typedef {"added" | "removed" | "modified"} DiffStatus
|
|
12
|
+
*
|
|
13
|
+
* @typedef {{ byPath: Map<string, DiffStatus>; allPaths: Set<string> }} DiffResult
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Deep equality check for two values. Ignores function and ref properties.
|
|
18
|
+
*
|
|
19
|
+
* @param {any} a
|
|
20
|
+
* @param {any} b
|
|
21
|
+
* @returns {boolean}
|
|
22
|
+
*/
|
|
23
|
+
function valuesEqual(a, b) {
|
|
24
|
+
if (a === b) return true;
|
|
25
|
+
if (typeof a !== "object" || typeof b !== "object" || !a || !b) return false;
|
|
26
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
27
|
+
|
|
28
|
+
if (Array.isArray(a)) {
|
|
29
|
+
if (a.length !== b.length) return false;
|
|
30
|
+
return a.every((/** @type {any} */ v, /** @type {number} */ i) => valuesEqual(v, b[i]));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const keysA = Object.keys(a).filter(
|
|
34
|
+
(/** @type {string} */ k) => !k.startsWith("on") && k !== "$ref",
|
|
35
|
+
);
|
|
36
|
+
const keysB = Object.keys(b).filter(
|
|
37
|
+
(/** @type {string} */ k) => !k.startsWith("on") && k !== "$ref",
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (keysA.length !== keysB.length) return false;
|
|
41
|
+
if (!keysA.every((/** @type {string} */ k) => keysB.includes(k))) return false;
|
|
42
|
+
|
|
43
|
+
return keysA.every((/** @type {string} */ k) => valuesEqual(a[k], b[k]));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Filter element children from a node's children array.
|
|
48
|
+
*
|
|
49
|
+
* @param {any} node
|
|
50
|
+
* @returns {any[]}
|
|
51
|
+
*/
|
|
52
|
+
function elementChildren(node) {
|
|
53
|
+
if (!node?.children || !Array.isArray(node.children)) return [];
|
|
54
|
+
return node.children.filter((/** @type {any} */ c) => c != null && typeof c === "object");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Mark an entire subtree with a diff status.
|
|
59
|
+
*
|
|
60
|
+
* @param {any} node
|
|
61
|
+
* @param {DiffStatus} status
|
|
62
|
+
* @param {string} path
|
|
63
|
+
* @param {Map<string, DiffStatus>} diffMap
|
|
64
|
+
* @param {Set<string>} allPaths
|
|
65
|
+
*/
|
|
66
|
+
function markRecursive(node, status, path, diffMap, allPaths) {
|
|
67
|
+
allPaths.add(path);
|
|
68
|
+
diffMap.set(path, status);
|
|
69
|
+
const children = elementChildren(node);
|
|
70
|
+
for (let i = 0; i < children.length; i++) {
|
|
71
|
+
const childPath = `${path}/children/${i}`;
|
|
72
|
+
markRecursive(children[i], status, childPath, diffMap, allPaths);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Compute structural diff between original and current documents.
|
|
78
|
+
*
|
|
79
|
+
* @param {any} originalDoc - Original document (from git)
|
|
80
|
+
* @param {any} currentDoc - Current document (in memory/on disk)
|
|
81
|
+
* @returns {DiffResult}
|
|
82
|
+
*/
|
|
83
|
+
export function computeDocumentDiff(originalDoc, currentDoc) {
|
|
84
|
+
/** @type {Map<string, DiffStatus>} */
|
|
85
|
+
const diffMap = new Map();
|
|
86
|
+
/** @type {Set<string>} */
|
|
87
|
+
const allPaths = new Set();
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Walk both trees in parallel and mark differences.
|
|
91
|
+
*
|
|
92
|
+
* @param {any} origNode
|
|
93
|
+
* @param {any} currNode
|
|
94
|
+
* @param {string} path
|
|
95
|
+
*/
|
|
96
|
+
const walk = (origNode, currNode, path = "") => {
|
|
97
|
+
const pathKey = path || "/";
|
|
98
|
+
allPaths.add(pathKey);
|
|
99
|
+
|
|
100
|
+
// Both exist but differ
|
|
101
|
+
if (origNode != null && currNode != null) {
|
|
102
|
+
if (!valuesEqual(origNode, currNode)) {
|
|
103
|
+
diffMap.set(pathKey, "modified");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Walk children
|
|
107
|
+
if (origNode.children && currNode.children) {
|
|
108
|
+
const origChildren = elementChildren(origNode);
|
|
109
|
+
const currChildren = elementChildren(currNode);
|
|
110
|
+
|
|
111
|
+
for (let i = 0; i < Math.max(origChildren.length, currChildren.length); i++) {
|
|
112
|
+
const childPath = pathKey === "/" ? `children/${i}` : `${pathKey}/children/${i}`;
|
|
113
|
+
walk(origChildren[i], currChildren[i], childPath);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// In current only (added)
|
|
120
|
+
if (currNode != null && origNode == null) {
|
|
121
|
+
diffMap.set(pathKey, "added");
|
|
122
|
+
const children = elementChildren(currNode);
|
|
123
|
+
for (let i = 0; i < children.length; i++) {
|
|
124
|
+
const childPath = pathKey === "/" ? `children/${i}` : `${pathKey}/children/${i}`;
|
|
125
|
+
markRecursive(children[i], "added", childPath, diffMap, allPaths);
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// In original only (removed)
|
|
131
|
+
if (origNode != null && currNode == null) {
|
|
132
|
+
diffMap.set(pathKey, "removed");
|
|
133
|
+
const children = elementChildren(origNode);
|
|
134
|
+
for (let i = 0; i < children.length; i++) {
|
|
135
|
+
const childPath = pathKey === "/" ? `children/${i}` : `${pathKey}/children/${i}`;
|
|
136
|
+
markRecursive(children[i], "removed", childPath, diffMap, allPaths);
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Start comparison
|
|
143
|
+
walk(originalDoc, currentDoc, "");
|
|
144
|
+
|
|
145
|
+
// Propagate "modified" status to parents of added/removed children
|
|
146
|
+
const propagateModified = () => {
|
|
147
|
+
let changed = false;
|
|
148
|
+
for (const [path, status] of diffMap.entries()) {
|
|
149
|
+
if ((status === "added" || status === "removed") && path !== "/") {
|
|
150
|
+
const parentPath = path.split("/").slice(0, -2).join("/") || "/";
|
|
151
|
+
if (parentPath !== "/") {
|
|
152
|
+
const parentStatus = diffMap.get(parentPath);
|
|
153
|
+
if (
|
|
154
|
+
parentStatus !== "modified" &&
|
|
155
|
+
parentStatus !== "added" &&
|
|
156
|
+
parentStatus !== "removed"
|
|
157
|
+
) {
|
|
158
|
+
diffMap.set(parentPath, "modified");
|
|
159
|
+
changed = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return changed;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Propagate upwards until no more changes
|
|
168
|
+
while (propagateModified()) {}
|
|
169
|
+
|
|
170
|
+
return { byPath: diffMap, allPaths };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Clear diff highlighting from all elements in a canvas.
|
|
175
|
+
*
|
|
176
|
+
* @param {HTMLElement} canvas
|
|
177
|
+
*/
|
|
178
|
+
export function clearDiffHighlight(canvas) {
|
|
179
|
+
canvas
|
|
180
|
+
.querySelectorAll(".element-diff-added, .element-diff-removed, .element-diff-modified")
|
|
181
|
+
.forEach((/** @type {Element} */ el) => {
|
|
182
|
+
el.classList.remove("element-diff-added", "element-diff-removed", "element-diff-modified");
|
|
183
|
+
});
|
|
184
|
+
}
|
|
@@ -3,14 +3,8 @@
|
|
|
3
3
|
* multiple canvas-related modules: element lookup, zoom, panel resolution, inline bubbling.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
canvasPanels,
|
|
9
|
-
elToPath,
|
|
10
|
-
pathsEqual,
|
|
11
|
-
getNodeAtPath,
|
|
12
|
-
parentElementPath,
|
|
13
|
-
} from "../store.js";
|
|
6
|
+
import { canvasPanels, elToPath, pathsEqual, getNodeAtPath, parentElementPath } from "../store.js";
|
|
7
|
+
import { activeTab } from "../workspace/workspace.js";
|
|
14
8
|
import { isInlineInContext } from "../editor/inline-edit.js";
|
|
15
9
|
|
|
16
10
|
/** @type {any} */
|
|
@@ -19,25 +13,27 @@ let _ctx = null;
|
|
|
19
13
|
/**
|
|
20
14
|
* Initialize the canvas helpers module.
|
|
21
15
|
*
|
|
22
|
-
* @param {{ getCanvasMode: () => string }} ctx
|
|
16
|
+
* @param {{ getCanvasMode: () => string; getZoom: () => number }} ctx
|
|
23
17
|
*/
|
|
24
18
|
export function initCanvasHelpers(ctx) {
|
|
25
19
|
_ctx = ctx;
|
|
26
20
|
}
|
|
27
21
|
|
|
28
|
-
/** Effective zoom scale — always 1 in edit (content) mode,
|
|
22
|
+
/** Effective zoom scale — always 1 in edit (content) mode, actual zoom otherwise. */
|
|
29
23
|
export function effectiveZoom() {
|
|
30
|
-
return _ctx.getCanvasMode() === "edit"
|
|
24
|
+
return _ctx.getCanvasMode() === "edit"
|
|
25
|
+
? 1
|
|
26
|
+
: (_ctx.getZoom?.() ?? activeTab.value?.session.ui.zoom ?? 1);
|
|
31
27
|
}
|
|
32
28
|
|
|
33
29
|
/** Return the active canvas panel based on the current activeMedia setting. */
|
|
34
30
|
export function getActivePanel() {
|
|
35
31
|
if (canvasPanels.length === 0) return null;
|
|
36
32
|
if (canvasPanels.length === 1) return canvasPanels[0];
|
|
37
|
-
const
|
|
33
|
+
const activeMedia = activeTab.value?.session.ui.activeMedia ?? null;
|
|
38
34
|
for (const p of canvasPanels) {
|
|
39
|
-
if (
|
|
40
|
-
if (p.mediaName ===
|
|
35
|
+
if (activeMedia === null && (p.mediaName === "base" || p.mediaName === null)) return p;
|
|
36
|
+
if (p.mediaName === activeMedia) return p;
|
|
41
37
|
}
|
|
42
38
|
return canvasPanels[0];
|
|
43
39
|
}
|
|
@@ -5,13 +5,22 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { elToPath, stripEventHandlers, projectState } from "../store.js";
|
|
8
|
+
import { activeTab } from "../workspace/workspace.js";
|
|
8
9
|
import { view } from "../view.js";
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
renderNode as runtimeRenderNode,
|
|
12
|
+
buildScope,
|
|
13
|
+
defineElement,
|
|
14
|
+
setSkipServerFunctions,
|
|
15
|
+
} from "@jxsuite/runtime";
|
|
10
16
|
import {
|
|
11
17
|
getEffectiveElements,
|
|
12
18
|
getEffectiveImports,
|
|
13
19
|
getEffectiveMedia,
|
|
14
20
|
getEffectiveHead,
|
|
21
|
+
getEffectiveLayoutPath,
|
|
22
|
+
resolveLayoutDoc,
|
|
23
|
+
distributePageIntoLayout,
|
|
15
24
|
} from "../site-context.js";
|
|
16
25
|
import { componentRegistry, computeRelativePath } from "../files/components.js";
|
|
17
26
|
import { prepareForEditMode } from "../utils/edit-display.js";
|
|
@@ -20,11 +29,60 @@ import { getActiveElement } from "../editor/inline-edit.js";
|
|
|
20
29
|
/** @type {any} */
|
|
21
30
|
let _ctx = null;
|
|
22
31
|
|
|
32
|
+
/** Set of DOM elements that originated from the layout (not page content). */
|
|
33
|
+
export const layoutElements = new WeakSet();
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Walk the merged document tree to find the path prefix where page children were distributed into
|
|
37
|
+
* the layout slot. Returns the path to the container whose children are the page content (first
|
|
38
|
+
* non-$__layout children array).
|
|
39
|
+
*
|
|
40
|
+
* @param {any} node
|
|
41
|
+
* @param {any[]} [path]
|
|
42
|
+
* @returns {any[] | null}
|
|
43
|
+
*/
|
|
44
|
+
function findPageContentPrefix(node, path = []) {
|
|
45
|
+
if (!node || typeof node !== "object") return null;
|
|
46
|
+
if (Array.isArray(node.children)) {
|
|
47
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
48
|
+
const child = node.children[i];
|
|
49
|
+
if (child && typeof child === "object" && !child.$__layout) {
|
|
50
|
+
return [...path, "children"];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
54
|
+
const child = node.children[i];
|
|
55
|
+
if (child && typeof child === "object" && child.$__layout) {
|
|
56
|
+
const found = findPageContentPrefix(child, [...path, "children", i]);
|
|
57
|
+
if (found) return found;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The path of the currently active layout file, or null. */
|
|
65
|
+
export let activeLayoutPath = /** @type {string | null} */ (null);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Recursively mark all nodes in a layout doc tree with $__layout: true so we can identify which
|
|
69
|
+
* rendered DOM elements came from the layout vs page content.
|
|
70
|
+
*/
|
|
71
|
+
function markLayoutNodes(/** @type {any} */ node) {
|
|
72
|
+
if (!node || typeof node !== "object") return;
|
|
73
|
+
node.$__layout = true;
|
|
74
|
+
if (Array.isArray(node.children)) {
|
|
75
|
+
for (const child of node.children) markLayoutNodes(child);
|
|
76
|
+
}
|
|
77
|
+
if (node.$elements) {
|
|
78
|
+
for (const el of node.$elements) markLayoutNodes(el);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
23
82
|
/**
|
|
24
83
|
* Initialize the canvas live render module.
|
|
25
84
|
*
|
|
26
85
|
* @param {{
|
|
27
|
-
* getState: () => any;
|
|
28
86
|
* getCanvasMode: () => string;
|
|
29
87
|
* }} ctx
|
|
30
88
|
*/
|
|
@@ -42,12 +100,50 @@ export function initCanvasLiveRender(ctx) {
|
|
|
42
100
|
* @param {any} canvasEl
|
|
43
101
|
*/
|
|
44
102
|
export async function renderCanvasLive(gen, doc, canvasEl) {
|
|
45
|
-
const
|
|
103
|
+
const tab = activeTab.value;
|
|
104
|
+
const S = { documentPath: tab?.documentPath, mode: tab?.doc.mode, document: tab?.doc.document };
|
|
46
105
|
const canvasMode = _ctx.getCanvasMode();
|
|
47
106
|
|
|
48
|
-
|
|
107
|
+
// Suppress server function resolution in non-preview modes to avoid
|
|
108
|
+
// failed proxy calls and infinite reactive retries (also covers
|
|
109
|
+
// async custom element connectedCallbacks that run after this function returns)
|
|
110
|
+
setSkipServerFunctions(canvasMode !== "preview");
|
|
111
|
+
|
|
112
|
+
let renderDoc =
|
|
49
113
|
canvasMode === "preview" ? structuredClone(doc) : prepareForEditMode(stripEventHandlers(doc));
|
|
50
114
|
|
|
115
|
+
// ─── Layout wrapping ────────────────────────────────────────────────────
|
|
116
|
+
// For page documents, resolve the layout and wrap content in the layout shell.
|
|
117
|
+
// Layout-originated nodes are marked with $__layout so we can distinguish them.
|
|
118
|
+
let layoutWrapped = false;
|
|
119
|
+
activeLayoutPath = null;
|
|
120
|
+
|
|
121
|
+
const isPage =
|
|
122
|
+
S.documentPath &&
|
|
123
|
+
projectState?.isSiteProject &&
|
|
124
|
+
(S.documentPath.startsWith("pages/") || S.documentPath.startsWith("./pages/"));
|
|
125
|
+
|
|
126
|
+
/** @type {any[] | null} Path prefix in merged doc where page children live */
|
|
127
|
+
let pageContentPrefix = null;
|
|
128
|
+
|
|
129
|
+
if (isPage) {
|
|
130
|
+
const layoutPath = getEffectiveLayoutPath(doc.$layout);
|
|
131
|
+
if (layoutPath) {
|
|
132
|
+
const layoutDoc = await resolveLayoutDoc(layoutPath);
|
|
133
|
+
if (layoutDoc) {
|
|
134
|
+
if (gen !== view.renderGeneration) return null;
|
|
135
|
+
activeLayoutPath = layoutPath.replace(/^\.\//, "");
|
|
136
|
+
markLayoutNodes(layoutDoc);
|
|
137
|
+
const pageForSlots = canvasMode === "preview" ? structuredClone(doc) : renderDoc;
|
|
138
|
+
const merged = distributePageIntoLayout(layoutDoc, pageForSlots);
|
|
139
|
+
renderDoc =
|
|
140
|
+
canvasMode === "preview" ? merged : prepareForEditMode(stripEventHandlers(merged));
|
|
141
|
+
layoutWrapped = true;
|
|
142
|
+
pageContentPrefix = findPageContentPrefix(merged);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
51
147
|
// In edit mode, collect paths where $map templates were inlined as children[0]
|
|
52
148
|
// so we can remap runtime paths (children,0,...) → (children,map,...)
|
|
53
149
|
const mapParentPaths = new Set();
|
|
@@ -224,26 +320,49 @@ export async function renderCanvasLive(gen, doc, canvasEl) {
|
|
|
224
320
|
if (gen !== view.renderGeneration) return null;
|
|
225
321
|
const el = /** @type {HTMLElement} */ (
|
|
226
322
|
runtimeRenderNode(renderDoc, $defs, {
|
|
227
|
-
onNodeCreated(/** @type {any} */ el, /** @type {any} */ path) {
|
|
323
|
+
onNodeCreated(/** @type {any} */ el, /** @type {any} */ path, /** @type {any} */ def) {
|
|
324
|
+
// Track layout-originated elements — don't store in elToPath to avoid
|
|
325
|
+
// path collisions with remapped page content paths
|
|
326
|
+
if (layoutWrapped && def?.$__layout) {
|
|
327
|
+
layoutElements.add(el);
|
|
328
|
+
if (el.setAttribute) el.setAttribute("data-jx-layout", "");
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Remap layout-wrapped paths: strip the layout prefix so paths are
|
|
333
|
+
// relative to the original page document (which is what S.document holds)
|
|
334
|
+
let mappedPath = path;
|
|
335
|
+
if (layoutWrapped && pageContentPrefix) {
|
|
336
|
+
const pfx = pageContentPrefix;
|
|
337
|
+
if (
|
|
338
|
+
path.length >= pfx.length &&
|
|
339
|
+
pfx.every((/** @type {any} */ seg, /** @type {number} */ i) => path[i] === seg)
|
|
340
|
+
) {
|
|
341
|
+
mappedPath = ["children", ...path.slice(pfx.length)];
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
228
345
|
// Remap $map paths: wrapper and template children → real document paths
|
|
229
346
|
// prepareForEditMode wraps $map template in: children[0] (wrapper) > children[0] (template)
|
|
230
347
|
// Real paths: wrapper → ['children'] ($map container), template → ['children', 'map']
|
|
231
|
-
let mappedPath = path;
|
|
232
348
|
if ((canvasMode === "design" || canvasMode === "edit") && mapParentPaths.size > 0) {
|
|
233
|
-
for (let i = 0; i <
|
|
234
|
-
if (
|
|
235
|
-
const parentKey =
|
|
349
|
+
for (let i = 0; i < mappedPath.length - 1; i++) {
|
|
350
|
+
if (mappedPath[i] === "children" && mappedPath[i + 1] === 0) {
|
|
351
|
+
const parentKey = mappedPath.slice(0, i).join("/");
|
|
236
352
|
if (mapParentPaths.has(parentKey)) {
|
|
237
|
-
if (
|
|
238
|
-
|
|
239
|
-
mappedPath = path.slice(0, i + 1);
|
|
353
|
+
if (mappedPath.length === i + 2) {
|
|
354
|
+
mappedPath = mappedPath.slice(0, i + 1);
|
|
240
355
|
} else if (
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
356
|
+
mappedPath.length >= i + 4 &&
|
|
357
|
+
mappedPath[i + 2] === "children" &&
|
|
358
|
+
mappedPath[i + 3] === 0
|
|
244
359
|
) {
|
|
245
|
-
|
|
246
|
-
|
|
360
|
+
mappedPath = [
|
|
361
|
+
...mappedPath.slice(0, i),
|
|
362
|
+
"children",
|
|
363
|
+
"map",
|
|
364
|
+
...mappedPath.slice(i + 4),
|
|
365
|
+
];
|
|
247
366
|
}
|
|
248
367
|
break;
|
|
249
368
|
}
|