@tmagic/editor 1.7.8 → 1.7.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) 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/StyleSetter/Index.vue_vue_type_script_setup_true_lang.js +5 -0
  8. package/dist/es/fields/StyleSetter/pro/Transform.js +5 -0
  9. package/dist/es/fields/StyleSetter/pro/Transform.vue_vue_type_script_setup_true_lang.js +54 -0
  10. package/dist/es/hooks/use-code-block-edit.js +3 -2
  11. package/dist/es/index.js +3 -3
  12. package/dist/es/initService.js +4 -4
  13. package/dist/es/layouts/workspace/viewer/Stage.vue_vue_type_script_setup_true_lang.js +64 -1
  14. package/dist/es/layouts/workspace/viewer/StageOverlay.vue_vue_type_script_setup_true_lang.js +1 -5
  15. package/dist/es/services/editor.js +191 -174
  16. package/dist/es/services/history.js +1 -9
  17. package/dist/es/type.js +28 -1
  18. package/dist/es/utils/dep/worker.js +1 -1
  19. package/dist/es/utils/editor-history.js +100 -0
  20. package/dist/es/utils/editor.js +178 -4
  21. package/dist/es/utils/props.js +4 -1
  22. package/dist/es/utils/undo-redo.js +7 -4
  23. package/dist/tmagic-editor.umd.cjs +744 -234
  24. package/package.json +8 -8
  25. package/src/Editor.vue +1 -0
  26. package/src/components/CodeBlockEditor.vue +1 -1
  27. package/src/components/CodeParams.vue +23 -7
  28. package/src/editorProps.ts +2 -0
  29. package/src/fields/DataSourceFieldSelect/FieldSelect.vue +39 -6
  30. package/src/fields/DataSourceFieldSelect/Index.vue +1 -0
  31. package/src/fields/DataSourceMethodSelect.vue +54 -13
  32. package/src/fields/DataSourceMethods.vue +9 -5
  33. package/src/fields/StyleSetter/Index.vue +5 -1
  34. package/src/fields/StyleSetter/pro/Transform.vue +54 -0
  35. package/src/fields/StyleSetter/pro/index.ts +1 -0
  36. package/src/hooks/use-code-block-edit.ts +8 -4
  37. package/src/initService.ts +6 -6
  38. package/src/layouts/workspace/viewer/Stage.vue +89 -1
  39. package/src/layouts/workspace/viewer/StageOverlay.vue +1 -6
  40. package/src/services/editor.ts +231 -270
  41. package/src/services/history.ts +1 -9
  42. package/src/type.ts +65 -5
  43. package/src/utils/editor-history.ts +138 -0
  44. package/src/utils/editor.ts +249 -4
  45. package/src/utils/props.ts +4 -0
  46. package/src/utils/undo-redo.ts +7 -6
  47. package/types/index.d.ts +179 -53
