@tmagic/editor 1.8.0-beta.1 → 1.8.0-beta.10

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 (232) hide show
  1. package/dist/es/Editor.vue_vue_type_script_setup_true_lang.js +31 -5
  2. package/dist/es/components/CodeBlockEditor.vue_vue_type_script_setup_true_lang.js +21 -113
  3. package/dist/es/components/CodeParams.vue_vue_type_script_setup_true_lang.js +7 -1
  4. package/dist/es/components/CompareForm.js +5 -0
  5. package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +196 -0
  6. package/dist/es/components/ContentMenu.vue_vue_type_script_setup_true_lang.js +1 -1
  7. package/dist/es/components/FloatingBox.vue_vue_type_script_setup_true_lang.js +2 -2
  8. package/dist/es/components/Icon.vue_vue_type_script_setup_true_lang.js +1 -1
  9. package/dist/es/components/Resizer.vue_vue_type_script_setup_true_lang.js +1 -1
  10. package/dist/es/components/ScrollBar.vue_vue_type_script_setup_true_lang.js +1 -1
  11. package/dist/es/components/ScrollViewer.vue_vue_type_script_setup_true_lang.js +1 -1
  12. package/dist/es/components/SearchInput.vue_vue_type_script_setup_true_lang.js +1 -1
  13. package/dist/es/components/SplitView.vue_vue_type_script_setup_true_lang.js +1 -1
  14. package/dist/es/components/ToolButton.vue_vue_type_script_setup_true_lang.js +7 -7
  15. package/dist/es/components/Tree.vue_vue_type_script_setup_true_lang.js +1 -1
  16. package/dist/es/components/TreeNode.vue_vue_type_script_setup_true_lang.js +1 -1
  17. package/dist/es/editorProps.js +2 -0
  18. package/dist/es/fields/Code.vue_vue_type_script_setup_true_lang.js +20 -4
  19. package/dist/es/fields/CodeLink.vue_vue_type_script_setup_true_lang.js +3 -6
  20. package/dist/es/fields/CodeSelect.vue_vue_type_script_setup_true_lang.js +19 -3
  21. package/dist/es/fields/CodeSelectCol.vue_vue_type_script_setup_true_lang.js +42 -8
  22. package/dist/es/fields/CondOpSelect.vue_vue_type_script_setup_true_lang.js +1 -1
  23. package/dist/es/fields/DataSourceFieldSelect/FieldSelect.vue_vue_type_script_setup_true_lang.js +14 -6
  24. package/dist/es/fields/DataSourceFieldSelect/Index.vue_vue_type_script_setup_true_lang.js +3 -3
  25. package/dist/es/fields/DataSourceFields.vue_vue_type_script_setup_true_lang.js +41 -16
  26. package/dist/es/fields/DataSourceInput.vue_vue_type_script_setup_true_lang.js +2 -2
  27. package/dist/es/fields/DataSourceMethodSelect.vue_vue_type_script_setup_true_name_true_lang.js +9 -5
  28. package/dist/es/fields/DataSourceMethods.vue_vue_type_script_setup_true_lang.js +40 -19
  29. package/dist/es/fields/DataSourceMocks.vue_vue_type_script_setup_true_lang.js +17 -9
  30. package/dist/es/fields/DataSourceSelect.vue_vue_type_script_setup_true_lang.js +4 -2
  31. package/dist/es/fields/DisplayConds.vue_vue_type_script_setup_true_lang.js +1 -1
  32. package/dist/es/fields/EventSelect.vue_vue_type_script_setup_true_lang.js +59 -12
  33. package/dist/es/fields/KeyValue.vue_vue_type_script_setup_true_lang.js +12 -7
  34. package/dist/es/fields/PageFragmentSelect.vue_vue_type_script_setup_true_lang.js +1 -1
  35. package/dist/es/fields/StyleSetter/Index.vue_vue_type_script_setup_true_lang.js +1 -1
  36. package/dist/es/fields/StyleSetter/components/BackgroundPosition.vue_vue_type_script_setup_true_lang.js +1 -1
  37. package/dist/es/fields/StyleSetter/components/Border.vue_vue_type_script_setup_true_lang.js +28 -6
  38. package/dist/es/fields/StyleSetter/components/Box.vue_vue_type_script_setup_true_lang.js +1 -1
  39. package/dist/es/fields/StyleSetter/icons/background-repeat/NoRepeat.js +1 -1
  40. package/dist/es/fields/StyleSetter/icons/background-repeat/Repeat.js +1 -1
  41. package/dist/es/fields/StyleSetter/icons/background-repeat/RepeatX.js +1 -1
  42. package/dist/es/fields/StyleSetter/icons/background-repeat/RepeatY.js +1 -1
  43. package/dist/es/fields/StyleSetter/icons/display/Block.js +1 -1
  44. package/dist/es/fields/StyleSetter/icons/display/Flex.js +1 -1
  45. package/dist/es/fields/StyleSetter/icons/display/Inline.js +1 -1
  46. package/dist/es/fields/StyleSetter/icons/display/InlineBlock.js +1 -1
  47. package/dist/es/fields/StyleSetter/icons/display/None.js +1 -1
  48. package/dist/es/fields/StyleSetter/icons/flex-direction/Column.js +1 -1
  49. package/dist/es/fields/StyleSetter/icons/flex-direction/ColumnReverse.js +1 -1
  50. package/dist/es/fields/StyleSetter/icons/flex-direction/Row.js +1 -1
  51. package/dist/es/fields/StyleSetter/icons/flex-direction/RowReverse.js +1 -1
  52. package/dist/es/fields/StyleSetter/icons/justify-content/Center.js +1 -1
  53. package/dist/es/fields/StyleSetter/icons/justify-content/FlexEnd.js +1 -1
  54. package/dist/es/fields/StyleSetter/icons/justify-content/FlexStart.js +1 -1
  55. package/dist/es/fields/StyleSetter/icons/justify-content/SpaceAround.js +1 -1
  56. package/dist/es/fields/StyleSetter/icons/justify-content/SpaceBetween.js +1 -1
  57. package/dist/es/fields/StyleSetter/icons/text-align/Center.js +1 -1
  58. package/dist/es/fields/StyleSetter/icons/text-align/Left.js +1 -1
  59. package/dist/es/fields/StyleSetter/icons/text-align/Right.js +1 -1
  60. package/dist/es/fields/StyleSetter/pro/Background.vue_vue_type_script_setup_true_lang.js +1 -1
  61. package/dist/es/fields/StyleSetter/pro/Border.vue_vue_type_script_setup_true_lang.js +1 -1
  62. package/dist/es/fields/StyleSetter/pro/Font.vue_vue_type_script_setup_true_lang.js +1 -1
  63. package/dist/es/fields/StyleSetter/pro/Layout.vue_vue_type_script_setup_true_lang.js +1 -1
  64. package/dist/es/fields/StyleSetter/pro/Position.vue_vue_type_script_setup_true_lang.js +1 -1
  65. package/dist/es/fields/StyleSetter/pro/Transform.vue_vue_type_script_setup_true_lang.js +1 -1
  66. package/dist/es/fields/UISelect.vue_vue_type_script_setup_true_lang.js +9 -6
  67. package/dist/es/hooks/use-code-block-edit.js +7 -4
  68. package/dist/es/hooks/use-data-source-edit.js +5 -2
  69. package/dist/es/hooks/use-stage.js +22 -10
  70. package/dist/es/icons/CenterIcon.vue_vue_type_script_setup_true_lang.js +1 -1
  71. package/dist/es/icons/CodeIcon.vue_vue_type_script_setup_true_lang.js +1 -1
  72. package/dist/es/icons/FolderMinusIcon.vue_vue_type_script_setup_true_lang.js +1 -1
  73. package/dist/es/index.js +14 -3
  74. package/dist/es/initService.js +7 -7
  75. package/dist/es/layouts/AddPageBox.vue_vue_type_script_setup_true_lang.js +1 -1
  76. package/dist/es/layouts/CodeEditor.vue_vue_type_script_setup_true_lang.js +7 -6
  77. package/dist/es/layouts/Framework.vue_vue_type_script_setup_true_lang.js +14 -9
  78. package/dist/es/layouts/NavMenu.vue_vue_type_script_setup_true_lang.js +12 -4
  79. package/dist/es/layouts/NavMenuColumn.vue_vue_type_script_setup_true_lang.js +1 -1
  80. package/dist/es/layouts/history-list/Bucket.js +5 -0
  81. package/dist/es/layouts/history-list/Bucket.vue_vue_type_script_setup_true_lang.js +71 -0
  82. package/dist/es/layouts/history-list/BucketTab.js +5 -0
  83. package/dist/es/layouts/history-list/BucketTab.vue_vue_type_script_setup_true_lang.js +62 -0
  84. package/dist/es/layouts/history-list/GroupRow.js +5 -0
  85. package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +269 -0
  86. package/dist/es/layouts/history-list/HistoryDiffDialog.js +5 -0
  87. package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +287 -0
  88. package/dist/es/layouts/history-list/HistoryListPanel.js +5 -0
  89. package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +327 -0
  90. package/dist/es/layouts/history-list/InitialRow.js +5 -0
  91. package/dist/es/layouts/history-list/InitialRow.vue_vue_type_script_setup_true_lang.js +83 -0
  92. package/dist/es/layouts/history-list/PageTab.js +5 -0
  93. package/dist/es/layouts/history-list/PageTab.vue_vue_type_script_setup_true_lang.js +95 -0
  94. package/dist/es/layouts/history-list/composables.js +184 -0
  95. package/dist/es/layouts/history-list/useHistoryList.js +56 -0
  96. package/dist/es/layouts/history-list/useHistoryRevert.js +306 -0
  97. package/dist/es/layouts/page-bar/AddButton.vue_vue_type_script_setup_true_lang.js +1 -1
  98. package/dist/es/layouts/page-bar/PageBar.vue_vue_type_script_setup_true_lang.js +5 -3
  99. package/dist/es/layouts/page-bar/PageBarScrollContainer.vue_vue_type_script_setup_true_lang.js +1 -1
  100. package/dist/es/layouts/page-bar/PageList.vue_vue_type_script_setup_true_lang.js +1 -1
  101. package/dist/es/layouts/page-bar/Search.vue_vue_type_script_setup_true_lang.js +2 -2
  102. package/dist/es/layouts/props-panel/FormPanel.vue_vue_type_script_setup_true_lang.js +1 -1
  103. package/dist/es/layouts/props-panel/PropsPanel.vue_vue_type_script_setup_true_lang.js +6 -2
  104. package/dist/es/layouts/sidebar/ComponentListPanel.vue_vue_type_script_setup_true_lang.js +2 -2
  105. package/dist/es/layouts/sidebar/Sidebar.vue_vue_type_script_setup_true_lang.js +8 -4
  106. package/dist/es/layouts/sidebar/code-block/CodeBlockList.vue_vue_type_script_setup_true_lang.js +4 -4
  107. package/dist/es/layouts/sidebar/code-block/CodeBlockListPanel.vue_vue_type_script_setup_true_lang.js +2 -2
  108. package/dist/es/layouts/sidebar/code-block/useContentMenu.js +1 -1
  109. package/dist/es/layouts/sidebar/data-source/DataSourceAddButton.vue_vue_type_script_setup_true_lang.js +1 -1
  110. package/dist/es/layouts/sidebar/data-source/DataSourceConfigPanel.vue_vue_type_script_setup_true_lang.js +19 -6
  111. package/dist/es/layouts/sidebar/data-source/DataSourceList.vue_vue_type_script_setup_true_lang.js +1 -1
  112. package/dist/es/layouts/sidebar/data-source/DataSourceListPanel.vue_vue_type_script_setup_true_lang.js +25 -3
  113. package/dist/es/layouts/sidebar/data-source/useContentMenu.js +1 -1
  114. package/dist/es/layouts/sidebar/layer/LayerMenu.vue_vue_type_script_setup_true_lang.js +6 -6
  115. package/dist/es/layouts/sidebar/layer/LayerNodeTool.vue_vue_type_script_setup_true_lang.js +2 -2
  116. package/dist/es/layouts/sidebar/layer/LayerPanel.vue_vue_type_script_setup_true_lang.js +1 -1
  117. package/dist/es/layouts/sidebar/layer/use-node-status.js +1 -1
  118. package/dist/es/layouts/workspace/Breadcrumb.vue_vue_type_script_setup_true_lang.js +1 -1
  119. package/dist/es/layouts/workspace/Workspace.vue_vue_type_script_setup_true_lang.js +1 -1
  120. package/dist/es/layouts/workspace/viewer/NodeListMenu.vue_vue_type_script_setup_true_lang.js +1 -1
  121. package/dist/es/layouts/workspace/viewer/Stage.vue_vue_type_script_setup_true_lang.js +2 -2
  122. package/dist/es/layouts/workspace/viewer/StageOverlay.vue_vue_type_script_setup_true_lang.js +1 -1
  123. package/dist/es/layouts/workspace/viewer/ViewerMenu.vue_vue_type_script_setup_true_lang.js +9 -9
  124. package/dist/es/services/BaseService.js +31 -68
  125. package/dist/es/services/codeBlock.js +286 -8
  126. package/dist/es/services/componentList.js +2 -2
  127. package/dist/es/services/dataSource.js +287 -8
  128. package/dist/es/services/dep.js +21 -9
  129. package/dist/es/services/editor.js +655 -194
  130. package/dist/es/services/events.js +2 -2
  131. package/dist/es/services/history.js +390 -47
  132. package/dist/es/services/keybinding.js +5 -5
  133. package/dist/es/services/props.js +8 -8
  134. package/dist/es/services/stageOverlay.js +2 -2
  135. package/dist/es/services/storage.js +2 -2
  136. package/dist/es/services/ui.js +5 -2
  137. package/dist/es/style.css +456 -8
  138. package/dist/es/utils/code-block.js +122 -0
  139. package/dist/es/utils/content-menu.js +19 -12
  140. package/dist/es/utils/data-source/index.js +2 -0
  141. package/dist/es/utils/dep/worker.js +1 -1
  142. package/dist/es/utils/editor.js +105 -51
  143. package/dist/es/utils/history.js +220 -0
  144. package/dist/es/utils/indexed-db.js +86 -0
  145. package/dist/es/utils/undo-redo.js +80 -1
  146. package/dist/style.css +456 -8
  147. package/dist/tmagic-editor.umd.cjs +10135 -5646
  148. package/package.json +8 -8
  149. package/src/Editor.vue +25 -1
  150. package/src/components/CodeBlockEditor.vue +20 -109
  151. package/src/components/CodeParams.vue +6 -0
  152. package/src/components/CompareForm.vue +277 -0
  153. package/src/components/ToolButton.vue +2 -2
  154. package/src/editorProps.ts +9 -0
  155. package/src/fields/Code.vue +19 -3
  156. package/src/fields/CodeLink.vue +2 -5
  157. package/src/fields/CodeSelect.vue +17 -1
  158. package/src/fields/CodeSelectCol.vue +37 -3
  159. package/src/fields/DataSourceFieldSelect/FieldSelect.vue +23 -3
  160. package/src/fields/DataSourceFieldSelect/Index.vue +2 -2
  161. package/src/fields/DataSourceFields.vue +48 -10
  162. package/src/fields/DataSourceInput.vue +3 -3
  163. package/src/fields/DataSourceMethodSelect.vue +12 -4
  164. package/src/fields/DataSourceMethods.vue +55 -24
  165. package/src/fields/DataSourceMocks.vue +13 -3
  166. package/src/fields/DataSourceSelect.vue +4 -1
  167. package/src/fields/EventSelect.vue +57 -8
  168. package/src/fields/KeyValue.vue +17 -4
  169. package/src/fields/StyleSetter/components/Border.vue +15 -6
  170. package/src/fields/UISelect.vue +5 -2
  171. package/src/hooks/use-code-block-edit.ts +9 -5
  172. package/src/hooks/use-data-source-edit.ts +2 -2
  173. package/src/hooks/use-stage.ts +26 -9
  174. package/src/index.ts +8 -0
  175. package/src/initService.ts +1 -1
  176. package/src/layouts/CodeEditor.vue +17 -5
  177. package/src/layouts/Framework.vue +11 -6
  178. package/src/layouts/NavMenu.vue +12 -3
  179. package/src/layouts/history-list/Bucket.vue +91 -0
  180. package/src/layouts/history-list/BucketTab.vue +72 -0
  181. package/src/layouts/history-list/GroupRow.vue +304 -0
  182. package/src/layouts/history-list/HistoryDiffDialog.vue +273 -0
  183. package/src/layouts/history-list/HistoryListPanel.vue +367 -0
  184. package/src/layouts/history-list/InitialRow.vue +71 -0
  185. package/src/layouts/history-list/PageTab.vue +118 -0
  186. package/src/layouts/history-list/composables.ts +309 -0
  187. package/src/layouts/history-list/useHistoryList.ts +60 -0
  188. package/src/layouts/history-list/useHistoryRevert.ts +405 -0
  189. package/src/layouts/page-bar/PageBar.vue +4 -2
  190. package/src/layouts/props-panel/PropsPanel.vue +5 -1
  191. package/src/layouts/sidebar/ComponentListPanel.vue +9 -5
  192. package/src/layouts/sidebar/Sidebar.vue +8 -3
  193. package/src/layouts/sidebar/code-block/CodeBlockList.vue +5 -5
  194. package/src/layouts/sidebar/code-block/CodeBlockListPanel.vue +1 -1
  195. package/src/layouts/sidebar/code-block/useContentMenu.ts +1 -1
  196. package/src/layouts/sidebar/data-source/DataSourceConfigPanel.vue +31 -2
  197. package/src/layouts/sidebar/data-source/DataSourceListPanel.vue +27 -2
  198. package/src/layouts/sidebar/data-source/useContentMenu.ts +1 -1
  199. package/src/layouts/sidebar/layer/LayerMenu.vue +19 -11
  200. package/src/layouts/sidebar/layer/LayerNodeTool.vue +7 -4
  201. package/src/layouts/sidebar/layer/use-node-status.ts +9 -6
  202. package/src/layouts/workspace/viewer/Stage.vue +1 -1
  203. package/src/layouts/workspace/viewer/ViewerMenu.vue +8 -8
  204. package/src/services/BaseService.ts +35 -86
  205. package/src/services/codeBlock.ts +361 -5
  206. package/src/services/dataSource.ts +336 -6
  207. package/src/services/dep.ts +32 -9
  208. package/src/services/editor.ts +834 -194
  209. package/src/services/history.ts +509 -49
  210. package/src/services/keybinding.ts +3 -3
  211. package/src/services/props.ts +2 -2
  212. package/src/services/ui.ts +7 -0
  213. package/src/theme/history-list-panel.scss +557 -0
  214. package/src/theme/page-bar.scss +0 -4
  215. package/src/theme/props-panel.scss +3 -3
  216. package/src/theme/style-setter/border.scss +4 -1
  217. package/src/theme/theme.scss +1 -0
  218. package/src/type.ts +594 -31
  219. package/src/utils/code-block.ts +150 -0
  220. package/src/utils/content-menu.ts +25 -13
  221. package/src/utils/data-source/index.ts +2 -0
  222. package/src/utils/dep/worker.ts +5 -3
  223. package/src/utils/editor.ts +166 -60
  224. package/src/utils/history.ts +286 -0
  225. package/src/utils/index.ts +3 -0
  226. package/src/utils/indexed-db.ts +122 -0
  227. package/src/utils/undo-redo.ts +111 -0
  228. package/types/index.d.ts +3171 -889
  229. package/dist/es/utils/compose.js +0 -44
  230. package/dist/es/utils/editor-history.js +0 -100
  231. package/src/utils/compose.ts +0 -52
  232. package/src/utils/editor-history.ts +0 -138
