react-archer
Version:
Draw arrows between DOM elements in React
185 lines (163 loc) • 3.83 kB
JavaScript
// @flow
import React from 'react';
import Point from './Point';
type Props = {
startingPoint: Point,
startingAnchor: AnchorPositionType,
endingPoint: Point,
endingAnchor: AnchorPositionType,
strokeColor: string,
arrowLength: number,
strokeWidth: number,
arrowLabel?: ?React$Node,
arrowMarkerId: string,
};
function computeEndingArrowDirectionVector(endingAnchor) {
switch (endingAnchor) {
case 'left':
return { arrowX: -1, arrowY: 0 };
case 'right':
return { arrowX: 1, arrowY: 0 };
case 'top':
return { arrowX: 0, arrowY: -1 };
case 'bottom':
return { arrowX: 0, arrowY: 1 };
default:
return { arrowX: 0, arrowY: 0 };
}
}
export function computeEndingPointAccordingToArrow(
xEnd: number,
yEnd: number,
arrowLength: number,
strokeWidth: number,
endingAnchor: AnchorPositionType,
) {
const { arrowX, arrowY } = computeEndingArrowDirectionVector(endingAnchor);
const xe = xEnd + (arrowX * arrowLength * strokeWidth) / 2;
const ye = yEnd + (arrowY * arrowLength * strokeWidth) / 2;
return { xe, ye };
}
export function computeStartingAnchorPosition(
xs: number,
ys: number,
xe: number,
ye: number,
startingAnchor: AnchorPositionType,
): { xa1: number, ya1: number } {
if (startingAnchor === 'top' || startingAnchor === 'bottom') {
return {
xa1: xs,
ya1: ys + (ye - ys) / 2,
};
}
if (startingAnchor === 'left' || startingAnchor === 'right') {
return {
xa1: xs + (xe - xs) / 2,
ya1: ys,
};
}
return { xa1: xs, ya1: ys };
}
export function computeEndingAnchorPosition(
xs: number,
ys: number,
xe: number,
ye: number,
endingAnchor: AnchorPositionType,
): { xa2: number, ya2: number } {
if (endingAnchor === 'top' || endingAnchor === 'bottom') {
return {
xa2: xe,
ya2: ye - (ye - ys) / 2,
};
}
if (endingAnchor === 'left' || endingAnchor === 'right') {
return {
xa2: xe - (xe - xs) / 2,
ya2: ye,
};
}
return { xa2: xe, ya2: ye };
}
export function computeLabelDimensions(
xs: number,
ys: number,
xe: number,
ye: number,
): { xl: number, yl: number, wl: number, hl: number } {
const wl = Math.abs(xe - xs);
const hl = Math.abs(ye - ys);
const xl = xe > xs ? xs : xe;
const yl = ye > ys ? ys : ye;
return {
xl,
yl,
wl,
hl,
};
}
const SvgArrow = ({
startingPoint,
startingAnchor,
endingPoint,
endingAnchor,
strokeColor,
arrowLength,
strokeWidth,
arrowLabel,
arrowMarkerId,
}: Props) => {
const actualArrowLength = arrowLength * 2;
const xs = startingPoint.x;
const ys = startingPoint.y;
const { xe, ye } = computeEndingPointAccordingToArrow(
endingPoint.x,
endingPoint.y,
actualArrowLength,
strokeWidth,
endingAnchor,
);
const { xa1, ya1 } = computeStartingAnchorPosition(
xs,
ys,
xe,
ye,
startingAnchor,
);
const { xa2, ya2 } = computeEndingAnchorPosition(
xs,
ys,
xe,
ye,
endingAnchor,
);
const pathString =
`M${xs},${ys} ` + `C${xa1},${ya1} ${xa2},${ya2} ` + `${xe},${ye}`;
const { xl, yl, wl, hl } = computeLabelDimensions(xs, ys, xe, ye);
return (
<g>
<path
d={pathString}
style={{ fill: 'none', stroke: strokeColor, strokeWidth }}
markerEnd={`url(${location.href}#${arrowMarkerId})`}
/>
{arrowLabel && (
<foreignObject x={xl} y={yl} width={wl} height={hl}>
<div
style={{
width: wl,
height: hl,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<div>{arrowLabel}</div>
</div>
</foreignObject>
)}
</g>
);
};
export default SvgArrow;