@particle-academy/fancy-flow 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -4871,6 +4871,28 @@ var addEdge = (edgeParams, edges, options = {}) => {
4871
4871
  }
4872
4872
  return edges.concat(edge);
4873
4873
  };
4874
+ var reconnectEdge = (oldEdge, newConnection, edges, options = { shouldReplaceId: true }) => {
4875
+ const { id: oldEdgeId, ...rest } = oldEdge;
4876
+ if (!newConnection.source || !newConnection.target) {
4877
+ options.onError?.("006", errorMessages["error006"]());
4878
+ return edges;
4879
+ }
4880
+ const foundEdge = edges.find((e) => e.id === oldEdge.id);
4881
+ if (!foundEdge) {
4882
+ options.onError?.("007", errorMessages["error007"](oldEdgeId));
4883
+ return edges;
4884
+ }
4885
+ const edgeIdGenerator = options.getEdgeId || getEdgeId;
4886
+ const edge = {
4887
+ ...rest,
4888
+ id: options.shouldReplaceId ? edgeIdGenerator(newConnection) : oldEdgeId,
4889
+ source: newConnection.source,
4890
+ target: newConnection.target,
4891
+ sourceHandle: newConnection.sourceHandle,
4892
+ targetHandle: newConnection.targetHandle
4893
+ };
4894
+ return edges.filter((e) => e.id !== oldEdgeId).concat(edge);
4895
+ };
4874
4896
  function getStraightPath({ sourceX, sourceY, targetX, targetY }) {
4875
4897
  const [labelX, labelY, offsetX, offsetY] = getEdgeCenter({
4876
4898
  sourceX,
@@ -7509,6 +7531,12 @@ function addEdge2(edgeParams, edges, options = {}) {
7509
7531
  onError: options.onError ?? defaultOnError
7510
7532
  });
7511
7533
  }
7534
+ function reconnectEdge2(oldEdge, newConnection, edges, options = { shouldReplaceId: true }) {
7535
+ return reconnectEdge(oldEdge, newConnection, edges, {
7536
+ ...options,
7537
+ onError: options.onError ?? defaultOnError
7538
+ });
7539
+ }
7512
7540
  var isNode = (element) => isNodeBase(element);
7513
7541
  var isEdge = (element) => isEdgeBase(element);
7514
7542
  function fixedForwardRef(render) {
@@ -11523,6 +11551,65 @@ function duplicateNode(node, id2, offset = 40) {
11523
11551
  data: JSON.parse(JSON.stringify(node.data ?? {}))
11524
11552
  };
11525
11553
  }
11554
+ function cloneSubgraph(nodes, edges, opts) {
11555
+ const offset = opts.offset ?? 40;
11556
+ const idMap = /* @__PURE__ */ new Map();
11557
+ for (const n of nodes) idMap.set(n.id, opts.makeId());
11558
+ const clonedNodes = nodes.map((n) => {
11559
+ const cloned = duplicateNode(n, idMap.get(n.id), offset);
11560
+ const parentId = n.parentId;
11561
+ if (parentId && idMap.has(parentId)) cloned.parentId = idMap.get(parentId);
11562
+ else if (parentId) delete cloned.parentId;
11563
+ return cloned;
11564
+ });
11565
+ const clonedEdges = edges.filter((e) => idMap.has(e.source) && idMap.has(e.target)).map((e) => ({ ...e, id: opts.makeId(), source: idMap.get(e.source), target: idMap.get(e.target) }));
11566
+ return { nodes: clonedNodes, edges: clonedEdges, idMap };
11567
+ }
11568
+ function reconnectEdge3(edges, oldEdge, newConnection) {
11569
+ return reconnectEdge2(oldEdge, newConnection, edges, { shouldReplaceId: false });
11570
+ }
11571
+ var nodeW = (n) => n.width ?? n.measured?.width ?? 0;
11572
+ var nodeH = (n) => n.height ?? n.measured?.height ?? 0;
11573
+ function alignNodes(nodes, edge) {
11574
+ if (nodes.length < 2) return nodes;
11575
+ const minL = Math.min(...nodes.map((n) => n.position.x));
11576
+ const maxR = Math.max(...nodes.map((n) => n.position.x + nodeW(n)));
11577
+ const minT = Math.min(...nodes.map((n) => n.position.y));
11578
+ const maxB = Math.max(...nodes.map((n) => n.position.y + nodeH(n)));
11579
+ const cx = (minL + maxR) / 2;
11580
+ const cy = (minT + maxB) / 2;
11581
+ return nodes.map((n) => {
11582
+ let { x, y } = n.position;
11583
+ if (edge === "left") x = minL;
11584
+ else if (edge === "right") x = maxR - nodeW(n);
11585
+ else if (edge === "hcenter") x = cx - nodeW(n) / 2;
11586
+ else if (edge === "top") y = minT;
11587
+ else if (edge === "bottom") y = maxB - nodeH(n);
11588
+ else if (edge === "vcenter") y = cy - nodeH(n) / 2;
11589
+ return { ...n, position: { x, y } };
11590
+ });
11591
+ }
11592
+ function distributeNodes(nodes, axis) {
11593
+ if (nodes.length < 3) return nodes;
11594
+ const size = (n) => axis === "h" ? nodeW(n) : nodeH(n);
11595
+ const coord = (n) => axis === "h" ? n.position.x : n.position.y;
11596
+ const sorted = [...nodes].sort((a, b) => coord(a) - coord(b));
11597
+ const start2 = coord(sorted[0]);
11598
+ const last = sorted[sorted.length - 1];
11599
+ const end = coord(last) + size(last);
11600
+ const totalSize = sorted.reduce((s, n) => s + size(n), 0);
11601
+ const gap = (end - start2 - totalSize) / (sorted.length - 1);
11602
+ const posById = /* @__PURE__ */ new Map();
11603
+ let cursor = start2;
11604
+ for (const n of sorted) {
11605
+ posById.set(n.id, cursor);
11606
+ cursor += size(n) + gap;
11607
+ }
11608
+ return nodes.map((n) => {
11609
+ const p = posById.get(n.id);
11610
+ return { ...n, position: axis === "h" ? { ...n.position, x: p } : { ...n.position, y: p } };
11611
+ });
11612
+ }
11526
11613
  var FlowEditorContext = ReactExports.createContext(null);
11527
11614
  var FlowEditorProvider = FlowEditorContext.Provider;
11528
11615
  function useFlowEditor() {
@@ -11692,6 +11779,89 @@ function FlowEditorInner({
11692
11779
  },
11693
11780
  [flow, onEdgeDelete, confirmDelete]
11694
11781
  );
11782
+ const selectedNodes = ReactExports.useMemo(() => flow.nodes.filter((n) => n.selected), [flow.nodes]);
11783
+ const selectedIds = ReactExports.useMemo(() => selectedNodes.map((n) => n.id), [selectedNodes]);
11784
+ const clipboard = ReactExports.useRef(null);
11785
+ const copySelection = ReactExports.useCallback(() => {
11786
+ const sel = flow.nodes.filter((n) => n.selected);
11787
+ if (sel.length === 0) return;
11788
+ const ids = new Set(sel.map((n) => n.id));
11789
+ const edges = flow.edges.filter((e) => ids.has(e.source) && ids.has(e.target));
11790
+ clipboard.current = { nodes: sel.map((n) => ({ ...n })), edges: edges.map((e) => ({ ...e })) };
11791
+ }, [flow]);
11792
+ const pasteClipboard = ReactExports.useCallback(
11793
+ (at) => {
11794
+ const clip = clipboard.current;
11795
+ if (!clip || clip.nodes.length === 0) return;
11796
+ const { nodes: clones, edges: cloneEdges } = cloneSubgraph(clip.nodes, clip.edges, { makeId: newNodeId, offset: 40 });
11797
+ let placed = clones;
11798
+ if (at) {
11799
+ const minX = Math.min(...clones.map((n) => n.position.x));
11800
+ const minY = Math.min(...clones.map((n) => n.position.y));
11801
+ placed = clones.map((n) => ({ ...n, position: { x: n.position.x - minX + at.x, y: n.position.y - minY + at.y } }));
11802
+ }
11803
+ flow.setGraph({
11804
+ nodes: [...flow.nodes.map((n) => ({ ...n, selected: false })), ...placed.map((n) => ({ ...n, selected: true }))],
11805
+ edges: [...flow.edges, ...cloneEdges]
11806
+ });
11807
+ },
11808
+ [flow]
11809
+ );
11810
+ const duplicateSelected = ReactExports.useCallback(() => {
11811
+ const sel = flow.nodes.filter((n) => n.selected);
11812
+ if (sel.length === 0) return;
11813
+ const ids = new Set(sel.map((n) => n.id));
11814
+ const internal2 = flow.edges.filter((e) => ids.has(e.source) && ids.has(e.target));
11815
+ const { nodes: clones, edges: cloneEdges } = cloneSubgraph(sel, internal2, { makeId: newNodeId, offset: 40 });
11816
+ flow.setGraph({
11817
+ nodes: [...flow.nodes.map((n) => ({ ...n, selected: false })), ...clones.map((n) => ({ ...n, selected: true }))],
11818
+ edges: [...flow.edges, ...cloneEdges]
11819
+ });
11820
+ }, [flow]);
11821
+ const alignSelected = ReactExports.useCallback(
11822
+ (edge) => {
11823
+ const sel = flow.nodes.filter((n) => n.selected);
11824
+ if (sel.length < 2) return;
11825
+ const byId = new Map(alignNodes(sel, edge).map((n) => [n.id, n]));
11826
+ flow.setNodes((all) => all.map((n) => byId.get(n.id) ?? n));
11827
+ },
11828
+ [flow]
11829
+ );
11830
+ const distributeSelected = ReactExports.useCallback(
11831
+ (axis) => {
11832
+ const sel = flow.nodes.filter((n) => n.selected);
11833
+ if (sel.length < 3) return;
11834
+ const byId = new Map(distributeNodes(sel, axis).map((n) => [n.id, n]));
11835
+ flow.setNodes((all) => all.map((n) => byId.get(n.id) ?? n));
11836
+ },
11837
+ [flow]
11838
+ );
11839
+ const onReconnect = ReactExports.useCallback(
11840
+ (oldEdge, conn) => flow.setEdges((eds) => reconnectEdge3(eds, oldEdge, conn)),
11841
+ [flow]
11842
+ );
11843
+ ReactExports.useEffect(() => {
11844
+ const onKey = (e) => {
11845
+ const t = e.target;
11846
+ if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
11847
+ if (!(e.ctrlKey || e.metaKey)) return;
11848
+ const key = e.key.toLowerCase();
11849
+ if (key === "c") copySelection();
11850
+ else if (key === "x") {
11851
+ e.preventDefault();
11852
+ copySelection();
11853
+ deleteNodes(selectedIds);
11854
+ } else if (key === "v") {
11855
+ e.preventDefault();
11856
+ pasteClipboard();
11857
+ } else if (key === "d") {
11858
+ e.preventDefault();
11859
+ duplicateSelected();
11860
+ }
11861
+ };
11862
+ window.addEventListener("keydown", onKey);
11863
+ return () => window.removeEventListener("keydown", onKey);
11864
+ }, [copySelection, pasteClipboard, duplicateSelected, deleteNodes, selectedIds]);
11695
11865
  const api = ReactExports.useMemo(() => {
11696
11866
  const toWorkflow = () => exportWorkflow({ nodes: flow.nodes, edges: flow.edges }, metadata);
11697
11867
  return {
@@ -11702,6 +11872,8 @@ function FlowEditorInner({
11702
11872
  selected: selected2,
11703
11873
  selectedEdgeId,
11704
11874
  selectedEdge,
11875
+ selectedIds,
11876
+ selectedNodes,
11705
11877
  running: runner.running,
11706
11878
  statuses: runner.statuses,
11707
11879
  select: setSelectedId,
@@ -11722,6 +11894,15 @@ function FlowEditorInner({
11722
11894
  return copy.id;
11723
11895
  },
11724
11896
  setGraph: (graph) => flow.setGraph(graph),
11897
+ duplicateSelected,
11898
+ alignSelected,
11899
+ distributeSelected,
11900
+ copy: copySelection,
11901
+ cut: () => {
11902
+ copySelection();
11903
+ deleteNodes(selectedIds);
11904
+ },
11905
+ paste: pasteClipboard,
11725
11906
  run: () => runner.run({ nodes: flow.nodes, edges: flow.edges }, executors),
11726
11907
  cancel: runner.cancel,
11727
11908
  reset: runner.reset,
@@ -11734,7 +11915,7 @@ function FlowEditorInner({
11734
11915
  canUndo: hist.canUndo,
11735
11916
  canRedo: hist.canRedo
11736
11917
  };
11737
- }, [flow, selectedId, selected2, selectedEdgeId, selectedEdge, runner, executors, metadata, addNode, deleteNodes, deleteEdges, hist, rf]);
11918
+ }, [flow, selectedId, selected2, selectedEdgeId, selectedEdge, selectedIds, selectedNodes, runner, executors, metadata, addNode, deleteNodes, deleteEdges, duplicateSelected, alignSelected, distributeSelected, copySelection, pasteClipboard, hist, rf]);
11738
11919
  ReactExports.useImperativeHandle(apiRef, () => api, [api]);
11739
11920
  const dropHandlers = paletteDropHandlers((kindName, evt) => {
11740
11921
  const point = rf.screenToFlowPosition({ x: evt.clientX, y: evt.clientY });
@@ -11794,6 +11975,8 @@ function FlowEditorInner({
11794
11975
  onNodesChange: flow.onNodesChange,
11795
11976
  onEdgesChange: flow.onEdgesChange,
11796
11977
  onConnect: flow.onConnect,
11978
+ onReconnect,
11979
+ edgesReconnectable: true,
11797
11980
  onNodeDragStart: hist.onNodeDragStart,
11798
11981
  onNodeClick: handleNodeClick2,
11799
11982
  onNodeContextMenu: builtins.contextMenu === false ? void 0 : handleNodeContextMenu,
@@ -12083,9 +12266,11 @@ exports.SubgraphNode = SubgraphNode;
12083
12266
  exports.TriggerNode = TriggerNode;
12084
12267
  exports.WORKFLOW_SCHEMA_URL = WORKFLOW_SCHEMA_URL;
12085
12268
  exports.WORKFLOW_SCHEMA_VERSION = WORKFLOW_SCHEMA_VERSION;
12269
+ exports.alignNodes = alignNodes;
12086
12270
  exports.applyStatusesToNodes = applyStatusesToNodes;
12087
12271
  exports.buildNodeTypes = buildNodeTypes;
12088
12272
  exports.categoryAccent = categoryAccent;
12273
+ exports.cloneSubgraph = cloneSubgraph;
12089
12274
  exports.createConnectionValidator = createConnectionValidator;
12090
12275
  exports.createHistory = createHistory;
12091
12276
  exports.decodePause = decodePause;
@@ -12093,6 +12278,7 @@ exports.defaultConfigFor = defaultConfigFor;
12093
12278
  exports.defaultNodeTypes = defaultNodeTypes;
12094
12279
  exports.defaultPortCompatibility = defaultPortCompatibility;
12095
12280
  exports.defineNode = defineNode;
12281
+ exports.distributeNodes = distributeNodes;
12096
12282
  exports.encodePause = encodePause;
12097
12283
  exports.exportWorkflow = exportWorkflow;
12098
12284
  exports.getNodeKind = getNodeKind;
@@ -12105,6 +12291,7 @@ exports.onNodeKindsChanged = onNodeKindsChanged;
12105
12291
  exports.onRichInputAdapterChanged = onRichInputAdapterChanged;
12106
12292
  exports.paletteDropHandlers = paletteDropHandlers;
12107
12293
  exports.pauseForHuman = pauseForHuman;
12294
+ exports.reconnectEdge = reconnectEdge3;
12108
12295
  exports.registerBuiltinKinds = registerBuiltinKinds;
12109
12296
  exports.registerNodeKind = registerNodeKind;
12110
12297
  exports.registerRichInputAdapter = registerRichInputAdapter;