@tmagic/editor 1.8.0-beta.0 → 1.8.0-beta.2

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.
Files changed (138) hide show
  1. package/dist/es/Editor.vue_vue_type_script_setup_true_lang.js +11 -2
  2. package/dist/es/components/CodeBlockEditor.vue_vue_type_script_setup_true_lang.js +18 -110
  3. package/dist/es/components/CodeParams.vue_vue_type_script_setup_true_lang.js +6 -0
  4. package/dist/es/components/CompareForm.js +5 -0
  5. package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +167 -0
  6. package/dist/es/fields/Code.vue_vue_type_script_setup_true_lang.js +19 -3
  7. package/dist/es/fields/CodeSelect.vue_vue_type_script_setup_true_lang.js +18 -2
  8. package/dist/es/fields/CodeSelectCol.vue_vue_type_script_setup_true_lang.js +37 -6
  9. package/dist/es/fields/DataSourceFieldSelect/FieldSelect.vue_vue_type_script_setup_true_lang.js +3 -1
  10. package/dist/es/fields/DataSourceFieldSelect/Index.vue_vue_type_script_setup_true_lang.js +12 -3
  11. package/dist/es/fields/DataSourceFields.vue_vue_type_script_setup_true_lang.js +14 -6
  12. package/dist/es/fields/DataSourceMethodSelect.vue_vue_type_script_setup_true_name_true_lang.js +3 -1
  13. package/dist/es/fields/DataSourceMethods.vue_vue_type_script_setup_true_lang.js +15 -7
  14. package/dist/es/fields/DataSourceMocks.vue_vue_type_script_setup_true_lang.js +14 -6
  15. package/dist/es/fields/DataSourceSelect.vue_vue_type_script_setup_true_lang.js +3 -1
  16. package/dist/es/fields/EventSelect.vue_vue_type_script_setup_true_lang.js +58 -11
  17. package/dist/es/fields/KeyValue.vue_vue_type_script_setup_true_lang.js +11 -6
  18. package/dist/es/fields/StyleSetter/Index.vue_vue_type_script_setup_true_lang.js +8 -2
  19. package/dist/es/fields/StyleSetter/components/Border.vue_vue_type_script_setup_true_lang.js +10 -2
  20. package/dist/es/fields/StyleSetter/components/Box.vue_vue_type_script_setup_true_lang.js +3 -1
  21. package/dist/es/fields/StyleSetter/pro/Background.vue_vue_type_script_setup_true_lang.js +10 -2
  22. package/dist/es/fields/StyleSetter/pro/Border.vue_vue_type_script_setup_true_lang.js +16 -3
  23. package/dist/es/fields/StyleSetter/pro/Font.vue_vue_type_script_setup_true_lang.js +10 -2
  24. package/dist/es/fields/StyleSetter/pro/Layout.vue_vue_type_script_setup_true_lang.js +14 -2
  25. package/dist/es/fields/StyleSetter/pro/Position.vue_vue_type_script_setup_true_lang.js +10 -2
  26. package/dist/es/fields/StyleSetter/pro/Transform.vue_vue_type_script_setup_true_lang.js +10 -2
  27. package/dist/es/fields/UISelect.vue_vue_type_script_setup_true_lang.js +8 -5
  28. package/dist/es/hooks/use-code-block-edit.js +2 -2
  29. package/dist/es/hooks/use-stage.js +15 -7
  30. package/dist/es/index.js +3 -1
  31. package/dist/es/initService.js +6 -6
  32. package/dist/es/layouts/CodeEditor.vue_vue_type_script_setup_true_lang.js +4 -0
  33. package/dist/es/layouts/Framework.vue_vue_type_script_setup_true_lang.js +12 -7
  34. package/dist/es/layouts/NavMenu.vue_vue_type_script_setup_true_lang.js +8 -0
  35. package/dist/es/layouts/history-list/Bucket.js +5 -0
  36. package/dist/es/layouts/history-list/Bucket.vue_vue_type_script_setup_true_lang.js +84 -0
  37. package/dist/es/layouts/history-list/CodeBlockTab.js +5 -0
  38. package/dist/es/layouts/history-list/CodeBlockTab.vue_vue_type_script_setup_true_lang.js +62 -0
  39. package/dist/es/layouts/history-list/DataSourceTab.js +5 -0
  40. package/dist/es/layouts/history-list/DataSourceTab.vue_vue_type_script_setup_true_lang.js +62 -0
  41. package/dist/es/layouts/history-list/GroupRow.js +5 -0
  42. package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +180 -0
  43. package/dist/es/layouts/history-list/HistoryDiffDialog.js +5 -0
  44. package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +195 -0
  45. package/dist/es/layouts/history-list/HistoryListPanel.js +5 -0
  46. package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +270 -0
  47. package/dist/es/layouts/history-list/InitialRow.js +5 -0
  48. package/dist/es/layouts/history-list/InitialRow.vue_vue_type_script_setup_true_lang.js +48 -0
  49. package/dist/es/layouts/history-list/PageTab.js +5 -0
  50. package/dist/es/layouts/history-list/PageTab.vue_vue_type_script_setup_true_lang.js +99 -0
  51. package/dist/es/layouts/history-list/composables.js +154 -0
  52. package/dist/es/layouts/sidebar/layer/use-node-status.js +38 -4
  53. package/dist/es/services/BaseService.js +31 -68
  54. package/dist/es/services/codeBlock.js +205 -8
  55. package/dist/es/services/componentList.js +2 -2
  56. package/dist/es/services/dataSource.js +217 -8
  57. package/dist/es/services/dep.js +21 -9
  58. package/dist/es/services/editor.js +350 -160
  59. package/dist/es/services/events.js +2 -2
  60. package/dist/es/services/history.js +407 -12
  61. package/dist/es/services/keybinding.js +2 -2
  62. package/dist/es/services/props.js +8 -8
  63. package/dist/es/services/stageOverlay.js +2 -2
  64. package/dist/es/services/storage.js +2 -2
  65. package/dist/es/services/ui.js +2 -2
  66. package/dist/es/style.css +347 -1
  67. package/dist/es/utils/code-block.js +122 -0
  68. package/dist/es/utils/content-menu.js +4 -5
  69. package/dist/es/utils/dep/worker.js +1 -1
  70. package/dist/es/utils/editor.js +3 -3
  71. package/dist/es/utils/undo-redo.js +20 -0
  72. package/dist/style.css +347 -1
  73. package/dist/tmagic-editor.umd.cjs +3273 -766
  74. package/package.json +7 -7
  75. package/src/Editor.vue +7 -0
  76. package/src/components/CodeBlockEditor.vue +20 -109
  77. package/src/components/CodeParams.vue +6 -0
  78. package/src/components/CompareForm.vue +225 -0
  79. package/src/editorProps.ts +2 -0
  80. package/src/fields/Code.vue +19 -3
  81. package/src/fields/CodeSelect.vue +17 -1
  82. package/src/fields/CodeSelectCol.vue +30 -2
  83. package/src/fields/DataSourceFieldSelect/FieldSelect.vue +7 -1
  84. package/src/fields/DataSourceFieldSelect/Index.vue +9 -2
  85. package/src/fields/DataSourceFields.vue +12 -3
  86. package/src/fields/DataSourceMethodSelect.vue +4 -1
  87. package/src/fields/DataSourceMethods.vue +14 -4
  88. package/src/fields/DataSourceMocks.vue +13 -3
  89. package/src/fields/DataSourceSelect.vue +4 -1
  90. package/src/fields/EventSelect.vue +57 -8
  91. package/src/fields/KeyValue.vue +17 -4
  92. package/src/fields/StyleSetter/Index.vue +6 -0
  93. package/src/fields/StyleSetter/components/Border.vue +15 -1
  94. package/src/fields/StyleSetter/components/Box.vue +2 -0
  95. package/src/fields/StyleSetter/pro/Background.vue +15 -1
  96. package/src/fields/StyleSetter/pro/Border.vue +24 -2
  97. package/src/fields/StyleSetter/pro/Font.vue +15 -1
  98. package/src/fields/StyleSetter/pro/Layout.vue +17 -1
  99. package/src/fields/StyleSetter/pro/Position.vue +15 -1
  100. package/src/fields/StyleSetter/pro/Transform.vue +15 -1
  101. package/src/fields/UISelect.vue +5 -2
  102. package/src/hooks/use-code-block-edit.ts +5 -2
  103. package/src/hooks/use-stage.ts +23 -7
  104. package/src/index.ts +1 -0
  105. package/src/layouts/CodeEditor.vue +15 -0
  106. package/src/layouts/Framework.vue +10 -5
  107. package/src/layouts/NavMenu.vue +9 -0
  108. package/src/layouts/history-list/Bucket.vue +99 -0
  109. package/src/layouts/history-list/CodeBlockTab.vue +61 -0
  110. package/src/layouts/history-list/DataSourceTab.vue +61 -0
  111. package/src/layouts/history-list/GroupRow.vue +207 -0
  112. package/src/layouts/history-list/HistoryDiffDialog.vue +212 -0
  113. package/src/layouts/history-list/HistoryListPanel.vue +271 -0
  114. package/src/layouts/history-list/InitialRow.vue +41 -0
  115. package/src/layouts/history-list/PageTab.vue +94 -0
  116. package/src/layouts/history-list/composables.ts +195 -0
  117. package/src/layouts/sidebar/layer/use-node-status.ts +64 -12
  118. package/src/services/BaseService.ts +35 -86
  119. package/src/services/codeBlock.ts +256 -5
  120. package/src/services/dataSource.ts +257 -6
  121. package/src/services/dep.ts +32 -9
  122. package/src/services/editor.ts +454 -171
  123. package/src/services/history.ts +452 -8
  124. package/src/services/props.ts +2 -2
  125. package/src/theme/history-list-panel.scss +423 -0
  126. package/src/theme/theme.scss +1 -0
  127. package/src/type.ts +179 -6
  128. package/src/utils/code-block.ts +150 -0
  129. package/src/utils/content-menu.ts +8 -5
  130. package/src/utils/dep/worker.ts +5 -3
  131. package/src/utils/editor.ts +4 -1
  132. package/src/utils/index.ts +1 -0
  133. package/src/utils/undo-redo.ts +23 -0
  134. package/types/index.d.ts +2107 -810
  135. package/dist/es/utils/compose.js +0 -44
  136. package/dist/es/utils/editor-history.js +0 -100
  137. package/src/utils/compose.ts +0 -52
  138. package/src/utils/editor-history.ts +0 -138
