@tmagic/editor 1.7.9 → 1.7.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/es/Editor.vue_vue_type_script_setup_true_lang.js +3 -1
  2. package/dist/es/components/CodeParams.vue_vue_type_script_setup_true_lang.js +17 -7
  3. package/dist/es/fields/DataSourceFieldSelect/FieldSelect.vue_vue_type_script_setup_true_lang.js +38 -9
  4. package/dist/es/fields/DataSourceFieldSelect/Index.vue_vue_type_script_setup_true_lang.js +3 -1
  5. package/dist/es/fields/DataSourceMethodSelect.vue_vue_type_script_setup_true_name_true_lang.js +38 -5
  6. package/dist/es/fields/DataSourceMethods.vue_vue_type_script_setup_true_lang.js +3 -2
  7. package/dist/es/fields/DisplayConds.vue_vue_type_script_setup_true_lang.js +1 -0
  8. package/dist/es/fields/StyleSetter/Index.vue_vue_type_script_setup_true_lang.js +5 -0
  9. package/dist/es/fields/StyleSetter/pro/Transform.js +5 -0
  10. package/dist/es/fields/StyleSetter/pro/Transform.vue_vue_type_script_setup_true_lang.js +54 -0
  11. package/dist/es/hooks/use-code-block-edit.js +3 -2
  12. package/dist/es/hooks/use-stage.js +5 -1
  13. package/dist/es/index.js +3 -3
  14. package/dist/es/initService.js +4 -4
  15. package/dist/es/layouts/NavMenu.vue_vue_type_script_setup_true_lang.js +2 -0
  16. package/dist/es/layouts/workspace/viewer/Stage.vue_vue_type_script_setup_true_lang.js +64 -1
  17. package/dist/es/layouts/workspace/viewer/StageOverlay.vue_vue_type_script_setup_true_lang.js +1 -5
  18. package/dist/es/services/editor.js +191 -174
  19. package/dist/es/services/history.js +1 -9
  20. package/dist/es/services/props.js +2 -0
  21. package/dist/es/services/ui.js +1 -0
  22. package/dist/es/type.js +28 -1
  23. package/dist/es/utils/dep/worker.js +1 -1
  24. package/dist/es/utils/editor-history.js +100 -0
  25. package/dist/es/utils/editor.js +178 -4
  26. package/dist/es/utils/props.js +6 -2
  27. package/dist/es/utils/undo-redo.js +7 -4
  28. package/dist/tmagic-editor.umd.cjs +768 -246
  29. package/package.json +9 -9
  30. package/src/Editor.vue +1 -0
  31. package/src/components/CodeBlockEditor.vue +1 -1
  32. package/src/components/CodeParams.vue +23 -7
  33. package/src/editorProps.ts +2 -0
  34. package/src/fields/DataSourceFieldSelect/FieldSelect.vue +39 -6
  35. package/src/fields/DataSourceFieldSelect/Index.vue +1 -0
  36. package/src/fields/DataSourceMethodSelect.vue +54 -13
  37. package/src/fields/DataSourceMethods.vue +9 -5
  38. package/src/fields/DisplayConds.vue +1 -0
  39. package/src/fields/StyleSetter/Index.vue +5 -1
  40. package/src/fields/StyleSetter/pro/Transform.vue +54 -0
  41. package/src/fields/StyleSetter/pro/index.ts +1 -0
  42. package/src/hooks/use-code-block-edit.ts +8 -4
  43. package/src/hooks/use-stage.ts +11 -4
  44. package/src/initService.ts +6 -6
  45. package/src/layouts/NavMenu.vue +2 -0
  46. package/src/layouts/workspace/viewer/Stage.vue +89 -1
  47. package/src/layouts/workspace/viewer/StageOverlay.vue +1 -6
  48. package/src/services/editor.ts +231 -270
  49. package/src/services/history.ts +1 -9
  50. package/src/services/ui.ts +1 -0
  51. package/src/type.ts +67 -5
  52. package/src/utils/editor-history.ts +138 -0
  53. package/src/utils/editor.ts +249 -4
  54. package/src/utils/props.ts +5 -0
  55. package/src/utils/undo-redo.ts +7 -6
  56. package/types/index.d.ts +611 -491
@@ -17,23 +17,13 @@
17
17
  */
18
18
 
19
19
  import { reactive, toRaw } from 'vue';
