@yh-ui/flow 1.0.52 → 1.0.54

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/Flow.vue CHANGED
@@ -120,7 +120,7 @@
120
120
  </template>
121
121
 
122
122
  <script setup>
123
- import { ref, computed, watch, onMounted, onBeforeUnmount, shallowRef, useId } from "vue";
123
+ import { ref, computed, watch, onMounted, onBeforeUnmount, shallowRef, useId, toRaw } from "vue";
124
124
  if (typeof window !== "undefined") window.__YH_FLOW_VERSION__ = "1.0.1";
125
125
  import EdgeRenderer from "./renderer/EdgeRenderer.vue";
126
126
  import EdgeHandlesRenderer from "./renderer/EdgeHandlesRenderer.vue";
@@ -135,7 +135,8 @@ import {
135
135
  createControlsPlugin,
136
136
  createSnapPlugin,
137
137
  createExportPlugin,
138
- createLayoutPlugin
138
+ createLayoutPlugin,
139
+ createHistoryPlugin
139
140
  } from "./plugins/plugins";
140
141
  import { useViewport } from "./core/useViewport";
141
142
  import { useNodes } from "./core/useNodes";
@@ -243,8 +244,10 @@ const usePlugin = (plugin) => {
243
244
  registeredPlugins.value = pluginManager.getPlugins();
244
245
  };
245
246
  const removePlugin = (pluginId) => {
246
- pluginManager.unregister(pluginId);
247
- registeredPlugins.value = pluginManager.getPlugins();
247
+ if (pluginManager.hasPlugin(pluginId)) {
248
+ pluginManager.unregister(pluginId);
249
+ registeredPlugins.value = pluginManager.getPlugins();
250
+ }
248
251
  };
