@yh-ui/flow 1.0.51 → 1.0.52

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.
@@ -1,4 +1,5 @@
1
1
  import { ref, computed } from "vue";
2
+ import { getNodeAbsolutePosition } from "../utils/custom-types.mjs";
2
3
  export function useSelection(options) {
3
4
  const { nodes, edges, onSelectionChange } = options;
4
5
  const selectionRect = ref(null);
@@ -23,10 +24,12 @@ export function useSelection(options) {
23
24
  width: maxX - minX,
24
25
  height: maxY - minY
25
26
  };
27
+ const nodeMap = new Map(nodes.value.map((n) => [n.id, n]));
26
28
  nodes.value = nodes.value.map((node) => {
27
- const nodeWidth = node.width || 200;
28
- const nodeHeight = node.height || 50;
29
- const isInside = node.position.x >= minX && node.position.y >= minY && node.position.x + nodeWidth <= maxX && node.position.y + nodeHeight <= maxY;
29
+ const nodeWidth = node.measured?.width ?? node.width ?? 200;
30
+ const nodeHeight = node.measured?.height ?? node.height ?? 50;
31
+ const absPos = getNodeAbsolutePosition(node, nodeMap);
32
+ const isInside = absPos.x >= minX && absPos.y >= minY && absPos.x + nodeWidth <= maxX && absPos.y + nodeHeight <= maxY;
30
33
  return { ...node, selected: isInside };
31
34
  });
32
35
  };
@@ -86,8 +86,8 @@ function useViewport(initialTransform, options = {}) {
86
86
  let maxX = -Infinity;
87
87
  let maxY = -Infinity;
88
88
  for (const node of nodes) {
89
- const w = node.width || 200;
90
- const h = node.height || 50;
89
+ const w = node.measured?.width ?? node.width ?? 200;
90
+ const h = node.measured?.height ?? node.height ?? 50;
91
91
  minX = Math.min(minX, node.position.x);
92
92
  minY = Math.min(minY, node.position.y);
93
93
  maxX = Math.max(maxX, node.position.x + w);
@@ -112,8 +112,8 @@ function useViewport(initialTransform, options = {}) {
112
112
  let centerX = 0;
113
113
  let centerY = 0;
114
114
  for (const node of nodes) {
115
- const w = node.width || 200;
116
- const h = node.height || 50;
115
+ const w = node.measured?.width ?? node.width ?? 200;
116
+ const h = node.measured?.height ?? node.height ?? 50;
117
117
  centerX += node.position.x + w / 2;
118
118
  centerY += node.position.y + h / 2;
119
119
  }
@@ -28,6 +28,10 @@ export interface UseViewportReturn {
28
28
  };
29
29
  width?: number;
30
30
  height?: number;
31
+ measured?: {
32
+ width: number;
33
+ height: number;
34
+ };
31
35
  }[], options?: {
32
36
  padding?: number;
33
37
  }) => void;
@@ -41,6 +45,10 @@ export interface UseViewportReturn {
41
45
  };
42
46
  width?: number;
43
47
  height?: number;
48
+ measured?: {
49
+ width: number;
50
+ height: number;
51
+ };
44
52
  }[], options?: {
45
53
  padding?: number;
46
54
  }) => void;