@@ -1,35 +1,28 @@
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
+ import { getLastPushedHistoryIds } from "../utils/history.js";
4
5
  import history_default from "./history.js";
5
6
  import storage_default, { Protocol } from "./storage.js";
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";
7
+ import { COPY_STORAGE_KEY, calcAlignCenterStyle, calcLayerTargetIndex, calcMoveStyle, classifyDragSources, collectRelatedNodes, describeStepForRevert, editorNodeMergeCustomizer, fixNodePosition, getInitPositionStyle, getNodeIndex, getPageFragmentList, getPageList, moveItemsInContainer, resolveSelectedNode, setChildrenLayout, setLayout, toggleFixedPosition } from "../utils/editor.js";
8
8
  import { beforePaste, getAddParent } from "../utils/operator.js";
9
9
  import { isFixed } from "@tmagic/stage";
10
- import { getNodeInfo, getNodePath, isPage, isPageFragment } from "@tmagic/utils";
10
+ import { getNodeInfo, getNodePath, getValueByKeyPath, guid, isPage, isPageFragment, setValueByKeyPath } from "@tmagic/utils";
11
11
  import { NodeType } from "@tmagic/core";
12
12
  import { reactive, toRaw } from "vue";
13
- import { cloneDeep, isObject, mergeWith, uniq } from "lodash-es";
13
+ import { cloneDeep as cloneDeep$1, isEmpty, isEqual, isObject, mergeWith, uniq } from "lodash-es";
14
14
  //#region packages/editor/src/services/editor.ts