249
252
  const nodesManager = useNodes(viewportRef, {
250
253
  nodes: nodesRef,
@@ -259,12 +262,51 @@ const selectionManager = useSelection({
259
262
  nodes: nodesRef,
260
263
  edges: edgesRef
261
264
  });
262
- const historyManager = useHistory(nodesRef, edgesRef, {
265
+ const localHistoryManager = useHistory(nodesRef, edgesRef, {
263
266
  maxHistory: props.maxHistory || 50,
264
267
  onHistoryChange: (canUndo, canRedo) => {
265
268
  emit("historyChange", { canUndo, canRedo });
266
269
  }
267
270
  });
271
+ const getHistoryPlugin = () => {
272
+ return pluginManager.getPlugin("history");
273
+ };
274
+ const historyManager = {
275
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
276
+ push: (state) => {
277
+ const plugin = getHistoryPlugin();
278
+ if (plugin) {
279
+ plugin.saveSnapshot();
280
+ } else {
281
+ localHistoryManager.push(state);
282
+ }
283
+ },
284
+ undo: () => {
285
+ const plugin = getHistoryPlugin();
286
+ if (plugin) {
287
+ plugin.undo();
288
+ } else {
289
+ localHistoryManager.undo();
290
+ }
291
+ },
292
+ redo: () => {
293
+ const plugin = getHistoryPlugin();
294
+ if (plugin) {
295
+ plugin.redo();
296
+ } else {
297
+ localHistoryManager.redo();
298
+ }
299
+ },
300
+ clear: () => {
301
+ const plugin = getHistoryPlugin();
302
+ if (plugin) {
303
+ plugin.clearHistory();
304
+ } else {
305
+ localHistoryManager.clear();
306
+ }
307
+ }
308
+ };
309
+ historyManager.push({ nodes: nodesRef.value, edges: edgesRef.value });
268
310
  const alignmentManager = useAlignment({
269
311
  nodes: nodesRef,
270
312
  snapThreshold: snapThreshold.value
@@ -392,6 +434,63 @@ const handleMouseMove = (event) => {
392
434
  }
393
435
  }
394
436
  };
437
+ const getNodeHandles = (node, type) => {
438
+ if (node.handleBounds) {
439
+ const handles = [];
440
+ if (node.handleBounds.top) handles.push(...node.handleBounds.top);
441
+ if (node.handleBounds.right) handles.push(...node.handleBounds.right);
442
+ if (node.handleBounds.bottom) handles.push(...node.handleBounds.bottom);
443
+ if (node.handleBounds.left) handles.push(...node.handleBounds.left);
444
+ return handles.filter((h) => h.type === type);
445
+ }
446
+ if (node.type === "group") return [];
447
+ if (node.type === "input") {
448
+ return type === "source" ? [{ id: void 0, type: "source", position: "right" }] : [];
449
+ }
450
+ if (node.type === "output") {
451
+ return type === "target" ? [{ id: void 0, type: "target", position: "left" }] : [];
452
+ }
453
+ if (node.type === "bpmn-start") {
454
+ return type === "source" ? [{ id: void 0, type: "source", position: "right" }] : [];
455
+ }
456
+ if (node.type === "bpmn-end") {
457
+ return type === "target" ? [{ id: void 0, type: "target", position: "left" }] : [];
458
+ }
459
+ if (node.type === "bpmn-task" || node.type === "bpmn-service-task" || node.type === "bpmn-user-task") {
460
+ if (type === "source") return [{ id: void 0, type: "source", position: "right" }];
461
+ return [{ id: void 0, type: "target", position: "left" }];
462
+ }
463
+ if (node.type === "bpmn-exclusive-gateway" || node.type === "bpmn-parallel-gateway" || node.type === "bpmn-inclusive-gateway") {
464
+ if (type === "source") {
465
+ return [
466
+ { id: void 0, type: "source", position: "right" },
467
+ { id: void 0, type: "source", position: "bottom" }
468
+ ];
469
+ }
470
+ return [{ id: void 0, type: "target", position: "left" }];
471
+ }
472
+ if (node.type === "ai-start") {
473
+ return type === "source" ? [{ id: void 0, type: "source", position: "right" }] : [];
474
+ }
475
+ if (node.type === "ai-end") {
476
+ return type === "target" ? [{ id: void 0, type: "target", position: "left" }] : [];
477
+ }
478
+ if (node.type === "ai-llm" || node.type === "ai-prompt" || node.type === "ai-agent" || node.type === "ai-tool" || node.type === "ai-memory") {
479
+ if (type === "source") return [{ id: void 0, type: "source", position: "right" }];
480
+ return [{ id: void 0, type: "target", position: "left" }];
481
+ }
482
+ if (node.type === "ai-condition") {
483
+ if (type === "source") {
484
+ return [
485
+ { id: void 0, type: "source", position: "right" },
486
+ { id: void 0, type: "source", position: "bottom" }
487
+ ];
488
+ }
489
+ return [{ id: void 0, type: "target", position: "left" }];
490
+ }
491
+ if (type === "source") return [{ id: void 0, type: "source", position: "right" }];
492
+ return [{ id: void 0, type: "target", position: "left" }];
493
+ };
395
494
  const handleMouseUp = (event) => {
396
495
  isPanning.value = false;
397
496
  if (isSelecting.value) {
@@ -411,17 +510,37 @@ const handleMouseUp = (event) => {
411
510
  if (targetNode) {
412
511
  const sourceNodeId = updatingEdge.value ? updatingEdge.value.handleType === "source" ? targetNode.id : updatingEdge.value.edge.source : connectionStart.value.nodeId;
413
512
  const targetNodeId = updatingEdge.value ? updatingEdge.value.handleType === "target" ? targetNode.id : updatingEdge.value.edge.target : targetNode.id;
414
- const sourceNode = nodesRef.value.find((n) => n.id === sourceNodeId);
415
- const validationResult = validateConnection(
416
- sourceNode,
417
- nodesRef.value.find((n) => n.id === targetNodeId),
418
- {
419
- source: sourceNodeId,
420
- target: targetNodeId,
421
- sourceHandle: updatingEdge.value && updatingEdge.value.handleType === "source" ? void 0 : updatingEdge.value?.edge.sourceHandle || connectionStart.value?.handleId,
422
- targetHandle: updatingEdge.value && updatingEdge.value.handleType === "target" ? void 0 : updatingEdge.value?.edge.targetHandle
513
+ let dropHandleId = void 0;
514
+ let dropHandleType = void 0;
515
+ const element = document.elementFromPoint(event.clientX, event.clientY);
516
+ const handleEl = element?.closest(".yh-flow-handle");
517
+ if (handleEl) {
518
+ dropHandleId = handleEl.getAttribute("data-handle-id") || void 0;
519
+ dropHandleType = handleEl.getAttribute("data-handle-type") || void 0;
520
+ }
521
+ let finalSourceHandle = void 0;
522
+ let finalTargetHandle = void 0;
523
+ if (updatingEdge.value) {
524
+ const { edge, handleType } = updatingEdge.value;
525
+ if (handleType === "source") {
526
+ finalSourceHandle = dropHandleType === "source" ? dropHandleId : getNodeHandles(targetNode, "source")[0]?.id || void 0;
527
+ finalTargetHandle = edge.targetHandle || void 0;
528
+ } else {
529
+ finalSourceHandle = edge.sourceHandle || void 0;
530
+ finalTargetHandle = dropHandleType === "target" ? dropHandleId : getNodeHandles(targetNode, "target")[0]?.id || void 0;
423
531
  }
424
- );
532
+ } else {
533
+ finalSourceHandle = connectionStart.value?.handleId || void 0;
534
+ finalTargetHandle = dropHandleType === "target" ? dropHandleId : getNodeHandles(targetNode, "target")[0]?.id || void 0;
535
+ }
536
+ const sourceNode = nodesRef.value.find((n) => n.id === sourceNodeId);
537
+ const targetNodeObj = nodesRef.value.find((n) => n.id === targetNodeId);
538
+ const validationResult = validateConnection(sourceNode, targetNodeObj, {
539
+ source: sourceNodeId,
540
+ target: targetNodeId,
541
+ sourceHandle: finalSourceHandle,
542
+ targetHandle: finalTargetHandle
543
+ });
425
544
  if (!validationResult.isValid) {
426
545
  console.warn("Invalid connection:", validationResult.message);
427
546
  isConnecting.value = false;
@@ -430,12 +549,12 @@ const handleMouseUp = (event) => {
430
549
  return;
431
550
  }
432
551
  if (updatingEdge.value) {
433
- const { edge, handleType } = updatingEdge.value;
552
+ const { edge } = updatingEdge.value;
434
553
  const connection = {
435
- source: handleType === "source" ? targetNode.id : edge.source,
436
- target: handleType === "target" ? targetNode.id : edge.target,
437
- sourceHandle: handleType === "source" ? void 0 : edge.sourceHandle,
438
- targetHandle: handleType === "target" ? void 0 : edge.targetHandle
554
+ source: sourceNodeId,
555
+ target: targetNodeId,
556
+ sourceHandle: finalSourceHandle,
557
+ targetHandle: finalTargetHandle
439
558
  };
440
559
  edgesManager.updateEdge(edge.id, connection);
441
560
  emit("edgeUpdate", { edge, connection });
@@ -443,10 +562,10 @@ const handleMouseUp = (event) => {
443
562
  } else {
444
563
  const newEdge = {
445
564
  id: `edge-${Date.now()}`,
446
- source: connectionStart.value.nodeId,
447
- target: targetNode.id,
448
- sourceHandle: connectionStart.value.handleId || void 0,
449
- targetHandle: void 0,
565
+ source: sourceNodeId,
566
+ target: targetNodeId,
567
+ sourceHandle: finalSourceHandle,
568
+ targetHandle: finalTargetHandle,
450
569
  type: "bezier"
451
570
  };
452
571
  edgesManager.addEdge(newEdge);
@@ -741,16 +860,19 @@ pluginManager.init(flowInstance);
741
860
  defineExpose(flowInstance);
742
861
  watch(
743
862
  () => props.nodes,
744
- (newNodes) => {
745
- if (draggingNodeId.value) return;
746
- nodesRef.value = newNodes || [];
863
+ (nodes) => {
864
+ if (toRaw(nodesRef.value) !== toRaw(nodes)) {
865
+ nodesRef.value = nodes;
866
+ }
747
867
  },
748
868
  { deep: true }
749
869
  );
750
870
  watch(
751
871
  () => props.edges,
752
- (newEdges) => {
753
- edgesRef.value = newEdges || [];
872
+ (edges) => {
873
+ if (toRaw(edgesRef.value) !== toRaw(edges)) {
874
+ edgesRef.value = edges;
875
+ }
754
876
  },
755
877
  { deep: true }
756
878
  );
@@ -806,13 +928,14 @@ watch(
806
928
  { deep: true }
807
929
  );
808
930
  let handleKeyDown;
931
+ let resizeObserver = null;
809
932
  onMounted(() => {
810
933
  document.addEventListener("mousemove", handleMouseMove);
811
934
  document.addEventListener("mouseup", handleMouseUp);
812
935
  if (containerRef.value) {
813
936
  containerWidth.value = containerRef.value.clientWidth;
814
937
  containerHeight.value = containerRef.value.clientHeight;
815
- const resizeObserver = new ResizeObserver((entries) => {
938
+ resizeObserver = new ResizeObserver((entries) => {
816
939
  for (const entry of entries) {
817
940
  containerWidth.value = entry.contentRect.width;
818
941
  containerHeight.value = entry.contentRect.height;
@@ -955,16 +1078,38 @@ onMounted(() => {
955
1078
  }
956
1079
  }
957
1080
  );
1081
+ watch(
1082
+ () => [props.history, props.maxHistory],
1083
+ ([history, maxHistory]) => {
1084
+ removePlugin("history");
1085
+ if (history) {
1086
+ usePlugin(
1087
+ createHistoryPlugin({
1088
+ enabled: true,
1089
+ maxHistory: maxHistory || 50,
1090
+ enableKeyboard: false,
1091
+ onHistoryChange: (canUndo, canRedo) => {
1092
+ emit("historyChange", { canUndo, canRedo });
1093
+ }
1094
+ })
1095
+ );
1096
+ }
1097
+ },
1098
+ { immediate: true }
1099
+ );
958
1100
  if (props.keyboardShortcuts) {
959
1101
  const keyboard = useKeyboard({
960
1102
  enabled: true,
961
1103
  onDelete: () => {
962
1104
  const selectedNodes = nodesRef.value.filter((n) => n.selected);
963
1105
  const selectedEdges = edgesRef.value.filter((e) => e.selected);
964
- selectedNodes.forEach((node) => nodesManager.removeNode(node.id));
965
- selectedEdges.forEach((edge) => edgesManager.removeEdge(edge.id));
966
- emit("update:nodes", nodesRef.value);
967
- emit("update:edges", edgesRef.value);
1106
+ if (selectedNodes.length > 0 || selectedEdges.length > 0) {
1107
+ historyManager.push({ nodes: nodesRef.value, edges: edgesRef.value });
1108
+ selectedNodes.forEach((node) => nodesManager.removeNode(node.id));
1109
+ selectedEdges.forEach((edge) => edgesManager.removeEdge(edge.id));
1110
+ emit("update:nodes", nodesRef.value);
1111
+ emit("update:edges", edgesRef.value);
1112
+ }
968
1113
  },
969
1114
  onUndo: () => historyManager.undo(),
970
1115
  onRedo: () => historyManager.redo(),
@@ -983,6 +1128,10 @@ onMounted(() => {
983
1128
  }
984
1129
  });
985
1130
  onBeforeUnmount(() => {
1131
+ if (resizeObserver) {
1132
+ resizeObserver.disconnect();
1133
+ resizeObserver = null;
1134
+ }
986
1135
  if (props.keyboardShortcuts && handleKeyDown) {
987
1136
  document.removeEventListener("keydown", handleKeyDown);
988
1137
  }
@@ -19,7 +19,9 @@ function useKeyboard(options = {}) {
19
19
  if (!enabled) return;
20
20
  const key = event.key;
21
21
  const ctrl = event.ctrlKey || event.metaKey;
22
- if ((key === "Delete" || key === "Backspace") && !event.target?.toString().includes("Input")) {
22
+ const target = event.target;
23
+ const isEditable = target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT" || target.isContentEditable === true);
24
+ if ((key === "Delete" || key === "Backspace") && !isEditable) {
23
25
  event.preventDefault();
24
26
  onDelete?.();
25
27
  }
@@ -13,7 +13,9 @@ export function useKeyboard(options = {}) {
13
13
  if (!enabled) return;
14
14
  const key = event.key;
15
15
  const ctrl = event.ctrlKey || event.metaKey;
16
- if ((key === "Delete" || key === "Backspace") && !event.target?.toString().includes("Input")) {
16
+ const target = event.target;
17
+ const isEditable = target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT" || target.isContentEditable === true);
18
+ if ((key === "Delete" || key === "Backspace") && !isEditable) {
17
19
  event.preventDefault();
18
20
  onDelete?.();
19
21
  }
@@ -20,7 +20,15 @@ const defaultOptions = {
20
20
  workerUrl: ""
21
21
  };
22
22
  async function applyDagreLayout(nodes, edges, options) {
23
- const dagreLib = await Promise.resolve().then(() => require("dagre"));
23
+ let dagreLib;
24
+ try {
25
+ dagreLib = await Promise.resolve().then(() => require("dagre"));
26
+ } catch {
27
+ throw new Error('[YH-UI Flow] Layout engine "dagre" is not installed. Please install "dagre" to use the dagre layout algorithm.');
28
+ }
29
+ if (!dagreLib) {
30
+ throw new Error('[YH-UI Flow] Layout engine "dagre" is not installed. Please install "dagre" to use the dagre layout algorithm.');
31
+ }
24
32
  const dagre = dagreLib.default || dagreLib;
25
33
  const graphlib = dagreLib.graphlib || dagre.graphlib;
26
34
  const g = new graphlib.Graph();
@@ -63,11 +71,18 @@ async function applyElkLayout(nodes, edges, options) {
63
71
  const elkPath = "elkjs";
64
72
  let elkLib;
65
73
  try {
66
- elkLib = await Promise.resolve(`${/* @vite-ignore */
67
- bundledPath}`).then(s => require(s));
74
+ try {
75
+ elkLib = await Promise.resolve(`${/* @vite-ignore */
76
+ bundledPath}`).then(s => require(s));
77
+ } catch {
78
+ elkLib = await Promise.resolve(`${/* @vite-ignore */
79
+ elkPath}`).then(s => require(s));
80
+ }
68
81
  } catch {
69
- elkLib = await Promise.resolve(`${/* @vite-ignore */
70
- elkPath}`).then(s => require(s));
82
+ throw new Error('[YH-UI Flow] Layout engine "elkjs" is not installed. Please install "elkjs" to use the elk layout algorithm.');
83
+ }
84
+ if (!elkLib) {
85
+ throw new Error('[YH-UI Flow] Layout engine "elkjs" is not installed. Please install "elkjs" to use the elk layout algorithm.');
71
86
  }
72
87
  const ELK = elkLib.default || elkLib;
73
88
  const elk = new ELK();
@@ -116,8 +131,16 @@ async function applyElkLayout(nodes, edges, options) {
116
131
  }
117
132
  async function applyForceLayout(nodes, edges, options, flowInstance) {
118
133
  const d3ForcePath = "d3-force";
119
- const d3ForceLib = await Promise.resolve(`${/* @vite-ignore */
120
- d3ForcePath}`).then(s => require(s));
134
+ let d3ForceLib;
135
+ try {
136
+ d3ForceLib = await Promise.resolve(`${/* @vite-ignore */
137
+ d3ForcePath}`).then(s => require(s));
138
+ } catch {
139
+ throw new Error('[YH-UI Flow] Layout engine "d3-force" is not installed. Please install "d3-force" to use the force layout algorithm.');
140
+ }
141
+ if (!d3ForceLib) {
142
+ throw new Error('[YH-UI Flow] Layout engine "d3-force" is not installed. Please install "d3-force" to use the force layout algorithm.');
143
+ }
121
144
  const d3Force = d3ForceLib.default || d3ForceLib;
122
145
  const forceNodes = nodes.map(node => ({
123
146
  id: node.id,
@@ -12,7 +12,22 @@ const defaultOptions = {
12
12
  workerUrl: ""
13
13
  };
14
14
  async function applyDagreLayout(nodes, edges, options) {
15
- const dagreLib = await import("dagre");
15
+ let dagreLib;
16
+ try {
17
+ dagreLib = await import(
18
+ /* @vite-ignore */
19
+ "dagre"
20
+ );
21
+ } catch {
22
+ throw new Error(
23
+ '[YH-UI Flow] Layout engine "dagre" is not installed. Please install "dagre" to use the dagre layout algorithm.'
24
+ );
25
+ }
26
+ if (!dagreLib) {
27
+ throw new Error(
28
+ '[YH-UI Flow] Layout engine "dagre" is not installed. Please install "dagre" to use the dagre layout algorithm.'
29
+ );
30
+ }
16
31
  const dagre = dagreLib.default || dagreLib;
17
32
  const graphlib = dagreLib.graphlib || dagre.graphlib;
18
33
  const g = new graphlib.Graph();
@@ -52,14 +67,25 @@ async function applyElkLayout(nodes, edges, options) {
52
67
  const elkPath = "elkjs";
53
68
  let elkLib;
54
69
  try {
55
- elkLib = await import(
56
- /* @vite-ignore */
57
- bundledPath
58
- );
70
+ try {
71
+ elkLib = await import(
72
+ /* @vite-ignore */
73
+ bundledPath
74
+ );
75
+ } catch {
76
+ elkLib = await import(
77
+ /* @vite-ignore */
78
+ elkPath
79
+ );
80
+ }
59
81
  } catch {
60
- elkLib = await import(
61
- /* @vite-ignore */
62
- elkPath
82
+ throw new Error(
83
+ '[YH-UI Flow] Layout engine "elkjs" is not installed. Please install "elkjs" to use the elk layout algorithm.'
84
+ );
85
+ }
86
+ if (!elkLib) {
87
+ throw new Error(
88
+ '[YH-UI Flow] Layout engine "elkjs" is not installed. Please install "elkjs" to use the elk layout algorithm.'
63
89
  );
64
90
  }
65
91
  const ELK = elkLib.default || elkLib;
@@ -106,10 +132,22 @@ async function applyElkLayout(nodes, edges, options) {
106
132
  }
107
133
  async function applyForceLayout(nodes, edges, options, flowInstance) {
108
134
  const d3ForcePath = "d3-force";
109
- const d3ForceLib = await import(
110
- /* @vite-ignore */
111
- d3ForcePath
112
- );
135
+ let d3ForceLib;
136
+ try {
137
+ d3ForceLib = await import(
138
+ /* @vite-ignore */
139
+ d3ForcePath
140
+ );
141
+ } catch {
142
+ throw new Error(
143
+ '[YH-UI Flow] Layout engine "d3-force" is not installed. Please install "d3-force" to use the force layout algorithm.'
144
+ );
145
+ }
146
+ if (!d3ForceLib) {
147
+ throw new Error(
148
+ '[YH-UI Flow] Layout engine "d3-force" is not installed. Please install "d3-force" to use the force layout algorithm.'
149
+ );
150
+ }
113
151
  const d3Force = d3ForceLib.default || d3ForceLib;
114
152
  const forceNodes = nodes.map((node) => ({
115
153
  id: node.id,
@@ -14,7 +14,11 @@
14
14
  >
15
15
  <defs>
16
16
  <!-- Dynamic masks to create a true gap in the line behind the label -->
17
- <mask v-for="ed in edgeData" :key="`mask-${ed.edge.id}`" :id="getMaskId(ed.edge.id)">
17
+ <mask
18
+ v-for="ed in edgeData.filter(e => e.edge.label)"
19
+ :key="`mask-${ed.edge.id}`"
20
+ :id="getMaskId(ed.edge.id)"
21
+ >
18
22
  <rect x="-5000" y="-5000" width="10000" height="10000" fill="white" />
19
23
  <rect
20
24
  :x="ed.labelX - ed.labelWidth / 2 - 4"
@@ -61,8 +65,28 @@
61
65
  style="cursor: pointer; pointer-events: all"
62
66
  />
63
67
 
64
- <!-- Visible Path -->
68
+ <!-- Visible Path with Mask (when label exists) -->
65
69
  <path
70
+ v-if="ed.edge.label"
71
+ :d="ed.path"
72
+ :stroke="ed.stroke"
73
+ :stroke-width="ed.strokeWidth"
74
+ fill="none"
75
+ :class="{
76
+ 'yh-flow-edge-path': true,
77
+ 'is-animated': ed.edge.animated
78
+ }"
79
+ :mask="`url(#${getMaskId(ed.edge.id)})`"
80
+ :style="{
81
+ pointerEvents: 'none',
82
+ transition: 'stroke 0.2s, stroke-width 0.2s',
83
+ stroke: ed.stroke
84
+ }"
85
+ />
86
+
87
+ <!-- Visible Path without Mask (when no label exists) -->
88
+ <path
89
+ v-else
66
90
  :d="ed.path"
67
91
  :stroke="ed.stroke"
68
92
  :stroke-width="ed.strokeWidth"
@@ -71,7 +95,6 @@
71
95
  'yh-flow-edge-path': true,
72
96
  'is-animated': ed.edge.animated
73
97
  }"
74
- :mask="ed.edge.label ? `url(#${getMaskId(ed.edge.id)})` : void 0"
75
98
  :style="{
76
99
  pointerEvents: 'none',
77
100
  transition: 'stroke 0.2s, stroke-width 0.2s',
@@ -151,6 +174,12 @@ const getLabelStyle = (edge) => {
151
174
  return styles;
152
175
  };
153
176
  const edgeData = computed(() => {
177
+ console.log(
178
+ "EdgeRenderer computed edgeData running. Nodes count:",
179
+ props.nodes.length,
180
+ "Node 1 pos:",
181
+ props.nodes.find((n) => n.id === "1")?.position
182
+ );
154
183
  const result = [];
155
184
  for (const edge of props.edges) {
156
185
  if (!edge || edge.hidden) continue;
@@ -64,7 +64,7 @@
64
64
  </template>
65
65
 
66
66
  <script setup>
67
- import { computed, ref, onMounted, onBeforeUnmount } from "vue";
67
+ import { computed, ref, onMounted, onBeforeUnmount, watch } from "vue";
68
68
  import {
69
69
  getCustomNodeTemplate,
70
70
  getCustomNode,
@@ -373,6 +373,30 @@ onBeforeUnmount(() => {
373
373
  }
374
374
  nodeElements.clear();
375
375
  });
376
+ watch(
377
+ () => props.nodes,
378
+ (newNodes) => {
379
+ let hasChanges = false;
380
+ newNodes.forEach((node) => {
381
+ if (!node.measured) {
382
+ const el = nodeElements.get(node.id);
383
+ if (el) {
384
+ const rect = el.getBoundingClientRect();
385
+ const width = Math.round(rect.width);
386
+ const height = Math.round(rect.height);
387
+ if (width > 0 && height > 0) {
388
+ node.measured = { width, height };
389
+ hasChanges = true;
390
+ }
391
+ }
392
+ }
393
+ });
394
+ if (hasChanges) {
395
+ emit("nodes-measured");
396
+ }
397
+ },
398
+ { flush: "post" }
399
+ );
376
400
  const setNodeRef = (el, nodeId) => {
377
401
  if (el) {
378
402
  nodeElements.set(nodeId, el);
@@ -8,6 +8,7 @@ exports.getEdgeCenter = getEdgeCenter;
8
8
  exports.getEdgePath = getEdgePath;
9
9
  exports.getEdgePosition = getEdgePosition;
10
10
  exports.getHandlePosition = getHandlePosition;
11
+ exports.getSelfLoopPath = getSelfLoopPath;
11
12
  exports.getSmoothStepPath = getSmoothStepPath;
12
13
  exports.getStepPath = getStepPath;
13
14
  exports.getStraightPath = getStraightPath;
@@ -51,6 +52,44 @@ function getHandlePosition(node, handlePosition = "right", _handleId) {
51
52
  }
52
53
  width = width || 150;
53
54
  height = height || 40;
55
+ if (node.handleBounds && node.handleBounds[handlePosition]) {
56
+ const handles = node.handleBounds[handlePosition] || [];
57
+ if (handles.length > 0) {
58
+ let handleIndex = -1;
59
+ if (_handleId) {
60
+ handleIndex = handles.findIndex(h => h.id === _handleId);
61
+ }
62
+ if (handleIndex === -1) {
63
+ handleIndex = 0;
64
+ }
65
+ const handle = handles[handleIndex];
66
+ if (handle) {
67
+ if (handle.x !== void 0 && handle.y !== void 0) {
68
+ return {
69
+ x: x + handle.x,
70
+ y: y + handle.y
71
+ };
72
+ }
73
+ const N = handles.length;
74
+ const i = handleIndex;
75
+ if (handlePosition === "left" || handlePosition === "right") {
76
+ const hX = handlePosition === "left" ? 0 : width;
77
+ const hY = height / (N + 1) * (i + 1);
78
+ return {
79
+ x: x + hX,
80
+ y: y + hY
81
+ };
82
+ } else {
83
+ const hX = width / (N + 1) * (i + 1);
84
+ const hY = handlePosition === "top" ? 0 : height;
85
+ return {
86
+ x: x + hX,
87
+ y: y + hY
88
+ };
89
+ }
90
+ }
91
+ }
92
+ }
54
93
  switch (handlePosition) {
55
94
  case "top":
56
95
  return {
@@ -170,7 +209,37 @@ function getSmoothStepPath(params) {
170
209
  return [`M${sourceX},${sourceY}`, `L${sourceX},${midY - sign1Y * r}`, `Q${sourceX},${midY} ${sourceX + sign2X * r},${midY}`, `L${targetX - sign2X * r},${midY}`, `Q${targetX},${midY} ${targetX},${midY + sign1Y * r}`, `L${targetX},${targetY}`].join(" ");
171
210
  }
172
211
  }
212
+ function getSelfLoopPath(sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, nodeWidth, nodeHeight) {
213
+ const w = nodeWidth ?? 150;
214
+ const h = nodeHeight ?? 50;
215
+ const dx = targetX - sourceX;
216
+ const dy = targetY - sourceY;
217
+ const dist = Math.sqrt(dx * dx + dy * dy);
218
+ if (dist < 5) {
219
+ const loopSize = 40;
220
+ if (sourcePosition === "right") {
221
+ return `M${sourceX},${sourceY} C${sourceX + loopSize},${sourceY - loopSize} ${sourceX + loopSize},${sourceY + loopSize} ${sourceX},${sourceY}`;
222
+ } else if (sourcePosition === "left") {
223
+ return `M${sourceX},${sourceY} C${sourceX - loopSize},${sourceY - loopSize} ${sourceX - loopSize},${sourceY + loopSize} ${sourceX},${sourceY}`;
224
+ } else if (sourcePosition === "top") {
225
+ return `M${sourceX},${sourceY} C${sourceX - loopSize},${sourceY - loopSize} ${sourceX + loopSize},${sourceY - loopSize} ${sourceX},${sourceY}`;
226
+ } else {
227
+ return `M${sourceX},${sourceY} C${sourceX - loopSize},${sourceY + loopSize} ${sourceX + loopSize},${sourceY + loopSize} ${sourceX},${sourceY}`;
228
+ }
229
+ }
230
+ const offset = Math.max(w, h, 60) * 0.5;
231
+ let c1x = sourceX;
232
+ let c1y = sourceY;
233
+ if (sourcePosition === "right") c1x += offset;else if (sourcePosition === "left") c1x -= offset;else if (sourcePosition === "top") c1y -= offset;else if (sourcePosition === "bottom") c1y += offset;
234
+ let c2x = targetX;
235
+ let c2y = targetY;
236
+ if (targetPosition === "right") c2x += offset;else if (targetPosition === "left") c2x -= offset;else if (targetPosition === "top") c2y -= offset;else if (targetPosition === "bottom") c2y += offset;
237
+ return `M${sourceX},${sourceY} C${c1x},${c1y} ${c2x},${c2y} ${targetX},${targetY}`;
238
+ }
173
239
  function getEdgePath(type, params) {
240
+ if (params.isSelfLoop) {
241
+ return getSelfLoopPath(params.sourceX, params.sourceY, params.targetX, params.targetY, params.sourcePosition, params.targetPosition, params.nodeWidth, params.nodeHeight);
242
+ }
174
243
  switch (type) {
175
244
  case "bezier":
176
245
  case "default":
@@ -191,8 +260,58 @@ function getEdgeCenter(params) {
191
260
  sourceY,
192
261
  targetX,
193
262
  targetY,
194
- type = "bezier"
263
+ type = "bezier",
264
+ isSelfLoop,
265
+ nodeWidth,
266
+ nodeHeight,
267
+ sourcePosition,
268
+ targetPosition
195
269
  } = params;
270
+ if (isSelfLoop) {
271
+ const w = nodeWidth ?? 150;
272
+ const h = nodeHeight ?? 50;
273
+ const dx = targetX - sourceX;
274
+ const dy = targetY - sourceY;
275
+ const dist = Math.sqrt(dx * dx + dy * dy);
276
+ let c1x = sourceX,
277
+ c1y = sourceY,
278
+ c2x = targetX,
279
+ c2y = targetY;
280
+ if (dist < 5) {
281
+ const loopSize = 40;
282
+ if (sourcePosition === "right") {
283
+ c1x = sourceX + loopSize;
284
+ c1y = sourceY - loopSize;
285
+ c2x = sourceX + loopSize;
286
+ c2y = sourceY + loopSize;
287
+ } else if (sourcePosition === "left") {
288
+ c1x = sourceX - loopSize;
289
+ c1y = sourceY - loopSize;
290
+ c2x = sourceX - loopSize;
291
+ c2y = sourceY + loopSize;
292
+ } else if (sourcePosition === "top") {
293
+ c1x = sourceX - loopSize;
294
+ c1y = sourceY - loopSize;
295
+ c2x = sourceX + loopSize;
296
+ c2y = sourceY - loopSize;
297
+ } else {
298
+ c1x = sourceX - loopSize;
299
+ c1y = sourceY + loopSize;
300
+ c2x = sourceX + loopSize;
301
+ c2y = sourceY + loopSize;
302
+ }
303
+ } else {
304
+ const offset = Math.max(w, h, 60) * 0.5;
305
+ if (sourcePosition === "right") c1x += offset;else if (sourcePosition === "left") c1x -= offset;else if (sourcePosition === "top") c1y -= offset;else if (sourcePosition === "bottom") c1y += offset;
306
+ if (targetPosition === "right") c2x += offset;else if (targetPosition === "left") c2x -= offset;else if (targetPosition === "top") c2y -= offset;else if (targetPosition === "bottom") c2y += offset;
307
+ }
308
+ return {
309
+ x: 0.125 * sourceX + 0.375 * c1x + 0.375 * c2x + 0.125 * targetX,
310
+ y: 0.125 * sourceY + 0.375 * c1y + 0.375 * c2y + 0.125 * targetY,
311
+ ox: 0,
312
+ oy: 0
313
+ };
314
+ }
196
315
  if (type === "bezier" || type === "default") {
197
316
  const curvature = params.curvature ?? 0.25;
198
317
  const srcDir = getDir(params.sourcePosition);
@@ -1,4 +1,4 @@
1
- import type { Position, EdgeType, NodeStyle } from '../types';
1
+ import type { Position, EdgeType, NodeStyle, NodeHandle } from '../types';
2
2
  export interface EdgePathParams {
3
3
  sourceX: number;
4
4
  sourceY: number;
@@ -8,6 +8,9 @@ export interface EdgePathParams {
8
8
  targetPosition: Position;
9
9
  /** 控制曲率,0~1,默认 0.25 */
10
10
  curvature?: number;
11
+ isSelfLoop?: boolean;
12
+ nodeWidth?: number;
13
+ nodeHeight?: number;
11
14
  }
12
15
  /**
13
16
  * 获取连接点的位置坐标
@@ -24,6 +27,12 @@ export declare function getHandlePosition(node: {
24
27
  width: number;
25
28
  height: number;
26
29
  };
30
+ handleBounds?: {
31
+ top?: NodeHandle[];
32
+ right?: NodeHandle[];
33
+ bottom?: NodeHandle[];
34
+ left?: NodeHandle[];
35
+ };
27
36
  }, handlePosition?: Position, _handleId?: string | null): {
28
37
  x: number;
29
38
  y: number;
@@ -54,6 +63,10 @@ export declare function getStepPath(params: EdgePathParams): string;
54
63
  * 生成平滑阶梯线路径(带圆角转折的 step)
55
64
  */
56
65
  export declare function getSmoothStepPath(params: EdgePathParams): string;
66
+ /**
67
+ * 生成自环曲线路径
68
+ */
69
+ export declare function getSelfLoopPath(sourceX: number, sourceY: number, targetX: number, targetY: number, sourcePosition: Position, targetPosition: Position, nodeWidth?: number, nodeHeight?: number): string;
57
70
  /**
58
71
  * 根据类型获取连线路径
59
72
  */
@@ -25,6 +25,35 @@ export function getHandlePosition(node, handlePosition = "right", _handleId) {
25
25
  }
26
26
  width = width || 150;
27
27
  height = height || 40;
28
+ if (node.handleBounds && node.handleBounds[handlePosition]) {
29
+ const handles = node.handleBounds[handlePosition] || [];
30
+ if (handles.length > 0) {
31
+ let handleIndex = -1;
32
+ if (_handleId) {
33
+ handleIndex = handles.findIndex((h) => h.id === _handleId);
34
+ }
35
+ if (handleIndex === -1) {
36
+ handleIndex = 0;
37
+ }
38
+ const handle = handles[handleIndex];
39
+ if (handle) {
40
+ if (handle.x !== void 0 && handle.y !== void 0) {
41
+ return { x: x + handle.x, y: y + handle.y };
42
+ }
43
+ const N = handles.length;
44
+ const i = handleIndex;
45
+ if (handlePosition === "left" || handlePosition === "right") {
46
+ const hX = handlePosition === "left" ? 0 : width;
47
+ const hY = height / (N + 1) * (i + 1);
48
+ return { x: x + hX, y: y + hY };
49
+ } else {
50
+ const hX = width / (N + 1) * (i + 1);
51
+ const hY = handlePosition === "top" ? 0 : height;
52
+ return { x: x + hX, y: y + hY };
53
+ }
54
+ }
55
+ }
56
+ }
28
57
  switch (handlePosition) {
29
58
  case "top":
30
59
  return { x: x + width / 2, y };
@@ -119,7 +148,52 @@ export function getSmoothStepPath(params) {
119
148
  ].join(" ");
120
149
  }
121
150
  }
151
+ export function getSelfLoopPath(sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, nodeWidth, nodeHeight) {
152
+ const w = nodeWidth ?? 150;
153
+ const h = nodeHeight ?? 50;
154
+ const dx = targetX - sourceX;
155
+ const dy = targetY - sourceY;
156
+ const dist = Math.sqrt(dx * dx + dy * dy);
157
+ if (dist < 5) {
158
+ const loopSize = 40;
159
+ if (sourcePosition === "right") {
160
+ return `M${sourceX},${sourceY} C${sourceX + loopSize},${sourceY - loopSize} ${sourceX + loopSize},${sourceY + loopSize} ${sourceX},${sourceY}`;
161
+ } else if (sourcePosition === "left") {
162
+ return `M${sourceX},${sourceY} C${sourceX - loopSize},${sourceY - loopSize} ${sourceX - loopSize},${sourceY + loopSize} ${sourceX},${sourceY}`;
163
+ } else if (sourcePosition === "top") {
164
+ return `M${sourceX},${sourceY} C${sourceX - loopSize},${sourceY - loopSize} ${sourceX + loopSize},${sourceY - loopSize} ${sourceX},${sourceY}`;
165
+ } else {
166
+ return `M${sourceX},${sourceY} C${sourceX - loopSize},${sourceY + loopSize} ${sourceX + loopSize},${sourceY + loopSize} ${sourceX},${sourceY}`;
167
+ }
168
+ }
169
+ const offset = Math.max(w, h, 60) * 0.5;
170
+ let c1x = sourceX;
171
+ let c1y = sourceY;
172
+ if (sourcePosition === "right") c1x += offset;
173
+ else if (sourcePosition === "left") c1x -= offset;
174
+ else if (sourcePosition === "top") c1y -= offset;
175
+ else if (sourcePosition === "bottom") c1y += offset;
176
+ let c2x = targetX;
177
+ let c2y = targetY;
178
+ if (targetPosition === "right") c2x += offset;
179
+ else if (targetPosition === "left") c2x -= offset;
180
+ else if (targetPosition === "top") c2y -= offset;
181
+ else if (targetPosition === "bottom") c2y += offset;
182
+ return `M${sourceX},${sourceY} C${c1x},${c1y} ${c2x},${c2y} ${targetX},${targetY}`;
183
+ }
122
184
  export function getEdgePath(type, params) {
185
+ if (params.isSelfLoop) {
186
+ return getSelfLoopPath(
187
+ params.sourceX,
188
+ params.sourceY,
189
+ params.targetX,
190
+ params.targetY,
191
+ params.sourcePosition,
192
+ params.targetPosition,
193
+ params.nodeWidth,
194
+ params.nodeHeight
195
+ );
196
+ }
123
197
  switch (type) {
124
198
  case "bezier":
125
199
  case "default":
@@ -135,7 +209,66 @@ export function getEdgePath(type, params) {
135
209
  }
136
210
  }
137
211
  export function getEdgeCenter(params) {
138
- const { sourceX, sourceY, targetX, targetY, type = "bezier" } = params;
212
+ const {
213
+ sourceX,
214
+ sourceY,
215
+ targetX,
216
+ targetY,
217
+ type = "bezier",
218
+ isSelfLoop,
219
+ nodeWidth,
220
+ nodeHeight,
221
+ sourcePosition,
222
+ targetPosition
223
+ } = params;
224
+ if (isSelfLoop) {
225
+ const w = nodeWidth ?? 150;
226
+ const h = nodeHeight ?? 50;
227
+ const dx = targetX - sourceX;
228
+ const dy = targetY - sourceY;
229
+ const dist = Math.sqrt(dx * dx + dy * dy);
230
+ let c1x = sourceX, c1y = sourceY, c2x = targetX, c2y = targetY;
231
+ if (dist < 5) {
232
+ const loopSize = 40;
233
+ if (sourcePosition === "right") {
234
+ c1x = sourceX + loopSize;
235
+ c1y = sourceY - loopSize;
236
+ c2x = sourceX + loopSize;
237
+ c2y = sourceY + loopSize;
238
+ } else if (sourcePosition === "left") {
239
+ c1x = sourceX - loopSize;
240
+ c1y = sourceY - loopSize;
241
+ c2x = sourceX - loopSize;
242
+ c2y = sourceY + loopSize;
243
+ } else if (sourcePosition === "top") {
244
+ c1x = sourceX - loopSize;
245
+ c1y = sourceY - loopSize;
246
+ c2x = sourceX + loopSize;
247
+ c2y = sourceY - loopSize;
248
+ } else {
249
+ c1x = sourceX - loopSize;
250
+ c1y = sourceY + loopSize;
251
+ c2x = sourceX + loopSize;
252
+ c2y = sourceY + loopSize;
253
+ }
254
+ } else {
255
+ const offset = Math.max(w, h, 60) * 0.5;
256
+ if (sourcePosition === "right") c1x += offset;
257
+ else if (sourcePosition === "left") c1x -= offset;
258
+ else if (sourcePosition === "top") c1y -= offset;
259
+ else if (sourcePosition === "bottom") c1y += offset;
260
+ if (targetPosition === "right") c2x += offset;
261
+ else if (targetPosition === "left") c2x -= offset;
262
+ else if (targetPosition === "top") c2y -= offset;
263
+ else if (targetPosition === "bottom") c2y += offset;
264
+ }
265
+ return {
266
+ x: 0.125 * sourceX + 0.375 * c1x + 0.375 * c2x + 0.125 * targetX,
267
+ y: 0.125 * sourceY + 0.375 * c1y + 0.375 * c2y + 0.125 * targetY,
268
+ ox: 0,
269
+ oy: 0
270
+ };
271
+ }
139
272
  if (type === "bezier" || type === "default") {
140
273
  const curvature = params.curvature ?? 0.25;
141
274
  const srcDir = getDir(params.sourcePosition);
@@ -56,14 +56,7 @@ function isValidConnection(sourceNode, targetNode, connection) {
56
56
  }
57
57
  if (connection.source === connection.target) {
58
58
  return {
59
- isValid: false,
60
- message: "Cannot connect to the same node"
61
- };
62
- }
63
- if (connection.target === connection.source) {
64
- return {
65
- isValid: false,
66
- message: "Cannot create self-loop"
59
+ isValid: true
67
60
  };
68
61
  }
69
62
  return {
@@ -41,10 +41,7 @@ export function isValidConnection(sourceNode, targetNode, connection) {
41
41
  return { isValid: false, message: "Target node not found" };
42
42
  }
43
43
  if (connection.source === connection.target) {
44
- return { isValid: false, message: "Cannot connect to the same node" };
45
- }
46
- if (connection.target === connection.source) {
47
- return { isValid: false, message: "Cannot create self-loop" };
44
+ return { isValid: true };
48
45
  }
49
46
  return { isValid: true };
50
47
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yh-ui/flow",
3
- "version": "1.0.52",
3
+ "version": "1.0.54",
4
4
  "description": "YH-UI High-performance Flow Chart Component",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -32,8 +32,8 @@
32
32
  "lint": "eslint ."
33
33
  },
34
34
  "dependencies": {
35
- "@yh-ui/utils": "^1.0.52",
36
- "@yh-ui/hooks": "^1.0.52"
35
+ "@yh-ui/utils": "^1.0.54",
36
+ "@yh-ui/hooks": "^1.0.54"
37
37
  },
38
38
  "devDependencies": {
39
39
  "vue": "^3.5.35",
@@ -44,7 +44,8 @@
44
44
  "vue": "^3.5.35",
45
45
  "dagre": ">=0.8.5",
46
46
  "elkjs": ">=0.9.0",
47
- "d3-force": ">=3.0.0"
47
+ "d3-force": ">=3.0.0",
48
+ "html-to-image": ">=1.11.0"
48
49
  },
49
50
  "peerDependenciesMeta": {
50
51
  "dagre": {
@@ -55,6 +56,9 @@
55
56
  },
56
57
  "d3-force": {
57
58
  "optional": true
59
+ },
60
+ "html-to-image": {
61
+ "optional": true
58
62
  }
59
63
  },
60
64
  "publishConfig": {