@flowgram-vue/document 0.2.0
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/LICENSE +22 -0
- package/dist/index.cjs +3233 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +1728 -0
- package/dist/index.js +3199 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
- package/src/datas/flow-node-render-data.ts +227 -0
- package/src/datas/flow-node-transform-data.ts +337 -0
- package/src/datas/flow-node-transition-data.ts +182 -0
- package/src/datas/index.ts +8 -0
- package/src/entities/flow-document-transformer-entity.ts +125 -0
- package/src/entities/flow-node-entity.ts +415 -0
- package/src/entities/flow-renderer-state-entity.ts +127 -0
- package/src/entities/index.ts +8 -0
- package/src/flow-document-config.ts +42 -0
- package/src/flow-document-container-module.ts +33 -0
- package/src/flow-document-contribution.ts +22 -0
- package/src/flow-document-options.ts +79 -0
- package/src/flow-document.ts +766 -0
- package/src/flow-render-tree.ts +236 -0
- package/src/flow-virtual-tree.ts +244 -0
- package/src/index.ts +16 -0
- package/src/layout/horizontal-fixed-layout.ts +198 -0
- package/src/layout/index.ts +7 -0
- package/src/layout/vertical-fixed-layout.ts +198 -0
- package/src/services/flow-drag-service.ts +211 -0
- package/src/services/flow-group-service/flow-group-controller.ts +120 -0
- package/src/services/flow-group-service/flow-group-service.ts +107 -0
- package/src/services/flow-group-service/flow-group-utils.ts +104 -0
- package/src/services/flow-group-service/index.ts +7 -0
- package/src/services/flow-operation-base-service.ts +333 -0
- package/src/services/index.ts +8 -0
- package/src/typings/flow-group.ts +8 -0
- package/src/typings/flow-layout.ts +72 -0
- package/src/typings/flow-node-register.ts +356 -0
- package/src/typings/flow-operation.ts +312 -0
- package/src/typings/flow-transition.ts +104 -0
- package/src/typings/flow.ts +70 -0
- package/src/typings/index.ts +11 -0
- package/src/utils/get-default-spacing.ts +21 -0
- package/src/utils/index.ts +6 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { injectable, inject, multiInject, optional } from 'inversify';
|
|
7
|
+
import { IPoint, OriginSchema, PaddingSchema, ScrollSchema, SizeSchema } from '@flowgram-vue/utils';
|
|
8
|
+
|
|
9
|
+
import { type FlowLayout, FlowLayoutDefault, FlowLayoutContribution } from '../typings';
|
|
10
|
+
import { type FlowDocument, FlowDocumentProvider } from '../flow-document';
|
|
11
|
+
import { FlowNodeEntity } from '../entities';
|
|
12
|
+
import { FlowNodeTransformData } from '../datas';
|
|
13
|
+
|
|
14
|
+
// 开始节点距离上边 36 像素
|
|
15
|
+
const DEFAULT_SCROLL = -36;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 用于描述节点的结构特征
|
|
19
|
+
*/
|
|
20
|
+
interface FlowNodeTransformStructData {
|
|
21
|
+
childrenLength: number;
|
|
22
|
+
index: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 用于描述节点的结构特征
|
|
26
|
+
*/
|
|
27
|
+
interface FlowNodeTransformStructData {
|
|
28
|
+
childrenLength: number;
|
|
29
|
+
index: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isStructDataEqual(
|
|
33
|
+
struct1: FlowNodeTransformStructData,
|
|
34
|
+
struct2: FlowNodeTransformStructData
|
|
35
|
+
): boolean {
|
|
36
|
+
return struct1.childrenLength === struct2.childrenLength && struct1.index === struct2.index;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@injectable()
|
|
40
|
+
export class VerticalFixedLayout implements FlowLayout {
|
|
41
|
+
name = FlowLayoutDefault.VERTICAL_FIXED_LAYOUT;
|
|
42
|
+
|
|
43
|
+
protected structDataMap = new WeakMap<FlowNodeEntity, FlowNodeTransformStructData>();
|
|
44
|
+
|
|
45
|
+
@inject(FlowDocumentProvider) protected documentProvider: FlowDocumentProvider;
|
|
46
|
+
|
|
47
|
+
@multiInject(FlowLayoutContribution)
|
|
48
|
+
@optional()
|
|
49
|
+
contribs?: FlowLayoutContribution[];
|
|
50
|
+
|
|
51
|
+
get document(): FlowDocument {
|
|
52
|
+
return this.documentProvider();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
reload() {
|
|
56
|
+
this.structDataMap = new WeakMap();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 更新布局
|
|
61
|
+
*/
|
|
62
|
+
update(): void {
|
|
63
|
+
this.updateLocalTransform(this.document.root);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 更新节点的偏移
|
|
68
|
+
* @param node
|
|
69
|
+
* @param forceChange
|
|
70
|
+
*/
|
|
71
|
+
updateLocalTransform(node: FlowNodeEntity, forceChange = false): boolean {
|
|
72
|
+
const { children, parent, isInlineBlock } = node;
|
|
73
|
+
|
|
74
|
+
const transform = node.getData<FlowNodeTransformData>(FlowNodeTransformData);
|
|
75
|
+
const { getDelta, getOrigin } = node.getNodeRegistry();
|
|
76
|
+
const lastStructData = this.structDataMap.get(node) || {
|
|
77
|
+
childrenLength: 0,
|
|
78
|
+
index: -1,
|
|
79
|
+
};
|
|
80
|
+
// 重新计算都要清空 bounds 缓存,因为 bounds 依赖所有
|
|
81
|
+
node.clearMemoGlobal();
|
|
82
|
+
let localDirty = transform.localDirty || forceChange;
|
|
83
|
+
const newStructData: FlowNodeTransformStructData = {
|
|
84
|
+
index: node.index,
|
|
85
|
+
childrenLength: node.children.length,
|
|
86
|
+
};
|
|
87
|
+
// index 变化也要重新计算
|
|
88
|
+
if (!isStructDataEqual(lastStructData, newStructData)) {
|
|
89
|
+
localDirty = true;
|
|
90
|
+
this.structDataMap.set(node, newStructData);
|
|
91
|
+
}
|
|
92
|
+
// Step1: 计算子节点
|
|
93
|
+
let siblingDirty = false;
|
|
94
|
+
if (children.length > 0) {
|
|
95
|
+
for (const child of children) {
|
|
96
|
+
const childDirty = this.updateLocalTransform(child, siblingDirty);
|
|
97
|
+
// 子节点变更则父节点跟着变更
|
|
98
|
+
if (childDirty) {
|
|
99
|
+
siblingDirty = true;
|
|
100
|
+
localDirty = true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// 如果没有变更则不执行
|
|
105
|
+
if (!localDirty) return false;
|
|
106
|
+
// Step2: 计算节点的 position 偏移量
|
|
107
|
+
node.clearMemoLocal();
|
|
108
|
+
transform.transform.update({
|
|
109
|
+
origin: getOrigin ? getOrigin(transform, this) : this.getDefaultNodeOrigin(),
|
|
110
|
+
});
|
|
111
|
+
const preTransform = transform.pre;
|
|
112
|
+
const delta = getDelta?.(transform, this) || { x: 0, y: 0 };
|
|
113
|
+
const inlineSpacingPre =
|
|
114
|
+
isInlineBlock && transform.parent?.inlineSpacingPre ? transform.parent?.inlineSpacingPre : 0;
|
|
115
|
+
const fromParentDelta = parent?.getNodeRegistry().getChildDelta?.(transform, this) || {
|
|
116
|
+
x: 0,
|
|
117
|
+
y: 0,
|
|
118
|
+
};
|
|
119
|
+
delta.x += fromParentDelta.x;
|
|
120
|
+
delta.y += fromParentDelta.y;
|
|
121
|
+
|
|
122
|
+
// Step3:根据上一个节点的相对偏移算当前偏移
|
|
123
|
+
const position = { x: delta.x, y: delta.y };
|
|
124
|
+
// 垂直布局
|
|
125
|
+
if (isInlineBlock) {
|
|
126
|
+
position.y += inlineSpacingPre;
|
|
127
|
+
} else {
|
|
128
|
+
position.y += preTransform?.localBounds.bottom || 0;
|
|
129
|
+
position.y += preTransform?.spacing || 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
transform.transform.update({
|
|
133
|
+
size: transform.data.size,
|
|
134
|
+
position,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// 布局结束后可执行额外逻辑
|
|
138
|
+
this.onAfterUpdateLocalTransform(transform);
|
|
139
|
+
|
|
140
|
+
transform.localDirty = false;
|
|
141
|
+
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
onAfterUpdateLocalTransform(transform: FlowNodeTransformData) {
|
|
146
|
+
// 执行 register 上的 onAfterUpdateLocalTransform
|
|
147
|
+
const { onAfterUpdateLocalTransform } = transform.entity.getNodeRegistry();
|
|
148
|
+
onAfterUpdateLocalTransform?.(transform, this);
|
|
149
|
+
|
|
150
|
+
// 执行 contribution 上的 onAfterUpdateLocalTransform
|
|
151
|
+
this.contribs?.forEach((_contrib) => {
|
|
152
|
+
_contrib?.onAfterUpdateLocalTransform?.(transform, this);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
getNodeTransform(node: FlowNodeEntity): FlowNodeTransformData {
|
|
157
|
+
return node.getData(FlowNodeTransformData)!;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
getPadding(node: FlowNodeEntity): PaddingSchema {
|
|
161
|
+
const { inlineSpacingPre, inlineSpacingAfter, padding } = node.getNodeMeta();
|
|
162
|
+
const transform = this.getNodeTransform(node);
|
|
163
|
+
if (padding) {
|
|
164
|
+
return typeof padding === 'function' ? padding(transform) : padding;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const paddingPre =
|
|
168
|
+
typeof inlineSpacingPre === 'function' ? inlineSpacingPre(transform) : inlineSpacingPre;
|
|
169
|
+
const paddingAfter =
|
|
170
|
+
typeof inlineSpacingAfter === 'function' ? inlineSpacingAfter(transform) : inlineSpacingAfter;
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
left: 0,
|
|
174
|
+
top: paddingPre,
|
|
175
|
+
right: 0,
|
|
176
|
+
bottom: paddingAfter,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
getInitScroll(contentSize: SizeSchema): ScrollSchema {
|
|
181
|
+
return {
|
|
182
|
+
scrollX: -contentSize.width / 2,
|
|
183
|
+
scrollY: DEFAULT_SCROLL,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
getDefaultInputPoint(node: FlowNodeEntity): IPoint {
|
|
188
|
+
return this.getNodeTransform(node).bounds.topCenter;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
getDefaultOutputPoint(node: FlowNodeEntity): IPoint {
|
|
192
|
+
return this.getNodeTransform(node).bounds.bottomCenter;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
getDefaultNodeOrigin(): OriginSchema {
|
|
196
|
+
return { x: 0.5, y: 0 };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { inject, injectable } from 'inversify';
|
|
7
|
+
import { Emitter } from '@flowgram-vue/utils';
|
|
8
|
+
import { EntityManager } from '@flowgram-vue/core';
|
|
9
|
+
|
|
10
|
+
import { FlowGroupUtils } from './flow-group-service/flow-group-utils';
|
|
11
|
+
import {
|
|
12
|
+
FlowNodeBaseType,
|
|
13
|
+
FlowNodeJSON,
|
|
14
|
+
FlowOperationBaseService,
|
|
15
|
+
LABEL_SIDE_TYPE,
|
|
16
|
+
} from '../typings';
|
|
17
|
+
import { FlowDocument } from '../flow-document';
|
|
18
|
+
import { FlowNodeEntity, FlowRendererStateEntity } from '../entities';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 拖拽相关操作
|
|
22
|
+
* 外部实现抽象类
|
|
23
|
+
*/
|
|
24
|
+
@injectable()
|
|
25
|
+
export class FlowDragService {
|
|
26
|
+
@inject(FlowDocument)
|
|
27
|
+
protected document: FlowDocument;
|
|
28
|
+
|
|
29
|
+
@inject(FlowOperationBaseService)
|
|
30
|
+
protected operationService: FlowOperationBaseService;
|
|
31
|
+
|
|
32
|
+
@inject(EntityManager)
|
|
33
|
+
protected entityManager: EntityManager;
|
|
34
|
+
|
|
35
|
+
protected onDropEmitter = new Emitter<{
|
|
36
|
+
dropNode: FlowNodeEntity;
|
|
37
|
+
dragNodes: FlowNodeEntity[];
|
|
38
|
+
dragJSON?: FlowNodeJSON;
|
|
39
|
+
}>();
|
|
40
|
+
|
|
41
|
+
readonly onDrop = this.onDropEmitter.event;
|
|
42
|
+
|
|
43
|
+
get renderState(): FlowRendererStateEntity {
|
|
44
|
+
return this.document.renderState;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 拖拽所有节点中的首个节点
|
|
48
|
+
get dragStartNode(): FlowNodeEntity {
|
|
49
|
+
return this.renderState.getDragStartEntity()!;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 拖拽的所有节点
|
|
53
|
+
get dragNodes(): FlowNodeEntity[] {
|
|
54
|
+
return this.renderState.getDragEntities();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 放置的区域
|
|
58
|
+
get dropNodeId(): string | undefined {
|
|
59
|
+
return this.renderState.getNodeDroppingId();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 是否在拖拽分支
|
|
63
|
+
get isDragBranch(): boolean {
|
|
64
|
+
return this.renderState.isBranch || this.dragStartNode?.isInlineBlock;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 拖拽的所有节点及其自节点
|
|
68
|
+
get nodeDragIdsWithChildren(): string[] {
|
|
69
|
+
return this.renderState.config.nodeDragIdsWithChildren || [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get dragging(): boolean {
|
|
73
|
+
return !!this.renderState.dragging;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get labelSide(): LABEL_SIDE_TYPE | undefined {
|
|
77
|
+
return this.renderState.config.dragLabelSide;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 放置到目标分支
|
|
82
|
+
*/
|
|
83
|
+
dropBranch(): void {
|
|
84
|
+
this.dropNode();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 移动并且创建节点
|
|
89
|
+
* Move and create node
|
|
90
|
+
*/
|
|
91
|
+
async dropCreateNode(
|
|
92
|
+
json: FlowNodeJSON,
|
|
93
|
+
onCreateNode?: (json: FlowNodeJSON, dropEntity: FlowNodeEntity) => Promise<FlowNodeEntity>
|
|
94
|
+
) {
|
|
95
|
+
const dropEntity = this.document.getNode(this.dropNodeId!);
|
|
96
|
+
|
|
97
|
+
if (!dropEntity) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (json) {
|
|
102
|
+
const dragNodes = await onCreateNode?.(json, dropEntity);
|
|
103
|
+
this.onDropEmitter.fire({
|
|
104
|
+
dropNode: dropEntity,
|
|
105
|
+
dragNodes: dragNodes ? [dragNodes] : [],
|
|
106
|
+
dragJSON: json,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 移动到目标节点
|
|
113
|
+
*/
|
|
114
|
+
dropNode(): void {
|
|
115
|
+
const dropEntity = this.document.getNode(this.dropNodeId!);
|
|
116
|
+
if (!dropEntity) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const sortNodes: FlowNodeEntity[] = [];
|
|
121
|
+
let curr: FlowNodeEntity | undefined = this.dragStartNode;
|
|
122
|
+
while (curr && this.dragNodes.includes(curr)) {
|
|
123
|
+
sortNodes.push(curr);
|
|
124
|
+
curr = curr.next;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.operationService.dragNodes({
|
|
128
|
+
dropNode: dropEntity,
|
|
129
|
+
nodes: sortNodes,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (sortNodes.length > 0) {
|
|
133
|
+
this.onDropEmitter.fire({
|
|
134
|
+
dropNode: dropEntity,
|
|
135
|
+
dragNodes: sortNodes,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 拖拽是否可以释放在该节点后面
|
|
142
|
+
*/
|
|
143
|
+
isDroppableNode(node: FlowNodeEntity) {
|
|
144
|
+
// 没有拖拽节点时,所有节点都不可 drop
|
|
145
|
+
if (!this.dragging || this.isDragBranch) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 当前节点 & 下一个节点是否在拖拽区域
|
|
150
|
+
if (
|
|
151
|
+
this.nodeDragIdsWithChildren.includes(node.id) ||
|
|
152
|
+
(node.next && this.nodeDragIdsWithChildren.includes(node.next.id))
|
|
153
|
+
) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (node.isInlineBlocks || node.isInlineBlock) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 分组节点不能嵌套
|
|
162
|
+
const hasGroupNode = this.dragNodes.some(
|
|
163
|
+
(node) => node.flowNodeType === FlowNodeBaseType.GROUP
|
|
164
|
+
);
|
|
165
|
+
if (hasGroupNode) {
|
|
166
|
+
const group = FlowGroupUtils.getNodeRecursionGroupController(node);
|
|
167
|
+
if (group) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 拖拽分支是否可以释放在该分支
|
|
177
|
+
* @param node 拖拽的分支节点
|
|
178
|
+
* @param side 分支的前面还是后面
|
|
179
|
+
*/
|
|
180
|
+
isDroppableBranch(node: FlowNodeEntity, side: LABEL_SIDE_TYPE = LABEL_SIDE_TYPE.NORMAL_BRANCH) {
|
|
181
|
+
// 外部添加拖拽标识,默认所有分支均可添加
|
|
182
|
+
if (this.renderState.isBranch) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
// 拖拽的是分支
|
|
186
|
+
if (this.isDragBranch) {
|
|
187
|
+
if (
|
|
188
|
+
// 拖拽到分支
|
|
189
|
+
!node.isInlineBlock ||
|
|
190
|
+
// 只能在同一分支条件下
|
|
191
|
+
node.parent !== this.dragStartNode.parent ||
|
|
192
|
+
// 自己不能拖拽给自己
|
|
193
|
+
node === this.dragStartNode
|
|
194
|
+
) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 分支的下一个节点不是当前要拖拽的节点
|
|
199
|
+
if (side === LABEL_SIDE_TYPE.NORMAL_BRANCH && node.next !== this.dragStartNode) {
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 分支的前一个节点不是当前要拖拽的节点
|
|
204
|
+
if (side === LABEL_SIDE_TYPE.PRE_BRANCH && node.pre !== this.dragStartNode) {
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Rectangle } from '@flowgram-vue/utils';
|
|
7
|
+
|
|
8
|
+
import { FlowNodeEntity } from '../../entities';
|
|
9
|
+
import { FlowNodeRenderData, FlowNodeTransformData } from '../../datas';
|
|
10
|
+
import { FlowGroupUtils } from './flow-group-utils';
|
|
11
|
+
|
|
12
|
+
/** 分组控制器 */
|
|
13
|
+
export class FlowGroupController {
|
|
14
|
+
private constructor(public readonly groupNode: FlowNodeEntity) {}
|
|
15
|
+
|
|
16
|
+
public get nodes(): FlowNodeEntity[] {
|
|
17
|
+
return this.groupNode.collapsedChildren || [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public get collapsed(): boolean {
|
|
21
|
+
const groupTransformData = this.groupNode.getData<FlowNodeTransformData>(FlowNodeTransformData);
|
|
22
|
+
return groupTransformData.collapsed;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public collapse(): void {
|
|
26
|
+
this.collapsed = true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public expand(): void {
|
|
30
|
+
this.collapsed = false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 获取分组外围的最大边框 */
|
|
34
|
+
public get bounds(): Rectangle {
|
|
35
|
+
const groupNodeBounds =
|
|
36
|
+
this.groupNode.getData<FlowNodeTransformData>(FlowNodeTransformData).bounds;
|
|
37
|
+
return groupNodeBounds;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 是否是开始节点 */
|
|
41
|
+
public isStartNode(node?: FlowNodeEntity): boolean {
|
|
42
|
+
if (!node) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const nodes = this.nodes;
|
|
46
|
+
if (!nodes[0]) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return node.id === nodes[0].id;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 是否是结束节点 */
|
|
53
|
+
public isEndNode(node?: FlowNodeEntity): boolean {
|
|
54
|
+
if (!node) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
const nodes = this.nodes;
|
|
58
|
+
if (!nodes[nodes.length - 1]) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return node.id === nodes[nodes.length - 1].id;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public set note(note: string) {
|
|
65
|
+
this.groupNode.getNodeMeta().note = note;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public get note(): string {
|
|
69
|
+
return this.groupNode.getNodeMeta().note || '';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public set noteHeight(height: number) {
|
|
73
|
+
this.groupNode.getNodeMeta().noteHeight = height;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public get noteHeight(): number {
|
|
77
|
+
return this.groupNode.getNodeMeta().noteHeight || 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public get positionConfig(): Record<string, number> {
|
|
81
|
+
return this.groupNode.getNodeMeta().positionConfig;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private set collapsed(collapsed: boolean) {
|
|
85
|
+
const groupTransformData = this.groupNode.getData<FlowNodeTransformData>(FlowNodeTransformData);
|
|
86
|
+
groupTransformData.collapsed = collapsed;
|
|
87
|
+
groupTransformData.localDirty = true;
|
|
88
|
+
if (groupTransformData.parent) groupTransformData.parent.localDirty = true;
|
|
89
|
+
if (groupTransformData.parent?.firstChild)
|
|
90
|
+
groupTransformData.parent.firstChild.localDirty = true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public set hovered(hovered: boolean) {
|
|
94
|
+
const groupRenderData = this.groupNode.getData<FlowNodeRenderData>(FlowNodeRenderData);
|
|
95
|
+
if (hovered) {
|
|
96
|
+
groupRenderData.toggleMouseEnter();
|
|
97
|
+
} else {
|
|
98
|
+
groupRenderData.toggleMouseLeave();
|
|
99
|
+
}
|
|
100
|
+
if (groupRenderData.hovered === hovered) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
groupRenderData.hovered = hovered;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public get hovered(): boolean {
|
|
107
|
+
const groupRenderData = this.groupNode.getData<FlowNodeRenderData>(FlowNodeRenderData);
|
|
108
|
+
return groupRenderData.hovered;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
public static create(groupNode?: FlowNodeEntity): FlowGroupController | undefined {
|
|
112
|
+
if (!groupNode) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (!FlowGroupUtils.isGroupNode(groupNode)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
return new FlowGroupController(groupNode);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { nanoid } from 'nanoid';
|
|
7
|
+
import { inject, injectable } from 'inversify';
|
|
8
|
+
import { EntityManager } from '@flowgram-vue/core';
|
|
9
|
+
|
|
10
|
+
import { FlowNodeBaseType, FlowOperationBaseService, OperationType } from '../../typings';
|
|
11
|
+
import { FlowNodeEntity } from '../../entities';
|
|
12
|
+
import { FlowGroupUtils } from './flow-group-utils';
|
|
13
|
+
import { FlowGroupController } from './flow-group-controller';
|
|
14
|
+
|
|
15
|
+
@injectable()
|
|
16
|
+
/** 分组服务 */
|
|
17
|
+
export class FlowGroupService {
|
|
18
|
+
@inject(EntityManager) public readonly entityManager: EntityManager;
|
|
19
|
+
|
|
20
|
+
@inject(FlowOperationBaseService)
|
|
21
|
+
public readonly operationService: FlowOperationBaseService;
|
|
22
|
+
|
|
23
|
+
/** 创建分组节点 */
|
|
24
|
+
public createGroup(nodes: FlowNodeEntity[]): FlowNodeEntity | undefined {
|
|
25
|
+
if (!nodes || !Array.isArray(nodes) || nodes.length === 0) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (!FlowGroupUtils.validate(nodes)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const sortedNodes: FlowNodeEntity[] = nodes.sort((a, b) => a.index - b.index);
|
|
32
|
+
const fromNode = sortedNodes[0];
|
|
33
|
+
const groupId = `group_${nanoid(5)}`;
|
|
34
|
+
this.operationService.apply({
|
|
35
|
+
type: OperationType.createGroup,
|
|
36
|
+
value: {
|
|
37
|
+
targetId: fromNode.id,
|
|
38
|
+
groupId,
|
|
39
|
+
nodeIds: nodes.map((node) => node.id),
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
const groupNode = this.entityManager.getEntityById<FlowNodeEntity>(groupId);
|
|
43
|
+
if (!groupNode) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const group = this.groupController(groupNode);
|
|
47
|
+
if (!group) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
group.expand();
|
|
51
|
+
return groupNode;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 删除分组 */
|
|
55
|
+
public deleteGroup(groupNode: FlowNodeEntity): void {
|
|
56
|
+
const json = groupNode.toJSON();
|
|
57
|
+
if (!groupNode.pre || !json) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this.operationService.apply({
|
|
61
|
+
type: OperationType.deleteNodes,
|
|
62
|
+
value: {
|
|
63
|
+
fromId: groupNode.pre.id,
|
|
64
|
+
nodes: [json],
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 取消分组 */
|
|
70
|
+
public ungroup(groupNode: FlowNodeEntity): void {
|
|
71
|
+
const group = this.groupController(groupNode);
|
|
72
|
+
if (!group) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const nodes = group.nodes;
|
|
76
|
+
if (!groupNode.pre) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
group.collapse();
|
|
80
|
+
this.operationService.apply({
|
|
81
|
+
type: OperationType.ungroup,
|
|
82
|
+
value: {
|
|
83
|
+
groupId: groupNode.id,
|
|
84
|
+
targetId: groupNode.pre.id,
|
|
85
|
+
nodeIds: nodes.map((node) => node.id),
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 返回所有分组节点 */
|
|
91
|
+
public getAllGroups(): FlowGroupController[] {
|
|
92
|
+
const allNodes = this.entityManager.getEntities<FlowNodeEntity>(FlowNodeEntity);
|
|
93
|
+
const groupNodes = allNodes.filter((node) => node.flowNodeType === FlowNodeBaseType.GROUP);
|
|
94
|
+
return groupNodes
|
|
95
|
+
.map((node) => this.groupController(node))
|
|
96
|
+
.filter(Boolean) as FlowGroupController[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 获取分组控制器*/
|
|
100
|
+
public groupController(group: FlowNodeEntity): FlowGroupController | undefined {
|
|
101
|
+
return FlowGroupController.create(group);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public static validate(nodes: FlowNodeEntity[]): boolean {
|
|
105
|
+
return FlowGroupUtils.validate(nodes);
|
|
106
|
+
}
|
|
107
|
+
}
|