@@ -58,8 +58,8 @@ export function useViewport(initialTransform, options = {}) {
58
58
  let maxX = -Infinity;
59
59
  let maxY = -Infinity;
60
60
  for (const node of nodes) {
61
- const w = node.width || 200;
62
- const h = node.height || 50;
61
+ const w = node.measured?.width ?? node.width ?? 200;
62
+ const h = node.measured?.height ?? node.height ?? 50;
63
63
  minX = Math.min(minX, node.position.x);
64
64
  minY = Math.min(minY, node.position.y);
65
65
  maxX = Math.max(maxX, node.position.x + w);
@@ -80,8 +80,8 @@ export function useViewport(initialTransform, options = {}) {
80
80
  let centerX = 0;
81
81
  let centerY = 0;
82
82
  for (const node of nodes) {
83
- const w = node.width || 200;
84
- const h = node.height || 50;
83
+ const w = node.measured?.width ?? node.width ?? 200;
84
+ const h = node.measured?.height ?? node.height ?? 50;
85
85
  centerX += node.position.x + w / 2;
86
86
  centerY += node.position.y + h / 2;
87
87
  }
@@ -34,8 +34,8 @@ async function applyDagreLayout(nodes, edges, options) {
34
34
  g.setDefaultEdgeLabel(() => ({}));
35
35
  nodes.forEach(node => {
36
36
  g.setNode(node.id, {
37
- width: node.width || 150,
38
- height: node.height || 50
37
+ width: node.measured?.width ?? node.width ?? 150,
38
+ height: node.measured?.height ?? node.height ?? 50
39
39
  });
40
40
  });
41
41
  edges.forEach(edge => {
@@ -48,8 +48,8 @@ async function applyDagreLayout(nodes, edges, options) {
48
48
  return {
49
49
  ...node,
50
50
  position: {
51
- x: layoutNode.x - (node.width || 150) / 2,
52
- y: layoutNode.y - (node.height || 50) / 2
51
+ x: layoutNode.x - (node.measured?.width ?? node.width ?? 150) / 2,
52
+ y: layoutNode.y - (node.measured?.height ?? node.height ?? 50) / 2
53
53
  }
54
54
  };
55
55
  });
@@ -88,8 +88,8 @@ async function applyElkLayout(nodes, edges, options) {
88
88
  },
89
89
  children: nodes.map(node => ({
90
90
  id: node.id,
91
- width: node.width || 150,
92
- height: node.height || 50
91
+ width: node.measured?.width ?? node.width ?? 150,
92
+ height: node.measured?.height ?? node.height ?? 50
93
93
  })),
94
94
  edges: edges.map((edge, index) => ({
95
95
  id: edge.id || `edge-${index}`,
@@ -26,8 +26,8 @@ async function applyDagreLayout(nodes, edges, options) {
26
26
  g.setDefaultEdgeLabel(() => ({}));
27
27
  nodes.forEach((node) => {
28
28
  g.setNode(node.id, {
29
- width: node.width || 150,
30
- height: node.height || 50
29
+ width: node.measured?.width ?? node.width ?? 150,
30
+ height: node.measured?.height ?? node.height ?? 50
31
31
  });
32
32
  });
33
33
  edges.forEach((edge) => {
@@ -40,8 +40,8 @@ async function applyDagreLayout(nodes, edges, options) {
40
40
  return {
41
41
  ...node,
42
42
  position: {
43
- x: layoutNode.x - (node.width || 150) / 2,
44
- y: layoutNode.y - (node.height || 50) / 2
43
+ x: layoutNode.x - (node.measured?.width ?? node.width ?? 150) / 2,
44
+ y: layoutNode.y - (node.measured?.height ?? node.height ?? 50) / 2
45
45
  }
46
46
  };
47
47
  });
@@ -81,8 +81,8 @@ async function applyElkLayout(nodes, edges, options) {
81
81
  },
82
82
  children: nodes.map((node) => ({
83
83
  id: node.id,
84
- width: node.width || 150,
85
- height: node.height || 50
84
+ width: node.measured?.width ?? node.width ?? 150,
85
+ height: node.measured?.height ?? node.height ?? 50
86
86
  })),
87
87
  edges: edges.map((edge, index) => ({
88
88
  id: edge.id || `edge-${index}`,
@@ -24,8 +24,8 @@ function computeBoundingBox(nodes, paddingX, paddingY) {
24
24
  maxX = -Infinity,
25
25
  maxY = -Infinity;
26
26
  nodes.forEach(node => {
27
- const w = node.width || 150;
28
- const h = node.height || 50;
27
+ const w = node.measured?.width ?? node.width ?? 150;
28
+ const h = node.measured?.height ?? node.height ?? 50;
29
29
  minX = Math.min(minX, node.position.x);
30
30
  minY = Math.min(minY, node.position.y);
31
31
  maxX = Math.max(maxX, node.position.x + w);
@@ -165,7 +165,15 @@ function createNodeGroupPlugin(options = {}) {
165
165
  }
166
166
  });
167
167
  if (hidden) {
168
+ const groupNode = flow.getNode(groupId);
169
+ const currentW = groupNode?.width ?? groupNode?.style?.width ?? 200;
170
+ const currentH = groupNode?.height ?? groupNode?.style?.height ?? 150;
168
171
  flow.updateNode(groupId, {
172
+ data: {
173
+ ...(groupNode?.data || {}),
174
+ _origWidth: currentW,
175
+ _origHeight: currentH
176
+ },
169
177
  style: {
170
178
  width: 160,
171
179
  height: 50,
@@ -10,8 +10,8 @@ function computeBoundingBox(nodes, paddingX, paddingY) {
10
10
  if (nodes.length === 0) return { x: 0, y: 0, width: 200, height: 150 };
11
11
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
12
12
  nodes.forEach((node) => {
13
- const w = node.width || 150;
14
- const h = node.height || 50;
13
+ const w = node.measured?.width ?? node.width ?? 150;
14
+ const h = node.measured?.height ?? node.height ?? 50;
15
15
  minX = Math.min(minX, node.position.x);
16
16
  minY = Math.min(minY, node.position.y);
17
17
  maxX = Math.max(maxX, node.position.x + w);
@@ -139,7 +139,15 @@ export function createNodeGroupPlugin(options = {}) {
139
139
  }
140
140
  });
141
141
  if (hidden) {
142
+ const groupNode = flow.getNode(groupId);
143
+ const currentW = groupNode?.width ?? groupNode?.style?.width ?? 200;
144
+ const currentH = groupNode?.height ?? groupNode?.style?.height ?? 150;
142
145
  flow.updateNode(groupId, {
146
+ data: {
147
+ ...groupNode?.data || {},
148
+ _origWidth: currentW,
149
+ _origHeight: currentH
150
+ },
143
151
  style: { width: 160, height: 50, minWidth: 160, minHeight: 50 },
144
152
  width: 160,
145
153
  height: 50
@@ -32,6 +32,7 @@
32
32
  <script setup>
33
33
  import { computed } from "vue";
34
34
  import { useFlowContext } from "../core/FlowContext";
35
+ import { getNodeAbsolutePosition } from "../utils/custom-types";
35
36
  const props = defineProps({
36
37
  snapThreshold: { type: Number, required: false }
37
38
  });
@@ -42,6 +43,9 @@ const draggingPosition = flowInstance.draggingPosition;
42
43
  const containerWidth = computed(() => flowInstance.$el?.clientWidth || 800);
43
44
  const containerHeight = computed(() => flowInstance.$el?.clientHeight || 600);
44
45
  const SNAP_THRESHOLD = props.snapThreshold ?? 10;
46
+ const nodeMap = computed(() => {
47
+ return new Map(nodes.value.map((n) => [n.id, n]));
48
+ });
45
49
  const horizontalLines = computed(() => {
46
50
  const dNodeId = draggingNodeId.value;
47
51
  const dPos = draggingPosition.value;
@@ -49,20 +53,23 @@ const horizontalLines = computed(() => {
49
53
  const lines = [];
50
54
  const draggingNode = nodes.value.find((n) => n.id === dNodeId);
51
55
  if (!draggingNode) return [];
52
- const nodeHeight = draggingNode.height || 50;
56
+ const nodeHeight = draggingNode.measured?.height ?? draggingNode.height ?? 50;
53
57
  const otherNodes = nodes.value.filter((n) => n.id !== dNodeId && !n.hidden);
58
+ const m = nodeMap.value;
59
+ const draggingNodeAbs = getNodeAbsolutePosition(draggingNode, m);
54
60
  const positions = [
55
- { y: draggingNode.position.y, key: "top" },
56
- { y: draggingNode.position.y + nodeHeight / 2, key: "center" },
57
- { y: draggingNode.position.y + nodeHeight, key: "bottom" }
61
+ { y: draggingNodeAbs.y, key: "top" },
62
+ { y: draggingNodeAbs.y + nodeHeight / 2, key: "center" },
63
+ { y: draggingNodeAbs.y + nodeHeight, key: "bottom" }
58
64
  ];
59
65
  for (const pos of positions) {
60
66
  for (const node of otherNodes) {
61
- const nHeight = node.height || 50;
67
+ const nHeight = node.measured?.height ?? node.height ?? 50;
68
+ const otherAbs = getNodeAbsolutePosition(node, m);
62
69
  const nodePositions = [
63
- { y: node.position.y, key: "top" },
64
- { y: node.position.y + nHeight / 2, key: "center" },
65
- { y: node.position.y + nHeight, key: "bottom" }
70
+ { y: otherAbs.y, key: "top" },
71
+ { y: otherAbs.y + nHeight / 2, key: "center" },
72
+ { y: otherAbs.y + nHeight, key: "bottom" }
66
73
  ];
67
74
  for (const np of nodePositions) {
68
75
  if (Math.abs(pos.y - np.y) < SNAP_THRESHOLD) {
@@ -81,20 +88,23 @@ const verticalLines = computed(() => {
81
88
  const lines = [];
82
89
  const draggingNode = nodes.value.find((n) => n.id === dNodeId);
83
90
  if (!draggingNode) return [];
84
- const nodeWidth = draggingNode.width || 200;
91
+ const nodeWidth = draggingNode.measured?.width ?? draggingNode.width ?? 200;
85
92
  const otherNodes = nodes.value.filter((n) => n.id !== dNodeId && !n.hidden);
93
+ const m = nodeMap.value;
94
+ const draggingNodeAbs = getNodeAbsolutePosition(draggingNode, m);
86
95
  const positions = [
87
- { x: draggingNode.position.x, key: "left" },
88
- { x: draggingNode.position.x + nodeWidth / 2, key: "center" },
89
- { x: draggingNode.position.x + nodeWidth, key: "right" }
96
+ { x: draggingNodeAbs.x, key: "left" },
97
+ { x: draggingNodeAbs.x + nodeWidth / 2, key: "center" },
98
+ { x: draggingNodeAbs.x + nodeWidth, key: "right" }
90
99
  ];
91
100
  for (const pos of positions) {
92
101
  for (const node of otherNodes) {
93
- const nWidth = node.width || 200;
102
+ const nWidth = node.measured?.width ?? node.width ?? 200;
103
+ const otherAbs = getNodeAbsolutePosition(node, m);
94
104
  const nodePositions = [
95
- { x: node.position.x, key: "left" },
96
- { x: node.position.x + nWidth / 2, key: "center" },
97
- { x: node.position.x + nWidth, key: "right" }
105
+ { x: otherAbs.x, key: "left" },
106
+ { x: otherAbs.x + nWidth / 2, key: "center" },
107
+ { x: otherAbs.x + nWidth, key: "right" }
98
108
  ];
99
109
  for (const np of nodePositions) {
100
110
  if (Math.abs(pos.x - np.x) < SNAP_THRESHOLD) {
@@ -40,6 +40,7 @@
40
40
  <script setup>
41
41
  import { computed } from "vue";
42
42
  import { getHandlePosition } from "../utils/edge";
43
+ import { getNodeAbsolutePosition } from "../utils/custom-types";
43
44
  const props = defineProps({
44
45
  edges: { type: Array, required: true },
45
46
  nodes: { type: Array, required: true }
@@ -59,8 +60,16 @@ const selectedUpdatableEdges = computed(() => {
59
60
  if (!sourceNode || !targetNode) continue;
60
61
  const sPosDesc = edge.sourceHandle || "right";
61
62
  const tPosDesc = edge.targetHandle || "left";
62
- const sPos = getHandlePosition(sourceNode, sPosDesc, edge.sourceHandle);
63
- const tPos = getHandlePosition(targetNode, tPosDesc, edge.targetHandle);
63
+ const sAbsNode = {
64
+ ...sourceNode,
65
+ position: getNodeAbsolutePosition(sourceNode, nodeMap.value)
66
+ };
67
+ const tAbsNode = {
68
+ ...targetNode,
69
+ position: getNodeAbsolutePosition(targetNode, nodeMap.value)
70
+ };
71
+ const sPos = getHandlePosition(sAbsNode, sPosDesc, edge.sourceHandle);
72
+ const tPos = getHandlePosition(tAbsNode, tPosDesc, edge.targetHandle);
64
73
  result.push({
65
74
  edge,
66
75
  sourceX: sPos.x,
@@ -111,7 +111,7 @@
111
111
  <script setup>
112
112
  import { computed } from "vue";
113
113
  import { getEdgePath, getEdgeCenter, getHandlePosition } from "../utils/edge";
114
- import { getCustomEdge } from "../utils/custom-types";
114
+ import { getCustomEdge, getNodeAbsolutePosition } from "../utils/custom-types";
115
115
  const props = defineProps({
116
116
  flowId: { type: String, required: true },
117
117
  edges: { type: Array, required: true },
@@ -160,8 +160,10 @@ const edgeData = computed(() => {
160
160
  if (!sourceNode || !targetNode) continue;
161
161
  const sPosDesc = edge.sourceHandle || "right";
162
162
  const tPosDesc = edge.targetHandle || "left";
163
- const sPos = getHandlePosition(sourceNode, sPosDesc, edge.sourceHandle);
164
- const tPos = getHandlePosition(targetNode, tPosDesc, edge.targetHandle);
163
+ const sAbsNode = { ...sourceNode, position: getNodeAbsolutePosition(sourceNode, nodeMap.value) };
164
+ const tAbsNode = { ...targetNode, position: getNodeAbsolutePosition(targetNode, nodeMap.value) };
165
+ const sPos = getHandlePosition(sAbsNode, sPosDesc, edge.sourceHandle);
166
+ const tPos = getHandlePosition(tAbsNode, tPosDesc, edge.targetHandle);
165
167
  const pathParams = {
166
168
  sourceX: sPos.x,
167
169
  sourceY: sPos.y,
@@ -94,6 +94,7 @@
94
94
  <script setup>
95
95
  import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue";
96
96
  import { useFlowContext } from "../core/FlowContext";
97
+ import { getNodeAbsolutePosition } from "../utils/custom-types";
97
98
  const props = defineProps({
98
99
  nodeColor: { type: [String, Function], required: false },
99
100
  nodeStrokeColor: { type: [String, Function], required: false },
@@ -120,6 +121,11 @@ const flowInstance = useFlowContext();
120
121
  const nodes = flowInstance.nodes;
121
122
  const edges = flowInstance.edges;
122
123
  const viewport = flowInstance.viewport;
124
+ const nodeMap = computed(() => {
125
+ const m = /* @__PURE__ */ new Map();
126
+ nodes.value.forEach((n) => m.set(n.id, n));
127
+ return m;
128
+ });
123
129
  let _mapScale = 1;
124
130
  let _minX = 0;
125
131
  let _minY = 0;
@@ -129,15 +135,17 @@ const computeGraphBounds = () => {
129
135
  const currentNodes = nodes.value;
130
136
  if (!currentNodes.length) return { minX: 0, minY: 0, maxX: 1, maxY: 1 };
131
137
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
138
+ const m = nodeMap.value;
132
139
  for (let i = 0; i < currentNodes.length; i++) {
133
140
  const n = currentNodes[i];
134
141
  if (n.hidden) continue;
135
- const nw = n.width || 120;
136
- const nh = n.height || 50;
137
- if (n.position.x < minX) minX = n.position.x;
138
- if (n.position.y < minY) minY = n.position.y;
139
- if (n.position.x + nw > maxX) maxX = n.position.x + nw;
140
- if (n.position.y + nh > maxY) maxY = n.position.y + nh;
142
+ const nw = n.measured?.width ?? n.width ?? 120;
143
+ const nh = n.measured?.height ?? n.height ?? 50;
144
+ const absPos = getNodeAbsolutePosition(n, m);
145
+ if (absPos.x < minX) minX = absPos.x;
146
+ if (absPos.y < minY) minY = absPos.y;
147
+ if (absPos.x + nw > maxX) maxX = absPos.x + nw;
148
+ if (absPos.y + nh > maxY) maxY = absPos.y + nh;
141
149
  }
142
150
  if (minX === Infinity) return { minX: 0, minY: 0, maxX: 1, maxY: 1 };
143
151
  return { minX, minY, maxX, maxY };
@@ -163,8 +171,7 @@ const drawCanvas = () => {
163
171
  updateVpRect(viewport.value);
164
172
  return;
165
173
  }
166
- const nodeMap = /* @__PURE__ */ new Map();
167
- nodes.value.forEach((n) => nodeMap.set(n.id, n));
174
+ const nodeMapVal = nodeMap.value;
168
175
  const rootStyles = getComputedStyle(canvas);
169
176
  const themeEdgeColor = rootStyles.getPropertyValue("--flow-edge-stroke").trim() || "#94a3b8";
170
177
  const themeMinimapNodeColor = rootStyles.getPropertyValue("--flow-minimap-node-color").trim() || "#cbd5e1";
@@ -175,16 +182,18 @@ const drawCanvas = () => {
175
182
  ctx.beginPath();
176
183
  edges.value.forEach((e) => {
177
184
  if (e.hidden) return;
178
- const src = nodeMap.get(e.source);
179
- const tgt = nodeMap.get(e.target);
185
+ const src = nodeMapVal.get(e.source);
186
+ const tgt = nodeMapVal.get(e.target);
180
187
  if (!src || !tgt) return;
188
+ const srcAbs = getNodeAbsolutePosition(src, nodeMapVal);
189
+ const tgtAbs = getNodeAbsolutePosition(tgt, nodeMapVal);
181
190
  ctx.moveTo(
182
- toMX(src.position.x + (src.width || 120) / 2),
183
- toMY(src.position.y + (src.height || 50) / 2)
191
+ toMX(srcAbs.x + (src.measured?.width ?? src.width ?? 120) / 2),
192
+ toMY(srcAbs.y + (src.measured?.height ?? src.height ?? 50) / 2)
184
193
  );
185
194
  ctx.lineTo(
186
- toMX(tgt.position.x + (tgt.width || 120) / 2),
187
- toMY(tgt.position.y + (tgt.height || 50) / 2)
195
+ toMX(tgtAbs.x + (tgt.measured?.width ?? tgt.width ?? 120) / 2),
196
+ toMY(tgtAbs.y + (tgt.measured?.height ?? tgt.height ?? 50) / 2)
188
197
  );
189
198
  });
190
199
  ctx.stroke();
@@ -196,10 +205,11 @@ const drawCanvas = () => {
196
205
  color = typeof props.nodeColor === "function" ? props.nodeColor(n) : props.nodeColor;
197
206
  }
198
207
  ctx.fillStyle = color;
199
- const nw = Math.max((n.width || 120) * _mapScale, 3);
200
- const nh = Math.max((n.height || 50) * _mapScale, 3);
201
- const nx = toMX(n.position.x);
202
- const ny = toMY(n.position.y);
208
+ const absPos = getNodeAbsolutePosition(n, nodeMapVal);
209
+ const nw = Math.max((n.measured?.width ?? n.width ?? 120) * _mapScale, 3);
210
+ const nh = Math.max((n.measured?.height ?? n.height ?? 50) * _mapScale, 3);
211
+ const nx = toMX(absPos.x);
212
+ const ny = toMY(absPos.y);
203
213
  ctx.fillRect(nx, ny, nw, nh);
204
214
  if (props.nodeStrokeWidth || props.nodeStrokeColor) {
205
215
  let strokeColor = "#94a3b8";
@@ -30,6 +30,7 @@ declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}
30
30
  "node-drag-end": (event: MouseEvent, node: Node<import("../types").NodeData>) => any;
31
31
  "connect-start": (event: MouseEvent, nodeId: string, handleId: string, handleType: HandleType) => any;
32
32
  "node-select-toggle": (nodeId: string) => any;
33
+ "nodes-measured": () => any;
33
34
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
34
35
  "onNode-click"?: ((event: MouseEvent, node: Node<import("../types").NodeData>) => any) | undefined;
35
36
  "onNode-dblclick"?: ((event: MouseEvent, node: Node<import("../types").NodeData>) => any) | undefined;
@@ -42,6 +43,7 @@ declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}
42
43
  "onNode-drag-end"?: ((event: MouseEvent, node: Node<import("../types").NodeData>) => any) | undefined;
43
44
  "onConnect-start"?: ((event: MouseEvent, nodeId: string, handleId: string, handleType: HandleType) => any) | undefined;
44
45
  "onNode-select-toggle"?: ((nodeId: string) => any) | undefined;
46
+ "onNodes-measured"?: (() => any) | undefined;
45
47
  }>, {
46
48
  draggable: boolean;
47
49
  connectable: boolean;
@@ -4,6 +4,7 @@
4
4
  v-for="node in visibleNodes"
5
5
  :key="node.id"
6
6
  :id="getNodeDomId(node.id)"
7
+ :ref="el => setNodeRef(el, node.id)"
7
8
  class="yh-flow-node"
8
9
  :class="{
9
10
  'is-selected': node.selected,
@@ -63,8 +64,12 @@
63
64
  </template>
64
65
 
65
66
  <script setup>
66
- import { computed, ref } from "vue";
67
- import { getCustomNodeTemplate, getCustomNode } from "../utils/custom-types";
67
+ import { computed, ref, onMounted, onBeforeUnmount } from "vue";
68
+ import {
69
+ getCustomNodeTemplate,
70
+ getCustomNode,
71
+ getNodeAbsolutePosition
72
+ } from "../utils/custom-types";
68
73
  import DiamondNode from "../components/nodes/DiamondNode.vue";
69
74
  import DatabaseNode from "../components/nodes/DatabaseNode.vue";
70
75
  const props = defineProps({
@@ -76,7 +81,7 @@ const props = defineProps({
76
81
  connectable: { type: Boolean, required: false, default: true },
77
82
  readonly: { type: Boolean, required: false, default: false }
78
83
  });
79
- const emit = defineEmits(["node-click", "node-dblclick", "node-contextmenu", "node-drag-start", "node-drag", "node-drag-end", "connect-start", "node-select-toggle"]);
84
+ const emit = defineEmits(["node-click", "node-dblclick", "node-contextmenu", "node-drag-start", "node-drag", "node-drag-end", "connect-start", "node-select-toggle", "nodes-measured"]);
80
85
  const getComponent = (type) => {
81
86
  if (type === "diamond") return DiamondNode;
82
87
  if (type === "database") return DatabaseNode;
@@ -89,24 +94,33 @@ const visibleNodes = computed(() => {
89
94
  return props.nodes.filter((n) => !n.hidden);
90
95
  });
91
96
  const getNodeDomId = (nodeId) => `${props.flowId}-node-${nodeId}`;
97
+ const nodeMap = computed(() => {
98
+ const m = /* @__PURE__ */ new Map();
99
+ props.nodes.forEach((n) => m.set(n.id, n));
100
+ return m;
101
+ });
92
102
  const getNodeStyle = (node) => {
93
- const width = node.width || 150;
94
- const height = node.height || 40;
95
103
  let zIndex = node.zIndex || 10;
96
104
  if (node.type === "group") {
97
105
  zIndex = node.selected ? 2 : 1;
98
106
  } else {
99
107
  zIndex = node.selected ? 100 : Math.max(10, zIndex);
100
108
  }
101
- return {
102
- transform: `translate(${node.position.x}px, ${node.position.y}px)`,
103
- width: `${width}px`,
104
- height: `${height}px`,
109
+ const absPos = getNodeAbsolutePosition(node, nodeMap.value);
110
+ const style = {
111
+ transform: `translate(${absPos.x}px, ${absPos.y}px)`,
105
112
  zIndex,
106
113
  "--flow-node-label-color": node.labelColor,
107
114
  "--flow-node-description-color": node.descriptionColor,
108
115
  ...node.style
109
116
  };
117
+ if (node.width !== void 0 && node.width !== null) {
118
+ style.width = `${node.width}px`;
119
+ }
120
+ if (node.height !== void 0 && node.height !== null) {
121
+ style.height = `${node.height}px`;
122
+ }
123
+ return style;
110
124
  };
111
125
  const getComponentProps = (node) => {
112
126
  const {
@@ -278,16 +292,18 @@ const handleMouseMove = (event) => {
278
292
  const dx = (event.clientX - dragStartPos.value.x) / props.transform.zoom;
279
293
  const dy = (event.clientY - dragStartPos.value.y) / props.transform.zoom;
280
294
  draggingNodes.value.forEach((nodeId) => {
295
+ const node = props.nodes.find((n) => n.id === nodeId);
296
+ if (!node) return;
297
+ if (node.parentId && draggingNodes.value.includes(node.parentId)) {
298
+ return;
299
+ }
281
300
  const startPos = nodesStartPositions.value.get(nodeId);
282
301
  if (startPos) {
283
302
  const newPosition = {
284
303
  x: startPos.x + dx,
285
304
  y: startPos.y + dy
286
305
  };
287
- const node = props.nodes.find((n) => n.id === nodeId);
288
- if (node) {
289
- emit("node-drag", event, node, newPosition);
290
- }
306
+ emit("node-drag", event, node, newPosition);
291
307
  }
292
308
  });
293
309
  };
@@ -324,6 +340,51 @@ const handleNodeContextMenu = (event, node) => {
324
340
  const handleConnectStart = (event, node, handle) => {
325
341
  emit("connect-start", event, node.id, handle.id || handle.position || "", handle.type);
326
342
  };
343
+ const nodeElements = /* @__PURE__ */ new Map();
344
+ let resizeObserver = null;
345
+ onMounted(() => {
346
+ resizeObserver = new ResizeObserver((entries) => {
347
+ let hasChanges = false;
348
+ for (const entry of entries) {
349
+ const target = entry.target;
350
+ const nodeId = target.getAttribute("data-node-id");
351
+ if (nodeId) {
352
+ const rect = target.getBoundingClientRect();
353
+ const width = Math.round(rect.width);
354
+ const height = Math.round(rect.height);
355
+ const node = props.nodes.find((n) => n.id === nodeId);
356
+ if (node) {
357
+ if (!node.measured || node.measured.width !== width || node.measured.height !== height) {
358
+ node.measured = { width, height };
359
+ hasChanges = true;
360
+ }
361
+ }
362
+ }
363
+ }
364
+ if (hasChanges) {
365
+ emit("nodes-measured");
366
+ }
367
+ });
368
+ nodeElements.forEach((el) => resizeObserver?.observe(el));
369
+ });
370
+ onBeforeUnmount(() => {
371
+ if (resizeObserver) {
372
+ resizeObserver.disconnect();
373
+ }
374
+ nodeElements.clear();
375
+ });
376
+ const setNodeRef = (el, nodeId) => {
377
+ if (el) {
378
+ nodeElements.set(nodeId, el);
379
+ resizeObserver?.observe(el);
380
+ } else {
381
+ const existingEl = nodeElements.get(nodeId);
382
+ if (existingEl) {
383
+ resizeObserver?.unobserve(existingEl);
384
+ nodeElements.delete(nodeId);
385
+ }
386
+ }
387
+ };
327
388
  </script>
328
389
 
329
390
  <style scoped>
@@ -30,6 +30,7 @@ declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}
30
30
  "node-drag-end": (event: MouseEvent, node: Node<import("../types").NodeData>) => any;
31
31
  "connect-start": (event: MouseEvent, nodeId: string, handleId: string, handleType: HandleType) => any;
32
32
  "node-select-toggle": (nodeId: string) => any;
33
+ "nodes-measured": () => any;
33
34
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
34
35
  "onNode-click"?: ((event: MouseEvent, node: Node<import("../types").NodeData>) => any) | undefined;
35
36
  "onNode-dblclick"?: ((event: MouseEvent, node: Node<import("../types").NodeData>) => any) | undefined;
@@ -42,6 +43,7 @@ declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}
42
43
  "onNode-drag-end"?: ((event: MouseEvent, node: Node<import("../types").NodeData>) => any) | undefined;
43
44
  "onConnect-start"?: ((event: MouseEvent, nodeId: string, handleId: string, handleType: HandleType) => any) | undefined;
44
45
  "onNode-select-toggle"?: ((nodeId: string) => any) | undefined;
46
+ "onNodes-measured"?: (() => any) | undefined;
45
47
  }>, {
46
48
  draggable: boolean;
47
49
  connectable: boolean;
@@ -54,6 +54,10 @@ export interface Node<Data = NodeData> {
54
54
  dragging?: boolean;
55
55
  width?: number;
56
56
  height?: number;
57
+ measured?: {
58
+ width: number;
59
+ height: number;
60
+ };
57
61
  parentId?: string;
58
62
  zIndex?: number;
59
63
  extent?: 'parent' | string;
@@ -262,6 +266,29 @@ export interface FlowInstance {
262
266
  /** Screenshot: pass options or legacy HTMLElement as container. Returns result with dataUrl/blob. */
263
267
  exportImage?: (options?: ScreenshotOptions | HTMLElement) => Promise<ScreenshotResult>;
264
268
  applyLayout?: (options?: unknown) => void | Promise<void>;
269
+ groupSelectedNodes?: (label?: string) => string | null;
270
+ ungroupNodes?: (groupId: string) => void;
271
+ toggleGroupCollapse?: (groupId: string) => void;
272
+ isGroupCollapsed?: (groupId: string) => boolean;
273
+ getGroupChildren?: (groupId: string) => Node[];
274
+ getGroupRegistry?: () => Map<string, {
275
+ groupId: string;
276
+ childIds: string[];
277
+ collapsed: boolean;
278
+ originalPositions: Record<string, {
279
+ x: number;
280
+ y: number;
281
+ }>;
282
+ }>;
283
+ undo?: () => void;
284
+ redo?: () => void;
285
+ saveSnapshot?: (description?: string) => void;
286
+ canUndo?: Ref<boolean>;
287
+ canRedo?: Ref<boolean>;
288
+ historyLength?: Ref<number>;
289
+ clearHistory?: () => void;
290
+ getHistory?: () => unknown[];
291
+ jumpToStep?: (index: number) => void;
265
292
  }
266
293
  /** 截图范围:当前视口 或 整图(自动 fitView 后截取再恢复视口) */
267
294
  export type ScreenshotMode = 'viewport' | 'full';