@flowgram-vue/renderer 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.
Files changed (50) hide show
  1. package/LICENSE +22 -0
  2. package/dist/index.cjs +2933 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.ts +721 -0
  5. package/dist/index.js +2918 -0
  6. package/dist/index.js.map +1 -0
  7. package/index.module.less +167 -0
  8. package/package.json +64 -0
  9. package/src/components/Adder.ts +107 -0
  10. package/src/components/BranchDraggableRenderer.ts +77 -0
  11. package/src/components/Collapse.ts +87 -0
  12. package/src/components/CollapseAdder.ts +94 -0
  13. package/src/components/CustomLine.ts +35 -0
  14. package/src/components/LabelsRenderer.ts +162 -0
  15. package/src/components/LinesRenderer.ts +99 -0
  16. package/src/components/MarkerActivatedArrow.ts +44 -0
  17. package/src/components/MarkerArrow.ts +44 -0
  18. package/src/components/RoundedTurningLine.ts +165 -0
  19. package/src/components/StraightLine.ts +31 -0
  20. package/src/components/utils.ts +295 -0
  21. package/src/entities/README.md +3 -0
  22. package/src/entities/flow-drag-entity.ts +267 -0
  23. package/src/entities/flow-select-config-entity.ts +114 -0
  24. package/src/entities/index.ts +8 -0
  25. package/src/entities/selector-box-config-entity.ts +88 -0
  26. package/src/env.d.ts +10 -0
  27. package/src/flow-renderer-container-module.ts +14 -0
  28. package/src/flow-renderer-contribution.ts +12 -0
  29. package/src/flow-renderer-registry.ts +155 -0
  30. package/src/flow-renderer-resize-observer.ts +56 -0
  31. package/src/hooks/use-base-color.ts +26 -0
  32. package/src/index.ts +16 -0
  33. package/src/layer-vue-provide.ts +44 -0
  34. package/src/layers/flow-context-menu-layer.ts +153 -0
  35. package/src/layers/flow-debug-layer.ts +227 -0
  36. package/src/layers/flow-drag-layer.ts +413 -0
  37. package/src/layers/flow-labels-layer.ts +100 -0
  38. package/src/layers/flow-lines-layer.ts +111 -0
  39. package/src/layers/flow-nodes-content-layer.ts +155 -0
  40. package/src/layers/flow-nodes-transform-layer.ts +151 -0
  41. package/src/layers/flow-scroll-bar-layer.ts +401 -0
  42. package/src/layers/flow-scroll-limit-layer.ts +36 -0
  43. package/src/layers/flow-selector-bounds-layer.ts +191 -0
  44. package/src/layers/flow-selector-box-layer.ts +244 -0
  45. package/src/layers/index.ts +16 -0
  46. package/src/utils/element.ts +36 -0
  47. package/src/utils/find-selected-nodes.ts +80 -0
  48. package/src/utils/index.ts +7 -0
  49. package/src/utils/scroll-bar-events.ts +13 -0
  50. package/src/utils/scroll-limit.ts +58 -0
