@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.
- package/dist/es/Editor.vue_vue_type_script_setup_true_lang.js +31 -5
- package/dist/es/components/CodeBlockEditor.vue_vue_type_script_setup_true_lang.js +21 -113
- package/dist/es/components/CodeParams.vue_vue_type_script_setup_true_lang.js +7 -1
- package/dist/es/components/CompareForm.js +5 -0
- package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +196 -0
- package/dist/es/components/ContentMenu.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/components/FloatingBox.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/es/components/Icon.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/components/Resizer.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/components/ScrollBar.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/components/ScrollViewer.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/components/SearchInput.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/components/SplitView.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/components/ToolButton.vue_vue_type_script_setup_true_lang.js +7 -7
- package/dist/es/components/Tree.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/components/TreeNode.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/editorProps.js +2 -0
- package/dist/es/fields/Code.vue_vue_type_script_setup_true_lang.js +20 -4
- package/dist/es/fields/CodeLink.vue_vue_type_script_setup_true_lang.js +3 -6
- package/dist/es/fields/CodeSelect.vue_vue_type_script_setup_true_lang.js +19 -3
- package/dist/es/fields/CodeSelectCol.vue_vue_type_script_setup_true_lang.js +42 -8
- package/dist/es/fields/CondOpSelect.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/DataSourceFieldSelect/FieldSelect.vue_vue_type_script_setup_true_lang.js +14 -6
- package/dist/es/fields/DataSourceFieldSelect/Index.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/es/fields/DataSourceFields.vue_vue_type_script_setup_true_lang.js +41 -16
- package/dist/es/fields/DataSourceInput.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/es/fields/DataSourceMethodSelect.vue_vue_type_script_setup_true_name_true_lang.js +9 -5
- package/dist/es/fields/DataSourceMethods.vue_vue_type_script_setup_true_lang.js +40 -19
- package/dist/es/fields/DataSourceMocks.vue_vue_type_script_setup_true_lang.js +17 -9
- package/dist/es/fields/DataSourceSelect.vue_vue_type_script_setup_true_lang.js +4 -2
- package/dist/es/fields/DisplayConds.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/EventSelect.vue_vue_type_script_setup_true_lang.js +59 -12
- package/dist/es/fields/KeyValue.vue_vue_type_script_setup_true_lang.js +12 -7
- package/dist/es/fields/PageFragmentSelect.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/StyleSetter/Index.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/StyleSetter/components/BackgroundPosition.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/StyleSetter/components/Border.vue_vue_type_script_setup_true_lang.js +28 -6
- package/dist/es/fields/StyleSetter/components/Box.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/StyleSetter/icons/background-repeat/NoRepeat.js +1 -1
- package/dist/es/fields/StyleSetter/icons/background-repeat/Repeat.js +1 -1
- package/dist/es/fields/StyleSetter/icons/background-repeat/RepeatX.js +1 -1
- package/dist/es/fields/StyleSetter/icons/background-repeat/RepeatY.js +1 -1
- package/dist/es/fields/StyleSetter/icons/display/Block.js +1 -1
- package/dist/es/fields/StyleSetter/icons/display/Flex.js +1 -1
- package/dist/es/fields/StyleSetter/icons/display/Inline.js +1 -1
- package/dist/es/fields/StyleSetter/icons/display/InlineBlock.js +1 -1
- package/dist/es/fields/StyleSetter/icons/display/None.js +1 -1
- package/dist/es/fields/StyleSetter/icons/flex-direction/Column.js +1 -1
- package/dist/es/fields/StyleSetter/icons/flex-direction/ColumnReverse.js +1 -1
- package/dist/es/fields/StyleSetter/icons/flex-direction/Row.js +1 -1
- package/dist/es/fields/StyleSetter/icons/flex-direction/RowReverse.js +1 -1
- package/dist/es/fields/StyleSetter/icons/justify-content/Center.js +1 -1
- package/dist/es/fields/StyleSetter/icons/justify-content/FlexEnd.js +1 -1
- package/dist/es/fields/StyleSetter/icons/justify-content/FlexStart.js +1 -1
- package/dist/es/fields/StyleSetter/icons/justify-content/SpaceAround.js +1 -1
- package/dist/es/fields/StyleSetter/icons/justify-content/SpaceBetween.js +1 -1
- package/dist/es/fields/StyleSetter/icons/text-align/Center.js +1 -1
- package/dist/es/fields/StyleSetter/icons/text-align/Left.js +1 -1
- package/dist/es/fields/StyleSetter/icons/text-align/Right.js +1 -1
- package/dist/es/fields/StyleSetter/pro/Background.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/StyleSetter/pro/Border.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/StyleSetter/pro/Font.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/StyleSetter/pro/Layout.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/StyleSetter/pro/Position.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/StyleSetter/pro/Transform.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/UISelect.vue_vue_type_script_setup_true_lang.js +9 -6
- package/dist/es/hooks/use-code-block-edit.js +7 -4
- package/dist/es/hooks/use-data-source-edit.js +5 -2
- package/dist/es/hooks/use-stage.js +22 -10
- package/dist/es/icons/CenterIcon.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/icons/CodeIcon.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/icons/FolderMinusIcon.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/index.js +14 -3
- package/dist/es/initService.js +7 -7
- package/dist/es/layouts/AddPageBox.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/CodeEditor.vue_vue_type_script_setup_true_lang.js +7 -6
- package/dist/es/layouts/Framework.vue_vue_type_script_setup_true_lang.js +14 -9
- package/dist/es/layouts/NavMenu.vue_vue_type_script_setup_true_lang.js +12 -4
- package/dist/es/layouts/NavMenuColumn.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/history-list/Bucket.js +5 -0
- package/dist/es/layouts/history-list/Bucket.vue_vue_type_script_setup_true_lang.js +71 -0
- package/dist/es/layouts/history-list/BucketTab.js +5 -0
- package/dist/es/layouts/history-list/BucketTab.vue_vue_type_script_setup_true_lang.js +62 -0
- package/dist/es/layouts/history-list/GroupRow.js +5 -0
- package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +269 -0
- package/dist/es/layouts/history-list/HistoryDiffDialog.js +5 -0
- package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +287 -0
- package/dist/es/layouts/history-list/HistoryListPanel.js +5 -0
- package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +327 -0
- package/dist/es/layouts/history-list/InitialRow.js +5 -0
- package/dist/es/layouts/history-list/InitialRow.vue_vue_type_script_setup_true_lang.js +83 -0
- package/dist/es/layouts/history-list/PageTab.js +5 -0
- package/dist/es/layouts/history-list/PageTab.vue_vue_type_script_setup_true_lang.js +95 -0
- package/dist/es/layouts/history-list/composables.js +184 -0
- package/dist/es/layouts/history-list/useHistoryList.js +56 -0
- package/dist/es/layouts/history-list/useHistoryRevert.js +306 -0
- package/dist/es/layouts/page-bar/AddButton.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/page-bar/PageBar.vue_vue_type_script_setup_true_lang.js +5 -3
- package/dist/es/layouts/page-bar/PageBarScrollContainer.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/page-bar/PageList.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/page-bar/Search.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/es/layouts/props-panel/FormPanel.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/props-panel/PropsPanel.vue_vue_type_script_setup_true_lang.js +6 -2
- package/dist/es/layouts/sidebar/ComponentListPanel.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/es/layouts/sidebar/Sidebar.vue_vue_type_script_setup_true_lang.js +8 -4
- package/dist/es/layouts/sidebar/code-block/CodeBlockList.vue_vue_type_script_setup_true_lang.js +4 -4
- package/dist/es/layouts/sidebar/code-block/CodeBlockListPanel.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/es/layouts/sidebar/code-block/useContentMenu.js +1 -1
- package/dist/es/layouts/sidebar/data-source/DataSourceAddButton.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/sidebar/data-source/DataSourceConfigPanel.vue_vue_type_script_setup_true_lang.js +19 -6
- package/dist/es/layouts/sidebar/data-source/DataSourceList.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/sidebar/data-source/DataSourceListPanel.vue_vue_type_script_setup_true_lang.js +25 -3
- package/dist/es/layouts/sidebar/data-source/useContentMenu.js +1 -1
- package/dist/es/layouts/sidebar/layer/LayerMenu.vue_vue_type_script_setup_true_lang.js +6 -6
- package/dist/es/layouts/sidebar/layer/LayerNodeTool.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/es/layouts/sidebar/layer/LayerPanel.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/sidebar/layer/use-node-status.js +1 -1
- package/dist/es/layouts/workspace/Breadcrumb.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/workspace/Workspace.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/workspace/viewer/NodeListMenu.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/workspace/viewer/Stage.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/es/layouts/workspace/viewer/StageOverlay.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/workspace/viewer/ViewerMenu.vue_vue_type_script_setup_true_lang.js +9 -9
- package/dist/es/services/BaseService.js +31 -68
- package/dist/es/services/codeBlock.js +286 -8
- package/dist/es/services/componentList.js +2 -2
- package/dist/es/services/dataSource.js +287 -8
- package/dist/es/services/dep.js +21 -9
- package/dist/es/services/editor.js +655 -194
- package/dist/es/services/events.js +2 -2
- package/dist/es/services/history.js +390 -47
- package/dist/es/services/keybinding.js +5 -5
- package/dist/es/services/props.js +8 -8
- package/dist/es/services/stageOverlay.js +2 -2
- package/dist/es/services/storage.js +2 -2
- package/dist/es/services/ui.js +5 -2
- package/dist/es/style.css +456 -8
- package/dist/es/utils/code-block.js +122 -0
- package/dist/es/utils/content-menu.js +19 -12
- package/dist/es/utils/data-source/index.js +2 -0
- package/dist/es/utils/dep/worker.js +1 -1
- package/dist/es/utils/editor.js +105 -51
- package/dist/es/utils/history.js +220 -0
- package/dist/es/utils/indexed-db.js +86 -0
- package/dist/es/utils/undo-redo.js +80 -1
- package/dist/style.css +456 -8
- package/dist/tmagic-editor.umd.cjs +10135 -5646
- package/package.json +8 -8
- package/src/Editor.vue +25 -1
- package/src/components/CodeBlockEditor.vue +20 -109
- package/src/components/CodeParams.vue +6 -0
- package/src/components/CompareForm.vue +277 -0
- package/src/components/ToolButton.vue +2 -2
- package/src/editorProps.ts +9 -0
- package/src/fields/Code.vue +19 -3
- package/src/fields/CodeLink.vue +2 -5
- package/src/fields/CodeSelect.vue +17 -1
- package/src/fields/CodeSelectCol.vue +37 -3
- package/src/fields/DataSourceFieldSelect/FieldSelect.vue +23 -3
- package/src/fields/DataSourceFieldSelect/Index.vue +2 -2
- package/src/fields/DataSourceFields.vue +48 -10
- package/src/fields/DataSourceInput.vue +3 -3
- package/src/fields/DataSourceMethodSelect.vue +12 -4
- package/src/fields/DataSourceMethods.vue +55 -24
- package/src/fields/DataSourceMocks.vue +13 -3
- package/src/fields/DataSourceSelect.vue +4 -1
- package/src/fields/EventSelect.vue +57 -8
- package/src/fields/KeyValue.vue +17 -4
- package/src/fields/StyleSetter/components/Border.vue +15 -6
- package/src/fields/UISelect.vue +5 -2
- package/src/hooks/use-code-block-edit.ts +9 -5
- package/src/hooks/use-data-source-edit.ts +2 -2
- package/src/hooks/use-stage.ts +26 -9
- package/src/index.ts +8 -0
- package/src/initService.ts +1 -1
- package/src/layouts/CodeEditor.vue +17 -5
- package/src/layouts/Framework.vue +11 -6
- package/src/layouts/NavMenu.vue +12 -3
- package/src/layouts/history-list/Bucket.vue +91 -0
- package/src/layouts/history-list/BucketTab.vue +72 -0
- package/src/layouts/history-list/GroupRow.vue +304 -0
- package/src/layouts/history-list/HistoryDiffDialog.vue +273 -0
- package/src/layouts/history-list/HistoryListPanel.vue +367 -0
- package/src/layouts/history-list/InitialRow.vue +71 -0
- package/src/layouts/history-list/PageTab.vue +118 -0
- package/src/layouts/history-list/composables.ts +309 -0
- package/src/layouts/history-list/useHistoryList.ts +60 -0
- package/src/layouts/history-list/useHistoryRevert.ts +405 -0
- package/src/layouts/page-bar/PageBar.vue +4 -2
- package/src/layouts/props-panel/PropsPanel.vue +5 -1
- package/src/layouts/sidebar/ComponentListPanel.vue +9 -5
- package/src/layouts/sidebar/Sidebar.vue +8 -3
- package/src/layouts/sidebar/code-block/CodeBlockList.vue +5 -5
- package/src/layouts/sidebar/code-block/CodeBlockListPanel.vue +1 -1
- package/src/layouts/sidebar/code-block/useContentMenu.ts +1 -1
- package/src/layouts/sidebar/data-source/DataSourceConfigPanel.vue +31 -2
- package/src/layouts/sidebar/data-source/DataSourceListPanel.vue +27 -2
- package/src/layouts/sidebar/data-source/useContentMenu.ts +1 -1
- package/src/layouts/sidebar/layer/LayerMenu.vue +19 -11
- package/src/layouts/sidebar/layer/LayerNodeTool.vue +7 -4
- package/src/layouts/sidebar/layer/use-node-status.ts +9 -6
- package/src/layouts/workspace/viewer/Stage.vue +1 -1
- package/src/layouts/workspace/viewer/ViewerMenu.vue +8 -8
- package/src/services/BaseService.ts +35 -86
- package/src/services/codeBlock.ts +361 -5
- package/src/services/dataSource.ts +336 -6
- package/src/services/dep.ts +32 -9
- package/src/services/editor.ts +834 -194
- package/src/services/history.ts +509 -49
- package/src/services/keybinding.ts +3 -3
- package/src/services/props.ts +2 -2
- package/src/services/ui.ts +7 -0
- package/src/theme/history-list-panel.scss +557 -0
- package/src/theme/page-bar.scss +0 -4
- package/src/theme/props-panel.scss +3 -3
- package/src/theme/style-setter/border.scss +4 -1
- package/src/theme/theme.scss +1 -0
- package/src/type.ts +594 -31
- package/src/utils/code-block.ts +150 -0
- package/src/utils/content-menu.ts +25 -13
- package/src/utils/data-source/index.ts +2 -0
- package/src/utils/dep/worker.ts +5 -3
- package/src/utils/editor.ts +166 -60
- package/src/utils/history.ts +286 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/indexed-db.ts +122 -0
- package/src/utils/undo-redo.ts +111 -0
- package/types/index.d.ts +3171 -889
- package/dist/es/utils/compose.js +0 -44
- package/dist/es/utils/editor-history.js +0 -100
- package/src/utils/compose.ts +0 -52
- package/src/utils/editor-history.ts +0 -138
package/src/services/editor.ts
CHANGED
|
@@ -17,13 +17,21 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { reactive, toRaw } from 'vue';
|
|
20
|
-
import { cloneDeep, isObject, mergeWith, uniq } from 'lodash-es';
|
|
20
|
+
import { cloneDeep, isEmpty, isEqual, isObject, mergeWith, uniq } from 'lodash-es';
|
|
21
21
|
|
|
22
22
|
import type { Id, MApp, MContainer, MNode, MPage, MPageFragment, TargetOptions } from '@tmagic/core';
|
|
23
23
|
import { NodeType } from '@tmagic/core';
|
|
24
24
|
import type { ChangeRecord } from '@tmagic/form';
|
|
25
25
|
import { isFixed } from '@tmagic/stage';
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
getNodeInfo,
|
|
28
|
+
getNodePath,
|
|
29
|
+
getValueByKeyPath,
|
|
30
|
+
guid,
|
|
31
|
+
isPage,
|
|
32
|
+
isPageFragment,
|
|
33
|
+
setValueByKeyPath,
|
|
34
|
+
} from '@tmagic/utils';
|
|
27
35
|
|
|
28
36
|
import BaseService from '@editor/services//BaseService';
|
|
29
37
|
import propsService from '@editor/services//props';
|
|
@@ -34,10 +42,13 @@ import type {
|
|
|
34
42
|
AsyncHookPlugin,
|
|
35
43
|
AsyncMethodName,
|
|
36
44
|
DslOpOptions,
|
|
45
|
+
DslOpWithHistoryIdsResult,
|
|
37
46
|
EditorEvents,
|
|
38
47
|
EditorNodeInfo,
|
|
48
|
+
HistoryOpSource,
|
|
39
49
|
HistoryOpType,
|
|
40
50
|
PastePosition,
|
|
51
|
+
StepDiffItem,
|
|
41
52
|
StepValue,
|
|
42
53
|
StoreState,
|
|
43
54
|
StoreStateKey,
|
|
@@ -50,6 +61,7 @@ import {
|
|
|
50
61
|
classifyDragSources,
|
|
51
62
|
collectRelatedNodes,
|
|
52
63
|
COPY_STORAGE_KEY,
|
|
64
|
+
describeStepForRevert,
|
|
53
65
|
editorNodeMergeCustomizer,
|
|
54
66
|
fixNodePosition,
|
|
55
67
|
getInitPositionStyle,
|
|
@@ -62,28 +74,24 @@ import {
|
|
|
62
74
|
setLayout,
|
|
63
75
|
toggleFixedPosition,
|
|
64
76
|
} from '@editor/utils/editor';
|
|
65
|
-
import
|
|
66
|
-
import { applyHistoryAddOp, applyHistoryRemoveOp, applyHistoryUpdateOp } from '@editor/utils/editor-history';
|
|
77
|
+
import { getLastPushedHistoryIds } from '@editor/utils/history';
|
|
67
78
|
import { beforePaste, getAddParent } from '@editor/utils/operator';
|
|
68
79
|
|
|
69
|
-
|
|
70
|
-
* 经过 BaseService 的插件 / 中间件包装后,源方法的最后一个形参可能被注入为 dispatch 函数
|
|
71
|
-
* 当 options 形参位置被注入为函数(或为 null)时,将其归一为空对象,避免后续逻辑误读
|
|
72
|
-
*/
|
|
73
|
-
const safeOptions = <T extends object>(options: unknown): T => {
|
|
74
|
-
const empty = {};
|
|
75
|
-
if (!options || typeof options === 'function') return empty as T;
|
|
76
|
-
return options as T;
|
|
77
|
-
};
|
|
80
|
+
type MoveItem = { node: MNode; parent: MContainer; pageForOp: { name: string; id: Id } | null };
|
|
78
81
|
|
|
79
82
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
83
|
+
* 把「变更前后节点快照」列表归一成 update 类型的 {@link StepDiffItem} 列表,供 {@link StepValue.diff} 使用。
|
|
84
|
+
* `changeRecords` 来自 form 端的 propPath/value 列表,撤销/重做时只对这些 propPath 做局部更新;
|
|
85
|
+
* 缺省(未传 / 空数组)才退化为整节点替换。
|
|
82
86
|
*/
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
+
const buildUpdateDiff = (
|
|
88
|
+
items: { oldNode: MNode; newNode: MNode; changeRecords?: ChangeRecord[] }[],
|
|
89
|
+
): StepDiffItem<MNode>[] =>
|
|
90
|
+
items.map(({ oldNode, newNode, changeRecords }) => ({
|
|
91
|
+
oldSchema: oldNode,
|
|
92
|
+
newSchema: newNode,
|
|
93
|
+
...(changeRecords?.length ? { changeRecords } : {}),
|
|
94
|
+
}));
|
|
87
95
|
|
|
88
96
|
class Editor extends BaseService {
|
|
89
97
|
public state: StoreState = reactive({
|
|
@@ -101,8 +109,13 @@ class Editor extends BaseService {
|
|
|
101
109
|
disabledMultiSelect: false,
|
|
102
110
|
alwaysMultiSelect: false,
|
|
103
111
|
});
|
|
104
|
-
private isHistoryStateChange = false;
|
|
105
112
|
private selectionBeforeOp: Id[] | null = null;
|
|
113
|
+
/**
|
|
114
|
+
* 最近一次 pushOpHistory 写入的历史记录 uuid。
|
|
115
|
+
* 供 *AndGetHistoryId 系列方法在调用普通操作后取回本次产生的历史记录 id;
|
|
116
|
+
* 普通操作不会读取它,调用前由 *AndGetHistoryId 重置为 null。
|
|
117
|
+
*/
|
|
118
|
+
private lastPushedHistoryId: string | null = null;
|
|
106
119
|
|
|
107
120
|
constructor() {
|
|
108
121
|
super(
|
|
@@ -116,8 +129,13 @@ class Editor extends BaseService {
|
|
|
116
129
|
* 设置当前指点节点配置
|
|
117
130
|
* @param name 'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'stage' | 'modifiedNodeIds' | 'pageLength' | 'pageFragmentLength
|
|
118
131
|
* @param value MNode
|
|
132
|
+
* @param options.historySource 设置 root 时,本次变更写入历史记录的「操作来源」(仅 name === 'root' 时生效)
|
|
119
133
|
*/
|
|
120
|
-
public set<K extends StoreStateKey, T extends StoreState[K]>(
|
|
134
|
+
public set<K extends StoreStateKey, T extends StoreState[K]>(
|
|
135
|
+
name: K,
|
|
136
|
+
value: T,
|
|
137
|
+
options: { historySource?: HistoryOpSource } = {},
|
|
138
|
+
) {
|
|
121
139
|
const preValue = this.state[name];
|
|
122
140
|
this.state[name] = value;
|
|
123
141
|
|
|
@@ -136,13 +154,32 @@ class Editor extends BaseService {
|
|
|
136
154
|
this.state.pageLength = getPageList(app).length || 0;
|
|
137
155
|
this.state.pageFragmentLength = getPageFragmentList(app).length || 0;
|
|
138
156
|
this.state.stageLoading = this.state.pageLength !== 0;
|
|
157
|
+
|
|
158
|
+
if (preValue && !isEmpty(preValue)) {
|
|
159
|
+
// 编辑期间再次整体设置 root(源码保存 / 外部重设 DSL / root 节点更新):与上一次 root
|
|
160
|
+
// 做页面级 diff,按 update / add / remove 入栈,作为正常历史记录体现整体替换。
|
|
161
|
+
this.pushRootDiffHistory(preValue as MApp, app, options.historySource);
|
|
162
|
+
} else {
|
|
163
|
+
// 首次设置 root:仅当该页面 / 页面片尚无基线标记时,才写入「未修改的初始状态」基线。
|
|
164
|
+
// 配合「先恢复历史再 set root」:若基线已随历史恢复建立(恢复后已有基线),则此处不再
|
|
165
|
+
// 重复创建,set root 不额外产生记录,由恢复出的历史栈作为当前状态来源。
|
|
166
|
+
// 标记不进入撤销/重做栈,仅作为该页历史列表底部的初始基线展示。
|
|
167
|
+
app.items?.forEach((pageNode) => {
|
|
168
|
+
if (pageNode?.id !== undefined && !historyService.getMarker('page', pageNode.id)) {
|
|
169
|
+
historyService.setMarker('page', pageNode.id, {
|
|
170
|
+
name: pageNode.name,
|
|
171
|
+
source: options.historySource,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
139
176
|
} else {
|
|
140
177
|
this.state.pageLength = 0;
|
|
141
178
|
this.state.pageFragmentLength = 0;
|
|
142
179
|
this.state.stageLoading = false;
|
|
143
180
|
}
|
|
144
181
|
|
|
145
|
-
this.emit('root-change', value as StoreState['root'], preValue as StoreState['root']);
|
|
182
|
+
this.emit('root-change', value as StoreState['root'], preValue as StoreState['root'], options);
|
|
146
183
|
}
|
|
147
184
|
}
|
|
148
185
|
|
|
@@ -167,7 +204,30 @@ class Editor extends BaseService {
|
|
|
167
204
|
root = toRaw(root);
|
|
168
205
|
}
|
|
169
206
|
|
|
170
|
-
|
|
207
|
+
if (!root) {
|
|
208
|
+
return { node: null, parent: null, page: null, path: [] };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (id === root.id) {
|
|
212
|
+
return { node: root, parent: null, page: null, path: [] };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 大多数查找的目标都在当前页面内,优先在当前页面子树中查找以避免对整棵树做全量遍历。
|
|
216
|
+
// 注意:不能直接使用 state.page,它可能与当前 root 不同步(指向已脱离的旧页面对象),
|
|
217
|
+
// 因此仅借用其 id,再从当前 root 中取回真正的页面对象(页面均为 root 的直接子节点,数量很少)。
|
|
218
|
+
const pageIdStr = `${this.get('page')?.id || ''}`;
|
|
219
|
+
const currentPageNode = root.items?.find((item) => `${item.id}` === pageIdStr);
|
|
220
|
+
if (currentPageNode && `${id}` !== pageIdStr) {
|
|
221
|
+
// util 仅读取 root.id 与 root.items,按容器结构传入当前页面是安全的
|
|
222
|
+
const info = getNodeInfo(id, currentPageNode);
|
|
223
|
+
if (info.node) {
|
|
224
|
+
return info;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 回退:在完整 root 上查找;当前页面已搜索过,用 skip 跳过其子树避免重复遍历,
|
|
229
|
+
// 同时保留真实的 parent / page 引用(id 命中当前页面节点本身时会在跳过子树前先匹配到)
|
|
230
|
+
return getNodeInfo(id, root, currentPageNode);
|
|
171
231
|
}
|
|
172
232
|
|
|
173
233
|
/**
|
|
@@ -212,7 +272,7 @@ class Editor extends BaseService {
|
|
|
212
272
|
* 只有容器拥有布局
|
|
213
273
|
*/
|
|
214
274
|
public async getLayout(parent: MNode, node?: MNode | null): Promise<Layout> {
|
|
215
|
-
if (node &&
|
|
275
|
+
if (node && isFixed(node.style || {})) return Layout.FIXED;
|
|
216
276
|
|
|
217
277
|
if (parent.layout) {
|
|
218
278
|
return parent.layout;
|
|
@@ -237,12 +297,6 @@ class Editor extends BaseService {
|
|
|
237
297
|
this.set('page', page);
|
|
238
298
|
this.set('parent', parent);
|
|
239
299
|
|
|
240
|
-
if (page) {
|
|
241
|
-
historyService.changePage(toRaw(page));
|
|
242
|
-
} else {
|
|
243
|
-
historyService.resetState();
|
|
244
|
-
}
|
|
245
|
-
|
|
246
300
|
if (node?.id) {
|
|
247
301
|
this.get('stage')
|
|
248
302
|
?.renderer?.runtime?.getApp?.()
|
|
@@ -388,16 +442,20 @@ class Editor extends BaseService {
|
|
|
388
442
|
* @param options 可选配置
|
|
389
443
|
* @param options.doNotSelect 添加后是否不更新当前选中节点(默认 false,添加后会选中新增的节点)
|
|
390
444
|
* @param options.doNotSwitchPage 添加后是否不切换当前页面(默认 false;新增页面 / 跨页新增时为 true 会跳过会引发页面切换的选中操作)
|
|
445
|
+
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
|
391
446
|
* @returns 添加后的节点
|
|
392
447
|
*/
|
|
393
448
|
public async add(
|
|
394
449
|
addNode: AddMNode | MNode[],
|
|
395
450
|
parent?: MContainer | null,
|
|
396
|
-
|
|
451
|
+
{
|
|
452
|
+
doNotSelect = false,
|
|
453
|
+
doNotSwitchPage = false,
|
|
454
|
+
doNotPushHistory = false,
|
|
455
|
+
historyDescription,
|
|
456
|
+
historySource,
|
|
457
|
+
}: DslOpOptions = {},
|
|
397
458
|
): Promise<MNode | MNode[]> {
|
|
398
|
-
const safeParentNode = safeParent(parent);
|
|
399
|
-
const { doNotSelect = false, doNotSwitchPage = false } = safeOptions<DslOpOptions>(options);
|
|
400
|
-
|
|
401
459
|
this.captureSelectionBeforeOp();
|
|
402
460
|
|
|
403
461
|
const stage = this.get('stage');
|
|
@@ -420,7 +478,7 @@ class Editor extends BaseService {
|
|
|
420
478
|
if ((isPage(node) || isPageFragment(node)) && root) {
|
|
421
479
|
return this.doAdd(node, root);
|
|
422
480
|
}
|
|
423
|
-
const parentNode =
|
|
481
|
+
const parentNode = parent ?? getAddParent(node);
|
|
424
482
|
if (!parentNode) throw new Error('未找到父元素');
|
|
425
483
|
return this.doAdd(node, parentNode);
|
|
426
484
|
}),
|
|
@@ -455,30 +513,40 @@ class Editor extends BaseService {
|
|
|
455
513
|
|
|
456
514
|
if (!(isPage(newNodes[0]) || isPageFragment(newNodes[0]))) {
|
|
457
515
|
const pageForOp = this.getNodeInfo(newNodes[0].id, false).page;
|
|
458
|
-
|
|
459
|
-
'
|
|
460
|
-
{
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
516
|
+
if (!doNotPushHistory) {
|
|
517
|
+
const parentId = (this.getParentById(newNodes[0].id, false) ?? this.get('root'))!.id;
|
|
518
|
+
this.pushOpHistory('add', {
|
|
519
|
+
diff: newNodes.map((n) => {
|
|
520
|
+
const p = this.getParentById(n.id, false) as MContainer;
|
|
521
|
+
const idx = p ? getNodeIndex(n.id, p) : -1;
|
|
522
|
+
return {
|
|
523
|
+
newSchema: cloneDeep(toRaw(n)),
|
|
524
|
+
parentId,
|
|
525
|
+
index: typeof idx === 'number' ? idx : -1,
|
|
526
|
+
};
|
|
527
|
+
}),
|
|
528
|
+
pageData: { name: pageForOp?.name || '', id: pageForOp!.id },
|
|
529
|
+
historyDescription,
|
|
530
|
+
source: historySource,
|
|
531
|
+
});
|
|
532
|
+
} else {
|
|
533
|
+
this.selectionBeforeOp = null;
|
|
534
|
+
}
|
|
472
535
|
}
|
|
473
536
|
|
|
474
537
|
this.emit('add', newNodes);
|
|
538
|
+
this.emit('change', {
|
|
539
|
+
type: 'add',
|
|
540
|
+
data: newNodes.map((node) => ({ node, page: this.getPageOfNode(node.id) })),
|
|
541
|
+
});
|
|
475
542
|
|
|
476
543
|
return Array.isArray(addNode) ? newNodes : newNodes[0];
|
|
477
544
|
}
|
|
478
545
|
|
|
479
|
-
public async doRemove(
|
|
480
|
-
|
|
481
|
-
|
|
546
|
+
public async doRemove(
|
|
547
|
+
node: MNode,
|
|
548
|
+
{ doNotSelect = false, doNotSwitchPage = false }: DslOpOptions = {},
|
|
549
|
+
): Promise<void> {
|
|
482
550
|
const root = this.get('root');
|
|
483
551
|
if (!root) throw new Error('root不能为空');
|
|
484
552
|
|
|
@@ -516,8 +584,6 @@ class Editor extends BaseService {
|
|
|
516
584
|
stage?.select(pages[0].id);
|
|
517
585
|
} else {
|
|
518
586
|
this.selectRoot();
|
|
519
|
-
|
|
520
|
-
historyService.resetPage();
|
|
521
587
|
}
|
|
522
588
|
};
|
|
523
589
|
|
|
@@ -557,15 +623,26 @@ class Editor extends BaseService {
|
|
|
557
623
|
* @param options 可选配置
|
|
558
624
|
* @param options.doNotSelect 删除后是否不更新当前选中节点(默认 false,删除后会选中父节点或首个页面)
|
|
559
625
|
* @param options.doNotSwitchPage 删除后是否不切换当前页面(默认 false;删除页面 / 页面片段时为 true 会跳过自动切换到首个剩余页面)
|
|
626
|
+
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
|
560
627
|
*/
|
|
561
|
-
public async remove(
|
|
562
|
-
|
|
563
|
-
|
|
628
|
+
public async remove(
|
|
629
|
+
nodeOrNodeList: MNode | MNode[],
|
|
630
|
+
{
|
|
631
|
+
doNotSelect = false,
|
|
632
|
+
doNotSwitchPage = false,
|
|
633
|
+
doNotPushHistory = false,
|
|
634
|
+
historyDescription,
|
|
635
|
+
historySource,
|
|
636
|
+
}: DslOpOptions = {},
|
|
637
|
+
): Promise<void> {
|
|
564
638
|
this.captureSelectionBeforeOp();
|
|
565
639
|
|
|
566
640
|
const nodes = Array.isArray(nodeOrNodeList) ? nodeOrNodeList : [nodeOrNodeList];
|
|
567
641
|
|
|
568
|
-
|
|
642
|
+
// 删除后节点已从树中移除,无法再反查所属 page,这里在删除前先逐个捕获用于 `change` 事件
|
|
643
|
+
const changeItems = nodes.map((node) => ({ node, page: this.getPageOfNode(node.id) }));
|
|
644
|
+
|
|
645
|
+
const removedItems: StepDiffItem<MNode>[] = [];
|
|
569
646
|
let pageForOp: { name: string; id: Id } | null = null;
|
|
570
647
|
if (!(isPage(nodes[0]) || isPageFragment(nodes[0]))) {
|
|
571
648
|
for (const n of nodes) {
|
|
@@ -576,7 +653,7 @@ class Editor extends BaseService {
|
|
|
576
653
|
}
|
|
577
654
|
const idx = getNodeIndex(curNode.id, parent);
|
|
578
655
|
removedItems.push({
|
|
579
|
-
|
|
656
|
+
oldSchema: cloneDeep(toRaw(curNode)),
|
|
580
657
|
parentId: parent.id,
|
|
581
658
|
index: typeof idx === 'number' ? idx : -1,
|
|
582
659
|
});
|
|
@@ -587,15 +664,25 @@ class Editor extends BaseService {
|
|
|
587
664
|
await Promise.all(nodes.map((node) => this.doRemove(node, { doNotSelect, doNotSwitchPage })));
|
|
588
665
|
|
|
589
666
|
if (removedItems.length > 0 && pageForOp) {
|
|
590
|
-
|
|
667
|
+
if (!doNotPushHistory) {
|
|
668
|
+
this.pushOpHistory('remove', {
|
|
669
|
+
diff: removedItems,
|
|
670
|
+
pageData: pageForOp,
|
|
671
|
+
historyDescription,
|
|
672
|
+
source: historySource,
|
|
673
|
+
});
|
|
674
|
+
} else {
|
|
675
|
+
this.selectionBeforeOp = null;
|
|
676
|
+
}
|
|
591
677
|
}
|
|
592
678
|
|
|
593
679
|
this.emit('remove', nodes);
|
|
680
|
+
this.emit('change', { type: 'remove', data: changeItems });
|
|
594
681
|
}
|
|
595
682
|
|
|
596
683
|
public async doUpdate(
|
|
597
684
|
config: MNode,
|
|
598
|
-
{ changeRecords = [] }: { changeRecords?: ChangeRecord[] } = {},
|
|
685
|
+
{ changeRecords = [], historySource }: { changeRecords?: ChangeRecord[]; historySource?: HistoryOpSource } = {},
|
|
599
686
|
): Promise<{ newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] }> {
|
|
600
687
|
const root = this.get('root');
|
|
601
688
|
if (!root) throw new Error('root为空');
|
|
@@ -608,14 +695,14 @@ class Editor extends BaseService {
|
|
|
608
695
|
|
|
609
696
|
const node = toRaw(info.node);
|
|
610
697
|
|
|
611
|
-
let newConfig = await toggleFixedPosition(toRaw(config), node,
|
|
698
|
+
let newConfig = await toggleFixedPosition(toRaw(config), node, info.path, this.getLayout);
|
|
612
699
|
|
|
613
700
|
newConfig = mergeWith(cloneDeep(node), newConfig, editorNodeMergeCustomizer);
|
|
614
701
|
|
|
615
702
|
if (!newConfig.type) throw new Error('配置缺少type值');
|
|
616
703
|
|
|
617
704
|
if (newConfig.type === NodeType.ROOT) {
|
|
618
|
-
this.set('root', newConfig as MApp);
|
|
705
|
+
this.set('root', newConfig as MApp, { historySource });
|
|
619
706
|
return {
|
|
620
707
|
oldNode: node,
|
|
621
708
|
newNode: newConfig,
|
|
@@ -668,37 +755,68 @@ class Editor extends BaseService {
|
|
|
668
755
|
* 更新节点
|
|
669
756
|
* update后会触发依赖收集,收集完后会掉stage.update方法
|
|
670
757
|
* @param config 新的节点配置,配置中需要有id信息
|
|
758
|
+
* @param data 额外数据
|
|
759
|
+
* @param data.changeRecords 单节点 form 端变更记录(多节点场景下被忽略,使用 changeRecordList)
|
|
760
|
+
* @param data.changeRecordList 多节点 form 端变更记录列表,按 config 数组同序对应每个节点;优先级高于 changeRecords
|
|
761
|
+
* @param data.doNotPushHistory 是否不写入历史记录(默认 false)
|
|
762
|
+
* @param data.historyDescription 入栈时附带的人类可读描述,用于历史面板展示(不影响 undo/redo 行为)
|
|
671
763
|
* @returns 更新后的节点配置
|
|
672
764
|
*/
|
|
673
765
|
public async update(
|
|
674
766
|
config: MNode | MNode[],
|
|
675
|
-
data: {
|
|
767
|
+
data: {
|
|
768
|
+
changeRecords?: ChangeRecord[];
|
|
769
|
+
changeRecordList?: ChangeRecord[][];
|
|
770
|
+
doNotPushHistory?: boolean;
|
|
771
|
+
historyDescription?: string;
|
|
772
|
+
historySource?: HistoryOpSource;
|
|
773
|
+
} = {},
|
|
676
774
|
): Promise<MNode | MNode[]> {
|
|
677
775
|
this.captureSelectionBeforeOp();
|
|
678
776
|
|
|
777
|
+
const { doNotPushHistory = false, changeRecordList, changeRecords, historyDescription, historySource } = data;
|
|
778
|
+
|
|
679
779
|
const nodes = Array.isArray(config) ? config : [config];
|
|
680
780
|
|
|
681
|
-
|
|
781
|
+
// 多节点必须使用 changeRecordList 为每个节点提供独立的记录;
|
|
782
|
+
// 否则同一份 changeRecords 会被复用到每个节点上,nodeUpdateHandler / 历史回放都会按错误的 propPath 处理。
|
|
783
|
+
const updateData = await Promise.all(
|
|
784
|
+
nodes.map((node, index) => {
|
|
785
|
+
const recordsForNode = changeRecordList ? (changeRecordList[index] ?? []) : (changeRecords ?? []);
|
|
786
|
+
return this.doUpdate(node, { changeRecords: recordsForNode, historySource });
|
|
787
|
+
}),
|
|
788
|
+
);
|
|
682
789
|
|
|
683
790
|
if (updateData[0].oldNode?.type !== NodeType.ROOT) {
|
|
684
791
|
const curNodes = this.get('nodes');
|
|
685
|
-
if (
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
'update',
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
792
|
+
if (curNodes.length) {
|
|
793
|
+
if (!doNotPushHistory) {
|
|
794
|
+
const pageForOp = this.getNodeInfo(nodes[0].id, false).page;
|
|
795
|
+
this.pushOpHistory('update', {
|
|
796
|
+
// 每个节点单独保留自己的 changeRecords,便于撤销/重做时按 propPath 精细化更新;
|
|
797
|
+
// 没有 changeRecords 的(如内部 sort/moveLayer 等整节点替换操作)会退化为全节点替换。
|
|
798
|
+
diff: buildUpdateDiff(
|
|
799
|
+
updateData.map((d) => ({
|
|
800
|
+
oldNode: cloneDeep(d.oldNode),
|
|
801
|
+
newNode: cloneDeep(d.newNode),
|
|
802
|
+
changeRecords: d.changeRecords?.length ? cloneDeep(d.changeRecords) : undefined,
|
|
803
|
+
})),
|
|
804
|
+
),
|
|
805
|
+
pageData: { name: pageForOp?.name || '', id: pageForOp!.id },
|
|
806
|
+
historyDescription,
|
|
807
|
+
source: historySource,
|
|
808
|
+
});
|
|
809
|
+
} else {
|
|
810
|
+
this.selectionBeforeOp = null;
|
|
811
|
+
}
|
|
697
812
|
}
|
|
698
|
-
this.isHistoryStateChange = false;
|
|
699
813
|
}
|
|
700
814
|
|
|
701
815
|
this.emit('update', updateData);
|
|
816
|
+
this.emit('change', {
|
|
817
|
+
type: 'update',
|
|
818
|
+
data: updateData.map((node) => ({ node, page: this.getPageOfNode(node.newNode.id) })),
|
|
819
|
+
});
|
|
702
820
|
return Array.isArray(config) ? updateData.map((item) => item.newNode) : updateData[0].newNode;
|
|
703
821
|
}
|
|
704
822
|
|
|
@@ -709,11 +827,14 @@ class Editor extends BaseService {
|
|
|
709
827
|
* @param options 可选配置
|
|
710
828
|
* @param options.doNotSelect 排序后是否不更新当前选中节点(默认 false)
|
|
711
829
|
* @param options.doNotSwitchPage 排序后是否不切换当前页面(排序只发生在同一父节点内,方法内为空操作;保留以与其它 DSL 操作 API 一致)
|
|
830
|
+
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
|
712
831
|
* @returns void
|
|
713
832
|
*/
|
|
714
|
-
public async sort(
|
|
715
|
-
|
|
716
|
-
|
|
833
|
+
public async sort(
|
|
834
|
+
id1: Id,
|
|
835
|
+
id2: Id,
|
|
836
|
+
{ doNotSelect = false, doNotPushHistory = false, historySource }: DslOpOptions = {},
|
|
837
|
+
): Promise<void> {
|
|
717
838
|
this.captureSelectionBeforeOp();
|
|
718
839
|
|
|
719
840
|
const root = this.get('root');
|
|
@@ -732,7 +853,7 @@ class Editor extends BaseService {
|
|
|
732
853
|
|
|
733
854
|
parent.items.splice(index2, 0, ...parent.items.splice(index1, 1));
|
|
734
855
|
|
|
735
|
-
await this.update(parent);
|
|
856
|
+
await this.update(parent, { doNotPushHistory, historySource });
|
|
736
857
|
if (!doNotSelect) {
|
|
737
858
|
await this.select(node);
|
|
738
859
|
}
|
|
@@ -779,15 +900,20 @@ class Editor extends BaseService {
|
|
|
779
900
|
* @param options 可选配置
|
|
780
901
|
* @param options.doNotSelect 粘贴后是否不更新当前选中节点(默认 false)
|
|
781
902
|
* @param options.doNotSwitchPage 粘贴后是否不切换当前页面(默认 false;跨页粘贴时为 true 会跳过页面切换)
|
|
903
|
+
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
|
782
904
|
* @returns 添加后的组件节点配置
|
|
783
905
|
*/
|
|
784
906
|
public async paste(
|
|
785
907
|
position: PastePosition = {},
|
|
786
908
|
collectorOptions?: TargetOptions,
|
|
787
|
-
|
|
909
|
+
{
|
|
910
|
+
doNotSelect = false,
|
|
911
|
+
doNotSwitchPage = false,
|
|
912
|
+
doNotPushHistory = false,
|
|
913
|
+
historyDescription,
|
|
914
|
+
historySource,
|
|
915
|
+
}: DslOpOptions = {},
|
|
788
916
|
): Promise<MNode | MNode[] | void> {
|
|
789
|
-
const { doNotSelect = false, doNotSwitchPage = false } = safeOptions<DslOpOptions>(options);
|
|
790
|
-
|
|
791
917
|
const config: MNode[] = storageService.getItem(COPY_STORAGE_KEY);
|
|
792
918
|
if (!Array.isArray(config)) return;
|
|
793
919
|
|
|
@@ -807,7 +933,13 @@ class Editor extends BaseService {
|
|
|
807
933
|
propsService.replaceRelateId(config, pasteConfigs, collectorOptions);
|
|
808
934
|
}
|
|
809
935
|
|
|
810
|
-
return this.add(pasteConfigs, parent, {
|
|
936
|
+
return this.add(pasteConfigs, parent, {
|
|
937
|
+
doNotSelect,
|
|
938
|
+
doNotSwitchPage,
|
|
939
|
+
doNotPushHistory,
|
|
940
|
+
historyDescription,
|
|
941
|
+
historySource,
|
|
942
|
+
});
|
|
811
943
|
}
|
|
812
944
|
|
|
813
945
|
public async doPaste(config: MNode[], position: PastePosition = {}): Promise<MNode[]> {
|
|
@@ -838,17 +970,19 @@ class Editor extends BaseService {
|
|
|
838
970
|
* @param options 可选配置
|
|
839
971
|
* @param options.doNotSelect 居中后是否不更新当前选中节点(默认 false)
|
|
840
972
|
* @param options.doNotSwitchPage 居中后是否不切换当前页面(居中只更新节点 style,方法内为空操作;保留以与其它 DSL 操作 API 一致)
|
|
973
|
+
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
|
841
974
|
* @returns 当前组件节点配置
|
|
842
975
|
*/
|
|
843
|
-
public async alignCenter(
|
|
844
|
-
|
|
845
|
-
|
|
976
|
+
public async alignCenter(
|
|
977
|
+
config: MNode | MNode[],
|
|
978
|
+
{ doNotSelect = false, doNotPushHistory = false, historyDescription, historySource }: DslOpOptions = {},
|
|
979
|
+
): Promise<MNode | MNode[]> {
|
|
846
980
|
const nodes = Array.isArray(config) ? config : [config];
|
|
847
981
|
const stage = this.get('stage');
|
|
848
982
|
|
|
849
983
|
const newNodes = await Promise.all(nodes.map((node) => this.doAlignCenter(node)));
|
|
850
984
|
|
|
851
|
-
const newNode = await this.update(newNodes);
|
|
985
|
+
const newNode = await this.update(newNodes, { doNotPushHistory, historyDescription, historySource });
|
|
852
986
|
|
|
853
987
|
if (!doNotSelect) {
|
|
854
988
|
if (newNodes.length > 1) {
|
|
@@ -864,8 +998,13 @@ class Editor extends BaseService {
|
|
|
864
998
|
/**
|
|
865
999
|
* 移动当前选中节点位置
|
|
866
1000
|
* @param offset 偏移量
|
|
1001
|
+
* @param options 可选配置
|
|
1002
|
+
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
|
867
1003
|
*/
|
|
868
|
-
public async moveLayer(
|
|
1004
|
+
public async moveLayer(
|
|
1005
|
+
offset: number | LayerOffset,
|
|
1006
|
+
{ doNotPushHistory = false, historyDescription, historySource }: DslOpOptions = {},
|
|
1007
|
+
): Promise<void> {
|
|
869
1008
|
this.captureSelectionBeforeOp();
|
|
870
1009
|
|
|
871
1010
|
const root = this.get('root');
|
|
@@ -902,94 +1041,135 @@ class Editor extends BaseService {
|
|
|
902
1041
|
});
|
|
903
1042
|
|
|
904
1043
|
this.addModifiedNodeId(parent.id);
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1044
|
+
if (!doNotPushHistory) {
|
|
1045
|
+
const pageForOp = this.getNodeInfo(node.id, false).page;
|
|
1046
|
+
this.pushOpHistory(
|
|
1047
|
+
'update',
|
|
1048
|
+
|
|
1049
|
+
{
|
|
1050
|
+
diff: buildUpdateDiff([{ oldNode: oldParent, newNode: cloneDeep(toRaw(parent)) }]),
|
|
1051
|
+
pageData: { name: pageForOp?.name || '', id: pageForOp!.id },
|
|
1052
|
+
historyDescription,
|
|
1053
|
+
source: historySource,
|
|
1054
|
+
},
|
|
1055
|
+
);
|
|
1056
|
+
} else {
|
|
1057
|
+
this.selectionBeforeOp = null;
|
|
1058
|
+
}
|
|
913
1059
|
|
|
914
1060
|
this.emit('move-layer', offset);
|
|
1061
|
+
this.emit('change', {
|
|
1062
|
+
type: 'move-layer',
|
|
1063
|
+
data: [{ node, page: this.getPageOfNode(node.id) }],
|
|
1064
|
+
offset,
|
|
1065
|
+
});
|
|
915
1066
|
}
|
|
916
1067
|
|
|
917
1068
|
/**
|
|
918
|
-
*
|
|
919
|
-
*
|
|
1069
|
+
* 移动一个或多个节点到指定容器中。
|
|
1070
|
+
*
|
|
1071
|
+
* 多选场景(config 是数组)只会产生一条历史记录,
|
|
1072
|
+
* `updatedItems` 涵盖所有源父容器 + 目标容器的前后快照。
|
|
1073
|
+
* 这避免了"多选移动到某容器"在历史栈里被切成 N 条记录。
|
|
1074
|
+
*
|
|
1075
|
+
* @param config 需要移动的节点(或节点数组,各项需带 id;style 等字段会与原节点合并)
|
|
920
1076
|
* @param targetId 容器ID
|
|
921
1077
|
* @param options 可选配置
|
|
922
1078
|
* @param options.doNotSelect 移动后是否不更新当前选中节点(默认 false)
|
|
923
1079
|
* @param options.doNotSwitchPage 移动后是否不切换当前页面(默认 false;目标容器位于其它页面时为 true 会跳过自动选中以避免页面切换)
|
|
1080
|
+
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
|
924
1081
|
*/
|
|
925
|
-
public async moveToContainer(
|
|
926
|
-
|
|
1082
|
+
public async moveToContainer(
|
|
1083
|
+
config: MNode | MNode[],
|
|
1084
|
+
targetId: Id,
|
|
1085
|
+
{
|
|
1086
|
+
doNotSelect = false,
|
|
1087
|
+
doNotSwitchPage = false,
|
|
1088
|
+
doNotPushHistory = false,
|
|
1089
|
+
historyDescription,
|
|
1090
|
+
historySource,
|
|
1091
|
+
}: DslOpOptions = {},
|
|
1092
|
+
): Promise<MNode | MNode[]> {
|
|
1093
|
+
const isBatch = Array.isArray(config);
|
|
1094
|
+
const configs = (isBatch ? config : [config]).filter((item) => !(isPage(item) || isPageFragment(item)));
|
|
1095
|
+
|
|
1096
|
+
if (configs.length === 0) {
|
|
1097
|
+
throw new Error('没有可移动的节点');
|
|
1098
|
+
}
|
|
927
1099
|
|
|
928
1100
|
this.captureSelectionBeforeOp();
|
|
929
1101
|
|
|
930
|
-
const root = this.get('root');
|
|
931
|
-
const { node, parent, page: pageForOp } = this.getNodeInfo(config.id, false);
|
|
932
1102
|
const target = this.getNodeById(targetId, false) as MContainer;
|
|
933
1103
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
const oldTarget = cloneDeep(toRaw(target));
|
|
938
|
-
|
|
939
|
-
const index = getNodeIndex(node.id, parent);
|
|
940
|
-
parent.items?.splice(index, 1);
|
|
941
|
-
|
|
942
|
-
await stage.remove({ id: node.id, parentId: parent.id, root: cloneDeep(root) });
|
|
1104
|
+
if (!target) {
|
|
1105
|
+
throw new Error('目标容器不存在');
|
|
1106
|
+
}
|
|
943
1107
|
|
|
944
|
-
|
|
1108
|
+
const root = this.get('root');
|
|
1109
|
+
const stage = this.get('stage');
|
|
945
1110
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
}
|
|
950
|
-
});
|
|
951
|
-
newConfig.style = getInitPositionStyle(newConfig.style, layout);
|
|
1111
|
+
if (!root || !stage) {
|
|
1112
|
+
throw new Error('root 或 stage为空');
|
|
1113
|
+
}
|
|
952
1114
|
|
|
953
|
-
|
|
1115
|
+
// 收集 (节点, 源父) 信息,过滤掉异常节点(找不到父或源父等于目标本身)
|
|
1116
|
+
const moves: MoveItem[] = [];
|
|
1117
|
+
for (const { id } of configs) {
|
|
1118
|
+
const { node, parent, page } = this.getNodeInfo(id, false);
|
|
1119
|
+
if (!node || !parent) continue;
|
|
1120
|
+
moves.push({ node, parent, pageForOp: page ? { name: page.name || '', id: page.id } : null });
|
|
1121
|
+
}
|
|
954
1122
|
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1123
|
+
if (moves.length === 0) {
|
|
1124
|
+
throw new Error('没有可移动的节点');
|
|
1125
|
+
}
|
|
958
1126
|
|
|
959
|
-
|
|
960
|
-
|
|
1127
|
+
// 记录所有涉及的源父容器(按 id 去重)+ 目标容器的前置快照;同一父容器只快照一次。
|
|
1128
|
+
const beforeSnapshots = new Map<Id, MNode>();
|
|
1129
|
+
beforeSnapshots.set(target.id, cloneDeep(toRaw(target)));
|
|
1130
|
+
for (const { parent } of moves) {
|
|
1131
|
+
if (!beforeSnapshots.has(parent.id)) {
|
|
1132
|
+
beforeSnapshots.set(parent.id, cloneDeep(toRaw(parent)));
|
|
961
1133
|
}
|
|
1134
|
+
}
|
|
962
1135
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1136
|
+
let newConfigs: MNode[] = [];
|
|
1137
|
+
|
|
1138
|
+
const moveNodes = moves.map(({ node }) => node);
|
|
1139
|
+
await this.remove(moveNodes, { doNotPushHistory: true, doNotSelect, doNotSwitchPage: true });
|
|
1140
|
+
|
|
1141
|
+
newConfigs = (await this.add(moveNodes, target, {
|
|
1142
|
+
doNotPushHistory: true,
|
|
1143
|
+
doNotSelect,
|
|
1144
|
+
doNotSwitchPage,
|
|
1145
|
+
})) as MNode[];
|
|
1146
|
+
|
|
1147
|
+
if (!doNotPushHistory) {
|
|
1148
|
+
// 整批只入栈一条历史:updatedItems 包含所有源父容器 + 目标容器的前后快照(撤销/重做最小依赖)。
|
|
1149
|
+
const updatedItems = Array.from(beforeSnapshots.entries()).map(([id, oldNode]) => ({
|
|
1150
|
+
oldNode,
|
|
1151
|
+
newNode: cloneDeep(toRaw(this.getNodeById(id, false))) as MNode,
|
|
1152
|
+
}));
|
|
1153
|
+
const historyPage = moves[0].pageForOp ?? { name: '', id: target.id };
|
|
1154
|
+
this.pushOpHistory('update', {
|
|
1155
|
+
diff: buildUpdateDiff(updatedItems),
|
|
1156
|
+
pageData: historyPage,
|
|
1157
|
+
historyDescription,
|
|
1158
|
+
source: historySource,
|
|
968
1159
|
});
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
await this.select(newConfig);
|
|
972
|
-
stage.select(newConfig.id);
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
this.addModifiedNodeId(target.id);
|
|
976
|
-
this.addModifiedNodeId(parent.id);
|
|
977
|
-
this.pushOpHistory(
|
|
978
|
-
'update',
|
|
979
|
-
{
|
|
980
|
-
updatedItems: [
|
|
981
|
-
{ oldNode: oldSourceParent, newNode: cloneDeep(toRaw(parent)) },
|
|
982
|
-
{ oldNode: oldTarget, newNode: cloneDeep(toRaw(target)) },
|
|
983
|
-
],
|
|
984
|
-
},
|
|
985
|
-
{ name: pageForOp?.name || '', id: pageForOp!.id },
|
|
986
|
-
);
|
|
987
|
-
|
|
988
|
-
return newConfig;
|
|
1160
|
+
} else {
|
|
1161
|
+
this.selectionBeforeOp = null;
|
|
989
1162
|
}
|
|
1163
|
+
|
|
1164
|
+
return isBatch ? newConfigs : newConfigs[0];
|
|
990
1165
|
}
|
|
991
1166
|
|
|
992
|
-
public async dragTo(
|
|
1167
|
+
public async dragTo(
|
|
1168
|
+
config: MNode | MNode[],
|
|
1169
|
+
targetParent: MContainer,
|
|
1170
|
+
targetIndex: number,
|
|
1171
|
+
{ doNotPushHistory = false, historyDescription, historySource }: DslOpOptions = {},
|
|
1172
|
+
) {
|
|
993
1173
|
this.captureSelectionBeforeOp();
|
|
994
1174
|
|
|
995
1175
|
if (!targetParent || !Array.isArray(targetParent.items)) return;
|
|
@@ -1049,18 +1229,115 @@ class Editor extends BaseService {
|
|
|
1049
1229
|
updatedItems.push({ oldNode, newNode: cloneDeep(toRaw(newNode)) });
|
|
1050
1230
|
}
|
|
1051
1231
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1232
|
+
if (!doNotPushHistory) {
|
|
1233
|
+
const pageForOp = this.getNodeInfo(configs[0].id, false).page;
|
|
1234
|
+
this.pushOpHistory('update', {
|
|
1235
|
+
diff: buildUpdateDiff(updatedItems),
|
|
1236
|
+
pageData: { name: pageForOp?.name || '', id: pageForOp!.id },
|
|
1237
|
+
historyDescription,
|
|
1238
|
+
source: historySource,
|
|
1239
|
+
});
|
|
1240
|
+
} else {
|
|
1241
|
+
this.selectionBeforeOp = null;
|
|
1242
|
+
}
|
|
1054
1243
|
|
|
1055
1244
|
this.emit('drag-to', { targetIndex, configs, targetParent });
|
|
1245
|
+
this.emit('change', {
|
|
1246
|
+
type: 'drag-to',
|
|
1247
|
+
data: configs.map((node) => ({ node, page: this.getPageOfNode(node.id) })),
|
|
1248
|
+
targetIndex,
|
|
1249
|
+
targetParent,
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// #region AndGetHistoryId
|
|
1254
|
+
/**
|
|
1255
|
+
* 下列 *AndGetHistoryId 方法与对应的普通操作(add / remove / update ...)行为完全一致,
|
|
1256
|
+
* 返回值在 {@link DslOpWithHistoryIdsResult} 中同时包含原操作结果与本次写入历史栈的 uuid 列表({@link StepValue.uuid}),
|
|
1257
|
+
* 可用于精确引用 / 定位该条历史记录(埋点、revert、跨端同步等)。
|
|
1258
|
+
*
|
|
1259
|
+
* 当本次操作未写入历史(doNotPushHistory 为 true、或操作无实际变更 / 提前返回)时 historyIds 为 `[]`。
|
|
1260
|
+
*/
|
|
1261
|
+
|
|
1262
|
+
/** 等价于 {@link add},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
|
|
1263
|
+
public async addAndGetHistoryId(
|
|
1264
|
+
addNode: AddMNode | MNode[],
|
|
1265
|
+
parent?: MContainer | null,
|
|
1266
|
+
options: DslOpOptions = {},
|
|
1267
|
+
): Promise<DslOpWithHistoryIdsResult<MNode | MNode[]>> {
|
|
1268
|
+
this.lastPushedHistoryId = null;
|
|
1269
|
+
const result = await this.add(addNode, parent, options);
|
|
1270
|
+
return { result, historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId) };
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/** 等价于 {@link remove},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
|
|
1274
|
+
public async removeAndGetHistoryId(
|
|
1275
|
+
nodeOrNodeList: MNode | MNode[],
|
|
1276
|
+
options: DslOpOptions = {},
|
|
1277
|
+
): Promise<DslOpWithHistoryIdsResult<void>> {
|
|
1278
|
+
this.lastPushedHistoryId = null;
|
|
1279
|
+
await this.remove(nodeOrNodeList, options);
|
|
1280
|
+
return { result: undefined, historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId) };
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/** 等价于 {@link update},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
|
|
1284
|
+
public async updateAndGetHistoryId(
|
|
1285
|
+
config: MNode | MNode[],
|
|
1286
|
+
data: {
|
|
1287
|
+
changeRecords?: ChangeRecord[];
|
|
1288
|
+
changeRecordList?: ChangeRecord[][];
|
|
1289
|
+
doNotPushHistory?: boolean;
|
|
1290
|
+
historyDescription?: string;
|
|
1291
|
+
historySource?: HistoryOpSource;
|
|
1292
|
+
} = {},
|
|
1293
|
+
): Promise<DslOpWithHistoryIdsResult<MNode | MNode[]>> {
|
|
1294
|
+
this.lastPushedHistoryId = null;
|
|
1295
|
+
const result = await this.update(config, data);
|
|
1296
|
+
return { result, historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId) };
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
/** 等价于 {@link moveLayer},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
|
|
1300
|
+
public async moveLayerAndGetHistoryId(
|
|
1301
|
+
offset: number | LayerOffset,
|
|
1302
|
+
options: DslOpOptions = {},
|
|
1303
|
+
): Promise<DslOpWithHistoryIdsResult<void>> {
|
|
1304
|
+
this.lastPushedHistoryId = null;
|
|
1305
|
+
await this.moveLayer(offset, options);
|
|
1306
|
+
return { result: undefined, historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId) };
|
|
1056
1307
|
}
|
|
1057
1308
|
|
|
1309
|
+
/** 等价于 {@link moveToContainer},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
|
|
1310
|
+
public async moveToContainerAndGetHistoryId(
|
|
1311
|
+
config: MNode | MNode[],
|
|
1312
|
+
targetId: Id,
|
|
1313
|
+
options: DslOpOptions = {},
|
|
1314
|
+
): Promise<DslOpWithHistoryIdsResult<MNode | MNode[]>> {
|
|
1315
|
+
this.lastPushedHistoryId = null;
|
|
1316
|
+
const result = await this.moveToContainer(config, targetId, options);
|
|
1317
|
+
return { result, historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId) };
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/** 等价于 {@link dragTo},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
|
|
1321
|
+
public async dragToAndGetHistoryId(
|
|
1322
|
+
config: MNode | MNode[],
|
|
1323
|
+
targetParent: MContainer,
|
|
1324
|
+
targetIndex: number,
|
|
1325
|
+
options: DslOpOptions = {},
|
|
1326
|
+
): Promise<DslOpWithHistoryIdsResult<void>> {
|
|
1327
|
+
this.lastPushedHistoryId = null;
|
|
1328
|
+
await this.dragTo(config, targetParent, targetIndex, options);
|
|
1329
|
+
return { result: undefined, historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId) };
|
|
1330
|
+
}
|
|
1331
|
+
// #endregion AndGetHistoryId
|
|
1332
|
+
|
|
1058
1333
|
/**
|
|
1059
1334
|
* 撤销当前操作
|
|
1060
1335
|
* @returns 被撤销的操作
|
|
1061
1336
|
*/
|
|
1062
1337
|
public async undo(): Promise<StepValue | null> {
|
|
1063
|
-
const
|
|
1338
|
+
const pageId = this.get('page')?.id;
|
|
1339
|
+
if (pageId === undefined) return null;
|
|
1340
|
+
const value = historyService.undo('page', pageId);
|
|
1064
1341
|
if (value) {
|
|
1065
1342
|
await this.applyHistoryOp(value, true);
|
|
1066
1343
|
}
|
|
@@ -1072,21 +1349,188 @@ class Editor extends BaseService {
|
|
|
1072
1349
|
* @returns 被恢复的操作
|
|
1073
1350
|
*/
|
|
1074
1351
|
public async redo(): Promise<StepValue | null> {
|
|
1075
|
-
const
|
|
1352
|
+
const pageId = this.get('page')?.id;
|
|
1353
|
+
if (pageId === undefined) return null;
|
|
1354
|
+
const value = historyService.redo('page', pageId);
|
|
1076
1355
|
if (value) {
|
|
1077
1356
|
await this.applyHistoryOp(value, false);
|
|
1078
1357
|
}
|
|
1079
1358
|
return value;
|
|
1080
1359
|
}
|
|
1081
1360
|
|
|
1082
|
-
|
|
1361
|
+
/**
|
|
1362
|
+
* 「回滚」指定页面历史步骤(类 git revert 语义):
|
|
1363
|
+
* - 不动原始历史栈结构(不移动 cursor、不丢弃任何步骤);
|
|
1364
|
+
* - 取出 `index` 对应的 step,**反向应用**一次(add→remove / remove→add / update→旧值);
|
|
1365
|
+
* - 把这次反向应用作为一条**新步骤**追加到栈顶,可被普通 undo / redo。
|
|
1366
|
+
*
|
|
1367
|
+
* 与 `gotoPageStep`(类 git reset)的区别在于此操作**不丢弃**目标之后的历史。
|
|
1368
|
+
* 与 `applyHistoryOp(reverse=true)` 的区别在于:本方法**不带** `doNotPushHistory`,
|
|
1369
|
+
* 反向应用会以一条新 step 入栈;并且不实施 step 中保存的选区与 modifiedNodeIds 状态,
|
|
1370
|
+
* 选区由用户当前位置决定,符合"新提交"语义。
|
|
1371
|
+
*
|
|
1372
|
+
* 仅对处于「已应用」状态的步骤生效——未应用的步骤本身就不存在于当前 DSL 中,反向无意义。
|
|
1373
|
+
*
|
|
1374
|
+
* @param index 目标 step 在所属页面栈中的索引(0 为最早),通常由历史面板传入
|
|
1375
|
+
* @returns 反向后产生的新 step;目标不存在 / 未应用 / 反向失败时返回 null
|
|
1376
|
+
*/
|
|
1377
|
+
public async revertPageStep(index: number): Promise<StepValue | null> {
|
|
1378
|
+
const list = historyService.getStepList('page', this.get('page')?.id);
|
|
1379
|
+
const entry = list[index];
|
|
1380
|
+
if (!entry?.applied) return null;
|
|
1381
|
+
|
|
1382
|
+
const { step } = entry;
|
|
1383
|
+
// 初始基线(index 0 的 initial step)是栈底线,不可回滚。
|
|
1384
|
+
if (step.opType === 'initial') return null;
|
|
1385
|
+
const root = this.get('root');
|
|
1386
|
+
if (!root) return null;
|
|
1387
|
+
|
|
1388
|
+
// 更新类步骤必须带 changeRecords 才支持回滚:缺失时只能整节点替换,会冲掉后续无关变更,故不支持。
|
|
1389
|
+
if (step.opType === 'update') {
|
|
1390
|
+
const items = step.diff ?? [];
|
|
1391
|
+
if (!items.length || !items.every((item) => item.changeRecords?.length)) return null;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// 反向应用产生的新 step 由内部 pushOpHistory 触发 history `change` 事件,监听一次以拿到引用。
|
|
1395
|
+
let revertedStep: StepValue | null = null;
|
|
1396
|
+
// page 的 `change` 事件回调签名为 `(pageId, step)`,这里只关心被回滚产生的新 step。
|
|
1397
|
+
const captureRevert = (_pageId: Id, s: StepValue) => {
|
|
1398
|
+
revertedStep = s;
|
|
1399
|
+
};
|
|
1400
|
+
historyService.once('change', captureRevert);
|
|
1401
|
+
|
|
1402
|
+
const historyDescription = `回滚 #${index + 1}: ${describeStepForRevert(step)}`;
|
|
1403
|
+
// revert 走 public add/remove/update,让操作以一条普通新 step 入栈;不要切换选区与页面,避免打断用户。
|
|
1404
|
+
const opts = { doNotSelect: true, doNotSwitchPage: true, historyDescription, historySource: 'rollback' } as const;
|
|
1405
|
+
|
|
1406
|
+
try {
|
|
1407
|
+
switch (step.opType) {
|
|
1408
|
+
case 'add': {
|
|
1409
|
+
// 原本是新增 → revert 即删除当时被加入的节点
|
|
1410
|
+
for (const { newSchema } of step.diff ?? []) {
|
|
1411
|
+
if (!newSchema) continue;
|
|
1412
|
+
const existing = this.getNodeById(newSchema.id, false);
|
|
1413
|
+
if (existing) {
|
|
1414
|
+
await this.remove(existing, opts);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
break;
|
|
1418
|
+
}
|
|
1419
|
+
case 'remove': {
|
|
1420
|
+
// 原本是删除 → revert 即把节点按原父容器加回来。
|
|
1421
|
+
// 按原 index 升序逐个插回,先小后大避免索引漂移。
|
|
1422
|
+
const items = step.diff ?? [];
|
|
1423
|
+
const sorted = [...items].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
1424
|
+
for (const { oldSchema, parentId } of sorted) {
|
|
1425
|
+
if (!oldSchema || parentId === undefined) continue;
|
|
1426
|
+
const parent = this.getNodeById(parentId, false) as MContainer | null;
|
|
1427
|
+
if (parent) {
|
|
1428
|
+
await this.add([cloneDeep(oldSchema)] as MNode[], parent, opts);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
break;
|
|
1432
|
+
}
|
|
1433
|
+
case 'update': {
|
|
1434
|
+
// 原本是更新 → revert 即把 oldSchema 的值写回;
|
|
1435
|
+
// 优先按 changeRecords 局部 patch(仅触达 propPath 下的字段,避免冲掉同节点上其它无关变更)。
|
|
1436
|
+
const items = step.diff ?? [];
|
|
1437
|
+
const configs = items
|
|
1438
|
+
.filter((item) => item.oldSchema && item.newSchema)
|
|
1439
|
+
.map(({ oldSchema, newSchema, changeRecords }) => {
|
|
1440
|
+
const oldNode = oldSchema!;
|
|
1441
|
+
const newNode = newSchema!;
|
|
1442
|
+
if (changeRecords?.length) {
|
|
1443
|
+
const patch: MNode = { id: newNode.id, type: newNode.type };
|
|
1444
|
+
for (const record of changeRecords) {
|
|
1445
|
+
if (!record.propPath) {
|
|
1446
|
+
// 没有 propPath 视为整节点替换
|
|
1447
|
+
return cloneDeep(oldNode);
|
|
1448
|
+
}
|
|
1449
|
+
const value = cloneDeep(getValueByKeyPath(record.propPath, oldNode));
|
|
1450
|
+
setValueByKeyPath(record.propPath, value, patch);
|
|
1451
|
+
}
|
|
1452
|
+
return patch;
|
|
1453
|
+
}
|
|
1454
|
+
return cloneDeep(oldNode);
|
|
1455
|
+
});
|
|
1456
|
+
if (configs.length) {
|
|
1457
|
+
await this.update(configs, { historyDescription, historySource: 'rollback' });
|
|
1458
|
+
}
|
|
1459
|
+
break;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
} finally {
|
|
1463
|
+
historyService.off('change', captureRevert);
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// 通知一次 history-change,让上层(如属性面板)按当前最新 DSL 刷新
|
|
1467
|
+
const page = toRaw(this.get('page'));
|
|
1468
|
+
if (page) {
|
|
1469
|
+
this.emit('history-change', page as MPage | MPageFragment);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
return revertedStep;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
/**
|
|
1476
|
+
* 通过历史记录 uuid 回滚当前页面的历史步骤,语义与 {@link revertPageStep} 完全一致,
|
|
1477
|
+
* 仅入参从 index 改为 uuid 列表({@link StepValue.uuid})。按数组顺序依次回滚,
|
|
1478
|
+
* 返回与入参同序的结果列表(某项失败时为 `null`)。
|
|
1479
|
+
*
|
|
1480
|
+
* @param uuids 目标历史记录的 uuid 列表,通常由 *AndGetHistoryId 方法返回的 `historyIds`
|
|
1481
|
+
*/
|
|
1482
|
+
public async revertPageStepById(uuids: string[]): Promise<(StepValue | null)[]> {
|
|
1483
|
+
const results: (StepValue | null)[] = [];
|
|
1484
|
+
const pageId = this.get('page')?.id;
|
|
1485
|
+
for (const uuid of uuids) {
|
|
1486
|
+
const location = historyService.findStepLocationByUuid('page', uuid, pageId);
|
|
1487
|
+
results.push(!location ? null : await this.revertPageStep(location.index));
|
|
1488
|
+
}
|
|
1489
|
+
return results;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/**
|
|
1493
|
+
* 跳转当前页面历史栈到指定游标位置。
|
|
1494
|
+
*
|
|
1495
|
+
* `targetCursor` 与 `UndoRedo.getCursor()` 同义:表示"已应用步骤数量",
|
|
1496
|
+
* 取值范围 `[0, length]`。当目标 < 当前游标时循环 undo,否则循环 redo。
|
|
1497
|
+
* 通常由历史面板传入「点击的 step.index + 1」作为目标。
|
|
1498
|
+
*
|
|
1499
|
+
* @returns 实际移动到的最终游标位置
|
|
1500
|
+
*/
|
|
1501
|
+
public async gotoPageStep(targetCursor: number): Promise<number> {
|
|
1502
|
+
const pageId = this.get('page')?.id;
|
|
1503
|
+
let cursor = historyService.getCursor('page', pageId);
|
|
1504
|
+
const { length } = historyService.getStepList('page', pageId);
|
|
1505
|
+
const target = Math.max(0, Math.min(targetCursor, length));
|
|
1506
|
+
while (cursor > target) {
|
|
1507
|
+
const step = await this.undo();
|
|
1508
|
+
if (!step) break;
|
|
1509
|
+
cursor -= 1;
|
|
1510
|
+
}
|
|
1511
|
+
while (cursor < target) {
|
|
1512
|
+
const step = await this.redo();
|
|
1513
|
+
if (!step) break;
|
|
1514
|
+
cursor += 1;
|
|
1515
|
+
}
|
|
1516
|
+
return cursor;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
public async move(
|
|
1520
|
+
left: number,
|
|
1521
|
+
top: number,
|
|
1522
|
+
{ doNotPushHistory = false, historyDescription, historySource }: DslOpOptions = {},
|
|
1523
|
+
) {
|
|
1083
1524
|
const node = toRaw(this.get('node'));
|
|
1084
1525
|
if (!node || isPage(node)) return;
|
|
1085
1526
|
|
|
1086
1527
|
const newStyle = calcMoveStyle(node.style || {}, left, top);
|
|
1087
1528
|
if (!newStyle) return;
|
|
1088
1529
|
|
|
1089
|
-
await this.update(
|
|
1530
|
+
await this.update(
|
|
1531
|
+
{ id: node.id, type: node.type, style: newStyle },
|
|
1532
|
+
{ doNotPushHistory, historyDescription, historySource },
|
|
1533
|
+
);
|
|
1090
1534
|
}
|
|
1091
1535
|
|
|
1092
1536
|
public resetState() {
|
|
@@ -1134,74 +1578,272 @@ class Editor extends BaseService {
|
|
|
1134
1578
|
}
|
|
1135
1579
|
|
|
1136
1580
|
private addModifiedNodeId(id: Id) {
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1581
|
+
this.get('modifiedNodeIds').set(id, id);
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
/**
|
|
1585
|
+
* 获取指定节点所属的页面 / 页面片:
|
|
1586
|
+
* - 普通节点返回其所在的 page;
|
|
1587
|
+
* - 节点本身就是 page / pageFragment 时返回它自己;
|
|
1588
|
+
* - 找不到时返回 null。
|
|
1589
|
+
* 供 `change` 事件携带「变更节点对应的 page」(而非编辑器当前选中页)。
|
|
1590
|
+
*/
|
|
1591
|
+
private getPageOfNode(id: Id): MPage | MPageFragment | null {
|
|
1592
|
+
const { node, page } = this.getNodeInfo(id, false);
|
|
1593
|
+
if (page) return page;
|
|
1594
|
+
if (node && (isPage(node) || isPageFragment(node))) return node as MPage | MPageFragment;
|
|
1595
|
+
return null;
|
|
1140
1596
|
}
|
|
1141
1597
|
|
|
1142
1598
|
private captureSelectionBeforeOp() {
|
|
1143
|
-
if (this.
|
|
1599
|
+
if (this.selectionBeforeOp) return;
|
|
1144
1600
|
this.selectionBeforeOp = this.get('nodes').map((n) => n.id);
|
|
1145
1601
|
}
|
|
1146
1602
|
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1603
|
+
/**
|
|
1604
|
+
* 比较「上一次 root」与「新 root」的页面 / 页面片,按页面粒度把整体替换拆成历史记录:
|
|
1605
|
+
* - 新旧都存在且内容变化的页面 → 一条 `update`(整页快照替换,无 changeRecords);
|
|
1606
|
+
* - 仅新 root 存在的页面 → 一条 `add`;
|
|
1607
|
+
* - 仅旧 root 存在的页面 → 一条 `remove`。
|
|
1608
|
+
*
|
|
1609
|
+
* 每条记录落到对应页面自己的历史栈(与普通节点操作一致),并标记来源 `source`。
|
|
1610
|
+
* 内容未变化的页面不产生记录,避免重复设置相同 DSL 时产生噪声。
|
|
1611
|
+
*/
|
|
1612
|
+
private pushRootDiffHistory(preRoot: MApp, nextRoot: MApp, source?: HistoryOpSource): void {
|
|
1613
|
+
const prevPages = preRoot?.items || [];
|
|
1614
|
+
const nextPages = nextRoot?.items || [];
|
|
1615
|
+
const prevMap = new Map(prevPages.map((p) => [`${p.id}`, p]));
|
|
1616
|
+
const nextMap = new Map(nextPages.map((p) => [`${p.id}`, p]));
|
|
1617
|
+
const indexInItems = (root: MApp, id: Id) => (root.items ?? []).findIndex((item) => `${item.id}` === `${id}`);
|
|
1618
|
+
|
|
1619
|
+
nextPages.forEach((nextPage) => {
|
|
1620
|
+
const prevPage = prevMap.get(`${nextPage.id}`);
|
|
1621
|
+
if (!prevPage) {
|
|
1622
|
+
this.pushPageDiffStep(
|
|
1623
|
+
'add',
|
|
1624
|
+
nextPage,
|
|
1625
|
+
{ newSchema: cloneDeep(toRaw(nextPage)), parentId: nextRoot.id, index: indexInItems(nextRoot, nextPage.id) },
|
|
1626
|
+
source,
|
|
1627
|
+
);
|
|
1628
|
+
} else if (!isEqual(toRaw(prevPage), toRaw(nextPage))) {
|
|
1629
|
+
this.pushPageDiffStep(
|
|
1630
|
+
'update',
|
|
1631
|
+
nextPage,
|
|
1632
|
+
{ oldSchema: cloneDeep(toRaw(prevPage)), newSchema: cloneDeep(toRaw(nextPage)) },
|
|
1633
|
+
source,
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
});
|
|
1637
|
+
|
|
1638
|
+
prevPages.forEach((prevPage) => {
|
|
1639
|
+
if (!nextMap.has(`${prevPage.id}`)) {
|
|
1640
|
+
this.pushPageDiffStep(
|
|
1641
|
+
'remove',
|
|
1642
|
+
prevPage,
|
|
1643
|
+
{ oldSchema: cloneDeep(toRaw(prevPage)), parentId: preRoot.id, index: indexInItems(preRoot, prevPage.id) },
|
|
1644
|
+
source,
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
/**
|
|
1651
|
+
* 构造一条页面级「set root」历史记录(不携带选区 / modifiedNodeIds 上下文)并落到该页面自己的栈。
|
|
1652
|
+
*
|
|
1653
|
+
* 连续 set root 替换:若该页栈最新一条已是**同来源**的 set root 记录({@link StepValue.rootStep} 且 `source` 相同),
|
|
1654
|
+
* 则用本次记录**替换**它而非新增,避免源码反复保存 / 外部重设 DSL 时堆积多条 root 记录;
|
|
1655
|
+
* 来源不同则照常新增(initial 基线不是 rootStep,不在此列)。
|
|
1656
|
+
*/
|
|
1657
|
+
private pushPageDiffStep(
|
|
1658
|
+
opType: HistoryOpType,
|
|
1659
|
+
page: MPage | MPageFragment,
|
|
1660
|
+
diffItem: StepDiffItem<MNode>,
|
|
1661
|
+
source?: HistoryOpSource,
|
|
1662
|
+
): void {
|
|
1663
|
+
const step: StepValue = {
|
|
1664
|
+
uuid: guid(),
|
|
1665
|
+
data: { name: page.name || '', id: page.id },
|
|
1666
|
+
opType,
|
|
1667
|
+
extra: {
|
|
1668
|
+
selectedBefore: [],
|
|
1669
|
+
selectedAfter: [],
|
|
1670
|
+
modifiedNodeIds: new Map(),
|
|
1671
|
+
},
|
|
1672
|
+
diff: [diffItem],
|
|
1673
|
+
rootStep: true,
|
|
1674
|
+
};
|
|
1675
|
+
if (source) step.source = source;
|
|
1676
|
+
|
|
1677
|
+
const top = historyService.getCurrentPageStep(page.id);
|
|
1678
|
+
if (top?.rootStep && top.source === source) {
|
|
1679
|
+
historyService.replaceCurrentStep('page', step, page.id);
|
|
1680
|
+
} else {
|
|
1681
|
+
historyService.push('page', step, page.id);
|
|
1151
1682
|
}
|
|
1683
|
+
}
|
|
1152
1684
|
|
|
1685
|
+
private pushOpHistory(
|
|
1686
|
+
opType: HistoryOpType,
|
|
1687
|
+
{
|
|
1688
|
+
diff,
|
|
1689
|
+
pageData,
|
|
1690
|
+
historyDescription,
|
|
1691
|
+
source,
|
|
1692
|
+
}: {
|
|
1693
|
+
diff: StepDiffItem<MNode>[];
|
|
1694
|
+
pageData: { name: string; id: Id };
|
|
1695
|
+
historyDescription?: string;
|
|
1696
|
+
source?: HistoryOpSource;
|
|
1697
|
+
},
|
|
1698
|
+
): string | null {
|
|
1153
1699
|
const step: StepValue = {
|
|
1700
|
+
uuid: guid(),
|
|
1154
1701
|
data: pageData,
|
|
1155
1702
|
opType,
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1703
|
+
extra: {
|
|
1704
|
+
selectedBefore: this.selectionBeforeOp ?? [],
|
|
1705
|
+
selectedAfter: this.get('nodes').map((n) => n.id),
|
|
1706
|
+
modifiedNodeIds: new Map(this.get('modifiedNodeIds')),
|
|
1707
|
+
},
|
|
1708
|
+
diff,
|
|
1160
1709
|
};
|
|
1161
|
-
|
|
1710
|
+
if (historyDescription) step.historyDescription = historyDescription;
|
|
1711
|
+
if (source) step.source = source;
|
|
1712
|
+
// 显式按 step.data.id 入栈:跨页操作(如 moveToContainer 从源页搬到目标页)
|
|
1713
|
+
// 必须落到正确的页面栈,否则会把记录错误地推到当前活动页 / 操作发起页。
|
|
1714
|
+
const pushed = historyService.push('page', step, pageData.id);
|
|
1715
|
+
// push 返回 null 表示当前没有可写入的页面栈(未真正入栈),此时不应返回 uuid。
|
|
1716
|
+
const historyId = pushed ? step.uuid : null;
|
|
1717
|
+
this.lastPushedHistoryId = historyId;
|
|
1162
1718
|
this.selectionBeforeOp = null;
|
|
1163
|
-
|
|
1719
|
+
return historyId;
|
|
1164
1720
|
}
|
|
1165
1721
|
|
|
1166
1722
|
/**
|
|
1167
1723
|
* 应用历史操作(撤销 / 重做)
|
|
1724
|
+
*
|
|
1725
|
+
* 所有 DSL 修改都走 `editor.add / remove / update`,并通过 `doNotPushHistory` 阻止再次入栈、
|
|
1726
|
+
* `doNotSelect / doNotSwitchPage` 让选区由方法末尾的统一逻辑兜底。
|
|
1727
|
+
*
|
|
1728
|
+
* 注意:这些公开方法会发出 add / remove / update 事件,业务侧若需要区分"用户操作"与"撤销重做触发",
|
|
1729
|
+
* 请监听 `history-change` 事件配合判断。
|
|
1730
|
+
*
|
|
1168
1731
|
* @param step 操作记录
|
|
1169
1732
|
* @param reverse true = 撤销,false = 重做
|
|
1170
1733
|
*/
|
|
1171
1734
|
private async applyHistoryOp(step: StepValue, reverse: boolean) {
|
|
1172
|
-
|
|
1173
|
-
|
|
1735
|
+
// 初始基线 step 仅作展示,不承载任何变更,撤销/重做时无需应用(正常流程下也不会被触达)。
|
|
1736
|
+
if (step.opType === 'initial') return;
|
|
1174
1737
|
const root = this.get('root');
|
|
1175
1738
|
const stage = this.get('stage');
|
|
1176
1739
|
if (!root) return;
|
|
1177
1740
|
|
|
1178
|
-
const
|
|
1179
|
-
root,
|
|
1180
|
-
stage,
|
|
1181
|
-
getNodeById: (id, raw) => this.getNodeById(id, raw),
|
|
1182
|
-
getNodeInfo: (id, raw) => this.getNodeInfo(id, raw),
|
|
1183
|
-
setRoot: (r) => this.set('root', r),
|
|
1184
|
-
setPage: (p) => this.set('page', p),
|
|
1185
|
-
getPage: () => this.get('page'),
|
|
1186
|
-
};
|
|
1741
|
+
const commonOpts = { doNotSelect: true, doNotSwitchPage: true, doNotPushHistory: true } as const;
|
|
1187
1742
|
|
|
1188
1743
|
switch (step.opType) {
|
|
1189
|
-
case 'add':
|
|
1190
|
-
|
|
1744
|
+
case 'add': {
|
|
1745
|
+
const items = step.diff ?? [];
|
|
1746
|
+
if (reverse) {
|
|
1747
|
+
// 撤销 add:把当时加入的节点删除
|
|
1748
|
+
for (const { newSchema } of items) {
|
|
1749
|
+
if (!newSchema) continue;
|
|
1750
|
+
const existing = this.getNodeById(newSchema.id, false);
|
|
1751
|
+
if (existing) {
|
|
1752
|
+
await this.remove(existing, commonOpts);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
} else {
|
|
1756
|
+
// 重做 add:按记录的 parentId / index 把节点重新插回父容器。
|
|
1757
|
+
// 按目标 index 升序逐个插入,先小后大避免索引漂移
|
|
1758
|
+
const sorted = [...items].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
1759
|
+
for (const { newSchema, parentId, index } of sorted) {
|
|
1760
|
+
if (!newSchema || parentId === undefined) continue;
|
|
1761
|
+
const parent = this.getNodeById(parentId, false) as MContainer | null;
|
|
1762
|
+
if (parent?.items) {
|
|
1763
|
+
if (typeof index === 'number' && index >= 0 && index < parent.items.length) {
|
|
1764
|
+
parent.items.splice(index, 0, cloneDeep(newSchema));
|
|
1765
|
+
} else {
|
|
1766
|
+
parent.items.push(cloneDeep(newSchema));
|
|
1767
|
+
}
|
|
1768
|
+
await stage?.add({
|
|
1769
|
+
config: cloneDeep(newSchema),
|
|
1770
|
+
parent: cloneDeep(parent),
|
|
1771
|
+
parentId: parent.id,
|
|
1772
|
+
root: cloneDeep(root),
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1191
1777
|
break;
|
|
1192
|
-
|
|
1193
|
-
|
|
1778
|
+
}
|
|
1779
|
+
case 'remove': {
|
|
1780
|
+
const items = step.diff ?? [];
|
|
1781
|
+
if (reverse) {
|
|
1782
|
+
// 撤销 remove:按原 index 升序逐个插回(先小后大避免索引漂移)
|
|
1783
|
+
const sorted = [...items].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
1784
|
+
for (const { oldSchema, parentId, index } of sorted) {
|
|
1785
|
+
if (!oldSchema || parentId === undefined) continue;
|
|
1786
|
+
const parent = this.getNodeById(parentId, false) as MContainer | null;
|
|
1787
|
+
if (parent?.items) {
|
|
1788
|
+
parent.items.splice(index ?? parent.items.length, 0, cloneDeep(oldSchema));
|
|
1789
|
+
await stage?.add({
|
|
1790
|
+
config: cloneDeep(oldSchema),
|
|
1791
|
+
parent: cloneDeep(parent),
|
|
1792
|
+
parentId,
|
|
1793
|
+
root: cloneDeep(root),
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
} else {
|
|
1798
|
+
// 重做 remove:再删一次
|
|
1799
|
+
for (const { oldSchema } of items) {
|
|
1800
|
+
if (!oldSchema) continue;
|
|
1801
|
+
const existing = this.getNodeById(oldSchema.id, false);
|
|
1802
|
+
if (existing) {
|
|
1803
|
+
await this.remove(existing, commonOpts);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1194
1807
|
break;
|
|
1195
|
-
|
|
1196
|
-
|
|
1808
|
+
}
|
|
1809
|
+
case 'update': {
|
|
1810
|
+
const items = step.diff ?? [];
|
|
1811
|
+
// 优先按 changeRecords 局部 patch:仅触达 propPath 下的字段,避免整节点替换冲掉同节点上其它无关变更。
|
|
1812
|
+
// 没有 changeRecords 的(如内部 sort/moveLayer/拖动等整节点快照场景)才退化为整节点替换。
|
|
1813
|
+
const configs = items
|
|
1814
|
+
.filter((item) => item.oldSchema && item.newSchema)
|
|
1815
|
+
.map(({ oldSchema, newSchema, changeRecords }) => {
|
|
1816
|
+
const oldNode = oldSchema!;
|
|
1817
|
+
const newNode = newSchema!;
|
|
1818
|
+
if (changeRecords?.length) {
|
|
1819
|
+
const sourceForValues = reverse ? oldNode : newNode;
|
|
1820
|
+
// 仅保留 id / type 作为最小骨架,再按 propPath 写入需要回滚/重做的字段;
|
|
1821
|
+
// 后续 update -> mergeWith 会与现有节点深合并,patch 中未涉及的字段不会被改动。
|
|
1822
|
+
const patch: MNode = { id: newNode.id, type: newNode.type };
|
|
1823
|
+
for (const record of changeRecords) {
|
|
1824
|
+
if (!record.propPath) {
|
|
1825
|
+
// 没有 propPath 视为整节点替换
|
|
1826
|
+
return cloneDeep(sourceForValues);
|
|
1827
|
+
}
|
|
1828
|
+
const value = cloneDeep(getValueByKeyPath(record.propPath, sourceForValues));
|
|
1829
|
+
setValueByKeyPath(record.propPath, value, patch);
|
|
1830
|
+
}
|
|
1831
|
+
return patch;
|
|
1832
|
+
}
|
|
1833
|
+
return cloneDeep(reverse ? oldNode : newNode);
|
|
1834
|
+
});
|
|
1835
|
+
if (configs.length) {
|
|
1836
|
+
await this.update(configs, { doNotPushHistory: true });
|
|
1837
|
+
}
|
|
1197
1838
|
break;
|
|
1839
|
+
}
|
|
1198
1840
|
}
|
|
1199
1841
|
|
|
1200
|
-
this.set('modifiedNodeIds', step.modifiedNodeIds);
|
|
1842
|
+
this.set('modifiedNodeIds', step.extra?.modifiedNodeIds ?? new Map());
|
|
1201
1843
|
|
|
1202
1844
|
const page = toRaw(this.get('page'));
|
|
1203
1845
|
if (page) {
|
|
1204
|
-
const selectIds = reverse ? step.selectedBefore : step.selectedAfter;
|
|
1846
|
+
const selectIds = (reverse ? step.extra?.selectedBefore : step.extra?.selectedAfter) ?? [];
|
|
1205
1847
|
setTimeout(() => {
|
|
1206
1848
|
if (!selectIds.length) return;
|
|
1207
1849
|
|
|
@@ -1216,8 +1858,6 @@ class Editor extends BaseService {
|
|
|
1216
1858
|
}, 0);
|
|
1217
1859
|
this.emit('history-change', page as MPage | MPageFragment);
|
|
1218
1860
|
}
|
|
1219
|
-
|
|
1220
|
-
this.isHistoryStateChange = false;
|
|
1221
1861
|
}
|
|
1222
1862
|
|
|
1223
1863
|
private selectedConfigExceptionHandler(config: MNode | Id): EditorNodeInfo {
|