feat: Transform react-flow to vue-jsx

This commit is contained in:
Braks
2021-07-08 23:37:11 +02:00
parent 4025ec82cb
commit 52e1188d04
46 changed files with 5463 additions and 94 deletions
+163
View File
@@ -0,0 +1,163 @@
import { CSSProperties, defineComponent, PropType } from 'vue';
import EdgeText from './EdgeText';
import { getMarkerEnd, getCenter } from './utils';
import { ArrowHeadType, Position } from '../../types';
interface GetBezierPathParams {
sourceX: number;
sourceY: number;
sourcePosition?: Position;
targetX: number;
targetY: number;
targetPosition?: Position;
centerX?: number;
centerY?: number;
}
export function getBezierPath({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
centerX,
centerY
}: GetBezierPathParams): string {
const [_centerX, _centerY] = getCenter({ sourceX, sourceY, targetX, targetY });
const leftAndRight = [Position.Left, Position.Right];
const cX = typeof centerX !== 'undefined' ? centerX : _centerX;
const cY = typeof centerY !== 'undefined' ? centerY : _centerY;
let path = `M${sourceX},${sourceY} C${sourceX},${cY} ${targetX},${cY} ${targetX},${targetY}`;
if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
path = `M${sourceX},${sourceY} C${cX},${sourceY} ${cX},${targetY} ${targetX},${targetY}`;
} else if (leftAndRight.includes(targetPosition)) {
path = `M${sourceX},${sourceY} C${sourceX},${targetY} ${sourceX},${targetY} ${targetX},${targetY}`;
} else if (leftAndRight.includes(sourcePosition)) {
path = `M${sourceX},${sourceY} C${targetX},${sourceY} ${targetX},${sourceY} ${targetX},${targetY}`;
}
return path;
}
const BezierEdge = defineComponent({
props: {
sourceX: {
type: Number,
required: true,
default: 0
},
sourceY: {
type: Number,
required: true,
default: 0
},
targetX: {
type: Number,
required: true,
default: 0
},
targetY: {
type: Number,
required: true,
default: 0
},
sourcePosition: {
type: String as PropType<Position>,
required: true,
default: Position.Bottom
},
targetPosition: {
type: String as PropType<Position>,
required: true,
default: Position.Top
},
label: {
type: String,
required: true
},
labelStyle: {
type: Object as PropType<any>,
required: true,
default: () => ({})
},
labelShowBg: {
type: Boolean,
required: false,
default: false
},
labelBgStyle: {
type: String,
required: true
},
labelBgPadding: {
type: ([0, 0] as any) as PropType<[number, number]>,
required: false,
default: () => [0, 0] as [number, number]
},
labelBgBorderRadius: {
type: Number,
required: false,
default: 0
},
arrowHeadType: {
type: Object as PropType<ArrowHeadType>,
required: true
},
markerEndId: {
type: String,
required: true
},
style: {
type: Object as PropType<CSSProperties>,
required: false,
default: () => ({})
}
},
setup(props) {
const [centerX, centerY] = getCenter({
sourceX: props.sourceX,
sourceY: props.sourceY,
targetX: props.targetX,
targetY: props.targetY,
sourcePosition: props.sourcePosition,
targetPosition: props.targetPosition
});
const path = getBezierPath({
sourceX: props.sourceX,
sourceY: props.sourceY,
targetX: props.targetX,
targetY: props.targetY,
targetPosition: props.targetPosition
});
const text = props.label ? (
<EdgeText
x={centerX}
y={centerY}
label={props.label}
labelStyle={props.labelStyle}
labelShowBg={props.labelShowBg}
labelBgStyle={props.labelBgStyle}
labelBgPadding={props.labelBgPadding}
labelBgBorderRadius={props.labelBgBorderRadius}
/>
) : null;
const markerEnd = getMarkerEnd(props.arrowHeadType, props.markerEndId);
return (
<>
<path style={props.style} d={path} class="react-flow__edge-path" marker-end={markerEnd} />
{text}
</>
);
}
});
export default BezierEdge;
+56
View File
@@ -0,0 +1,56 @@
import { Position } from '../../types';
import { defineComponent, HTMLAttributes, PropType } from 'vue';
const shiftX = (x: number, shift: number, position: Position): number => {
if (position === Position.Left) return x - shift;
if (position === Position.Right) return x + shift;
return x;
};
const shiftY = (y: number, shift: number, position: Position): number => {
if (position === Position.Top) return y - shift;
if (position === Position.Bottom) return y + shift;
return y;
};
export interface EdgeAnchorProps extends HTMLAttributes {
position: Position;
centerX: number;
centerY: number;
radius?: number;
}
export const EdgeAnchor = defineComponent({
props: {
position: {
type: String as PropType<EdgeAnchorProps['position']>,
required: true
},
centerX: {
type: Number as PropType<EdgeAnchorProps['centerX']>,
required: true
},
centerY: {
type: Number as PropType<EdgeAnchorProps['centerY']>,
required: true
},
radius: {
type: Number as PropType<EdgeAnchorProps['radius']>,
required: false,
default: 10
}
},
setup(props) {
const radius = props.radius || 10;
return () => (
<circle
class="react-flow__edgeupdater"
cx={shiftX(props.centerX, radius, props.position)}
cy={shiftY(props.centerY, radius, props.position)}
r={radius}
stroke="transparent"
fill="transparent"
/>
);
}
});
+88
View File
@@ -0,0 +1,88 @@
import { EdgeTextProps, Rect } from '../../types';
import { defineComponent, PropType, ref, watch } from 'vue';
const EdgeText = defineComponent({
props: {
x: {
type: Number as PropType<EdgeTextProps['x']>,
required: true
},
y: {
type: Number as PropType<EdgeTextProps['y']>,
required: true
},
label: {
type: String as PropType<EdgeTextProps['label']>,
required: true
},
labelStyle: {
type: Object as PropType<EdgeTextProps['labelStyle']>,
default: () => ({})
},
labelShowBg: {
type: Boolean as PropType<EdgeTextProps['labelShowBg']>,
default: true
},
labelBgStyle: {
type: Object as PropType<EdgeTextProps['labelBgStyle']>,
default: () => ({})
},
labelBgPadding: {
/* @ts-ignore */
type: Array as PropType<[number, number]>,
default: () => [2, 4]
},
labelBgBorderRadius: {
type: Number as PropType<EdgeTextProps['labelBgBorderRadius']>,
default: 2
}
},
setup(props, { slots }) {
const edgeRef = ref<SVGTextElement | null>(null);
const edgeTextBox = ref<Rect>({ x: 0, y: 0, width: 0, height: 0 });
const label = ref(props.label);
watch(label, () => {
if (edgeRef.value) {
const textBbox = edgeRef.value.getBBox();
edgeRef.value = {
x: textBbox.x,
y: textBbox.y,
width: textBbox.width,
height: textBbox.height
} as any;
}
});
if (typeof label.value === 'undefined' || !label.value) {
return null;
}
return (
<g
transform={`translate(${props.x - edgeTextBox.value.width / 2} ${props.y - edgeTextBox.value.height / 2})`}
class="react-flow__edge-textwrapper"
>
{props.labelShowBg && (
<rect
width={edgeTextBox.value.width + 2 * props.labelBgPadding[0]}
x={-props.labelBgPadding[0]}
y={-props.labelBgPadding[1]}
height={edgeTextBox.value.height + 2 * props.labelBgPadding[1]}
class="react-flow__edge-textbg"
style={props.labelBgStyle}
rx={props.labelBgBorderRadius}
ry={props.labelBgBorderRadius}
/>
)}
<text class="react-flow__edge-text" y={edgeTextBox.value.height / 2} dy="0.3em" ref={edgeRef} style={props.labelStyle}>
{label}
</text>
{slots.default ? slots.default() : ''}
</g>
);
}
});
export default EdgeText;
+141
View File
@@ -0,0 +1,141 @@
import { defineComponent } from 'vue';
import EdgeText from './EdgeText';
import { getMarkerEnd, getCenter } from './utils';
import { EdgeSmoothStepProps, Position } from '../../types';
// These are some helper methods for drawing the round corners
// The name indicates the direction of the path. "bottomLeftCorner" goes
// from bottom to the left and "leftBottomCorner" goes from left to the bottom.
// We have to consider the direction of the paths because of the animated lines.
const bottomLeftCorner = (x: number, y: number, size: number): string => `L ${x},${y - size}Q ${x},${y} ${x + size},${y}`;
const leftBottomCorner = (x: number, y: number, size: number): string => `L ${x + size},${y}Q ${x},${y} ${x},${y - size}`;
const bottomRightCorner = (x: number, y: number, size: number): string => `L ${x},${y - size}Q ${x},${y} ${x - size},${y}`;
const rightBottomCorner = (x: number, y: number, size: number): string => `L ${x - size},${y}Q ${x},${y} ${x},${y - size}`;
const leftTopCorner = (x: number, y: number, size: number): string => `L ${x + size},${y}Q ${x},${y} ${x},${y + size}`;
const topLeftCorner = (x: number, y: number, size: number): string => `L ${x},${y + size}Q ${x},${y} ${x + size},${y}`;
const topRightCorner = (x: number, y: number, size: number): string => `L ${x},${y + size}Q ${x},${y} ${x - size},${y}`;
const rightTopCorner = (x: number, y: number, size: number): string => `L ${x - size},${y}Q ${x},${y} ${x},${y + size}`;
interface GetSmoothStepPathParams {
sourceX: number;
sourceY: number;
sourcePosition?: Position;
targetX: number;
targetY: number;
targetPosition?: Position;
borderRadius?: number;
centerX?: number;
centerY?: number;
}
export function getSmoothStepPath({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
borderRadius = 5,
centerX,
centerY
}: GetSmoothStepPathParams): string {
const [_centerX, _centerY, offsetX, offsetY] = getCenter({ sourceX, sourceY, targetX, targetY });
const cornerWidth = Math.min(borderRadius, Math.abs(targetX - sourceX));
const cornerHeight = Math.min(borderRadius, Math.abs(targetY - sourceY));
const cornerSize = Math.min(cornerWidth, cornerHeight, offsetX, offsetY);
const leftAndRight = [Position.Left, Position.Right];
const cX = typeof centerX !== 'undefined' ? centerX : _centerX;
const cY = typeof centerY !== 'undefined' ? centerY : _centerY;
let firstCornerPath;
let secondCornerPath;
if (sourceX <= targetX) {
firstCornerPath = sourceY <= targetY ? bottomLeftCorner(sourceX, cY, cornerSize) : topLeftCorner(sourceX, cY, cornerSize);
secondCornerPath = sourceY <= targetY ? rightTopCorner(targetX, cY, cornerSize) : rightBottomCorner(targetX, cY, cornerSize);
} else {
firstCornerPath = sourceY < targetY ? bottomRightCorner(sourceX, cY, cornerSize) : topRightCorner(sourceX, cY, cornerSize);
secondCornerPath = sourceY < targetY ? leftTopCorner(targetX, cY, cornerSize) : leftBottomCorner(targetX, cY, cornerSize);
}
if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
if (sourceX <= targetX) {
firstCornerPath = sourceY <= targetY ? rightTopCorner(cX, sourceY, cornerSize) : rightBottomCorner(cX, sourceY, cornerSize);
secondCornerPath = sourceY <= targetY ? bottomLeftCorner(cX, targetY, cornerSize) : topLeftCorner(cX, targetY, cornerSize);
} else if (sourcePosition === Position.Right && targetPosition === Position.Left) {
// and sourceX > targetX
firstCornerPath = sourceY <= targetY ? leftTopCorner(cX, sourceY, cornerSize) : leftBottomCorner(cX, sourceY, cornerSize);
secondCornerPath =
sourceY <= targetY ? bottomRightCorner(cX, targetY, cornerSize) : topRightCorner(cX, targetY, cornerSize);
}
} else if (leftAndRight.includes(sourcePosition) && !leftAndRight.includes(targetPosition)) {
if (sourceX <= targetX) {
firstCornerPath =
sourceY <= targetY ? rightTopCorner(targetX, sourceY, cornerSize) : rightBottomCorner(targetX, sourceY, cornerSize);
} else {
firstCornerPath =
sourceY <= targetY ? leftTopCorner(targetX, sourceY, cornerSize) : leftBottomCorner(targetX, sourceY, cornerSize);
}
secondCornerPath = '';
} else if (!leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
if (sourceX <= targetX) {
firstCornerPath =
sourceY <= targetY ? bottomLeftCorner(sourceX, targetY, cornerSize) : topLeftCorner(sourceX, targetY, cornerSize);
} else {
firstCornerPath =
sourceY <= targetY ? bottomRightCorner(sourceX, targetY, cornerSize) : topRightCorner(sourceX, targetY, cornerSize);
}
secondCornerPath = '';
}
return `M ${sourceX},${sourceY}${firstCornerPath}${secondCornerPath}L ${targetX},${targetY}`;
}
const SmoothStepEdge = defineComponent({
props: EdgeSmoothStepProps,
setup(props) {
const [centerX, centerY] = getCenter({
sourceX: props.sourceX,
sourceY: props.sourceY,
targetX: props.targetX,
targetY: props.targetY,
sourcePosition: props.sourcePosition,
targetPosition: props.targetPosition
});
const path = getSmoothStepPath({
sourceX: props.sourceX,
sourceY: props.sourceY,
targetX: props.targetX,
targetY: props.targetY,
sourcePosition: props.sourcePosition,
targetPosition: props.targetPosition,
borderRadius: props.borderRadius
});
const markerEnd = getMarkerEnd(props.arrowHeadType, props.markerEndId);
const text = props.label ? (
<EdgeText
x={centerX}
y={centerY}
label={props.label}
labelStyle={props.labelStyle}
labelShowBg={props.labelShowBg}
labelBgStyle={props.labelBgStyle}
labelBgPadding={props.labelBgPadding}
labelBgBorderRadius={props.labelBgBorderRadius}
/>
) : null;
return (
<>
<path style={props.style} class="react-flow__edge-path" d={path} marker-end={markerEnd} />
{text}
</>
);
}
});
export default SmoothStepEdge;
+14
View File
@@ -0,0 +1,14 @@
import { defineComponent } from 'vue';
import { EdgeSmoothStepProps } from '../../types';
import SmoothStepEdge from './SmoothStepEdge';
const StepEdge = defineComponent({
components: { SmoothStepEdge },
props: EdgeSmoothStepProps,
setup(props) {
return () => <SmoothStepEdge {...props} borderRadius={0} />;
}
});
export default StepEdge;
+44
View File
@@ -0,0 +1,44 @@
import { defineComponent } from 'vue';
import EdgeText from './EdgeText';
import { getMarkerEnd } from './utils';
import { EdgeProps } from '../../types';
const StraightEdge = defineComponent({
props: EdgeProps,
setup(props) {
const yOffset = Math.abs(props.targetY - props.sourceY) / 2;
const centerY = props.targetY < props.sourceY ? props.targetY + yOffset : props.targetY - yOffset;
const xOffset = Math.abs(props.targetX - props.sourceX) / 2;
const centerX = props.targetX < props.sourceX ? props.targetX + xOffset : props.targetX - xOffset;
const markerEnd = getMarkerEnd(props.arrowHeadType, props.markerEndId);
const text = props.label ? (
<EdgeText
x={centerX}
y={centerY}
label={props.label}
labelStyle={props.labelStyle}
labelShowBg={props.labelShowBg}
labelBgStyle={props.labelBgStyle}
labelBgPadding={props.labelBgPadding}
labelBgBorderRadius={props.labelBgBorderRadius}
/>
) : null;
return (
<>
<path
style={props.style}
class="react-flow__edge-path"
d={`M ${props.sourceX},${props.sourceY}L ${props.targetX},${props.targetY}`}
marker-end={markerEnd}
/>
{text}
</>
);
}
});
export default StraightEdge;
+4
View File
@@ -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';
+54
View File
@@ -0,0 +1,54 @@
import { ArrowHeadType, Position } 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';
};
export interface GetCenterParams {
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
sourcePosition?: Position;
targetPosition?: Position;
}
const LeftOrRight = [Position.Left, Position.Right];
export const getCenter = ({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition = Position.Bottom,
targetPosition = Position.Top
}: GetCenterParams): [number, number, number, number] => {
const sourceIsLeftOrRight = LeftOrRight.includes(sourcePosition);
const targetIsLeftOrRight = LeftOrRight.includes(targetPosition);
// we expect flows to be horizontal or vertical (all handles left or right respectively top or bottom)
// a mixed edge is when one the source is on the left and the target is on the top for example.
const mixedEdge = (sourceIsLeftOrRight && !targetIsLeftOrRight) || (targetIsLeftOrRight && !sourceIsLeftOrRight);
if (mixedEdge) {
const xOffset = sourceIsLeftOrRight ? Math.abs(targetX - sourceX) : 0;
const centerX = sourceX > targetX ? sourceX - xOffset : sourceX + xOffset;
const yOffset = sourceIsLeftOrRight ? 0 : Math.abs(targetY - sourceY);
const centerY = sourceY < targetY ? sourceY + yOffset : sourceY - yOffset;
return [centerX, centerY, xOffset, yOffset];
}
const xOffset = Math.abs(targetX - sourceX) / 2;
const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset;
const yOffset = Math.abs(targetY - sourceY) / 2;
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
return [centerX, centerY, xOffset, yOffset];
};
+183
View File
@@ -0,0 +1,183 @@
import { Component, computed, defineComponent, ref } from 'vue';
import store from '../../store';
import { Edge, EdgeProps, Position, WrapEdgeProps } from '../../types';
import { onMouseDown } from '../Handle/handler';
import { EdgeAnchor } from './EdgeAnchor';
export default (EdgeComponent: any): Component<EdgeProps> => {
return defineComponent({
components: { EdgeComponent },
props: WrapEdgeProps,
setup(props) {
console.log('wrap');
const pinia = store();
const updating = ref<boolean>(false);
const inactive = !props.elementsSelectable && !props.onClick;
const edgeClasses = [
'react-flow__edge',
`react-flow__edge-${props.type}`,
{ selected: props.selected, animated: props.animated, inactive, updating }
];
const edgeElement = computed<Edge>(() => {
const el: Edge = {
id: props.id || '',
source: props.source || '',
target: props.target || '',
type: props.type
};
if (props.sourceHandleId) {
el.sourceHandle = props.sourceHandleId;
}
if (props.targetHandleId) {
el.targetHandle = props.targetHandleId;
}
if (typeof props.data !== 'undefined') {
el.data = props.data;
}
return el;
});
const onEdgeClick = (event: MouseEvent) => {
if (props.elementsSelectable) {
pinia.unsetNodesSelection();
pinia.addSelectedElements(edgeElement.value as any);
}
props.onClick?.(event, edgeElement.value);
};
const onEdgeContextMenu = (event: MouseEvent) => {
props.onContextMenu?.(event, edgeElement.value);
};
const onEdgeMouseEnter = (event: MouseEvent) => {
props.onMouseEnter?.(event, edgeElement.value);
};
const onEdgeMouseMove = (event: MouseEvent) => {
props.onMouseMove?.(event, edgeElement.value);
};
const onEdgeMouseLeave = (event: MouseEvent) => {
props.onMouseLeave?.(event, edgeElement.value);
};
const handleEdgeUpdater = (event: MouseEvent, isSourceHandle: boolean) => {
const nodeId = isSourceHandle ? props.target : props.source;
const handleId = isSourceHandle ? props.targetHandleId : props.sourceHandleId;
const isValidConnection = () => true;
const isTarget = isSourceHandle;
props.onEdgeUpdateStart?.(event, edgeElement.value);
const _onEdgeUpdate = props.onEdgeUpdateEnd
? (evt: MouseEvent) => {
if (props.onEdgeUpdateEnd) props.onEdgeUpdateEnd(evt, edgeElement.value);
}
: undefined;
onMouseDown(
event,
handleId,
nodeId || '',
pinia.setConnectionNodeId,
pinia.setConnectionPosition,
props.onConnectEdge,
isTarget,
isValidConnection,
pinia.connectionMode,
isSourceHandle ? 'target' : 'source',
_onEdgeUpdate
);
};
const onEdgeUpdaterSourceMouseDown = (event: MouseEvent) => {
handleEdgeUpdater(event, true);
};
const onEdgeUpdaterTargetMouseDown = (event: MouseEvent) => {
handleEdgeUpdater(event, false);
};
const onEdgeUpdaterMouseEnter = () => (updating.value = true);
const onEdgeUpdaterMouseOut = () => (updating.value = false);
if (props.isHidden) {
return null;
}
return () => (
<g
class={edgeClasses}
onClick={onEdgeClick}
onContextmenu={onEdgeContextMenu}
onMouseenter={onEdgeMouseEnter}
onMousemove={onEdgeMouseMove}
onMouseleave={onEdgeMouseLeave}
>
<EdgeComponent
id={props.id}
source={props.source}
target={props.target}
selected={props.selected}
animated={props.animated}
label={props.label}
labelStyle={props.labelStyle}
labelShowBg={props.labelShowBg}
labelBgStyle={props.labelBgStyle}
labelBgPadding={props.labelBgPadding}
labelBgBorderRadius={props.labelBgBorderRadius}
data={props.data}
style={props.style}
arrowHeadType={props.arrowHeadType}
sourceX={props.sourceX}
sourceY={props.sourceY}
targetX={props.targetX}
targetY={props.targetY}
sourcePosition={props.sourcePosition}
targetPosition={props.targetPosition}
markerEndId={props.markerEndId}
sourceHandleId={props.sourceHandleId}
targetHandleId={props.targetHandleId}
/>
{props.handleEdgeUpdate && (
<g
onMousedown={onEdgeUpdaterSourceMouseDown}
onMouseenter={onEdgeUpdaterMouseEnter}
onMouseout={onEdgeUpdaterMouseOut}
>
<EdgeAnchor
position={props.sourcePosition as Position}
centerX={props.sourceX as number}
centerY={props.sourceY as number}
radius={props.edgeUpdaterRadius}
/>
</g>
)}
{props.handleEdgeUpdate && (
<g
onMousedown={onEdgeUpdaterTargetMouseDown}
onMouseenter={onEdgeUpdaterMouseEnter}
onMouseout={onEdgeUpdaterMouseOut}
>
<EdgeAnchor
position={props.sourcePosition as Position}
centerX={props.sourceX as number}
centerY={props.sourceY as number}
radius={props.edgeUpdaterRadius}
/>
</g>
)}
</g>
);
}
});
};