@@ -0,0 +1,244 @@
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 { domUtils, PositionSchema } from '@flowgram-vue/utils';
8
+ import { FlowDocument, FlowNodeEntity, FlowNodeTransformData } from '@flowgram-vue/document';
9
+ import {
10
+ ContextMenuService,
11
+ EditorState,
12
+ EditorStateConfigEntity,
13
+ Layer,
14
+ LayerOptions,
15
+ observeEntity,
16
+ PipelineLayerPriority,
17
+ PlaygroundConfigEntity,
18
+ PlaygroundDrag,
19
+ SelectionService,
20
+ } from '@flowgram-vue/core';
21
+
22
+ import { FlowSelectConfigEntity, SelectorBoxConfigEntity } from '../entities';
23
+
24
+ export interface FlowSelectorBoxOptions extends LayerOptions {
25
+ /**
26
+ * 默认不提供则为点击空白地方可以框选
27
+ * @param e
28
+ * @param entity
29
+ */
30
+ canSelect?: (e: MouseEvent, entity: SelectorBoxConfigEntity) => boolean;
31
+ }
32
+ /**
33
+ * 流程选择框
34
+ */
35
+ @injectable()
36
+ export class FlowSelectorBoxLayer extends Layer<FlowSelectorBoxOptions> {
37
+ @inject(FlowDocument)
38
+ protected flowDocument: FlowDocument;
39
+
40
+ @inject(ContextMenuService)
41
+ readonly contextMenuService: ContextMenuService;
42
+
43
+ @observeEntity(PlaygroundConfigEntity)
44
+ protected playgroundConfigEntity: PlaygroundConfigEntity;
45
+
46
+ @inject(SelectionService) readonly selectionService: SelectionService;
47
+
48
+ @observeEntity(SelectorBoxConfigEntity)
49
+ protected selectorBoxConfigEntity: SelectorBoxConfigEntity;
50
+
51
+ @observeEntity(FlowSelectConfigEntity)
52
+ protected selectConfigEntity: FlowSelectConfigEntity;
53
+
54
+ @observeEntity(EditorStateConfigEntity)
55
+ protected editorStateConfig: EditorStateConfigEntity;
56
+
57
+ readonly node = domUtils.createDivWithClass('gedit-selector-box-layer');
58
+
59
+ /**
60
+ * 选择框
61
+ */
62
+ protected selectorBox = this.createDOMCache('gedit-selector-box');
63
+
64
+ /**
65
+ * 用于遮挡鼠标,避免触发 hover
66
+ */
67
+ protected selectorBoxBlock = this.createDOMCache('gedit-selector-box-block');
68
+
69
+ protected transformVisibles: FlowNodeTransformData[];
70
+
71
+ /**
72
+ * 拖动选择框
73
+ */
74
+ protected selectboxDragger = new PlaygroundDrag({
75
+ onDragStart: (e) => {
76
+ this.selectConfigEntity.clearSelectedNodes();
77
+ const mousePos = this.playgroundConfigEntity.getPosFromMouseEvent(e);
78
+ this.transformVisibles = this.flowDocument
79
+ .getRenderDatas(FlowNodeTransformData, false)
80
+ .filter((transform) => {
81
+ const { entity } = transform;
82
+ if (entity.originParent) {
83
+ return (
84
+ this.nodeSelectable(entity, mousePos) &&
85
+ this.nodeSelectable(entity.originParent, mousePos)
86
+ );
87
+ }
88
+ return this.nodeSelectable(entity, mousePos);
89
+ });
90
+ this.selectorBoxConfigEntity.setDragInfo(e);
91
+ this.updateSelectorBox(this.selectorBoxConfigEntity);
92
+ },
93
+ onDrag: (e) => {
94
+ this.selectorBoxConfigEntity.setDragInfo(e);
95
+ // 更新选择框
96
+ this.selectConfigEntity.selectFromBounds(
97
+ this.selectorBoxConfigEntity.toRectangle(this.playgroundConfigEntity.finalScale),
98
+ this.transformVisibles
99
+ );
100
+ this.updateSelectorBox(this.selectorBoxConfigEntity);
101
+ },
102
+ onDragEnd: (e) => {
103
+ this.selectorBoxConfigEntity.setDragInfo(e);
104
+ this.transformVisibles.length = 0;
105
+ this.updateSelectorBox(this.selectorBoxConfigEntity);
106
+ },
107
+ });
108
+
109
+ onReady(): void {
110
+ if (!this.options.canSelect) {
111
+ this.options.canSelect = (e: MouseEvent) => {
112
+ const target = e.target as HTMLElement | undefined;
113
+ // 默认点击空白地方可以框选
114
+ return target === this.pipelineNode || target === this.playgroundNode;
115
+ };
116
+ }
117
+ // 将选中的节点同步到全局
118
+ // TODO 后续要统一到 selection service
119
+ this.toDispose.pushAll([
120
+ this.selectConfigEntity.onConfigChanged(() => {
121
+ this.selectionService.selection = this.selectConfigEntity.selectedNodes;
122
+ }),
123
+ this.selectionService.onSelectionChanged(() => {
124
+ const selectedNodes = this.selectionService.selection.filter(
125
+ (entity) => entity instanceof FlowNodeEntity
126
+ );
127
+
128
+ this.selectConfigEntity.selectedNodes = selectedNodes as FlowNodeEntity[];
129
+ }),
130
+ ]);
131
+ this.listenPlaygroundEvent(
132
+ 'mousedown',
133
+ (e: MouseEvent): boolean | undefined => {
134
+ if (!this.isEnabled()) return;
135
+ // 自定义拦截选择框事件
136
+ if (this.options.canSelect && !this.options.canSelect(e, this.selectorBoxConfigEntity)) {
137
+ return;
138
+ }
139
+
140
+ const currentState = this.editorStateConfig.getCurrentState();
141
+
142
+ // 鼠标友好模式,框选后,再次点击其他地方或者框选其他地方,需要清空已有选择的节点
143
+ if (currentState === EditorState.STATE_MOUSE_FRIENDLY_SELECT) {
144
+ this.selectConfigEntity.clearSelectedNodes();
145
+ }
146
+
147
+ // const target = e.target as HTMLElement | undefined;
148
+ // TODO 下边这些特化逻辑迁移到固定布局逻辑
149
+ // const linesLayer = document.querySelector('.gedit-flow-lines-layer');
150
+ // const toolsTarget = document.querySelector('.flow-canvas-selector-box-tools');
151
+ // const isInTools = toolsTarget && (toolsTarget === target || toolsTarget.contains(target!));
152
+ // 保证 service 更新后进行是否清除的计算
153
+ // setTimeout(() => {
154
+ // // 如果点击到选中区域的菜单栏
155
+ // if (!isInTools && !this.contextMenuService.rightPanelVisible) {
156
+ // // 取消之前的选择状态
157
+ // this.selectConfigEntity.clearSelectedNodes();
158
+ // }
159
+ // }, 0);
160
+ // if (
161
+ // target === this.pipelineNode ||
162
+ // target === this.playgroundNode // 点击空白区域
163
+ // linesLayer?.contains(target!) // 点击 svg 线条
164
+ // target?.classList.contains('flow-canvas-adder') || // 点击添加按钮的留白区域
165
+ // target?.classList.contains('flow-canvas-block-icon') // 点击添加按钮的留白区域
166
+ // ) {
167
+ // return true;
168
+ // }
169
+ this.selectboxDragger.start(e.clientX, e.clientY, this.config);
170
+ return true;
171
+ },
172
+ PipelineLayerPriority.BASE_LAYER
173
+ );
174
+ }
175
+
176
+ isEnabled(): boolean {
177
+ const currentState = this.editorStateConfig.getCurrentState();
178
+ const isMouseFriendly = currentState === EditorState.STATE_MOUSE_FRIENDLY_SELECT;
179
+
180
+ return (
181
+ !this.config.disabled &&
182
+ !this.config.readonly &&
183
+ // 鼠标友好模式下,需要按下 shift 启动框选
184
+ ((isMouseFriendly && this.editorStateConfig.isPressingShift) ||
185
+ currentState === EditorState.STATE_SELECT) &&
186
+ !this.selectorBoxConfigEntity.disabled
187
+ );
188
+ }
189
+
190
+ /**
191
+ * Destroy
192
+ */
193
+ dispose(): void {
194
+ this.selectorBox.dispose();
195
+ this.selectorBoxBlock.dispose();
196
+ super.dispose();
197
+ }
198
+
199
+ protected updateSelectorBox(selector: SelectorBoxConfigEntity): void {
200
+ const node = this.selectorBox.get();
201
+ const block = this.selectorBoxBlock.get();
202
+ // 非可用状态且在 moving 则关闭选择框
203
+ if (!this.isEnabled() && selector.isMoving) {
204
+ this.selectorBoxConfigEntity.collapse();
205
+ }
206
+ if (!this.isEnabled() || !selector.isMoving) {
207
+ node.setStyle({
208
+ display: 'none',
209
+ });
210
+ block.setStyle({
211
+ display: 'none',
212
+ });
213
+ } else {
214
+ node.setStyle({
215
+ display: 'block',
216
+ left: selector.position.x,
217
+ top: selector.position.y,
218
+ width: selector.size.width,
219
+ height: selector.size.height,
220
+ });
221
+ // 这是遮挡滑块,防止触发节点 hover
222
+ block.setStyle({
223
+ display: 'block',
224
+ left: selector.position.x - 10,
225
+ top: selector.position.y - 10,
226
+ width: selector.size.width + 20,
227
+ height: selector.size.height + 20,
228
+ });
229
+ }
230
+ }
231
+
232
+ private nodeSelectable(node: FlowNodeEntity, mousePos: PositionSchema) {
233
+ const selectable = node.getNodeMeta().selectable;
234
+ if (typeof selectable === 'function') {
235
+ return selectable(node, mousePos);
236
+ } else {
237
+ return selectable;
238
+ }
239
+ }
240
+
241
+ // autorun(): void {
242
+ // this.updateSelectorBox(this.selectorBoxConfigEntity);
243
+ // }
244
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export * from './flow-nodes-transform-layer';
7
+ export * from './flow-nodes-content-layer';
8
+ export * from './flow-lines-layer';
9
+ export * from './flow-labels-layer';
10
+ export * from './flow-debug-layer';
11
+ export * from './flow-scroll-bar-layer';
12
+ export * from './flow-drag-layer';
13
+ export * from './flow-selector-box-layer';
14
+ export * from './flow-selector-bounds-layer';
15
+ export * from './flow-context-menu-layer';
16
+ export * from './flow-scroll-limit-layer';
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { isNil } from 'lodash-es';
7
+ export const isHidden = (dom?: HTMLElement) => {
8
+ if (!dom || isNil(dom?.offsetParent)) {
9
+ return true;
10
+ }
11
+ const style = window.getComputedStyle(dom);
12
+ if (style?.display === 'none') {
13
+ return true;
14
+ }
15
+ return false;
16
+ };
17
+
18
+ export const isRectInit = (rect?: DOMRect): boolean => {
19
+ if (!rect) {
20
+ return false;
21
+ }
22
+ // 检查所有属性是否都为0,表示DOMRect未初始化
23
+ if (
24
+ rect.bottom === 0 &&
25
+ rect.height === 0 &&
26
+ rect.left === 0 &&
27
+ rect.right === 0 &&
28
+ rect.top === 0 &&
29
+ rect.width === 0 &&
30
+ rect.x === 0 &&
31
+ rect.y === 0
32
+ ) {
33
+ return false;
34
+ }
35
+ return true;
36
+ };
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { uniq } from 'lodash-es';
7
+ import { type FlowNodeEntity } from '@flowgram-vue/document';
8
+
9
+ function getNodePath(node: FlowNodeEntity): FlowNodeEntity[] {
10
+ const path: FlowNodeEntity[] = [node];
11
+ node = node.parent as FlowNodeEntity;
12
+ while (node) {
13
+ path.push(node);
14
+ node = node.parent as FlowNodeEntity;
15
+ }
16
+ return path.reverse();
17
+ }
18
+
19
+ /**
20
+ * 过滤掉画布节点, 有 originParent,都是非独立节点
21
+ * @param entity
22
+ */
23
+ function findRealEntity(entity: FlowNodeEntity): FlowNodeEntity {
24
+ while (entity.originParent) {
25
+ entity = entity.originParent;
26
+ }
27
+ return entity;
28
+ }
29
+ /**
30
+ * 生成选中节点的路径
31
+ * 如
32
+ * [
33
+ * 'root',
34
+ * 'exclusiveSplit_30baf8b1da0',
35
+ * 'exclusiveSplit_d0070ce5d04',
36
+ * 'createRecord_47e8fe1dfc3'
37
+ * ],
38
+ * [
39
+ * 'root',
40
+ * 'exclusiveSplit_30baf8b1da0',
41
+ * 'exclusiveSplit_d0070ce5d04',
42
+ * 'createRecord_32dcdd10274'
43
+ * ],
44
+ * [
45
+ * 'root',
46
+ * 'exclusiveSplit_30baf8b1da0',
47
+ * 'exclusiveSplit_d0070ce5d04',
48
+ * 'exclusiveSplit_a5579b3997d', // 这里产生分叉
49
+ * 'createRecord_b57b00eee94' // 父亲节点分叉了,这里就忽略了
50
+ * ]
51
+ * ]
52
+ * 1. 相同分支的节点,选择每个节点
53
+ * 2. 跨分支的节点选择共同的父节点
54
+ */
55
+ export function findSelectedNodes(nodes: FlowNodeEntity[]): FlowNodeEntity[] {
56
+ if (nodes.length === 0) return [];
57
+ /**
58
+ * 生成节点的路径
59
+ */
60
+ const nodePathList: FlowNodeEntity[][] = nodes.map((n) => getNodePath(n));
61
+ /**
62
+ * 只需要比较最小的路径
63
+ */
64
+ const minLength = Math.min(...nodePathList.map((n) => n.length));
65
+ let index = 0;
66
+ let selectedItems: FlowNodeEntity[] = [];
67
+ /**
68
+ * 从二维数组的每一层打平去看,看看有没有分叉,如果有分叉就在当前层停止并作为选中的节点
69
+ */
70
+ while (index < minLength) {
71
+ // eslint-disable-next-line no-loop-func
72
+ selectedItems = uniq(nodePathList.map((p) => p[index]));
73
+ // 存在分叉
74
+ if (selectedItems.length > 1) {
75
+ break;
76
+ }
77
+ index += 1;
78
+ }
79
+ return uniq(selectedItems.map((item) => findRealEntity(item)));
80
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export * from './scroll-limit';
7
+ export * from './scroll-bar-events';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ /**
7
+ * 滚动条点击事件监听
8
+ */
9
+ export const ScrollBarEvents = Symbol('ScrollBarEvents');
10
+
11
+ export interface ScrollBarEvents {
12
+ dragStart: () => void;
13
+ }
@@ -0,0 +1,58 @@
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
+ import { type PlaygroundConfigEntity } from '@flowgram-vue/core';
8
+
9
+ export interface ScrollData {
10
+ scrollX: number;
11
+ scrollY: number;
12
+ }
13
+
14
+ // viewport 缩小 30 像素
15
+ const SCROLL_LIMIT_PADDING = -120;
16
+
17
+ export function getScrollViewport(
18
+ scrollData: ScrollData,
19
+ config: PlaygroundConfigEntity
20
+ ): Rectangle {
21
+ const scale = config.finalScale;
22
+ return new Rectangle(
23
+ scrollData.scrollX / scale,
24
+ scrollData.scrollY / scale,
25
+ config.config.width / scale,
26
+ config.config.height / scale
27
+ ).pad(SCROLL_LIMIT_PADDING / scale, SCROLL_LIMIT_PADDING / scale);
28
+ }
29
+
30
+ /**
31
+ * 限制滚动
32
+ */
33
+ export function scrollLimit(
34
+ scroll: ScrollData,
35
+ boundsList: Rectangle[],
36
+ config: PlaygroundConfigEntity,
37
+ initScroll: () => ScrollData
38
+ ): ScrollData {
39
+ scroll = { ...scroll };
40
+ const configData = config.config;
41
+ const oldScroll = { scrollX: configData.scrollX, scrollY: configData.scrollY };
42
+ // 画布 size 还没初始化滚动不限制
43
+ if (boundsList.length === 0 || configData.width === 0 || configData.height === 0) return scroll;
44
+ const viewport = getScrollViewport(scroll, config);
45
+ const isVisible = boundsList.find((bounds) => Rectangle.isViewportVisible(bounds, viewport));
46
+ if (!isVisible) {
47
+ const oldViewport = getScrollViewport(oldScroll, config);
48
+ const isOldVisible = boundsList.find((bounds) =>
49
+ Rectangle.isViewportVisible(bounds, oldViewport)
50
+ );
51
+ // 如果之前也是不可见就不阻止
52
+ if (!isOldVisible) {
53
+ return initScroll();
54
+ }
55
+ return oldScroll;
56
+ }
57
+ return scroll;
58
+ }