15
15
  /**
16
- * 经过 BaseService 的插件 / 中间件包装后,源方法的最后一个形参可能被注入为 dispatch 函数
17
- * options 形参位置被注入为函数(或为 null)时,将其归一为空对象,避免后续逻辑误读
16
+ * 把「变更前后节点快照」列表归一成 update 类型的 {@link StepDiffItem} 列表,供 {@link StepValue.diff} 使用。
17
+ * `changeRecords` 来自 form 端的 propPath/value 列表,撤销/重做时只对这些 propPath 做局部更新;
18
+ * 缺省(未传 / 空数组)才退化为整节点替换。
18
19
  */
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;
31
- };
32
- var Editor = class extends BaseService_default {
20
+ var buildUpdateDiff = (items) => items.map(({ oldNode, newNode, changeRecords }) => ({
21
+ oldSchema: oldNode,
22
+ newSchema: newNode,
23
+ ...changeRecords?.length ? { changeRecords } : {}
24
+ }));
25
+ var Editor = class extends BaseService {
33
26
  state = reactive({
34
27
  root: null,
35
28
  page: null,
@@ -45,8 +38,13 @@ var Editor = class extends BaseService_default {
45
38
  disabledMultiSelect: false,
46
39
  alwaysMultiSelect: false
47
40
  });
48
- isHistoryStateChange = false;
49
41
  selectionBeforeOp = null;
42
+ /**
43
+ * 最近一次 pushOpHistory 写入的历史记录 uuid。
44
+ * 供 *AndGetHistoryId 系列方法在调用普通操作后取回本次产生的历史记录 id;
45
+ * 普通操作不会读取它,调用前由 *AndGetHistoryId 重置为 null。
46
+ */
47
+ lastPushedHistoryId = null;
50
48
  constructor() {
51
49
  super(canUsePluginMethods.async.map((methodName) => ({
52
50
  name: methodName,
@@ -61,8 +59,9 @@ var Editor = class extends BaseService_default {
61
59
  * 设置当前指点节点配置
62
60
  * @param name 'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'stage' | 'modifiedNodeIds' | 'pageLength' | 'pageFragmentLength
63
61
  * @param value MNode
62
+ * @param options.historySource 设置 root 时,本次变更写入历史记录的「操作来源」(仅 name === 'root' 时生效)
64
63
  */
65
- set(name, value) {
64
+ set(name, value, options = {}) {
66
65
  const preValue = this.state[name];
67
66
  this.state[name] = value;
68
67
  if (name === "nodes" && Array.isArray(value)) this.set("node", value[0]);
@@ -73,12 +72,19 @@ var Editor = class extends BaseService_default {
73
72
  this.state.pageLength = getPageList(app).length || 0;
74
73
  this.state.pageFragmentLength = getPageFragmentList(app).length || 0;
75
74
  this.state.stageLoading = this.state.pageLength !== 0;
75
+ if (preValue && !isEmpty(preValue)) this.pushRootDiffHistory(preValue, app, options.historySource);
76
+ else app.items?.forEach((pageNode) => {
77
+ if (pageNode?.id !== void 0 && !history_default.getMarker("page", pageNode.id)) history_default.setMarker("page", pageNode.id, {
78
+ name: pageNode.name,
79
+ source: options.historySource
80
+ });
81
+ });
76
82
  } else {
77
83
  this.state.pageLength = 0;
78
84
  this.state.pageFragmentLength = 0;
79
85
  this.state.stageLoading = false;
80
86
  }
81
- this.emit("root-change", value, preValue);
87
+ this.emit("root-change", value, preValue, options);
82
88
  }
83
89
  }
84
90
  /**
@@ -98,7 +104,25 @@ var Editor = class extends BaseService_default {
98
104
  getNodeInfo(id, raw = true) {
99
105
  let root = this.get("root");
100
106
  if (raw) root = toRaw(root);
101
- return getNodeInfo(id, root);
107
+ if (!root) return {
108
+ node: null,
109
+ parent: null,
110
+ page: null,
111
+ path: []
112
+ };
113
+ if (id === root.id) return {
114
+ node: root,
115
+ parent: null,
116
+ page: null,
117
+ path: []
118
+ };
119
+ const pageIdStr = `${this.get("page")?.id || ""}`;
120
+ const currentPageNode = root.items?.find((item) => `${item.id}` === pageIdStr);
121
+ if (currentPageNode && `${id}` !== pageIdStr) {
122
+ const info = getNodeInfo(id, currentPageNode);
123
+ if (info.node) return info;
124
+ }
125
+ return getNodeInfo(id, root, currentPageNode);
102
126
  }
103
127
  /**
104
128
  * 根据ID获取指点节点配置
@@ -137,7 +161,7 @@ var Editor = class extends BaseService_default {
137
161
  * 只有容器拥有布局
138
162
  */
139
163
  async getLayout(parent, node) {
140
- if (node && typeof node !== "function" && isFixed(node.style || {})) return Layout.FIXED;
164
+ if (node && isFixed(node.style || {})) return Layout.FIXED;
141
165
  if (parent.layout) return parent.layout;
142
166
  if (!parent.style?.position) return Layout.RELATIVE;
143
167
  return Layout.ABSOLUTE;
@@ -152,8 +176,6 @@ var Editor = class extends BaseService_default {
152
176
  this.set("nodes", node ? [node] : []);
153
177
  this.set("page", page);
154
178
  this.set("parent", parent);
155
- if (page) history_default.changePage(toRaw(page));
156
- else history_default.resetState();
157
179
  if (node?.id) this.get("stage")?.renderer?.runtime?.getApp?.()?.page?.emit("editor:select", {
158
180
  node,
159
181
  page,
@@ -231,18 +253,18 @@ var Editor = class extends BaseService_default {
231
253
  const layout = await this.getLayout(toRaw(parent), node);
232
254
  node.style = getInitPositionStyle(node.style, layout);
233
255
  await stage?.add({
234
- config: cloneDeep(node),
235
- parent: cloneDeep(parent),
256
+ config: cloneDeep$1(node),
257
+ parent: cloneDeep$1(parent),
236
258
  parentId: parent.id,
237
- root: cloneDeep(root)
259
+ root: cloneDeep$1(root)
238
260
  });
239
261
  const newStyle = fixNodePosition(node, parent, stage);
240
262
  if (newStyle && (newStyle.top !== node.style.top || newStyle.left !== node.style.left)) {
241
263
  node.style = newStyle;
242
264
  await stage?.update({
243
- config: cloneDeep(node),
265
+ config: cloneDeep$1(node),
244
266
  parentId: parent.id,
245
- root: cloneDeep(root)
267
+ root: cloneDeep$1(root)
246
268
  });
247
269
  }
248
270
  this.addModifiedNodeId(node.id);
@@ -255,11 +277,10 @@ var Editor = class extends BaseService_default {
255
277
  * @param options 可选配置
256
278
  * @param options.doNotSelect 添加后是否不更新当前选中节点(默认 false,添加后会选中新增的节点)
257
279
  * @param options.doNotSwitchPage 添加后是否不切换当前页面(默认 false;新增页面 / 跨页新增时为 true 会跳过会引发页面切换的选中操作)
280
+ * @param options.doNotPushHistory 是否不写入历史记录(默认 false)
258
281
  * @returns 添加后的节点
259
282
  */
260
- async add(addNode, parent, options) {
261
- const safeParentNode = safeParent(parent);
262
- const { doNotSelect = false, doNotSwitchPage = false } = safeOptions(options);
283
+ async add(addNode, parent, { doNotSelect = false, doNotSwitchPage = false, doNotPushHistory = false, historyDescription, historySource } = {}) {
263
284
  this.captureSelectionBeforeOp();
264
285
  const stage = this.get("stage");
265
286
  const addNodes = [];
@@ -271,7 +292,7 @@ var Editor = class extends BaseService_default {
271
292
  const newNodes = await Promise.all(addNodes.map((node) => {
272
293
  const root = this.get("root");
273
294
  if ((isPage(node) || isPageFragment(node)) && root) return this.doAdd(node, root);
274
- const parentNode = safeParentNode ?? getAddParent(node);
295
+ const parentNode = parent ?? getAddParent(node);
275
296
  if (!parentNode) throw new Error("未找到父元素");
276
297
  return this.doAdd(node, parentNode);
277
298
  }));
@@ -292,23 +313,38 @@ var Editor = class extends BaseService_default {
292
313
  }
293
314
  if (!(isPage(newNodes[0]) || isPageFragment(newNodes[0]))) {
294
315
  const pageForOp = this.getNodeInfo(newNodes[0].id, false).page;
295
- this.pushOpHistory("add", {
296
- nodes: newNodes.map((n) => cloneDeep(toRaw(n))),
297
- parentId: (this.getParentById(newNodes[0].id, false) ?? this.get("root")).id,
298
- indexMap: Object.fromEntries(newNodes.map((n) => {
299
- const p = this.getParentById(n.id, false);
300
- return [n.id, p ? getNodeIndex(n.id, p) : -1];
301
- }))
302
- }, {
303
- name: pageForOp?.name || "",
304
- id: pageForOp.id
305
- });
316
+ if (!doNotPushHistory) {
317
+ const parentId = (this.getParentById(newNodes[0].id, false) ?? this.get("root")).id;
318
+ this.pushOpHistory("add", {
319
+ diff: newNodes.map((n) => {
320
+ const p = this.getParentById(n.id, false);
321
+ const idx = p ? getNodeIndex(n.id, p) : -1;
322
+ return {
323
+ newSchema: cloneDeep$1(toRaw(n)),
324
+ parentId,
325
+ index: typeof idx === "number" ? idx : -1
326
+ };
327
+ }),
328
+ pageData: {
329
+ name: pageForOp?.name || "",
330
+ id: pageForOp.id
331
+ },
332
+ historyDescription,
333
+ source: historySource
334
+ });
335
+ } else this.selectionBeforeOp = null;
306
336
  }
307
337
  this.emit("add", newNodes);
338
+ this.emit("change", {
339
+ type: "add",
340
+ data: newNodes.map((node) => ({
341
+ node,
342
+ page: this.getPageOfNode(node.id)
343
+ }))
344
+ });
308
345
  return Array.isArray(addNode) ? newNodes : newNodes[0];
309
346
  }
310
- async doRemove(node, options) {
311
- const { doNotSelect = false, doNotSwitchPage = false } = safeOptions(options);
347
+ async doRemove(node, { doNotSelect = false, doNotSwitchPage = false } = {}) {
312
348
  const root = this.get("root");
313
349
  if (!root) throw new Error("root不能为空");
314
350
  const { parent, node: curNode } = this.getNodeInfo(node.id, false);
@@ -320,7 +356,7 @@ var Editor = class extends BaseService_default {
320
356
  stage?.remove({
321
357
  id: node.id,
322
358
  parentId: parent.id,
323
- root: cloneDeep(root)
359
+ root: cloneDeep$1(root)
324
360
  });
325
361
  const selectedNodes = this.get("nodes");
326
362
  const removedSelectedIndex = selectedNodes.findIndex((n) => `${n.id}` === `${node.id}`);
@@ -337,10 +373,7 @@ var Editor = class extends BaseService_default {
337
373
  if (pages[0]) {
338
374
  await this.select(pages[0]);
339
375
  stage?.select(pages[0].id);
340
- } else {
341
- this.selectRoot();
342
- history_default.resetPage();
343
- }
376
+ } else this.selectRoot();
344
377
  };
345
378
  const rootItems = root.items || [];
346
379
  if (isPage(node)) {
@@ -367,11 +400,15 @@ var Editor = class extends BaseService_default {
367
400
  * @param options 可选配置
368
401
  * @param options.doNotSelect 删除后是否不更新当前选中节点(默认 false,删除后会选中父节点或首个页面)
369
402
  * @param options.doNotSwitchPage 删除后是否不切换当前页面(默认 false;删除页面 / 页面片段时为 true 会跳过自动切换到首个剩余页面)
403
+ * @param options.doNotPushHistory 是否不写入历史记录(默认 false)
370
404
  */
371
- async remove(nodeOrNodeList, options) {
372
- const { doNotSelect = false, doNotSwitchPage = false } = safeOptions(options);
405
+ async remove(nodeOrNodeList, { doNotSelect = false, doNotSwitchPage = false, doNotPushHistory = false, historyDescription, historySource } = {}) {
373
406
  this.captureSelectionBeforeOp();
374
407
  const nodes = Array.isArray(nodeOrNodeList) ? nodeOrNodeList : [nodeOrNodeList];
408
+ const changeItems = nodes.map((node) => ({
409
+ node,
410
+ page: this.getPageOfNode(node.id)
411
+ }));
375
412
  const removedItems = [];
376
413
  let pageForOp = null;
377
414
  if (!(isPage(nodes[0]) || isPageFragment(nodes[0]))) for (const n of nodes) {
@@ -383,7 +420,7 @@ var Editor = class extends BaseService_default {
383
420
  };
384
421
  const idx = getNodeIndex(curNode.id, parent);
385
422
  removedItems.push({
386
- node: cloneDeep(toRaw(curNode)),
423
+ oldSchema: cloneDeep$1(toRaw(curNode)),
387
424
  parentId: parent.id,
388
425
  index: typeof idx === "number" ? idx : -1
389
426
  });
@@ -393,21 +430,30 @@ var Editor = class extends BaseService_default {
393
430
  doNotSelect,
394
431
  doNotSwitchPage
395
432
  })));
396
- if (removedItems.length > 0 && pageForOp) this.pushOpHistory("remove", { removedItems }, pageForOp);
433
+ if (removedItems.length > 0 && pageForOp) if (!doNotPushHistory) this.pushOpHistory("remove", {
434
+ diff: removedItems,
435
+ pageData: pageForOp,
436
+ historyDescription,
437
+ source: historySource
438
+ });
439
+ else this.selectionBeforeOp = null;
397
440
  this.emit("remove", nodes);
441
+ this.emit("change", {
442
+ type: "remove",
443
+ data: changeItems
444
+ });
398
445
  }
399
- async doUpdate(config, { changeRecords = [] } = {}) {
400
- const root = this.get("root");
401
- if (!root) throw new Error("root为空");
446
+ async doUpdate(config, { changeRecords = [], historySource } = {}) {
447
+ if (!this.get("root")) throw new Error("root为空");
402
448
  if (!config?.id) throw new Error("没有配置或者配置缺少id值");
403
449
  const info = this.getNodeInfo(config.id, false);
404
450
  if (!info.node) throw new Error(`获取不到id为${config.id}的节点`);
405
451
  const node = toRaw(info.node);
406
- let newConfig = await toggleFixedPosition(toRaw(config), node, root, this.getLayout);
407
- newConfig = mergeWith(cloneDeep(node), newConfig, editorNodeMergeCustomizer);
452
+ let newConfig = await toggleFixedPosition(toRaw(config), node, info.path, this.getLayout);
453
+ newConfig = mergeWith(cloneDeep$1(node), newConfig, editorNodeMergeCustomizer);
408
454
  if (!newConfig.type) throw new Error("配置缺少type值");
409
455
  if (newConfig.type === NodeType.ROOT) {
410
- this.set("root", newConfig);
456
+ this.set("root", newConfig, { historySource });
411
457
  return {
412
458
  oldNode: node,
413
459
  newNode: newConfig,
@@ -444,27 +490,50 @@ var Editor = class extends BaseService_default {
444
490
  * 更新节点
445
491
  * update后会触发依赖收集,收集完后会掉stage.update方法
446
492
  * @param config 新的节点配置,配置中需要有id信息
493
+ * @param data 额外数据
494
+ * @param data.changeRecords 单节点 form 端变更记录(多节点场景下被忽略,使用 changeRecordList)
495
+ * @param data.changeRecordList 多节点 form 端变更记录列表,按 config 数组同序对应每个节点;优先级高于 changeRecords
496
+ * @param data.doNotPushHistory 是否不写入历史记录(默认 false)
497
+ * @param data.historyDescription 入栈时附带的人类可读描述,用于历史面板展示(不影响 undo/redo 行为)
447
498
  * @returns 更新后的节点配置
448
499
  */
449
500
  async update(config, data = {}) {
450
501
  this.captureSelectionBeforeOp();
502
+ const { doNotPushHistory = false, changeRecordList, changeRecords, historyDescription, historySource } = data;
451
503
  const nodes = Array.isArray(config) ? config : [config];
452
- const updateData = await Promise.all(nodes.map((node) => this.doUpdate(node, data)));
504
+ const updateData = await Promise.all(nodes.map((node, index) => {
505
+ const recordsForNode = changeRecordList ? changeRecordList[index] ?? [] : changeRecords ?? [];
506
+ return this.doUpdate(node, {
507
+ changeRecords: recordsForNode,
508
+ historySource
509
+ });
510
+ }));
453
511
  if (updateData[0].oldNode?.type !== NodeType.ROOT) {
454
- const curNodes = this.get("nodes");
455
- if (!this.isHistoryStateChange && curNodes.length) {
512
+ if (this.get("nodes").length) if (!doNotPushHistory) {
456
513
  const pageForOp = this.getNodeInfo(nodes[0].id, false).page;
457
- this.pushOpHistory("update", { updatedItems: updateData.map((d) => ({
458
- oldNode: cloneDeep(d.oldNode),
459
- newNode: cloneDeep(toRaw(d.newNode))
460
- })) }, {
461
- name: pageForOp?.name || "",
462
- id: pageForOp.id
514
+ this.pushOpHistory("update", {
515
+ diff: buildUpdateDiff(updateData.map((d) => ({
516
+ oldNode: cloneDeep$1(d.oldNode),
517
+ newNode: cloneDeep$1(d.newNode),
518
+ changeRecords: d.changeRecords?.length ? cloneDeep$1(d.changeRecords) : void 0
519
+ }))),
520
+ pageData: {
521
+ name: pageForOp?.name || "",
522
+ id: pageForOp.id
523
+ },
524
+ historyDescription,
525
+ source: historySource
463
526
  });
464
- }
465
- this.isHistoryStateChange = false;
527
+ } else this.selectionBeforeOp = null;
466
528
  }
467
529
  this.emit("update", updateData);
530
+ this.emit("change", {
531
+ type: "update",
532
+ data: updateData.map((node) => ({
533
+ node,
534
+ page: this.getPageOfNode(node.newNode.id)
535
+ }))
536
+ });
468
537
  return Array.isArray(config) ? updateData.map((item) => item.newNode) : updateData[0].newNode;
469
538
  }
470
539
  /**
@@ -474,27 +543,30 @@ var Editor = class extends BaseService_default {
474
543
  * @param options 可选配置
475
544
  * @param options.doNotSelect 排序后是否不更新当前选中节点(默认 false)
476
545
  * @param options.doNotSwitchPage 排序后是否不切换当前页面(排序只发生在同一父节点内,方法内为空操作;保留以与其它 DSL 操作 API 一致)
546
+ * @param options.doNotPushHistory 是否不写入历史记录(默认 false)
477
547
  * @returns void
478
548
  */
479
- async sort(id1, id2, options) {
480
- const { doNotSelect = false } = safeOptions(options);
549
+ async sort(id1, id2, { doNotSelect = false, doNotPushHistory = false, historySource } = {}) {
481
550
  this.captureSelectionBeforeOp();
482
551
  const root = this.get("root");
483
552
  if (!root) throw new Error("root为空");
484
553
  const node = this.get("node");
485
554
  if (!node) throw new Error("当前节点为空");
486
- const parent = cloneDeep(toRaw(this.get("parent")));
555
+ const parent = cloneDeep$1(toRaw(this.get("parent")));
487
556
  if (!parent) throw new Error("父节点为空");
488
557
  const index2 = parent.items.findIndex((node) => `${node.id}` === `${id2}`);
489
558
  if (index2 < 0) return;
490
559
  const index1 = parent.items.findIndex((node) => `${node.id}` === `${id1}`);
491
560
  parent.items.splice(index2, 0, ...parent.items.splice(index1, 1));
492
- await this.update(parent);
561
+ await this.update(parent, {
562
+ doNotPushHistory,
563
+ historySource
564
+ });
493
565
  if (!doNotSelect) await this.select(node);
494
566
  this.get("stage")?.update({
495
- config: cloneDeep(node),
567
+ config: cloneDeep$1(node),
496
568
  parentId: parent.id,
497
- root: cloneDeep(root)
569
+ root: cloneDeep$1(root)
498
570
  });
499
571
  }
500
572
  /**
@@ -522,10 +594,10 @@ var Editor = class extends BaseService_default {
522
594
  * @param options 可选配置
523
595
  * @param options.doNotSelect 粘贴后是否不更新当前选中节点(默认 false)
524
596
  * @param options.doNotSwitchPage 粘贴后是否不切换当前页面(默认 false;跨页粘贴时为 true 会跳过页面切换)
597
+ * @param options.doNotPushHistory 是否不写入历史记录(默认 false)
525
598
  * @returns 添加后的组件节点配置
526
599
  */
527
- async paste(position = {}, collectorOptions, options) {
528
- const { doNotSelect = false, doNotSwitchPage = false } = safeOptions(options);
600
+ async paste(position = {}, collectorOptions, { doNotSelect = false, doNotSwitchPage = false, doNotPushHistory = false, historyDescription, historySource } = {}) {
529
601
  const config = storage_default.getItem(COPY_STORAGE_KEY);
530
602
  if (!Array.isArray(config)) return;
531
603
  const node = this.get("node");
@@ -538,18 +610,21 @@ var Editor = class extends BaseService_default {
538
610
  if (collectorOptions && typeof collectorOptions.isTarget === "function") props_default.replaceRelateId(config, pasteConfigs, collectorOptions);
539
611
  return this.add(pasteConfigs, parent, {
540
612
  doNotSelect,
541
- doNotSwitchPage
613
+ doNotSwitchPage,
614
+ doNotPushHistory,
615
+ historyDescription,
616
+ historySource
542
617
  });
543
618
  }
544
619
  async doPaste(config, position = {}) {
545
620
  props_default.clearRelateId();
546
621
  const doc = this.get("stage")?.renderer?.contentWindow?.document;
547
- return beforePaste(position, cloneDeep(config), doc);
622
+ return beforePaste(position, cloneDeep$1(config), doc);
548
623
  }
549
624
  async doAlignCenter(config) {
550
625
  const parent = this.getParentById(config.id);
551
626
  if (!parent) throw new Error("找不到父节点");
552
- const node = cloneDeep(toRaw(config));
627
+ const node = cloneDeep$1(toRaw(config));
553
628
  const layout = await this.getLayout(parent, node);
554
629
  const doc = this.get("stage")?.renderer?.contentWindow?.document;
555
630
  const newStyle = calcAlignCenterStyle(node, parent, layout, doc);
@@ -563,14 +638,18 @@ var Editor = class extends BaseService_default {
563
638
  * @param options 可选配置
564
639
  * @param options.doNotSelect 居中后是否不更新当前选中节点(默认 false)
565
640
  * @param options.doNotSwitchPage 居中后是否不切换当前页面(居中只更新节点 style,方法内为空操作;保留以与其它 DSL 操作 API 一致)
641
+ * @param options.doNotPushHistory 是否不写入历史记录(默认 false)
566
642
  * @returns 当前组件节点配置
567
643
  */
568
- async alignCenter(config, options) {
569
- const { doNotSelect = false } = safeOptions(options);
644
+ async alignCenter(config, { doNotSelect = false, doNotPushHistory = false, historyDescription, historySource } = {}) {
570
645
  const nodes = Array.isArray(config) ? config : [config];
571
646
  const stage = this.get("stage");
572
647
  const newNodes = await Promise.all(nodes.map((node) => this.doAlignCenter(node)));
573
- const newNode = await this.update(newNodes);
648
+ const newNode = await this.update(newNodes, {
649
+ doNotPushHistory,
650
+ historyDescription,
651
+ historySource
652
+ });
574
653
  if (!doNotSelect) if (newNodes.length > 1) await stage?.multiSelect(newNodes.map((node) => node.id));
575
654
  else await stage?.select(newNodes[0].id);
576
655
  return newNode;
@@ -578,8 +657,10 @@ var Editor = class extends BaseService_default {
578
657
  /**
579
658
  * 移动当前选中节点位置
580
659
  * @param offset 偏移量
660
+ * @param options 可选配置
661
+ * @param options.doNotPushHistory 是否不写入历史记录(默认 false)
581
662
  */
582
- async moveLayer(offset) {
663
+ async moveLayer(offset, { doNotPushHistory = false, historyDescription, historySource } = {}) {
583
664
  this.captureSelectionBeforeOp();
584
665
  const root = this.get("root");
585
666
  if (!root) throw new Error("root为空");
@@ -592,95 +673,122 @@ var Editor = class extends BaseService_default {
592
673
  const isRelative = await this.getLayout(parent, node) === Layout.RELATIVE;
593
674
  const offsetIndex = calcLayerTargetIndex(index, offset, brothers.length, isRelative);
594
675
  if (offsetIndex > 0 && offsetIndex > brothers.length || offsetIndex < 0) return;
595
- const oldParent = cloneDeep(toRaw(parent));
676
+ const oldParent = cloneDeep$1(toRaw(parent));
596
677
  brothers.splice(index, 1);
597
678
  brothers.splice(offsetIndex, 0, node);
598
679
  const grandparent = this.getParentById(parent.id);
599
680
  this.get("stage")?.update({
600
- config: cloneDeep(toRaw(parent)),
681
+ config: cloneDeep$1(toRaw(parent)),
601
682
  parentId: grandparent?.id,
602
- root: cloneDeep(root)
683
+ root: cloneDeep$1(root)
603
684
  });
604
685
  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
- });
686
+ if (!doNotPushHistory) {
687
+ const pageForOp = this.getNodeInfo(node.id, false).page;
688
+ this.pushOpHistory("update", {
689
+ diff: buildUpdateDiff([{
690
+ oldNode: oldParent,
691
+ newNode: cloneDeep$1(toRaw(parent))
692
+ }]),
693
+ pageData: {
694
+ name: pageForOp?.name || "",
695
+ id: pageForOp.id
696
+ },
697
+ historyDescription,
698
+ source: historySource
699
+ });
700
+ } else this.selectionBeforeOp = null;
613
701
  this.emit("move-layer", offset);
702
+ this.emit("change", {
703
+ type: "move-layer",
704
+ data: [{
705
+ node,
706
+ page: this.getPageOfNode(node.id)
707
+ }],
708
+ offset
709
+ });
614
710
  }
615
711
  /**
616
- * 移动到指定容器中
617
- * @param config 需要移动的节点
712
+ * 移动一个或多个节点到指定容器中。
713
+ *
714
+ * 多选场景(config 是数组)只会产生一条历史记录,
715
+ * `updatedItems` 涵盖所有源父容器 + 目标容器的前后快照。
716
+ * 这避免了"多选移动到某容器"在历史栈里被切成 N 条记录。
717
+ *
718
+ * @param config 需要移动的节点(或节点数组,各项需带 id;style 等字段会与原节点合并)
618
719
  * @param targetId 容器ID
619
720
  * @param options 可选配置
620
721
  * @param options.doNotSelect 移动后是否不更新当前选中节点(默认 false)
621
722
  * @param options.doNotSwitchPage 移动后是否不切换当前页面(默认 false;目标容器位于其它页面时为 true 会跳过自动选中以避免页面切换)
723
+ * @param options.doNotPushHistory 是否不写入历史记录(默认 false)
622
724
  */
623
- async moveToContainer(config, targetId, options) {
624
- const { doNotSelect = false, doNotSwitchPage = false } = safeOptions(options);
725
+ async moveToContainer(config, targetId, { doNotSelect = false, doNotSwitchPage = false, doNotPushHistory = false, historyDescription, historySource } = {}) {
726
+ const isBatch = Array.isArray(config);
727
+ const configs = (isBatch ? config : [config]).filter((item) => !(isPage(item) || isPageFragment(item)));
728
+ if (configs.length === 0) throw new Error("没有可移动的节点");
625
729
  this.captureSelectionBeforeOp();
626
- const root = this.get("root");
627
- const { node, parent, page: pageForOp } = this.getNodeInfo(config.id, false);
628
730
  const target = this.getNodeById(targetId, false);
731
+ if (!target) throw new Error("目标容器不存在");
732
+ const root = this.get("root");
629
733
  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
734
+ if (!root || !stage) throw new Error("root stage为空");
735
+ const moves = [];
736
+ for (const { id } of configs) {
737
+ const { node, parent, page } = this.getNodeInfo(id, false);
738
+ if (!node || !parent) continue;
739
+ moves.push({
740
+ node,
741
+ parent,
742
+ pageForOp: page ? {
743
+ name: page.name || "",
744
+ id: page.id
745
+ } : null
670
746
  });
671
- return newConfig;
672
747
  }
748
+ if (moves.length === 0) throw new Error("没有可移动的节点");
749
+ const beforeSnapshots = /* @__PURE__ */ new Map();
750
+ beforeSnapshots.set(target.id, cloneDeep$1(toRaw(target)));
751
+ for (const { parent } of moves) if (!beforeSnapshots.has(parent.id)) beforeSnapshots.set(parent.id, cloneDeep$1(toRaw(parent)));
752
+ let newConfigs = [];
753
+ const moveNodes = moves.map(({ node }) => node);
754
+ await this.remove(moveNodes, {
755
+ doNotPushHistory: true,
756
+ doNotSelect,
757
+ doNotSwitchPage: true
758
+ });
759
+ newConfigs = await this.add(moveNodes, target, {
760
+ doNotPushHistory: true,
761
+ doNotSelect,
762
+ doNotSwitchPage
763
+ });
764
+ if (!doNotPushHistory) {
765
+ const updatedItems = Array.from(beforeSnapshots.entries()).map(([id, oldNode]) => ({
766
+ oldNode,
767
+ newNode: cloneDeep$1(toRaw(this.getNodeById(id, false)))
768
+ }));
769
+ const historyPage = moves[0].pageForOp ?? {
770
+ name: "",
771
+ id: target.id
772
+ };
773
+ this.pushOpHistory("update", {
774
+ diff: buildUpdateDiff(updatedItems),
775
+ pageData: historyPage,
776
+ historyDescription,
777
+ source: historySource
778
+ });
779
+ } else this.selectionBeforeOp = null;
780
+ return isBatch ? newConfigs : newConfigs[0];
673
781
  }
674
- async dragTo(config, targetParent, targetIndex) {
782
+ async dragTo(config, targetParent, targetIndex, { doNotPushHistory = false, historyDescription, historySource } = {}) {
675
783
  this.captureSelectionBeforeOp();
676
784
  if (!targetParent || !Array.isArray(targetParent.items)) return;
677
785
  const configs = Array.isArray(config) ? config : [config];
678
786
  const beforeSnapshots = /* @__PURE__ */ new Map();
679
787
  for (const cfg of configs) {
680
788
  const { parent } = this.getNodeInfo(cfg.id, false);
681
- if (parent && !beforeSnapshots.has(`${parent.id}`)) beforeSnapshots.set(`${parent.id}`, cloneDeep(toRaw(parent)));
789
+ if (parent && !beforeSnapshots.has(`${parent.id}`)) beforeSnapshots.set(`${parent.id}`, cloneDeep$1(toRaw(parent)));
682
790
  }
683
- if (!beforeSnapshots.has(`${targetParent.id}`)) beforeSnapshots.set(`${targetParent.id}`, cloneDeep(toRaw(targetParent)));
791
+ if (!beforeSnapshots.has(`${targetParent.id}`)) beforeSnapshots.set(`${targetParent.id}`, cloneDeep$1(toRaw(targetParent)));
684
792
  const newLayout = await this.getLayout(targetParent);
685
793
  const { sameParentIndices, crossParentConfigs, aborted } = classifyDragSources(configs, targetParent, (id, raw) => this.getNodeInfo(id, raw));
686
794
  if (aborted) return;
@@ -699,35 +807,111 @@ var Editor = class extends BaseService_default {
699
807
  const root = this.get("root");
700
808
  const stage = this.get("stage");
701
809
  if (stage && page && root) stage.update({
702
- config: cloneDeep(page),
810
+ config: cloneDeep$1(page),
703
811
  parentId: root.id,
704
- root: cloneDeep(root)
812
+ root: cloneDeep$1(root)
705
813
  });
706
814
  const updatedItems = [];
707
815
  for (const oldNode of beforeSnapshots.values()) {
708
816
  const newNode = this.getNodeById(oldNode.id, false);
709
817
  if (newNode) updatedItems.push({
710
818
  oldNode,
711
- newNode: cloneDeep(toRaw(newNode))
819
+ newNode: cloneDeep$1(toRaw(newNode))
712
820
  });
713
821
  }
714
- const pageForOp = this.getNodeInfo(configs[0].id, false).page;
715
- this.pushOpHistory("update", { updatedItems }, {
716
- name: pageForOp?.name || "",
717
- id: pageForOp.id
718
- });
822
+ if (!doNotPushHistory) {
823
+ const pageForOp = this.getNodeInfo(configs[0].id, false).page;
824
+ this.pushOpHistory("update", {
825
+ diff: buildUpdateDiff(updatedItems),
826
+ pageData: {
827
+ name: pageForOp?.name || "",
828
+ id: pageForOp.id
829
+ },
830
+ historyDescription,
831
+ source: historySource
832
+ });
833
+ } else this.selectionBeforeOp = null;
719
834
  this.emit("drag-to", {
720
835
  targetIndex,
721
836
  configs,
722
837
  targetParent
723
838
  });
839
+ this.emit("change", {
840
+ type: "drag-to",
841
+ data: configs.map((node) => ({
842
+ node,
843
+ page: this.getPageOfNode(node.id)
844
+ })),
845
+ targetIndex,
846
+ targetParent
847
+ });
848
+ }
849
+ /**
850
+ * 下列 *AndGetHistoryId 方法与对应的普通操作(add / remove / update ...)行为完全一致,
851
+ * 返回值在 {@link DslOpWithHistoryIdsResult} 中同时包含原操作结果与本次写入历史栈的 uuid 列表({@link StepValue.uuid}),
852
+ * 可用于精确引用 / 定位该条历史记录(埋点、revert、跨端同步等)。
853
+ *
854
+ * 当本次操作未写入历史(doNotPushHistory 为 true、或操作无实际变更 / 提前返回)时 historyIds 为 `[]`。
855
+ */
856
+ /** 等价于 {@link add},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
857
+ async addAndGetHistoryId(addNode, parent, options = {}) {
858
+ this.lastPushedHistoryId = null;
859
+ return {
860
+ result: await this.add(addNode, parent, options),
861
+ historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId)
862
+ };
863
+ }
864
+ /** 等价于 {@link remove},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
865
+ async removeAndGetHistoryId(nodeOrNodeList, options = {}) {
866
+ this.lastPushedHistoryId = null;
867
+ await this.remove(nodeOrNodeList, options);
868
+ return {
869
+ result: void 0,
870
+ historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId)
871
+ };
872
+ }
873
+ /** 等价于 {@link update},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
874
+ async updateAndGetHistoryId(config, data = {}) {
875
+ this.lastPushedHistoryId = null;
876
+ return {
877
+ result: await this.update(config, data),
878
+ historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId)
879
+ };
880
+ }
881
+ /** 等价于 {@link moveLayer},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
882
+ async moveLayerAndGetHistoryId(offset, options = {}) {
883
+ this.lastPushedHistoryId = null;
884
+ await this.moveLayer(offset, options);
885
+ return {
886
+ result: void 0,
887
+ historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId)
888
+ };
889
+ }
890
+ /** 等价于 {@link moveToContainer},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
891
+ async moveToContainerAndGetHistoryId(config, targetId, options = {}) {
892
+ this.lastPushedHistoryId = null;
893
+ return {
894
+ result: await this.moveToContainer(config, targetId, options),
895
+ historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId)
896
+ };
897
+ }
898
+ /** 等价于 {@link dragTo},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
899
+ async dragToAndGetHistoryId(config, targetParent, targetIndex, options = {}) {
900
+ this.lastPushedHistoryId = null;
901
+ await this.dragTo(config, targetParent, targetIndex, options);
902
+ return {
903
+ result: void 0,
904
+ historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId)
905
+ };
724
906
  }
725
907
  /**
726
908
  * 撤销当前操作
727
909
  * @returns 被撤销的操作
728
910
  */
729
911
  async undo() {
730
- const value = history_default.undo();
912
+ const pageId = this.get("page")?.id;
913
+ if (pageId === void 0) return null;
914
+ const value = history_default.undo("page", pageId);
731
915
  if (value) await this.applyHistoryOp(value, true);
732
916
  return value;
733
917
  }
@@ -736,11 +920,141 @@ var Editor = class extends BaseService_default {
736
920
  * @returns 被恢复的操作
737
921
  */
738
922
  async redo() {
739
- const value = history_default.redo();
923
+ const pageId = this.get("page")?.id;
924
+ if (pageId === void 0) return null;
925
+ const value = history_default.redo("page", pageId);
740
926
  if (value) await this.applyHistoryOp(value, false);
741
927
  return value;
742
928
  }
743
- async move(left, top) {
929
+ /**
930
+ * 「回滚」指定页面历史步骤(类 git revert 语义):
931
+ * - 不动原始历史栈结构(不移动 cursor、不丢弃任何步骤);
932
+ * - 取出 `index` 对应的 step,**反向应用**一次(add→remove / remove→add / update→旧值);
933
+ * - 把这次反向应用作为一条**新步骤**追加到栈顶,可被普通 undo / redo。
934
+ *
935
+ * 与 `gotoPageStep`(类 git reset)的区别在于此操作**不丢弃**目标之后的历史。
936
+ * 与 `applyHistoryOp(reverse=true)` 的区别在于:本方法**不带** `doNotPushHistory`,
937
+ * 反向应用会以一条新 step 入栈;并且不实施 step 中保存的选区与 modifiedNodeIds 状态,
938
+ * 选区由用户当前位置决定,符合"新提交"语义。
939
+ *
940
+ * 仅对处于「已应用」状态的步骤生效——未应用的步骤本身就不存在于当前 DSL 中,反向无意义。
941
+ *
942
+ * @param index 目标 step 在所属页面栈中的索引(0 为最早),通常由历史面板传入
943
+ * @returns 反向后产生的新 step;目标不存在 / 未应用 / 反向失败时返回 null
944
+ */
945
+ async revertPageStep(index) {
946
+ const entry = history_default.getStepList("page", this.get("page")?.id)[index];
947
+ if (!entry?.applied) return null;
948
+ const { step } = entry;
949
+ if (step.opType === "initial") return null;
950
+ if (!this.get("root")) return null;
951
+ if (step.opType === "update") {
952
+ const items = step.diff ?? [];
953
+ if (!items.length || !items.every((item) => item.changeRecords?.length)) return null;
954
+ }
955
+ let revertedStep = null;
956
+ const captureRevert = (_pageId, s) => {
957
+ revertedStep = s;
958
+ };
959
+ history_default.once("change", captureRevert);
960
+ const historyDescription = `回滚 #${index + 1}: ${describeStepForRevert(step)}`;
961
+ const opts = {
962
+ doNotSelect: true,
963
+ doNotSwitchPage: true,
964
+ historyDescription,
965
+ historySource: "rollback"
966
+ };
967
+ try {
968
+ switch (step.opType) {
969
+ case "add":
970
+ for (const { newSchema } of step.diff ?? []) {
971
+ if (!newSchema) continue;
972
+ const existing = this.getNodeById(newSchema.id, false);
973
+ if (existing) await this.remove(existing, opts);
974
+ }
975
+ break;
976
+ case "remove": {
977
+ const sorted = [...step.diff ?? []].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
978
+ for (const { oldSchema, parentId } of sorted) {
979
+ if (!oldSchema || parentId === void 0) continue;
980
+ const parent = this.getNodeById(parentId, false);
981
+ if (parent) await this.add([cloneDeep$1(oldSchema)], parent, opts);
982
+ }
983
+ break;
984
+ }
985
+ case "update": {
986
+ const configs = (step.diff ?? []).filter((item) => item.oldSchema && item.newSchema).map(({ oldSchema, newSchema, changeRecords }) => {
987
+ const oldNode = oldSchema;
988
+ const newNode = newSchema;
989
+ if (changeRecords?.length) {
990
+ const patch = {
991
+ id: newNode.id,
992
+ type: newNode.type
993
+ };
994
+ for (const record of changeRecords) {
995
+ if (!record.propPath) return cloneDeep$1(oldNode);
996
+ const value = cloneDeep$1(getValueByKeyPath(record.propPath, oldNode));
997
+ setValueByKeyPath(record.propPath, value, patch);
998
+ }
999
+ return patch;
1000
+ }
1001
+ return cloneDeep$1(oldNode);
1002
+ });
1003
+ if (configs.length) await this.update(configs, {
1004
+ historyDescription,
1005
+ historySource: "rollback"
1006
+ });
1007
+ break;
1008
+ }
1009
+ }
1010
+ } finally {
1011
+ history_default.off("change", captureRevert);
1012
+ }
1013
+ const page = toRaw(this.get("page"));
1014
+ if (page) this.emit("history-change", page);
1015
+ return revertedStep;
1016
+ }
1017
+ /**
1018
+ * 通过历史记录 uuid 回滚当前页面的历史步骤,语义与 {@link revertPageStep} 完全一致,
1019
+ * 仅入参从 index 改为 uuid 列表({@link StepValue.uuid})。按数组顺序依次回滚,
1020
+ * 返回与入参同序的结果列表(某项失败时为 `null`)。
1021
+ *
1022
+ * @param uuids 目标历史记录的 uuid 列表,通常由 *AndGetHistoryId 方法返回的 `historyIds`
1023
+ */
1024
+ async revertPageStepById(uuids) {
1025
+ const results = [];
1026
+ const pageId = this.get("page")?.id;
1027
+ for (const uuid of uuids) {
1028
+ const location = history_default.findStepLocationByUuid("page", uuid, pageId);
1029
+ results.push(!location ? null : await this.revertPageStep(location.index));
1030
+ }
1031
+ return results;
1032
+ }
1033
+ /**
1034
+ * 跳转当前页面历史栈到指定游标位置。
1035
+ *
1036
+ * `targetCursor` 与 `UndoRedo.getCursor()` 同义:表示"已应用步骤数量",
1037
+ * 取值范围 `[0, length]`。当目标 < 当前游标时循环 undo,否则循环 redo。
1038
+ * 通常由历史面板传入「点击的 step.index + 1」作为目标。
1039
+ *
1040
+ * @returns 实际移动到的最终游标位置
1041
+ */
1042
+ async gotoPageStep(targetCursor) {
1043
+ const pageId = this.get("page")?.id;
1044
+ let cursor = history_default.getCursor("page", pageId);
1045
+ const { length } = history_default.getStepList("page", pageId);
1046
+ const target = Math.max(0, Math.min(targetCursor, length));
1047
+ while (cursor > target) {
1048
+ if (!await this.undo()) break;
1049
+ cursor -= 1;
1050
+ }
1051
+ while (cursor < target) {
1052
+ if (!await this.redo()) break;
1053
+ cursor += 1;
1054
+ }
1055
+ return cursor;
1056
+ }
1057
+ async move(left, top, { doNotPushHistory = false, historyDescription, historySource } = {}) {
744
1058
  const node = toRaw(this.get("node"));
745
1059
  if (!node || isPage(node)) return;
746
1060
  const newStyle = calcMoveStyle(node.style || {}, left, top);
@@ -749,6 +1063,10 @@ var Editor = class extends BaseService_default {
749
1063
  id: node.id,
750
1064
  type: node.type,
751
1065
  style: newStyle
1066
+ }, {
1067
+ doNotPushHistory,
1068
+ historyDescription,
1069
+ historySource
752
1070
  });
753
1071
  }
754
1072
  resetState() {
@@ -783,63 +1101,207 @@ var Editor = class extends BaseService_default {
783
1101
  return super.emit(eventName, ...args);
784
1102
  }
785
1103
  addModifiedNodeId(id) {
786
- if (!this.isHistoryStateChange) this.get("modifiedNodeIds").set(id, id);
1104
+ this.get("modifiedNodeIds").set(id, id);
1105
+ }
1106
+ /**
1107
+ * 获取指定节点所属的页面 / 页面片:
1108
+ * - 普通节点返回其所在的 page;
1109
+ * - 节点本身就是 page / pageFragment 时返回它自己;
1110
+ * - 找不到时返回 null。
1111
+ * 供 `change` 事件携带「变更节点对应的 page」(而非编辑器当前选中页)。
1112
+ */
1113
+ getPageOfNode(id) {
1114
+ const { node, page } = this.getNodeInfo(id, false);
1115
+ if (page) return page;
1116
+ if (node && (isPage(node) || isPageFragment(node))) return node;
1117
+ return null;
787
1118
  }
788
1119
  captureSelectionBeforeOp() {
789
- if (this.isHistoryStateChange || this.selectionBeforeOp) return;
1120
+ if (this.selectionBeforeOp) return;
790
1121
  this.selectionBeforeOp = this.get("nodes").map((n) => n.id);
791
1122
  }
792
- pushOpHistory(opType, extra, pageData) {
793
- if (this.isHistoryStateChange) {
794
- this.selectionBeforeOp = null;
795
- return;
796
- }
1123
+ /**
1124
+ * 比较「上一次 root」与「新 root」的页面 / 页面片,按页面粒度把整体替换拆成历史记录:
1125
+ * - 新旧都存在且内容变化的页面 → 一条 `update`(整页快照替换,无 changeRecords);
1126
+ * - 仅新 root 存在的页面 → 一条 `add`;
1127
+ * - 仅旧 root 存在的页面 → 一条 `remove`。
1128
+ *
1129
+ * 每条记录落到对应页面自己的历史栈(与普通节点操作一致),并标记来源 `source`。
1130
+ * 内容未变化的页面不产生记录,避免重复设置相同 DSL 时产生噪声。
1131
+ */
1132
+ pushRootDiffHistory(preRoot, nextRoot, source) {
1133
+ const prevPages = preRoot?.items || [];
1134
+ const nextPages = nextRoot?.items || [];
1135
+ const prevMap = new Map(prevPages.map((p) => [`${p.id}`, p]));
1136
+ const nextMap = new Map(nextPages.map((p) => [`${p.id}`, p]));
1137
+ const indexInItems = (root, id) => (root.items ?? []).findIndex((item) => `${item.id}` === `${id}`);
1138
+ nextPages.forEach((nextPage) => {
1139
+ const prevPage = prevMap.get(`${nextPage.id}`);
1140
+ if (!prevPage) this.pushPageDiffStep("add", nextPage, {
1141
+ newSchema: cloneDeep$1(toRaw(nextPage)),
1142
+ parentId: nextRoot.id,
1143
+ index: indexInItems(nextRoot, nextPage.id)
1144
+ }, source);
1145
+ else if (!isEqual(toRaw(prevPage), toRaw(nextPage))) this.pushPageDiffStep("update", nextPage, {
1146
+ oldSchema: cloneDeep$1(toRaw(prevPage)),
1147
+ newSchema: cloneDeep$1(toRaw(nextPage))
1148
+ }, source);
1149
+ });
1150
+ prevPages.forEach((prevPage) => {
1151
+ if (!nextMap.has(`${prevPage.id}`)) this.pushPageDiffStep("remove", prevPage, {
1152
+ oldSchema: cloneDeep$1(toRaw(prevPage)),
1153
+ parentId: preRoot.id,
1154
+ index: indexInItems(preRoot, prevPage.id)
1155
+ }, source);
1156
+ });
1157
+ }
1158
+ /**
1159
+ * 构造一条页面级「set root」历史记录(不携带选区 / modifiedNodeIds 上下文)并落到该页面自己的栈。
1160
+ *
1161
+ * 连续 set root 替换:若该页栈最新一条已是**同来源**的 set root 记录({@link StepValue.rootStep} 且 `source` 相同),
1162
+ * 则用本次记录**替换**它而非新增,避免源码反复保存 / 外部重设 DSL 时堆积多条 root 记录;
1163
+ * 来源不同则照常新增(initial 基线不是 rootStep,不在此列)。
1164
+ */
1165
+ pushPageDiffStep(opType, page, diffItem, source) {
797
1166
  const step = {
1167
+ uuid: guid(),
1168
+ data: {
1169
+ name: page.name || "",
1170
+ id: page.id
1171
+ },
1172
+ opType,
1173
+ extra: {
1174
+ selectedBefore: [],
1175
+ selectedAfter: [],
1176
+ modifiedNodeIds: /* @__PURE__ */ new Map()
1177
+ },
1178
+ diff: [diffItem],
1179
+ rootStep: true
1180
+ };
1181
+ if (source) step.source = source;
1182
+ const top = history_default.getCurrentPageStep(page.id);
1183
+ if (top?.rootStep && top.source === source) history_default.replaceCurrentStep("page", step, page.id);
1184
+ else history_default.push("page", step, page.id);
1185
+ }
1186
+ pushOpHistory(opType, { diff, pageData, historyDescription, source }) {
1187
+ const step = {
1188
+ uuid: guid(),
798
1189
  data: pageData,
799
1190
  opType,
800
- selectedBefore: this.selectionBeforeOp ?? [],
801
- selectedAfter: this.get("nodes").map((n) => n.id),
802
- modifiedNodeIds: new Map(this.get("modifiedNodeIds")),
803
- ...extra
1191
+ extra: {
1192
+ selectedBefore: this.selectionBeforeOp ?? [],
1193
+ selectedAfter: this.get("nodes").map((n) => n.id),
1194
+ modifiedNodeIds: new Map(this.get("modifiedNodeIds"))
1195
+ },
1196
+ diff
804
1197
  };
805
- history_default.push(step);
1198
+ if (historyDescription) step.historyDescription = historyDescription;
1199
+ if (source) step.source = source;
1200
+ const historyId = history_default.push("page", step, pageData.id) ? step.uuid : null;
1201
+ this.lastPushedHistoryId = historyId;
806
1202
  this.selectionBeforeOp = null;
807
- this.isHistoryStateChange = false;
1203
+ return historyId;
808
1204
  }
809
1205
  /**
810
1206
  * 应用历史操作(撤销 / 重做)
1207
+ *
1208
+ * 所有 DSL 修改都走 `editor.add / remove / update`,并通过 `doNotPushHistory` 阻止再次入栈、
1209
+ * `doNotSelect / doNotSwitchPage` 让选区由方法末尾的统一逻辑兜底。
1210
+ *
1211
+ * 注意:这些公开方法会发出 add / remove / update 事件,业务侧若需要区分"用户操作"与"撤销重做触发",
1212
+ * 请监听 `history-change` 事件配合判断。
1213
+ *
811
1214
  * @param step 操作记录
812
1215
  * @param reverse true = 撤销,false = 重做
813
1216
  */
814
1217
  async applyHistoryOp(step, reverse) {
815
- this.isHistoryStateChange = true;
1218
+ if (step.opType === "initial") return;
816
1219
  const root = this.get("root");
817
1220
  const stage = this.get("stage");
818
1221
  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")
1222
+ const commonOpts = {
1223
+ doNotSelect: true,
1224
+ doNotSwitchPage: true,
1225
+ doNotPushHistory: true
827
1226
  };
828
1227
  switch (step.opType) {
829
- case "add":
830
- await applyHistoryAddOp(step, reverse, ctx);
1228
+ case "add": {
1229
+ const items = step.diff ?? [];
1230
+ if (reverse) for (const { newSchema } of items) {
1231
+ if (!newSchema) continue;
1232
+ const existing = this.getNodeById(newSchema.id, false);
1233
+ if (existing) await this.remove(existing, commonOpts);
1234
+ }
1235
+ else {
1236
+ const sorted = [...items].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
1237
+ for (const { newSchema, parentId, index } of sorted) {
1238
+ if (!newSchema || parentId === void 0) continue;
1239
+ const parent = this.getNodeById(parentId, false);
1240
+ if (parent?.items) {
1241
+ if (typeof index === "number" && index >= 0 && index < parent.items.length) parent.items.splice(index, 0, cloneDeep$1(newSchema));
1242
+ else parent.items.push(cloneDeep$1(newSchema));
1243
+ await stage?.add({
1244
+ config: cloneDeep$1(newSchema),
1245
+ parent: cloneDeep$1(parent),
1246
+ parentId: parent.id,
1247
+ root: cloneDeep$1(root)
1248
+ });
1249
+ }
1250
+ }
1251
+ }
831
1252
  break;
832
- case "remove":
833
- await applyHistoryRemoveOp(step, reverse, ctx);
1253
+ }
1254
+ case "remove": {
1255
+ const items = step.diff ?? [];
1256
+ if (reverse) {
1257
+ const sorted = [...items].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
1258
+ for (const { oldSchema, parentId, index } of sorted) {
1259
+ if (!oldSchema || parentId === void 0) continue;
1260
+ const parent = this.getNodeById(parentId, false);
1261
+ if (parent?.items) {
1262
+ parent.items.splice(index ?? parent.items.length, 0, cloneDeep$1(oldSchema));
1263
+ await stage?.add({
1264
+ config: cloneDeep$1(oldSchema),
1265
+ parent: cloneDeep$1(parent),
1266
+ parentId,
1267
+ root: cloneDeep$1(root)
1268
+ });
1269
+ }
1270
+ }
1271
+ } else for (const { oldSchema } of items) {
1272
+ if (!oldSchema) continue;
1273
+ const existing = this.getNodeById(oldSchema.id, false);
1274
+ if (existing) await this.remove(existing, commonOpts);
1275
+ }
834
1276
  break;
835
- case "update":
836
- await applyHistoryUpdateOp(step, reverse, ctx);
1277
+ }
1278
+ case "update": {
1279
+ const configs = (step.diff ?? []).filter((item) => item.oldSchema && item.newSchema).map(({ oldSchema, newSchema, changeRecords }) => {
1280
+ const oldNode = oldSchema;
1281
+ const newNode = newSchema;
1282
+ if (changeRecords?.length) {
1283
+ const sourceForValues = reverse ? oldNode : newNode;
1284
+ const patch = {
1285
+ id: newNode.id,
1286
+ type: newNode.type
1287
+ };
1288
+ for (const record of changeRecords) {
1289
+ if (!record.propPath) return cloneDeep$1(sourceForValues);
1290
+ const value = cloneDeep$1(getValueByKeyPath(record.propPath, sourceForValues));
1291
+ setValueByKeyPath(record.propPath, value, patch);
1292
+ }
1293
+ return patch;
1294
+ }
1295
+ return cloneDeep$1(reverse ? oldNode : newNode);
1296
+ });
1297
+ if (configs.length) await this.update(configs, { doNotPushHistory: true });
837
1298
  break;
1299
+ }
838
1300
  }
839
- this.set("modifiedNodeIds", step.modifiedNodeIds);
1301
+ this.set("modifiedNodeIds", step.extra?.modifiedNodeIds ?? /* @__PURE__ */ new Map());
840
1302
  const page = toRaw(this.get("page"));
841
1303
  if (page) {
842
- const selectIds = reverse ? step.selectedBefore : step.selectedAfter;
1304
+ const selectIds = (reverse ? step.extra?.selectedBefore : step.extra?.selectedAfter) ?? [];
843
1305
  setTimeout(() => {
844
1306
  if (!selectIds.length) return;
845
1307
  if (selectIds.length > 1) {
@@ -849,7 +1311,6 @@ var Editor = class extends BaseService_default {
849
1311
  }, 0);
850
1312
  this.emit("history-change", page);
851
1313
  }
852
- this.isHistoryStateChange = false;
853
1314
  }
854
1315
  selectedConfigExceptionHandler(config) {
855
1316
  return resolveSelectedNode(config, (id) => this.getNodeInfo(id), this.state.root?.id);