@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.
- package/dist/studio.js +137161 -137144
- package/dist/studio.js.map +100 -97
- package/package.json +2 -2
- package/src/canvas/canvas-helpers.js +121 -0
- package/src/canvas/canvas-live-render.js +286 -0
- package/src/canvas/canvas-render.js +429 -0
- package/src/canvas/canvas-utils.js +33 -69
- package/src/editor/component-inline-edit.js +5 -4
- package/src/editor/content-inline-edit.js +11 -18
- package/src/panels/block-action-bar.js +8 -4
- package/src/panels/canvas-dnd.js +2 -13
- package/src/panels/left-panel.js +1 -2
- package/src/panels/overlays.js +9 -23
- package/src/panels/panel-events.js +10 -13
- package/src/panels/pseudo-preview.js +7 -14
- package/src/panels/shared.js +1 -1
- package/src/platform.js +9 -6
- package/src/state.js +9 -1
- package/src/store.js +9 -0
- package/src/studio.js +57 -807
- package/src/view.js +0 -2
|
@@ -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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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 =
|
|
96
|
+
const activePanel = getActivePanel();
|
|
104
97
|
if (activePanel) {
|
|
105
|
-
const newEl =
|
|
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 =
|
|
141
|
+
const activePanel = getActivePanel();
|
|
149
142
|
if (activePanel) {
|
|
150
|
-
const el =
|
|
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 =
|
|
175
|
+
const activePanel = getActivePanel();
|
|
183
176
|
if (activePanel) {
|
|
184
|
-
const newEl =
|
|
177
|
+
const newEl = findCanvasElement(newPath, activePanel.canvas);
|
|
185
178
|
if (newEl && isEditableBlock(newEl)) {
|
|
186
179
|
enterInlineEdit(newEl, newPath);
|
|
187
180
|
}
|