@jxsuite/studio 0.9.0 → 0.10.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.
@@ -0,0 +1,429 @@
1
+ /**
2
+ * Canvas render — extracted from studio.js (Phase 4o). Multi-mode canvas rendering orchestrator:
3
+ * dispatches to manage/settings/source/edit/design/preview rendering paths.
4
+ */
5
+
6
+ import { html, render as litRender, nothing } from "lit-html";
7
+ import { ref } from "lit-html/directives/ref.js";
8
+ import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
9
+
10
+ import { canvasWrap, canvasPanels, updateCanvas } from "../store.js";
11
+ import { view } from "../view.js";
12
+ import {
13
+ canvasPanelTemplate,
14
+ applyTransform,
15
+ observeCenterUntilStable,
16
+ renderZoomIndicator,
17
+ resetZoomIndicator,
18
+ updateActivePanelHeaders,
19
+ } from "./canvas-utils.js";
20
+ import { effectiveZoom, overlayBoxDescriptor } from "./canvas-helpers.js";
21
+ import {
22
+ parseMediaEntries,
23
+ activeBreakpointsForWidth,
24
+ collectMediaOverrides,
25
+ applyOverridesToCanvas,
26
+ } from "../utils/canvas-media.js";
27
+ import { getEffectiveMedia } from "../site-context.js";
28
+ import { renderCanvasLive } from "./canvas-live-render.js";
29
+ import { renderCanvasNode } from "../panels/preview-render.js";
30
+ import { registerPanelDnD } from "../panels/canvas-dnd.js";
31
+ import { registerPanelEvents } from "../panels/panel-events.js";
32
+ import { updateForcedPseudoPreview } from "../panels/pseudo-preview.js";
33
+ import { renderStylebookMode } from "../panels/stylebook-panel.js";
34
+ import { dismissLinkPopover, dismissBlockActionBar } from "../panels/block-action-bar.js";
35
+ import { dismissContextMenu } from "../editor/context-menu.js";
36
+ import { dismissSlashMenu } from "../editor/slash-menu.js";
37
+ import { renderBrowse } from "../browse/browse.js";
38
+ import { renderFunctionEditor } from "../panels/editors.js";
39
+ import { mediaDisplayName } from "../panels/shared.js";
40
+ import { statusMessage } from "../panels/statusbar.js";
41
+ import * as overlaysPanel from "../panels/overlays.js";
42
+
43
+ /** @type {any} */
44
+ let _ctx = null;
45
+
46
+ /**
47
+ * Initialize the canvas render module.
48
+ *
49
+ * @param {{
50
+ * getCanvasMode: () => string;
51
+ * setCanvasMode: (mode: string) => void;
52
+ * getState: () => any;
53
+ * update: (s: any) => void;
54
+ * openFileFromTree: (path: string) => void;
55
+ * exportFile: () => void;
56
+ * }} ctx
57
+ */
58
+ export function initCanvasRender(ctx) {
59
+ _ctx = ctx;
60
+ }
61
+
62
+ export function renderCanvas() {
63
+ const S = _ctx.getState();
64
+ const canvasMode = _ctx.getCanvasMode();
65
+
66
+ // Advance render generation so stale async renders from the previous cycle bail out
67
+ ++view.renderGeneration;
68
+
69
+ // Always clear Lit's internal state so it builds fresh DOM. Stale async
70
+ // renderCanvasLive calls from a previous cycle can corrupt nested ChildPart
71
+ // markers (Comment nodes inside panzoom-wrap) in ways the root-only
72
+ // ensureLitState check cannot detect.
73
+ // @ts-ignore
74
+ if (canvasWrap["_$litPart$"]) {
75
+ canvasWrap.textContent = "";
76
+ // @ts-ignore
77
+ delete canvasWrap["_$litPart$"];
78
+ }
79
+
80
+ // Function editor mode: editing a function body in Monaco (JS)
81
+ if (S.ui.editingFunction) {
82
+ renderFunctionEditor();
83
+ return;
84
+ }
85
+
86
+ // Dispose function editor if switching away
87
+ if (view.functionEditor) {
88
+ view.functionEditor.dispose();
89
+ view.functionEditor = null;
90
+ }
91
+
92
+ // Source mode: update existing Monaco editor without recreating
93
+ if (canvasMode === "source" && view.monacoEditor) {
94
+ const jsonStr = JSON.stringify(S.document, null, 2);
95
+ const currentVal = view.monacoEditor.getValue();
96
+ if (currentVal !== jsonStr) {
97
+ view.monacoEditor._ignoreNextChange = true;
98
+ view.monacoEditor.setValue(jsonStr);
99
+ }
100
+ return;
101
+ }
102
+
103
+ // Detect whether this is a mode transition or a content-only re-render
104
+ const modeChanged = canvasMode !== view.prevCanvasMode;
105
+ view.prevCanvasMode = canvasMode;
106
+
107
+ // DnD handlers are registered on inner canvas elements that get replaced on every
108
+ // content render, so always clean them up.
109
+ for (const fn of view.canvasDndCleanups) fn();
110
+ view.canvasDndCleanups = [];
111
+
112
+ // Panel event handlers (click, dblclick, etc.) capture closures over panel references.
113
+ // Always re-register to keep closures fresh across document switches.
114
+ for (const fn of view.canvasEventCleanups) fn();
115
+ view.canvasEventCleanups = [];
116
+
117
+ // Panel JS objects are cheap — always clear and repopulate from templates.
118
+ // The actual DOM elements are preserved by Lit's diffing on content-only re-renders.
119
+ canvasPanels.length = 0;
120
+
121
+ if (modeChanged) {
122
+ // Full teardown on mode transitions — new panel structure needed
123
+ if (view.centerObserver) {
124
+ view.centerObserver.disconnect();
125
+ view.centerObserver = null;
126
+ }
127
+
128
+ // Dispose Monaco editor if switching away from source mode
129
+ if (view.monacoEditor) {
130
+ view.monacoEditor.dispose();
131
+ view.monacoEditor = null;
132
+ }
133
+
134
+ litRender(nothing, canvasWrap);
135
+ view.panzoomWrap = null;
136
+ // Reset inline style overrides from other modes
137
+ canvasWrap.style.padding = "";
138
+ canvasWrap.style.alignItems = "";
139
+ canvasWrap.style.display = "";
140
+ canvasWrap.style.overflow = "";
141
+ canvasWrap.style.overflow = "";
142
+
143
+ // Clear zoom indicator (only re-rendered by design/preview/stylebook)
144
+ resetZoomIndicator();
145
+
146
+ // Dismiss open popovers/toolbars that are no longer relevant
147
+ dismissBlockActionBar();
148
+ dismissLinkPopover();
149
+ dismissContextMenu();
150
+ dismissSlashMenu();
151
+ }
152
+
153
+ // Manage mode: project-level file browser table
154
+ if (canvasMode === "manage") {
155
+ canvasWrap.style.padding = "0";
156
+ canvasWrap.style.overflow = "auto";
157
+ renderBrowse(canvasWrap, {
158
+ openFile: (/** @type {string} */ path) => {
159
+ _ctx.setCanvasMode("edit");
160
+ _ctx.openFileFromTree(path);
161
+ },
162
+ });
163
+ return;
164
+ }
165
+
166
+ // Settings mode: render element catalog with panzoom surface
167
+ if (canvasMode === "settings") {
168
+ renderStylebookMode({
169
+ canvasPanelTemplate,
170
+ applyTransform,
171
+ observeCenterUntilStable,
172
+ renderZoomIndicator,
173
+ updateActivePanelHeaders,
174
+ overlayBoxDescriptor,
175
+ effectiveZoom,
176
+ });
177
+ return;
178
+ }
179
+
180
+ // Source mode: create Monaco editor instead of canvas
181
+ if (canvasMode === "source") {
182
+ canvasWrap.style.padding = "0";
183
+ canvasWrap.style.display = "block";
184
+ /** @type {HTMLDivElement | null} */
185
+ let editorContainer = null;
186
+ litRender(
187
+ html`<div class="source-wrap">
188
+ <div class="source-toolbar">
189
+ <sp-action-button size="s" @click=${_ctx.exportFile}>
190
+ <sp-icon-export slot="icon"></sp-icon-export>
191
+ Export
192
+ </sp-action-button>
193
+ </div>
194
+ <div
195
+ class="source-editor"
196
+ ${ref((el) => {
197
+ if (el) editorContainer = /** @type {HTMLDivElement} */ (el);
198
+ })}
199
+ ></div>
200
+ </div>`,
201
+ canvasWrap,
202
+ );
203
+
204
+ const jsonStr = JSON.stringify(S.document, null, 2);
205
+ view.monacoEditor = monaco.editor.create(/** @type {any} */ (editorContainer), {
206
+ value: jsonStr,
207
+ language: "json",
208
+ theme: "vs-dark",
209
+ automaticLayout: true,
210
+ minimap: { enabled: false },
211
+ fontSize: 12,
212
+ fontFamily: "'SF Mono', 'Fira Code', 'Consolas', monospace",
213
+ lineNumbers: "on",
214
+ scrollBeyondLastLine: false,
215
+ wordWrap: "on",
216
+ tabSize: 2,
217
+ });
218
+
219
+ // Debounced sync back to state
220
+ /** @type {any} */
221
+ let debounce;
222
+ view.monacoEditor.onDidChangeModelContent(() => {
223
+ if (view.monacoEditor._ignoreNextChange) {
224
+ view.monacoEditor._ignoreNextChange = false;
225
+ return;
226
+ }
227
+ clearTimeout(debounce);
228
+ debounce = setTimeout(() => {
229
+ try {
230
+ const parsed = JSON.parse(view.monacoEditor.getValue());
231
+ _ctx.update({ ..._ctx.getState(), document: parsed, dirty: true });
232
+ } catch {
233
+ // Invalid JSON — don't update state
234
+ }
235
+ }, 600);
236
+ });
237
+ return;
238
+ }
239
+
240
+ // Edit (content) mode — centered column, no panzoom, always 100%
241
+ if (canvasMode === "edit") {
242
+ if (modeChanged) {
243
+ canvasWrap.style.padding = "0";
244
+ canvasWrap.style.overflow = "hidden";
245
+
246
+ // Remove zoom indicator left over from design/preview mode
247
+ resetZoomIndicator();
248
+ }
249
+
250
+ const { tpl: panelTpl, panel } = canvasPanelTemplate(null, null, true);
251
+ const editTpl = html`
252
+ <div class="content-edit-canvas">
253
+ <div class="content-edit-column">${panelTpl}</div>
254
+ </div>
255
+ `;
256
+ litRender(editTpl, canvasWrap);
257
+ canvasPanels.push(panel);
258
+ renderCanvasIntoPanel(panel, new Set(), S.ui.featureToggles);
259
+ return;
260
+ }
261
+
262
+ // Normal canvas mode (design / preview) — set up panzoom surface
263
+ if (modeChanged) {
264
+ canvasWrap.style.padding = "0";
265
+ canvasWrap.style.overflow = "hidden";
266
+ }
267
+
268
+ const {
269
+ sizeBreakpoints,
270
+ featureQueries: _featureQueries,
271
+ baseWidth,
272
+ } = parseMediaEntries(getEffectiveMedia(S.document.$media));
273
+ const hasMedia = sizeBreakpoints.length > 0;
274
+ const featureToggles = S.ui.featureToggles;
275
+
276
+ // Create panzoom wrapper (the element that gets transformed)
277
+ if (!hasMedia) {
278
+ // Single panel — use baseWidth if a custom one is defined, otherwise full-width
279
+ const effectiveMedia = getEffectiveMedia(S.document.$media);
280
+ const hasBaseWidth = effectiveMedia && effectiveMedia["--"];
281
+ const label = hasBaseWidth ? `${mediaDisplayName("--")} (${baseWidth}px)` : null;
282
+ const { tpl: panelTpl, panel } = canvasPanelTemplate(
283
+ hasBaseWidth ? "base" : null,
284
+ label,
285
+ !hasBaseWidth,
286
+ hasBaseWidth ? baseWidth : undefined,
287
+ );
288
+ litRender(
289
+ html`
290
+ <div
291
+ class="panzoom-wrap"
292
+ style="transform-origin:0 0"
293
+ ${ref((el) => {
294
+ if (el) view.panzoomWrap = /** @type {HTMLDivElement} */ (el);
295
+ })}
296
+ >
297
+ ${panelTpl}
298
+ </div>
299
+ `,
300
+ canvasWrap,
301
+ );
302
+ canvasPanels.push(panel);
303
+ renderCanvasIntoPanel(panel, new Set(), featureToggles);
304
+ applyTransform();
305
+ if (modeChanged) {
306
+ observeCenterUntilStable();
307
+ }
308
+ renderZoomIndicator();
309
+ return;
310
+ }
311
+
312
+ // Build all panels: base first, then breakpoints in declared order (ascending for min-width,
313
+ // descending for max-width — matching the direction of the design's media queries).
314
+ const allPanelDefs = [
315
+ {
316
+ name: "base",
317
+ displayName: mediaDisplayName("--"),
318
+ width: baseWidth,
319
+ activeSet: activeBreakpointsForWidth(sizeBreakpoints, baseWidth),
320
+ },
321
+ ];
322
+ for (const bp of sizeBreakpoints) {
323
+ allPanelDefs.push({
324
+ name: bp.name,
325
+ displayName: mediaDisplayName(bp.name),
326
+ width: bp.width,
327
+ activeSet: activeBreakpointsForWidth(sizeBreakpoints, bp.width),
328
+ });
329
+ }
330
+
331
+ /** @type {{ tpl: any; panel: any; activeSet: any }[]} */
332
+ const panelEntries = allPanelDefs.map((def) => {
333
+ const label = `${def.displayName} (${def.width}px)`;
334
+ const { tpl, panel } = canvasPanelTemplate(def.name, label, false, def.width);
335
+ return { tpl, panel, activeSet: def.activeSet };
336
+ });
337
+
338
+ litRender(
339
+ html`
340
+ <div
341
+ class="panzoom-wrap"
342
+ style="transform-origin:0 0"
343
+ ${ref((el) => {
344
+ if (el) view.panzoomWrap = /** @type {HTMLDivElement} */ (el);
345
+ })}
346
+ >
347
+ ${panelEntries.map((e) => e.tpl)}
348
+ </div>
349
+ `,
350
+ canvasWrap,
351
+ );
352
+
353
+ for (const { panel, activeSet } of panelEntries) {
354
+ canvasPanels.push(panel);
355
+ renderCanvasIntoPanel(panel, activeSet, featureToggles);
356
+ }
357
+
358
+ // Highlight active panel header
359
+ updateActivePanelHeaders();
360
+
361
+ // Apply current zoom + pan transform
362
+ applyTransform();
363
+ if (modeChanged) {
364
+ observeCenterUntilStable();
365
+ }
366
+
367
+ // Floating zoom indicator
368
+ renderZoomIndicator();
369
+ }
370
+
371
+ /**
372
+ * Render document into a single canvas panel. Tries runtime rendering first, falls back to
373
+ * structural preview.
374
+ *
375
+ * @param {any} panel
376
+ * @param {any} activeBreakpoints
377
+ * @param {any} featureToggles
378
+ */
379
+ function renderCanvasIntoPanel(panel, activeBreakpoints, featureToggles) {
380
+ const gen = view.renderGeneration;
381
+ const S = _ctx.getState();
382
+ renderCanvasLive(gen, S.document, panel.canvas).then((/** @type {any} */ scope) => {
383
+ // Skip post-render setup if a newer render has started
384
+ if (gen !== view.renderGeneration) return;
385
+ if (scope) {
386
+ updateCanvas({ status: "ready", scope, error: null });
387
+ applyCanvasMediaOverrides(panel.canvas, activeBreakpoints);
388
+ statusMessage("Runtime render OK", 1500);
389
+ } else {
390
+ // Fallback to structural preview
391
+ updateCanvas({ status: "ready", scope: null, error: null });
392
+ renderCanvasNode(
393
+ _ctx.getState().document,
394
+ [],
395
+ panel.canvas,
396
+ activeBreakpoints,
397
+ featureToggles,
398
+ );
399
+ }
400
+ registerPanelDnD(panel);
401
+ registerPanelEvents(panel);
402
+ renderOverlays();
403
+ updateForcedPseudoPreview();
404
+ });
405
+ }
406
+
407
+ /**
408
+ * Apply media query overrides as inline styles on matching canvas elements. Needed because the
409
+ * runtime renders base styles as inline — @media CSS rules in the injected stylesheet can't win
410
+ * against inline specificity.
411
+ *
412
+ * @param {Element} canvasEl
413
+ * @param {Set<string>} activeBreakpoints
414
+ */
415
+ function applyCanvasMediaOverrides(canvasEl, activeBreakpoints) {
416
+ if (!activeBreakpoints.size) return;
417
+ const S = _ctx.getState();
418
+ const docMedia = getEffectiveMedia(S.document.$media || {});
419
+ const validBreakpoints = new Set();
420
+ for (const name of activeBreakpoints) {
421
+ if (docMedia[name]) validBreakpoints.add(name);
422
+ }
423
+ const overrides = collectMediaOverrides(document.styleSheets, validBreakpoints);
424
+ applyOverridesToCanvas(canvasEl, overrides);
425
+ }
426
+
427
+ export function renderOverlays() {
428
+ overlaysPanel.render();
429
+ }
@@ -9,6 +9,7 @@ import { styleMap } from "lit-html/directives/style-map.js";
9
9
  import { ifDefined } from "lit-html/directives/if-defined.js";
