@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.
- package/dist/es/Editor.vue_vue_type_script_setup_true_lang.js +3 -1
- package/dist/es/components/CodeParams.vue_vue_type_script_setup_true_lang.js +17 -7
- package/dist/es/fields/DataSourceFieldSelect/FieldSelect.vue_vue_type_script_setup_true_lang.js +38 -9
- package/dist/es/fields/DataSourceFieldSelect/Index.vue_vue_type_script_setup_true_lang.js +3 -1
- package/dist/es/fields/DataSourceMethodSelect.vue_vue_type_script_setup_true_name_true_lang.js +38 -5
- package/dist/es/fields/DataSourceMethods.vue_vue_type_script_setup_true_lang.js +3 -2
- package/dist/es/fields/StyleSetter/Index.vue_vue_type_script_setup_true_lang.js +5 -0
- package/dist/es/fields/StyleSetter/pro/Transform.js +5 -0
- package/dist/es/fields/StyleSetter/pro/Transform.vue_vue_type_script_setup_true_lang.js +54 -0
- package/dist/es/hooks/use-code-block-edit.js +3 -2
- package/dist/es/index.js +3 -3
- package/dist/es/initService.js +4 -4
- package/dist/es/layouts/workspace/viewer/Stage.vue_vue_type_script_setup_true_lang.js +64 -1
- package/dist/es/layouts/workspace/viewer/StageOverlay.vue_vue_type_script_setup_true_lang.js +1 -5
- package/dist/es/services/editor.js +191 -174
- package/dist/es/services/history.js +1 -9
- package/dist/es/type.js +28 -1
- package/dist/es/utils/dep/worker.js +1 -1
- package/dist/es/utils/editor-history.js +100 -0
- package/dist/es/utils/editor.js +178 -4
- package/dist/es/utils/props.js +4 -1
- package/dist/es/utils/undo-redo.js +7 -4
- package/dist/tmagic-editor.umd.cjs +744 -234
- package/package.json +8 -8
- package/src/Editor.vue +1 -0
- package/src/components/CodeBlockEditor.vue +1 -1
- package/src/components/CodeParams.vue +23 -7
- package/src/editorProps.ts +2 -0
- package/src/fields/DataSourceFieldSelect/FieldSelect.vue +39 -6
- package/src/fields/DataSourceFieldSelect/Index.vue +1 -0
- package/src/fields/DataSourceMethodSelect.vue +54 -13
- package/src/fields/DataSourceMethods.vue +9 -5
- package/src/fields/StyleSetter/Index.vue +5 -1
- package/src/fields/StyleSetter/pro/Transform.vue +54 -0
- package/src/fields/StyleSetter/pro/index.ts +1 -0
- package/src/hooks/use-code-block-edit.ts +8 -4
- package/src/initService.ts +6 -6
- package/src/layouts/workspace/viewer/Stage.vue +89 -1
- package/src/layouts/workspace/viewer/StageOverlay.vue +1 -6
- package/src/services/editor.ts +231 -270
- package/src/services/history.ts +1 -9
- package/src/type.ts +65 -5
- package/src/utils/editor-history.ts +138 -0
- package/src/utils/editor.ts +249 -4
- package/src/utils/props.ts +4 -0
- package/src/utils/undo-redo.ts +7 -6
- package/types/index.d.ts +179 -53
package/src/services/history.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/utils/editor.ts
CHANGED
|
@@ -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 {
|
|
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
|
+
};
|
package/src/utils/props.ts
CHANGED
package/src/utils/undo-redo.ts
CHANGED
|
@@ -23,7 +23,7 @@ export class UndoRedo<T = any> {
|
|
|
23
23
|
private listCursor: number;
|
|
24
24
|
private listMaxSize: number;
|
|
25
25
|
|
|
26
|
-
constructor(listMaxSize =
|
|
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 >
|
|
45
|
+
return this.listCursor > 0;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
68
|
+
return element;
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
public getCurrentElement(): T | null {
|