diff --git a/README.md b/README.md
index 92de8c8f..d7b513e9 100644
--- a/README.md
+++ b/README.md
@@ -91,10 +91,12 @@ const BasicFlow = () => ;
- `nodesDraggable`: default: `true`
- `nodesConnectable`: default: `true`
- `elementsSelectable`: default: `true`
+- `zoomOnScroll`: default: `true`
- `selectNodesOnDrag`: default: `true`
- `minZoom`: default: `0.5`
- `maxZoom`: default: `2`
- `defaultZoom`: default: `1`
+- `arrowHeadColor`: default: `#bbb`
## React Flow Instance
@@ -247,6 +249,8 @@ If you wanted to display this edge, you would need a node with id = 1 (source no
- `labelStyle`: css properties for the text
- `labelShowBg`: boolean - default: `true`
- `labelBgStyle`: css properties for the text background
+- `arrowHeadType`: 'arrow' or 'arrowclosed' - defines the arrowhead of the edge
+- `markerEndId`: custom marker end url - if this is used `arrowHeadType` gets ignored
You can find an example with lots of different edges in the [edges example](https://react-flow.netlify.app/edges).
diff --git a/example/src/Edges/index.js b/example/src/Edges/index.js
index 81f87136..545d2e4a 100644
--- a/example/src/Edges/index.js
+++ b/example/src/Edges/index.js
@@ -25,13 +25,21 @@ const initialElements = [
{ id: 'e3-4', source: '3', target: '4', type: 'straight', label: 'straight edge' },
{ id: 'e3-3a', source: '3', target: '3a', type: 'straight', label: 'label only edge', style: { stroke: 'none' } },
{ id: 'e3-5', source: '4', target: '5', animated: true, label: 'animated styled edge', style: { stroke: 'red' } },
- { id: 'e5-6', source: '5', target: '6', label: 'styled label', labelStyle: { fill: 'red', fontWeight: 700 } },
+ {
+ id: 'e5-6',
+ source: '5',
+ target: '6',
+ label: 'styled label',
+ labelStyle: { fill: 'red', fontWeight: 700 },
+ arrowHeadType: 'arrow',
+ },
{
id: 'e5-7',
source: '5',
target: '7',
label: 'label with styled bg',
labelBgStyle: { fill: '#eee', fillOpacity: 0.7 },
+ arrowHeadType: 'arrowclosed',
},
{ id: 'e5-8', source: '5', target: '8', type: 'custom', label: 'custom edge' },
];
diff --git a/example/src/Interaction/index.js b/example/src/Interaction/index.js
index 26281ec4..406405f0 100644
--- a/example/src/Interaction/index.js
+++ b/example/src/Interaction/index.js
@@ -18,6 +18,7 @@ const InteractionFlow = () => {
const [isSelectable, setIsSelectable] = useState(false);
const [isDraggable, setIsDraggable] = useState(false);
const [isConnectable, setIsConnectable] = useState(false);
+ const [zoomOnScroll, setZoomOnScroll] = useState(true);
return (
{
elementsSelectable={isSelectable}
nodesConnectable={isConnectable}
nodesDraggable={isDraggable}
+ zoomOnScroll={zoomOnScroll}
onConnect={onConnect}
>
@@ -67,6 +69,18 @@ const InteractionFlow = () => {
/>
+
+
+
);
diff --git a/example/src/Overview/index.js b/example/src/Overview/index.js
index dffbb66f..6969995a 100644
--- a/example/src/Overview/index.js
+++ b/example/src/Overview/index.js
@@ -70,7 +70,7 @@ const initialElements = [
>
),
},
- position: { x: 250, y: 300 },
+ position: { x: 250, y: 325 },
},
{
id: '6',
@@ -88,7 +88,7 @@ const initialElements = [
{ id: 'e1-2', source: '1', target: '2', label: 'this is an edge label' },
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e3-4', source: '3', target: '4', animated: true, label: 'animated edge' },
- { id: 'e4-5', source: '4', target: '5' },
+ { id: 'e4-5', source: '4', target: '5', arrowHeadType: 'arrowclosed', label: 'edge with arrow head' },
{ id: 'e5-6', source: '5', target: '6', type: 'smoothstep', label: 'smooth step edge' },
{
id: 'e5-7',
diff --git a/package-lock.json b/package-lock.json
index cfff3d5d..332a4f58 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6177,6 +6177,11 @@
"integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
"dev": true
},
+ "classcat": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/classcat/-/classcat-4.1.0.tgz",
+ "integrity": "sha512-RA8O5oCi1I1CF6rR4cRBROh8MtZzM4w7xKLm0jd+S6UN2G4FIto+9DVOeFc46JEZFN5PVe/EZWLQO1VU/AUH4A=="
+ },
"classnames": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
diff --git a/package.json b/package.json
index b923d423..a40a57bf 100644
--- a/package.json
+++ b/package.json
@@ -24,7 +24,7 @@
},
"dependencies": {
"@welldone-software/why-did-you-render": "^4.2.5",
- "classnames": "^2.2.6",
+ "classcat": "^4.1.0",
"d3-selection": "^1.4.1",
"d3-zoom": "^1.8.3",
"easy-peasy": "^3.3.1",
diff --git a/src/additional-components/Background/index.tsx b/src/additional-components/Background/index.tsx
index 4228a089..86a24a09 100644
--- a/src/additional-components/Background/index.tsx
+++ b/src/additional-components/Background/index.tsx
@@ -1,5 +1,5 @@
import React, { memo, HTMLAttributes } from 'react';
-import classnames from 'classnames';
+import cc from 'classcat';
import { useStoreState } from '../../store/hooks';
import { BackgroundVariant } from '../../types';
@@ -25,7 +25,7 @@ const Background = memo(
const height = useStoreState((s) => s.height);
const [x, y, scale] = useStoreState((s) => s.transform);
- const bgClasses = classnames('react-flow__background', className);
+ const bgClasses = cc(['react-flow__background', className]);
const bgColor = color ? color : defaultColors[variant];
const scaledGap = gap * scale;
const xOffset = x % scaledGap;
diff --git a/src/additional-components/Controls/index.tsx b/src/additional-components/Controls/index.tsx
index 0852f340..d1c78370 100644
--- a/src/additional-components/Controls/index.tsx
+++ b/src/additional-components/Controls/index.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import classnames from 'classnames';
+import cc from 'classcat';
import { useStoreState, useStoreActions } from '../../store/hooks';
@@ -24,7 +24,7 @@ const Controls = ({ style, showZoom = true, showFitView = true, showInteractive
const zoomOut = useStoreActions((actions) => actions.zoomOut);
const isInteractive = useStoreState((s) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable);
- const mapClasses = classnames('react-flow__controls', className);
+ const mapClasses = cc(['react-flow__controls', className]);
return (
diff --git a/src/additional-components/MiniMap/index.tsx b/src/additional-components/MiniMap/index.tsx
index 021ed7c1..6c109d7f 100644
--- a/src/additional-components/MiniMap/index.tsx
+++ b/src/additional-components/MiniMap/index.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import classnames from 'classnames';
+import cc from 'classcat';
import { useStoreState } from '../../store/hooks';
import { getRectOfNodes, getBoundsofRects } from '../../utils/graph';
@@ -31,7 +31,7 @@ const MiniMap = ({
const [tX, tY, tScale] = useStoreState((s) => s.transform);
const nodes = useStoreState((s) => s.nodes);
- const mapClasses = classnames('react-flow__minimap', className);
+ const mapClasses = cc(['react-flow__minimap', className]);
const elementWidth = (style.width || defaultWidth)! as number;
const elementHeight = (style.height || defaultHeight)! as number;
const nodeColorFunc = (nodeColor instanceof Function ? nodeColor : () => nodeColor) as StringFunc;
diff --git a/src/components/ConnectionLine/index.tsx b/src/components/ConnectionLine/index.tsx
index d7b7033d..6daba9cb 100644
--- a/src/components/ConnectionLine/index.tsx
+++ b/src/components/ConnectionLine/index.tsx
@@ -1,5 +1,5 @@
import React, { useEffect, useState, CSSProperties } from 'react';
-import cx from 'classnames';
+import cc from 'classcat';
import { getBezierPath } from '../Edges/BezierEdge';
import { getStepPath } from '../Edges/StepEdge';
@@ -46,7 +46,7 @@ export default ({
return null;
}
- const connectionLineClasses: string = cx('react-flow__connection', className);
+ const connectionLineClasses: string = cc(['react-flow__connection', className]);
const sourceHandle = handleId
? sourceNode.__rf.handleBounds[connectionHandleType].find((d: HandleElement) => d.id === handleId)
diff --git a/src/components/Edges/BezierEdge.tsx b/src/components/Edges/BezierEdge.tsx
index 816ed3db..9b27b6fc 100644
--- a/src/components/Edges/BezierEdge.tsx
+++ b/src/components/Edges/BezierEdge.tsx
@@ -1,6 +1,7 @@
import React, { memo } from 'react';
import EdgeText from './EdgeText';
+import { getMarkerEnd } from './utils';
import { EdgeBezierProps, Position } from '../../types';
interface GetBezierPathParams {
@@ -52,6 +53,8 @@ export default memo(
labelShowBg,
labelBgStyle,
style,
+ arrowHeadType,
+ markerEndId,
}: EdgeBezierProps) => {
const yOffset = Math.abs(targetY - sourceY) / 2;
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
@@ -81,9 +84,11 @@ export default memo(
/>
) : null;
+ const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
+
return (
<>
-
+
{text}
>
);
diff --git a/src/components/Edges/SmoothStepEdge.tsx b/src/components/Edges/SmoothStepEdge.tsx
index 8f61e7c8..a203f28c 100644
--- a/src/components/Edges/SmoothStepEdge.tsx
+++ b/src/components/Edges/SmoothStepEdge.tsx
@@ -1,6 +1,7 @@
import React, { memo } from 'react';
import EdgeText from './EdgeText';
+import { getMarkerEnd } from './utils';
import { EdgeBezierProps, Position } from '../../types';
// These are some helper methods for drawing the round corners
@@ -138,6 +139,8 @@ export default memo(
style,
sourcePosition = Position.Bottom,
targetPosition = Position.Top,
+ arrowHeadType,
+ markerEndId,
}: EdgeBezierProps) => {
const yOffset = Math.abs(targetY - sourceY) / 2;
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
@@ -158,6 +161,8 @@ export default memo(
targetPosition,
});
+ const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
+
const text = label ? (
-
+
{text}
>
);
diff --git a/src/components/Edges/StepEdge.tsx b/src/components/Edges/StepEdge.tsx
index 6a0af45d..9475685b 100644
--- a/src/components/Edges/StepEdge.tsx
+++ b/src/components/Edges/StepEdge.tsx
@@ -1,6 +1,7 @@
import React, { memo } from 'react';
import EdgeText from './EdgeText';
+import { getMarkerEnd } from './utils';
import { EdgeProps } from '../../types';
interface GetStepPathParams {
@@ -16,7 +17,19 @@ export function getStepPath({ centerY, sourceX, sourceY, targetX, targetY }: Get
}
export default memo(
- ({ sourceX, sourceY, targetX, targetY, label, labelStyle, labelShowBg, labelBgStyle, style }: EdgeProps) => {
+ ({
+ sourceX,
+ sourceY,
+ targetX,
+ targetY,
+ label,
+ labelStyle,
+ labelShowBg,
+ labelBgStyle,
+ style,
+ arrowHeadType,
+ markerEndId,
+ }: EdgeProps) => {
const yOffset = Math.abs(targetY - sourceY) / 2;
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
@@ -25,6 +38,8 @@ export default memo(
const path = getStepPath({ centerY, sourceX, sourceY, targetX, targetY });
+ const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
+
const text = label ? (
-
+
{text}
>
);
diff --git a/src/components/Edges/StraightEdge.tsx b/src/components/Edges/StraightEdge.tsx
index 13b62620..8721cf44 100644
--- a/src/components/Edges/StraightEdge.tsx
+++ b/src/components/Edges/StraightEdge.tsx
@@ -1,15 +1,29 @@
import React, { memo } from 'react';
import EdgeText from './EdgeText';
+import { getMarkerEnd } from './utils';
import { EdgeProps } from '../../types';
export default memo(
- ({ sourceX, sourceY, targetX, targetY, label, labelStyle, labelShowBg, labelBgStyle, style }: EdgeProps) => {
+ ({
+ sourceX,
+ sourceY,
+ targetX,
+ targetY,
+ label,
+ labelStyle,
+ labelShowBg,
+ labelBgStyle,
+ style,
+ arrowHeadType,
+ markerEndId,
+ }: EdgeProps) => {
const yOffset = Math.abs(targetY - sourceY) / 2;
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
const xOffset = Math.abs(targetX - sourceX) / 2;
const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset;
+ const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
const text = label ? (
-
+
{text}
>
);
diff --git a/src/components/Edges/index.ts b/src/components/Edges/index.ts
new file mode 100644
index 00000000..65beb017
--- /dev/null
+++ b/src/components/Edges/index.ts
@@ -0,0 +1,4 @@
+export { default as BezierEdge } from './BezierEdge';
+export { default as StepEdge } from './StepEdge';
+export { default as SmoothStepEdge } from './SmoothStepEdge';
+export { default as StraightEdge } from './StraightEdge';
diff --git a/src/components/Edges/utils.ts b/src/components/Edges/utils.ts
new file mode 100644
index 00000000..a25f135c
--- /dev/null
+++ b/src/components/Edges/utils.ts
@@ -0,0 +1,9 @@
+import { ArrowHeadType } from '../../types';
+
+export const getMarkerEnd = (arrowHeadType?: ArrowHeadType, markerEndId?: string): string => {
+ if (typeof markerEndId !== 'undefined' && markerEndId) {
+ return `url(#${markerEndId})`;
+ }
+
+ return typeof arrowHeadType !== 'undefined' ? `url(#react-flow__${arrowHeadType})` : 'none';
+};
diff --git a/src/components/Edges/wrapEdge.tsx b/src/components/Edges/wrapEdge.tsx
index 2b45535e..01eda6b7 100644
--- a/src/components/Edges/wrapEdge.tsx
+++ b/src/components/Edges/wrapEdge.tsx
@@ -1,5 +1,5 @@
import React, { memo, ComponentType, CSSProperties } from 'react';
-import cx from 'classnames';
+import cc from 'classcat';
import { useStoreActions } from '../../store/hooks';
import { ElementId, Edge, EdgeCompProps } from '../../types';
@@ -39,7 +39,7 @@ export default (EdgeComponent: ComponentType) => {
...rest
}: EdgeWrapperProps) => {
const setSelectedElements = useStoreActions((a) => a.setSelectedElements);
- const edgeClasses = cx('react-flow__edge', `react-flow__edge-${type}`, className, { selected, animated });
+ const edgeClasses = cc(['react-flow__edge', `react-flow__edge-${type}`, className, { selected, animated }]);
const edgeGroupStyle: CSSProperties = {
pointerEvents: elementsSelectable ? 'all' : 'none',
};
diff --git a/src/components/Handle/BaseHandle.tsx b/src/components/Handle/BaseHandle.tsx
index 57cc5443..7a16a001 100644
--- a/src/components/Handle/BaseHandle.tsx
+++ b/src/components/Handle/BaseHandle.tsx
@@ -1,5 +1,5 @@
import React, { memo, MouseEvent as ReactMouseEvent, CSSProperties } from 'react';
-import cx from 'classnames';
+import cc from 'classcat';
import { HandleType, ElementId, Position, XYPosition, OnConnectFunc, Connection, SetConnectionId } from '../../types';
@@ -144,10 +144,16 @@ const BaseHandle = memo(
...rest
}: BaseHandleProps) => {
const isTarget = type === 'target';
- const handleClasses = cx('react-flow__handle', `react-flow__handle-${position}`, 'nodrag', className, {
- source: !isTarget,
- target: isTarget,
- });
+ const handleClasses = cc([
+ 'react-flow__handle',
+ `react-flow__handle-${position}`,
+ 'nodrag',
+ className,
+ {
+ source: !isTarget,
+ target: isTarget,
+ },
+ ]);
const nodeIdWithHandleId = id ? `${nodeId}__${id}` : nodeId;
diff --git a/src/components/Handle/index.tsx b/src/components/Handle/index.tsx
index 4c4d8875..adfdacd2 100644
--- a/src/components/Handle/index.tsx
+++ b/src/components/Handle/index.tsx
@@ -1,5 +1,5 @@
import React, { memo, useContext } from 'react';
-import classnames from 'classnames';
+import cc from 'classcat';
import { useStoreActions, useStoreState } from '../../store/hooks';
import BaseHandle from './BaseHandle';
@@ -26,7 +26,7 @@ const Handle = memo(
onConnectAction(params);
onConnect(params);
};
- const handleClasses = classnames(className, { connectable: isConnectable });
+ const handleClasses = cc([className, { connectable: isConnectable }]);
return (
) => {
const [offset, setOffset] = useState({ x: 0, y: 0 });
const [isDragging, setDragging] = useState(false);
const position = { x: xPos, y: yPos };
- const nodeClasses = cx('react-flow__node', `react-flow__node-${type}`, className, {
- selected,
- selectable: isSelectable,
- });
+ const nodeClasses = cc([
+ 'react-flow__node',
+ `react-flow__node-${type}`,
+ className,
+ {
+ selected,
+ selectable: isSelectable,
+ },
+ ]);
const node = { id, type, position, data };
const onMouseEnterHandler = useMemo(() => {
if (!onMouseEnter || isDragging) {
diff --git a/src/container/EdgeRenderer/MarkerDefinitions.tsx b/src/container/EdgeRenderer/MarkerDefinitions.tsx
new file mode 100644
index 00000000..2fceee6c
--- /dev/null
+++ b/src/container/EdgeRenderer/MarkerDefinitions.tsx
@@ -0,0 +1,56 @@
+import React, { ReactNode } from 'react';
+
+interface MarkerProps {
+ id: string;
+ children: ReactNode;
+}
+
+const Marker = ({ id, children }: MarkerProps) => (
+
+ {children}
+
+);
+
+interface MarkerDefinitionsProps {
+ color: string;
+}
+
+const MarkerDefinitions = ({ color }: MarkerDefinitionsProps) => {
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+MarkerDefinitions.displayName = 'MarkerDefinitions';
+
+export default MarkerDefinitions;
diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx
index 3ecfd03a..f1a215f1 100644
--- a/src/container/EdgeRenderer/index.tsx
+++ b/src/container/EdgeRenderer/index.tsx
@@ -3,6 +3,7 @@ import React, { memo, CSSProperties } from 'react';
import { useStoreState } from '../../store/hooks';
import ConnectionLine from '../../components/ConnectionLine/index';
import { isEdge } from '../../utils/graph';
+import MarkerDefinitions from './MarkerDefinitions';
import { XYPosition, Position, Edge, Node, ElementId, HandleElement, Elements, ConnectionLineType } from '../../types';
interface EdgeRendererProps {
@@ -12,6 +13,8 @@ interface EdgeRendererProps {
connectionLineType: ConnectionLineType;
connectionLineStyle?: CSSProperties;
onElementClick?: (element: Node | Edge) => void;
+ arrowHeadColor: string;
+ markerEndId?: string;
}
interface EdgePositions {
@@ -171,6 +174,7 @@ function renderEdge(
labelShowBg={edge.labelShowBg}
labelBgStyle={edge.labelBgStyle}
style={edge.style}
+ arrowHeadType={edge.arrowHeadType}
source={sourceId}
target={targetId}
sourceHandleId={sourceHandleId}
@@ -182,6 +186,7 @@ function renderEdge(
sourcePosition={sourcePosition}
targetPosition={targetPosition}
elementsSelectable={elementsSelectable}
+ markerEndId={props.markerEndId}
/>
);
}
@@ -197,7 +202,7 @@ const EdgeRenderer = memo((props: EdgeRendererProps) => {
const nodesConnectable = useStoreState((s) => s.nodesConnectable);
const elementsSelectable = useStoreState((s) => s.elementsSelectable);
- const { width, height, connectionLineStyle, connectionLineType } = props;
+ const { width, height, connectionLineStyle, connectionLineType, arrowHeadColor } = props;
if (!width) {
return null;
@@ -208,6 +213,7 @@ const EdgeRenderer = memo((props: EdgeRendererProps) => {
return (