@jxsuite/studio 0.28.5 → 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
package/src/tabs/transact.ts
CHANGED
|
@@ -2,16 +2,77 @@
|
|
|
2
2
|
import { toRaw } from "../reactivity";
|
|
3
3
|
import { jsonClone } from "../utils/studio-utils";
|
|
4
4
|
import { childIndex, getNodeAtPath, isAncestor, parentElementPath, pathsEqual } from "../state";
|
|
5
|
+
import {
|
|
6
|
+
beginRecording,
|
|
7
|
+
endRecording,
|
|
8
|
+
getPatchConsumer,
|
|
9
|
+
recordDocOp,
|
|
10
|
+
recordPatch,
|
|
11
|
+
} from "./patch-ops";
|
|
12
|
+
|
|
13
|
+
import type { JxDocOp, JxDocOpPair, TransactionRecord } from "./patch-ops";
|
|
5
14
|
|
|
6
15
|
import type { Tab } from "../tabs/tab";
|
|
7
16
|
import type { JxPath } from "../state";
|
|
8
17
|
|
|
9
18
|
import type { JsonValue } from "../types";
|
|
10
19
|
import type { JxEventBinding, JxMutableNode, JxStateObject, JxStyle } from "@jxsuite/schema/types";
|
|
11
|
-
import { ensureNestedStyle, getNestedStyle } from "@jxsuite/schema/guards";
|
|
20
|
+
import { ensureNestedStyle, getNestedStyle, isEventBinding } from "@jxsuite/schema/guards";
|
|
12
21
|
|
|
13
22
|
const HISTORY_LIMIT = 100;
|
|
14
23
|
|
|
24
|
+
/** Every Nth history entry stores a full snapshot so states can be materialized by replay. */
|
|
25
|
+
const CHECKPOINT_INTERVAL = 20;
|
|
26
|
+
|
|
27
|
+
/** Opt-out flag for patch-based history (forces full-snapshot-per-edit legacy behavior). */
|
|
28
|
+
function patchHistoryEnabled() {
|
|
29
|
+
try {
|
|
30
|
+
return typeof localStorage === "undefined" || !localStorage.getItem("jx-legacy-history");
|
|
31
|
+
} catch {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Deep-clone a recorded value (undefined passes through; reactive proxies are read through). */
|
|
37
|
+
function cloneValue<T>(v: T): T {
|
|
38
|
+
return v === undefined || v === null ? v : (jsonClone(v as object) as T);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Forward/inverse pair for a single-key change on the node at path. */
|
|
42
|
+
function setKeyPair(path: JxPath, key: string, before: unknown, after: unknown): JxDocOpPair {
|
|
43
|
+
return {
|
|
44
|
+
forward: { key, op: "set-key", path, value: after },
|
|
45
|
+
inverse: { key, op: "set-key", path, value: before },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Translate `path` into post-splice coordinates when it descends through `parentPath`'s children at
|
|
51
|
+
* or beyond the spliced index: a removal (delta -1) at fromIndex shifts deeper paths down; an
|
|
52
|
+
* insertion (delta +1, inclusive) shifts them up. Returns `path` unchanged when the splice doesn't
|
|
53
|
+
* affect it. Used to express a move's inverse in post-mutation coordinates.
|
|
54
|
+
*/
|
|
55
|
+
function shiftPrefixedIndex(
|
|
56
|
+
parentPath: JxPath,
|
|
57
|
+
path: JxPath,
|
|
58
|
+
spliceIndex: number,
|
|
59
|
+
delta: number,
|
|
60
|
+
inclusive: boolean,
|
|
61
|
+
): JxPath {
|
|
62
|
+
const d = parentPath.length;
|
|
63
|
+
const isProperPrefix = d < path.length && parentPath.every((seg, i) => seg === path[i]);
|
|
64
|
+
if (!isProperPrefix || path[d] !== "children" || typeof path[d + 1] !== "number") {
|
|
65
|
+
return path;
|
|
66
|
+
}
|
|
67
|
+
const idx = path[d + 1] as number;
|
|
68
|
+
if (inclusive ? idx < spliceIndex : idx <= spliceIndex) {
|
|
69
|
+
return path;
|
|
70
|
+
}
|
|
71
|
+
const shifted = [...path];
|
|
72
|
+
shifted[d + 1] = idx + delta;
|
|
73
|
+
return shifted;
|
|
74
|
+
}
|
|
75
|
+
|
|
15
76
|
/** Any value that can sit at a document node key (undefined/null/"" deletes it). */
|
|
16
77
|
export type JxNodeValue =
|
|
17
78
|
| JsonValue
|
|
@@ -52,30 +113,121 @@ export function transactDoc(
|
|
|
52
113
|
if (!tab) {
|
|
53
114
|
return;
|
|
54
115
|
}
|
|
55
|
-
|
|
116
|
+
const selectionBefore = tab.session.selection ? [...tab.session.selection] : null;
|
|
117
|
+
beginRecording();
|
|
118
|
+
let record: TransactionRecord;
|
|
119
|
+
try {
|
|
120
|
+
mutationFn(tab);
|
|
121
|
+
} finally {
|
|
122
|
+
record = endRecording();
|
|
123
|
+
}
|
|
56
124
|
|
|
57
125
|
// Replace the document root reference so effects tracking tab.doc.document re-trigger.
|
|
58
126
|
// Nested objects are shared — the mutation already modified them in place.
|
|
59
127
|
const raw = toRaw(tab.doc.document);
|
|
60
|
-
|
|
128
|
+
const newRef = { ...raw };
|
|
61
129
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
130
|
+
// When the canvas patcher can apply this change surgically, mark the new root reference as
|
|
131
|
+
// Consumed BEFORE assigning it — the canvas doc-effect runs synchronously on assignment and
|
|
132
|
+
// Skips the full render for consumed references. Panels' own effects still fire as before.
|
|
133
|
+
// Anything unrecorded or unclassifiable falls through to today's full-render path.
|
|
134
|
+
const consumer = getPatchConsumer();
|
|
135
|
+
const verdict =
|
|
136
|
+
consumer === null
|
|
137
|
+
? { patchable: false, reason: "no-patch-consumer" }
|
|
138
|
+
: record.ops.length === 0
|
|
139
|
+
? { patchable: false, reason: "unrecorded-mutation" }
|
|
140
|
+
: consumer.classify(tab, record.ops);
|
|
141
|
+
if (verdict.patchable) {
|
|
142
|
+
consumer!.markConsumed(newRef);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
tab.doc.document = newRef;
|
|
146
|
+
|
|
147
|
+
if (verdict.patchable) {
|
|
148
|
+
try {
|
|
149
|
+
consumer!.apply(tab, record.ops);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
consumer!.escalate(
|
|
152
|
+
`patch-apply-failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
153
|
+
);
|
|
71
154
|
}
|
|
72
|
-
|
|
73
|
-
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!skipHistory) {
|
|
158
|
+
pushHistoryEntry(tab, raw, record, selectionBefore);
|
|
74
159
|
}
|
|
75
160
|
|
|
76
161
|
tab.doc.dirty = true;
|
|
77
162
|
}
|
|
78
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Append a history entry for a completed transaction. With patch-based history, entries carry
|
|
166
|
+
* replayable forward/inverse ops and only periodic checkpoints store full document snapshots —
|
|
167
|
+
* eliminating the whole-document clone per edit. Non-invertible or un-instrumented transactions
|
|
168
|
+
* store a snapshot, which always keeps every state recoverable.
|
|
169
|
+
*
|
|
170
|
+
* @param {Tab} tab
|
|
171
|
+
* @param {JxMutableNode} raw Post-mutation raw document
|
|
172
|
+
* @param {TransactionRecord} record
|
|
173
|
+
* @param {JxPath | null} selectionBefore
|
|
174
|
+
*/
|
|
175
|
+
function pushHistoryEntry(
|
|
176
|
+
tab: Tab,
|
|
177
|
+
raw: JxMutableNode,
|
|
178
|
+
record: TransactionRecord,
|
|
179
|
+
selectionBefore: JxPath | null,
|
|
180
|
+
) {
|
|
181
|
+
const truncated = tab.history.snapshots.slice(0, tab.history.index + 1);
|
|
182
|
+
const useOps = patchHistoryEnabled() && record.invertible && record.docOps.length > 0;
|
|
183
|
+
const needCheckpoint = !useOps || truncated.length % CHECKPOINT_INTERVAL === 0;
|
|
184
|
+
truncated.push({
|
|
185
|
+
document: needCheckpoint ? jsonClone(raw) : null,
|
|
186
|
+
forwardOps: useOps ? record.docOps.map((p) => p.forward) : null,
|
|
187
|
+
inverseOps: useOps ? record.docOps.map((p) => p.inverse) : null,
|
|
188
|
+
selection: tab.session.selection ? [...tab.session.selection] : null,
|
|
189
|
+
selectionBefore,
|
|
190
|
+
});
|
|
191
|
+
if (truncated.length > HISTORY_LIMIT) {
|
|
192
|
+
// The new base entry must be a self-contained checkpoint before the old base is dropped.
|
|
193
|
+
if (!truncated[1]!.document) {
|
|
194
|
+
truncated[1]!.document = materializeState(truncated, 1);
|
|
195
|
+
}
|
|
196
|
+
truncated.shift();
|
|
197
|
+
}
|
|
198
|
+
tab.history.snapshots = truncated;
|
|
199
|
+
tab.history.index = truncated.length - 1;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Reconstruct the document at history state `target`: copy the nearest checkpoint at or before it,
|
|
204
|
+
* then replay forward ops. Every entry holds either a snapshot or forward ops, so this always
|
|
205
|
+
* succeeds.
|
|
206
|
+
*
|
|
207
|
+
* @param {HistorySnapshot[]} snapshots
|
|
208
|
+
* @param {number} target
|
|
209
|
+
* @returns {JxMutableNode}
|
|
210
|
+
*/
|
|
211
|
+
function materializeState(
|
|
212
|
+
snapshots: { document: JxMutableNode | null; forwardOps?: JxDocOp[] | null }[],
|
|
213
|
+
target: number,
|
|
214
|
+
): JxMutableNode {
|
|
215
|
+
let base = target;
|
|
216
|
+
while (base >= 0 && !snapshots[base]!.document) {
|
|
217
|
+
base -= 1;
|
|
218
|
+
}
|
|
219
|
+
if (base < 0) {
|
|
220
|
+
throw new Error("history-missing-checkpoint");
|
|
221
|
+
}
|
|
222
|
+
const doc = jsonClone(snapshots[base]!.document) as JxMutableNode;
|
|
223
|
+
for (let i = base + 1; i <= target; i++) {
|
|
224
|
+
for (const op of snapshots[i]!.forwardOps ?? []) {
|
|
225
|
+
applyDocOpToDoc(doc, op);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return doc;
|
|
229
|
+
}
|
|
230
|
+
|
|
79
231
|
/**
|
|
80
232
|
* Convenience: transact with a mutation fn that receives the document directly.
|
|
81
233
|
*
|
|
@@ -91,17 +243,168 @@ export function transact(
|
|
|
91
243
|
transactDoc(tab, (t) => fn(t.doc.document), opts);
|
|
92
244
|
}
|
|
93
245
|
|
|
246
|
+
// ─── Document-op application ─────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
/** Document-level keys whose changes require a full scope/panel rebuild on the canvas. */
|
|
249
|
+
const DOC_META_KEYS = new Set(["state", "$media", "$head", "$elements", "imports", "$layout"]);
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Apply a replayable doc op to a bare document tree (history replay — no canvas recording).
|
|
253
|
+
*
|
|
254
|
+
* @param {JxMutableNode} doc
|
|
255
|
+
* @param {JxDocOp} op
|
|
256
|
+
*/
|
|
257
|
+
function applyDocOpToDoc(doc: JxMutableNode, op: JxDocOp) {
|
|
258
|
+
switch (op.op) {
|
|
259
|
+
case "set-key": {
|
|
260
|
+
const node = getNodeAtPath(doc, op.path);
|
|
261
|
+
if (!node) {
|
|
262
|
+
throw new Error(`doc-op-node-not-found:${op.path.join("/")}`);
|
|
263
|
+
}
|
|
264
|
+
if (op.value === undefined) {
|
|
265
|
+
delete node[op.key];
|
|
266
|
+
} else {
|
|
267
|
+
node[op.key] = cloneValue(op.value) as JxNodeValue;
|
|
268
|
+
}
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
case "insert-child": {
|
|
272
|
+
const parent = getNodeAtPath(doc, op.parentPath);
|
|
273
|
+
childArray(parent).splice(op.index, 0, cloneValue(op.node) as JxMutableNode);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
case "remove-child": {
|
|
277
|
+
const parent = getNodeAtPath(doc, op.parentPath);
|
|
278
|
+
childArray(parent).splice(op.index, 1);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
case "set-child": {
|
|
282
|
+
const parent = getNodeAtPath(doc, op.parentPath);
|
|
283
|
+
childArray(parent).splice(op.index, 1, cloneValue(op.node) as JxMutableNode);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
case "move-child": {
|
|
287
|
+
const fromParent = getNodeAtPath(doc, op.fromParentPath);
|
|
288
|
+
const toParent = getNodeAtPath(doc, op.toParentPath);
|
|
289
|
+
const [node] = childArray(fromParent).splice(op.fromIndex, 1);
|
|
290
|
+
childArray(toParent).splice(op.toIndex, 0, node!);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
default: {
|
|
294
|
+
throw new Error(`unknown-doc-op:${(op as JxDocOp).op}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Apply a doc op to the live document AND record the matching canvas patch op, so undo/redo
|
|
301
|
+
* transactions patch the canvas surgically like any other edit.
|
|
302
|
+
*
|
|
303
|
+
* @param {Tab} tab
|
|
304
|
+
* @param {JxDocOp} op
|
|
305
|
+
*/
|
|
306
|
+
function applyDocOp(tab: Tab, op: JxDocOp) {
|
|
307
|
+
const prev =
|
|
308
|
+
op.op === "set-key" ? (getNodeAtPath(tab.doc.document, op.path)?.[op.key] as unknown) : null;
|
|
309
|
+
applyDocOpToDoc(tab.doc.document, op);
|
|
310
|
+
switch (op.op) {
|
|
311
|
+
case "set-key": {
|
|
312
|
+
if (DOC_META_KEYS.has(op.key)) {
|
|
313
|
+
recordPatch({ key: op.key, op: "doc-meta" });
|
|
314
|
+
} else if (op.key === "style") {
|
|
315
|
+
recordPatch({ op: "set-style", path: op.path });
|
|
316
|
+
} else if (op.key === "textContent") {
|
|
317
|
+
recordPatch({ op: "set-text", path: op.path });
|
|
318
|
+
} else if (op.key === "attributes" || op.key === "cases" || op.key === "$props") {
|
|
319
|
+
recordPatch({ op: "replace", path: op.path });
|
|
320
|
+
} else {
|
|
321
|
+
const isEvent =
|
|
322
|
+
op.key.startsWith("on") && (isEventBinding(op.value) || isEventBinding(prev));
|
|
323
|
+
recordPatch({ isEvent, key: op.key, op: "set-prop", path: op.path });
|
|
324
|
+
}
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
case "insert-child": {
|
|
328
|
+
recordPatch({ index: op.index, op: "insert", parentPath: op.parentPath });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
case "remove-child": {
|
|
332
|
+
recordPatch({ op: "remove", path: [...op.parentPath, "children", op.index] });
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
case "set-child": {
|
|
336
|
+
recordPatch({ op: "replace", path: [...op.parentPath, "children", op.index] });
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
case "move-child": {
|
|
340
|
+
recordPatch({
|
|
341
|
+
fromPath: [...op.fromParentPath, "children", op.fromIndex],
|
|
342
|
+
op: "move",
|
|
343
|
+
toIndex: op.toIndex,
|
|
344
|
+
toParentPath: op.toParentPath,
|
|
345
|
+
});
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
default: {
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
94
354
|
// ─── Undo / Redo ─────────────────────────────────────────────────────────────
|
|
95
355
|
|
|
356
|
+
/** Restore a materialized snapshot state (full-render path). */
|
|
357
|
+
function restoreState(tab: Tab, index: number) {
|
|
358
|
+
const { snapshots } = tab.history;
|
|
359
|
+
tab.doc.document = materializeState(snapshots, index);
|
|
360
|
+
const snap = snapshots[index]!;
|
|
361
|
+
tab.session.selection = snap.selection ? [...toRaw(snap.selection)] : null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Debug-flag assertion: ops-based undo/redo must land on the same state replay produces. */
|
|
365
|
+
function assertHistoryConsistency(tab: Tab, index: number) {
|
|
366
|
+
try {
|
|
367
|
+
if (typeof localStorage === "undefined" || !localStorage.getItem("jx-canvas-debug")) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
} catch {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const expected = JSON.stringify(materializeState(tab.history.snapshots, index));
|
|
374
|
+
const actual = JSON.stringify(jsonClone(toRaw(tab.doc.document)));
|
|
375
|
+
if (expected !== actual) {
|
|
376
|
+
console.error("jx history: ops-based undo/redo diverged from checkpoint replay", {
|
|
377
|
+
index,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
96
382
|
/** @param {Tab} tab */
|
|
97
383
|
export function undo(tab: Tab) {
|
|
98
384
|
if (tab.history.index <= 0) {
|
|
99
385
|
return;
|
|
100
386
|
}
|
|
101
|
-
tab.history.index
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
387
|
+
const entry = tab.history.snapshots[tab.history.index]!;
|
|
388
|
+
const inverseOps = patchHistoryEnabled() ? entry.inverseOps : null;
|
|
389
|
+
if (inverseOps && inverseOps.length > 0) {
|
|
390
|
+
// Surgical path: apply inverse ops through the normal transaction pipeline (the canvas
|
|
391
|
+
// Patches in place when it can), in reverse recording order.
|
|
392
|
+
transactDoc(
|
|
393
|
+
tab,
|
|
394
|
+
(t) => {
|
|
395
|
+
for (let i = inverseOps.length - 1; i >= 0; i--) {
|
|
396
|
+
applyDocOp(t, toRaw(inverseOps[i]) as JxDocOp);
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
{ skipHistory: true },
|
|
400
|
+
);
|
|
401
|
+
tab.session.selection = entry.selectionBefore ? [...toRaw(entry.selectionBefore)] : null;
|
|
402
|
+
tab.history.index -= 1;
|
|
403
|
+
assertHistoryConsistency(tab, tab.history.index);
|
|
404
|
+
} else {
|
|
405
|
+
tab.history.index -= 1;
|
|
406
|
+
restoreState(tab, tab.history.index);
|
|
407
|
+
}
|
|
105
408
|
tab.doc.dirty = true;
|
|
106
409
|
}
|
|
107
410
|
|
|
@@ -110,10 +413,25 @@ export function redo(tab: Tab) {
|
|
|
110
413
|
if (tab.history.index >= tab.history.snapshots.length - 1) {
|
|
111
414
|
return;
|
|
112
415
|
}
|
|
113
|
-
tab.history.index
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
416
|
+
const entry = tab.history.snapshots[tab.history.index + 1]!;
|
|
417
|
+
const forwardOps = patchHistoryEnabled() ? entry.forwardOps : null;
|
|
418
|
+
if (forwardOps && forwardOps.length > 0) {
|
|
419
|
+
transactDoc(
|
|
420
|
+
tab,
|
|
421
|
+
(t) => {
|
|
422
|
+
for (const op of forwardOps) {
|
|
423
|
+
applyDocOp(t, toRaw(op) as JxDocOp);
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
{ skipHistory: true },
|
|
427
|
+
);
|
|
428
|
+
tab.session.selection = entry.selection ? [...toRaw(entry.selection)] : null;
|
|
429
|
+
tab.history.index += 1;
|
|
430
|
+
assertHistoryConsistency(tab, tab.history.index);
|
|
431
|
+
} else {
|
|
432
|
+
tab.history.index += 1;
|
|
433
|
+
restoreState(tab, tab.history.index);
|
|
434
|
+
}
|
|
117
435
|
tab.doc.dirty = true;
|
|
118
436
|
}
|
|
119
437
|
|
|
@@ -136,6 +454,11 @@ export function mutateInsertNode(
|
|
|
136
454
|
return;
|
|
137
455
|
}
|
|
138
456
|
childArray(parent).splice(index, 0, structuredClone(nodeDef));
|
|
457
|
+
recordPatch({ index, op: "insert", parentPath });
|
|
458
|
+
recordDocOp({
|
|
459
|
+
forward: { index, node: cloneValue(nodeDef), op: "insert-child", parentPath },
|
|
460
|
+
inverse: { index, op: "remove-child", parentPath },
|
|
461
|
+
});
|
|
139
462
|
}
|
|
140
463
|
|
|
141
464
|
/**
|
|
@@ -148,7 +471,15 @@ export function mutateRemoveNode(tab: Tab, path: JxPath) {
|
|
|
148
471
|
}
|
|
149
472
|
const elemPath = parentElementPath(path) as JxPath;
|
|
150
473
|
const idx = childIndex(path) as number;
|
|
151
|
-
(getNodeAtPath(tab.doc.document, elemPath).children as JxMutableNode[]).splice(
|
|
474
|
+
const [removed] = (getNodeAtPath(tab.doc.document, elemPath).children as JxMutableNode[]).splice(
|
|
475
|
+
idx,
|
|
476
|
+
1,
|
|
477
|
+
);
|
|
478
|
+
recordPatch({ op: "remove", path });
|
|
479
|
+
recordDocOp({
|
|
480
|
+
forward: { index: idx, op: "remove-child", parentPath: elemPath },
|
|
481
|
+
inverse: { index: idx, node: cloneValue(removed), op: "insert-child", parentPath: elemPath },
|
|
482
|
+
});
|
|
152
483
|
if (tab.session.selection && isAncestor(path, tab.session.selection)) {
|
|
153
484
|
tab.session.selection = null;
|
|
154
485
|
}
|
|
@@ -170,6 +501,11 @@ export function mutateDuplicateNode(tab: Tab, path: JxPath) {
|
|
|
170
501
|
const idx = childIndex(path) as number;
|
|
171
502
|
const clone = structuredClone(toRaw(node));
|
|
172
503
|
(getNodeAtPath(tab.doc.document, elemPath).children as JxMutableNode[]).splice(idx + 1, 0, clone);
|
|
504
|
+
recordPatch({ index: idx + 1, op: "insert", parentPath: elemPath });
|
|
505
|
+
recordDocOp({
|
|
506
|
+
forward: { index: idx + 1, node: cloneValue(clone), op: "insert-child", parentPath: elemPath },
|
|
507
|
+
inverse: { index: idx + 1, op: "remove-child", parentPath: elemPath },
|
|
508
|
+
});
|
|
173
509
|
tab.session.selection = [...elemPath, "children", idx + 1];
|
|
174
510
|
}
|
|
175
511
|
|
|
@@ -192,7 +528,13 @@ export function mutateWrapNode(tab: Tab, path: JxPath, wrapperTag = "div") {
|
|
|
192
528
|
children: [structuredClone(toRaw(node))],
|
|
193
529
|
tagName: wrapperTag,
|
|
194
530
|
};
|
|
531
|
+
const original = cloneValue(node);
|
|
195
532
|
(getNodeAtPath(tab.doc.document, elemPath).children as JxMutableNode[]).splice(idx, 1, wrapper);
|
|
533
|
+
recordPatch({ op: "replace", path });
|
|
534
|
+
recordDocOp({
|
|
535
|
+
forward: { index: idx, node: cloneValue(wrapper), op: "set-child", parentPath: elemPath },
|
|
536
|
+
inverse: { index: idx, node: original, op: "set-child", parentPath: elemPath },
|
|
537
|
+
});
|
|
196
538
|
tab.session.selection = [...elemPath, "children", idx];
|
|
197
539
|
}
|
|
198
540
|
|
|
@@ -223,6 +565,26 @@ export function mutateMoveNode(tab: Tab, fromPath: JxPath, toParentPath: JxPath,
|
|
|
223
565
|
adjustedIndex -= 1;
|
|
224
566
|
}
|
|
225
567
|
childArray(toParent).splice(adjustedIndex, 0, node);
|
|
568
|
+
recordPatch({ fromPath, op: "move", toIndex: adjustedIndex, toParentPath });
|
|
569
|
+
recordDocOp({
|
|
570
|
+
forward: {
|
|
571
|
+
fromIndex: fromIdx,
|
|
572
|
+
fromParentPath,
|
|
573
|
+
op: "move-child",
|
|
574
|
+
toIndex: adjustedIndex,
|
|
575
|
+
toParentPath,
|
|
576
|
+
},
|
|
577
|
+
inverse: {
|
|
578
|
+
fromIndex: adjustedIndex,
|
|
579
|
+
// The inverse runs against the post-move document, so both parent paths are translated
|
|
580
|
+
// Into post-move coordinates: the removal shifted paths under fromParent, the insertion
|
|
581
|
+
// Shifted paths under toParent.
|
|
582
|
+
fromParentPath: shiftPrefixedIndex(fromParentPath, toParentPath, fromIdx, -1, false),
|
|
583
|
+
op: "move-child",
|
|
584
|
+
toIndex: fromIdx,
|
|
585
|
+
toParentPath: shiftPrefixedIndex(toParentPath, fromParentPath, adjustedIndex, 1, true),
|
|
586
|
+
},
|
|
587
|
+
});
|
|
226
588
|
|
|
227
589
|
if (pathsEqual(tab.session.selection, fromPath)) {
|
|
228
590
|
let idx = toIndex;
|
|
@@ -245,11 +607,25 @@ export function mutateMoveNode(tab: Tab, fromPath: JxPath, toParentPath: JxPath,
|
|
|
245
607
|
*/
|
|
246
608
|
export function mutateUpdateProperty(tab: Tab, path: JxPath, key: string, value?: JxNodeValue) {
|
|
247
609
|
const node = getNodeAtPath(tab.doc.document, path);
|
|
610
|
+
const prev = node[key];
|
|
611
|
+
const before = cloneValue(prev);
|
|
248
612
|
if (value === undefined || value === null || value === "") {
|
|
249
613
|
delete node[key];
|
|
250
614
|
} else {
|
|
251
615
|
node[key] = value;
|
|
252
616
|
}
|
|
617
|
+
recordDocOp(setKeyPair(path, key, before, cloneValue(node[key])));
|
|
618
|
+
if (key === "textContent") {
|
|
619
|
+
recordPatch({ op: "set-text", path });
|
|
620
|
+
} else if (key === "style") {
|
|
621
|
+
recordPatch({ op: "set-style", path });
|
|
622
|
+
} else {
|
|
623
|
+
// Event-binding edits are invisible on the canvas (stripped in design/edit renders); the
|
|
624
|
+
// Deleted-value case checks the previous value since the new one is gone.
|
|
625
|
+
const bindingValue = value === undefined || value === null || value === "" ? prev : value;
|
|
626
|
+
const isEvent = key.startsWith("on") && isEventBinding(bindingValue);
|
|
627
|
+
recordPatch({ isEvent, key, op: "set-prop", path });
|
|
628
|
+
}
|
|
253
629
|
}
|
|
254
630
|
|
|
255
631
|
/**
|
|
@@ -260,6 +636,7 @@ export function mutateUpdateProperty(tab: Tab, path: JxPath, key: string, value?
|
|
|
260
636
|
*/
|
|
261
637
|
export function mutateUpdateStyle(tab: Tab, path: JxPath, prop: string, value: string | undefined) {
|
|
262
638
|
const node = getNodeAtPath(tab.doc.document, path);
|
|
639
|
+
const styleBefore = cloneValue(node.style);
|
|
263
640
|
if (!node.style) {
|
|
264
641
|
node.style = {};
|
|
265
642
|
}
|
|
@@ -271,6 +648,8 @@ export function mutateUpdateStyle(tab: Tab, path: JxPath, prop: string, value: s
|
|
|
271
648
|
if (Object.keys(node.style).length === 0) {
|
|
272
649
|
delete node.style;
|
|
273
650
|
}
|
|
651
|
+
recordDocOp(setKeyPair(path, "style", styleBefore, cloneValue(node.style)));
|
|
652
|
+
recordPatch({ op: "set-style", path });
|
|
274
653
|
}
|
|
275
654
|
|
|
276
655
|
/**
|
|
@@ -286,6 +665,7 @@ export function mutateUpdateAttribute(
|
|
|
286
665
|
value?: string | undefined,
|
|
287
666
|
) {
|
|
288
667
|
const node = getNodeAtPath(tab.doc.document, path);
|
|
668
|
+
const attrsBefore = cloneValue(node.attributes);
|
|
289
669
|
if (!node.attributes) {
|
|
290
670
|
node.attributes = {};
|
|
291
671
|
}
|
|
@@ -297,6 +677,8 @@ export function mutateUpdateAttribute(
|
|
|
297
677
|
if (Object.keys(node.attributes).length === 0) {
|
|
298
678
|
delete node.attributes;
|
|
299
679
|
}
|
|
680
|
+
recordDocOp(setKeyPair(path, "attributes", attrsBefore, cloneValue(node.attributes)));
|
|
681
|
+
recordPatch({ attr, op: "set-attr", path });
|
|
300
682
|
}
|
|
301
683
|
|
|
302
684
|
/**
|
|
@@ -317,6 +699,7 @@ export function mutateUpdateMediaStyle(
|
|
|
317
699
|
return mutateUpdateStyle(tab, path, prop, value);
|
|
318
700
|
}
|
|
319
701
|
const node = getNodeAtPath(tab.doc.document, path);
|
|
702
|
+
const styleBefore = cloneValue(node.style);
|
|
320
703
|
if (!node.style) {
|
|
321
704
|
node.style = {};
|
|
322
705
|
}
|
|
@@ -333,6 +716,8 @@ export function mutateUpdateMediaStyle(
|
|
|
333
716
|
if (Object.keys(node.style).length === 0) {
|
|
334
717
|
delete node.style;
|
|
335
718
|
}
|
|
719
|
+
recordDocOp(setKeyPair(path, "style", styleBefore, cloneValue(node.style)));
|
|
720
|
+
recordPatch({ op: "set-style", path });
|
|
336
721
|
}
|
|
337
722
|
|
|
338
723
|
/**
|
|
@@ -350,6 +735,7 @@ export function mutateUpdateNestedStyle(
|
|
|
350
735
|
value: string | undefined,
|
|
351
736
|
) {
|
|
352
737
|
const node = getNodeAtPath(tab.doc.document, path);
|
|
738
|
+
const styleBefore = cloneValue(node.style);
|
|
353
739
|
if (!node.style) {
|
|
354
740
|
node.style = {};
|
|
355
741
|
}
|
|
@@ -365,6 +751,8 @@ export function mutateUpdateNestedStyle(
|
|
|
365
751
|
if (Object.keys(node.style).length === 0) {
|
|
366
752
|
delete node.style;
|
|
367
753
|
}
|
|
754
|
+
recordDocOp(setKeyPair(path, "style", styleBefore, cloneValue(node.style)));
|
|
755
|
+
recordPatch({ op: "set-style", path });
|
|
368
756
|
}
|
|
369
757
|
|
|
370
758
|
/**
|
|
@@ -384,6 +772,7 @@ export function mutateUpdateMediaNestedStyle(
|
|
|
384
772
|
value: string | undefined,
|
|
385
773
|
) {
|
|
386
774
|
const node = getNodeAtPath(tab.doc.document, path);
|
|
775
|
+
const styleBefore = cloneValue(node.style);
|
|
387
776
|
if (!node.style) {
|
|
388
777
|
node.style = {};
|
|
389
778
|
}
|
|
@@ -404,6 +793,8 @@ export function mutateUpdateMediaNestedStyle(
|
|
|
404
793
|
if (Object.keys(node.style).length === 0) {
|
|
405
794
|
delete node.style;
|
|
406
795
|
}
|
|
796
|
+
recordDocOp(setKeyPair(path, "style", styleBefore, cloneValue(node.style)));
|
|
797
|
+
recordPatch({ op: "set-style", path });
|
|
407
798
|
}
|
|
408
799
|
|
|
409
800
|
/**
|
|
@@ -424,6 +815,7 @@ export function mutateUpdateNestedStylePath(
|
|
|
424
815
|
value: string | undefined,
|
|
425
816
|
) {
|
|
426
817
|
const node = getNodeAtPath(tab.doc.document, path);
|
|
818
|
+
const styleBefore = cloneValue(node.style);
|
|
427
819
|
if (!node.style) {
|
|
428
820
|
node.style = {};
|
|
429
821
|
}
|
|
@@ -436,9 +828,9 @@ export function mutateUpdateNestedStylePath(
|
|
|
436
828
|
// Clean up empty parent objects
|
|
437
829
|
let cur: JxStyle | undefined = node.style;
|
|
438
830
|
for (let i = 0; i < stylePath.length && cur; i++) {
|
|
439
|
-
const child = getNestedStyle(cur, stylePath[i]);
|
|
831
|
+
const child = getNestedStyle(cur, stylePath[i]!);
|
|
440
832
|
if (child && Object.keys(child).length === 0) {
|
|
441
|
-
delete cur[stylePath[i]];
|
|
833
|
+
delete cur[stylePath[i]!];
|
|
442
834
|
break;
|
|
443
835
|
}
|
|
444
836
|
cur = child;
|
|
@@ -449,6 +841,8 @@ export function mutateUpdateNestedStylePath(
|
|
|
449
841
|
if (Object.keys(node.style).length === 0) {
|
|
450
842
|
delete node.style;
|
|
451
843
|
}
|
|
844
|
+
recordDocOp(setKeyPair(path, "style", styleBefore, cloneValue(node.style)));
|
|
845
|
+
recordPatch({ op: "set-style", path });
|
|
452
846
|
}
|
|
453
847
|
|
|
454
848
|
/**
|
|
@@ -470,6 +864,7 @@ export function mutateUpdateMediaNestedStylePath(
|
|
|
470
864
|
value: string | undefined,
|
|
471
865
|
) {
|
|
472
866
|
const node = getNodeAtPath(tab.doc.document, path);
|
|
867
|
+
const styleBefore = cloneValue(node.style);
|
|
473
868
|
if (!node.style) {
|
|
474
869
|
node.style = {};
|
|
475
870
|
}
|
|
@@ -483,9 +878,9 @@ export function mutateUpdateMediaNestedStylePath(
|
|
|
483
878
|
delete obj[prop];
|
|
484
879
|
let cur: JxStyle | undefined = media;
|
|
485
880
|
for (let i = 0; i < stylePath.length && cur; i++) {
|
|
486
|
-
const child = getNestedStyle(cur, stylePath[i]);
|
|
881
|
+
const child = getNestedStyle(cur, stylePath[i]!);
|
|
487
882
|
if (child && Object.keys(child).length === 0) {
|
|
488
|
-
delete cur[stylePath[i]];
|
|
883
|
+
delete cur[stylePath[i]!];
|
|
489
884
|
break;
|
|
490
885
|
}
|
|
491
886
|
cur = child;
|
|
@@ -499,6 +894,8 @@ export function mutateUpdateMediaNestedStylePath(
|
|
|
499
894
|
if (Object.keys(node.style).length === 0) {
|
|
500
895
|
delete node.style;
|
|
501
896
|
}
|
|
897
|
+
recordDocOp(setKeyPair(path, "style", styleBefore, cloneValue(node.style)));
|
|
898
|
+
recordPatch({ op: "set-style", path });
|
|
502
899
|
}
|
|
503
900
|
|
|
504
901
|
/**
|
|
@@ -508,11 +905,14 @@ export function mutateUpdateMediaNestedStylePath(
|
|
|
508
905
|
*/
|
|
509
906
|
export function mutateReplaceStyle(tab: Tab, path: JxPath, style: JxStyle | undefined) {
|
|
510
907
|
const node = getNodeAtPath(tab.doc.document, path);
|
|
908
|
+
const styleBefore = cloneValue(node.style);
|
|
511
909
|
if (style && Object.keys(style).length > 0) {
|
|
512
910
|
node.style = style;
|
|
513
911
|
} else {
|
|
514
912
|
delete node.style;
|
|
515
913
|
}
|
|
914
|
+
recordDocOp(setKeyPair(path, "style", styleBefore, cloneValue(node.style)));
|
|
915
|
+
recordPatch({ op: "set-style", path });
|
|
516
916
|
}
|
|
517
917
|
|
|
518
918
|
/**
|
|
@@ -522,10 +922,13 @@ export function mutateReplaceStyle(tab: Tab, path: JxPath, style: JxStyle | unde
|
|
|
522
922
|
*/
|
|
523
923
|
export function mutateAddDef(tab: Tab, name: string, def: Record<string, JsonValue>) {
|
|
524
924
|
const doc = tab.doc.document;
|
|
925
|
+
const before = cloneValue(doc.state);
|
|
525
926
|
if (!doc.state) {
|
|
526
927
|
doc.state = {};
|
|
527
928
|
}
|
|
528
929
|
doc.state[name] = def;
|
|
930
|
+
recordDocOp(setKeyPair([], "state", before, cloneValue(doc.state)));
|
|
931
|
+
recordPatch({ key: "state", op: "doc-meta" });
|
|
529
932
|
}
|
|
530
933
|
|
|
531
934
|
/**
|
|
@@ -535,10 +938,13 @@ export function mutateAddDef(tab: Tab, name: string, def: Record<string, JsonVal
|
|
|
535
938
|
export function mutateRemoveDef(tab: Tab, name: string) {
|
|
536
939
|
const doc = tab.doc.document;
|
|
537
940
|
if (doc.state) {
|
|
941
|
+
const before = cloneValue(doc.state);
|
|
538
942
|
delete doc.state[name];
|
|
539
943
|
if (Object.keys(doc.state).length === 0) {
|
|
540
944
|
delete doc.state;
|
|
541
945
|
}
|
|
946
|
+
recordDocOp(setKeyPair([], "state", before, cloneValue(doc.state)));
|
|
947
|
+
recordPatch({ key: "state", op: "doc-meta" });
|
|
542
948
|
}
|
|
543
949
|
}
|
|
544
950
|
|
|
@@ -549,6 +955,7 @@ export function mutateRemoveDef(tab: Tab, name: string) {
|
|
|
549
955
|
*/
|
|
550
956
|
export function mutateUpdateDef(tab: Tab, name: string, updates: Record<string, unknown>) {
|
|
551
957
|
const doc = tab.doc.document;
|
|
958
|
+
const stateBefore = cloneValue(doc.state);
|
|
552
959
|
if (!doc.state) {
|
|
553
960
|
doc.state = {};
|
|
554
961
|
}
|
|
@@ -566,6 +973,8 @@ export function mutateUpdateDef(tab: Tab, name: string, updates: Record<string,
|
|
|
566
973
|
delete entry[k];
|
|
567
974
|
}
|
|
568
975
|
}
|
|
976
|
+
recordDocOp(setKeyPair([], "state", stateBefore, cloneValue(doc.state)));
|
|
977
|
+
recordPatch({ key: "state", op: "doc-meta" });
|
|
569
978
|
}
|
|
570
979
|
|
|
571
980
|
/**
|
|
@@ -578,8 +987,11 @@ export function mutateRenameDef(tab: Tab, oldName: string, newName: string) {
|
|
|
578
987
|
if (!doc.state || !doc.state[oldName]) {
|
|
579
988
|
return;
|
|
580
989
|
}
|
|
990
|
+
const before = cloneValue(doc.state);
|
|
581
991
|
doc.state[newName] = doc.state[oldName];
|
|
582
992
|
delete doc.state[oldName];
|
|
993
|
+
recordDocOp(setKeyPair([], "state", before, cloneValue(doc.state)));
|
|
994
|
+
recordPatch({ key: "state", op: "doc-meta" });
|
|
583
995
|
}
|
|
584
996
|
|
|
585
997
|
/**
|
|
@@ -589,6 +1001,7 @@ export function mutateRenameDef(tab: Tab, oldName: string, newName: string) {
|
|
|
589
1001
|
*/
|
|
590
1002
|
export function mutateUpdateMedia(tab: Tab, name: string, query?: string | undefined) {
|
|
591
1003
|
const doc = tab.doc.document;
|
|
1004
|
+
const mediaBefore = cloneValue(doc.$media);
|
|
592
1005
|
if (!doc.$media) {
|
|
593
1006
|
doc.$media = {};
|
|
594
1007
|
}
|
|
@@ -600,6 +1013,8 @@ export function mutateUpdateMedia(tab: Tab, name: string, query?: string | undef
|
|
|
600
1013
|
} else {
|
|
601
1014
|
doc.$media[name] = query;
|
|
602
1015
|
}
|
|
1016
|
+
recordDocOp(setKeyPair([], "$media", mediaBefore, cloneValue(doc.$media)));
|
|
1017
|
+
recordPatch({ key: "$media", op: "doc-meta" });
|
|
603
1018
|
}
|
|
604
1019
|
|
|
605
1020
|
/**
|
|
@@ -610,6 +1025,7 @@ export function mutateUpdateMedia(tab: Tab, name: string, query?: string | undef
|
|
|
610
1025
|
*/
|
|
611
1026
|
export function mutateUpdateProp(tab: Tab, path: JxPath, propName: string, value: JsonValue) {
|
|
612
1027
|
const node = getNodeAtPath(tab.doc.document, path);
|
|
1028
|
+
const propsBefore = cloneValue(node.$props);
|
|
613
1029
|
if (!node.$props) {
|
|
614
1030
|
node.$props = {};
|
|
615
1031
|
}
|
|
@@ -621,6 +1037,8 @@ export function mutateUpdateProp(tab: Tab, path: JxPath, propName: string, value
|
|
|
621
1037
|
if (Object.keys(node.$props).length === 0) {
|
|
622
1038
|
delete node.$props;
|
|
623
1039
|
}
|
|
1040
|
+
recordDocOp(setKeyPair(path, "$props", propsBefore, cloneValue(node.$props)));
|
|
1041
|
+
recordPatch({ op: "replace", path });
|
|
624
1042
|
}
|
|
625
1043
|
|
|
626
1044
|
/**
|
|
@@ -636,10 +1054,13 @@ export function mutateAddSwitchCase(
|
|
|
636
1054
|
caseDef?: JxMutableNode,
|
|
637
1055
|
) {
|
|
638
1056
|
const node = getNodeAtPath(tab.doc.document, path);
|
|
1057
|
+
const before = cloneValue(node.cases);
|
|
639
1058
|
if (!node.cases) {
|
|
640
1059
|
node.cases = {};
|
|
641
1060
|
}
|
|
642
1061
|
node.cases[caseName] = caseDef || { tagName: "div", textContent: caseName };
|
|
1062
|
+
recordDocOp(setKeyPair(path, "cases", before, cloneValue(node.cases)));
|
|
1063
|
+
recordPatch({ op: "replace", path });
|
|
643
1064
|
}
|
|
644
1065
|
|
|
645
1066
|
/**
|
|
@@ -650,7 +1071,10 @@ export function mutateAddSwitchCase(
|
|
|
650
1071
|
export function mutateRemoveSwitchCase(tab: Tab, path: JxPath, caseName: string) {
|
|
651
1072
|
const node = getNodeAtPath(tab.doc.document, path);
|
|
652
1073
|
if (node.cases) {
|
|
1074
|
+
const before = cloneValue(node.cases);
|
|
653
1075
|
delete node.cases[caseName];
|
|
1076
|
+
recordDocOp(setKeyPair(path, "cases", before, cloneValue(node.cases)));
|
|
1077
|
+
recordPatch({ op: "replace", path });
|
|
654
1078
|
}
|
|
655
1079
|
}
|
|
656
1080
|
|
|
@@ -665,8 +1089,11 @@ export function mutateRenameSwitchCase(tab: Tab, path: JxPath, oldName: string,
|
|
|
665
1089
|
if (!node.cases || !node.cases[oldName]) {
|
|
666
1090
|
return;
|
|
667
1091
|
}
|
|
1092
|
+
const before = cloneValue(node.cases);
|
|
668
1093
|
node.cases[newName] = node.cases[oldName];
|
|
669
1094
|
delete node.cases[oldName];
|
|
1095
|
+
recordDocOp(setKeyPair(path, "cases", before, cloneValue(node.cases)));
|
|
1096
|
+
recordPatch({ op: "replace", path });
|
|
670
1097
|
}
|
|
671
1098
|
|
|
672
1099
|
// ─── Frontmatter ─────────────────────────────────────────────────────────────
|