10
10
 
11
11
  import { getState, renderOnly, updateUi, canvasWrap, canvasPanels } from "../store.js";
12
+ import { ensureLitState } from "../panels/shared.js";
12
13
  import { view } from "../view.js";
13
14
 
14
15
  /** @type {any} */
@@ -206,14 +207,8 @@ export function fitToScreen() {
206
207
 
207
208
  /** Reset the zoom indicator (clear its content). Called when switching to non-panzoom modes. */
208
209
  export function resetZoomIndicator() {
209
- try {
210
- litRender(nothing, zoomIndicatorHost);
211
- } catch {
212
- const newHost = document.createElement("div");
213
- newHost.style.display = "contents";
214
- zoomIndicatorHost.replaceWith(newHost);
215
- zoomIndicatorHost = newHost;
216
- }
210
+ ensureLitState(zoomIndicatorHost);
211
+ litRender(nothing, zoomIndicatorHost);
217
212
  }
218
213
 
219
214
  /**
@@ -221,69 +216,38 @@ export function resetZoomIndicator() {
221
216
  * computed from canvas-wrap bounds.
222
217
  */
223
218
  export function renderZoomIndicator() {
224
- if (!zoomIndicatorHost.isConnected) document.body.appendChild(zoomIndicatorHost);
225
219
  const zoom = _ctx.getZoom();
226
- try {
227
- litRender(
228
- html`
229
- <div class="zoom-indicator">
230
- <span class="zoom-indicator-label">${Math.round(zoom * 100)}%</span>
231
- <sp-action-button
232
- quiet
233
- size="s"
234
- class="zoom-fit-btn"
235
- title="Fit to screen"
236
- @click=${fitToScreen}
237
- >
238
- <svg
239
- width="14"
240
- height="14"
241
- viewBox="0 0 16 16"
242
- fill="none"
243
- stroke="currentColor"
244
- stroke-width="1.5"
245
- >
246
- <rect x="2" y="2" width="12" height="12" rx="1" />
247
- <path d="M2 6h12M6 2v12" />
248
- </svg>
249
- </sp-action-button>
250
- </div>
251
- `,
252
- zoomIndicatorHost,
253
- );
254
- } catch {
255
- const newHost = document.createElement("div");
256
- newHost.style.display = "contents";
257
- zoomIndicatorHost.replaceWith(newHost);
258
- zoomIndicatorHost = newHost;
259
- litRender(
260
- html`
261
- <div class="zoom-indicator">
262
- <span class="zoom-indicator-label">${Math.round(zoom * 100)}%</span>
263
- <sp-action-button
264
- quiet
265
- size="s"
266
- class="zoom-fit-btn"
267
- title="Fit to screen"
268
- @click=${fitToScreen}
269
- >
270
- <svg
271
- width="14"
272
- height="14"
273
- viewBox="0 0 16 16"
274
- fill="none"
275
- stroke="currentColor"
276
- stroke-width="1.5"
277
- >
278
- <rect x="2" y="2" width="12" height="12" rx="1" />
279
- <path d="M2 6h12M6 2v12" />
280
- </svg>
281
- </sp-action-button>
282
- </div>
283
- `,
284
- zoomIndicatorHost,
285
- );
220
+ if (!zoomIndicatorHost.isConnected) {
221
+ document.body.appendChild(zoomIndicatorHost);
286
222
  }
223
+ ensureLitState(zoomIndicatorHost);
224
+ litRender(
225
+ html`
226
+ <div class="zoom-indicator">
227
+ <span class="zoom-indicator-label">${Math.round(zoom * 100)}%</span>
228
+ <sp-action-button
229
+ quiet
230
+ size="s"
231
+ class="zoom-fit-btn"
232
+ title="Fit to screen"
233
+ @click=${fitToScreen}
234
+ >
235
+ <svg
236
+ width="14"
237
+ height="14"
238
+ viewBox="0 0 16 16"
239
+ fill="none"
240
+ stroke="currentColor"
241
+ stroke-width="1.5"
242
+ >
243
+ <rect x="2" y="2" width="12" height="12" rx="1" />
244
+ <path d="M2 6h12M6 2v12" />
245
+ </svg>
246
+ </sp-action-button>
247
+ </div>
248
+ `,
249
+ zoomIndicatorHost,
250
+ );
287
251
  positionZoomIndicator();
288
252
  }
289
253
 
@@ -6,6 +6,7 @@
6
6
  import {
7
7
  getState,
8
8
  update,
9
+ updateUi,
9
10
  renderOnly,
10
11
  getNodeAtPath,
11
12
  selectNode,
@@ -135,7 +136,7 @@ export function enterComponentInlineEdit(el, path) {
135
136
 
136
137
  if (hitPath) {
137
138
  const media = hitMedia === "base" ? null : (hitMedia ?? null);
138
- view.pendingInlineEdit = { path: hitPath, mediaName: hitMedia };
139
+ updateUi("pendingInlineEdit", { path: hitPath, mediaName: hitMedia });
139
140
  const withMedia = { ...S, ui: { ...S.ui, activeMedia: media } };
140
141
  if (isEmpty && pPath) {
141
142
  let s = removeNode(withMedia, editPath);
@@ -144,7 +145,7 @@ export function enterComponentInlineEdit(el, path) {
144
145
  const hitParent = parentElementPath(hitPath);
145
146
  if (hitParent && pPath && hitParent.join("/") === pPath.join("/") && hitIdx > removedIdx) {
146
147
  hitPath = [...pPath, "children", hitIdx - 1];
147
- view.pendingInlineEdit = { path: hitPath, mediaName: hitMedia };
148
+ updateUi("pendingInlineEdit", { path: hitPath, mediaName: hitMedia });
148
149
  }
149
150
  update(selectNode(s, hitPath));
150
151
  } else if (newText !== originalText) {
@@ -223,7 +224,7 @@ function splitParagraph() {
223
224
  s = insertNode(s, pPath, idx + 1, newDef);
224
225
  s = selectNode(s, newPath);
225
226
 
226
- view.pendingInlineEdit = { path: newPath, mediaName };
227
+ updateUi("pendingInlineEdit", { path: newPath, mediaName });
227
228
  update(s);
228
229
  }
229
230
 
@@ -311,6 +312,6 @@ function handleComponentSlashSelect(cmd) {
311
312
  s = selectNode(s, newPath);
312
313
 
313
314
  const hasText = newDef.textContent != null;
314
- if (hasText) view.pendingInlineEdit = { path: newPath, mediaName };
315
+ if (hasText) updateUi("pendingInlineEdit", { path: newPath, mediaName });
315
316
  update(s);
316
317
  }
@@ -10,6 +10,7 @@ import {
10
10
  selectNode,
11
11
  insertNode,
12
12
  updateProperty,
13
+ getNodeAtPath,
13
14
  parentElementPath,
14
15
  childIndex,
15
16
  canvasPanels,
@@ -19,18 +20,7 @@ import { startEditing, isEditableBlock } from "./inline-edit.js";
19
20
  import { restoreTemplateExpressions } from "../utils/edit-display.js";
20
21
  import { renderBlockActionBar } from "../panels/block-action-bar.js";
21
22
  import { defaultDef } from "../panels/shared.js";
22
-
23
- /** @type {any} */
24
- let _ctx = null;
25
-
26
- /**
27
- * Initialize the content inline edit module.
28
- *
29
- * @param {{ findCanvasElement: Function; getActivePanel: Function }} ctx
30
- */
31
- export function initContentInlineEdit(ctx) {
32
- _ctx = ctx;
33
- }
23
+ import { findCanvasElement, getActivePanel } from "../canvas/canvas-helpers.js";
34
24
 
35
25
  /**
36
26
  * Enter rich-text inline editing on a canvas element (edit/content mode).
@@ -57,11 +47,14 @@ export function enterInlineEdit(el, path) {
57
47
  /** @type {any} */ textContent,
58
48
  ) {
59
49
  const S = getState();
50
+ const node = getNodeAtPath(S.document, commitPath);
60
51
  if (children) {
52
+ if (node && JSON.stringify(node.children) === JSON.stringify(children)) return;
61
53
  let s = updateProperty(S, commitPath, "textContent", undefined);
62
54
  s = updateProperty(s, commitPath, "children", children);
63
55
  update(s);
64
56
  } else if (textContent != null) {
57
+ if (node && node.textContent === textContent && !node.children) return;
65
58
  let s = updateProperty(S, commitPath, "children", undefined);
66
59
  s = updateProperty(s, commitPath, "textContent", textContent);
67
60
  update(s);
@@ -100,9 +93,9 @@ export function enterInlineEdit(el, path) {
100
93
 
101
94
  // Re-enter editing on the new element after render
102
95
  requestAnimationFrame(() => {
103
- const activePanel = _ctx.getActivePanel();
96
+ const activePanel = getActivePanel();
104
97
  if (activePanel) {
105
- const newEl = _ctx.findCanvasElement(newPath, activePanel.canvas);
98
+ const newEl = findCanvasElement(newPath, activePanel.canvas);
106
99
  if (newEl && isEditableBlock(newEl)) {
107
100
  enterInlineEdit(newEl, newPath);
108
101
  // Place cursor at start of new element
@@ -145,9 +138,9 @@ export function enterInlineEdit(el, path) {
145
138
  update(s);
146
139
 
147
140
  requestAnimationFrame(() => {
148
- const activePanel = _ctx.getActivePanel();
141
+ const activePanel = getActivePanel();
149
142
  if (activePanel) {
150
- const el = _ctx.findCanvasElement(afterPath, activePanel.canvas);
143
+ const el = findCanvasElement(afterPath, activePanel.canvas);
151
144
  if (el && isEditableBlock(el)) {
152
145
  enterInlineEdit(el, afterPath);
153
146
  }
@@ -179,9 +172,9 @@ export function enterInlineEdit(el, path) {
179
172
 
180
173
  // If the inserted element is editable, enter editing
181
174
  requestAnimationFrame(() => {
182
- const activePanel = _ctx.getActivePanel();
175
+ const activePanel = getActivePanel();
183
176
  if (activePanel) {
184
- const newEl = _ctx.findCanvasElement(newPath, activePanel.canvas);
177
+ const newEl = findCanvasElement(newPath, activePanel.canvas);
185
178
  if (newEl && isEditableBlock(newEl)) {
186
179
  enterInlineEdit(newEl, newPath);
187
180
  }