@jxsuite/studio 0.28.5 → 0.30.1
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.css +98 -98
- package/dist/studio.js +9368 -6586
- package/dist/studio.js.map +109 -83
- package/dist/workers/editor.worker.js +79 -79
- package/dist/workers/json.worker.js +109 -109
- package/dist/workers/ts.worker.js +82 -82
- package/package.json +11 -10
- package/src/browse/browse-modal.ts +2 -2
- package/src/browse/browse.ts +20 -19
- package/src/canvas/canvas-diff.ts +3 -3
- package/src/canvas/canvas-helpers.ts +15 -2
- package/src/canvas/canvas-live-render.ts +168 -86
- package/src/canvas/canvas-patcher.ts +656 -0
- package/src/canvas/canvas-perf.ts +68 -0
- package/src/canvas/canvas-render.ts +135 -23
- package/src/canvas/canvas-subtree-render.ts +113 -0
- package/src/canvas/canvas-utils.ts +4 -0
- package/src/editor/component-inline-edit.ts +4 -35
- package/src/editor/context-menu.ts +96 -76
- package/src/editor/convert-to-component.ts +11 -2
- package/src/editor/convert-to-repeater.ts +4 -5
- package/src/editor/inline-edit.ts +9 -9
- package/src/editor/inline-format.ts +11 -11
- package/src/editor/merge-tags.ts +120 -0
- package/src/editor/shortcuts.ts +4 -4
- package/src/files/components.ts +37 -0
- package/src/files/file-ops.ts +28 -7
- package/src/files/files.ts +68 -24
- package/src/files/fs-events.ts +162 -0
- package/src/github/github-auth.ts +14 -2
- package/src/github/github-publish.ts +12 -2
- package/src/panels/activity-bar.ts +1 -1
- package/src/panels/ai-panel.ts +67 -39
- package/src/panels/block-action-bar.ts +72 -1
- package/src/panels/canvas-dnd.ts +55 -26
- package/src/panels/data-explorer.ts +5 -3
- package/src/panels/dnd.ts +14 -17
- package/src/panels/editors.ts +5 -23
- package/src/panels/elements-panel.ts +2 -12
- package/src/panels/events-panel.ts +1 -1
- package/src/panels/git-panel.ts +6 -6
- package/src/panels/head-panel.ts +1 -1
- package/src/panels/layers-panel.ts +177 -147
- package/src/panels/preview-render.ts +15 -0
- package/src/panels/properties-panel.ts +16 -27
- package/src/panels/quick-search.ts +2 -2
- package/src/panels/right-panel.ts +2 -6
- package/src/panels/shared.ts +3 -0
- package/src/panels/signals-panel.ts +43 -33
- package/src/panels/statusbar.ts +15 -6
- package/src/panels/style-panel.ts +3 -3
- package/src/panels/style-utils.ts +3 -3
- package/src/panels/stylebook-panel.ts +4 -4
- package/src/panels/tab-bar.ts +198 -0
- package/src/panels/tab-strip.ts +2 -2
- package/src/panels/toolbar.ts +6 -75
- package/src/platforms/devserver.ts +113 -35
- package/src/reactivity.ts +2 -0
- package/src/services/cem-export.ts +23 -2
- package/src/services/code-services.ts +16 -2
- package/src/services/monaco-setup.ts +2 -1
- package/src/settings/content-types-editor.ts +16 -16
- package/src/settings/css-vars-editor.ts +2 -2
- package/src/settings/defs-editor.ts +14 -14
- package/src/settings/general-settings.ts +6 -6
- package/src/settings/head-editor.ts +2 -2
- package/src/site-context.ts +1 -1
- package/src/state.ts +66 -12
- package/src/store.ts +15 -3
- package/src/studio.ts +88 -35
- package/src/tabs/patch-ops.ts +143 -0
- package/src/tabs/tab.ts +15 -1
- package/src/tabs/transact.ts +454 -27
- package/src/types.ts +71 -1
- package/src/ui/color-selector.ts +7 -7
- package/src/ui/icons.ts +0 -30
- package/src/ui/media-picker.ts +2 -2
- package/src/ui/panel-resize.ts +14 -8
- package/src/ui/unit-selector.ts +5 -2
- package/src/ui/value-selector.ts +3 -3
- package/src/utils/canvas-media.ts +6 -6
- package/src/utils/edit-display.ts +125 -83
- package/src/utils/google-fonts.ts +1 -1
- package/src/utils/inherited-style.ts +3 -2
- package/src/utils/studio-utils.ts +1 -1
- package/src/view.ts +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.1",
|
|
4
4
|
"description": "Jx Studio — visual builder for Jx documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -23,18 +23,19 @@
|
|
|
23
23
|
"provenance": true
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
|
-
"build": "bun build ./src/studio.ts --outdir dist --target browser --sourcemap=linked && bun
|
|
26
|
+
"build": "bun build ./src/studio.ts --outdir dist --target browser --sourcemap=linked && bun run scripts/build-workers.ts",
|
|
27
|
+
"build:metafile": "bun build ./src/studio.ts --outdir dist --target browser --sourcemap=linked --metafile=metafile.json",
|
|
27
28
|
"gen:webdata": "bun run scripts/gen-webdata.js",
|
|
28
29
|
"test": "bun test --isolate",
|
|
29
30
|
"test:coverage": "bun test --isolate --coverage",
|
|
30
31
|
"upgrade": "bunx npm-check-updates -u && bun install"
|
|
31
32
|
},
|
|
32
33
|
"dependencies": {
|
|
33
|
-
"@atlaskit/pragmatic-drag-and-drop": "^
|
|
34
|
-
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^
|
|
35
|
-
"@jxsuite/parser": "^0.
|
|
36
|
-
"@jxsuite/runtime": "^0.
|
|
37
|
-
"@jxsuite/schema": "^0.
|
|
34
|
+
"@atlaskit/pragmatic-drag-and-drop": "^2.0.1",
|
|
35
|
+
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^2.0.0",
|
|
36
|
+
"@jxsuite/parser": "^0.30.1",
|
|
37
|
+
"@jxsuite/runtime": "^0.30.1",
|
|
38
|
+
"@jxsuite/schema": "^0.30.1",
|
|
38
39
|
"@spectrum-web-components/accordion": "^1.12.1",
|
|
39
40
|
"@spectrum-web-components/action-bar": "1.12.1",
|
|
40
41
|
"@spectrum-web-components/action-button": "^1.12.1",
|
|
@@ -78,11 +79,11 @@
|
|
|
78
79
|
"yaml": "^2.9.0"
|
|
79
80
|
},
|
|
80
81
|
"devDependencies": {
|
|
81
|
-
"@happy-dom/global-registrator": "^20.10.
|
|
82
|
+
"@happy-dom/global-registrator": "^20.10.6",
|
|
82
83
|
"@types/mdast": "^4.0.4",
|
|
83
|
-
"@webref/css": "^8.
|
|
84
|
+
"@webref/css": "^8.6.0",
|
|
84
85
|
"@webref/elements": "^2.7.1",
|
|
85
|
-
"@webref/idl": "^3.81.
|
|
86
|
+
"@webref/idl": "^3.81.1"
|
|
86
87
|
},
|
|
87
88
|
"//vue-reactivity": "Exact pin required — must match @jxsuite/runtime to avoid cross-boundary proxy bugs"
|
|
88
89
|
}
|
|
@@ -40,10 +40,10 @@ export function openBrowseModal() {
|
|
|
40
40
|
${ref((el) => {
|
|
41
41
|
if (el) {
|
|
42
42
|
requestAnimationFrame(() => {
|
|
43
|
-
renderBrowse(el as HTMLElement, {
|
|
43
|
+
void renderBrowse(el as HTMLElement, {
|
|
44
44
|
openFile: (path: string) => {
|
|
45
45
|
closeBrowseModal();
|
|
46
|
-
openFileInTab(path);
|
|
46
|
+
void openFileInTab(path);
|
|
47
47
|
},
|
|
48
48
|
});
|
|
49
49
|
});
|
package/src/browse/browse.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { repeat } from "lit-html/directives/repeat.js";
|
|
|
14
14
|
import { getPlatform } from "../platform";
|
|
15
15
|
import { projectState } from "../store";
|
|
16
16
|
import { yamlDefault } from "../settings/schema-field-ui";
|
|
17
|
+
import type { SchemaProperty } from "../settings/schema-field-ui";
|
|
17
18
|
import { invalidateMediaCache } from "../ui/media-picker";
|
|
18
19
|
import { statusMessage } from "../panels/statusbar";
|
|
19
20
|
import { componentRegistry } from "../files/components";
|
|
@@ -31,7 +32,7 @@ import {
|
|
|
31
32
|
} from "../format/format-host";
|
|
32
33
|
|
|
33
34
|
import type { ComponentEntry } from "../files/components";
|
|
34
|
-
import type { ContentTypeDef } from "@jxsuite/schema/types";
|
|
35
|
+
import type { ContentTypeDef, JxDocument } from "@jxsuite/schema/types";
|
|
35
36
|
|
|
36
37
|
// ─── Category definitions ────────────────────────────────────────────────────
|
|
37
38
|
|
|
@@ -267,9 +268,9 @@ function buildFrontmatterYaml(contentTypeName: string) {
|
|
|
267
268
|
}
|
|
268
269
|
|
|
269
270
|
let yaml = "";
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
yaml += `${field}: ${yamlDefault(
|
|
271
|
+
const props = col.schema.properties as Record<string, SchemaProperty>;
|
|
272
|
+
for (const [field, def] of Object.entries(props)) {
|
|
273
|
+
yaml += `${field}: ${yamlDefault(def.type || "", def.format || "")}\n`;
|
|
273
274
|
}
|
|
274
275
|
return yaml || "title: Untitled\n";
|
|
275
276
|
}
|
|
@@ -311,7 +312,7 @@ function getContentTypeTypes() {
|
|
|
311
312
|
*/
|
|
312
313
|
async function handleNewEntity(
|
|
313
314
|
typeKey: string,
|
|
314
|
-
|
|
315
|
+
_container: HTMLElement,
|
|
315
316
|
ctx: { openFile: (path: string) => void },
|
|
316
317
|
) {
|
|
317
318
|
const isContentType = typeKey.startsWith("contentType:");
|
|
@@ -384,7 +385,7 @@ async function handleUpload(
|
|
|
384
385
|
}
|
|
385
386
|
invalidateBrowseCache();
|
|
386
387
|
invalidateMediaCache();
|
|
387
|
-
renderBrowse(container, ctx);
|
|
388
|
+
void renderBrowse(container, ctx);
|
|
388
389
|
}
|
|
389
390
|
|
|
390
391
|
// ─── Context menu ───────────────────────────────────────────────────────────
|
|
@@ -467,7 +468,7 @@ function showBrowseContextMenu(
|
|
|
467
468
|
style=${item.danger ? "color: var(--danger)" : ""}
|
|
468
469
|
@click=${() => {
|
|
469
470
|
dismissBrowseContextMenu();
|
|
470
|
-
item.action?.();
|
|
471
|
+
void item.action?.();
|
|
471
472
|
}}
|
|
472
473
|
>${item.label}</sp-menu-item
|
|
473
474
|
>`,
|
|
@@ -505,7 +506,7 @@ async function browseRenameFile(
|
|
|
505
506
|
const platform = getPlatform();
|
|
506
507
|
await platform.renameFile(file.path, newPath);
|
|
507
508
|
invalidateBrowseCache();
|
|
508
|
-
renderBrowse(container, ctx);
|
|
509
|
+
void renderBrowse(container, ctx);
|
|
509
510
|
statusMessage(`Renamed to ${newName}`);
|
|
510
511
|
} catch (error) {
|
|
511
512
|
statusMessage(`Error: ${errorMessage(error)}`);
|
|
@@ -533,7 +534,7 @@ async function browseDuplicateFile(
|
|
|
533
534
|
const content = await platform.readFile(file.path);
|
|
534
535
|
await platform.writeFile(copyPath, content);
|
|
535
536
|
invalidateBrowseCache();
|
|
536
|
-
renderBrowse(container, ctx);
|
|
537
|
+
void renderBrowse(container, ctx);
|
|
537
538
|
statusMessage(`Duplicated as ${copyName}`);
|
|
538
539
|
} catch (error) {
|
|
539
540
|
statusMessage(`Error: ${errorMessage(error)}`);
|
|
@@ -558,7 +559,7 @@ async function browseDeleteFile(
|
|
|
558
559
|
const platform = getPlatform();
|
|
559
560
|
await platform.deleteFile(file.path);
|
|
560
561
|
invalidateBrowseCache();
|
|
561
|
-
renderBrowse(container, ctx);
|
|
562
|
+
void renderBrowse(container, ctx);
|
|
562
563
|
statusMessage(`Deleted ${file.name}`);
|
|
563
564
|
} catch (error) {
|
|
564
565
|
statusMessage(`Error: ${errorMessage(error)}`);
|
|
@@ -669,9 +670,9 @@ async function renderDocPreview(filePath: string) {
|
|
|
669
670
|
let doc;
|
|
670
671
|
if (formatForPath(filePath)) {
|
|
671
672
|
const result = await parseSourceForPath(filePath, content);
|
|
672
|
-
doc = result.document;
|
|
673
|
+
doc = result.document as JxDocument;
|
|
673
674
|
} else {
|
|
674
|
-
doc = JSON.parse(content);
|
|
675
|
+
doc = JSON.parse(content) as JxDocument;
|
|
675
676
|
}
|
|
676
677
|
const scope = await buildScope(doc, {}, location.href);
|
|
677
678
|
const el = renderNode(doc, scope);
|
|
@@ -748,7 +749,7 @@ function renderCard(
|
|
|
748
749
|
${needsPreview
|
|
749
750
|
? ref((el: Element | undefined) => {
|
|
750
751
|
if (el) {
|
|
751
|
-
loadPreview(el, file);
|
|
752
|
+
void loadPreview(el, file);
|
|
752
753
|
}
|
|
753
754
|
})
|
|
754
755
|
: nothing}
|
|
@@ -798,7 +799,7 @@ export async function renderBrowse(
|
|
|
798
799
|
?selected=${activeCategory === cat.key}
|
|
799
800
|
@click=${() => {
|
|
800
801
|
activeCategory = cat.key;
|
|
801
|
-
renderBrowse(container, ctx);
|
|
802
|
+
void renderBrowse(container, ctx);
|
|
802
803
|
}}
|
|
803
804
|
>
|
|
804
805
|
${cat.label}
|
|
@@ -812,7 +813,7 @@ export async function renderBrowse(
|
|
|
812
813
|
.value=${searchQuery}
|
|
813
814
|
@input=${(e: Event) => {
|
|
814
815
|
searchQuery = (e.target as HTMLInputElement).value;
|
|
815
|
-
renderBrowse(container, ctx);
|
|
816
|
+
void renderBrowse(container, ctx);
|
|
816
817
|
}}
|
|
817
818
|
@submit=${(e: Event) => e.preventDefault()}
|
|
818
819
|
></sp-search>
|
|
@@ -856,7 +857,7 @@ export async function renderBrowse(
|
|
|
856
857
|
@change=${(e: Event) => {
|
|
857
858
|
const input = e.target as HTMLInputElement;
|
|
858
859
|
if (input.files?.length) {
|
|
859
|
-
handleUpload(input.files, container, ctx);
|
|
860
|
+
void handleUpload(input.files, container, ctx);
|
|
860
861
|
}
|
|
861
862
|
input.value = "";
|
|
862
863
|
}}
|
|
@@ -867,7 +868,7 @@ export async function renderBrowse(
|
|
|
867
868
|
?selected=${viewMode === "grid"}
|
|
868
869
|
@click=${() => {
|
|
869
870
|
viewMode = "grid";
|
|
870
|
-
renderBrowse(container, ctx);
|
|
871
|
+
void renderBrowse(container, ctx);
|
|
871
872
|
}}
|
|
872
873
|
title="Grid view"
|
|
873
874
|
>
|
|
@@ -878,7 +879,7 @@ export async function renderBrowse(
|
|
|
878
879
|
?selected=${viewMode === "table"}
|
|
879
880
|
@click=${() => {
|
|
880
881
|
viewMode = "table";
|
|
881
|
-
renderBrowse(container, ctx);
|
|
882
|
+
void renderBrowse(container, ctx);
|
|
882
883
|
}}
|
|
883
884
|
title="Table view"
|
|
884
885
|
>
|
|
@@ -956,7 +957,7 @@ export async function renderBrowse(
|
|
|
956
957
|
(e.currentTarget as HTMLElement).classList.remove("browse-drop-active");
|
|
957
958
|
const droppedFiles = e.dataTransfer?.files;
|
|
958
959
|
if (droppedFiles?.length) {
|
|
959
|
-
handleUpload(droppedFiles, container, ctx);
|
|
960
|
+
void handleUpload(droppedFiles, container, ctx);
|
|
960
961
|
}
|
|
961
962
|
}}
|
|
962
963
|
>
|
|
@@ -93,7 +93,7 @@ function markRecursive(
|
|
|
93
93
|
const children = elementChildren(node);
|
|
94
94
|
for (let i = 0; i < children.length; i++) {
|
|
95
95
|
const childPath = `${path}/children/${i}`;
|
|
96
|
-
markRecursive(children[i]
|
|
96
|
+
markRecursive(children[i]!, status, childPath, diffMap, allPaths);
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
@@ -151,7 +151,7 @@ export function computeDocumentDiff(
|
|
|
151
151
|
const children = elementChildren(currNode);
|
|
152
152
|
for (let i = 0; i < children.length; i++) {
|
|
153
153
|
const childPath = pathKey === "/" ? `children/${i}` : `${pathKey}/children/${i}`;
|
|
154
|
-
markRecursive(children[i]
|
|
154
|
+
markRecursive(children[i]!, "added", childPath, diffMap, allPaths);
|
|
155
155
|
}
|
|
156
156
|
return;
|
|
157
157
|
}
|
|
@@ -162,7 +162,7 @@ export function computeDocumentDiff(
|
|
|
162
162
|
const children = elementChildren(origNode);
|
|
163
163
|
for (let i = 0; i < children.length; i++) {
|
|
164
164
|
const childPath = pathKey === "/" ? `children/${i}` : `${pathKey}/children/${i}`;
|
|
165
|
-
markRecursive(children[i]
|
|
165
|
+
markRecursive(children[i]!, "removed", childPath, diffMap, allPaths);
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
};
|
|
@@ -107,14 +107,26 @@ export function findCanvasElement(path: JxPath, canvasEl: HTMLElement) {
|
|
|
107
107
|
return el;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
let i = 0;
|
|
111
|
+
while (i < path.length) {
|
|
112
|
+
const seg = path[i];
|
|
113
|
+
// A lone "map" segment steps into a repeater perimeter's single template child.
|
|
114
|
+
if (seg === "map") {
|
|
115
|
+
el = el.children[0] as HTMLElement | undefined;
|
|
116
|
+
i += 1;
|
|
117
|
+
if (!el) {
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (seg !== "children" && seg !== "cases") {
|
|
112
123
|
return null;
|
|
113
124
|
}
|
|
114
125
|
const idx = path[i + 1];
|
|
115
126
|
if (idx === undefined) {
|
|
116
127
|
el = el.children[0] as HTMLElement | undefined;
|
|
117
128
|
} else if (idx === "map") {
|
|
129
|
+
// Legacy whole-children template `[..., "children", "map"]`: perimeter at child[0].
|
|
118
130
|
el = el.children[0]?.children[0] as HTMLElement | undefined;
|
|
119
131
|
} else {
|
|
120
132
|
el = el.children[idx as number] as HTMLElement | undefined;
|
|
@@ -122,6 +134,7 @@ export function findCanvasElement(path: JxPath, canvasEl: HTMLElement) {
|
|
|
122
134
|
if (!el) {
|
|
123
135
|
break;
|
|
124
136
|
}
|
|
137
|
+
i += 2;
|
|
125
138
|
}
|
|
126
139
|
|
|
127
140
|
if (el) {
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
* mapping ($map remapping), site-level style injection, and $head element injection.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { elToPath, projectState, stripEventHandlers } from "../store";
|
|
9
|
+
import { elToPath, elToScope, projectState, stripEventHandlers } from "../store";
|
|
10
10
|
import { errorMessage } from "@jxsuite/schema/parse";
|
|
11
11
|
import { activeTab } from "../workspace/workspace";
|
|
12
12
|
import { view } from "../view";
|
|
13
|
-
import { toRaw } from "../reactivity";
|
|
13
|
+
import { effectScope, toRaw } from "../reactivity";
|
|
14
14
|
import {
|
|
15
15
|
buildScope,
|
|
16
16
|
defineElement,
|
|
@@ -33,6 +33,8 @@ import { buildNestedSiteCSS } from "./nested-site-style";
|
|
|
33
33
|
|
|
34
34
|
import type { JxDocument, JxElement, JxMutableNode } from "@jxsuite/schema/types";
|
|
35
35
|
import type { ComponentEntry } from "../files/components.js";
|
|
36
|
+
import type { CanvasPanel } from "../types";
|
|
37
|
+
import type { EffectScope } from "../reactivity";
|
|
36
38
|
|
|
37
39
|
export { buildNestedSiteCSS } from "./nested-site-style";
|
|
38
40
|
|
|
@@ -57,26 +59,31 @@ const _failedElements = new Set();
|
|
|
57
59
|
let _failedElementsDocPath: string | null = null;
|
|
58
60
|
|
|
59
61
|
/**
|
|
60
|
-
* Walk the merged document tree to find
|
|
61
|
-
*
|
|
62
|
-
* non-$__layout
|
|
62
|
+
* Walk the merged document tree to find where page children were distributed into the layout slot.
|
|
63
|
+
* Returns the `prefix` path to the container whose children hold the page content (first container
|
|
64
|
+
* with a non-$__layout child), plus the `offset` — the index of the first page-content child within
|
|
65
|
+
* that container's children array. The offset is non-zero when the layout places sibling nodes
|
|
66
|
+
* (e.g. a `<noscript>`) before the `<slot>`, which shifts every page child's render index relative
|
|
67
|
+
* to its page-document index. The path mapper subtracts this offset so canvas paths line up with
|
|
68
|
+
* the page document (and thus the layers panel).
|
|
63
69
|
*
|
|
64
70
|
* @param {JxMutableNode} node
|
|
65
71
|
* @param {(string | number)[]} [path]
|
|
66
|
-
* @returns {(string | number)[] | null}
|
|
72
|
+
* @returns {{ prefix: (string | number)[]; offset: number } | null}
|
|
67
73
|
*/
|
|
68
74
|
function findPageContentPrefix(
|
|
69
75
|
node: JxMutableNode,
|
|
70
76
|
path: (string | number)[] = [],
|
|
71
|
-
): (string | number)[] | null {
|
|
77
|
+
): { prefix: (string | number)[]; offset: number } | null {
|
|
72
78
|
if (!node || typeof node !== "object") {
|
|
73
79
|
return null;
|
|
74
80
|
}
|
|
75
81
|
if (Array.isArray(node.children)) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
const firstPageIdx = node.children.findIndex(
|
|
83
|
+
(child) => child && typeof child === "object" && !child.$__layout,
|
|
84
|
+
);
|
|
85
|
+
if (firstPageIdx !== -1) {
|
|
86
|
+
return { offset: firstPageIdx, prefix: [...path, "children"] };
|
|
80
87
|
}
|
|
81
88
|
for (let i = 0; i < node.children.length; i++) {
|
|
82
89
|
const child = node.children[i];
|
|
@@ -131,6 +138,92 @@ export function initCanvasLiveRender(ctx: { getCanvasMode: () => string }) {
|
|
|
131
138
|
_ctx = ctx;
|
|
132
139
|
}
|
|
133
140
|
|
|
141
|
+
/** Context needed to map runtime render paths back to document paths. */
|
|
142
|
+
export interface PathMapperCtx {
|
|
143
|
+
canvasMode: string;
|
|
144
|
+
layoutWrapped: boolean;
|
|
145
|
+
pageContentPrefix: (string | number)[] | null;
|
|
146
|
+
/** Index of the first page-content child within the slot container (leading layout siblings). */
|
|
147
|
+
pageContentOffset?: number;
|
|
148
|
+
/** Document paths of every mapped-array node, used to remap edit-mode repeater perimeters. */
|
|
149
|
+
arrayPaths: Set<string>;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Build the onNodeCreated callback that records each rendered element's document path in elToPath.
|
|
154
|
+
* Handles layout-prefix stripping and repeater-perimeter/template remapping. Shared by full panel
|
|
155
|
+
* renders and isolated subtree re-renders so path bookkeeping stays consistent.
|
|
156
|
+
*/
|
|
157
|
+
export function makePathMapper(ctx: PathMapperCtx) {
|
|
158
|
+
const { canvasMode, layoutWrapped, pageContentPrefix, pageContentOffset, arrayPaths } = ctx;
|
|
159
|
+
return function onNodeCreated(
|
|
160
|
+
created: Node,
|
|
161
|
+
path: (string | number)[],
|
|
162
|
+
def: unknown,
|
|
163
|
+
state?: Record<string, unknown>,
|
|
164
|
+
) {
|
|
165
|
+
if (!(created instanceof HTMLElement)) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (state) {
|
|
169
|
+
elToScope.set(created, state);
|
|
170
|
+
}
|
|
171
|
+
// Track layout-originated elements — don't store in elToPath to avoid
|
|
172
|
+
// Path collisions with remapped page content paths
|
|
173
|
+
if (layoutWrapped && typeof def === "object" && (def as JxMutableNode)?.$__layout) {
|
|
174
|
+
layoutElements.add(created);
|
|
175
|
+
created.dataset.jxLayout = "";
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Remap layout-wrapped paths: strip the layout prefix so paths are
|
|
180
|
+
// Relative to the original page document (which is what S.document holds)
|
|
181
|
+
let mappedPath = path;
|
|
182
|
+
if (layoutWrapped && pageContentPrefix) {
|
|
183
|
+
const pfx = pageContentPrefix;
|
|
184
|
+
if (
|
|
185
|
+
path.length >= pfx.length &&
|
|
186
|
+
pfx.every((seg: string | number, i: number) => path[i] === seg)
|
|
187
|
+
) {
|
|
188
|
+
// Page children render at container indices [offset, offset+1, …] when the layout places
|
|
189
|
+
// Sibling nodes before the <slot>. Subtract the offset so they map back to the page
|
|
190
|
+
// Document's 0-based child indices (what flattenTree / the layers panel use).
|
|
191
|
+
const rest = path.slice(pfx.length);
|
|
192
|
+
const [containerIdx] = rest;
|
|
193
|
+
mappedPath =
|
|
194
|
+
typeof containerIdx === "number"
|
|
195
|
+
? ["children", containerIdx - (pageContentOffset ?? 0), ...rest.slice(1)]
|
|
196
|
+
: ["children", ...rest];
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Remap repeater perimeters: prepareForEditMode renders each mapped-array node as a
|
|
201
|
+
// `<div class="repeater-perimeter">` at the array's own child index, with the map template as
|
|
202
|
+
// Its single child[0]. The perimeter's render path already equals the array's document path,
|
|
203
|
+
// So only the template hop needs collapsing: for any array document path P, a render path of
|
|
204
|
+
// `[...P, "children", 0, ...rest]` maps to `[...P, "map", ...rest]`. Looping handles nested
|
|
205
|
+
// Repeaters (an array whose template contains another array).
|
|
206
|
+
if ((canvasMode === "design" || canvasMode === "edit") && arrayPaths.size > 0) {
|
|
207
|
+
let changed = true;
|
|
208
|
+
while (changed) {
|
|
209
|
+
changed = false;
|
|
210
|
+
for (let i = 1; i < mappedPath.length - 1; i++) {
|
|
211
|
+
if (
|
|
212
|
+
mappedPath[i] === "children" &&
|
|
213
|
+
mappedPath[i + 1] === 0 &&
|
|
214
|
+
arrayPaths.has(mappedPath.slice(0, i).join("/"))
|
|
215
|
+
) {
|
|
216
|
+
mappedPath = [...mappedPath.slice(0, i), "map", ...mappedPath.slice(i + 2)];
|
|
217
|
+
changed = true;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
elToPath.set(created, mappedPath);
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
134
227
|
/**
|
|
135
228
|
* Render a Jx document into a canvas element using the real runtime. Populates elToPath for each
|
|
136
229
|
* created element via onNodeCreated callback. Returns the live state scope on success, null on
|
|
@@ -139,8 +232,14 @@ export function initCanvasLiveRender(ctx: { getCanvasMode: () => string }) {
|
|
|
139
232
|
* @param {number} gen - Render generation for staleness detection
|
|
140
233
|
* @param {JxMutableNode} doc
|
|
141
234
|
* @param {HTMLElement} canvasEl
|
|
235
|
+
* @param {CanvasPanel | null} [panel] - Panel to persist render context on (for surgical patches)
|
|
142
236
|
*/
|
|
143
|
-
export async function renderCanvasLive(
|
|
237
|
+
export async function renderCanvasLive(
|
|
238
|
+
gen: number,
|
|
239
|
+
doc: JxMutableNode,
|
|
240
|
+
canvasEl: HTMLElement,
|
|
241
|
+
panel: CanvasPanel | null = null,
|
|
242
|
+
) {
|
|
144
243
|
const tab = activeTab.value;
|
|
145
244
|
const S = {
|
|
146
245
|
document: tab?.doc.document,
|
|
@@ -177,11 +276,13 @@ export async function renderCanvasLive(gen: number, doc: JxMutableNode, canvasEl
|
|
|
177
276
|
|
|
178
277
|
/** @type {(string | number)[] | null} Path prefix in merged doc where page children live */
|
|
179
278
|
let pageContentPrefix = null;
|
|
279
|
+
/** Index of the first page child within the slot container (offset by leading layout siblings). */
|
|
280
|
+
let pageContentOffset = 0;
|
|
180
281
|
|
|
181
282
|
if (isPage) {
|
|
182
283
|
const layoutPath = getEffectiveLayoutPath(doc.$layout);
|
|
183
284
|
if (layoutPath) {
|
|
184
|
-
const layoutDoc = await resolveLayoutDoc(layoutPath);
|
|
285
|
+
const layoutDoc = (await resolveLayoutDoc(layoutPath)) as JxMutableNode | null;
|
|
185
286
|
if (layoutDoc) {
|
|
186
287
|
if (gen !== view.renderGeneration) {
|
|
187
288
|
return null;
|
|
@@ -193,25 +294,30 @@ export async function renderCanvasLive(gen: number, doc: JxMutableNode, canvasEl
|
|
|
193
294
|
renderDoc =
|
|
194
295
|
canvasMode === "preview" ? merged : prepareForEditMode(stripEventHandlers(merged));
|
|
195
296
|
layoutWrapped = true;
|
|
196
|
-
|
|
297
|
+
const pageContent = findPageContentPrefix(merged);
|
|
298
|
+
pageContentPrefix = pageContent?.prefix ?? null;
|
|
299
|
+
pageContentOffset = pageContent?.offset ?? 0;
|
|
197
300
|
}
|
|
198
301
|
}
|
|
199
302
|
}
|
|
200
303
|
|
|
201
|
-
// In edit mode, collect
|
|
202
|
-
//
|
|
203
|
-
const
|
|
304
|
+
// In edit mode, collect the document path of every mapped-array node so the path mapper can
|
|
305
|
+
// Remap their edit-mode perimeters (and templates) back to document paths.
|
|
306
|
+
const arrayPaths = new Set<string>();
|
|
307
|
+
let renderScope: EffectScope | null = null;
|
|
204
308
|
if (canvasMode === "design" || canvasMode === "edit") {
|
|
205
|
-
(function
|
|
309
|
+
(function findArrayPaths(node: JxMutableNode, path: (string | number)[]) {
|
|
206
310
|
if (!node || typeof node !== "object") {
|
|
207
311
|
return;
|
|
208
312
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
(node
|
|
213
|
-
|
|
214
|
-
|
|
313
|
+
// The node itself is a mapped array (reached as a member or whole-children slot).
|
|
314
|
+
if ((node as unknown as { $prototype?: string }).$prototype === "Array") {
|
|
315
|
+
arrayPaths.add(path.join("/"));
|
|
316
|
+
const mapDef = (node as JxMutableNode).map;
|
|
317
|
+
if (mapDef && typeof mapDef === "object") {
|
|
318
|
+
findArrayPaths(mapDef, [...path, "map"]);
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
215
321
|
}
|
|
216
322
|
if (Array.isArray(node.children)) {
|
|
217
323
|
for (let i = 0; i < node.children.length; i++) {
|
|
@@ -219,12 +325,19 @@ export async function renderCanvasLive(gen: number, doc: JxMutableNode, canvasEl
|
|
|
219
325
|
if (typeof child === "string") {
|
|
220
326
|
continue;
|
|
221
327
|
}
|
|
222
|
-
|
|
328
|
+
findArrayPaths(child!, [...path, "children", i]);
|
|
223
329
|
}
|
|
330
|
+
} else if (
|
|
331
|
+
node.children &&
|
|
332
|
+
typeof node.children === "object" &&
|
|
333
|
+
(node.children as unknown as { $prototype?: string }).$prototype === "Array"
|
|
334
|
+
) {
|
|
335
|
+
// Legacy whole-children repeater.
|
|
336
|
+
findArrayPaths(node.children as JxMutableNode, [...path, "children"]);
|
|
224
337
|
}
|
|
225
338
|
if (node.$switch && node.cases) {
|
|
226
339
|
for (const [k, v] of Object.entries(node.cases)) {
|
|
227
|
-
|
|
340
|
+
findArrayPaths(v, [...path, "cases", k]);
|
|
228
341
|
}
|
|
229
342
|
}
|
|
230
343
|
})(doc, []);
|
|
@@ -433,67 +546,22 @@ export async function renderCanvasLive(gen: number, doc: JxMutableNode, canvasEl
|
|
|
433
546
|
if (gen !== view.renderGeneration) {
|
|
434
547
|
return null;
|
|
435
548
|
}
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
) {
|
|
443
|
-
if (!(created instanceof HTMLElement)) {
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
// Track layout-originated elements — don't store in elToPath to avoid
|
|
447
|
-
// Path collisions with remapped page content paths
|
|
448
|
-
if (layoutWrapped && typeof def === "object" && def?.$__layout) {
|
|
449
|
-
layoutElements.add(created);
|
|
450
|
-
created.dataset.jxLayout = "";
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// Remap layout-wrapped paths: strip the layout prefix so paths are
|
|
455
|
-
// Relative to the original page document (which is what S.document holds)
|
|
456
|
-
let mappedPath = path;
|
|
457
|
-
if (layoutWrapped && pageContentPrefix) {
|
|
458
|
-
const pfx = pageContentPrefix;
|
|
459
|
-
if (
|
|
460
|
-
path.length >= pfx.length &&
|
|
461
|
-
pfx.every((seg: string | number, i: number) => path[i] === seg)
|
|
462
|
-
) {
|
|
463
|
-
mappedPath = ["children", ...path.slice(pfx.length)];
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Remap $map paths: wrapper and template children → real document paths
|
|
468
|
-
// PrepareForEditMode wraps $map template in: children[0] (wrapper) > children[0] (template)
|
|
469
|
-
// Real paths: wrapper → ['children'] ($map container), template → ['children', 'map']
|
|
470
|
-
if ((canvasMode === "design" || canvasMode === "edit") && mapParentPaths.size > 0) {
|
|
471
|
-
for (let i = 0; i < mappedPath.length - 1; i++) {
|
|
472
|
-
if (mappedPath[i] === "children" && mappedPath[i + 1] === 0) {
|
|
473
|
-
const parentKey = mappedPath.slice(0, i).join("/");
|
|
474
|
-
if (mapParentPaths.has(parentKey)) {
|
|
475
|
-
if (mappedPath.length === i + 2) {
|
|
476
|
-
mappedPath = mappedPath.slice(0, i + 1);
|
|
477
|
-
} else if (
|
|
478
|
-
mappedPath.length >= i + 4 &&
|
|
479
|
-
mappedPath[i + 2] === "children" &&
|
|
480
|
-
mappedPath[i + 3] === 0
|
|
481
|
-
) {
|
|
482
|
-
mappedPath = [
|
|
483
|
-
...mappedPath.slice(0, i),
|
|
484
|
-
"children",
|
|
485
|
-
"map",
|
|
486
|
-
...mappedPath.slice(i + 4),
|
|
487
|
-
];
|
|
488
|
-
}
|
|
489
|
-
break;
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
elToPath.set(created, mappedPath);
|
|
495
|
-
},
|
|
549
|
+
const pathMapper = makePathMapper({
|
|
550
|
+
arrayPaths,
|
|
551
|
+
canvasMode,
|
|
552
|
+
layoutWrapped,
|
|
553
|
+
pageContentOffset,
|
|
554
|
+
pageContentPrefix,
|
|
496
555
|
});
|
|
556
|
+
// Render inside a detached effect scope so the tree's reactive effects (template bindings,
|
|
557
|
+
// $map/$switch containers) can be disposed when the panel is rebuilt, instead of leaking.
|
|
558
|
+
renderScope = effectScope(true);
|
|
559
|
+
const el = /** @type {HTMLElement} */ renderScope.run(() =>
|
|
560
|
+
runtimeRenderNode(renderDoc, $defs, {
|
|
561
|
+
_path: [],
|
|
562
|
+
onNodeCreated: pathMapper,
|
|
563
|
+
}),
|
|
564
|
+
)!;
|
|
497
565
|
if ((canvasMode === "design" || canvasMode === "edit") && el instanceof HTMLElement) {
|
|
498
566
|
// Disable pointer events on all rendered elements for edit mode
|
|
499
567
|
el.style.pointerEvents = "none";
|
|
@@ -533,8 +601,22 @@ export async function renderCanvasLive(gen: number, doc: JxMutableNode, canvasEl
|
|
|
533
601
|
}
|
|
534
602
|
});
|
|
535
603
|
}
|
|
604
|
+
if (panel) {
|
|
605
|
+
panel.renderScope?.stop();
|
|
606
|
+
panel.renderScope = renderScope;
|
|
607
|
+
panel.liveCtx = {
|
|
608
|
+
arrayPaths,
|
|
609
|
+
canvasMode,
|
|
610
|
+
layoutWrapped,
|
|
611
|
+
pageContentOffset,
|
|
612
|
+
pageContentPrefix,
|
|
613
|
+
pathMapper,
|
|
614
|
+
scope: $defs as Record<string, unknown>,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
536
617
|
return $defs;
|
|
537
618
|
} catch (error) {
|
|
619
|
+
renderScope?.stop();
|
|
538
620
|
console.warn("renderCanvasLive failed:", errorMessage(error), error);
|
|
539
621
|
return null;
|
|
540
622
|
}
|