@tmagic/editor 1.8.0-beta.0 → 1.8.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/Editor.vue_vue_type_script_setup_true_lang.js +11 -2
- package/dist/es/components/CodeBlockEditor.vue_vue_type_script_setup_true_lang.js +18 -110
- package/dist/es/components/CodeParams.vue_vue_type_script_setup_true_lang.js +6 -0
- package/dist/es/components/CompareForm.js +5 -0
- package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +167 -0
- package/dist/es/fields/Code.vue_vue_type_script_setup_true_lang.js +19 -3
- package/dist/es/fields/CodeSelect.vue_vue_type_script_setup_true_lang.js +18 -2
- package/dist/es/fields/CodeSelectCol.vue_vue_type_script_setup_true_lang.js +37 -6
- package/dist/es/fields/DataSourceFieldSelect/FieldSelect.vue_vue_type_script_setup_true_lang.js +3 -1
- package/dist/es/fields/DataSourceFieldSelect/Index.vue_vue_type_script_setup_true_lang.js +12 -3
- package/dist/es/fields/DataSourceFields.vue_vue_type_script_setup_true_lang.js +14 -6
- package/dist/es/fields/DataSourceMethodSelect.vue_vue_type_script_setup_true_name_true_lang.js +3 -1
- package/dist/es/fields/DataSourceMethods.vue_vue_type_script_setup_true_lang.js +15 -7
- package/dist/es/fields/DataSourceMocks.vue_vue_type_script_setup_true_lang.js +14 -6
- package/dist/es/fields/DataSourceSelect.vue_vue_type_script_setup_true_lang.js +3 -1
- package/dist/es/fields/EventSelect.vue_vue_type_script_setup_true_lang.js +58 -11
- package/dist/es/fields/KeyValue.vue_vue_type_script_setup_true_lang.js +11 -6
- package/dist/es/fields/StyleSetter/Index.vue_vue_type_script_setup_true_lang.js +8 -2
- package/dist/es/fields/StyleSetter/components/Border.vue_vue_type_script_setup_true_lang.js +10 -2
- package/dist/es/fields/StyleSetter/components/Box.vue_vue_type_script_setup_true_lang.js +3 -1
- package/dist/es/fields/StyleSetter/pro/Background.vue_vue_type_script_setup_true_lang.js +10 -2
- package/dist/es/fields/StyleSetter/pro/Border.vue_vue_type_script_setup_true_lang.js +16 -3
- package/dist/es/fields/StyleSetter/pro/Font.vue_vue_type_script_setup_true_lang.js +10 -2
- package/dist/es/fields/StyleSetter/pro/Layout.vue_vue_type_script_setup_true_lang.js +14 -2
- package/dist/es/fields/StyleSetter/pro/Position.vue_vue_type_script_setup_true_lang.js +10 -2
- package/dist/es/fields/StyleSetter/pro/Transform.vue_vue_type_script_setup_true_lang.js +10 -2
- package/dist/es/fields/UISelect.vue_vue_type_script_setup_true_lang.js +8 -5
- package/dist/es/hooks/use-code-block-edit.js +2 -2
- package/dist/es/hooks/use-stage.js +15 -7
- package/dist/es/index.js +3 -1
- package/dist/es/initService.js +6 -6
- package/dist/es/layouts/CodeEditor.vue_vue_type_script_setup_true_lang.js +4 -0
- package/dist/es/layouts/Framework.vue_vue_type_script_setup_true_lang.js +12 -7
- package/dist/es/layouts/NavMenu.vue_vue_type_script_setup_true_lang.js +8 -0
- 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 +84 -0
- package/dist/es/layouts/history-list/CodeBlockTab.js +5 -0
- package/dist/es/layouts/history-list/CodeBlockTab.vue_vue_type_script_setup_true_lang.js +62 -0
- package/dist/es/layouts/history-list/DataSourceTab.js +5 -0
- package/dist/es/layouts/history-list/DataSourceTab.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 +180 -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 +195 -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 +270 -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 +48 -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 +99 -0
- package/dist/es/layouts/history-list/composables.js +154 -0
- package/dist/es/layouts/sidebar/layer/use-node-status.js +38 -4
- package/dist/es/services/BaseService.js +31 -68
- package/dist/es/services/codeBlock.js +205 -8
- package/dist/es/services/componentList.js +2 -2
- package/dist/es/services/dataSource.js +217 -8
- package/dist/es/services/dep.js +21 -9
- package/dist/es/services/editor.js +350 -160
- package/dist/es/services/events.js +2 -2
- package/dist/es/services/history.js +407 -12
- package/dist/es/services/keybinding.js +2 -2
- 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 +2 -2
- package/dist/es/style.css +347 -1
- package/dist/es/utils/code-block.js +122 -0
- package/dist/es/utils/content-menu.js +4 -5
- package/dist/es/utils/dep/worker.js +1 -1
- package/dist/es/utils/editor.js +3 -3
- package/dist/es/utils/undo-redo.js +20 -0
- package/dist/style.css +347 -1
- package/dist/tmagic-editor.umd.cjs +3273 -766
- package/package.json +7 -7
- package/src/Editor.vue +7 -0
- package/src/components/CodeBlockEditor.vue +20 -109
- package/src/components/CodeParams.vue +6 -0
- package/src/components/CompareForm.vue +225 -0
- package/src/editorProps.ts +2 -0
- package/src/fields/Code.vue +19 -3
- package/src/fields/CodeSelect.vue +17 -1
- package/src/fields/CodeSelectCol.vue +30 -2
- package/src/fields/DataSourceFieldSelect/FieldSelect.vue +7 -1
- package/src/fields/DataSourceFieldSelect/Index.vue +9 -2
- package/src/fields/DataSourceFields.vue +12 -3
- package/src/fields/DataSourceMethodSelect.vue +4 -1
- package/src/fields/DataSourceMethods.vue +14 -4
- 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/Index.vue +6 -0
- package/src/fields/StyleSetter/components/Border.vue +15 -1
- package/src/fields/StyleSetter/components/Box.vue +2 -0
- package/src/fields/StyleSetter/pro/Background.vue +15 -1
- package/src/fields/StyleSetter/pro/Border.vue +24 -2
- package/src/fields/StyleSetter/pro/Font.vue +15 -1
- package/src/fields/StyleSetter/pro/Layout.vue +17 -1
- package/src/fields/StyleSetter/pro/Position.vue +15 -1
- package/src/fields/StyleSetter/pro/Transform.vue +15 -1
- package/src/fields/UISelect.vue +5 -2
- package/src/hooks/use-code-block-edit.ts +5 -2
- package/src/hooks/use-stage.ts +23 -7
- package/src/index.ts +1 -0
- package/src/layouts/CodeEditor.vue +15 -0
- package/src/layouts/Framework.vue +10 -5
- package/src/layouts/NavMenu.vue +9 -0
- package/src/layouts/history-list/Bucket.vue +99 -0
- package/src/layouts/history-list/CodeBlockTab.vue +61 -0
- package/src/layouts/history-list/DataSourceTab.vue +61 -0
- package/src/layouts/history-list/GroupRow.vue +207 -0
- package/src/layouts/history-list/HistoryDiffDialog.vue +212 -0
- package/src/layouts/history-list/HistoryListPanel.vue +271 -0
- package/src/layouts/history-list/InitialRow.vue +41 -0
- package/src/layouts/history-list/PageTab.vue +94 -0
- package/src/layouts/history-list/composables.ts +195 -0
- package/src/layouts/sidebar/layer/use-node-status.ts +64 -12
- package/src/services/BaseService.ts +35 -86
- package/src/services/codeBlock.ts +256 -5
- package/src/services/dataSource.ts +257 -6
- package/src/services/dep.ts +32 -9
- package/src/services/editor.ts +454 -171
- package/src/services/history.ts +452 -8
- package/src/services/props.ts +2 -2
- package/src/theme/history-list-panel.scss +423 -0
- package/src/theme/theme.scss +1 -0
- package/src/type.ts +179 -6
- package/src/utils/code-block.ts +150 -0
- package/src/utils/content-menu.ts +8 -5
- package/src/utils/dep/worker.ts +5 -3
- package/src/utils/editor.ts +4 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/undo-redo.ts +23 -0
- package/types/index.d.ts +2107 -810
- 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
|
@@ -23,7 +23,7 @@ import type { Id, MApp, MContainer, MNode, MPage, MPageFragment, TargetOptions }
|
|
|
23
23
|
import { NodeType } from '@tmagic/core';
|
|
24
24
|
import type { ChangeRecord } from '@tmagic/form';
|
|
25
25
|
import { isFixed } from '@tmagic/stage';
|
|
26
|
-
import { getNodeInfo, getNodePath, isPage, isPageFragment } from '@tmagic/utils';
|
|
26
|
+
import { getNodeInfo, getNodePath, getValueByKeyPath, isPage, isPageFragment, setValueByKeyPath } from '@tmagic/utils';
|
|
27
27
|
|
|
28
28
|
import BaseService from '@editor/services//BaseService';
|
|
29
29
|
import propsService from '@editor/services//props';
|
|
@@ -62,27 +62,40 @@ import {
|
|
|
62
62
|
setLayout,
|
|
63
63
|
toggleFixedPosition,
|
|
64
64
|
} from '@editor/utils/editor';
|
|
65
|
-
import type { HistoryOpContext } from '@editor/utils/editor-history';
|
|
66
|
-
import { applyHistoryAddOp, applyHistoryRemoveOp, applyHistoryUpdateOp } from '@editor/utils/editor-history';
|
|
67
65
|
import { beforePaste, getAddParent } from '@editor/utils/operator';
|
|
68
66
|
|
|
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
|
-
};
|
|
67
|
+
type MoveItem = { node: MNode; parent: MContainer; pageForOp: { name: string; id: Id } | null };
|
|
78
68
|
|
|
79
69
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
70
|
+
* 给「回滚」生成的新 step 用的简短描述生成器。
|
|
71
|
+
* 与 UI 层 `describePageStep` 同义,但避免 service 反向依赖 layouts/,故在此本地实现。
|
|
82
72
|
*/
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
73
|
+
const describeStepForRevert = (step: StepValue): string => {
|
|
74
|
+
switch (step.opType) {
|
|
75
|
+
case 'add': {
|
|
76
|
+
const count = step.nodes?.length ?? 0;
|
|
77
|
+
const node = step.nodes?.[0];
|
|
78
|
+
const label = node?.name || node?.type || (node?.id !== undefined ? `${node.id}` : '');
|
|
79
|
+
return `撤回新增 ${count} 个节点${count === 1 && label ? `(${label})` : ''}`;
|
|
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
|
+
}
|
|
86
99
|
};
|
|
87
100
|
|
|
88
101
|
class Editor extends BaseService {
|
|
@@ -101,7 +114,6 @@ class Editor extends BaseService {
|
|
|
101
114
|
disabledMultiSelect: false,
|
|
102
115
|
alwaysMultiSelect: false,
|
|
103
116
|
});
|
|
104
|
-
private isHistoryStateChange = false;
|
|
105
117
|
private selectionBeforeOp: Id[] | null = null;
|
|
106
118
|
|
|
107
119
|
constructor() {
|
|
@@ -212,7 +224,7 @@ class Editor extends BaseService {
|
|
|
212
224
|
* 只有容器拥有布局
|
|
213
225
|
*/
|
|
214
226
|
public async getLayout(parent: MNode, node?: MNode | null): Promise<Layout> {
|
|
215
|
-
if (node &&
|
|
227
|
+
if (node && isFixed(node.style || {})) return Layout.FIXED;
|
|
216
228
|
|
|
217
229
|
if (parent.layout) {
|
|
218
230
|
return parent.layout;
|
|
@@ -388,16 +400,14 @@ class Editor extends BaseService {
|
|
|
388
400
|
* @param options 可选配置
|
|
389
401
|
* @param options.doNotSelect 添加后是否不更新当前选中节点(默认 false,添加后会选中新增的节点)
|
|
390
402
|
* @param options.doNotSwitchPage 添加后是否不切换当前页面(默认 false;新增页面 / 跨页新增时为 true 会跳过会引发页面切换的选中操作)
|
|
403
|
+
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
|
391
404
|
* @returns 添加后的节点
|
|
392
405
|
*/
|
|
393
406
|
public async add(
|
|
394
407
|
addNode: AddMNode | MNode[],
|
|
395
408
|
parent?: MContainer | null,
|
|
396
|
-
|
|
409
|
+
{ doNotSelect = false, doNotSwitchPage = false, doNotPushHistory = false, historyDescription }: DslOpOptions = {},
|
|
397
410
|
): Promise<MNode | MNode[]> {
|
|
398
|
-
const safeParentNode = safeParent(parent);
|
|
399
|
-
const { doNotSelect = false, doNotSwitchPage = false } = safeOptions<DslOpOptions>(options);
|
|
400
|
-
|
|
401
411
|
this.captureSelectionBeforeOp();
|
|
402
412
|
|
|
403
413
|
const stage = this.get('stage');
|
|
@@ -420,7 +430,7 @@ class Editor extends BaseService {
|
|
|
420
430
|
if ((isPage(node) || isPageFragment(node)) && root) {
|
|
421
431
|
return this.doAdd(node, root);
|
|
422
432
|
}
|
|
423
|
-
const parentNode =
|
|
433
|
+
const parentNode = parent ?? getAddParent(node);
|
|
424
434
|
if (!parentNode) throw new Error('未找到父元素');
|
|
425
435
|
return this.doAdd(node, parentNode);
|
|
426
436
|
}),
|
|
@@ -455,20 +465,25 @@ class Editor extends BaseService {
|
|
|
455
465
|
|
|
456
466
|
if (!(isPage(newNodes[0]) || isPageFragment(newNodes[0]))) {
|
|
457
467
|
const pageForOp = this.getNodeInfo(newNodes[0].id, false).page;
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
468
|
+
if (!doNotPushHistory) {
|
|
469
|
+
this.pushOpHistory(
|
|
470
|
+
'add',
|
|
471
|
+
{
|
|
472
|
+
nodes: newNodes.map((n) => cloneDeep(toRaw(n))),
|
|
473
|
+
parentId: (this.getParentById(newNodes[0].id, false) ?? this.get('root'))!.id,
|
|
474
|
+
indexMap: Object.fromEntries(
|
|
475
|
+
newNodes.map((n) => {
|
|
476
|
+
const p = this.getParentById(n.id, false) as MContainer;
|
|
477
|
+
return [n.id, p ? getNodeIndex(n.id, p) : -1];
|
|
478
|
+
}),
|
|
479
|
+
),
|
|
480
|
+
},
|
|
481
|
+
{ name: pageForOp?.name || '', id: pageForOp!.id },
|
|
482
|
+
historyDescription,
|
|
483
|
+
);
|
|
484
|
+
} else {
|
|
485
|
+
this.selectionBeforeOp = null;
|
|
486
|
+
}
|
|
472
487
|
}
|
|
473
488
|
|
|
474
489
|
this.emit('add', newNodes);
|
|
@@ -476,9 +491,10 @@ class Editor extends BaseService {
|
|
|
476
491
|
return Array.isArray(addNode) ? newNodes : newNodes[0];
|
|
477
492
|
}
|
|
478
493
|
|
|
479
|
-
public async doRemove(
|
|
480
|
-
|
|
481
|
-
|
|
494
|
+
public async doRemove(
|
|
495
|
+
node: MNode,
|
|
496
|
+
{ doNotSelect = false, doNotSwitchPage = false }: DslOpOptions = {},
|
|
497
|
+
): Promise<void> {
|
|
482
498
|
const root = this.get('root');
|
|
483
499
|
if (!root) throw new Error('root不能为空');
|
|
484
500
|
|
|
@@ -557,10 +573,12 @@ class Editor extends BaseService {
|
|
|
557
573
|
* @param options 可选配置
|
|
558
574
|
* @param options.doNotSelect 删除后是否不更新当前选中节点(默认 false,删除后会选中父节点或首个页面)
|
|
559
575
|
* @param options.doNotSwitchPage 删除后是否不切换当前页面(默认 false;删除页面 / 页面片段时为 true 会跳过自动切换到首个剩余页面)
|
|
576
|
+
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
|
560
577
|
*/
|
|
561
|
-
public async remove(
|
|
562
|
-
|
|
563
|
-
|
|
578
|
+
public async remove(
|
|
579
|
+
nodeOrNodeList: MNode | MNode[],
|
|
580
|
+
{ doNotSelect = false, doNotSwitchPage = false, doNotPushHistory = false, historyDescription }: DslOpOptions = {},
|
|
581
|
+
): Promise<void> {
|
|
564
582
|
this.captureSelectionBeforeOp();
|
|
565
583
|
|
|
566
584
|
const nodes = Array.isArray(nodeOrNodeList) ? nodeOrNodeList : [nodeOrNodeList];
|
|
@@ -587,7 +605,11 @@ class Editor extends BaseService {
|
|
|
587
605
|
await Promise.all(nodes.map((node) => this.doRemove(node, { doNotSelect, doNotSwitchPage })));
|
|
588
606
|
|
|
589
607
|
if (removedItems.length > 0 && pageForOp) {
|
|
590
|
-
|
|
608
|
+
if (!doNotPushHistory) {
|
|
609
|
+
this.pushOpHistory('remove', { removedItems }, pageForOp, historyDescription);
|
|
610
|
+
} else {
|
|
611
|
+
this.selectionBeforeOp = null;
|
|
612
|
+
}
|
|
591
613
|
}
|
|
592
614
|
|
|
593
615
|
this.emit('remove', nodes);
|
|
@@ -668,34 +690,60 @@ class Editor extends BaseService {
|
|
|
668
690
|
* 更新节点
|
|
669
691
|
* update后会触发依赖收集,收集完后会掉stage.update方法
|
|
670
692
|
* @param config 新的节点配置,配置中需要有id信息
|
|
693
|
+
* @param data 额外数据
|
|
694
|
+
* @param data.changeRecords 单节点 form 端变更记录(多节点场景下被忽略,使用 changeRecordList)
|
|
695
|
+
* @param data.changeRecordList 多节点 form 端变更记录列表,按 config 数组同序对应每个节点;优先级高于 changeRecords
|
|
696
|
+
* @param data.doNotPushHistory 是否不写入历史记录(默认 false)
|
|
697
|
+
* @param data.historyDescription 入栈时附带的人类可读描述,用于历史面板展示(不影响 undo/redo 行为)
|
|
671
698
|
* @returns 更新后的节点配置
|
|
672
699
|
*/
|
|
673
700
|
public async update(
|
|
674
701
|
config: MNode | MNode[],
|
|
675
|
-
data: {
|
|
702
|
+
data: {
|
|
703
|
+
changeRecords?: ChangeRecord[];
|
|
704
|
+
changeRecordList?: ChangeRecord[][];
|
|
705
|
+
doNotPushHistory?: boolean;
|
|
706
|
+
historyDescription?: string;
|
|
707
|
+
} = {},
|
|
676
708
|
): Promise<MNode | MNode[]> {
|
|
677
709
|
this.captureSelectionBeforeOp();
|
|
678
710
|
|
|
711
|
+
const { doNotPushHistory = false, changeRecordList, changeRecords, historyDescription } = data;
|
|
712
|
+
|
|
679
713
|
const nodes = Array.isArray(config) ? config : [config];
|
|
680
714
|
|
|
681
|
-
|
|
715
|
+
// 多节点必须使用 changeRecordList 为每个节点提供独立的记录;
|
|
716
|
+
// 否则同一份 changeRecords 会被复用到每个节点上,nodeUpdateHandler / 历史回放都会按错误的 propPath 处理。
|
|
717
|
+
const updateData = await Promise.all(
|
|
718
|
+
nodes.map((node, index) => {
|
|
719
|
+
const recordsForNode = changeRecordList ? (changeRecordList[index] ?? []) : (changeRecords ?? []);
|
|
720
|
+
return this.doUpdate(node, { changeRecords: recordsForNode });
|
|
721
|
+
}),
|
|
722
|
+
);
|
|
682
723
|
|
|
683
724
|
if (updateData[0].oldNode?.type !== NodeType.ROOT) {
|
|
684
725
|
const curNodes = this.get('nodes');
|
|
685
|
-
if (
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
726
|
+
if (curNodes.length) {
|
|
727
|
+
if (!doNotPushHistory) {
|
|
728
|
+
const pageForOp = this.getNodeInfo(nodes[0].id, false).page;
|
|
729
|
+
this.pushOpHistory(
|
|
730
|
+
'update',
|
|
731
|
+
{
|
|
732
|
+
updatedItems: updateData.map((d) => ({
|
|
733
|
+
oldNode: cloneDeep(d.oldNode),
|
|
734
|
+
newNode: cloneDeep(toRaw(d.newNode)),
|
|
735
|
+
// 每个节点单独保留自己的 changeRecords,便于撤销/重做时按 propPath 精细化更新;
|
|
736
|
+
// 没有 changeRecords 的(如内部 sort/moveLayer 等整节点替换操作)会退化为全节点替换。
|
|
737
|
+
changeRecords: d.changeRecords?.length ? cloneDeep(d.changeRecords) : undefined,
|
|
738
|
+
})),
|
|
739
|
+
},
|
|
740
|
+
{ name: pageForOp?.name || '', id: pageForOp!.id },
|
|
741
|
+
historyDescription,
|
|
742
|
+
);
|
|
743
|
+
} else {
|
|
744
|
+
this.selectionBeforeOp = null;
|
|
745
|
+
}
|
|
697
746
|
}
|
|
698
|
-
this.isHistoryStateChange = false;
|
|
699
747
|
}
|
|
700
748
|
|
|
701
749
|
this.emit('update', updateData);
|
|
@@ -709,11 +757,14 @@ class Editor extends BaseService {
|
|
|
709
757
|
* @param options 可选配置
|
|
710
758
|
* @param options.doNotSelect 排序后是否不更新当前选中节点(默认 false)
|
|
711
759
|
* @param options.doNotSwitchPage 排序后是否不切换当前页面(排序只发生在同一父节点内,方法内为空操作;保留以与其它 DSL 操作 API 一致)
|
|
760
|
+
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
|
712
761
|
* @returns void
|
|
713
762
|
*/
|
|
714
|
-
public async sort(
|
|
715
|
-
|
|
716
|
-
|
|
763
|
+
public async sort(
|
|
764
|
+
id1: Id,
|
|
765
|
+
id2: Id,
|
|
766
|
+
{ doNotSelect = false, doNotPushHistory = false }: DslOpOptions = {},
|
|
767
|
+
): Promise<void> {
|
|
717
768
|
this.captureSelectionBeforeOp();
|
|
718
769
|
|
|
719
770
|
const root = this.get('root');
|
|
@@ -732,7 +783,7 @@ class Editor extends BaseService {
|
|
|
732
783
|
|
|
733
784
|
parent.items.splice(index2, 0, ...parent.items.splice(index1, 1));
|
|
734
785
|
|
|
735
|
-
await this.update(parent);
|
|
786
|
+
await this.update(parent, { doNotPushHistory });
|
|
736
787
|
if (!doNotSelect) {
|
|
737
788
|
await this.select(node);
|
|
738
789
|
}
|
|
@@ -779,15 +830,14 @@ class Editor extends BaseService {
|
|
|
779
830
|
* @param options 可选配置
|
|
780
831
|
* @param options.doNotSelect 粘贴后是否不更新当前选中节点(默认 false)
|
|
781
832
|
* @param options.doNotSwitchPage 粘贴后是否不切换当前页面(默认 false;跨页粘贴时为 true 会跳过页面切换)
|
|
833
|
+
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
|
782
834
|
* @returns 添加后的组件节点配置
|
|
783
835
|
*/
|
|
784
836
|
public async paste(
|
|
785
837
|
position: PastePosition = {},
|
|
786
838
|
collectorOptions?: TargetOptions,
|
|
787
|
-
|
|
839
|
+
{ doNotSelect = false, doNotSwitchPage = false, doNotPushHistory = false }: DslOpOptions = {},
|
|
788
840
|
): Promise<MNode | MNode[] | void> {
|
|
789
|
-
const { doNotSelect = false, doNotSwitchPage = false } = safeOptions<DslOpOptions>(options);
|
|
790
|
-
|
|
791
841
|
const config: MNode[] = storageService.getItem(COPY_STORAGE_KEY);
|
|
792
842
|
if (!Array.isArray(config)) return;
|
|
793
843
|
|
|
@@ -807,7 +857,7 @@ class Editor extends BaseService {
|
|
|
807
857
|
propsService.replaceRelateId(config, pasteConfigs, collectorOptions);
|
|
808
858
|
}
|
|
809
859
|
|
|
810
|
-
return this.add(pasteConfigs, parent, { doNotSelect, doNotSwitchPage });
|
|
860
|
+
return this.add(pasteConfigs, parent, { doNotSelect, doNotSwitchPage, doNotPushHistory });
|
|
811
861
|
}
|
|
812
862
|
|
|
813
863
|
public async doPaste(config: MNode[], position: PastePosition = {}): Promise<MNode[]> {
|
|
@@ -838,17 +888,19 @@ class Editor extends BaseService {
|
|
|
838
888
|
* @param options 可选配置
|
|
839
889
|
* @param options.doNotSelect 居中后是否不更新当前选中节点(默认 false)
|
|
840
890
|
* @param options.doNotSwitchPage 居中后是否不切换当前页面(居中只更新节点 style,方法内为空操作;保留以与其它 DSL 操作 API 一致)
|
|
891
|
+
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
|
841
892
|
* @returns 当前组件节点配置
|
|
842
893
|
*/
|
|
843
|
-
public async alignCenter(
|
|
844
|
-
|
|
845
|
-
|
|
894
|
+
public async alignCenter(
|
|
895
|
+
config: MNode | MNode[],
|
|
896
|
+
{ doNotSelect = false, doNotPushHistory = false }: DslOpOptions = {},
|
|
897
|
+
): Promise<MNode | MNode[]> {
|
|
846
898
|
const nodes = Array.isArray(config) ? config : [config];
|
|
847
899
|
const stage = this.get('stage');
|
|
848
900
|
|
|
849
901
|
const newNodes = await Promise.all(nodes.map((node) => this.doAlignCenter(node)));
|
|
850
902
|
|
|
851
|
-
const newNode = await this.update(newNodes);
|
|
903
|
+
const newNode = await this.update(newNodes, { doNotPushHistory });
|
|
852
904
|
|
|
853
905
|
if (!doNotSelect) {
|
|
854
906
|
if (newNodes.length > 1) {
|
|
@@ -864,8 +916,10 @@ class Editor extends BaseService {
|
|
|
864
916
|
/**
|
|
865
917
|
* 移动当前选中节点位置
|
|
866
918
|
* @param offset 偏移量
|
|
919
|
+
* @param options 可选配置
|
|
920
|
+
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
|
867
921
|
*/
|
|
868
|
-
public async moveLayer(offset: number | LayerOffset): Promise<void> {
|
|
922
|
+
public async moveLayer(offset: number | LayerOffset, { doNotPushHistory = false }: DslOpOptions = {}): Promise<void> {
|
|
869
923
|
this.captureSelectionBeforeOp();
|
|
870
924
|
|
|
871
925
|
const root = this.get('root');
|
|
@@ -902,94 +956,116 @@ class Editor extends BaseService {
|
|
|
902
956
|
});
|
|
903
957
|
|
|
904
958
|
this.addModifiedNodeId(parent.id);
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
959
|
+
if (!doNotPushHistory) {
|
|
960
|
+
const pageForOp = this.getNodeInfo(node.id, false).page;
|
|
961
|
+
this.pushOpHistory(
|
|
962
|
+
'update',
|
|
963
|
+
{
|
|
964
|
+
updatedItems: [{ oldNode: oldParent, newNode: cloneDeep(toRaw(parent)) }],
|
|
965
|
+
},
|
|
966
|
+
{ name: pageForOp?.name || '', id: pageForOp!.id },
|
|
967
|
+
);
|
|
968
|
+
} else {
|
|
969
|
+
this.selectionBeforeOp = null;
|
|
970
|
+
}
|
|
913
971
|
|
|
914
972
|
this.emit('move-layer', offset);
|
|
915
973
|
}
|
|
916
974
|
|
|
917
975
|
/**
|
|
918
|
-
*
|
|
919
|
-
*
|
|
976
|
+
* 移动一个或多个节点到指定容器中。
|
|
977
|
+
*
|
|
978
|
+
* 多选场景(config 是数组)只会产生一条历史记录,
|
|
979
|
+
* `updatedItems` 涵盖所有源父容器 + 目标容器的前后快照。
|
|
980
|
+
* 这避免了"多选移动到某容器"在历史栈里被切成 N 条记录。
|
|
981
|
+
*
|
|
982
|
+
* @param config 需要移动的节点(或节点数组,各项需带 id;style 等字段会与原节点合并)
|
|
920
983
|
* @param targetId 容器ID
|
|
921
984
|
* @param options 可选配置
|
|
922
985
|
* @param options.doNotSelect 移动后是否不更新当前选中节点(默认 false)
|
|
923
986
|
* @param options.doNotSwitchPage 移动后是否不切换当前页面(默认 false;目标容器位于其它页面时为 true 会跳过自动选中以避免页面切换)
|
|
987
|
+
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
|
924
988
|
*/
|
|
925
|
-
public async moveToContainer(
|
|
926
|
-
|
|
989
|
+
public async moveToContainer(
|
|
990
|
+
config: MNode | MNode[],
|
|
991
|
+
targetId: Id,
|
|
992
|
+
{ doNotSelect = false, doNotSwitchPage = false, doNotPushHistory = false }: DslOpOptions = {},
|
|
993
|
+
): Promise<MNode | MNode[]> {
|
|
994
|
+
const isBatch = Array.isArray(config);
|
|
995
|
+
const configs = (isBatch ? config : [config]).filter((item) => !(isPage(item) || isPageFragment(item)));
|
|
996
|
+
|
|
997
|
+
if (configs.length === 0) {
|
|
998
|
+
throw new Error('没有可移动的节点');
|
|
999
|
+
}
|
|
927
1000
|
|
|
928
1001
|
this.captureSelectionBeforeOp();
|
|
929
1002
|
|
|
930
|
-
const root = this.get('root');
|
|
931
|
-
const { node, parent, page: pageForOp } = this.getNodeInfo(config.id, false);
|
|
932
1003
|
const target = this.getNodeById(targetId, false) as MContainer;
|
|
933
1004
|
|
|
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) });
|
|
943
|
-
|
|
944
|
-
const layout = await this.getLayout(target);
|
|
945
|
-
|
|
946
|
-
const newConfig = mergeWith(cloneDeep(node), config, (_objValue, srcValue) => {
|
|
947
|
-
if (Array.isArray(srcValue)) {
|
|
948
|
-
return srcValue;
|
|
949
|
-
}
|
|
950
|
-
});
|
|
951
|
-
newConfig.style = getInitPositionStyle(newConfig.style, layout);
|
|
1005
|
+
if (!target) {
|
|
1006
|
+
throw new Error('目标容器不存在');
|
|
1007
|
+
}
|
|
952
1008
|
|
|
953
|
-
|
|
1009
|
+
const root = this.get('root');
|
|
1010
|
+
const stage = this.get('stage');
|
|
954
1011
|
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1012
|
+
if (!root || !stage) {
|
|
1013
|
+
throw new Error('root 或 stage为空');
|
|
1014
|
+
}
|
|
958
1015
|
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1016
|
+
// 收集 (节点, 源父) 信息,过滤掉异常节点(找不到父或源父等于目标本身)
|
|
1017
|
+
const moves: MoveItem[] = [];
|
|
1018
|
+
for (const { id } of configs) {
|
|
1019
|
+
const { node, parent, page } = this.getNodeInfo(id, false);
|
|
1020
|
+
if (!node || !parent) continue;
|
|
1021
|
+
moves.push({ node, parent, pageForOp: page ? { name: page.name || '', id: page.id } : null });
|
|
1022
|
+
}
|
|
962
1023
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
parentId: targetParent?.id,
|
|
967
|
-
root: cloneDeep(root),
|
|
968
|
-
});
|
|
1024
|
+
if (moves.length === 0) {
|
|
1025
|
+
throw new Error('没有可移动的节点');
|
|
1026
|
+
}
|
|
969
1027
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1028
|
+
// 记录所有涉及的源父容器(按 id 去重)+ 目标容器的前置快照;同一父容器只快照一次。
|
|
1029
|
+
const beforeSnapshots = new Map<Id, MNode>();
|
|
1030
|
+
beforeSnapshots.set(target.id, cloneDeep(toRaw(target)));
|
|
1031
|
+
for (const { parent } of moves) {
|
|
1032
|
+
if (!beforeSnapshots.has(parent.id)) {
|
|
1033
|
+
beforeSnapshots.set(parent.id, cloneDeep(toRaw(parent)));
|
|
973
1034
|
}
|
|
1035
|
+
}
|
|
974
1036
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1037
|
+
let newConfigs: MNode[] = [];
|
|
1038
|
+
|
|
1039
|
+
const moveNodes = moves.map(({ node }) => node);
|
|
1040
|
+
await this.remove(moveNodes, { doNotPushHistory: true, doNotSelect, doNotSwitchPage: true });
|
|
1041
|
+
|
|
1042
|
+
newConfigs = (await this.add(moveNodes, target, {
|
|
1043
|
+
doNotPushHistory: true,
|
|
1044
|
+
doNotSelect,
|
|
1045
|
+
doNotSwitchPage,
|
|
1046
|
+
})) as MNode[];
|
|
1047
|
+
|
|
1048
|
+
if (!doNotPushHistory) {
|
|
1049
|
+
// 整批只入栈一条历史:updatedItems 包含所有源父容器 + 目标容器的前后快照(撤销/重做最小依赖)。
|
|
1050
|
+
const updatedItems = Array.from(beforeSnapshots.entries()).map(([id, oldNode]) => ({
|
|
1051
|
+
oldNode,
|
|
1052
|
+
newNode: cloneDeep(toRaw(this.getNodeById(id, false))) as MNode,
|
|
1053
|
+
}));
|
|
1054
|
+
const historyPage = moves[0].pageForOp ?? { name: '', id: target.id };
|
|
1055
|
+
this.pushOpHistory('update', { updatedItems }, historyPage);
|
|
1056
|
+
} else {
|
|
1057
|
+
this.selectionBeforeOp = null;
|
|
989
1058
|
}
|
|
1059
|
+
|
|
1060
|
+
return isBatch ? newConfigs : newConfigs[0];
|
|
990
1061
|
}
|
|
991
1062
|
|
|
992
|
-
public async dragTo(
|
|
1063
|
+
public async dragTo(
|
|
1064
|
+
config: MNode | MNode[],
|
|
1065
|
+
targetParent: MContainer,
|
|
1066
|
+
targetIndex: number,
|
|
1067
|
+
{ doNotPushHistory = false }: DslOpOptions = {},
|
|
1068
|
+
) {
|
|
993
1069
|
this.captureSelectionBeforeOp();
|
|
994
1070
|
|
|
995
1071
|
if (!targetParent || !Array.isArray(targetParent.items)) return;
|
|
@@ -1049,8 +1125,12 @@ class Editor extends BaseService {
|
|
|
1049
1125
|
updatedItems.push({ oldNode, newNode: cloneDeep(toRaw(newNode)) });
|
|
1050
1126
|
}
|
|
1051
1127
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1128
|
+
if (!doNotPushHistory) {
|
|
1129
|
+
const pageForOp = this.getNodeInfo(configs[0].id, false).page;
|
|
1130
|
+
this.pushOpHistory('update', { updatedItems }, { name: pageForOp?.name || '', id: pageForOp!.id });
|
|
1131
|
+
} else {
|
|
1132
|
+
this.selectionBeforeOp = null;
|
|
1133
|
+
}
|
|
1054
1134
|
|
|
1055
1135
|
this.emit('drag-to', { targetIndex, configs, targetParent });
|
|
1056
1136
|
}
|
|
@@ -1079,14 +1159,140 @@ class Editor extends BaseService {
|
|
|
1079
1159
|
return value;
|
|
1080
1160
|
}
|
|
1081
1161
|
|
|
1082
|
-
|
|
1162
|
+
/**
|
|
1163
|
+
* 「回滚」指定页面历史步骤(类 git revert 语义):
|
|
1164
|
+
* - 不动原始历史栈结构(不移动 cursor、不丢弃任何步骤);
|
|
1165
|
+
* - 取出 `index` 对应的 step,**反向应用**一次(add→remove / remove→add / update→旧值);
|
|
1166
|
+
* - 把这次反向应用作为一条**新步骤**追加到栈顶,可被普通 undo / redo。
|
|
1167
|
+
*
|
|
1168
|
+
* 与 `gotoPageStep`(类 git reset)的区别在于此操作**不丢弃**目标之后的历史。
|
|
1169
|
+
* 与 `applyHistoryOp(reverse=true)` 的区别在于:本方法**不带** `doNotPushHistory`,
|
|
1170
|
+
* 反向应用会以一条新 step 入栈;并且不实施 step 中保存的选区与 modifiedNodeIds 状态,
|
|
1171
|
+
* 选区由用户当前位置决定,符合"新提交"语义。
|
|
1172
|
+
*
|
|
1173
|
+
* 仅对处于「已应用」状态的步骤生效——未应用的步骤本身就不存在于当前 DSL 中,反向无意义。
|
|
1174
|
+
*
|
|
1175
|
+
* @param index 目标 step 在所属页面栈中的索引(0 为最早),通常由历史面板传入
|
|
1176
|
+
* @returns 反向后产生的新 step;目标不存在 / 未应用 / 反向失败时返回 null
|
|
1177
|
+
*/
|
|
1178
|
+
public async revertPageStep(index: number): Promise<StepValue | null> {
|
|
1179
|
+
const list = historyService.getPageStepList();
|
|
1180
|
+
const entry = list[index];
|
|
1181
|
+
if (!entry?.applied) return null;
|
|
1182
|
+
|
|
1183
|
+
const { step } = entry;
|
|
1184
|
+
const root = this.get('root');
|
|
1185
|
+
if (!root) return null;
|
|
1186
|
+
|
|
1187
|
+
// 反向应用产生的新 step 由内部 pushOpHistory 触发 history `change` 事件,监听一次以拿到引用。
|
|
1188
|
+
let revertedStep: StepValue | null = null;
|
|
1189
|
+
const captureRevert = (s: StepValue) => {
|
|
1190
|
+
revertedStep = s;
|
|
1191
|
+
};
|
|
1192
|
+
historyService.once('change', captureRevert);
|
|
1193
|
+
|
|
1194
|
+
const historyDescription = `回滚 #${index + 1}: ${describeStepForRevert(step)}`;
|
|
1195
|
+
// revert 走 public add/remove/update,让操作以一条普通新 step 入栈;不要切换选区与页面,避免打断用户。
|
|
1196
|
+
const opts = { doNotSelect: true, doNotSwitchPage: true, historyDescription } as const;
|
|
1197
|
+
|
|
1198
|
+
try {
|
|
1199
|
+
switch (step.opType) {
|
|
1200
|
+
case 'add': {
|
|
1201
|
+
// 原本是新增 → revert 即删除当时被加入的节点
|
|
1202
|
+
const nodes = step.nodes ?? [];
|
|
1203
|
+
for (const n of nodes) {
|
|
1204
|
+
const existing = this.getNodeById(n.id, false);
|
|
1205
|
+
if (existing) {
|
|
1206
|
+
await this.remove(existing, opts);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
break;
|
|
1210
|
+
}
|
|
1211
|
+
case 'remove': {
|
|
1212
|
+
// 原本是删除 → revert 即把节点按原父容器加回来。
|
|
1213
|
+
// 按原 index 升序逐个插回,先小后大避免索引漂移。
|
|
1214
|
+
const items = step.removedItems ?? [];
|
|
1215
|
+
const sorted = [...items].sort((a, b) => a.index - b.index);
|
|
1216
|
+
for (const { node, parentId } of sorted) {
|
|
1217
|
+
const parent = this.getNodeById(parentId, false) as MContainer | null;
|
|
1218
|
+
if (parent) {
|
|
1219
|
+
await this.add([cloneDeep(node)] as MNode[], parent, opts);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
break;
|
|
1223
|
+
}
|
|
1224
|
+
case 'update': {
|
|
1225
|
+
// 原本是更新 → revert 即把 oldNode 的值写回;
|
|
1226
|
+
// 优先按 changeRecords 局部 patch(仅触达 propPath 下的字段,避免冲掉同节点上其它无关变更)。
|
|
1227
|
+
const items = step.updatedItems ?? [];
|
|
1228
|
+
const configs = items.map(({ oldNode, newNode, changeRecords }) => {
|
|
1229
|
+
if (changeRecords?.length) {
|
|
1230
|
+
const patch: MNode = { id: newNode.id, type: newNode.type };
|
|
1231
|
+
for (const record of changeRecords) {
|
|
1232
|
+
if (!record.propPath) {
|
|
1233
|
+
// 没有 propPath 视为整节点替换
|
|
1234
|
+
return cloneDeep(oldNode);
|
|
1235
|
+
}
|
|
1236
|
+
const value = cloneDeep(getValueByKeyPath(record.propPath, oldNode));
|
|
1237
|
+
setValueByKeyPath(record.propPath, value, patch);
|
|
1238
|
+
}
|
|
1239
|
+
return patch;
|
|
1240
|
+
}
|
|
1241
|
+
return cloneDeep(oldNode);
|
|
1242
|
+
});
|
|
1243
|
+
if (configs.length) {
|
|
1244
|
+
await this.update(configs, { historyDescription });
|
|
1245
|
+
}
|
|
1246
|
+
break;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
} finally {
|
|
1250
|
+
historyService.off('change', captureRevert);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// 通知一次 history-change,让上层(如属性面板)按当前最新 DSL 刷新
|
|
1254
|
+
const page = toRaw(this.get('page'));
|
|
1255
|
+
if (page) {
|
|
1256
|
+
this.emit('history-change', page as MPage | MPageFragment);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
return revertedStep;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/**
|
|
1263
|
+
* 跳转当前页面历史栈到指定游标位置。
|
|
1264
|
+
*
|
|
1265
|
+
* `targetCursor` 与 `UndoRedo.getCursor()` 同义:表示"已应用步骤数量",
|
|
1266
|
+
* 取值范围 `[0, length]`。当目标 < 当前游标时循环 undo,否则循环 redo。
|
|
1267
|
+
* 通常由历史面板传入「点击的 step.index + 1」作为目标。
|
|
1268
|
+
*
|
|
1269
|
+
* @returns 实际移动到的最终游标位置
|
|
1270
|
+
*/
|
|
1271
|
+
public async gotoPageStep(targetCursor: number): Promise<number> {
|
|
1272
|
+
let cursor = historyService.getPageCursor();
|
|
1273
|
+
const { length } = historyService.getPageStepList();
|
|
1274
|
+
const target = Math.max(0, Math.min(targetCursor, length));
|
|
1275
|
+
while (cursor > target) {
|
|
1276
|
+
const step = await this.undo();
|
|
1277
|
+
if (!step) break;
|
|
1278
|
+
cursor -= 1;
|
|
1279
|
+
}
|
|
1280
|
+
while (cursor < target) {
|
|
1281
|
+
const step = await this.redo();
|
|
1282
|
+
if (!step) break;
|
|
1283
|
+
cursor += 1;
|
|
1284
|
+
}
|
|
1285
|
+
return cursor;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
public async move(left: number, top: number, { doNotPushHistory = false }: DslOpOptions = {}) {
|
|
1083
1289
|
const node = toRaw(this.get('node'));
|
|
1084
1290
|
if (!node || isPage(node)) return;
|
|
1085
1291
|
|
|
1086
1292
|
const newStyle = calcMoveStyle(node.style || {}, left, top);
|
|
1087
1293
|
if (!newStyle) return;
|
|
1088
1294
|
|
|
1089
|
-
await this.update({ id: node.id, type: node.type, style: newStyle });
|
|
1295
|
+
await this.update({ id: node.id, type: node.type, style: newStyle }, { doNotPushHistory });
|
|
1090
1296
|
}
|
|
1091
1297
|
|
|
1092
1298
|
public resetState() {
|
|
@@ -1134,22 +1340,20 @@ class Editor extends BaseService {
|
|
|
1134
1340
|
}
|
|
1135
1341
|
|
|
1136
1342
|
private addModifiedNodeId(id: Id) {
|
|
1137
|
-
|
|
1138
|
-
this.get('modifiedNodeIds').set(id, id);
|
|
1139
|
-
}
|
|
1343
|
+
this.get('modifiedNodeIds').set(id, id);
|
|
1140
1344
|
}
|
|
1141
1345
|
|
|
1142
1346
|
private captureSelectionBeforeOp() {
|
|
1143
|
-
if (this.
|
|
1347
|
+
if (this.selectionBeforeOp) return;
|
|
1144
1348
|
this.selectionBeforeOp = this.get('nodes').map((n) => n.id);
|
|
1145
1349
|
}
|
|
1146
1350
|
|
|
1147
|
-
private pushOpHistory(
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1351
|
+
private pushOpHistory(
|
|
1352
|
+
opType: HistoryOpType,
|
|
1353
|
+
extra: Partial<StepValue>,
|
|
1354
|
+
pageData: { name: string; id: Id },
|
|
1355
|
+
historyDescription?: string,
|
|
1356
|
+
) {
|
|
1153
1357
|
const step: StepValue = {
|
|
1154
1358
|
data: pageData,
|
|
1155
1359
|
opType,
|
|
@@ -1158,43 +1362,124 @@ class Editor extends BaseService {
|
|
|
1158
1362
|
modifiedNodeIds: new Map(this.get('modifiedNodeIds')),
|
|
1159
1363
|
...extra,
|
|
1160
1364
|
};
|
|
1161
|
-
|
|
1365
|
+
if (historyDescription) step.historyDescription = historyDescription;
|
|
1366
|
+
// 显式按 step.data.id 入栈:跨页操作(如 moveToContainer 从源页搬到目标页)
|
|
1367
|
+
// 必须落到正确的页面栈,否则会把记录错误地推到当前活动页 / 操作发起页。
|
|
1368
|
+
historyService.push(step, pageData.id);
|
|
1162
1369
|
this.selectionBeforeOp = null;
|
|
1163
|
-
this.isHistoryStateChange = false;
|
|
1164
1370
|
}
|
|
1165
1371
|
|
|
1166
1372
|
/**
|
|
1167
1373
|
* 应用历史操作(撤销 / 重做)
|
|
1374
|
+
*
|
|
1375
|
+
* 所有 DSL 修改都走 `editor.add / remove / update`,并通过 `doNotPushHistory` 阻止再次入栈、
|
|
1376
|
+
* `doNotSelect / doNotSwitchPage` 让选区由方法末尾的统一逻辑兜底。
|
|
1377
|
+
*
|
|
1378
|
+
* 注意:这些公开方法会发出 add / remove / update 事件,业务侧若需要区分"用户操作"与"撤销重做触发",
|
|
1379
|
+
* 请监听 `history-change` 事件配合判断。
|
|
1380
|
+
*
|
|
1168
1381
|
* @param step 操作记录
|
|
1169
1382
|
* @param reverse true = 撤销,false = 重做
|
|
1170
1383
|
*/
|
|
1171
1384
|
private async applyHistoryOp(step: StepValue, reverse: boolean) {
|
|
1172
|
-
this.isHistoryStateChange = true;
|
|
1173
|
-
|
|
1174
1385
|
const root = this.get('root');
|
|
1175
1386
|
const stage = this.get('stage');
|
|
1176
1387
|
if (!root) return;
|
|
1177
1388
|
|
|
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
|
-
};
|
|
1389
|
+
const commonOpts = { doNotSelect: true, doNotSwitchPage: true, doNotPushHistory: true } as const;
|
|
1187
1390
|
|
|
1188
1391
|
switch (step.opType) {
|
|
1189
|
-
case 'add':
|
|
1190
|
-
|
|
1392
|
+
case 'add': {
|
|
1393
|
+
const nodes = step.nodes ?? [];
|
|
1394
|
+
if (reverse) {
|
|
1395
|
+
// 撤销 add:把当时加入的节点删除
|
|
1396
|
+
for (const n of nodes) {
|
|
1397
|
+
const existing = this.getNodeById(n.id, false);
|
|
1398
|
+
if (existing) {
|
|
1399
|
+
await this.remove(existing, commonOpts);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
} else {
|
|
1403
|
+
// 重做 add:按记录的 indexMap 把节点重新插回父容器
|
|
1404
|
+
const parent = this.getNodeById(step.parentId!, false) as MContainer | null;
|
|
1405
|
+
if (parent) {
|
|
1406
|
+
// 按目标 index 升序逐个插入,先小后大避免索引漂移
|
|
1407
|
+
const sorted = [...nodes].sort((a, b) => (step.indexMap?.[a.id] ?? 0) - (step.indexMap?.[b.id] ?? 0));
|
|
1408
|
+
for (const n of sorted) {
|
|
1409
|
+
const idx = step.indexMap?.[n.id];
|
|
1410
|
+
if (parent.items) {
|
|
1411
|
+
if (typeof idx === 'number' && idx >= 0 && idx < parent.items.length) {
|
|
1412
|
+
parent.items.splice(idx, 0, cloneDeep(n));
|
|
1413
|
+
} else {
|
|
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
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1191
1426
|
break;
|
|
1192
|
-
|
|
1193
|
-
|
|
1427
|
+
}
|
|
1428
|
+
case 'remove': {
|
|
1429
|
+
const items = step.removedItems ?? [];
|
|
1430
|
+
if (reverse) {
|
|
1431
|
+
// 撤销 remove:按原 index 升序逐个插回(先小后大避免索引漂移)
|
|
1432
|
+
const sorted = [...items].sort((a, b) => a.index - b.index);
|
|
1433
|
+
for (const { node, parentId, index } of sorted) {
|
|
1434
|
+
const parent = this.getNodeById(parentId, false) as MContainer | null;
|
|
1435
|
+
if (parent?.items) {
|
|
1436
|
+
parent.items.splice(index, 0, cloneDeep(node));
|
|
1437
|
+
await stage?.add({
|
|
1438
|
+
config: cloneDeep(node),
|
|
1439
|
+
parent: cloneDeep(parent),
|
|
1440
|
+
parentId,
|
|
1441
|
+
root: cloneDeep(root),
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
} else {
|
|
1446
|
+
// 重做 remove:再删一次
|
|
1447
|
+
for (const { node } of items) {
|
|
1448
|
+
const existing = this.getNodeById(node.id, false);
|
|
1449
|
+
if (existing) {
|
|
1450
|
+
await this.remove(existing, commonOpts);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1194
1454
|
break;
|
|
1195
|
-
|
|
1196
|
-
|
|
1455
|
+
}
|
|
1456
|
+
case 'update': {
|
|
1457
|
+
const items = step.updatedItems ?? [];
|
|
1458
|
+
// 优先按 changeRecords 局部 patch:仅触达 propPath 下的字段,避免整节点替换冲掉同节点上其它无关变更。
|
|
1459
|
+
// 没有 changeRecords 的(如内部 sort/moveLayer/拖动等整节点快照场景)才退化为整节点替换。
|
|
1460
|
+
const configs = items.map(({ oldNode, newNode, changeRecords }) => {
|
|
1461
|
+
if (changeRecords?.length) {
|
|
1462
|
+
const sourceForValues = reverse ? oldNode : newNode;
|
|
1463
|
+
// 仅保留 id / type 作为最小骨架,再按 propPath 写入需要回滚/重做的字段;
|
|
1464
|
+
// 后续 update -> mergeWith 会与现有节点深合并,patch 中未涉及的字段不会被改动。
|
|
1465
|
+
const patch: MNode = { id: newNode.id, type: newNode.type };
|
|
1466
|
+
for (const record of changeRecords) {
|
|
1467
|
+
if (!record.propPath) {
|
|
1468
|
+
// 没有 propPath 视为整节点替换
|
|
1469
|
+
return cloneDeep(sourceForValues);
|
|
1470
|
+
}
|
|
1471
|
+
const value = cloneDeep(getValueByKeyPath(record.propPath, sourceForValues));
|
|
1472
|
+
setValueByKeyPath(record.propPath, value, patch);
|
|
1473
|
+
}
|
|
1474
|
+
return patch;
|
|
1475
|
+
}
|
|
1476
|
+
return cloneDeep(reverse ? oldNode : newNode);
|
|
1477
|
+
});
|
|
1478
|
+
if (configs.length) {
|
|
1479
|
+
await this.update(configs, { doNotPushHistory: true });
|
|
1480
|
+
}
|
|
1197
1481
|
break;
|
|
1482
|
+
}
|
|
1198
1483
|
}
|
|
1199
1484
|
|
|
1200
1485
|
this.set('modifiedNodeIds', step.modifiedNodeIds);
|
|
@@ -1216,8 +1501,6 @@ class Editor extends BaseService {
|
|
|
1216
1501
|
}, 0);
|
|
1217
1502
|
this.emit('history-change', page as MPage | MPageFragment);
|
|
1218
1503
|
}
|
|
1219
|
-
|
|
1220
|
-
this.isHistoryStateChange = false;
|
|
1221
1504
|
}
|
|
1222
1505
|
|
|
1223
1506
|
private selectedConfigExceptionHandler(config: MNode | Id): EditorNodeInfo {
|