@jxsuite/studio 0.28.4 → 0.29.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 +6769 -5399
- package/dist/studio.js.map +86 -79
- package/package.json +5 -4
- 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 +92 -74
- 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 +25 -21
- 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 +12 -15
- 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 +24 -22
- 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 +60 -34
- 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 +73 -33
- 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 +41 -0
- 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 +6 -1
- package/src/ui/unit-selector.ts +2 -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
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas render instrumentation. Counts full renders vs surgical patches so tests and users can
|
|
3
|
+
* verify that hot-path edits do not rebuild the canvas.
|
|
4
|
+
*
|
|
5
|
+
* Inspect in the browser via `globalThis.__jxCanvasPerf`; enable per-event logging with
|
|
6
|
+
* `localStorage["jx-canvas-debug"] = "1"`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface CanvasPerf {
|
|
10
|
+
/** Full renderCanvas() invocations (whole-canvas orchestration). */
|
|
11
|
+
fullRenders: number;
|
|
12
|
+
/** Individual panel renders (one per breakpoint panel per full render). */
|
|
13
|
+
panelRenders: number;
|
|
14
|
+
/** Doc-effect triggers skipped because the change was consumed by the patcher. */
|
|
15
|
+
skippedFullRenders: number;
|
|
16
|
+
/** Patch ops applied surgically to the live canvas DOM. */
|
|
17
|
+
patchedOps: number;
|
|
18
|
+
/** Isolated subtree re-renders (structural patches). */
|
|
19
|
+
subtreeRenders: number;
|
|
20
|
+
/** Patch batches that escalated to a full render. */
|
|
21
|
+
escalations: number;
|
|
22
|
+
lastEscalationReason: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const canvasPerf: CanvasPerf = {
|
|
26
|
+
fullRenders: 0,
|
|
27
|
+
panelRenders: 0,
|
|
28
|
+
skippedFullRenders: 0,
|
|
29
|
+
patchedOps: 0,
|
|
30
|
+
subtreeRenders: 0,
|
|
31
|
+
escalations: 0,
|
|
32
|
+
lastEscalationReason: "",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
(globalThis as Record<string, unknown>).__jxCanvasPerf = canvasPerf;
|
|
36
|
+
|
|
37
|
+
function debugEnabled() {
|
|
38
|
+
try {
|
|
39
|
+
return typeof localStorage !== "undefined" && Boolean(localStorage.getItem("jx-canvas-debug"));
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Log a canvas perf event when debug mode is on. */
|
|
46
|
+
export function perfLog(event: string, detail?: unknown) {
|
|
47
|
+
if (debugEnabled()) {
|
|
48
|
+
console.debug(`[jx-canvas] ${event}`, detail ?? "");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Record an escalation to full render with its reason. */
|
|
53
|
+
export function recordEscalation(reason: string) {
|
|
54
|
+
canvasPerf.escalations += 1;
|
|
55
|
+
canvasPerf.lastEscalationReason = reason;
|
|
56
|
+
perfLog("escalation", reason);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Reset all counters (for tests). */
|
|
60
|
+
export function resetCanvasPerf() {
|
|
61
|
+
canvasPerf.fullRenders = 0;
|
|
62
|
+
canvasPerf.panelRenders = 0;
|
|
63
|
+
canvasPerf.skippedFullRenders = 0;
|
|
64
|
+
canvasPerf.patchedOps = 0;
|
|
65
|
+
canvasPerf.subtreeRenders = 0;
|
|
66
|
+
canvasPerf.escalations = 0;
|
|
67
|
+
canvasPerf.lastEscalationReason = "";
|
|
68
|
+
}
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
} from "../utils/canvas-media";
|
|
34
34
|
import { getEffectiveMedia } from "../site-context";
|
|
35
35
|
import { renderCanvasLive } from "./canvas-live-render";
|
|
36
|
+
import { canvasPerf } from "./canvas-perf";
|
|
36
37
|
import { renderCanvasNode } from "../panels/preview-render";
|
|
37
38
|
import { registerPanelDnD } from "../panels/canvas-dnd";
|
|
38
39
|
import { registerPanelEvents } from "../panels/panel-events";
|
|
@@ -57,8 +58,6 @@ interface CanvasRenderCtx {
|
|
|
57
58
|
getCanvasMode: () => string;
|
|
58
59
|
setCanvasMode: (mode: string) => void;
|
|
59
60
|
openFileFromTree: (path: string) => void;
|
|
60
|
-
exportFile: () => void;
|
|
61
|
-
closeFunctionEditor: () => void;
|
|
62
61
|
gitDiffState: GitDiffState | null;
|
|
63
62
|
setGitDiffState: (state: GitDiffState | null) => void;
|
|
64
63
|
}
|
|
@@ -100,13 +99,99 @@ async function sourceContent(tab: Tab, lang: string) {
|
|
|
100
99
|
return JSON.stringify(tab.doc.document, null, 2);
|
|
101
100
|
}
|
|
102
101
|
|
|
102
|
+
// Double-RAF scheduling so the canvas render yields to higher-priority panel paints first.
|
|
103
|
+
// Concurrent schedule requests within the same frame are deduped.
|
|
104
|
+
let _canvasRafId = 0;
|
|
105
|
+
export function scheduleCanvasRender() {
|
|
106
|
+
if (_canvasRafId) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
_canvasRafId = requestAnimationFrame(() => {
|
|
110
|
+
_canvasRafId = requestAnimationFrame(() => {
|
|
111
|
+
_canvasRafId = 0;
|
|
112
|
+
try {
|
|
113
|
+
renderCanvas();
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error("renderCanvas error:", error);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Eject canvasWrap's DOM and Lit render part together. Setting textContent/innerHTML removes the
|
|
123
|
+
* comment nodes Lit uses as a ChildPart's markers; if the private `_$litPart$` reference is left
|
|
124
|
+
* behind, the next litRender() reuses a part whose markers are detached from the DOM and throws
|
|
125
|
+
* "This `ChildPart` has no `parentNode`…". Always eject the markers through this helper so the two
|
|
126
|
+
* operations can never drift apart.
|
|
127
|
+
*/
|
|
128
|
+
function hardClearCanvasWrap() {
|
|
129
|
+
canvasWrap.textContent = "";
|
|
130
|
+
// @ts-expect-error -- _$litPart$ is Lit's private render-part marker, not in the DOM types
|
|
131
|
+
delete canvasWrap["_$litPart$"];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Tear the canvas all the way back down to a pristine state. Invoked whenever there is no active
|
|
136
|
+
* tab (e.g. every tab was closed) so the canvas can never get wedged in a half-initialized state:
|
|
137
|
+
* open editors and observers are disposed, outgoing panel scopes stopped, pending cleanups run, the
|
|
138
|
+
* Lit render part is ejected cleanly, inline style overrides reset, and `prevCanvasMode` cleared so
|
|
139
|
+
* the very next render is treated as a fresh mode transition and rebuilds the surface from
|
|
140
|
+
* scratch.
|
|
141
|
+
*/
|
|
142
|
+
function resetCanvasView() {
|
|
143
|
+
if (view.functionEditor) {
|
|
144
|
+
view.functionEditor.dispose();
|
|
145
|
+
view.functionEditor = null;
|
|
146
|
+
}
|
|
147
|
+
if (view.monacoEditor) {
|
|
148
|
+
view.monacoEditor.getModel()?.dispose();
|
|
149
|
+
view.monacoEditor.dispose();
|
|
150
|
+
view.monacoEditor = null;
|
|
151
|
+
}
|
|
152
|
+
if (view.centerObserver) {
|
|
153
|
+
view.centerObserver.disconnect();
|
|
154
|
+
view.centerObserver = null;
|
|
155
|
+
}
|
|
156
|
+
for (const fn of view.canvasDndCleanups) {
|
|
157
|
+
fn();
|
|
158
|
+
}
|
|
159
|
+
view.canvasDndCleanups = [];
|
|
160
|
+
for (const fn of view.canvasEventCleanups) {
|
|
161
|
+
fn();
|
|
162
|
+
}
|
|
163
|
+
view.canvasEventCleanups = [];
|
|
164
|
+
for (const p of canvasPanels) {
|
|
165
|
+
p.renderScope?.stop();
|
|
166
|
+
p.renderScope = null;
|
|
167
|
+
}
|
|
168
|
+
canvasPanels.length = 0;
|
|
169
|
+
|
|
170
|
+
hardClearCanvasWrap();
|
|
171
|
+
|
|
172
|
+
view.panzoomWrap = null;
|
|
173
|
+
canvasWrap.style.padding = "";
|
|
174
|
+
canvasWrap.style.alignItems = "";
|
|
175
|
+
canvasWrap.style.flexDirection = "";
|
|
176
|
+
canvasWrap.style.display = "";
|
|
177
|
+
canvasWrap.style.overflow = "";
|
|
178
|
+
resetZoomIndicator();
|
|
179
|
+
dismissBlockActionBar();
|
|
180
|
+
dismissLinkPopover();
|
|
181
|
+
dismissContextMenu();
|
|
182
|
+
dismissSlashMenu();
|
|
183
|
+
view.prevCanvasMode = null;
|
|
184
|
+
}
|
|
185
|
+
|
|
103
186
|
export function renderCanvas() {
|
|
104
187
|
const tab = activeTab.value;
|
|
105
188
|
if (!tab) {
|
|
189
|
+
// No active tab — reset every piece of canvas view state so reopening a file can never inherit
|
|
190
|
+
// A stale Lit part, a dead Monaco editor, or a mismatched prevCanvasMode (the toxic states that
|
|
191
|
+
// Previously left the canvas unrenderable until a full reload).
|
|
192
|
+
resetCanvasView();
|
|
106
193
|
if (!projectState) {
|
|
107
194
|
renderWelcome(canvasWrap);
|
|
108
|
-
} else {
|
|
109
|
-
canvasWrap.textContent = "";
|
|
110
195
|
}
|
|
111
196
|
return;
|
|
112
197
|
}
|
|
@@ -120,6 +205,7 @@ export function renderCanvas() {
|
|
|
120
205
|
|
|
121
206
|
// Advance render generation so stale async renders from the previous cycle bail out
|
|
122
207
|
view.renderGeneration += 1;
|
|
208
|
+
canvasPerf.fullRenders += 1;
|
|
123
209
|
|
|
124
210
|
// Detect whether this is a mode transition or a content-only re-render
|
|
125
211
|
const modeChanged = canvasMode !== view.prevCanvasMode;
|
|
@@ -130,14 +216,12 @@ export function renderCanvas() {
|
|
|
130
216
|
// RenderCanvasLive uses atomic clear (innerHTML = "" right before appendChild).
|
|
131
217
|
// @ts-expect-error -- _$litPart$ is Lit's private render-part marker, not in the DOM types
|
|
132
218
|
if (modeChanged && canvasWrap["_$litPart$"]) {
|
|
133
|
-
|
|
134
|
-
// @ts-expect-error -- _$litPart$ is Lit's private render-part marker, not in the DOM types
|
|
135
|
-
delete canvasWrap["_$litPart$"];
|
|
219
|
+
hardClearCanvasWrap();
|
|
136
220
|
}
|
|
137
221
|
|
|
138
222
|
// Function editor mode: editing a function body in Monaco (JS)
|
|
139
223
|
if (S.ui.editingFunction) {
|
|
140
|
-
renderFunctionEditor(
|
|
224
|
+
renderFunctionEditor();
|
|
141
225
|
return;
|
|
142
226
|
}
|
|
143
227
|
|
|
@@ -199,6 +283,12 @@ export function renderCanvas() {
|
|
|
199
283
|
|
|
200
284
|
// Panel JS objects are cheap — always clear and repopulate from templates.
|
|
201
285
|
// The actual DOM elements are preserved by Lit's diffing on content-only re-renders.
|
|
286
|
+
// Stop each outgoing panel's render effect scope (and its surgical-subtree child scopes)
|
|
287
|
+
// So render-time reactive effects don't accumulate across renders.
|
|
288
|
+
for (const p of canvasPanels) {
|
|
289
|
+
p.renderScope?.stop();
|
|
290
|
+
p.renderScope = null;
|
|
291
|
+
}
|
|
202
292
|
canvasPanels.length = 0;
|
|
203
293
|
|
|
204
294
|
if (modeChanged) {
|
|
@@ -258,12 +348,6 @@ export function renderCanvas() {
|
|
|
258
348
|
let editorContainer: HTMLDivElement | null = null;
|
|
259
349
|
litRender(
|
|
260
350
|
html`<div class="source-wrap">
|
|
261
|
-
<div class="source-toolbar">
|
|
262
|
-
<sp-action-button size="s" @click=${ctx.exportFile}>
|
|
263
|
-
<sp-icon-export slot="icon"></sp-icon-export>
|
|
264
|
-
Export
|
|
265
|
-
</sp-action-button>
|
|
266
|
-
</div>
|
|
267
351
|
<div
|
|
268
352
|
class="source-editor"
|
|
269
353
|
${ref((el) => {
|
|
@@ -336,7 +420,7 @@ export function renderCanvas() {
|
|
|
336
420
|
}
|
|
337
421
|
} else if (lang === "json") {
|
|
338
422
|
try {
|
|
339
|
-
tabNow.doc.document = JSON.parse(editor.getValue());
|
|
423
|
+
tabNow.doc.document = JSON.parse(editor.getValue()) as JxMutableNode;
|
|
340
424
|
tabNow.doc.dirty = true;
|
|
341
425
|
} catch {
|
|
342
426
|
// Invalid JSON — don't update state
|
|
@@ -395,18 +479,17 @@ export function renderCanvas() {
|
|
|
395
479
|
canvasWrap,
|
|
396
480
|
);
|
|
397
481
|
|
|
398
|
-
canvasPanels.push(origPanel as unknown as CanvasPanel);
|
|
399
|
-
canvasPanels.push(currPanel as unknown as CanvasPanel);
|
|
482
|
+
canvasPanels.push(origPanel as unknown as CanvasPanel, currPanel as unknown as CanvasPanel);
|
|
400
483
|
|
|
401
484
|
/** @param {string} content */
|
|
402
|
-
const parseContent = (content: string) => {
|
|
485
|
+
const parseContent = (content: string): Promise<JxMutableNode> => {
|
|
403
486
|
const fmtPath = gitDiffState.filePath ?? "";
|
|
404
487
|
if (formatForPath(fmtPath)) {
|
|
405
488
|
return parseSourceForPath(fmtPath, content).then((r) => r.document);
|
|
406
489
|
}
|
|
407
490
|
return Promise.resolve().then(() => {
|
|
408
491
|
try {
|
|
409
|
-
return JSON.parse(content);
|
|
492
|
+
return JSON.parse(content) as JxMutableNode;
|
|
410
493
|
} catch {
|
|
411
494
|
return {
|
|
412
495
|
children: [{ tagName: "p", textContent: "Failed to parse" }],
|
|
@@ -417,7 +500,7 @@ export function renderCanvas() {
|
|
|
417
500
|
};
|
|
418
501
|
|
|
419
502
|
const { featureToggles } = S.ui;
|
|
420
|
-
Promise.all([
|
|
503
|
+
void Promise.all([
|
|
421
504
|
parseContent(gitDiffState.originalContent || ""),
|
|
422
505
|
parseContent(gitDiffState.currentContent || ""),
|
|
423
506
|
]).then(([originalDoc, currentDoc]) => {
|
|
@@ -568,7 +651,7 @@ export function renderCanvas() {
|
|
|
568
651
|
);
|
|
569
652
|
|
|
570
653
|
for (let i = 0; i < panelEntries.length; i++) {
|
|
571
|
-
const { panel, activeSet } = panelEntries[i]
|
|
654
|
+
const { panel, activeSet } = panelEntries[i]!;
|
|
572
655
|
const p = panel as CanvasPanel;
|
|
573
656
|
canvasPanels.push(p);
|
|
574
657
|
if (i === 0) {
|
|
@@ -615,7 +698,12 @@ function renderCanvasIntoPanel(
|
|
|
615
698
|
const docToRender = docOverride || (tab?.doc.document as JxMutableNode);
|
|
616
699
|
const canvas = panel.canvas as HTMLElement;
|
|
617
700
|
|
|
618
|
-
|
|
701
|
+
canvasPerf.panelRenders += 1;
|
|
702
|
+
panel.ready = false;
|
|
703
|
+
panel.liveCtx = null;
|
|
704
|
+
panel.activeBreakpoints = activeBreakpoints;
|
|
705
|
+
|
|
706
|
+
renderCanvasLive(gen, docToRender, canvas, panel)
|
|
619
707
|
.then((scope: Record<string, unknown> | null) => {
|
|
620
708
|
// Skip post-render setup if a newer render has started
|
|
621
709
|
if (gen !== view.renderGeneration) {
|
|
@@ -625,6 +713,9 @@ function renderCanvasIntoPanel(
|
|
|
625
713
|
updateCanvas({ error: null, scope, status: "ready" });
|
|
626
714
|
applyCanvasMediaOverrides(canvas, activeBreakpoints);
|
|
627
715
|
statusMessage("Runtime render OK", 1500);
|
|
716
|
+
// Panel is patchable only when the live runtime path rendered the real document
|
|
717
|
+
panel.ready = !docOverride;
|
|
718
|
+
scheduleStyleTagSweep();
|
|
628
719
|
|
|
629
720
|
// Apply diff highlighting if in git-diff mode
|
|
630
721
|
if (gitDiffState && docOverride) {
|
|
@@ -741,7 +832,7 @@ function applyDiffHighlightToCanvas(
|
|
|
741
832
|
* @param {Element} canvasEl
|
|
742
833
|
* @param {Set<string>} activeBreakpoints
|
|
743
834
|
*/
|
|
744
|
-
function applyCanvasMediaOverrides(canvasEl: Element, activeBreakpoints: Set<string>) {
|
|
835
|
+
export function applyCanvasMediaOverrides(canvasEl: Element, activeBreakpoints: Set<string>) {
|
|
745
836
|
if (activeBreakpoints.size === 0) {
|
|
746
837
|
return;
|
|
747
838
|
}
|
|
@@ -764,3 +855,24 @@ function applyCanvasMediaOverrides(canvasEl: Element, activeBreakpoints: Set<str
|
|
|
764
855
|
export function renderOverlays() {
|
|
765
856
|
overlaysPanel.render();
|
|
766
857
|
}
|
|
858
|
+
|
|
859
|
+
// ─── Style-tag hygiene ────────────────────────────────────────────────────────
|
|
860
|
+
|
|
861
|
+
let _sweepTimer: ReturnType<typeof setTimeout> | undefined;
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Remove scoped <style> tags whose owner elements are no longer in the document. The runtime emits
|
|
865
|
+
* one tag per styled element (nested selectors / media rules) and full re-renders orphan the
|
|
866
|
+
* previous tree's tags. Debounced so in-flight panel renders finish attaching first.
|
|
867
|
+
*/
|
|
868
|
+
function scheduleStyleTagSweep() {
|
|
869
|
+
clearTimeout(_sweepTimer);
|
|
870
|
+
_sweepTimer = setTimeout(() => {
|
|
871
|
+
for (const tag of document.head.querySelectorAll("style[data-jx-owner]")) {
|
|
872
|
+
const uid = (tag as HTMLElement).dataset.jxOwner;
|
|
873
|
+
if (uid && !document.querySelector(`[data-jx="${uid}"]`)) {
|
|
874
|
+
tag.remove();
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}, 250);
|
|
878
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/// <reference lib="dom.iterable" />
|
|
3
|
+
/**
|
|
4
|
+
* Isolated subtree rendering for surgical canvas patches. Re-renders a single document node into
|
|
5
|
+
* fresh DOM using the same edit-mode transform, runtime renderer, scope, and path mapper as the
|
|
6
|
+
* panel's last full render — so the patched DOM is indistinguishable from a full re-render.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { elToRenderScope, elToScope, getNodeAtPath, stripEventHandlers } from "../store";
|
|
10
|
+
import { prepareForEditMode } from "../utils/edit-display";
|
|
11
|
+
import { renderNode as runtimeRenderNode } from "@jxsuite/runtime";
|
|
12
|
+
import { effectScope } from "../reactivity";
|
|
13
|
+
import { canvasPerf } from "./canvas-perf";
|
|
14
|
+
|
|
15
|
+
import type { JxPath } from "../state";
|
|
16
|
+
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
17
|
+
import type { CanvasPanel, PanelLiveCtx } from "../types";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Render the document node at docPath into a detached DOM subtree for the given panel.
|
|
21
|
+
*
|
|
22
|
+
* The scope comes from the parent element's recorded render scope (elToScope), falling back to the
|
|
23
|
+
* panel's root scope — so $-bindings, $media, and state references resolve exactly as they did in
|
|
24
|
+
* the full render. Throws when the panel has no live render context.
|
|
25
|
+
*
|
|
26
|
+
* @param {CanvasPanel} panel
|
|
27
|
+
* @param {JxMutableNode} doc Raw (non-reactive) document root
|
|
28
|
+
* @param {JxPath} docPath
|
|
29
|
+
* @param {Element} parentEl The rendered parent element (scope source)
|
|
30
|
+
*/
|
|
31
|
+
export function renderSubtree(
|
|
32
|
+
panel: CanvasPanel,
|
|
33
|
+
doc: JxMutableNode,
|
|
34
|
+
docPath: JxPath,
|
|
35
|
+
parentEl: Element,
|
|
36
|
+
): HTMLElement | Text {
|
|
37
|
+
const { liveCtx } = panel;
|
|
38
|
+
if (!liveCtx) {
|
|
39
|
+
throw new Error("panel-missing-live-ctx");
|
|
40
|
+
}
|
|
41
|
+
const node = getNodeAtPath(doc, docPath) as JxMutableNode | string | undefined;
|
|
42
|
+
if (node === undefined) {
|
|
43
|
+
throw new Error(`node-not-found:${docPath.join("/")}`);
|
|
44
|
+
}
|
|
45
|
+
const def = typeof node === "string" ? node : prepareForEditMode(stripEventHandlers(node));
|
|
46
|
+
const scope = elToScope.get(parentEl) ?? liveCtx.scope;
|
|
47
|
+
|
|
48
|
+
// Render inside an effect scope parented to the panel's render scope: disposable on its own
|
|
49
|
+
// When the subtree is later replaced/removed, and disposed with the panel on full renders.
|
|
50
|
+
const render = () => {
|
|
51
|
+
const subScope = effectScope();
|
|
52
|
+
const rendered = subScope.run(() =>
|
|
53
|
+
runtimeRenderNode(def, scope, {
|
|
54
|
+
_path: docPathToRenderPath(docPath, liveCtx),
|
|
55
|
+
onNodeCreated: liveCtx.pathMapper,
|
|
56
|
+
}),
|
|
57
|
+
)!;
|
|
58
|
+
if (rendered instanceof HTMLElement) {
|
|
59
|
+
elToRenderScope.set(rendered, subScope);
|
|
60
|
+
}
|
|
61
|
+
return rendered;
|
|
62
|
+
};
|
|
63
|
+
const el = panel.renderScope ? panel.renderScope.run(render)! : render();
|
|
64
|
+
canvasPerf.subtreeRenders += 1;
|
|
65
|
+
|
|
66
|
+
// Match full-render edit display: canvas content never receives pointer events directly.
|
|
67
|
+
if (el instanceof HTMLElement) {
|
|
68
|
+
el.style.pointerEvents = "none";
|
|
69
|
+
for (const child of el.querySelectorAll("*")) {
|
|
70
|
+
(child as HTMLElement).style.pointerEvents = "none";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return el;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Inverse of makePathMapper's layout-prefix strip: document paths are relative to the page doc,
|
|
78
|
+
* render paths to the layout-merged doc. ($map/$switch paths never reach here — the patcher
|
|
79
|
+
* escalates them.)
|
|
80
|
+
*
|
|
81
|
+
* @param {JxPath} docPath
|
|
82
|
+
* @param {PanelLiveCtx} liveCtx
|
|
83
|
+
*/
|
|
84
|
+
function docPathToRenderPath(docPath: JxPath, liveCtx: PanelLiveCtx): (string | number)[] {
|
|
85
|
+
if (liveCtx.layoutWrapped && liveCtx.pageContentPrefix && docPath[0] === "children") {
|
|
86
|
+
// Re-apply the slot-container offset stripped by makePathMapper: page child index N renders at
|
|
87
|
+
// Container index N + offset (offset = leading layout siblings before the <slot>).
|
|
88
|
+
const [, idx] = docPath;
|
|
89
|
+
return typeof idx === "number"
|
|
90
|
+
? [...liveCtx.pageContentPrefix, idx + (liveCtx.pageContentOffset ?? 0), ...docPath.slice(2)]
|
|
91
|
+
: [...liveCtx.pageContentPrefix, ...docPath.slice(1)];
|
|
92
|
+
}
|
|
93
|
+
return docPath;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The DOM node currently occupying children[index] of the rendered parent, or null when appending
|
|
98
|
+
* at the end. The runtime renders an optional leading text node (from textContent) followed by one
|
|
99
|
+
* DOM node per children entry, in order.
|
|
100
|
+
*
|
|
101
|
+
* @param {Element} parentEl
|
|
102
|
+
* @param {JxMutableNode} parentNode
|
|
103
|
+
* @param {number} index
|
|
104
|
+
*/
|
|
105
|
+
export function domChildReference(
|
|
106
|
+
parentEl: Element,
|
|
107
|
+
parentNode: JxMutableNode,
|
|
108
|
+
index: number,
|
|
109
|
+
): ChildNode | null {
|
|
110
|
+
const text = parentNode.textContent;
|
|
111
|
+
const textOffset = text != null && String(text) !== "" ? 1 : 0;
|
|
112
|
+
return parentEl.childNodes[textOffset + index] ?? null;
|
|
113
|
+
}
|
|
@@ -64,12 +64,16 @@ export function canvasPanelTemplate(
|
|
|
64
64
|
// Which lit runs synchronously during render — before any consumer reads them.
|
|
65
65
|
const panel = {
|
|
66
66
|
_width: width || null,
|
|
67
|
+
activeBreakpoints: null,
|
|
67
68
|
canvas: null,
|
|
68
69
|
dropLine: null,
|
|
69
70
|
element: null,
|
|
71
|
+
liveCtx: null,
|
|
70
72
|
mediaName: mediaName || "",
|
|
71
73
|
overlay: null,
|
|
72
74
|
overlayClk: null,
|
|
75
|
+
ready: false,
|
|
76
|
+
renderScope: null,
|
|
73
77
|
scrollContainer: null,
|
|
74
78
|
viewport: null,
|
|
75
79
|
} as unknown as CanvasPanel;
|
|
@@ -27,16 +27,6 @@ import { dismissSlashMenu, isSlashMenuOpen, showSlashMenu } from "./slash-menu";
|
|
|
27
27
|
import { renderBlockActionBar } from "../panels/block-action-bar";
|
|
28
28
|
import { defaultDef } from "../panels/shared";
|
|
29
29
|
|
|
30
|
-
/**
|
|
31
|
-
* @type {{
|
|
32
|
-
* findCanvasElement: (
|
|
33
|
-
* path: import("../state").JxPath,
|
|
34
|
-
* canvasEl: HTMLElement,
|
|
35
|
-
* ) => HTMLElement | null;
|
|
36
|
-
* } | null}
|
|
37
|
-
*/
|
|
38
|
-
let _ctx = null;
|
|
39
|
-
|
|
40
30
|
/**
|
|
41
31
|
* Initialize the component inline edit module.
|
|
42
32
|
*
|
|
@@ -45,12 +35,13 @@ let _ctx = null;
|
|
|
45
35
|
* path: import("../state").JxPath,
|
|
46
36
|
* canvasEl: HTMLElement,
|
|
47
37
|
* ) => HTMLElement | null;
|
|
48
|
-
* }}
|
|
38
|
+
* }} _ctx
|
|
49
39
|
*/
|
|
50
|
-
export function initComponentInlineEdit(
|
|
40
|
+
export function initComponentInlineEdit(_ctx: {
|
|
51
41
|
findCanvasElement: (path: JxPath, canvasEl: HTMLElement) => HTMLElement | null;
|
|
52
42
|
}) {
|
|
53
|
-
|
|
43
|
+
// Public init hook retained for API symmetry; the ctx it once stored is no longer read here.
|
|
44
|
+
void _ctx;
|
|
54
45
|
}
|
|
55
46
|
|
|
56
47
|
/**
|
|
@@ -278,28 +269,6 @@ function splitParagraph() {
|
|
|
278
269
|
updateUi("pendingInlineEdit", { mediaName, path: newPath });
|
|
279
270
|
}
|
|
280
271
|
|
|
281
|
-
function _commitComponentInlineEdit() {
|
|
282
|
-
if (!view.componentInlineEdit) {
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
const { el, path, originalText } = view.componentInlineEdit;
|
|
286
|
-
const newText = (el.textContent ?? "").trim();
|
|
287
|
-
|
|
288
|
-
cleanupComponentInlineEdit(el);
|
|
289
|
-
|
|
290
|
-
const pPath = parentElementPath(path);
|
|
291
|
-
if (!newText && pPath) {
|
|
292
|
-
transactDoc(activeTab.value, (t) => mutateRemoveNode(t, path));
|
|
293
|
-
} else if (newText !== originalText) {
|
|
294
|
-
transactDoc(activeTab.value, (t) =>
|
|
295
|
-
mutateUpdateProperty(t, path, "textContent", newText || undefined),
|
|
296
|
-
);
|
|
297
|
-
} else {
|
|
298
|
-
renderOnly("canvas");
|
|
299
|
-
renderOnly("overlays");
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
272
|
function cancelComponentInlineEdit() {
|
|
304
273
|
if (!view.componentInlineEdit) {
|
|
305
274
|
return;
|