@tmagic/editor 1.8.0-beta.3 → 1.8.0-beta.5
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/components/CompareForm.vue_vue_type_script_setup_true_lang.js +15 -1
- package/dist/es/components/ToolButton.vue_vue_type_script_setup_true_lang.js +6 -6
- package/dist/es/hooks/use-code-block-edit.js +6 -3
- package/dist/es/hooks/use-data-source-edit.js +5 -2
- package/dist/es/hooks/use-stage.js +6 -3
- package/dist/es/index.js +6 -3
- package/dist/es/initService.js +1 -1
- package/dist/es/layouts/Framework.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/NavMenu.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/history-list/Bucket.vue_vue_type_script_setup_true_lang.js +19 -51
- package/dist/es/layouts/history-list/BucketTab.vue_vue_type_script_setup_true_lang.js +22 -36
- package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +152 -93
- package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +36 -10
- package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +334 -208
- package/dist/es/layouts/history-list/InitialRow.vue_vue_type_script_setup_true_lang.js +28 -7
- package/dist/es/layouts/history-list/PageTab.vue_vue_type_script_setup_true_lang.js +47 -57
- package/dist/es/layouts/history-list/composables.js +118 -25
- package/dist/es/layouts/page-bar/PageBar.vue_vue_type_script_setup_true_lang.js +4 -2
- package/dist/es/layouts/props-panel/PropsPanel.vue_vue_type_script_setup_true_lang.js +5 -1
- package/dist/es/layouts/sidebar/ComponentListPanel.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/sidebar/Sidebar.vue_vue_type_script_setup_true_lang.js +5 -1
- package/dist/es/layouts/sidebar/code-block/CodeBlockList.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/es/layouts/sidebar/code-block/CodeBlockListPanel.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/sidebar/code-block/useContentMenu.js +1 -1
- package/dist/es/layouts/sidebar/data-source/DataSourceListPanel.vue_vue_type_script_setup_true_lang.js +1 -1
- 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 +5 -5
- package/dist/es/layouts/sidebar/layer/LayerNodeTool.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 +1 -1
- package/dist/es/layouts/workspace/viewer/ViewerMenu.vue_vue_type_script_setup_true_lang.js +8 -8
- package/dist/es/services/codeBlock.js +105 -42
- package/dist/es/services/dataSource.js +85 -35
- package/dist/es/services/editor.js +317 -122
- package/dist/es/services/history.js +270 -204
- package/dist/es/services/keybinding.js +3 -3
- package/dist/es/services/ui.js +3 -0
- package/dist/es/style.css +65 -10
- package/dist/es/utils/content-menu.js +17 -9
- package/dist/es/utils/editor.js +35 -1
- package/dist/es/utils/history.js +223 -0
- package/dist/es/utils/indexed-db.js +86 -0
- package/dist/es/utils/undo-redo.js +60 -1
- package/dist/style.css +65 -10
- package/dist/tmagic-editor.umd.cjs +2047 -954
- package/package.json +7 -7
- package/src/components/CompareForm.vue +21 -1
- package/src/components/ToolButton.vue +2 -2
- package/src/hooks/use-code-block-edit.ts +4 -3
- package/src/hooks/use-data-source-edit.ts +2 -2
- package/src/hooks/use-stage.ts +3 -3
- package/src/index.ts +1 -0
- package/src/initService.ts +1 -1
- package/src/layouts/Framework.vue +1 -1
- package/src/layouts/NavMenu.vue +1 -1
- package/src/layouts/history-list/Bucket.vue +34 -67
- package/src/layouts/history-list/BucketTab.vue +46 -51
- package/src/layouts/history-list/GroupRow.vue +150 -98
- package/src/layouts/history-list/HistoryDiffDialog.vue +96 -69
- package/src/layouts/history-list/HistoryListPanel.vue +298 -111
- package/src/layouts/history-list/InitialRow.vue +25 -9
- package/src/layouts/history-list/PageTab.vue +58 -48
- package/src/layouts/history-list/composables.ts +210 -29
- 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 +6 -1
- 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/DataSourceListPanel.vue +1 -1
- 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/workspace/viewer/Stage.vue +1 -1
- package/src/layouts/workspace/viewer/ViewerMenu.vue +8 -8
- package/src/services/codeBlock.ts +129 -35
- package/src/services/dataSource.ts +105 -34
- package/src/services/editor.ts +470 -157
- package/src/services/history.ts +312 -203
- package/src/services/keybinding.ts +3 -3
- package/src/services/ui.ts +7 -0
- package/src/theme/history-list-panel.scss +87 -6
- package/src/theme/page-bar.scss +0 -4
- package/src/theme/props-panel.scss +3 -3
- package/src/type.ts +221 -61
- package/src/utils/content-menu.ts +18 -9
- package/src/utils/editor.ts +41 -1
- package/src/utils/history.ts +298 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/indexed-db.ts +122 -0
- package/src/utils/undo-redo.ts +88 -0
- package/types/index.d.ts +871 -308
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';
|
|
@@ -36,8 +44,10 @@ import type {
|
|
|
36
44
|
DslOpOptions,
|
|
37
45
|
EditorEvents,
|
|
38
46
|
EditorNodeInfo,
|
|
47
|
+
HistoryOpSource,
|
|
39
48
|
HistoryOpType,
|
|
40
49
|
PastePosition,
|
|
50
|
+
StepDiffItem,
|
|
41
51
|
StepValue,
|
|
42
52
|
StoreState,
|
|
43
53
|
StoreStateKey,
|
|
@@ -50,6 +60,7 @@ import {
|
|
|
50
60
|
classifyDragSources,
|
|
51
61
|
collectRelatedNodes,
|
|
52
62
|
COPY_STORAGE_KEY,
|
|
63
|
+
describeStepForRevert,
|
|
53
64
|
editorNodeMergeCustomizer,
|
|
54
65
|
fixNodePosition,
|
|
55
66
|
getInitPositionStyle,
|
|
@@ -67,36 +78,18 @@ import { beforePaste, getAddParent } from '@editor/utils/operator';
|
|
|
67
78
|
type MoveItem = { node: MNode; parent: MContainer; pageForOp: { name: string; id: Id } | null };
|
|
68
79
|
|
|
69
80
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
81
|
+
* 把「变更前后节点快照」列表归一成 update 类型的 {@link StepDiffItem} 列表,供 {@link StepValue.diff} 使用。
|
|
82
|
+
* `changeRecords` 来自 form 端的 propPath/value 列表,撤销/重做时只对这些 propPath 做局部更新;
|
|
83
|
+
* 缺省(未传 / 空数组)才退化为整节点替换。
|
|
72
84
|
*/
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
case 'remove': {
|
|
82
|
-
const count = step.removedItems?.length ?? 0;
|
|
83
|
-
const node = step.removedItems?.[0]?.node;
|
|
84
|
-
const label = node?.name || node?.type || (node?.id !== undefined ? `${node.id}` : '');
|
|
85
|
-
return `还原已删除的 ${count} 个节点${count === 1 && label ? `(${label})` : ''}`;
|
|
86
|
-
}
|
|
87
|
-
case 'update':
|
|
88
|
-
default: {
|
|
89
|
-
const items = step.updatedItems ?? [];
|
|
90
|
-
if (items.length === 1) {
|
|
91
|
-
const { newNode, oldNode, changeRecords } = items[0];
|
|
92
|
-
const target = newNode?.name || newNode?.type || oldNode?.name || oldNode?.type || `${newNode?.id ?? ''}`;
|
|
93
|
-
const propPath = changeRecords?.[0]?.propPath;
|
|
94
|
-
return propPath ? `还原 ${target} · ${propPath}` : `还原 ${target}`;
|
|
95
|
-
}
|
|
96
|
-
return `还原 ${items.length} 个节点的修改`;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
};
|
|
85
|
+
const buildUpdateDiff = (
|
|
86
|
+
items: { oldNode: MNode; newNode: MNode; changeRecords?: ChangeRecord[] }[],
|
|
87
|
+
): StepDiffItem<MNode>[] =>
|
|
88
|
+
items.map(({ oldNode, newNode, changeRecords }) => ({
|
|
89
|
+
oldSchema: oldNode,
|
|
90
|
+
newSchema: newNode,
|
|
91
|
+
...(changeRecords?.length ? { changeRecords } : {}),
|
|
92
|
+
}));
|
|
100
93
|
|
|
101
94
|
class Editor extends BaseService {
|
|
102
95
|
public state: StoreState = reactive({
|
|
@@ -115,6 +108,12 @@ class Editor extends BaseService {
|
|
|
115
108
|
alwaysMultiSelect: false,
|
|
116
109
|
});
|
|
117
110
|
private selectionBeforeOp: Id[] | null = null;
|
|
111
|
+
/**
|
|
112
|
+
* 最近一次 pushOpHistory 写入的历史记录 uuid。
|
|
113
|
+
* 供 *AndGetHistoryId 系列方法在调用普通操作后取回本次产生的历史记录 id;
|
|
114
|
+
* 普通操作不会读取它,调用前由 *AndGetHistoryId 重置为 null。
|
|
115
|
+
*/
|
|
116
|
+
private lastPushedHistoryId: string | null = null;
|
|
118
117
|
|
|
119
118
|
constructor() {
|
|
120
119
|
super(
|
|
@@ -128,8 +127,13 @@ class Editor extends BaseService {
|
|
|
128
127
|
* 设置当前指点节点配置
|
|
129
128
|
* @param name 'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'stage' | 'modifiedNodeIds' | 'pageLength' | 'pageFragmentLength
|
|
130
129
|
* @param value MNode
|
|
130
|
+
* @param options.historySource 设置 root 时,本次变更写入历史记录的「操作来源」(仅 name === 'root' 时生效)
|
|
131
131
|
*/
|
|
132
|
-
public set<K extends StoreStateKey, T extends StoreState[K]>(
|
|
132
|
+
public set<K extends StoreStateKey, T extends StoreState[K]>(
|
|
133
|
+
name: K,
|
|
134
|
+
value: T,
|
|
135
|
+
options: { historySource?: HistoryOpSource } = {},
|
|
136
|
+
) {
|
|
133
137
|
const preValue = this.state[name];
|
|
134
138
|
this.state[name] = value;
|
|
135
139
|
|
|
@@ -148,6 +152,25 @@ class Editor extends BaseService {
|
|
|
148
152
|
this.state.pageLength = getPageList(app).length || 0;
|
|
149
153
|
this.state.pageFragmentLength = getPageFragmentList(app).length || 0;
|
|
150
154
|
this.state.stageLoading = this.state.pageLength !== 0;
|
|
155
|
+
|
|
156
|
+
if (preValue && !isEmpty(preValue)) {
|
|
157
|
+
// 编辑期间再次整体设置 root(源码保存 / 外部重设 DSL / root 节点更新):与上一次 root
|
|
158
|
+
// 做页面级 diff,按 update / add / remove 入栈,作为正常历史记录体现整体替换。
|
|
159
|
+
this.pushRootDiffHistory(preValue as MApp, app, options.historySource);
|
|
160
|
+
} else {
|
|
161
|
+
// 首次设置 root:仅当该页面 / 页面片尚无基线标记时,才写入「未修改的初始状态」基线。
|
|
162
|
+
// 配合「先恢复历史再 set root」:若基线已随历史恢复建立(恢复后已有基线),则此处不再
|
|
163
|
+
// 重复创建,set root 不额外产生记录,由恢复出的历史栈作为当前状态来源。
|
|
164
|
+
// 标记不进入撤销/重做栈,仅作为该页历史列表底部的初始基线展示。
|
|
165
|
+
app.items?.forEach((pageNode) => {
|
|
166
|
+
if (pageNode?.id !== undefined && !historyService.getPageMarker(pageNode.id)) {
|
|
167
|
+
historyService.setPageMarker(pageNode.id, {
|
|
168
|
+
name: pageNode.name,
|
|
169
|
+
source: options.historySource,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
151
174
|
} else {
|
|
152
175
|
this.state.pageLength = 0;
|
|
153
176
|
this.state.pageFragmentLength = 0;
|
|
@@ -179,7 +202,30 @@ class Editor extends BaseService {
|
|
|
179
202
|
root = toRaw(root);
|
|
180
203
|
}
|
|
181
204
|
|
|
182
|
-
|
|
205
|
+
if (!root) {
|
|
206
|
+
return { node: null, parent: null, page: null };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (id === root.id) {
|
|
210
|
+
return { node: root, parent: null, page: null };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 大多数查找的目标都在当前页面内,优先在当前页面子树中查找以避免对整棵树做全量遍历。
|
|
214
|
+
// 注意:不能直接使用 state.page,它可能与当前 root 不同步(指向已脱离的旧页面对象),
|
|
215
|
+
// 因此仅借用其 id,再从当前 root 中取回真正的页面对象(页面均为 root 的直接子节点,数量很少)。
|
|
216
|
+
const pageIdStr = `${this.get('page')?.id || ''}`;
|
|
217
|
+
const currentPageNode = root.items?.find((item) => `${item.id}` === pageIdStr);
|
|
218
|
+
if (currentPageNode && `${id}` !== pageIdStr) {
|
|
219
|
+
// util 仅读取 root.id 与 root.items,按容器结构传入当前页面是安全的
|
|
220
|
+
const info = getNodeInfo(id, currentPageNode);
|
|
221
|
+
if (info.node) {
|
|
222
|
+
return info;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 回退:在完整 root 上查找;当前页面已搜索过,用 skip 跳过其子树避免重复遍历,
|
|
227
|
+
// 同时保留真实的 parent / page 引用(id 命中当前页面节点本身时会在跳过子树前先匹配到)
|
|
228
|
+
return getNodeInfo(id, root, currentPageNode);
|
|
183
229
|
}
|
|
184
230
|
|
|
185
231
|
/**
|
|
@@ -406,7 +452,13 @@ class Editor extends BaseService {
|
|
|
406
452
|
public async add(
|
|
407
453
|
addNode: AddMNode | MNode[],
|
|
408
454
|
parent?: MContainer | null,
|
|
409
|
-
{
|
|
455
|
+
{
|
|
456
|
+
doNotSelect = false,
|
|
457
|
+
doNotSwitchPage = false,
|
|
458
|
+
doNotPushHistory = false,
|
|
459
|
+
historyDescription,
|
|
460
|
+
historySource,
|
|
461
|
+
}: DslOpOptions = {},
|
|
410
462
|
): Promise<MNode | MNode[]> {
|
|
411
463
|
this.captureSelectionBeforeOp();
|
|
412
464
|
|
|
@@ -466,21 +518,21 @@ class Editor extends BaseService {
|
|
|
466
518
|
if (!(isPage(newNodes[0]) || isPageFragment(newNodes[0]))) {
|
|
467
519
|
const pageForOp = this.getNodeInfo(newNodes[0].id, false).page;
|
|
468
520
|
if (!doNotPushHistory) {
|
|
469
|
-
this.
|
|
470
|
-
|
|
471
|
-
{
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
},
|
|
481
|
-
{ name: pageForOp?.name || '', id: pageForOp!.id },
|
|
521
|
+
const parentId = (this.getParentById(newNodes[0].id, false) ?? this.get('root'))!.id;
|
|
522
|
+
this.pushOpHistory('add', {
|
|
523
|
+
diff: newNodes.map((n) => {
|
|
524
|
+
const p = this.getParentById(n.id, false) as MContainer;
|
|
525
|
+
const idx = p ? getNodeIndex(n.id, p) : -1;
|
|
526
|
+
return {
|
|
527
|
+
newSchema: cloneDeep(toRaw(n)),
|
|
528
|
+
parentId,
|
|
529
|
+
index: typeof idx === 'number' ? idx : -1,
|
|
530
|
+
};
|
|
531
|
+
}),
|
|
532
|
+
pageData: { name: pageForOp?.name || '', id: pageForOp!.id },
|
|
482
533
|
historyDescription,
|
|
483
|
-
|
|
534
|
+
source: historySource,
|
|
535
|
+
});
|
|
484
536
|
} else {
|
|
485
537
|
this.selectionBeforeOp = null;
|
|
486
538
|
}
|
|
@@ -577,13 +629,19 @@ class Editor extends BaseService {
|
|
|
577
629
|
*/
|
|
578
630
|
public async remove(
|
|
579
631
|
nodeOrNodeList: MNode | MNode[],
|
|
580
|
-
{
|
|
632
|
+
{
|
|
633
|
+
doNotSelect = false,
|
|
634
|
+
doNotSwitchPage = false,
|
|
635
|
+
doNotPushHistory = false,
|
|
636
|
+
historyDescription,
|
|
637
|
+
historySource,
|
|
638
|
+
}: DslOpOptions = {},
|
|
581
639
|
): Promise<void> {
|
|
582
640
|
this.captureSelectionBeforeOp();
|
|
583
641
|
|
|
584
642
|
const nodes = Array.isArray(nodeOrNodeList) ? nodeOrNodeList : [nodeOrNodeList];
|
|
585
643
|
|
|
586
|
-
const removedItems:
|
|
644
|
+
const removedItems: StepDiffItem<MNode>[] = [];
|
|
587
645
|
let pageForOp: { name: string; id: Id } | null = null;
|
|
588
646
|
if (!(isPage(nodes[0]) || isPageFragment(nodes[0]))) {
|
|
589
647
|
for (const n of nodes) {
|
|
@@ -594,7 +652,7 @@ class Editor extends BaseService {
|
|
|
594
652
|
}
|
|
595
653
|
const idx = getNodeIndex(curNode.id, parent);
|
|
596
654
|
removedItems.push({
|
|
597
|
-
|
|
655
|
+
oldSchema: cloneDeep(toRaw(curNode)),
|
|
598
656
|
parentId: parent.id,
|
|
599
657
|
index: typeof idx === 'number' ? idx : -1,
|
|
600
658
|
});
|
|
@@ -606,7 +664,12 @@ class Editor extends BaseService {
|
|
|
606
664
|
|
|
607
665
|
if (removedItems.length > 0 && pageForOp) {
|
|
608
666
|
if (!doNotPushHistory) {
|
|
609
|
-
this.pushOpHistory('remove', {
|
|
667
|
+
this.pushOpHistory('remove', {
|
|
668
|
+
diff: removedItems,
|
|
669
|
+
pageData: pageForOp,
|
|
670
|
+
historyDescription,
|
|
671
|
+
source: historySource,
|
|
672
|
+
});
|
|
610
673
|
} else {
|
|
611
674
|
this.selectionBeforeOp = null;
|
|
612
675
|
}
|
|
@@ -617,7 +680,7 @@ class Editor extends BaseService {
|
|
|
617
680
|
|
|
618
681
|
public async doUpdate(
|
|
619
682
|
config: MNode,
|
|
620
|
-
{ changeRecords = [] }: { changeRecords?: ChangeRecord[] } = {},
|
|
683
|
+
{ changeRecords = [], historySource }: { changeRecords?: ChangeRecord[]; historySource?: HistoryOpSource } = {},
|
|
621
684
|
): Promise<{ newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] }> {
|
|
622
685
|
const root = this.get('root');
|
|
623
686
|
if (!root) throw new Error('root为空');
|
|
@@ -637,7 +700,7 @@ class Editor extends BaseService {
|
|
|
637
700
|
if (!newConfig.type) throw new Error('配置缺少type值');
|
|
638
701
|
|
|
639
702
|
if (newConfig.type === NodeType.ROOT) {
|
|
640
|
-
this.set('root', newConfig as MApp);
|
|
703
|
+
this.set('root', newConfig as MApp, { historySource });
|
|
641
704
|
return {
|
|
642
705
|
oldNode: node,
|
|
643
706
|
newNode: newConfig,
|
|
@@ -704,11 +767,12 @@ class Editor extends BaseService {
|
|
|
704
767
|
changeRecordList?: ChangeRecord[][];
|
|
705
768
|
doNotPushHistory?: boolean;
|
|
706
769
|
historyDescription?: string;
|
|
770
|
+
historySource?: HistoryOpSource;
|
|
707
771
|
} = {},
|
|
708
772
|
): Promise<MNode | MNode[]> {
|
|
709
773
|
this.captureSelectionBeforeOp();
|
|
710
774
|
|
|
711
|
-
const { doNotPushHistory = false, changeRecordList, changeRecords, historyDescription } = data;
|
|
775
|
+
const { doNotPushHistory = false, changeRecordList, changeRecords, historyDescription, historySource } = data;
|
|
712
776
|
|
|
713
777
|
const nodes = Array.isArray(config) ? config : [config];
|
|
714
778
|
|
|
@@ -717,7 +781,7 @@ class Editor extends BaseService {
|
|
|
717
781
|
const updateData = await Promise.all(
|
|
718
782
|
nodes.map((node, index) => {
|
|
719
783
|
const recordsForNode = changeRecordList ? (changeRecordList[index] ?? []) : (changeRecords ?? []);
|
|
720
|
-
return this.doUpdate(node, { changeRecords: recordsForNode });
|
|
784
|
+
return this.doUpdate(node, { changeRecords: recordsForNode, historySource });
|
|
721
785
|
}),
|
|
722
786
|
);
|
|
723
787
|
|
|
@@ -726,20 +790,20 @@ class Editor extends BaseService {
|
|
|
726
790
|
if (curNodes.length) {
|
|
727
791
|
if (!doNotPushHistory) {
|
|
728
792
|
const pageForOp = this.getNodeInfo(nodes[0].id, false).page;
|
|
729
|
-
this.pushOpHistory(
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
793
|
+
this.pushOpHistory('update', {
|
|
794
|
+
// 每个节点单独保留自己的 changeRecords,便于撤销/重做时按 propPath 精细化更新;
|
|
795
|
+
// 没有 changeRecords 的(如内部 sort/moveLayer 等整节点替换操作)会退化为全节点替换。
|
|
796
|
+
diff: buildUpdateDiff(
|
|
797
|
+
updateData.map((d) => ({
|
|
733
798
|
oldNode: cloneDeep(d.oldNode),
|
|
734
|
-
newNode: cloneDeep(
|
|
735
|
-
// 每个节点单独保留自己的 changeRecords,便于撤销/重做时按 propPath 精细化更新;
|
|
736
|
-
// 没有 changeRecords 的(如内部 sort/moveLayer 等整节点替换操作)会退化为全节点替换。
|
|
799
|
+
newNode: cloneDeep(d.newNode),
|
|
737
800
|
changeRecords: d.changeRecords?.length ? cloneDeep(d.changeRecords) : undefined,
|
|
738
801
|
})),
|
|
739
|
-
|
|
740
|
-
{ name: pageForOp?.name || '', id: pageForOp!.id },
|
|
802
|
+
),
|
|
803
|
+
pageData: { name: pageForOp?.name || '', id: pageForOp!.id },
|
|
741
804
|
historyDescription,
|
|
742
|
-
|
|
805
|
+
source: historySource,
|
|
806
|
+
});
|
|
743
807
|
} else {
|
|
744
808
|
this.selectionBeforeOp = null;
|
|
745
809
|
}
|
|
@@ -763,7 +827,7 @@ class Editor extends BaseService {
|
|
|
763
827
|
public async sort(
|
|
764
828
|
id1: Id,
|
|
765
829
|
id2: Id,
|
|
766
|
-
{ doNotSelect = false, doNotPushHistory = false }: DslOpOptions = {},
|
|
830
|
+
{ doNotSelect = false, doNotPushHistory = false, historySource }: DslOpOptions = {},
|
|
767
831
|
): Promise<void> {
|
|
768
832
|
this.captureSelectionBeforeOp();
|
|
769
833
|
|
|
@@ -783,7 +847,7 @@ class Editor extends BaseService {
|
|
|
783
847
|
|
|
784
848
|
parent.items.splice(index2, 0, ...parent.items.splice(index1, 1));
|
|
785
849
|
|
|
786
|
-
await this.update(parent, { doNotPushHistory });
|
|
850
|
+
await this.update(parent, { doNotPushHistory, historySource });
|
|
787
851
|
if (!doNotSelect) {
|
|
788
852
|
await this.select(node);
|
|
789
853
|
}
|
|
@@ -836,7 +900,13 @@ class Editor extends BaseService {
|
|
|
836
900
|
public async paste(
|
|
837
901
|
position: PastePosition = {},
|
|
838
902
|
collectorOptions?: TargetOptions,
|
|
839
|
-
{
|
|
903
|
+
{
|
|
904
|
+
doNotSelect = false,
|
|
905
|
+
doNotSwitchPage = false,
|
|
906
|
+
doNotPushHistory = false,
|
|
907
|
+
historyDescription,
|
|
908
|
+
historySource,
|
|
909
|
+
}: DslOpOptions = {},
|
|
840
910
|
): Promise<MNode | MNode[] | void> {
|
|
841
911
|
const config: MNode[] = storageService.getItem(COPY_STORAGE_KEY);
|
|
842
912
|
if (!Array.isArray(config)) return;
|
|
@@ -857,7 +927,13 @@ class Editor extends BaseService {
|
|
|
857
927
|
propsService.replaceRelateId(config, pasteConfigs, collectorOptions);
|
|
858
928
|
}
|
|
859
929
|
|
|
860
|
-
return this.add(pasteConfigs, parent, {
|
|
930
|
+
return this.add(pasteConfigs, parent, {
|
|
931
|
+
doNotSelect,
|
|
932
|
+
doNotSwitchPage,
|
|
933
|
+
doNotPushHistory,
|
|
934
|
+
historyDescription,
|
|
935
|
+
historySource,
|
|
936
|
+
});
|
|
861
937
|
}
|
|
862
938
|
|
|
863
939
|
public async doPaste(config: MNode[], position: PastePosition = {}): Promise<MNode[]> {
|
|
@@ -893,14 +969,14 @@ class Editor extends BaseService {
|
|
|
893
969
|
*/
|
|
894
970
|
public async alignCenter(
|
|
895
971
|
config: MNode | MNode[],
|
|
896
|
-
{ doNotSelect = false, doNotPushHistory = false }: DslOpOptions = {},
|
|
972
|
+
{ doNotSelect = false, doNotPushHistory = false, historyDescription, historySource }: DslOpOptions = {},
|
|
897
973
|
): Promise<MNode | MNode[]> {
|
|
898
974
|
const nodes = Array.isArray(config) ? config : [config];
|
|
899
975
|
const stage = this.get('stage');
|
|
900
976
|
|
|
901
977
|
const newNodes = await Promise.all(nodes.map((node) => this.doAlignCenter(node)));
|
|
902
978
|
|
|
903
|
-
const newNode = await this.update(newNodes, { doNotPushHistory });
|
|
979
|
+
const newNode = await this.update(newNodes, { doNotPushHistory, historyDescription, historySource });
|
|
904
980
|
|
|
905
981
|
if (!doNotSelect) {
|
|
906
982
|
if (newNodes.length > 1) {
|
|
@@ -919,7 +995,10 @@ class Editor extends BaseService {
|
|
|
919
995
|
* @param options 可选配置
|
|
920
996
|
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
|
921
997
|
*/
|
|
922
|
-
public async moveLayer(
|
|
998
|
+
public async moveLayer(
|
|
999
|
+
offset: number | LayerOffset,
|
|
1000
|
+
{ doNotPushHistory = false, historyDescription, historySource }: DslOpOptions = {},
|
|
1001
|
+
): Promise<void> {
|
|
923
1002
|
this.captureSelectionBeforeOp();
|
|
924
1003
|
|
|
925
1004
|
const root = this.get('root');
|
|
@@ -960,10 +1039,13 @@ class Editor extends BaseService {
|
|
|
960
1039
|
const pageForOp = this.getNodeInfo(node.id, false).page;
|
|
961
1040
|
this.pushOpHistory(
|
|
962
1041
|
'update',
|
|
1042
|
+
|
|
963
1043
|
{
|
|
964
|
-
|
|
1044
|
+
diff: buildUpdateDiff([{ oldNode: oldParent, newNode: cloneDeep(toRaw(parent)) }]),
|
|
1045
|
+
pageData: { name: pageForOp?.name || '', id: pageForOp!.id },
|
|
1046
|
+
historyDescription,
|
|
1047
|
+
source: historySource,
|
|
965
1048
|
},
|
|
966
|
-
{ name: pageForOp?.name || '', id: pageForOp!.id },
|
|
967
1049
|
);
|
|
968
1050
|
} else {
|
|
969
1051
|
this.selectionBeforeOp = null;
|
|
@@ -989,7 +1071,13 @@ class Editor extends BaseService {
|
|
|
989
1071
|
public async moveToContainer(
|
|
990
1072
|
config: MNode | MNode[],
|
|
991
1073
|
targetId: Id,
|
|
992
|
-
{
|
|
1074
|
+
{
|
|
1075
|
+
doNotSelect = false,
|
|
1076
|
+
doNotSwitchPage = false,
|
|
1077
|
+
doNotPushHistory = false,
|
|
1078
|
+
historyDescription,
|
|
1079
|
+
historySource,
|
|
1080
|
+
}: DslOpOptions = {},
|
|
993
1081
|
): Promise<MNode | MNode[]> {
|
|
994
1082
|
const isBatch = Array.isArray(config);
|
|
995
1083
|
const configs = (isBatch ? config : [config]).filter((item) => !(isPage(item) || isPageFragment(item)));
|
|
@@ -1052,7 +1140,12 @@ class Editor extends BaseService {
|
|
|
1052
1140
|
newNode: cloneDeep(toRaw(this.getNodeById(id, false))) as MNode,
|
|
1053
1141
|
}));
|
|
1054
1142
|
const historyPage = moves[0].pageForOp ?? { name: '', id: target.id };
|
|
1055
|
-
this.pushOpHistory('update', {
|
|
1143
|
+
this.pushOpHistory('update', {
|
|
1144
|
+
diff: buildUpdateDiff(updatedItems),
|
|
1145
|
+
pageData: historyPage,
|
|
1146
|
+
historyDescription,
|
|
1147
|
+
source: historySource,
|
|
1148
|
+
});
|
|
1056
1149
|
} else {
|
|
1057
1150
|
this.selectionBeforeOp = null;
|
|
1058
1151
|
}
|
|
@@ -1064,7 +1157,7 @@ class Editor extends BaseService {
|
|
|
1064
1157
|
config: MNode | MNode[],
|
|
1065
1158
|
targetParent: MContainer,
|
|
1066
1159
|
targetIndex: number,
|
|
1067
|
-
{ doNotPushHistory = false }: DslOpOptions = {},
|
|
1160
|
+
{ doNotPushHistory = false, historyDescription, historySource }: DslOpOptions = {},
|
|
1068
1161
|
) {
|
|
1069
1162
|
this.captureSelectionBeforeOp();
|
|
1070
1163
|
|
|
@@ -1127,7 +1220,12 @@ class Editor extends BaseService {
|
|
|
1127
1220
|
}
|
|
1128
1221
|
if (!doNotPushHistory) {
|
|
1129
1222
|
const pageForOp = this.getNodeInfo(configs[0].id, false).page;
|
|
1130
|
-
this.pushOpHistory('update', {
|
|
1223
|
+
this.pushOpHistory('update', {
|
|
1224
|
+
diff: buildUpdateDiff(updatedItems),
|
|
1225
|
+
pageData: { name: pageForOp?.name || '', id: pageForOp!.id },
|
|
1226
|
+
historyDescription,
|
|
1227
|
+
source: historySource,
|
|
1228
|
+
});
|
|
1131
1229
|
} else {
|
|
1132
1230
|
this.selectionBeforeOp = null;
|
|
1133
1231
|
}
|
|
@@ -1135,6 +1233,86 @@ class Editor extends BaseService {
|
|
|
1135
1233
|
this.emit('drag-to', { targetIndex, configs, targetParent });
|
|
1136
1234
|
}
|
|
1137
1235
|
|
|
1236
|
+
// #region AndGetHistoryId
|
|
1237
|
+
/**
|
|
1238
|
+
* 下列 *AndGetHistoryId 方法与对应的普通操作(add / remove / update ...)行为完全一致,
|
|
1239
|
+
* 唯一区别是返回值为本次写入历史栈的历史记录 uuid({@link StepValue.uuid}),
|
|
1240
|
+
* 而非节点 / 节点数组。可用于精确引用 / 定位该条历史记录(埋点、revert、跨端同步等)。
|
|
1241
|
+
*
|
|
1242
|
+
* 当本次操作未写入历史(doNotPushHistory 为 true、或操作无实际变更 / 提前返回)时返回 null。
|
|
1243
|
+
*/
|
|
1244
|
+
|
|
1245
|
+
/** 等价于 {@link add},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
|
|
1246
|
+
public async addAndGetHistoryId(
|
|
1247
|
+
addNode: AddMNode | MNode[],
|
|
1248
|
+
parent?: MContainer | null,
|
|
1249
|
+
options: DslOpOptions = {},
|
|
1250
|
+
): Promise<string | null> {
|
|
1251
|
+
this.lastPushedHistoryId = null;
|
|
1252
|
+
await this.add(addNode, parent, options);
|
|
1253
|
+
return this.lastPushedHistoryId;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
/** 等价于 {@link remove},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
|
|
1257
|
+
public async removeAndGetHistoryId(
|
|
1258
|
+
nodeOrNodeList: MNode | MNode[],
|
|
1259
|
+
options: DslOpOptions = {},
|
|
1260
|
+
): Promise<string | null> {
|
|
1261
|
+
this.lastPushedHistoryId = null;
|
|
1262
|
+
await this.remove(nodeOrNodeList, options);
|
|
1263
|
+
return this.lastPushedHistoryId;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/** 等价于 {@link update},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
|
|
1267
|
+
public async updateAndGetHistoryId(
|
|
1268
|
+
config: MNode | MNode[],
|
|
1269
|
+
data: {
|
|
1270
|
+
changeRecords?: ChangeRecord[];
|
|
1271
|
+
changeRecordList?: ChangeRecord[][];
|
|
1272
|
+
doNotPushHistory?: boolean;
|
|
1273
|
+
historyDescription?: string;
|
|
1274
|
+
historySource?: HistoryOpSource;
|
|
1275
|
+
} = {},
|
|
1276
|
+
): Promise<string | null> {
|
|
1277
|
+
this.lastPushedHistoryId = null;
|
|
1278
|
+
await this.update(config, data);
|
|
1279
|
+
return this.lastPushedHistoryId;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/** 等价于 {@link moveLayer},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
|
|
1283
|
+
public async moveLayerAndGetHistoryId(
|
|
1284
|
+
offset: number | LayerOffset,
|
|
1285
|
+
options: DslOpOptions = {},
|
|
1286
|
+
): Promise<string | null> {
|
|
1287
|
+
this.lastPushedHistoryId = null;
|
|
1288
|
+
await this.moveLayer(offset, options);
|
|
1289
|
+
return this.lastPushedHistoryId;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
/** 等价于 {@link moveToContainer},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
|
|
1293
|
+
public async moveToContainerAndGetHistoryId(
|
|
1294
|
+
config: MNode | MNode[],
|
|
1295
|
+
targetId: Id,
|
|
1296
|
+
options: DslOpOptions = {},
|
|
1297
|
+
): Promise<string | null> {
|
|
1298
|
+
this.lastPushedHistoryId = null;
|
|
1299
|
+
await this.moveToContainer(config, targetId, options);
|
|
1300
|
+
return this.lastPushedHistoryId;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/** 等价于 {@link dragTo},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
|
|
1304
|
+
public async dragToAndGetHistoryId(
|
|
1305
|
+
config: MNode | MNode[],
|
|
1306
|
+
targetParent: MContainer,
|
|
1307
|
+
targetIndex: number,
|
|
1308
|
+
options: DslOpOptions = {},
|
|
1309
|
+
): Promise<string | null> {
|
|
1310
|
+
this.lastPushedHistoryId = null;
|
|
1311
|
+
await this.dragTo(config, targetParent, targetIndex, options);
|
|
1312
|
+
return this.lastPushedHistoryId;
|
|
1313
|
+
}
|
|
1314
|
+
// #endregion AndGetHistoryId
|
|
1315
|
+
|
|
1138
1316
|
/**
|
|
1139
1317
|
* 撤销当前操作
|
|
1140
1318
|
* @returns 被撤销的操作
|
|
@@ -1181,9 +1359,17 @@ class Editor extends BaseService {
|
|
|
1181
1359
|
if (!entry?.applied) return null;
|
|
1182
1360
|
|
|
1183
1361
|
const { step } = entry;
|
|
1362
|
+
// 初始基线(index 0 的 initial step)是栈底线,不可回滚。
|
|
1363
|
+
if (step.opType === 'initial') return null;
|
|
1184
1364
|
const root = this.get('root');
|
|
1185
1365
|
if (!root) return null;
|
|
1186
1366
|
|
|
1367
|
+
// 更新类步骤必须带 changeRecords 才支持回滚:缺失时只能整节点替换,会冲掉后续无关变更,故不支持。
|
|
1368
|
+
if (step.opType === 'update') {
|
|
1369
|
+
const items = step.diff ?? [];
|
|
1370
|
+
if (!items.length || !items.every((item) => item.changeRecords?.length)) return null;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1187
1373
|
// 反向应用产生的新 step 由内部 pushOpHistory 触发 history `change` 事件,监听一次以拿到引用。
|
|
1188
1374
|
let revertedStep: StepValue | null = null;
|
|
1189
1375
|
const captureRevert = (s: StepValue) => {
|
|
@@ -1193,15 +1379,15 @@ class Editor extends BaseService {
|
|
|
1193
1379
|
|
|
1194
1380
|
const historyDescription = `回滚 #${index + 1}: ${describeStepForRevert(step)}`;
|
|
1195
1381
|
// revert 走 public add/remove/update,让操作以一条普通新 step 入栈;不要切换选区与页面,避免打断用户。
|
|
1196
|
-
const opts = { doNotSelect: true, doNotSwitchPage: true, historyDescription } as const;
|
|
1382
|
+
const opts = { doNotSelect: true, doNotSwitchPage: true, historyDescription, historySource: 'rollback' } as const;
|
|
1197
1383
|
|
|
1198
1384
|
try {
|
|
1199
1385
|
switch (step.opType) {
|
|
1200
1386
|
case 'add': {
|
|
1201
1387
|
// 原本是新增 → revert 即删除当时被加入的节点
|
|
1202
|
-
const
|
|
1203
|
-
|
|
1204
|
-
const existing = this.getNodeById(
|
|
1388
|
+
for (const { newSchema } of step.diff ?? []) {
|
|
1389
|
+
if (!newSchema) continue;
|
|
1390
|
+
const existing = this.getNodeById(newSchema.id, false);
|
|
1205
1391
|
if (existing) {
|
|
1206
1392
|
await this.remove(existing, opts);
|
|
1207
1393
|
}
|
|
@@ -1211,37 +1397,42 @@ class Editor extends BaseService {
|
|
|
1211
1397
|
case 'remove': {
|
|
1212
1398
|
// 原本是删除 → revert 即把节点按原父容器加回来。
|
|
1213
1399
|
// 按原 index 升序逐个插回,先小后大避免索引漂移。
|
|
1214
|
-
const items = step.
|
|
1215
|
-
const sorted = [...items].sort((a, b) => a.index - b.index);
|
|
1216
|
-
for (const {
|
|
1400
|
+
const items = step.diff ?? [];
|
|
1401
|
+
const sorted = [...items].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
1402
|
+
for (const { oldSchema, parentId } of sorted) {
|
|
1403
|
+
if (!oldSchema || parentId === undefined) continue;
|
|
1217
1404
|
const parent = this.getNodeById(parentId, false) as MContainer | null;
|
|
1218
1405
|
if (parent) {
|
|
1219
|
-
await this.add([cloneDeep(
|
|
1406
|
+
await this.add([cloneDeep(oldSchema)] as MNode[], parent, opts);
|
|
1220
1407
|
}
|
|
1221
1408
|
}
|
|
1222
1409
|
break;
|
|
1223
1410
|
}
|
|
1224
1411
|
case 'update': {
|
|
1225
|
-
// 原本是更新 → revert 即把
|
|
1412
|
+
// 原本是更新 → revert 即把 oldSchema 的值写回;
|
|
1226
1413
|
// 优先按 changeRecords 局部 patch(仅触达 propPath 下的字段,避免冲掉同节点上其它无关变更)。
|
|
1227
|
-
const items = step.
|
|
1228
|
-
const configs = items
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1414
|
+
const items = step.diff ?? [];
|
|
1415
|
+
const configs = items
|
|
1416
|
+
.filter((item) => item.oldSchema && item.newSchema)
|
|
1417
|
+
.map(({ oldSchema, newSchema, changeRecords }) => {
|
|
1418
|
+
const oldNode = oldSchema!;
|
|
1419
|
+
const newNode = newSchema!;
|
|
1420
|
+
if (changeRecords?.length) {
|
|
1421
|
+
const patch: MNode = { id: newNode.id, type: newNode.type };
|
|
1422
|
+
for (const record of changeRecords) {
|
|
1423
|
+
if (!record.propPath) {
|
|
1424
|
+
// 没有 propPath 视为整节点替换
|
|
1425
|
+
return cloneDeep(oldNode);
|
|
1426
|
+
}
|
|
1427
|
+
const value = cloneDeep(getValueByKeyPath(record.propPath, oldNode));
|
|
1428
|
+
setValueByKeyPath(record.propPath, value, patch);
|
|
1235
1429
|
}
|
|
1236
|
-
|
|
1237
|
-
setValueByKeyPath(record.propPath, value, patch);
|
|
1430
|
+
return patch;
|
|
1238
1431
|
}
|
|
1239
|
-
return
|
|
1240
|
-
}
|
|
1241
|
-
return cloneDeep(oldNode);
|
|
1242
|
-
});
|
|
1432
|
+
return cloneDeep(oldNode);
|
|
1433
|
+
});
|
|
1243
1434
|
if (configs.length) {
|
|
1244
|
-
await this.update(configs, { historyDescription });
|
|
1435
|
+
await this.update(configs, { historyDescription, historySource: 'rollback' });
|
|
1245
1436
|
}
|
|
1246
1437
|
break;
|
|
1247
1438
|
}
|
|
@@ -1259,6 +1450,20 @@ class Editor extends BaseService {
|
|
|
1259
1450
|
return revertedStep;
|
|
1260
1451
|
}
|
|
1261
1452
|
|
|
1453
|
+
/**
|
|
1454
|
+
* 通过历史记录 uuid 回滚当前页面的某条历史步骤,语义与 {@link revertPageStep} 完全一致,
|
|
1455
|
+
* 仅入参从 index 改为 uuid({@link StepValue.uuid})。uuid 不随栈内步骤增删而变化,
|
|
1456
|
+
* 更适合业务侧持有引用后再回滚(埋点、跨端同步等场景)。
|
|
1457
|
+
*
|
|
1458
|
+
* @param uuid 目标历史记录的 uuid,通常由 *AndGetHistoryId 方法返回
|
|
1459
|
+
* @returns 反向后产生的新 step;找不到对应 uuid / 未应用 / 反向失败时返回 null
|
|
1460
|
+
*/
|
|
1461
|
+
public async revertPageStepById(uuid: string): Promise<StepValue | null> {
|
|
1462
|
+
const index = historyService.getPageStepIndexByUuid(uuid);
|
|
1463
|
+
if (index < 0) return null;
|
|
1464
|
+
return this.revertPageStep(index);
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1262
1467
|
/**
|
|
1263
1468
|
* 跳转当前页面历史栈到指定游标位置。
|
|
1264
1469
|
*
|
|
@@ -1285,14 +1490,21 @@ class Editor extends BaseService {
|
|
|
1285
1490
|
return cursor;
|
|
1286
1491
|
}
|
|
1287
1492
|
|
|
1288
|
-
public async move(
|
|
1493
|
+
public async move(
|
|
1494
|
+
left: number,
|
|
1495
|
+
top: number,
|
|
1496
|
+
{ doNotPushHistory = false, historyDescription, historySource }: DslOpOptions = {},
|
|
1497
|
+
) {
|
|
1289
1498
|
const node = toRaw(this.get('node'));
|
|
1290
1499
|
if (!node || isPage(node)) return;
|
|
1291
1500
|
|
|
1292
1501
|
const newStyle = calcMoveStyle(node.style || {}, left, top);
|
|
1293
1502
|
if (!newStyle) return;
|
|
1294
1503
|
|
|
1295
|
-
await this.update(
|
|
1504
|
+
await this.update(
|
|
1505
|
+
{ id: node.id, type: node.type, style: newStyle },
|
|
1506
|
+
{ doNotPushHistory, historyDescription, historySource },
|
|
1507
|
+
);
|
|
1296
1508
|
}
|
|
1297
1509
|
|
|
1298
1510
|
public resetState() {
|
|
@@ -1348,25 +1560,119 @@ class Editor extends BaseService {
|
|
|
1348
1560
|
this.selectionBeforeOp = this.get('nodes').map((n) => n.id);
|
|
1349
1561
|
}
|
|
1350
1562
|
|
|
1563
|
+
/**
|
|
1564
|
+
* 比较「上一次 root」与「新 root」的页面 / 页面片,按页面粒度把整体替换拆成历史记录:
|
|
1565
|
+
* - 新旧都存在且内容变化的页面 → 一条 `update`(整页快照替换,无 changeRecords);
|
|
1566
|
+
* - 仅新 root 存在的页面 → 一条 `add`;
|
|
1567
|
+
* - 仅旧 root 存在的页面 → 一条 `remove`。
|
|
1568
|
+
*
|
|
1569
|
+
* 每条记录落到对应页面自己的历史栈(与普通节点操作一致),并标记来源 `source`。
|
|
1570
|
+
* 内容未变化的页面不产生记录,避免重复设置相同 DSL 时产生噪声。
|
|
1571
|
+
*/
|
|
1572
|
+
private pushRootDiffHistory(preRoot: MApp, nextRoot: MApp, source?: HistoryOpSource): void {
|
|
1573
|
+
const prevPages = preRoot?.items || [];
|
|
1574
|
+
const nextPages = nextRoot?.items || [];
|
|
1575
|
+
const prevMap = new Map(prevPages.map((p) => [`${p.id}`, p]));
|
|
1576
|
+
const nextMap = new Map(nextPages.map((p) => [`${p.id}`, p]));
|
|
1577
|
+
const indexInItems = (root: MApp, id: Id) => (root.items ?? []).findIndex((item) => `${item.id}` === `${id}`);
|
|
1578
|
+
|
|
1579
|
+
nextPages.forEach((nextPage) => {
|
|
1580
|
+
const prevPage = prevMap.get(`${nextPage.id}`);
|
|
1581
|
+
if (!prevPage) {
|
|
1582
|
+
this.pushPageDiffStep(
|
|
1583
|
+
'add',
|
|
1584
|
+
nextPage,
|
|
1585
|
+
{ newSchema: cloneDeep(toRaw(nextPage)), parentId: nextRoot.id, index: indexInItems(nextRoot, nextPage.id) },
|
|
1586
|
+
source,
|
|
1587
|
+
);
|
|
1588
|
+
} else if (!isEqual(toRaw(prevPage), toRaw(nextPage))) {
|
|
1589
|
+
this.pushPageDiffStep(
|
|
1590
|
+
'update',
|
|
1591
|
+
nextPage,
|
|
1592
|
+
{ oldSchema: cloneDeep(toRaw(prevPage)), newSchema: cloneDeep(toRaw(nextPage)) },
|
|
1593
|
+
source,
|
|
1594
|
+
);
|
|
1595
|
+
}
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
prevPages.forEach((prevPage) => {
|
|
1599
|
+
if (!nextMap.has(`${prevPage.id}`)) {
|
|
1600
|
+
this.pushPageDiffStep(
|
|
1601
|
+
'remove',
|
|
1602
|
+
prevPage,
|
|
1603
|
+
{ oldSchema: cloneDeep(toRaw(prevPage)), parentId: preRoot.id, index: indexInItems(preRoot, prevPage.id) },
|
|
1604
|
+
source,
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
/**
|
|
1611
|
+
* 构造一条页面级「set root」历史记录(不携带选区 / modifiedNodeIds 上下文)并落到该页面自己的栈。
|
|
1612
|
+
*
|
|
1613
|
+
* 连续 set root 替换:若该页栈最新一条已是**同来源**的 set root 记录({@link StepValue.rootStep} 且 `source` 相同),
|
|
1614
|
+
* 则用本次记录**替换**它而非新增,避免源码反复保存 / 外部重设 DSL 时堆积多条 root 记录;
|
|
1615
|
+
* 来源不同则照常新增(initial 基线不是 rootStep,不在此列)。
|
|
1616
|
+
*/
|
|
1617
|
+
private pushPageDiffStep(
|
|
1618
|
+
opType: HistoryOpType,
|
|
1619
|
+
page: MPage | MPageFragment,
|
|
1620
|
+
diffItem: StepDiffItem<MNode>,
|
|
1621
|
+
source?: HistoryOpSource,
|
|
1622
|
+
): void {
|
|
1623
|
+
const step: StepValue = {
|
|
1624
|
+
uuid: guid(),
|
|
1625
|
+
data: { name: page.name || '', id: page.id },
|
|
1626
|
+
opType,
|
|
1627
|
+
selectedBefore: [],
|
|
1628
|
+
selectedAfter: [],
|
|
1629
|
+
modifiedNodeIds: new Map(),
|
|
1630
|
+
diff: [diffItem],
|
|
1631
|
+
rootStep: true,
|
|
1632
|
+
};
|
|
1633
|
+
if (source) step.source = source;
|
|
1634
|
+
|
|
1635
|
+
const top = historyService.getCurrentPageStep(page.id);
|
|
1636
|
+
if (top?.rootStep && top.source === source) {
|
|
1637
|
+
historyService.replaceCurrentPageStep(step, page.id);
|
|
1638
|
+
} else {
|
|
1639
|
+
historyService.push(step, page.id);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1351
1643
|
private pushOpHistory(
|
|
1352
1644
|
opType: HistoryOpType,
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1645
|
+
{
|
|
1646
|
+
diff,
|
|
1647
|
+
pageData,
|
|
1648
|
+
historyDescription,
|
|
1649
|
+
source,
|
|
1650
|
+
}: {
|
|
1651
|
+
diff: StepDiffItem<MNode>[];
|
|
1652
|
+
pageData: { name: string; id: Id };
|
|
1653
|
+
historyDescription?: string;
|
|
1654
|
+
source?: HistoryOpSource;
|
|
1655
|
+
},
|
|
1656
|
+
): string | null {
|
|
1357
1657
|
const step: StepValue = {
|
|
1658
|
+
uuid: guid(),
|
|
1358
1659
|
data: pageData,
|
|
1359
1660
|
opType,
|
|
1360
1661
|
selectedBefore: this.selectionBeforeOp ?? [],
|
|
1361
1662
|
selectedAfter: this.get('nodes').map((n) => n.id),
|
|
1362
1663
|
modifiedNodeIds: new Map(this.get('modifiedNodeIds')),
|
|
1363
|
-
|
|
1664
|
+
diff,
|
|
1364
1665
|
};
|
|
1365
1666
|
if (historyDescription) step.historyDescription = historyDescription;
|
|
1667
|
+
if (source) step.source = source;
|
|
1366
1668
|
// 显式按 step.data.id 入栈:跨页操作(如 moveToContainer 从源页搬到目标页)
|
|
1367
1669
|
// 必须落到正确的页面栈,否则会把记录错误地推到当前活动页 / 操作发起页。
|
|
1368
|
-
historyService.push(step, pageData.id);
|
|
1670
|
+
const pushed = historyService.push(step, pageData.id);
|
|
1671
|
+
// push 返回 null 表示当前没有可写入的页面栈(未真正入栈),此时不应返回 uuid。
|
|
1672
|
+
const historyId = pushed ? step.uuid : null;
|
|
1673
|
+
this.lastPushedHistoryId = historyId;
|
|
1369
1674
|
this.selectionBeforeOp = null;
|
|
1675
|
+
return historyId;
|
|
1370
1676
|
}
|
|
1371
1677
|
|
|
1372
1678
|
/**
|
|
@@ -1382,6 +1688,8 @@ class Editor extends BaseService {
|
|
|
1382
1688
|
* @param reverse true = 撤销,false = 重做
|
|
1383
1689
|
*/
|
|
1384
1690
|
private async applyHistoryOp(step: StepValue, reverse: boolean) {
|
|
1691
|
+
// 初始基线 step 仅作展示,不承载任何变更,撤销/重做时无需应用(正常流程下也不会被触达)。
|
|
1692
|
+
if (step.opType === 'initial') return;
|
|
1385
1693
|
const root = this.get('root');
|
|
1386
1694
|
const stage = this.get('stage');
|
|
1387
1695
|
if (!root) return;
|
|
@@ -1390,52 +1698,52 @@ class Editor extends BaseService {
|
|
|
1390
1698
|
|
|
1391
1699
|
switch (step.opType) {
|
|
1392
1700
|
case 'add': {
|
|
1393
|
-
const
|
|
1701
|
+
const items = step.diff ?? [];
|
|
1394
1702
|
if (reverse) {
|
|
1395
1703
|
// 撤销 add:把当时加入的节点删除
|
|
1396
|
-
for (const
|
|
1397
|
-
|
|
1704
|
+
for (const { newSchema } of items) {
|
|
1705
|
+
if (!newSchema) continue;
|
|
1706
|
+
const existing = this.getNodeById(newSchema.id, false);
|
|
1398
1707
|
if (existing) {
|
|
1399
1708
|
await this.remove(existing, commonOpts);
|
|
1400
1709
|
}
|
|
1401
1710
|
}
|
|
1402
1711
|
} else {
|
|
1403
|
-
// 重做 add:按记录的
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
if (parent.items) {
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
parent.items.push(cloneDeep(n));
|
|
1415
|
-
}
|
|
1416
|
-
await stage?.add({
|
|
1417
|
-
config: cloneDeep(n),
|
|
1418
|
-
parent: cloneDeep(parent),
|
|
1419
|
-
parentId: parent.id,
|
|
1420
|
-
root: cloneDeep(root),
|
|
1421
|
-
});
|
|
1712
|
+
// 重做 add:按记录的 parentId / index 把节点重新插回父容器。
|
|
1713
|
+
// 按目标 index 升序逐个插入,先小后大避免索引漂移
|
|
1714
|
+
const sorted = [...items].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
1715
|
+
for (const { newSchema, parentId, index } of sorted) {
|
|
1716
|
+
if (!newSchema || parentId === undefined) continue;
|
|
1717
|
+
const parent = this.getNodeById(parentId, false) as MContainer | null;
|
|
1718
|
+
if (parent?.items) {
|
|
1719
|
+
if (typeof index === 'number' && index >= 0 && index < parent.items.length) {
|
|
1720
|
+
parent.items.splice(index, 0, cloneDeep(newSchema));
|
|
1721
|
+
} else {
|
|
1722
|
+
parent.items.push(cloneDeep(newSchema));
|
|
1422
1723
|
}
|
|
1724
|
+
await stage?.add({
|
|
1725
|
+
config: cloneDeep(newSchema),
|
|
1726
|
+
parent: cloneDeep(parent),
|
|
1727
|
+
parentId: parent.id,
|
|
1728
|
+
root: cloneDeep(root),
|
|
1729
|
+
});
|
|
1423
1730
|
}
|
|
1424
1731
|
}
|
|
1425
1732
|
}
|
|
1426
1733
|
break;
|
|
1427
1734
|
}
|
|
1428
1735
|
case 'remove': {
|
|
1429
|
-
const items = step.
|
|
1736
|
+
const items = step.diff ?? [];
|
|
1430
1737
|
if (reverse) {
|
|
1431
1738
|
// 撤销 remove:按原 index 升序逐个插回(先小后大避免索引漂移)
|
|
1432
|
-
const sorted = [...items].sort((a, b) => a.index - b.index);
|
|
1433
|
-
for (const {
|
|
1739
|
+
const sorted = [...items].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
1740
|
+
for (const { oldSchema, parentId, index } of sorted) {
|
|
1741
|
+
if (!oldSchema || parentId === undefined) continue;
|
|
1434
1742
|
const parent = this.getNodeById(parentId, false) as MContainer | null;
|
|
1435
1743
|
if (parent?.items) {
|
|
1436
|
-
parent.items.splice(index, 0, cloneDeep(
|
|
1744
|
+
parent.items.splice(index ?? parent.items.length, 0, cloneDeep(oldSchema));
|
|
1437
1745
|
await stage?.add({
|
|
1438
|
-
config: cloneDeep(
|
|
1746
|
+
config: cloneDeep(oldSchema),
|
|
1439
1747
|
parent: cloneDeep(parent),
|
|
1440
1748
|
parentId,
|
|
1441
1749
|
root: cloneDeep(root),
|
|
@@ -1444,8 +1752,9 @@ class Editor extends BaseService {
|
|
|
1444
1752
|
}
|
|
1445
1753
|
} else {
|
|
1446
1754
|
// 重做 remove:再删一次
|
|
1447
|
-
for (const {
|
|
1448
|
-
|
|
1755
|
+
for (const { oldSchema } of items) {
|
|
1756
|
+
if (!oldSchema) continue;
|
|
1757
|
+
const existing = this.getNodeById(oldSchema.id, false);
|
|
1449
1758
|
if (existing) {
|
|
1450
1759
|
await this.remove(existing, commonOpts);
|
|
1451
1760
|
}
|
|
@@ -1454,27 +1763,31 @@ class Editor extends BaseService {
|
|
|
1454
1763
|
break;
|
|
1455
1764
|
}
|
|
1456
1765
|
case 'update': {
|
|
1457
|
-
const items = step.
|
|
1766
|
+
const items = step.diff ?? [];
|
|
1458
1767
|
// 优先按 changeRecords 局部 patch:仅触达 propPath 下的字段,避免整节点替换冲掉同节点上其它无关变更。
|
|
1459
1768
|
// 没有 changeRecords 的(如内部 sort/moveLayer/拖动等整节点快照场景)才退化为整节点替换。
|
|
1460
|
-
const configs = items
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1769
|
+
const configs = items
|
|
1770
|
+
.filter((item) => item.oldSchema && item.newSchema)
|
|
1771
|
+
.map(({ oldSchema, newSchema, changeRecords }) => {
|
|
1772
|
+
const oldNode = oldSchema!;
|
|
1773
|
+
const newNode = newSchema!;
|
|
1774
|
+
if (changeRecords?.length) {
|
|
1775
|
+
const sourceForValues = reverse ? oldNode : newNode;
|
|
1776
|
+
// 仅保留 id / type 作为最小骨架,再按 propPath 写入需要回滚/重做的字段;
|
|
1777
|
+
// 后续 update -> mergeWith 会与现有节点深合并,patch 中未涉及的字段不会被改动。
|
|
1778
|
+
const patch: MNode = { id: newNode.id, type: newNode.type };
|
|
1779
|
+
for (const record of changeRecords) {
|
|
1780
|
+
if (!record.propPath) {
|
|
1781
|
+
// 没有 propPath 视为整节点替换
|
|
1782
|
+
return cloneDeep(sourceForValues);
|
|
1783
|
+
}
|
|
1784
|
+
const value = cloneDeep(getValueByKeyPath(record.propPath, sourceForValues));
|
|
1785
|
+
setValueByKeyPath(record.propPath, value, patch);
|
|
1470
1786
|
}
|
|
1471
|
-
|
|
1472
|
-
setValueByKeyPath(record.propPath, value, patch);
|
|
1787
|
+
return patch;
|
|
1473
1788
|
}
|
|
1474
|
-
return
|
|
1475
|
-
}
|
|
1476
|
-
return cloneDeep(reverse ? oldNode : newNode);
|
|
1477
|
-
});
|
|
1789
|
+
return cloneDeep(reverse ? oldNode : newNode);
|
|
1790
|
+
});
|
|
1478
1791
|
if (configs.length) {
|
|
1479
1792
|
await this.update(configs, { doNotPushHistory: true });
|
|
1480
1793
|
}
|