@@ -56,15 +56,7 @@ class History extends BaseService {
56
56
  this.state.pageId = page.id;
57
57
 
58
58
  if (!this.state.pageSteps[this.state.pageId]) {
59
- const undoRedo = new UndoRedo<StepValue>();
60
-
61
- undoRedo.pushElement({
62
- data: page,
63
- modifiedNodeIds: new Map(),
64
- nodeId: page.id,
65
- });
66
-
67
- this.state.pageSteps[this.state.pageId] = undoRedo;
59
+ this.state.pageSteps[this.state.pageId] = new UndoRedo<StepValue>();
68
60
  }
69
61
 
70
62
  this.setCanUndoRedo();
package/src/type.ts CHANGED
@@ -20,10 +20,10 @@ import type { Component } from 'vue';
20
20
  import type EventEmitter from 'events';
21
21
  import type * as Monaco from 'monaco-editor';
22
22
  import type { default as Sortable, Options, SortableEvent } from 'sortablejs';
23
- import type { PascalCasedProperties } from 'type-fest';
23
+ import type { PascalCasedProperties, Writable } from 'type-fest';
24
24
 
25
25
  import type { CodeBlockContent, CodeBlockDSL, Id, MApp, MContainer, MNode, MPage, MPageFragment } from '@tmagic/core';
26
- import type { FormConfig, TableColumnConfig } from '@tmagic/form';
26
+ import type { ChangeRecord, FormConfig, TableColumnConfig, TypeFunction } from '@tmagic/form';
27
27
  import type StageCore from '@tmagic/stage';
28
28
  import type {
29
29
  ContainerHighlightType,
@@ -164,6 +164,8 @@ export interface StageOptions {
164
164
  disabledMultiSelect?: boolean;
165
165
  disabledRule?: boolean;
166
166
  zoom?: number;
167
+ /** 画布双击前的钩子函数,返回 false 则阻止默认的双击行为 */
168
+ beforeDblclick?: (event: MouseEvent) => Promise<boolean | void> | boolean | void;
167
169
  }
168
170
 
169
171
  export interface StoreState {
@@ -541,14 +543,31 @@ export interface CodeParamStatement {
541
543
  /** 参数名称 */
542
544
  name: string;
543
545
  /** 参数类型 */
544
- type?: string;
546
+ type?: string | TypeFunction<string>;
545
547
  [key: string]: any;
546
548
  }
547
549
 
550
+ export type HistoryOpType = 'add' | 'remove' | 'update';
551
+
548
552
  export interface StepValue {
549
- data: MPage | MPageFragment;
553
+ /** 页面信息 */
554
+ data: { name: string; id: Id };
555
+ opType: HistoryOpType;
556
+ /** 操作前选中的节点 ID,用于撤销后恢复选择状态 */
557
+ selectedBefore: Id[];
558
+ /** 操作后选中的节点 ID,用于重做后恢复选择状态 */
559
+ selectedAfter: Id[];
550
560
  modifiedNodeIds: Map<Id, Id>;
551
- nodeId: Id;
561
+ /** opType 'add': 新增的节点 */
562
+ nodes?: MNode[];
563
+ /** opType 'add': 父节点 ID */
564
+ parentId?: Id;
565
+ /** opType 'add': 每个新增节点在父节点 items 中的索引 */
566
+ indexMap?: Record<string, number>;
567
+ /** opType 'remove': 被删除的节点及其位置信息 */
568
+ removedItems?: { node: MNode; parentId: Id; index: number }[];
569
+ /** opType 'update': 变更前后的节点快照 */
570
+ updatedItems?: { oldNode: MNode; newNode: MNode }[];
552
571
  }
553
572
 
554
573
  export interface HistoryState {
@@ -712,3 +731,44 @@ export type CustomContentMenuFunction = (
712
731
  menus: (MenuButton | MenuComponent)[],
713
732
  type: 'layer' | 'data-source' | 'viewer' | 'code-block',
714
733
  ) => (MenuButton | MenuComponent)[];
734
+
735
+ export interface EditorEvents {
736
+ 'root-change': [value: StoreState['root'], preValue?: StoreState['root']];
737
+ select: [node: MNode | null];
738
+ add: [nodes: MNode[]];
739
+ remove: [nodes: MNode[]];
740
+ update: [nodes: { newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] }[]];
741
+ 'move-layer': [offset: number | LayerOffset];
742
+ 'drag-to': [data: { targetIndex: number; configs: MNode | MNode[]; targetParent: MContainer }];
743
+ 'history-change': [data: MPage | MPageFragment];
744
+ }
745
+
746
+ export const canUsePluginMethods = {
747
+ async: [
748
+ 'getLayout',
749
+ 'highlight',
750
+ 'select',
751
+ 'multiSelect',
752
+ 'doAdd',
753
+ 'add',
754
+ 'doRemove',
755
+ 'remove',
756
+ 'doUpdate',
757
+ 'update',
758
+ 'sort',
759
+ 'copy',
760
+ 'paste',
761
+ 'doPaste',
762
+ 'doAlignCenter',
763
+ 'alignCenter',
764
+ 'moveLayer',
765
+ 'moveToContainer',
766
+ 'dragTo',
767
+ 'undo',
768
+ 'redo',
769
+ 'move',
770
+ ] as const,
771
+ sync: [],
772
+ };
773
+
774
+ export type AsyncMethodName = Writable<(typeof canUsePluginMethods)['async']>;
@@ -0,0 +1,138 @@
1
+ /*
2
+ * Tencent is pleased to support the open source community by making TMagicEditor available.
3
+ *
4
+ * Copyright (C) 2025 Tencent. All rights reserved.
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */
18
+
19
+ import { toRaw } from 'vue';
20
+ import { cloneDeep } from 'lodash-es';
21
+
22
+ import type { Id, MApp, MContainer, MNode, MPage, MPageFragment } from '@tmagic/core';
23
+ import { NodeType } from '@tmagic/core';
24
+ import type StageCore from '@tmagic/stage';
25
+ import { isPage, isPageFragment } from '@tmagic/utils';
26
+
27
+ import type { EditorNodeInfo, StepValue } from '@editor/type';
28
+ import { getNodeIndex } from '@editor/utils/editor';
29
+
30
+ export interface HistoryOpContext {
31
+ root: MApp;
32
+ stage: StageCore | null;
33
+ getNodeById(id: Id, raw?: boolean): MNode | null;
34
+ getNodeInfo(id: Id, raw?: boolean): EditorNodeInfo;
35
+ setRoot(root: MApp): void;
36
+ setPage(page: MPage | MPageFragment): void;
37
+ getPage(): MPage | MPageFragment | null;
38
+ }
39
+
40
+ /**
41
+ * 应用 add 类型的历史操作
42
+ * reverse=true(撤销):从父节点中移除已添加的节点
43
+ * reverse=false(重做):重新添加节点到父节点中
44
+ */
45
+ export async function applyHistoryAddOp(step: StepValue, reverse: boolean, ctx: HistoryOpContext): Promise<void> {
46
+ const { root, stage } = ctx;
47
+
48
+ if (reverse) {
49
+ for (const node of step.nodes ?? []) {
50
+ const parent = ctx.getNodeById(step.parentId!, false) as MContainer;
51
+ if (!parent?.items) continue;
52
+ const idx = getNodeIndex(node.id, parent);
53
+ if (typeof idx === 'number' && idx !== -1) {
54
+ parent.items.splice(idx, 1);
55
+ }
56
+ await stage?.remove({ id: node.id, parentId: parent.id, root: cloneDeep(root) });
57
+ }
58
+ } else {
59
+ const parent = ctx.getNodeById(step.parentId!, false) as MContainer;
60
+ if (parent?.items) {
61
+ for (const node of step.nodes ?? []) {
62
+ const idx = step.indexMap?.[node.id] ?? parent.items.length;
63
+ parent.items.splice(idx, 0, cloneDeep(node));
64
+ await stage?.add({
65
+ config: cloneDeep(node),
66
+ parent: cloneDeep(parent),
67
+ parentId: parent.id,
68
+ root: cloneDeep(root),
69
+ });
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ /**
76
+ * 应用 remove 类型的历史操作
77
+ * reverse=true(撤销):将已删除的节点按原位置重新插入
78
+ * reverse=false(重做):再次删除节点
79
+ */
80
+ export async function applyHistoryRemoveOp(step: StepValue, reverse: boolean, ctx: HistoryOpContext): Promise<void> {
81
+ const { root, stage } = ctx;
82
+
83
+ if (reverse) {
84
+ const sorted = [...(step.removedItems ?? [])].sort((a, b) => a.index - b.index);
85
+ for (const { node, parentId, index } of sorted) {
86
+ const parent = ctx.getNodeById(parentId, false) as MContainer;
87
+ if (!parent?.items) continue;
88
+ parent.items.splice(index, 0, cloneDeep(node));
89
+ await stage?.add({ config: cloneDeep(node), parent: cloneDeep(parent), parentId, root: cloneDeep(root) });
90
+ }
91
+ } else {
92
+ for (const { node, parentId } of step.removedItems ?? []) {
93
+ const parent = ctx.getNodeById(parentId, false) as MContainer;
94
+ if (!parent?.items) continue;
95
+ const idx = getNodeIndex(node.id, parent);
96
+ if (typeof idx === 'number' && idx !== -1) {
97
+ parent.items.splice(idx, 1);
98
+ }
99
+ await stage?.remove({ id: node.id, parentId, root: cloneDeep(root) });
100
+ }
101
+ }
102
+ }
103
+
104
+ /**
105
+ * 应用 update 类型的历史操作
106
+ * reverse=true(撤销):将节点恢复为 oldNode
107
+ * reverse=false(重做):将节点更新为 newNode
108
+ */
109
+ export async function applyHistoryUpdateOp(step: StepValue, reverse: boolean, ctx: HistoryOpContext): Promise<void> {
110
+ const { root, stage } = ctx;
111
+ const items = step.updatedItems ?? [];
112
+
113
+ for (const { oldNode, newNode } of items) {
114
+ const config = reverse ? oldNode : newNode;
115
+ if (config.type === NodeType.ROOT) {
116
+ ctx.setRoot(cloneDeep(config) as MApp);
117
+ continue;
118
+ }
119
+ const info = ctx.getNodeInfo(config.id, false);
120
+ if (!info.parent) continue;
121
+ const idx = getNodeIndex(config.id, info.parent);
122
+ if (typeof idx !== 'number' || idx === -1) continue;
123
+ info.parent.items![idx] = cloneDeep(config);
124
+
125
+ if (isPage(config) || isPageFragment(config)) {
126
+ ctx.setPage(config as MPage | MPageFragment);
127
+ }
128
+ }
129
+
130
+ const curPage = ctx.getPage();
131
+ if (stage && curPage) {
132
+ await stage.update({
133
+ config: cloneDeep(toRaw(curPage)),
134
+ parentId: root.id,
135
+ root: cloneDeep(toRaw(root)),
136
+ });
137
+ }
138
+ }
@@ -17,12 +17,13 @@
17
17
  */
18
18
 
19
19
  import { detailedDiff } from 'deep-object-diff';
20
- import { isObject } from 'lodash-es';
20
+ import { cloneDeep, get, isObject } from 'lodash-es';
21
21
  import serialize from 'serialize-javascript';
22
22
 
23
- import type { Id, MApp, MContainer, MNode, MPage, MPageFragment } from '@tmagic/core';
24
- import { NODE_CONDS_KEY, NodeType } from '@tmagic/core';
23
+ import type { Id, MApp, MContainer, MNode, MPage, MPageFragment, TargetOptions } from '@tmagic/core';
24
+ import { NODE_CONDS_KEY, NodeType, Target, Watcher } from '@tmagic/core';
25
25
  import type StageCore from '@tmagic/stage';
26
+ import { isFixed } from '@tmagic/stage';
26
27
  import {
27
28
  calcValueByFontsize,
28
29
  getElById,
@@ -34,7 +35,8 @@ import {
34
35
  isValueIncludeDataSource,
35
36
  } from '@tmagic/utils';
36
37
 
37
- import { Layout } from '@editor/type';
38
+ import type { EditorNodeInfo } from '@editor/type';
39
+ import { LayerOffset, Layout } from '@editor/type';
38
40
 
39
41
  export const COPY_STORAGE_KEY = '$MagicEditorCopyData';
40
42
  export const COPY_CODE_STORAGE_KEY = '$MagicEditorCopyCode';
@@ -436,3 +438,246 @@ export const buildChangeRecords = (value: any, basePath: string) => {
436
438
 
437
439
  return changeRecords;
438
440
  };
441
+
442
+ /**
443
+ * 根据节点配置或ID解析出选中节点信息,并进行合法性校验
444
+ * @param config 节点配置或节点ID
445
+ * @param getNodeInfoFn 获取节点信息的回调函数
446
+ * @param rootId 根节点ID,用于排除根节点被选中
447
+ * @returns 选中节点的完整信息(node、parent、page)
448
+ */
449
+ export const resolveSelectedNode = (
450
+ config: MNode | Id,
451
+ getNodeInfoFn: (id: Id) => EditorNodeInfo,
452
+ rootId?: Id,
453
+ ): EditorNodeInfo => {
454
+ const id: Id = typeof config === 'string' || typeof config === 'number' ? config : config.id;
455
+
456
+ if (!id) {
457
+ throw new Error('没有ID,无法选中');
458
+ }
459
+
460
+ const { node, parent, page } = getNodeInfoFn(id);
461
+ if (!node) throw new Error('获取不到组件信息');
462
+ if (node.id === rootId) throw new Error('不能选根节点');
463
+
464
+ return { node, parent, page };
465
+ };
466
+
467
+ /**
468
+ * 处理节点在 fixed 定位与其他定位之间的切换
469
+ * 当节点从非 fixed 变为 fixed 时,根据节点路径累加偏移量;反之则还原偏移量
470
+ * @param dist 更新后的节点配置(目标状态)
471
+ * @param src 更新前的节点配置(原始状态)
472
+ * @param root 根节点配置,用于计算节点路径上的偏移量
473
+ * @param getLayoutFn 获取父节点布局方式的回调函数
474
+ * @returns 处理后的节点配置(深拷贝)
475
+ */
476
+ export const toggleFixedPosition = async (
477
+ dist: MNode,
478
+ src: MNode,
479
+ root: MApp,
480
+ getLayoutFn: (parent: MNode, node?: MNode | null) => Promise<Layout>,
481
+ ): Promise<MNode> => {
482
+ const newConfig = cloneDeep(dist);
483
+
484
+ if (!isPop(src) && newConfig.style?.position) {
485
+ if (isFixed(newConfig.style) && !isFixed(src.style || {})) {
486
+ newConfig.style = change2Fixed(newConfig, root);
487
+ } else if (!isFixed(newConfig.style) && isFixed(src.style || {})) {
488
+ newConfig.style = await Fixed2Other(newConfig, root, getLayoutFn);
489
+ }
490
+ }
491
+
492
+ return newConfig;
493
+ };
494
+
495
+ /**
496
+ * 根据键盘移动的偏移量计算节点的新样式
497
+ * 仅对 absolute 或 fixed 定位的节点生效;优先修改 top/left,若未设置则修改 bottom/right
498
+ * @param style 节点当前样式
499
+ * @param left 水平方向偏移量(正值向右,负值向左)
500
+ * @param top 垂直方向偏移量(正值向下,负值向上)
501
+ * @returns 计算后的新样式对象,若节点不支持移动则返回 null
502
+ */
503
+ export const calcMoveStyle = (style: Record<string, any>, left: number, top: number): Record<string, any> | null => {
504
+ if (!style || !['absolute', 'fixed'].includes(style.position)) return null;
505
+
506
+ const newStyle: Record<string, any> = { ...style };
507
+
508
+ if (top) {
509
+ if (isNumber(style.top)) {
510
+ newStyle.top = Number(style.top) + Number(top);
511
+ newStyle.bottom = '';
512
+ } else if (isNumber(style.bottom)) {
513
+ newStyle.bottom = Number(style.bottom) - Number(top);
514
+ newStyle.top = '';
515
+ }
516
+ }
517
+
518
+ if (left) {
519
+ if (isNumber(style.left)) {
520
+ newStyle.left = Number(style.left) + Number(left);
521
+ newStyle.right = '';
522
+ } else if (isNumber(style.right)) {
523
+ newStyle.right = Number(style.right) - Number(left);
524
+ newStyle.left = '';
525
+ }
526
+ }
527
+
528
+ return newStyle;
529
+ };
530
+
531
+ /**
532
+ * 计算节点水平居中对齐后的样式
533
+ * 流式布局(relative)下不做处理;优先通过 DOM 元素实际宽度计算,回退到配置中的 width 值
534
+ * @param node 需要居中的节点配置
535
+ * @param parent 父容器节点配置
536
+ * @param layout 当前布局方式
537
+ * @param doc 画布 document 对象,用于获取 DOM 元素实际宽度
538
+ * @returns 计算后的新样式对象,若不支持居中则返回 null
539
+ */
540
+ export const calcAlignCenterStyle = (
541
+ node: MNode,
542
+ parent: MContainer,
543
+ layout: Layout,
544
+ doc?: Document,
545
+ ): Record<string, any> | null => {
546
+ if (layout === Layout.RELATIVE || !node.style) return null;
547
+
548
+ const style = { ...node.style };
549
+
550
+ if (doc) {
551
+ const el = getElById()(doc, node.id);
552
+ const parentEl = layout === Layout.FIXED ? doc.body : el?.offsetParent;
553
+ if (parentEl && el) {
554
+ style.left = calcValueByFontsize(doc, (parentEl.clientWidth - el.clientWidth) / 2);
555
+ style.right = '';
556
+ }
557
+ } else if (parent.style && isNumber(parent.style?.width) && isNumber(node.style?.width)) {
558
+ style.left = (parent.style.width - node.style.width) / 2;
559
+ style.right = '';
560
+ }
561
+
562
+ return style;
563
+ };
564
+
565
+ /**
566
+ * 计算图层移动后的目标索引
567
+ * 流式布局与绝对定位布局的移动方向相反:流式布局中"上移"对应索引减小,绝对定位中"上移"对应索引增大
568
+ * @param currentIndex 节点当前在兄弟列表中的索引
569
+ * @param offset 移动偏移量,支持数值或 LayerOffset.TOP / LayerOffset.BOTTOM
570
+ * @param brothersLength 兄弟节点总数
571
+ * @param isRelative 是否为流式布局
572
+ * @returns 目标索引位置
573
+ */
574
+ export const calcLayerTargetIndex = (
575
+ currentIndex: number,
576
+ offset: number | LayerOffset,
577
+ brothersLength: number,
578
+ isRelative: boolean,
579
+ ): number => {
580
+ if (offset === LayerOffset.TOP) {
581
+ return isRelative ? 0 : brothersLength;
582
+ }
583
+ if (offset === LayerOffset.BOTTOM) {
584
+ return isRelative ? brothersLength : 0;
585
+ }
586
+ return currentIndex + (isRelative ? -(offset as number) : (offset as number));
587
+ };
588
+
589
+ /**
590
+ * 节点配置合并策略:用于 mergeWith 的自定义回调
591
+ * - undefined 且 source 拥有该 key 时返回空字符串
592
+ * - 原来是数组而新值是对象时,使用新值
593
+ * - 新值是数组时,直接替换而非逐元素合并
594
+ */
595
+ export const editorNodeMergeCustomizer = (objValue: any, srcValue: any, key: string, _object: any, source: any) => {
596
+ if (typeof srcValue === 'undefined' && Object.hasOwn(source, key)) {
597
+ return '';
598
+ }
599
+
600
+ if (isObject(srcValue) && Array.isArray(objValue)) {
601
+ return srcValue;
602
+ }
603
+ if (Array.isArray(srcValue)) {
604
+ return srcValue;
605
+ }
606
+ };
607
+
608
+ /**
609
+ * 收集复制节点关联的依赖节点,将关联节点追加到 copyNodes 数组中
610
+ * @param copyNodes 待复制的节点列表(会被就地修改)
611
+ * @param collectorOptions 依赖收集器配置
612
+ * @param getNodeById 根据 ID 获取节点的回调函数
613
+ */
614
+ export const collectRelatedNodes = (
615
+ copyNodes: MNode[],
616
+ collectorOptions: TargetOptions,
617
+ getNodeById: (id: Id) => MNode | null,
618
+ ): void => {
619
+ const customTarget = new Target({ ...collectorOptions });
620
+ const coperWatcher = new Watcher();
621
+
622
+ coperWatcher.addTarget(customTarget);
623
+ coperWatcher.collect(copyNodes, {}, true, collectorOptions.type);
624
+
625
+ Object.keys(customTarget.deps).forEach((nodeId: Id) => {
626
+ const node = getNodeById(nodeId);
627
+ if (!node) return;
628
+ customTarget.deps[nodeId].keys.forEach((key) => {
629
+ const relateNodeId = get(node, key);
630
+ const isExist = copyNodes.find((n) => n.id === relateNodeId);
631
+ if (!isExist) {
632
+ const relateNode = getNodeById(relateNodeId);
633
+ if (relateNode) {
634
+ copyNodes.push(relateNode);
635
+ }
636
+ }
637
+ });
638
+ });
639
+ };
640
+
641
+ export interface DragClassification {
642
+ sameParentIndices: number[];
643
+ crossParentConfigs: { config: MNode; parent: MContainer }[];
644
+ /** 当同父容器节点索引异常时置为 true,调用方应中止拖拽操作 */
645
+ aborted: boolean;
646
+ }
647
+
648
+ /**
649
+ * 对拖拽的节点进行分类:同父容器内移动 vs 跨容器移动
650
+ * @param configs 被拖拽的节点列表
651
+ * @param targetParent 目标父容器
652
+ * @param getNodeInfo 获取节点信息的回调
653
+ * @returns 分类结果,包含同容器索引列表和跨容器节点列表
654
+ */
655
+ export const classifyDragSources = (
656
+ configs: MNode[],
657
+ targetParent: MContainer,
658
+ getNodeInfo: (id: Id, raw?: boolean) => EditorNodeInfo,
659
+ ): DragClassification => {
660
+ const sameParentIndices: number[] = [];
661
+ const crossParentConfigs: { config: MNode; parent: MContainer }[] = [];
662
+
663
+ for (const config of configs) {
664
+ const { parent, node: curNode } = getNodeInfo(config.id, false);
665
+ if (!parent || !curNode) continue;
666
+
667
+ const path = getNodePath(curNode.id, parent.items);
668
+ if (path.some((node) => `${targetParent.id}` === `${node.id}`)) continue;
669
+
670
+ const index = getNodeIndex(curNode.id, parent);
671
+
672
+ if (`${parent.id}` === `${targetParent.id}`) {
673
+ if (typeof index !== 'number' || index === -1) {
674
+ return { sameParentIndices, crossParentConfigs, aborted: true };
675
+ }
676
+ sameParentIndices.push(index);
677
+ } else {
678
+ crossParentConfigs.push({ config, parent });
679
+ }
680
+ }
681
+
682
+ return { sameParentIndices, crossParentConfigs, aborted: false };
683
+ };
@@ -108,6 +108,10 @@ export const styleTabConfig: TabPaneConfig = {
108
108
  'borderColor',
109
109
  ],
110
110
  } as unknown as ChildConfig,
111
+ {
112
+ name: 'transform',
113
+ defaultValue: () => ({}),
114
+ },
111
115
  ],
112
116
  },
113
117
  ],
@@ -23,7 +23,7 @@ export class UndoRedo<T = any> {
23
23
  private listCursor: number;
24
24
  private listMaxSize: number;
25
25
 
26
- constructor(listMaxSize = 20) {
26
+ constructor(listMaxSize = 100) {
27
27
  const minListMaxSize = 2;
28
28
  this.elementList = [];
29
29
  this.listCursor = 0;
@@ -42,29 +42,30 @@ export class UndoRedo<T = any> {
42
42
  }
43
43
 
44
44
  public canUndo(): boolean {
45
- return this.listCursor > 1;
45
+ return this.listCursor > 0;
46
46
  }
47
47
 
48
- // 返回undo后的当前元素
48
+ /** 返回被撤销的操作 */
49
49
  public undo(): T | null {
50
50
  if (!this.canUndo()) {
51
51
  return null;
52
52
  }
53
53
  this.listCursor -= 1;
54
- return this.getCurrentElement();
54
+ return cloneDeep(this.elementList[this.listCursor]);
55
55
  }
56
56
 
57
57
  public canRedo() {
58
58
  return this.elementList.length > this.listCursor;
59
59
  }
60
60
 
61
- // 返回redo后的当前元素
61
+ /** 返回被重做的操作 */
62
62
  public redo(): T | null {
63
63
  if (!this.canRedo()) {
64
64
  return null;
65
65
  }
66
+ const element = cloneDeep(this.elementList[this.listCursor]);
66
67
  this.listCursor += 1;
67
- return this.getCurrentElement();
68
+ return element;
68
69
  }
69
70
 
70
71
  public getCurrentElement(): T | null {