@@ -1,35 +1,47 @@
1
1
  import { Layout, canUsePluginMethods } from "../type.js";
2
- import BaseService_default from "./BaseService.js";
2
+ import BaseService from "./BaseService.js";
3
3
  import props_default from "./props.js";
4
4
  import history_default from "./history.js";
5
5
  import storage_default, { Protocol } from "./storage.js";
6
6
  import { COPY_STORAGE_KEY, calcAlignCenterStyle, calcLayerTargetIndex, calcMoveStyle, classifyDragSources, collectRelatedNodes, editorNodeMergeCustomizer, fixNodePosition, getInitPositionStyle, getNodeIndex, getPageFragmentList, getPageList, moveItemsInContainer, resolveSelectedNode, setChildrenLayout, setLayout, toggleFixedPosition } from "../utils/editor.js";
7
- import { applyHistoryAddOp, applyHistoryRemoveOp, applyHistoryUpdateOp } from "../utils/editor-history.js";
8
7
  import { beforePaste, getAddParent } from "../utils/operator.js";
9
8
  import { isFixed } from "@tmagic/stage";
10
- import { getNodeInfo, getNodePath, isPage, isPageFragment } from "@tmagic/utils";
9
+ import { getNodeInfo, getNodePath, getValueByKeyPath, isPage, isPageFragment, setValueByKeyPath } from "@tmagic/utils";
11
10
  import { NodeType } from "@tmagic/core";
12
11
  import { reactive, toRaw } from "vue";
13
- import { cloneDeep, isObject, mergeWith, uniq } from "lodash-es";
12
+ import { cloneDeep as cloneDeep$1, isObject, mergeWith, uniq } from "lodash-es";
14
13
  //#region packages/editor/src/services/editor.ts
15
14
  /**
16
- * 经过 BaseService 的插件 / 中间件包装后,源方法的最后一个形参可能被注入为 dispatch 函数
17
- * options 形参位置被注入为函数(或为 null)时,将其归一为空对象,避免后续逻辑误读
15
+ * 给「回滚」生成的新 step 用的简短描述生成器。
16
+ * UI `describePageStep` 同义,但避免 service 反向依赖 layouts/,故在此本地实现。
18
17
  */
