@flowgram-vue/free-container-plugin 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 (38) hide show
  1. package/LICENSE +22 -0
  2. package/dist/index.cjs +0 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.js +0 -0
  6. package/dist/index.js.map +1 -0
  7. package/package.json +66 -0
  8. package/src/env.d.ts +10 -0
  9. package/src/index.ts +8 -0
  10. package/src/node-into-container/constant.ts +9 -0
  11. package/src/node-into-container/index.ts +13 -0
  12. package/src/node-into-container/plugin.ts +26 -0
  13. package/src/node-into-container/service.ts +370 -0
  14. package/src/node-into-container/type.ts +29 -0
  15. package/src/sub-canvas/components/background/index.ts +6 -0
  16. package/src/sub-canvas/components/background/sub-canvas-background.vue +64 -0
  17. package/src/sub-canvas/components/border/index.ts +6 -0
  18. package/src/sub-canvas/components/border/sub-canvas-border.vue +47 -0
  19. package/src/sub-canvas/components/index.ts +9 -0
  20. package/src/sub-canvas/components/render/index.ts +6 -0
  21. package/src/sub-canvas/components/render/sub-canvas-render.vue +54 -0
  22. package/src/sub-canvas/components/tips/global-store.ts +38 -0
  23. package/src/sub-canvas/components/tips/index.ts +6 -0
  24. package/src/sub-canvas/components/tips/is-mac-os.ts +6 -0
  25. package/src/sub-canvas/components/tips/sub-canvas-tips.vue +157 -0
  26. package/src/sub-canvas/components/tips/use-control.ts +65 -0
  27. package/src/sub-canvas/hooks/index.ts +8 -0
  28. package/src/sub-canvas/hooks/use-node-size.ts +69 -0
  29. package/src/sub-canvas/hooks/use-sync-node-render-size.ts +26 -0
  30. package/src/sub-canvas/index.ts +7 -0
  31. package/src/utils/adjust-sub-node-position.ts +41 -0
  32. package/src/utils/get-collision-transform.ts +41 -0
  33. package/src/utils/get-container-transforms.ts +27 -0
  34. package/src/utils/index.ts +18 -0
  35. package/src/utils/is-container.ts +9 -0
  36. package/src/utils/is-point-in-rect.ts +9 -0
  37. package/src/utils/is-rect-intersects.ts +15 -0
  38. package/src/utils/next-frame.ts +8 -0