20
- import { cloneDeep, get, isObject, mergeWith, uniq } from 'lodash-es';
21
- import type { Writable } from 'type-fest';
20
+ import { cloneDeep, isObject, mergeWith, uniq } from 'lodash-es';
22
21
 
23
22
  import type { Id, MApp, MContainer, MNode, MPage, MPageFragment, TargetOptions } from '@tmagic/core';
24
- import { NodeType, Target, Watcher } from '@tmagic/core';
23
+ import { NodeType } from '@tmagic/core';
25
24
  import type { ChangeRecord } from '@tmagic/form';
26
25
  import { isFixed } from '@tmagic/stage';
27
- import {
28
- calcValueByFontsize,
29
- getElById,
30
- getNodeInfo,
31
- getNodePath,
32
- isNumber,
33
- isPage,
34
- isPageFragment,
35
- isPop,
36
- } from '@tmagic/utils';
26
+ import { getNodeInfo, getNodePath, isPage, isPageFragment } from '@tmagic/utils';
37
27
 
38
28
  import BaseService from '@editor/services//BaseService';
39
29
  import propsService from '@editor/services//props';
@@ -42,69 +32,39 @@ import storageService, { Protocol } from '@editor/services/storage';
42
32
  import type {
43
33
  AddMNode,
44
34
  AsyncHookPlugin,
35
+ AsyncMethodName,
36
+ EditorEvents,
45
37
  EditorNodeInfo,
38
+ HistoryOpType,
46
39
  PastePosition,
47
40
  StepValue,
48
41
  StoreState,
49
42
  StoreStateKey,
50
43
  } from '@editor/type';
51
- import { LayerOffset, Layout } from '@editor/type';
44
+ import { canUsePluginMethods, LayerOffset, Layout } from '@editor/type';
52
45
  import {
53
- change2Fixed,
46
+ calcAlignCenterStyle,
47
+ calcLayerTargetIndex,
48
+ calcMoveStyle,
49
+ classifyDragSources,
50
+ collectRelatedNodes,
54
51
  COPY_STORAGE_KEY,
55
- Fixed2Other,
52
+ editorNodeMergeCustomizer,
56
53
  fixNodePosition,
57
54
  getInitPositionStyle,
58
55
  getNodeIndex,
59
56
  getPageFragmentList,
60
57
  getPageList,
61
58
  moveItemsInContainer,
59
+ resolveSelectedNode,
62
60
  setChildrenLayout,
63
61
  setLayout,
62
+ toggleFixedPosition,
64
63
  } from '@editor/utils/editor';
64
+ import type { HistoryOpContext } from '@editor/utils/editor-history';
65
+ import { applyHistoryAddOp, applyHistoryRemoveOp, applyHistoryUpdateOp } from '@editor/utils/editor-history';
65
66
  import { beforePaste, getAddParent } from '@editor/utils/operator';
66
67
 