19
- var safeOptions = (options) => {
20
- const empty = {};
21
- if (!options || typeof options === "function") return empty;
22
- return options;
23
- };
24
- /**
25
- * 经过 BaseService 的插件 / 中间件包装后,源方法的形参可能被注入为 dispatch 函数
26
- * 当 parent 形参位置被注入为函数(或为空值)时,归一为 null,由调用方继续走默认 parent 逻辑
27
- */
28
- var safeParent = (parent) => {
29
- if (!parent || typeof parent === "function") return null;
30
- return parent;
18
+ var describeStepForRevert = (step) => {
19
+ switch (step.opType) {
20
+ case "add": {
21
+ const count = step.nodes?.length ?? 0;
22
+ const node = step.nodes?.[0];
23
+ const label = node?.name || node?.type || (node?.id !== void 0 ? `${node.id}` : "");
24
+ return `撤回新增 ${count} 个节点${count === 1 && label ? `(${label})` : ""}`;
25
+ }
26
+ case "remove": {
27
+ const count = step.removedItems?.length ?? 0;
28
+ const node = step.removedItems?.[0]?.node;
29
+ const label = node?.name || node?.type || (node?.id !== void 0 ? `${node.id}` : "");
30
+ return `还原已删除的 ${count} 个节点${count === 1 && label ? `(${label})` : ""}`;
31
+ }
32
+ default: {
33
+ const items = step.updatedItems ?? [];
34
+ if (items.length === 1) {
35
+ const { newNode, oldNode, changeRecords } = items[0];
36
+ const target = newNode?.name || newNode?.type || oldNode?.name || oldNode?.type || `${newNode?.id ?? ""}`;
37
+ const propPath = changeRecords?.[0]?.propPath;
38
+ return propPath ? `还原 ${target} · ${propPath}` : `还原 ${target}`;
39
+ }
40
+ return `还原 ${items.length} 个节点的修改`;
41
+ }
42
+ }
31
43
  };
32
- var Editor = class extends BaseService_default {
44
+ var Editor = class extends BaseService {
33
45
  state = reactive({
34
46
  root: null,
35
47
  page: null,
@@ -45,7 +57,6 @@ var Editor = class extends BaseService_default {
45
57
  disabledMultiSelect: false,
46
58
  alwaysMultiSelect: false
47
59
  });
48
- isHistoryStateChange = false;
49
60
  selectionBeforeOp = null;
50
61
  constructor() {
51
62
  super(canUsePluginMethods.async.map((methodName) => ({
@@ -137,7 +148,7 @@ var Editor = class extends BaseService_default {
137
148
  * 只有容器拥有布局
138
149
  */
139
150
  async getLayout(parent, node) {
140
- if (node && typeof node !== "function" && isFixed(node.style || {})) return Layout.FIXED;
151
+ if (node && isFixed(node.style || {})) return Layout.FIXED;
141
152
  if (parent.layout) return parent.layout;
142
153
  if (!parent.style?.position) return Layout.RELATIVE;
143
154
  return Layout.ABSOLUTE;
@@ -231,18 +242,18 @@ var Editor = class extends BaseService_default {
231
242
  const layout = await this.getLayout(toRaw(parent), node);
232
243
  node.style = getInitPositionStyle(node.style, layout);
233
244
  await stage?.add({
234
- config: cloneDeep(node),
235
- parent: cloneDeep(parent),
245
+ config: cloneDeep$1(node),
246
+ parent: cloneDeep$1(parent),
236
247
  parentId: parent.id,
237
- root: cloneDeep(root)
248
+ root: cloneDeep$1(root)
238
249
  });
239
250
  const newStyle = fixNodePosition(node, parent, stage);
240
251
  if (newStyle && (newStyle.top !== node.style.top || newStyle.left !== node.style.left)) {
241
252
  node.style = newStyle;
242
253
  await stage?.update({
243
- config: cloneDeep(node),
254
+ config: cloneDeep$1(node),
244
255
  parentId: parent.id,
245
- root: cloneDeep(root)
256
+ root: cloneDeep$1(root)
246
257
  });
247
258
  }
248
259
  this.addModifiedNodeId(node.id);
@@ -255,11 +266,10 @@ var Editor = class extends BaseService_default {
255
266
  * @param options 可选配置
256
267
  * @param options.doNotSelect 添加后是否不更新当前选中节点(默认 false,添加后会选中新增的节点)
257
268
  * @param options.doNotSwitchPage 添加后是否不切换当前页面(默认 false;新增页面 / 跨页新增时为 true 会跳过会引发页面切换的选中操作)
269
+ * @param options.doNotPushHistory 是否不写入历史记录(默认 false)
258
270
  * @returns 添加后的节点
259
271
  */
260
- async add(addNode, parent, options) {
261
- const safeParentNode = safeParent(parent);
262
- const { doNotSelect = false, doNotSwitchPage = false } = safeOptions(options);
272
+ async add(addNode, parent, { doNotSelect = false, doNotSwitchPage = false, doNotPushHistory = false, historyDescription } = {}) {
263
273
  this.captureSelectionBeforeOp();
264
274
  const stage = this.get("stage");
265
275
  const addNodes = [];
@@ -271,7 +281,7 @@ var Editor = class extends BaseService_default {
271
281
  const newNodes = await Promise.all(addNodes.map((node) => {
272
282
  const root = this.get("root");
273
283
  if ((isPage(node) || isPageFragment(node)) && root) return this.doAdd(node, root);
274
- const parentNode = safeParentNode ?? getAddParent(node);
284
+ const parentNode = parent ?? getAddParent(node);
275
285
  if (!parentNode) throw new Error("未找到父元素");
276
286
  return this.doAdd(node, parentNode);
277
287
  }));
@@ -292,8 +302,8 @@ var Editor = class extends BaseService_default {
292
302
  }
293
303
  if (!(isPage(newNodes[0]) || isPageFragment(newNodes[0]))) {
294
304
  const pageForOp = this.getNodeInfo(newNodes[0].id, false).page;
295
- this.pushOpHistory("add", {
296
- nodes: newNodes.map((n) => cloneDeep(toRaw(n))),
305
+ if (!doNotPushHistory) this.pushOpHistory("add", {
306
+ nodes: newNodes.map((n) => cloneDeep$1(toRaw(n))),
297
307
  parentId: (this.getParentById(newNodes[0].id, false) ?? this.get("root")).id,
298
308
  indexMap: Object.fromEntries(newNodes.map((n) => {
299
309
  const p = this.getParentById(n.id, false);
@@ -302,13 +312,13 @@ var Editor = class extends BaseService_default {
302
312
  }, {
303
313
  name: pageForOp?.name || "",
304
314
  id: pageForOp.id
305
- });
315
+ }, historyDescription);
316
+ else this.selectionBeforeOp = null;
306
317
  }
307
318
  this.emit("add", newNodes);
308
319
  return Array.isArray(addNode) ? newNodes : newNodes[0];
309
320
  }
310
- async doRemove(node, options) {
311
- const { doNotSelect = false, doNotSwitchPage = false } = safeOptions(options);
321
+ async doRemove(node, { doNotSelect = false, doNotSwitchPage = false } = {}) {
312
322
  const root = this.get("root");
313
323
  if (!root) throw new Error("root不能为空");
314
324
  const { parent, node: curNode } = this.getNodeInfo(node.id, false);
@@ -320,7 +330,7 @@ var Editor = class extends BaseService_default {
320
330
  stage?.remove({
321
331
  id: node.id,
322
332
  parentId: parent.id,
323
- root: cloneDeep(root)
333
+ root: cloneDeep$1(root)
324
334
  });
325
335
  const selectedNodes = this.get("nodes");
326
336
  const removedSelectedIndex = selectedNodes.findIndex((n) => `${n.id}` === `${node.id}`);
@@ -367,9 +377,9 @@ var Editor = class extends BaseService_default {
367
377
  * @param options 可选配置
368
378
  * @param options.doNotSelect 删除后是否不更新当前选中节点(默认 false,删除后会选中父节点或首个页面)
369
379
  * @param options.doNotSwitchPage 删除后是否不切换当前页面(默认 false;删除页面 / 页面片段时为 true 会跳过自动切换到首个剩余页面)
380
+ * @param options.doNotPushHistory 是否不写入历史记录(默认 false)
370
381
  */
371
- async remove(nodeOrNodeList, options) {
372
- const { doNotSelect = false, doNotSwitchPage = false } = safeOptions(options);
382
+ async remove(nodeOrNodeList, { doNotSelect = false, doNotSwitchPage = false, doNotPushHistory = false, historyDescription } = {}) {
373
383
  this.captureSelectionBeforeOp();
374
384
  const nodes = Array.isArray(nodeOrNodeList) ? nodeOrNodeList : [nodeOrNodeList];
375
385
  const removedItems = [];
@@ -383,7 +393,7 @@ var Editor = class extends BaseService_default {
383
393
  };
384
394
  const idx = getNodeIndex(curNode.id, parent);
385
395
  removedItems.push({
386
- node: cloneDeep(toRaw(curNode)),
396
+ node: cloneDeep$1(toRaw(curNode)),
387
397
  parentId: parent.id,
388
398
  index: typeof idx === "number" ? idx : -1
389
399
  });
@@ -393,7 +403,8 @@ var Editor = class extends BaseService_default {
393
403
  doNotSelect,
394
404
  doNotSwitchPage
395
405
  })));
396
- if (removedItems.length > 0 && pageForOp) this.pushOpHistory("remove", { removedItems }, pageForOp);
406
+ if (removedItems.length > 0 && pageForOp) if (!doNotPushHistory) this.pushOpHistory("remove", { removedItems }, pageForOp, historyDescription);
407
+ else this.selectionBeforeOp = null;
397
408
  this.emit("remove", nodes);
398
409
  }
399
410
  async doUpdate(config, { changeRecords = [] } = {}) {
@@ -404,7 +415,7 @@ var Editor = class extends BaseService_default {
404
415
  if (!info.node) throw new Error(`获取不到id为${config.id}的节点`);
405
416
  const node = toRaw(info.node);
406
417
  let newConfig = await toggleFixedPosition(toRaw(config), node, root, this.getLayout);
407
- newConfig = mergeWith(cloneDeep(node), newConfig, editorNodeMergeCustomizer);
418
+ newConfig = mergeWith(cloneDeep$1(node), newConfig, editorNodeMergeCustomizer);
408
419
  if (!newConfig.type) throw new Error("配置缺少type值");
409
420
  if (newConfig.type === NodeType.ROOT) {
410
421
  this.set("root", newConfig);
@@ -444,25 +455,33 @@ var Editor = class extends BaseService_default {
444
455
  * 更新节点
445
456
  * update后会触发依赖收集,收集完后会掉stage.update方法
446
457
  * @param config 新的节点配置,配置中需要有id信息
458
+ * @param data 额外数据
459
+ * @param data.changeRecords 单节点 form 端变更记录(多节点场景下被忽略,使用 changeRecordList)
460
+ * @param data.changeRecordList 多节点 form 端变更记录列表,按 config 数组同序对应每个节点;优先级高于 changeRecords
461
+ * @param data.doNotPushHistory 是否不写入历史记录(默认 false)
462
+ * @param data.historyDescription 入栈时附带的人类可读描述,用于历史面板展示(不影响 undo/redo 行为)
447
463
  * @returns 更新后的节点配置
448
464
  */
449
465
  async update(config, data = {}) {
450
466
  this.captureSelectionBeforeOp();
467
+ const { doNotPushHistory = false, changeRecordList, changeRecords, historyDescription } = data;
451
468
  const nodes = Array.isArray(config) ? config : [config];
452
- const updateData = await Promise.all(nodes.map((node) => this.doUpdate(node, data)));
469
+ const updateData = await Promise.all(nodes.map((node, index) => {
470
+ const recordsForNode = changeRecordList ? changeRecordList[index] ?? [] : changeRecords ?? [];
471
+ return this.doUpdate(node, { changeRecords: recordsForNode });
472
+ }));
453
473
  if (updateData[0].oldNode?.type !== NodeType.ROOT) {
454
- const curNodes = this.get("nodes");
455
- if (!this.isHistoryStateChange && curNodes.length) {
474
+ if (this.get("nodes").length) if (!doNotPushHistory) {
456
475
  const pageForOp = this.getNodeInfo(nodes[0].id, false).page;
457
476
  this.pushOpHistory("update", { updatedItems: updateData.map((d) => ({
458
- oldNode: cloneDeep(d.oldNode),
459
- newNode: cloneDeep(toRaw(d.newNode))
477
+ oldNode: cloneDeep$1(d.oldNode),
478
+ newNode: cloneDeep$1(toRaw(d.newNode)),
479
+ changeRecords: d.changeRecords?.length ? cloneDeep$1(d.changeRecords) : void 0
460
480
  })) }, {
461
481
  name: pageForOp?.name || "",
462
482
  id: pageForOp.id
463
- });
464
- }
465
- this.isHistoryStateChange = false;
483
+ }, historyDescription);
484
+ } else this.selectionBeforeOp = null;
466
485
  }
467
486
  this.emit("update", updateData);
468
487
  return Array.isArray(config) ? updateData.map((item) => item.newNode) : updateData[0].newNode;
@@ -474,27 +493,27 @@ var Editor = class extends BaseService_default {
474
493
  * @param options 可选配置
475
494
  * @param options.doNotSelect 排序后是否不更新当前选中节点(默认 false)
476
495
  * @param options.doNotSwitchPage 排序后是否不切换当前页面(排序只发生在同一父节点内,方法内为空操作;保留以与其它 DSL 操作 API 一致)
496
+ * @param options.doNotPushHistory 是否不写入历史记录(默认 false)
477
497
  * @returns void
478
498
  */
479
- async sort(id1, id2, options) {
480
- const { doNotSelect = false } = safeOptions(options);
499
+ async sort(id1, id2, { doNotSelect = false, doNotPushHistory = false } = {}) {
481
500
  this.captureSelectionBeforeOp();
482
501
  const root = this.get("root");
483
502
  if (!root) throw new Error("root为空");
484
503
  const node = this.get("node");
485
504
  if (!node) throw new Error("当前节点为空");
486
- const parent = cloneDeep(toRaw(this.get("parent")));
505
+ const parent = cloneDeep$1(toRaw(this.get("parent")));
487
506
  if (!parent) throw new Error("父节点为空");
488
507
  const index2 = parent.items.findIndex((node) => `${node.id}` === `${id2}`);
489
508
  if (index2 < 0) return;
490
509
  const index1 = parent.items.findIndex((node) => `${node.id}` === `${id1}`);
491
510
  parent.items.splice(index2, 0, ...parent.items.splice(index1, 1));
492
- await this.update(parent);
511
+ await this.update(parent, { doNotPushHistory });
493
512
  if (!doNotSelect) await this.select(node);
494
513
  this.get("stage")?.update({
495
- config: cloneDeep(node),
514
+ config: cloneDeep$1(node),
496
515
  parentId: parent.id,
497
- root: cloneDeep(root)
516
+ root: cloneDeep$1(root)
498
517
  });
499
518
  }
500
519
  /**
@@ -522,10 +541,10 @@ var Editor = class extends BaseService_default {
522
541
  * @param options 可选配置
523
542
  * @param options.doNotSelect 粘贴后是否不更新当前选中节点(默认 false)
524
543
  * @param options.doNotSwitchPage 粘贴后是否不切换当前页面(默认 false;跨页粘贴时为 true 会跳过页面切换)
544
+ * @param options.doNotPushHistory 是否不写入历史记录(默认 false)
525
545
  * @returns 添加后的组件节点配置
526
546
  */
527
- async paste(position = {}, collectorOptions, options) {
528
- const { doNotSelect = false, doNotSwitchPage = false } = safeOptions(options);
547
+ async paste(position = {}, collectorOptions, { doNotSelect = false, doNotSwitchPage = false, doNotPushHistory = false } = {}) {
529
548
  const config = storage_default.getItem(COPY_STORAGE_KEY);
530
549
  if (!Array.isArray(config)) return;
531
550
  const node = this.get("node");
@@ -538,18 +557,19 @@ var Editor = class extends BaseService_default {
538
557
  if (collectorOptions && typeof collectorOptions.isTarget === "function") props_default.replaceRelateId(config, pasteConfigs, collectorOptions);
539
558
  return this.add(pasteConfigs, parent, {
540
559
  doNotSelect,
541
- doNotSwitchPage
560
+ doNotSwitchPage,
561
+ doNotPushHistory
542
562
  });
543
563
  }
544
564
  async doPaste(config, position = {}) {
545
565
  props_default.clearRelateId();
546
566
  const doc = this.get("stage")?.renderer?.contentWindow?.document;
547
- return beforePaste(position, cloneDeep(config), doc);
567
+ return beforePaste(position, cloneDeep$1(config), doc);
548
568
  }
549
569
  async doAlignCenter(config) {
550
570
  const parent = this.getParentById(config.id);
551
571
  if (!parent) throw new Error("找不到父节点");
552
- const node = cloneDeep(toRaw(config));
572
+ const node = cloneDeep$1(toRaw(config));
553
573
  const layout = await this.getLayout(parent, node);
554
574
  const doc = this.get("stage")?.renderer?.contentWindow?.document;
555
575
  const newStyle = calcAlignCenterStyle(node, parent, layout, doc);
@@ -563,14 +583,14 @@ var Editor = class extends BaseService_default {
563
583
  * @param options 可选配置
564
584
  * @param options.doNotSelect 居中后是否不更新当前选中节点(默认 false)
565
585
  * @param options.doNotSwitchPage 居中后是否不切换当前页面(居中只更新节点 style,方法内为空操作;保留以与其它 DSL 操作 API 一致)
586
+ * @param options.doNotPushHistory 是否不写入历史记录(默认 false)
566
587
  * @returns 当前组件节点配置
567
588
  */
568
- async alignCenter(config, options) {
569
- const { doNotSelect = false } = safeOptions(options);
589
+ async alignCenter(config, { doNotSelect = false, doNotPushHistory = false } = {}) {
570
590
  const nodes = Array.isArray(config) ? config : [config];
571
591
  const stage = this.get("stage");
572
592
  const newNodes = await Promise.all(nodes.map((node) => this.doAlignCenter(node)));
573
- const newNode = await this.update(newNodes);
593
+ const newNode = await this.update(newNodes, { doNotPushHistory });
574
594
  if (!doNotSelect) if (newNodes.length > 1) await stage?.multiSelect(newNodes.map((node) => node.id));
575
595
  else await stage?.select(newNodes[0].id);
576
596
  return newNode;
@@ -578,8 +598,10 @@ var Editor = class extends BaseService_default {
578
598
  /**
579
599
  * 移动当前选中节点位置
580
600
  * @param offset 偏移量
601
+ * @param options 可选配置
602
+ * @param options.doNotPushHistory 是否不写入历史记录(默认 false)
581
603
  */
582
- async moveLayer(offset) {
604
+ async moveLayer(offset, { doNotPushHistory = false } = {}) {
583
605
  this.captureSelectionBeforeOp();
584
606
  const root = this.get("root");
585
607
  if (!root) throw new Error("root为空");
@@ -592,95 +614,104 @@ var Editor = class extends BaseService_default {
592
614
  const isRelative = await this.getLayout(parent, node) === Layout.RELATIVE;
593
615
  const offsetIndex = calcLayerTargetIndex(index, offset, brothers.length, isRelative);
594
616
  if (offsetIndex > 0 && offsetIndex > brothers.length || offsetIndex < 0) return;
595
- const oldParent = cloneDeep(toRaw(parent));
617
+ const oldParent = cloneDeep$1(toRaw(parent));
596
618
  brothers.splice(index, 1);
597
619
  brothers.splice(offsetIndex, 0, node);
598
620
  const grandparent = this.getParentById(parent.id);
599
621
  this.get("stage")?.update({
600
- config: cloneDeep(toRaw(parent)),
622
+ config: cloneDeep$1(toRaw(parent)),
601
623
  parentId: grandparent?.id,
602
- root: cloneDeep(root)
624
+ root: cloneDeep$1(root)
603
625
  });
604
626
  this.addModifiedNodeId(parent.id);
605
- const pageForOp = this.getNodeInfo(node.id, false).page;
606
- this.pushOpHistory("update", { updatedItems: [{
607
- oldNode: oldParent,
608
- newNode: cloneDeep(toRaw(parent))
609
- }] }, {
610
- name: pageForOp?.name || "",
611
- id: pageForOp.id
612
- });
627
+ if (!doNotPushHistory) {
628
+ const pageForOp = this.getNodeInfo(node.id, false).page;
629
+ this.pushOpHistory("update", { updatedItems: [{
630
+ oldNode: oldParent,
631
+ newNode: cloneDeep$1(toRaw(parent))
632
+ }] }, {
633
+ name: pageForOp?.name || "",
634
+ id: pageForOp.id
635
+ });
636
+ } else this.selectionBeforeOp = null;
613
637
  this.emit("move-layer", offset);
614
638
  }
615
639
  /**
616
- * 移动到指定容器中
617
- * @param config 需要移动的节点
640
+ * 移动一个或多个节点到指定容器中。
641
+ *
642
+ * 多选场景(config 是数组)只会产生一条历史记录,
643
+ * `updatedItems` 涵盖所有源父容器 + 目标容器的前后快照。
644
+ * 这避免了"多选移动到某容器"在历史栈里被切成 N 条记录。
645
+ *
646
+ * @param config 需要移动的节点(或节点数组,各项需带 id;style 等字段会与原节点合并)
618
647
  * @param targetId 容器ID
619
648
  * @param options 可选配置
620
649
  * @param options.doNotSelect 移动后是否不更新当前选中节点(默认 false)
621
650
  * @param options.doNotSwitchPage 移动后是否不切换当前页面(默认 false;目标容器位于其它页面时为 true 会跳过自动选中以避免页面切换)
651
+ * @param options.doNotPushHistory 是否不写入历史记录(默认 false)
622
652
  */
623
- async moveToContainer(config, targetId, options) {
624
- const { doNotSelect = false, doNotSwitchPage = false } = safeOptions(options);
653
+ async moveToContainer(config, targetId, { doNotSelect = false, doNotSwitchPage = false, doNotPushHistory = false } = {}) {
654
+ const isBatch = Array.isArray(config);
655
+ const configs = (isBatch ? config : [config]).filter((item) => !(isPage(item) || isPageFragment(item)));
656
+ if (configs.length === 0) throw new Error("没有可移动的节点");
625
657
  this.captureSelectionBeforeOp();
626
- const root = this.get("root");
627
- const { node, parent, page: pageForOp } = this.getNodeInfo(config.id, false);
628
658
  const target = this.getNodeById(targetId, false);
659
+ if (!target) throw new Error("目标容器不存在");
660
+ const root = this.get("root");
629
661
  const stage = this.get("stage");
630
- if (root && node && parent && stage) {
631
- const oldSourceParent = cloneDeep(toRaw(parent));
632
- const oldTarget = cloneDeep(toRaw(target));
633
- const index = getNodeIndex(node.id, parent);
634
- parent.items?.splice(index, 1);
635
- await stage.remove({
636
- id: node.id,
637
- parentId: parent.id,
638
- root: cloneDeep(root)
639
- });
640
- const layout = await this.getLayout(target);
641
- const newConfig = mergeWith(cloneDeep(node), config, (_objValue, srcValue) => {
642
- if (Array.isArray(srcValue)) return srcValue;
643
- });
644
- newConfig.style = getInitPositionStyle(newConfig.style, layout);
645
- target.items.push(newConfig);
646
- const targetWouldSwitchPage = this.isOnDifferentPage(target);
647
- const skipSelect = doNotSelect || doNotSwitchPage && targetWouldSwitchPage;
648
- if (!skipSelect) await stage.select(targetId);
649
- const targetParent = this.getParentById(target.id);
650
- await stage.update({
651
- config: cloneDeep(target),
652
- parentId: targetParent?.id,
653
- root: cloneDeep(root)
654
- });
655
- if (!skipSelect) {
656
- await this.select(newConfig);
657
- stage.select(newConfig.id);
658
- }
659
- this.addModifiedNodeId(target.id);
660
- this.addModifiedNodeId(parent.id);
661
- this.pushOpHistory("update", { updatedItems: [{
662
- oldNode: oldSourceParent,
663
- newNode: cloneDeep(toRaw(parent))
664
- }, {
665
- oldNode: oldTarget,
666
- newNode: cloneDeep(toRaw(target))
667
- }] }, {
668
- name: pageForOp?.name || "",
669
- id: pageForOp.id
662
+ if (!root || !stage) throw new Error("root stage为空");
663
+ const moves = [];
664
+ for (const { id } of configs) {
665
+ const { node, parent, page } = this.getNodeInfo(id, false);
666
+ if (!node || !parent) continue;
667
+ moves.push({
668
+ node,
669
+ parent,
670
+ pageForOp: page ? {
671
+ name: page.name || "",
672
+ id: page.id
673
+ } : null
670
674
  });
671
- return newConfig;
672
675
  }
676
+ if (moves.length === 0) throw new Error("没有可移动的节点");
677
+ const beforeSnapshots = /* @__PURE__ */ new Map();
678
+ beforeSnapshots.set(target.id, cloneDeep$1(toRaw(target)));
679
+ for (const { parent } of moves) if (!beforeSnapshots.has(parent.id)) beforeSnapshots.set(parent.id, cloneDeep$1(toRaw(parent)));
680
+ let newConfigs = [];
681
+ const moveNodes = moves.map(({ node }) => node);
682
+ await this.remove(moveNodes, {
683
+ doNotPushHistory: true,
684
+ doNotSelect,
685
+ doNotSwitchPage: true
686
+ });
687
+ newConfigs = await this.add(moveNodes, target, {
688
+ doNotPushHistory: true,
689
+ doNotSelect,
690
+ doNotSwitchPage
691
+ });
692
+ if (!doNotPushHistory) {
693
+ const updatedItems = Array.from(beforeSnapshots.entries()).map(([id, oldNode]) => ({
694
+ oldNode,
695
+ newNode: cloneDeep$1(toRaw(this.getNodeById(id, false)))
696
+ }));
697
+ const historyPage = moves[0].pageForOp ?? {
698
+ name: "",
699
+ id: target.id
700
+ };
701
+ this.pushOpHistory("update", { updatedItems }, historyPage);
702
+ } else this.selectionBeforeOp = null;
703
+ return isBatch ? newConfigs : newConfigs[0];
673
704
  }
674
- async dragTo(config, targetParent, targetIndex) {
705
+ async dragTo(config, targetParent, targetIndex, { doNotPushHistory = false } = {}) {
675
706
  this.captureSelectionBeforeOp();
676
707
  if (!targetParent || !Array.isArray(targetParent.items)) return;
677
708
  const configs = Array.isArray(config) ? config : [config];
678
709
  const beforeSnapshots = /* @__PURE__ */ new Map();
679
710
  for (const cfg of configs) {
680
711
  const { parent } = this.getNodeInfo(cfg.id, false);
681
- if (parent && !beforeSnapshots.has(`${parent.id}`)) beforeSnapshots.set(`${parent.id}`, cloneDeep(toRaw(parent)));
712
+ if (parent && !beforeSnapshots.has(`${parent.id}`)) beforeSnapshots.set(`${parent.id}`, cloneDeep$1(toRaw(parent)));
682
713
  }
683
- if (!beforeSnapshots.has(`${targetParent.id}`)) beforeSnapshots.set(`${targetParent.id}`, cloneDeep(toRaw(targetParent)));
714
+ if (!beforeSnapshots.has(`${targetParent.id}`)) beforeSnapshots.set(`${targetParent.id}`, cloneDeep$1(toRaw(targetParent)));
684
715
  const newLayout = await this.getLayout(targetParent);
685
716
  const { sameParentIndices, crossParentConfigs, aborted } = classifyDragSources(configs, targetParent, (id, raw) => this.getNodeInfo(id, raw));
686
717
  if (aborted) return;
@@ -699,23 +730,25 @@ var Editor = class extends BaseService_default {
699
730
  const root = this.get("root");
700
731
  const stage = this.get("stage");
701
732
  if (stage && page && root) stage.update({
702
- config: cloneDeep(page),
733
+ config: cloneDeep$1(page),
703
734
  parentId: root.id,
704
- root: cloneDeep(root)
735
+ root: cloneDeep$1(root)
705
736
  });
706
737
  const updatedItems = [];
707
738
  for (const oldNode of beforeSnapshots.values()) {
708
739
  const newNode = this.getNodeById(oldNode.id, false);
709
740
  if (newNode) updatedItems.push({
710
741
  oldNode,
711
- newNode: cloneDeep(toRaw(newNode))
742
+ newNode: cloneDeep$1(toRaw(newNode))
712
743
  });
713
744
  }
714
- const pageForOp = this.getNodeInfo(configs[0].id, false).page;
715
- this.pushOpHistory("update", { updatedItems }, {
716
- name: pageForOp?.name || "",
717
- id: pageForOp.id
718
- });
745
+ if (!doNotPushHistory) {
746
+ const pageForOp = this.getNodeInfo(configs[0].id, false).page;
747
+ this.pushOpHistory("update", { updatedItems }, {
748
+ name: pageForOp?.name || "",
749
+ id: pageForOp.id
750
+ });
751
+ } else this.selectionBeforeOp = null;
719
752
  this.emit("drag-to", {
720
753
  targetIndex,
721
754
  configs,
@@ -740,7 +773,107 @@ var Editor = class extends BaseService_default {
740
773
  if (value) await this.applyHistoryOp(value, false);
741
774
  return value;
742
775
  }
743
- async move(left, top) {
776
+ /**
777
+ * 「回滚」指定页面历史步骤(类 git revert 语义):
778
+ * - 不动原始历史栈结构(不移动 cursor、不丢弃任何步骤);
779
+ * - 取出 `index` 对应的 step,**反向应用**一次(add→remove / remove→add / update→旧值);
780
+ * - 把这次反向应用作为一条**新步骤**追加到栈顶,可被普通 undo / redo。
781
+ *
782
+ * 与 `gotoPageStep`(类 git reset)的区别在于此操作**不丢弃**目标之后的历史。
783
+ * 与 `applyHistoryOp(reverse=true)` 的区别在于:本方法**不带** `doNotPushHistory`,
784
+ * 反向应用会以一条新 step 入栈;并且不实施 step 中保存的选区与 modifiedNodeIds 状态,
785
+ * 选区由用户当前位置决定,符合"新提交"语义。
786
+ *
787
+ * 仅对处于「已应用」状态的步骤生效——未应用的步骤本身就不存在于当前 DSL 中,反向无意义。
788
+ *
789
+ * @param index 目标 step 在所属页面栈中的索引(0 为最早),通常由历史面板传入
790
+ * @returns 反向后产生的新 step;目标不存在 / 未应用 / 反向失败时返回 null
791
+ */
792
+ async revertPageStep(index) {
793
+ const entry = history_default.getPageStepList()[index];
794
+ if (!entry?.applied) return null;
795
+ const { step } = entry;
796
+ if (!this.get("root")) return null;
797
+ let revertedStep = null;
798
+ const captureRevert = (s) => {
799
+ revertedStep = s;
800
+ };
801
+ history_default.once("change", captureRevert);
802
+ const historyDescription = `回滚 #${index + 1}: ${describeStepForRevert(step)}`;
803
+ const opts = {
804
+ doNotSelect: true,
805
+ doNotSwitchPage: true,
806
+ historyDescription
807
+ };
808
+ try {
809
+ switch (step.opType) {
810
+ case "add": {
811
+ const nodes = step.nodes ?? [];
812
+ for (const n of nodes) {
813
+ const existing = this.getNodeById(n.id, false);
814
+ if (existing) await this.remove(existing, opts);
815
+ }
816
+ break;
817
+ }
818
+ case "remove": {
819
+ const sorted = [...step.removedItems ?? []].sort((a, b) => a.index - b.index);
820
+ for (const { node, parentId } of sorted) {
821
+ const parent = this.getNodeById(parentId, false);
822
+ if (parent) await this.add([cloneDeep$1(node)], parent, opts);
823
+ }
824
+ break;
825
+ }
826
+ case "update": {
827
+ const configs = (step.updatedItems ?? []).map(({ oldNode, newNode, changeRecords }) => {
828
+ if (changeRecords?.length) {
829
+ const patch = {
830
+ id: newNode.id,
831
+ type: newNode.type
832
+ };
833
+ for (const record of changeRecords) {
834
+ if (!record.propPath) return cloneDeep$1(oldNode);
835
+ const value = cloneDeep$1(getValueByKeyPath(record.propPath, oldNode));
836
+ setValueByKeyPath(record.propPath, value, patch);
837
+ }
838
+ return patch;
839
+ }
840
+ return cloneDeep$1(oldNode);
841
+ });
842
+ if (configs.length) await this.update(configs, { historyDescription });
843
+ break;
844
+ }
845
+ }
846
+ } finally {
847
+ history_default.off("change", captureRevert);
848
+ }
849
+ const page = toRaw(this.get("page"));
850
+ if (page) this.emit("history-change", page);
851
+ return revertedStep;
852
+ }
853
+ /**
854
+ * 跳转当前页面历史栈到指定游标位置。
855
+ *
856
+ * `targetCursor` 与 `UndoRedo.getCursor()` 同义:表示"已应用步骤数量",
857
+ * 取值范围 `[0, length]`。当目标 < 当前游标时循环 undo,否则循环 redo。
858
+ * 通常由历史面板传入「点击的 step.index + 1」作为目标。
859
+ *
860
+ * @returns 实际移动到的最终游标位置
861
+ */
862
+ async gotoPageStep(targetCursor) {
863
+ let cursor = history_default.getPageCursor();
864
+ const { length } = history_default.getPageStepList();
865
+ const target = Math.max(0, Math.min(targetCursor, length));
866
+ while (cursor > target) {
867
+ if (!await this.undo()) break;
868
+ cursor -= 1;
869
+ }
870
+ while (cursor < target) {
871
+ if (!await this.redo()) break;
872
+ cursor += 1;
873
+ }
874
+ return cursor;
875
+ }
876
+ async move(left, top, { doNotPushHistory = false } = {}) {
744
877
  const node = toRaw(this.get("node"));
745
878
  if (!node || isPage(node)) return;
746
879
  const newStyle = calcMoveStyle(node.style || {}, left, top);
@@ -749,7 +882,7 @@ var Editor = class extends BaseService_default {
749
882
  id: node.id,
750
883
  type: node.type,
751
884
  style: newStyle
752
- });
885
+ }, { doNotPushHistory });
753
886
  }
754
887
  resetState() {
755
888
  this.set("root", null);
@@ -783,17 +916,13 @@ var Editor = class extends BaseService_default {
783
916
  return super.emit(eventName, ...args);
784
917
  }
785
918
  addModifiedNodeId(id) {
786
- if (!this.isHistoryStateChange) this.get("modifiedNodeIds").set(id, id);
919
+ this.get("modifiedNodeIds").set(id, id);
787
920
  }
788
921
  captureSelectionBeforeOp() {
789
- if (this.isHistoryStateChange || this.selectionBeforeOp) return;
922
+ if (this.selectionBeforeOp) return;
790
923
  this.selectionBeforeOp = this.get("nodes").map((n) => n.id);
791
924
  }
792
- pushOpHistory(opType, extra, pageData) {
793
- if (this.isHistoryStateChange) {
794
- this.selectionBeforeOp = null;
795
- return;
796
- }
925
+ pushOpHistory(opType, extra, pageData, historyDescription) {
797
926
  const step = {
798
927
  data: pageData,
799
928
  opType,
@@ -802,39 +931,101 @@ var Editor = class extends BaseService_default {
802
931
  modifiedNodeIds: new Map(this.get("modifiedNodeIds")),
803
932
  ...extra
804
933
  };
805
- history_default.push(step);
934
+ if (historyDescription) step.historyDescription = historyDescription;
935
+ history_default.push(step, pageData.id);
806
936
  this.selectionBeforeOp = null;
807
- this.isHistoryStateChange = false;
808
937
  }
809
938
  /**
810
939
  * 应用历史操作(撤销 / 重做)
940
+ *
941
+ * 所有 DSL 修改都走 `editor.add / remove / update`,并通过 `doNotPushHistory` 阻止再次入栈、
942
+ * `doNotSelect / doNotSwitchPage` 让选区由方法末尾的统一逻辑兜底。
943
+ *
944
+ * 注意:这些公开方法会发出 add / remove / update 事件,业务侧若需要区分"用户操作"与"撤销重做触发",
945
+ * 请监听 `history-change` 事件配合判断。
946
+ *
811
947
  * @param step 操作记录
812
948
  * @param reverse true = 撤销,false = 重做
813
949
  */
814
950
  async applyHistoryOp(step, reverse) {
815
- this.isHistoryStateChange = true;
816
951
  const root = this.get("root");
817
952
  const stage = this.get("stage");
818
953
  if (!root) return;
819
- const ctx = {
820
- root,
821
- stage,
822
- getNodeById: (id, raw) => this.getNodeById(id, raw),
823
- getNodeInfo: (id, raw) => this.getNodeInfo(id, raw),
824
- setRoot: (r) => this.set("root", r),
825
- setPage: (p) => this.set("page", p),
826
- getPage: () => this.get("page")
954
+ const commonOpts = {
955
+ doNotSelect: true,
956
+ doNotSwitchPage: true,
957
+ doNotPushHistory: true
827
958
  };
828
959
  switch (step.opType) {
829
- case "add":
830
- await applyHistoryAddOp(step, reverse, ctx);
960
+ case "add": {
961
+ const nodes = step.nodes ?? [];
962
+ if (reverse) for (const n of nodes) {
963
+ const existing = this.getNodeById(n.id, false);
964
+ if (existing) await this.remove(existing, commonOpts);
965
+ }
966
+ else {
967
+ const parent = this.getNodeById(step.parentId, false);
968
+ if (parent) {
969
+ const sorted = [...nodes].sort((a, b) => (step.indexMap?.[a.id] ?? 0) - (step.indexMap?.[b.id] ?? 0));
970
+ for (const n of sorted) {
971
+ const idx = step.indexMap?.[n.id];
972
+ if (parent.items) {
973
+ if (typeof idx === "number" && idx >= 0 && idx < parent.items.length) parent.items.splice(idx, 0, cloneDeep$1(n));
974
+ else parent.items.push(cloneDeep$1(n));
975
+ await stage?.add({
976
+ config: cloneDeep$1(n),
977
+ parent: cloneDeep$1(parent),
978
+ parentId: parent.id,
979
+ root: cloneDeep$1(root)
980
+ });
981
+ }
982
+ }
983
+ }
984
+ }
831
985
  break;
832
- case "remove":
833
- await applyHistoryRemoveOp(step, reverse, ctx);
986
+ }
987
+ case "remove": {
988
+ const items = step.removedItems ?? [];
989
+ if (reverse) {
990
+ const sorted = [...items].sort((a, b) => a.index - b.index);
991
+ for (const { node, parentId, index } of sorted) {
992
+ const parent = this.getNodeById(parentId, false);
993
+ if (parent?.items) {
994
+ parent.items.splice(index, 0, cloneDeep$1(node));
995
+ await stage?.add({
996
+ config: cloneDeep$1(node),
997
+ parent: cloneDeep$1(parent),
998
+ parentId,
999
+ root: cloneDeep$1(root)
1000
+ });
1001
+ }
1002
+ }
1003
+ } else for (const { node } of items) {
1004
+ const existing = this.getNodeById(node.id, false);
1005
+ if (existing) await this.remove(existing, commonOpts);
1006
+ }
834
1007
  break;
835
- case "update":
836
- await applyHistoryUpdateOp(step, reverse, ctx);
1008
+ }
1009
+ case "update": {
1010
+ const configs = (step.updatedItems ?? []).map(({ oldNode, newNode, changeRecords }) => {
1011
+ if (changeRecords?.length) {
1012
+ const sourceForValues = reverse ? oldNode : newNode;
1013
+ const patch = {
1014
+ id: newNode.id,
1015
+ type: newNode.type
1016
+ };
1017
+ for (const record of changeRecords) {
1018
+ if (!record.propPath) return cloneDeep$1(sourceForValues);
1019
+ const value = cloneDeep$1(getValueByKeyPath(record.propPath, sourceForValues));
1020
+ setValueByKeyPath(record.propPath, value, patch);
1021
+ }
1022
+ return patch;
1023
+ }
1024
+ return cloneDeep$1(reverse ? oldNode : newNode);
1025
+ });
1026
+ if (configs.length) await this.update(configs, { doNotPushHistory: true });
837
1027
  break;
1028
+ }
838
1029
  }
839
1030
  this.set("modifiedNodeIds", step.modifiedNodeIds);
840
1031
  const page = toRaw(this.get("page"));
@@ -849,7 +1040,6 @@ var Editor = class extends BaseService_default {
849
1040
  }, 0);
850
1041
  this.emit("history-change", page);
851
1042
  }
852
- this.isHistoryStateChange = false;
853
1043
  }
854
1044
  selectedConfigExceptionHandler(config) {
855
1045
  return resolveSelectedNode(config, (id) => this.getNodeInfo(id), this.state.root?.id);