@@ -0,0 +1,157 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+
7
+ import { defineComponent, h, type PropType, type VNode } from 'vue';
8
+
9
+ import { I18n } from '@flowgram-vue/i18n';
10
+
11
+ import { useControlTips } from './use-control';
12
+ import { isMacOS } from './is-mac-os';
13
+
14
+ function IconClose() {
15
+ return h(
16
+ 'svg',
17
+ {
18
+ xmlns: 'http://www.w3.org/2000/svg',
19
+ width: '16',
20
+ height: '16',
21
+ fill: 'none',
22
+ viewBox: '0 0 16 16',
23
+ },
24
+ [
25
+ h('path', {
26
+ fill: '#060709',
27
+ 'fill-opacity': '0.5',
28
+ d: 'M12.13 12.128a.5.5 0 0 0 .001-.706L8.71 8l3.422-3.423a.5.5 0 0 0-.001-.705.5.5 0 0 0-.706-.002L8.002 7.293 4.579 3.87a.5.5 0 0 0-.705.002.5.5 0 0 0-.002.705L7.295 8l-3.423 3.422a.5.5 0 0 0 .002.706c.195.195.51.197.705.001l3.423-3.422 3.422 3.422c.196.196.51.194.706-.001',
29
+ }),
30
+ ]
31
+ );
32
+ }
33
+
34
+ export default defineComponent({
35
+ name: 'SubCanvasTips',
36
+ props: {
37
+ tipText: {
38
+ type: [String, Object] as PropType<string | VNode>,
39
+ default: undefined,
40
+ },
41
+ neverRemindText: {
42
+ type: [String, Object] as PropType<string | VNode>,
43
+ default: undefined,
44
+ },
45
+ },
46
+ setup(props) {
47
+ const { visible, close, closeForever } = useControlTips();
48
+
49
+ return () => {
50
+ const displayContent =
51
+ props.tipText || I18n.t('Hold {{key}} to drag node out', { key: isMacOS ? 'Cmd ⌘' : 'Ctrl' });
52
+
53
+ if (!visible.value) {
54
+ return null;
55
+ }
56
+
57
+ return h('div', { class: 'sub-canvas-tips' }, [
58
+ h('div', { class: 'container' }, [
59
+ h('div', { class: 'content' }, [
60
+ typeof displayContent === 'string'
61
+ ? h('p', { class: 'text' }, displayContent)
62
+ : h('div', { class: 'custom-content' }, [displayContent]),
63
+ h('div', { class: 'space', style: { width: '0px' } }),
64
+ ]),
65
+ h('div', { class: 'actions' }, [
66
+ h(
67
+ 'p',
68
+ { class: 'close-forever', onClick: closeForever },
69
+ props.neverRemindText || I18n.t('Never Remind')
70
+ ),
71
+ h('div', { class: 'close', onClick: close }, [IconClose()]),
72
+ ]),
73
+ ]),
74
+ ]);
75
+ };
76
+ },
77
+ });
78
+ </script>
79
+
80
+ <style>
81
+ .sub-canvas-tips {
82
+ pointer-events: auto;
83
+ position: absolute;
84
+ top: 0;
85
+ width: 100%;
86
+ height: 28px;
87
+ }
88
+
89
+ .sub-canvas-tips .container {
90
+ height: 100%;
91
+ background-color: #e4e6f5;
92
+ border-radius: 4px 4px 0 0;
93
+ }
94
+
95
+ .sub-canvas-tips .content {
96
+ overflow: hidden;
97
+ display: inline-flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ width: 100%;
101
+ height: 100%;
102
+ }
103
+
104
+ .sub-canvas-tips .text {
105
+ font-size: 14px;
106
+ font-weight: 400;
107
+ font-style: normal;
108
+ line-height: 20px;
109
+ color: rgba(15, 21, 40, 82%);
110
+ text-overflow: ellipsis;
111
+ }
112
+
113
+ .sub-canvas-tips .custom-content {
114
+ display: flex;
115
+ align-items: center;
116
+ justify-content: center;
117
+ width: 100%;
118
+ height: 100%;
119
+ font-size: 14px;
120
+ font-weight: 400;
121
+ line-height: 20px;
122
+ color: rgba(15, 21, 40, 82%);
123
+ overflow: hidden;
124
+ }
125
+
126
+ .sub-canvas-tips .space {
127
+ width: 128px;
128
+ }
129
+
130
+ .sub-canvas-tips .actions {
131
+ position: absolute;
132
+ top: 0;
133
+ right: 0;
134
+ display: flex;
135
+ gap: 8px;
136
+ align-items: center;
137
+ height: 28px;
138
+ padding: 0 16px;
139
+ }
140
+
141
+ .sub-canvas-tips .close-forever {
142
+ cursor: pointer;
143
+ padding: 0 3px;
144
+ font-size: 12px;
145
+ font-weight: 400;
146
+ font-style: normal;
147
+ line-height: 12px;
148
+ color: rgba(32, 41, 69, 62%);
149
+ }
150
+
151
+ .sub-canvas-tips .close {
152
+ display: flex;
153
+ cursor: pointer;
154
+ height: 100%;
155
+ align-items: center;
156
+ }
157
+ </style>
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { onBeforeUnmount, ref } from 'vue';
7
+
8
+ import { useCurrentEntity } from '@flowgram-vue/free-layout-core';
9
+ import { useService } from '@flowgram-vue/core';
10
+
11
+ import { NodeIntoContainerService, NodeIntoContainerType } from '../../../node-into-container';
12
+ import { TipsGlobalStore } from './global-store';
13
+
14
+ export const useControlTips = () => {
15
+ const node = useCurrentEntity();
16
+ const visible = ref(false);
17
+ const globalStore = TipsGlobalStore.instance;
18
+
19
+ const nodeIntoContainerService = useService<NodeIntoContainerService>(NodeIntoContainerService);
20
+
21
+ const show = () => {
22
+ if (globalStore.isClosed()) {
23
+ return;
24
+ }
25
+ visible.value = true;
26
+ };
27
+
28
+ const close = () => {
29
+ globalStore.close();
30
+ visible.value = false;
31
+ };
32
+
33
+ const closeForever = () => {
34
+ globalStore.closeForever();
35
+ close();
36
+ };
37
+
38
+ const inDisposer = nodeIntoContainerService.on((e) => {
39
+ if (e.type !== NodeIntoContainerType.In) {
40
+ return;
41
+ }
42
+ if (e.targetContainer === node) {
43
+ show();
44
+ }
45
+ });
46
+ const outDisposer = nodeIntoContainerService.on((e) => {
47
+ if (e.type !== NodeIntoContainerType.Out) {
48
+ return;
49
+ }
50
+ if (e.sourceContainer === node && !node.blocks.length) {
51
+ visible.value = false;
52
+ }
53
+ });
54
+
55
+ onBeforeUnmount(() => {
56
+ inDisposer.dispose();
57
+ outDisposer.dispose();
58
+ });
59
+
60
+ return {
61
+ visible,
62
+ close,
63
+ closeForever,
64
+ };
65
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export { useNodeSize } from './use-node-size';
7
+ export type { NodeSize } from './use-node-size';
8
+ export { useSyncNodeRenderSize } from './use-sync-node-render-size';
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { onBeforeUnmount, ref } from 'vue';
7
+
8
+ import {
9
+ useCurrentEntity,
10
+ WorkflowNodeMeta,
11
+ WorkflowNodePortsData,
12
+ } from '@flowgram-vue/free-layout-core';
13
+ import { FlowNodeTransformData } from '@flowgram-vue/document';
14
+
15
+ export interface NodeSize {
16
+ width: number;
17
+ height: number;
18
+ }
19
+
20
+ export const useNodeSize = (): NodeSize | undefined => {
21
+ const node = useCurrentEntity();
22
+ const nodeMeta = node.getNodeMeta<WorkflowNodeMeta>();
23
+ const { size = { width: 300, height: 200 }, isContainer } = nodeMeta;
24
+
25
+ const transform = node.getData<FlowNodeTransformData>(FlowNodeTransformData);
26
+ const width = ref(size.width);
27
+ const height = ref(size.height);
28
+
29
+ const updatePorts = () => {
30
+ const portsData = node.getData<WorkflowNodePortsData>(WorkflowNodePortsData);
31
+ portsData.updateDynamicPorts();
32
+ };
33
+
34
+ const updateSize = () => {
35
+ // 无子节点时
36
+ if (node.blocks.length === 0) {
37
+ width.value = size.width;
38
+ height.value = size.height;
39
+ return;
40
+ }
41
+ // 存在子节点时,只监听宽高变化
42
+ width.value = transform.bounds.width;
43
+ height.value = transform.bounds.height;
44
+ };
45
+
46
+ const dispose = transform.onDataChange(() => {
47
+ updateSize();
48
+ updatePorts();
49
+ });
50
+
51
+ updateSize();
52
+
53
+ onBeforeUnmount(() => {
54
+ dispose.dispose();
55
+ });
56
+
57
+ if (!isContainer) {
58
+ return;
59
+ }
60
+
61
+ return {
62
+ get width() {
63
+ return width.value;
64
+ },
65
+ get height() {
66
+ return height.value;
67
+ },
68
+ };
69
+ };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { watch } from 'vue';
7
+
8
+ import { useCurrentEntity } from '@flowgram-vue/free-layout-core';
9
+
10
+ import { NodeSize } from './use-node-size';
11
+
12
+ export const useSyncNodeRenderSize = (nodeSize?: NodeSize) => {
13
+ const node = useCurrentEntity();
14
+
15
+ watch(
16
+ () => [nodeSize?.width, nodeSize?.height],
17
+ () => {
18
+ if (!nodeSize) {
19
+ return;
20
+ }
21
+ node.renderData.node.style.width = `${nodeSize.width}px`;
22
+ node.renderData.node.style.height = `${nodeSize.height}px`;
23
+ },
24
+ { immediate: true, flush: 'post' }
25
+ );
26
+ };
@@ -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 './hooks';
7
+ export * from './components';
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { IPoint, PaddingSchema } from '@flowgram-vue/utils';
7
+ import { WorkflowNodeEntity } from '@flowgram-vue/free-layout-core';
8
+ import { FlowNodeBaseType } from '@flowgram-vue/document';
9
+
10
+ /**
11
+ * 如果存在容器节点,且传入鼠标坐标,需要用容器的坐标减去传入的鼠标坐标
12
+ */
13
+ export const adjustSubNodePosition = (params: {
14
+ targetNode: WorkflowNodeEntity;
15
+ containerNode: WorkflowNodeEntity;
16
+ containerPadding: PaddingSchema;
17
+ }): IPoint => {
18
+ const { targetNode, containerNode, containerPadding } = params;
19
+ if (containerNode.flowNodeType === FlowNodeBaseType.ROOT) {
20
+ return targetNode.transform.position;
21
+ }
22
+ const nodeWorldTransform = targetNode.transform.transform.worldTransform;
23
+ const containerWorldTransform = containerNode.transform.transform.worldTransform;
24
+ const nodePosition = {
25
+ x: nodeWorldTransform.tx,
26
+ y: nodeWorldTransform.ty,
27
+ };
28
+ const isParentEmpty = !containerNode.children || containerNode.children.length === 0;
29
+ if (isParentEmpty) {
30
+ // 确保空容器节点不偏移
31
+ return {
32
+ x: 0,
33
+ y: containerPadding.top,
34
+ };
35
+ } else {
36
+ return {
37
+ x: nodePosition.x - containerWorldTransform.tx,
38
+ y: nodePosition.y - containerWorldTransform.ty,
39
+ };
40
+ }
41
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { PositionSchema, Rectangle } from '@flowgram-vue/utils';
7
+ import { WorkflowDocument } from '@flowgram-vue/free-layout-core';
8
+ import { FlowNodeTransformData } from '@flowgram-vue/document';
9
+
10
+ import { isRectIntersects } from './is-rect-intersects';
11
+ import { isPointInRect } from './is-point-in-rect';
12
+
13
+ /** 获取重叠位置 */
14
+ export const getCollisionTransform = (params: {
15
+ transforms: FlowNodeTransformData[];
16
+ targetRect?: Rectangle;
17
+ targetPoint?: PositionSchema;
18
+ withPadding?: boolean;
19
+ document: WorkflowDocument;
20
+ }): FlowNodeTransformData | undefined => {
21
+ const { targetRect, targetPoint, transforms, withPadding = false, document } = params;
22
+ const collisionTransform = transforms.find((transform) => {
23
+ const { bounds, entity } = transform;
24
+ const padding = withPadding ? document.layout.getPadding(entity) : { left: 0, right: 0 };
25
+ const transformRect = new Rectangle(
26
+ bounds.x + padding.left + padding.right,
27
+ bounds.y,
28
+ bounds.width,
29
+ bounds.height
30
+ );
31
+ // 检测两个正方形是否相互碰撞
32
+ if (targetRect) {
33
+ return isRectIntersects(targetRect, transformRect);
34
+ }
35
+ if (targetPoint) {
36
+ return isPointInRect(targetPoint, transformRect);
37
+ }
38
+ return false;
39
+ });
40
+ return collisionTransform;
41
+ };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { WorkflowNodeEntity } from '@flowgram-vue/free-layout-core';
7
+ import { FlowNodeTransformData } from '@flowgram-vue/document';
8
+
9
+ import { isContainer } from './is-container';
10
+
11
+ /** 获取容器节点transforms */
12
+ export const getContainerTransforms = (allNodes: WorkflowNodeEntity[]): FlowNodeTransformData[] =>
13
+ allNodes
14
+ .filter((node) => {
15
+ if (node.originParent) {
16
+ return node.getNodeMeta().selectable && node.originParent.getNodeMeta().selectable;
17
+ }
18
+ return node.getNodeMeta().selectable;
19
+ })
20
+ .filter((node) => isContainer(node))
21
+ .sort((a, b) => {
22
+ const aIndex = a.renderData.stackIndex;
23
+ const bIndex = b.renderData.stackIndex;
24
+ // 确保层级高的节点在前面
25
+ return bIndex - aIndex;
26
+ })
27
+ .map((node) => node.transform);
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { nextFrame } from './next-frame';
7
+ import { isContainer } from './is-container';
8
+ import { getContainerTransforms } from './get-container-transforms';
9
+ import { getCollisionTransform } from './get-collision-transform';
10
+ import { adjustSubNodePosition } from './adjust-sub-node-position';
11
+
12
+ export const ContainerUtils = {
13
+ nextFrame,
14
+ isContainer,
15
+ adjustSubNodePosition,
16
+ getContainerTransforms,
17
+ getCollisionTransform,
18
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { WorkflowNodeEntity, WorkflowNodeMeta } from '@flowgram-vue/free-layout-core';
7
+
8
+ export const isContainer = (node?: WorkflowNodeEntity): boolean =>
9
+ node?.getNodeMeta<WorkflowNodeMeta>().isContainer ?? false;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { PositionSchema, Rectangle } from '@flowgram-vue/utils';
7
+
8
+ export const isPointInRect = (point: PositionSchema, rect: Rectangle): boolean =>
9
+ point.x >= rect.left && point.x <= rect.right && point.y >= rect.top && point.y <= rect.bottom;
@@ -0,0 +1,15 @@
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
+ export const isRectIntersects = (rectA: Rectangle, rectB: Rectangle): boolean => {
9
+ // 检查水平方向是否有重叠
10
+ const hasHorizontalOverlap = rectA.right > rectB.left && rectA.left < rectB.right;
11
+ // 检查垂直方向是否有重叠
12
+ const hasVerticalOverlap = rectA.bottom > rectB.top && rectA.top < rectB.bottom;
13
+ // 只有当水平和垂直方向都有重叠时,两个矩形才相交
14
+ return hasHorizontalOverlap && hasVerticalOverlap;
15
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export const nextFrame = async (): Promise<void> => {
7
+ await new Promise((resolve) => requestAnimationFrame(resolve));
8
+ };