67
- export interface EditorEvents {
68
- 'root-change': [value: StoreState['root'], preValue?: StoreState['root']];
69
- select: [node: MNode | null];
70
- add: [nodes: MNode[]];
71
- remove: [nodes: MNode[]];
72
- update: [nodes: { newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] }[]];
73
- 'move-layer': [offset: number | LayerOffset];
74
- 'drag-to': [data: { targetIndex: number; configs: MNode | MNode[]; targetParent: MContainer }];
75
- 'history-change': [data: MPage | MPageFragment];
76
- }
77
-
78
- const canUsePluginMethods = {
79
- async: [
80
- 'getLayout',
81
- 'highlight',
82
- 'select',
83
- 'multiSelect',
84
- 'doAdd',
85
- 'add',
86
- 'doRemove',
87
- 'remove',
88
- 'doUpdate',
89
- 'update',
90
- 'sort',
91
- 'copy',
92
- 'paste',
93
- 'doPaste',
94
- 'doAlignCenter',
95
- 'alignCenter',
96
- 'moveLayer',
97
- 'moveToContainer',
98
- 'dragTo',
99
- 'undo',
100
- 'redo',
101
- 'move',
102
- ] as const,
103
- sync: [],
104
- };
105
-
106
- type AsyncMethodName = Writable<(typeof canUsePluginMethods)['async']>;
107
-
108
68
  class Editor extends BaseService {
109
69
  public state: StoreState = reactive({
110
70
  root: null,
@@ -121,6 +81,7 @@ class Editor extends BaseService {
121
81
  disabledMultiSelect: false,
122
82
  });
123
83
  private isHistoryStateChange = false;
84
+ private selectionBeforeOp: Id[] | null = null;
124
85
 
125
86
  constructor() {
126
87
  super(
@@ -390,6 +351,8 @@ class Editor extends BaseService {
390
351
  * @returns 添加后的节点
391
352
  */
392
353
  public async add(addNode: AddMNode | MNode[], parent?: MContainer | null): Promise<MNode | MNode[]> {
354
+ this.captureSelectionBeforeOp();
355
+
393
356
  const stage = this.get('stage');
394
357
 
395
358
  // 新增多个组件只存在于粘贴多个组件,粘贴的是一个完整的config,所以不再需要getPropsValue
@@ -435,7 +398,21 @@ class Editor extends BaseService {
435
398
  }
436
399
 
437
400
  if (!(isPage(newNodes[0]) || isPageFragment(newNodes[0]))) {
438
- this.pushHistoryState();
401
+ const pageForOp = this.getNodeInfo(newNodes[0].id, false).page;
402
+ this.pushOpHistory(
403
+ 'add',
404
+ {
405
+ nodes: newNodes.map((n) => cloneDeep(toRaw(n))),
406
+ parentId: (this.getParentById(newNodes[0].id, false) ?? this.get('root'))!.id,
407
+ indexMap: Object.fromEntries(
408
+ newNodes.map((n) => {
409
+ const p = this.getParentById(n.id, false) as MContainer;
410
+ return [n.id, p ? getNodeIndex(n.id, p) : -1];
411
+ }),
412
+ ),
413
+ },
414
+ { name: pageForOp?.name || '', id: pageForOp!.id },
415
+ );
439
416
  }
440
417
 
441
418
  this.emit('add', newNodes);
@@ -498,13 +475,33 @@ class Editor extends BaseService {
498
475
  * @param {Object} node
499
476
  */
500
477
  public async remove(nodeOrNodeList: MNode | MNode[]): Promise<void> {
478
+ this.captureSelectionBeforeOp();
479
+
501
480
  const nodes = Array.isArray(nodeOrNodeList) ? nodeOrNodeList : [nodeOrNodeList];
502
481
 
482
+ const removedItems: { node: MNode; parentId: Id; index: number }[] = [];
483
+ let pageForOp: { name: string; id: Id } | null = null;
484
+ if (!(isPage(nodes[0]) || isPageFragment(nodes[0]))) {
485
+ for (const n of nodes) {
486
+ const { parent, node: curNode, page } = this.getNodeInfo(n.id, false);
487
+ if (parent && curNode) {
488
+ if (!pageForOp && page) {
489
+ pageForOp = { name: page.name || '', id: page.id };
490
+ }
491
+ const idx = getNodeIndex(curNode.id, parent);
492
+ removedItems.push({
493
+ node: cloneDeep(toRaw(curNode)),
494
+ parentId: parent.id,
495
+ index: typeof idx === 'number' ? idx : -1,
496
+ });
497
+ }
498
+ }
499
+ }
500
+
503
501
  await Promise.all(nodes.map((node) => this.doRemove(node)));
504
502
 
505
- if (!(isPage(nodes[0]) || isPageFragment(nodes[0]))) {
506
- // 更新历史记录
507
- this.pushHistoryState();
503
+ if (removedItems.length > 0 && pageForOp) {
504
+ this.pushOpHistory('remove', { removedItems }, pageForOp);
508
505
  }
509
506
 
510
507
  this.emit('remove', nodes);
@@ -525,21 +522,9 @@ class Editor extends BaseService {
525
522
 
526
523
  const node = toRaw(info.node);
527
524
 
528
- let newConfig = await this.toggleFixedPosition(toRaw(config), node, root);
525
+ let newConfig = await toggleFixedPosition(toRaw(config), node, root, this.getLayout);
529
526
 
530
- newConfig = mergeWith(cloneDeep(node), newConfig, (objValue, srcValue, key, object: any, source: any) => {
531
- if (typeof srcValue === 'undefined' && Object.hasOwn(source, key)) {
532
- return '';
533
- }
534
-
535
- if (isObject(srcValue) && Array.isArray(objValue)) {
536
- // 原来的配置是数组,新的配置是对象,则直接使用新的值
537
- return srcValue;
538
- }
539
- if (Array.isArray(srcValue)) {
540
- return srcValue;
541
- }
542
- });
527
+ newConfig = mergeWith(cloneDeep(node), newConfig, editorNodeMergeCustomizer);
543
528
 
544
529
  if (!newConfig.type) throw new Error('配置缺少type值');
545
530
 
@@ -597,12 +582,28 @@ class Editor extends BaseService {
597
582
  config: MNode | MNode[],
598
583
  data: { changeRecords?: ChangeRecord[] } = {},
599
584
  ): Promise<MNode | MNode[]> {
585
+ this.captureSelectionBeforeOp();
586
+
600
587
  const nodes = Array.isArray(config) ? config : [config];
601
588
 
602
589
  const updateData = await Promise.all(nodes.map((node) => this.doUpdate(node, data)));
603
590
 
604
591
  if (updateData[0].oldNode?.type !== NodeType.ROOT) {
605
- this.pushHistoryState();
592
+ const curNodes = this.get('nodes');
593
+ if (!this.isHistoryStateChange && curNodes.length) {
594
+ const pageForOp = this.getNodeInfo(nodes[0].id, false).page;
595
+ this.pushOpHistory(
596
+ 'update',
597
+ {
598
+ updatedItems: updateData.map((d) => ({
599
+ oldNode: cloneDeep(d.oldNode),
600
+ newNode: cloneDeep(toRaw(d.newNode)),
601
+ })),
602
+ },
603
+ { name: pageForOp?.name || '', id: pageForOp!.id },
604
+ );
605
+ }
606
+ this.isHistoryStateChange = false;
606
607
  }
607
608
 
608
609
  this.emit('update', updateData);
@@ -616,6 +617,8 @@ class Editor extends BaseService {
616
617
  * @returns void
617
618
  */
618
619
  public async sort(id1: Id, id2: Id): Promise<void> {
620
+ this.captureSelectionBeforeOp();
621
+
619
622
  const root = this.get('root');
620
623
  if (!root) throw new Error('root为空');
621
624
 
@@ -640,9 +643,6 @@ class Editor extends BaseService {
640
643
  parentId: parent.id,
641
644
  root: cloneDeep(root),
642
645
  });
643
-
644
- this.addModifiedNodeId(parent.id);
645
- this.pushHistoryState();
646
646
  }
647
647
 
648
648
  /**
@@ -664,31 +664,8 @@ class Editor extends BaseService {
664
664
  public copyWithRelated(config: MNode | MNode[], collectorOptions?: TargetOptions): void {
665
665
  const copyNodes: MNode[] = Array.isArray(config) ? config : [config];
666
666
 
667
- // 初始化复制组件相关的依赖收集器
668
667
  if (collectorOptions && typeof collectorOptions.isTarget === 'function') {
669
- const customTarget = new Target({
670
- ...collectorOptions,
671
- });
672
-
673
- const coperWatcher = new Watcher();
674
-
675
- coperWatcher.addTarget(customTarget);
676
-
677
- coperWatcher.collect(copyNodes, {}, true, collectorOptions.type);
678
- Object.keys(customTarget.deps).forEach((nodeId: Id) => {
679
- const node = this.getNodeById(nodeId);
680
- if (!node) return;
681
- customTarget!.deps[nodeId].keys.forEach((key) => {
682
- const relateNodeId = get(node, key);
683
- const isExist = copyNodes.find((node) => node.id === relateNodeId);
684
- if (!isExist) {
685
- const relateNode = this.getNodeById(relateNodeId);
686
- if (relateNode) {
687
- copyNodes.push(relateNode);
688
- }
689
- }
690
- });
691
- });
668
+ collectRelatedNodes(copyNodes, collectorOptions, (id) => this.getNodeById(id));
692
669
  }
693
670
 
694
671
  storageService.setItem(COPY_STORAGE_KEY, copyNodes, {
@@ -733,32 +710,16 @@ class Editor extends BaseService {
733
710
 
734
711
  public async doAlignCenter(config: MNode): Promise<MNode> {
735
712
  const parent = this.getParentById(config.id);
736
-
737
713
  if (!parent) throw new Error('找不到父节点');
738
714
 
739
715
  const node = cloneDeep(toRaw(config));
740
716
  const layout = await this.getLayout(parent, node);
741
- if (layout === Layout.RELATIVE) {
742
- return config;
743
- }
744
-
745
- if (!node.style) return config;
717
+ const doc = this.get('stage')?.renderer?.contentWindow?.document;
718
+ const newStyle = calcAlignCenterStyle(node, parent, layout, doc);
746
719
 
747
- const stage = this.get('stage');
748
- const doc = stage?.renderer?.contentWindow?.document;
749
-
750
- if (doc) {
751
- const el = getElById()(doc, node.id);
752
- const parentEl = layout === Layout.FIXED ? doc.body : el?.offsetParent;
753
- if (parentEl && el) {
754
- node.style.left = calcValueByFontsize(doc, (parentEl.clientWidth - el.clientWidth) / 2);
755
- node.style.right = '';
756
- }
757
- } else if (parent.style && isNumber(parent.style?.width) && isNumber(node.style?.width)) {
758
- node.style.left = (parent.style.width - node.style.width) / 2;
759
- node.style.right = '';
760
- }
720
+ if (!newStyle) return config;
761
721
 
722
+ node.style = newStyle;
762
723
  return node;
763
724
  }
764
725
 
@@ -789,6 +750,8 @@ class Editor extends BaseService {
789
750
  * @param offset 偏移量
790
751
  */
791
752
  public async moveLayer(offset: number | LayerOffset): Promise<void> {
753
+ this.captureSelectionBeforeOp();
754
+
792
755
  const root = this.get('root');
793
756
  if (!root) throw new Error('root为空');
794
757
 
@@ -801,22 +764,16 @@ class Editor extends BaseService {
801
764
  const brothers: MNode[] = parent.items || [];
802
765
  const index = brothers.findIndex((item) => `${item.id}` === `${node?.id}`);
803
766
 
804
- // 流式布局与绝对定位布局操作的相反的
805
767
  const layout = await this.getLayout(parent, node);
806
768
  const isRelative = layout === Layout.RELATIVE;
807
-
808
- let offsetIndex: number;
809
- if (offset === LayerOffset.TOP) {
810
- offsetIndex = isRelative ? 0 : brothers.length;
811
- } else if (offset === LayerOffset.BOTTOM) {
812
- offsetIndex = isRelative ? brothers.length : 0;
813
- } else {
814
- offsetIndex = index + (isRelative ? -offset : offset);
815
- }
769
+ const offsetIndex = calcLayerTargetIndex(index, offset, brothers.length, isRelative);
816
770
 
817
771
  if ((offsetIndex > 0 && offsetIndex > brothers.length) || offsetIndex < 0) {
818
772
  return;
819
773
  }
774
+
775
+ const oldParent = cloneDeep(toRaw(parent));
776
+
820
777
  brothers.splice(index, 1);
821
778
  brothers.splice(offsetIndex, 0, node);
822
779
 
@@ -829,7 +786,14 @@ class Editor extends BaseService {
829
786
  });
830
787
 
831
788
  this.addModifiedNodeId(parent.id);
832
- this.pushHistoryState();
789
+ const pageForOp = this.getNodeInfo(node.id, false).page;
790
+ this.pushOpHistory(
791
+ 'update',
792
+ {
793
+ updatedItems: [{ oldNode: oldParent, newNode: cloneDeep(toRaw(parent)) }],
794
+ },
795
+ { name: pageForOp?.name || '', id: pageForOp!.id },
796
+ );
833
797
 
834
798
  this.emit('move-layer', offset);
835
799
  }
@@ -840,12 +804,17 @@ class Editor extends BaseService {
840
804
  * @param targetId 容器ID
841
805
  */
842
806
  public async moveToContainer(config: MNode, targetId: Id): Promise<MNode | undefined> {
807
+ this.captureSelectionBeforeOp();
808
+
843
809
  const root = this.get('root');
844
- const { node, parent } = this.getNodeInfo(config.id, false);
810
+ const { node, parent, page: pageForOp } = this.getNodeInfo(config.id, false);
845
811
  const target = this.getNodeById(targetId, false) as MContainer;
846
812
 
847
813
  const stage = this.get('stage');
848
814
  if (root && node && parent && stage) {
815
+ const oldSourceParent = cloneDeep(toRaw(parent));
816
+ const oldTarget = cloneDeep(toRaw(target));
817
+
849
818
  const index = getNodeIndex(node.id, parent);
850
819
  parent.items?.splice(index, 1);
851
820
 
@@ -876,62 +845,60 @@ class Editor extends BaseService {
876
845
 
877
846
  this.addModifiedNodeId(target.id);
878
847
  this.addModifiedNodeId(parent.id);
879
- this.pushHistoryState();
848
+ this.pushOpHistory(
849
+ 'update',
850
+ {
851
+ updatedItems: [
852
+ { oldNode: oldSourceParent, newNode: cloneDeep(toRaw(parent)) },
853
+ { oldNode: oldTarget, newNode: cloneDeep(toRaw(target)) },
854
+ ],
855
+ },
856
+ { name: pageForOp?.name || '', id: pageForOp!.id },
857
+ );
880
858
 
881
859
  return newConfig;
882
860
  }
883
861
  }
884
862
 
885
863
  public async dragTo(config: MNode | MNode[], targetParent: MContainer, targetIndex: number) {
864
+ this.captureSelectionBeforeOp();
865
+
886
866
  if (!targetParent || !Array.isArray(targetParent.items)) return;
887
867
 
888
868
  const configs = Array.isArray(config) ? config : [config];
889
869
 
890
- const sourceIndicesInTargetParent: number[] = [];
891
- const sourceOutTargetParent: MNode[] = [];
892
-
893
- const newLayout = await this.getLayout(targetParent);
894
-
895
- // eslint-disable-next-line no-restricted-syntax
896
- forConfigs: for (const config of configs) {
897
- const { parent, node: curNode } = this.getNodeInfo(config.id, false);
898
- if (!parent || !curNode) {
899
- continue;
900
- }
901
-
902
- const path = getNodePath(curNode.id, parent.items);
903
-
904
- for (const node of path) {
905
- if (targetParent.id === node.id) {
906
- continue forConfigs;
907
- }
870
+ const beforeSnapshots = new Map<string, MNode>();
871
+ for (const cfg of configs) {
872
+ const { parent } = this.getNodeInfo(cfg.id, false);
873
+ if (parent && !beforeSnapshots.has(`${parent.id}`)) {
874
+ beforeSnapshots.set(`${parent.id}`, cloneDeep(toRaw(parent)));
908
875
  }
876
+ }
877
+ if (!beforeSnapshots.has(`${targetParent.id}`)) {
878
+ beforeSnapshots.set(`${targetParent.id}`, cloneDeep(toRaw(targetParent)));
879
+ }
909
880
 
910
- const index = getNodeIndex(curNode.id, parent);
911
-
912
- if (parent.id === targetParent.id) {
913
- if (typeof index !== 'number' || index === -1) {
914
- return;
915
- }
916
- sourceIndicesInTargetParent.push(index);
917
- } else {
918
- const layout = await this.getLayout(parent);
919
-
920
- if (newLayout !== layout) {
921
- setLayout(config, newLayout);
922
- }
881
+ const newLayout = await this.getLayout(targetParent);
882
+ const { sameParentIndices, crossParentConfigs, aborted } = classifyDragSources(configs, targetParent, (id, raw) =>
883
+ this.getNodeInfo(id, raw),
884
+ );
885
+ if (aborted) return;
923
886
 
924
- parent.items?.splice(index, 1);
925
- sourceOutTargetParent.push(config);
926
- this.addModifiedNodeId(parent.id);
887
+ for (const { config: crossConfig, parent } of crossParentConfigs) {
888
+ const layout = await this.getLayout(parent);
889
+ if (newLayout !== layout) {
890
+ setLayout(crossConfig, newLayout);
927
891
  }
892
+ const index = getNodeIndex(crossConfig.id, parent);
893
+ parent.items?.splice(index, 1);
894
+ this.addModifiedNodeId(parent.id);
928
895
  }
929
896
 
930
- moveItemsInContainer(sourceIndicesInTargetParent, targetParent, targetIndex);
897
+ moveItemsInContainer(sameParentIndices, targetParent, targetIndex);
931
898
 
932
- sourceOutTargetParent.forEach((config, index) => {
933
- targetParent.items?.splice(targetIndex + index, 0, config);
934
- this.addModifiedNodeId(config.id);
899
+ crossParentConfigs.forEach(({ config: crossConfig }, index) => {
900
+ targetParent.items?.splice(targetIndex + index, 0, crossConfig);
901
+ this.addModifiedNodeId(crossConfig.id);
935
902
  });
936
903
 
937
904
  const page = this.get('page');
@@ -946,28 +913,40 @@ class Editor extends BaseService {
946
913
  });
947
914
  }
948
915
 
949
- this.pushHistoryState();
916
+ const updatedItems: { oldNode: MNode; newNode: MNode }[] = [];
917
+ for (const oldNode of beforeSnapshots.values()) {
918
+ const newNode = this.getNodeById(oldNode.id, false);
919
+ if (newNode) {
920
+ updatedItems.push({ oldNode, newNode: cloneDeep(toRaw(newNode)) });
921
+ }
922
+ }
923
+ const pageForOp = this.getNodeInfo(configs[0].id, false).page;
924
+ this.pushOpHistory('update', { updatedItems }, { name: pageForOp?.name || '', id: pageForOp!.id });
950
925
 
951
926
  this.emit('drag-to', { targetIndex, configs, targetParent });
952
927
  }
953
928
 
954
929
  /**
955
930
  * 撤销当前操作
956
- * @returns 上一次数据
931
+ * @returns 被撤销的操作
957
932
  */
958
933
  public async undo(): Promise<StepValue | null> {
959
934
  const value = historyService.undo();
960
- await this.changeHistoryState(value);
935
+ if (value) {
936
+ await this.applyHistoryOp(value, true);
937
+ }
961
938
  return value;
962
939
  }
963
940
 
964
941
  /**
965
942
  * 恢复到下一步
966
- * @returns 下一步数据
943
+ * @returns 被恢复的操作
967
944
  */
968
945
  public async redo(): Promise<StepValue | null> {
969
946
  const value = historyService.redo();
970
- await this.changeHistoryState(value);
947
+ if (value) {
948
+ await this.applyHistoryOp(value, false);
949
+ }
971
950
  return value;
972
951
  }
973
952
 
@@ -975,47 +954,10 @@ class Editor extends BaseService {
975
954
  const node = toRaw(this.get('node'));
976
955
  if (!node || isPage(node)) return;
977
956
 
978
- const { style, id, type } = node;
979
- if (!style || !['absolute', 'fixed'].includes(style.position)) return;
980
-
981
- const update = (style: { [key: string]: any }) =>
982
- this.update({
983
- id,
984
- type,
985
- style,
986
- });
957
+ const newStyle = calcMoveStyle(node.style || {}, left, top);
958
+ if (!newStyle) return;
987
959
 
988
- if (top) {
989
- if (isNumber(style.top)) {
990
- update({
991
- ...style,
992
- top: Number(style.top) + Number(top),
993
- bottom: '',
994
- });
995
- } else if (isNumber(style.bottom)) {
996
- update({
997
- ...style,
998
- bottom: Number(style.bottom) - Number(top),
999
- top: '',
1000
- });
1001
- }
1002
- }
1003
-
1004
- if (left) {
1005
- if (isNumber(style.left)) {
1006
- update({
1007
- ...style,
1008
- left: Number(style.left) + Number(left),
1009
- right: '',
1010
- });
1011
- } else if (isNumber(style.right)) {
1012
- update({
1013
- ...style,
1014
- right: Number(style.right) - Number(left),
1015
- left: '',
1016
- });
1017
- }
1018
- }
960
+ await this.update({ id: node.id, type: node.type, style: newStyle });
1019
961
  }
1020
962
 
1021
963
  public resetState() {
@@ -1068,70 +1010,89 @@ class Editor extends BaseService {
1068
1010
  }
1069
1011
  }
1070
1012
 
1071
- private pushHistoryState() {
1072
- const curNode = cloneDeep(toRaw(this.get('node')));
1073
- const page = this.get('page');
1074
- if (!this.isHistoryStateChange && curNode && page) {
1075
- historyService.push({
1076
- data: cloneDeep(toRaw(page)),
1077
- modifiedNodeIds: this.get('modifiedNodeIds'),
1078
- nodeId: curNode.id,
1079
- });
1013
+ private captureSelectionBeforeOp() {
1014
+ if (this.isHistoryStateChange || this.selectionBeforeOp) return;
1015
+ this.selectionBeforeOp = this.get('nodes').map((n) => n.id);
1016
+ }
1017
+
1018
+ private pushOpHistory(opType: HistoryOpType, extra: Partial<StepValue>, pageData: { name: string; id: Id }) {
1019
+ if (this.isHistoryStateChange) {
1020
+ this.selectionBeforeOp = null;
1021
+ return;
1080
1022
  }
1023
+
1024
+ const step: StepValue = {
1025
+ data: pageData,
1026
+ opType,
1027
+ selectedBefore: this.selectionBeforeOp ?? [],
1028
+ selectedAfter: this.get('nodes').map((n) => n.id),
1029
+ modifiedNodeIds: new Map(this.get('modifiedNodeIds')),
1030
+ ...extra,
1031
+ };
1032
+ historyService.push(step);
1033
+ this.selectionBeforeOp = null;
1081
1034
  this.isHistoryStateChange = false;
1082
1035
  }
1083
1036
 
1084
- private async changeHistoryState(value: StepValue | null) {
1085
- if (!value) return;
1086
-
1037
+ /**
1038
+ * 应用历史操作(撤销 / 重做)
1039
+ * @param step 操作记录
1040
+ * @param reverse true = 撤销,false = 重做
1041
+ */
1042
+ private async applyHistoryOp(step: StepValue, reverse: boolean) {
1087
1043
  this.isHistoryStateChange = true;
1088
- await this.update(value.data);
1089
- this.set('modifiedNodeIds', value.modifiedNodeIds);
1090
- setTimeout(() => {
1091
- if (!value.nodeId) return;
1092
- this.select(value.nodeId).then(() => {
1093
- this.get('stage')?.select(value.nodeId);
1094
- });
1095
- }, 0);
1096
- this.emit('history-change', value.data);
1097
- }
1098
1044
 
1099
- private async toggleFixedPosition(dist: MNode, src: MNode, root: MApp) {
1100
- const newConfig = cloneDeep(dist);
1045
+ const root = this.get('root');
1046
+ const stage = this.get('stage');
1047
+ if (!root) return;
1101
1048
 
1102
- if (!isPop(src) && newConfig.style?.position) {
1103
- if (isFixed(newConfig.style) && !isFixed(src.style || {})) {
1104
- newConfig.style = change2Fixed(newConfig, root);
1105
- } else if (!isFixed(newConfig.style) && isFixed(src.style || {})) {
1106
- newConfig.style = await Fixed2Other(newConfig, root, this.getLayout);
1107
- }
1049
+ const ctx: HistoryOpContext = {
1050
+ root,
1051
+ stage,
1052
+ getNodeById: (id, raw) => this.getNodeById(id, raw),
1053
+ getNodeInfo: (id, raw) => this.getNodeInfo(id, raw),
1054
+ setRoot: (r) => this.set('root', r),
1055
+ setPage: (p) => this.set('page', p),
1056
+ getPage: () => this.get('page'),
1057
+ };
1058
+
1059
+ switch (step.opType) {
1060
+ case 'add':
1061
+ await applyHistoryAddOp(step, reverse, ctx);
1062
+ break;
1063
+ case 'remove':
1064
+ await applyHistoryRemoveOp(step, reverse, ctx);
1065
+ break;
1066
+ case 'update':
1067
+ await applyHistoryUpdateOp(step, reverse, ctx);
1068
+ break;
1108
1069
  }
1109
1070
 
1110
- return newConfig;
1111
- }
1071
+ this.set('modifiedNodeIds', step.modifiedNodeIds);
1112
1072
 
1113
- private selectedConfigExceptionHandler(config: MNode | Id): EditorNodeInfo {
1114
- let id: Id;
1115
- if (typeof config === 'string' || typeof config === 'number') {
1116
- id = config;
1117
- } else {
1118
- id = config.id;
1119
- }
1120
- if (!id) {
1121
- throw new Error('没有ID,无法选中');
1073
+ const page = toRaw(this.get('page'));
1074
+ if (page) {
1075
+ const selectIds = reverse ? step.selectedBefore : step.selectedAfter;
1076
+ setTimeout(() => {
1077
+ if (!selectIds.length) return;
1078
+
1079
+ if (selectIds.length > 1) {
1080
+ this.multiSelect(selectIds);
1081
+ stage?.multiSelect(selectIds);
1082
+ } else {
1083
+ this.select(selectIds[0])
1084
+ .then(() => stage?.select(selectIds[0]))
1085
+ .catch(() => {});
1086
+ }
1087
+ }, 0);
1088
+ this.emit('history-change', page as MPage | MPageFragment);
1122
1089
  }
1123
1090
 
1124
- const { node, parent, page } = this.getNodeInfo(id);
1125
- if (!node) throw new Error('获取不到组件信息');
1091
+ this.isHistoryStateChange = false;
1092
+ }
1126
1093
 
1127
- if (node.id === this.state.root?.id) {
1128
- throw new Error('不能选根节点');
1129
- }
1130
- return {
1131
- node,
1132
- parent,
1133
- page,
1134
- };
1094
+ private selectedConfigExceptionHandler(config: MNode | Id): EditorNodeInfo {
1095
+ return resolveSelectedNode(config, (id) => this.getNodeInfo(id), this.state.root?.id);
1135
1096
  }
1136
1097
  }
1137
1098