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
+169
View File
@@ -0,0 +1,169 @@
import { ref, defineComponent, CSSProperties, PropType } from 'vue';
import { getBezierPath } from '../Edges/BezierEdge';
import { getSmoothStepPath } from '../Edges/SmoothStepEdge';
import {
ElementId,
Node,
Transform,
HandleElement,
Position,
ConnectionLineType,
ConnectionLineComponent,
HandleType
} from '../../types';
interface ConnectionLineProps {
connectionNodeId: ElementId;
connectionHandleId: ElementId | null;
connectionHandleType: HandleType;
connectionPositionX: number;
connectionPositionY: number;
connectionLineType: ConnectionLineType;
nodes: Node[];
transform: Transform;
isConnectable: boolean;
connectionLineStyle?: CSSProperties;
CustomConnectionLineComponent?: ConnectionLineComponent;
}
const ConnectionLine = defineComponent({
props: {
connectionNodeId: {
type: String as PropType<ConnectionLineProps['connectionNodeId']>,
required: true
},
connectionHandleId: {
type: String as PropType<ConnectionLineProps['connectionHandleId']>,
required: true
},
connectionHandleType: {
type: String as PropType<ConnectionLineProps['connectionHandleType']>,
required: true
},
connectionLineStyle: {
type: Object as PropType<ConnectionLineProps['connectionLineStyle']>,
required: false,
default: () => ({})
},
connectionPositionX: {
type: Number as PropType<ConnectionLineProps['connectionPositionX']>,
required: false,
default: 0
},
connectionPositionY: {
type: Number as PropType<ConnectionLineProps['connectionPositionY']>,
required: false,
default: 0
},
connectionLineType: {
type: String as PropType<ConnectionLineProps['connectionLineType']>,
required: false,
default: ConnectionLineType.Bezier
},
nodes: {
type: Array as PropType<ConnectionLineProps['nodes']>,
required: true,
default: () => []
},
transform: {
type: Object as PropType<ConnectionLineProps['transform']>,
required: false,
default: () => ({})
},
isConnectable: {
type: Boolean as PropType<ConnectionLineProps['isConnectable']>,
required: false,
default: false
},
CustomConnectionLineComponent: {
type: Object as PropType<ConnectionLineProps['CustomConnectionLineComponent']>,
required: false,
default: () => ({})
}
},
setup(props) {
console.log('connectionline');
const sourceNode = ref<Node | null>(props.nodes.find((n) => n.id === props.connectionNodeId) || null);
if (!sourceNode.value || !props.isConnectable) {
return null;
}
const sourceHandle = props.connectionHandleId
? sourceNode.value.__rf.handleBounds[props.connectionHandleType].find(
(d: HandleElement) => d.id === props.connectionHandleId
)
: sourceNode.value.__rf.handleBounds[props.connectionHandleType][0];
const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNode.value.__rf.width / 2;
const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.value.__rf.height;
const sourceX = sourceNode.value.__rf.position.x + sourceHandleX;
const sourceY = sourceNode.value.__rf.position.y + sourceHandleY;
const targetX = (props.connectionPositionX - props.transform[0]) / props.transform[2];
const targetY = (props.connectionPositionY - props.transform[1]) / props.transform[2];
const isRightOrLeft = sourceHandle?.position === Position.Left || sourceHandle?.position === Position.Right;
const targetPosition = isRightOrLeft ? Position.Left : Position.Top;
if (props.CustomConnectionLineComponent) {
return () => (
<g class="react-flow__connection">
<component
is={props.CustomConnectionLineComponent}
sourceX={sourceX}
sourceY={sourceY}
sourcePosition={sourceHandle?.position}
targetX={targetX}
targetY={targetY}
targetPosition={targetPosition}
connectionLineType={props.connectionLineType}
connectionLineStyle={props.connectionLineStyle}
/>
</g>
);
}
let dAttr = '';
if (props.connectionLineType === ConnectionLineType.Bezier) {
dAttr = getBezierPath({
sourceX,
sourceY,
sourcePosition: sourceHandle?.position,
targetX,
targetY,
targetPosition
});
} else if (props.connectionLineType === ConnectionLineType.Step) {
dAttr = getSmoothStepPath({
sourceX,
sourceY,
sourcePosition: sourceHandle?.position,
targetX,
targetY,
targetPosition,
borderRadius: 0
});
} else if (props.connectionLineType === ConnectionLineType.SmoothStep) {
dAttr = getSmoothStepPath({
sourceX,
sourceY,
sourcePosition: sourceHandle?.position,
targetX,
targetY,
targetPosition
});
} else {
dAttr = `M${sourceX},${sourceY} ${targetX},${targetY}`;
}
return () => (
<g class="react-flow__connection">
<path d={dAttr} class="react-flow__connection-path" style={props.connectionLineStyle} />
</g>
);
}
});
export default ConnectionLine;
+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>
);
}
});
};
+193
View File
@@ -0,0 +1,193 @@
import { getHostForElement } from '../../utils';
import {
ElementId,
XYPosition,
OnConnectFunc,
OnConnectStartFunc,
OnConnectStopFunc,
OnConnectEndFunc,
ConnectionMode,
SetConnectionId,
Connection,
HandleType
} from '../../types';
export type ValidConnectionFunc = (connection: Connection) => boolean;
export type SetSourceIdFunc = (params: SetConnectionId) => void;
export type SetPosition = (pos: XYPosition) => void;
type Result = {
elementBelow: Element | null;
isValid: boolean;
connection: Connection;
isHoveringHandle: boolean;
};
// checks if element below mouse is a handle and returns connection in form of an object { source: 123, target: 312 }
function checkElementBelowIsValid(
event: MouseEvent,
connectionMode: ConnectionMode,
isTarget: boolean,
nodeId: ElementId,
handleId: ElementId | null,
isValidConnection: ValidConnectionFunc,
doc: Document | ShadowRoot
) {
const elementBelow = doc.elementFromPoint(event.clientX, event.clientY);
const elementBelowIsTarget = elementBelow?.classList.contains('target') || false;
const elementBelowIsSource = elementBelow?.classList.contains('source') || false;
const result: Result = {
elementBelow,
isValid: false,
connection: { source: null, target: null, sourceHandle: null, targetHandle: null },
isHoveringHandle: false
};
if (elementBelow && (elementBelowIsTarget || elementBelowIsSource)) {
result.isHoveringHandle = true;
// in strict mode we don't allow target to target or source to source connections
const isValid =
connectionMode === ConnectionMode.Strict ? (isTarget && elementBelowIsSource) || (!isTarget && elementBelowIsTarget) : true;
if (isValid) {
const elementBelowNodeId = elementBelow.getAttribute('data-nodeid');
const elementBelowHandleId = elementBelow.getAttribute('data-handleid');
const connection: Connection = isTarget
? {
source: elementBelowNodeId,
sourceHandle: elementBelowHandleId,
target: nodeId,
targetHandle: handleId
}
: {
source: nodeId,
sourceHandle: handleId,
target: elementBelowNodeId,
targetHandle: elementBelowHandleId
};
result.connection = connection;
result.isValid = isValidConnection(connection);
}
}
return result;
}
function resetRecentHandle(hoveredHandle: Element): void {
hoveredHandle?.classList.remove('react-flow__handle-valid');
hoveredHandle?.classList.remove('react-flow__handle-connecting');
}
export function onMouseDown(
event: MouseEvent,
handleId: ElementId | null,
nodeId: ElementId,
setConnectionNodeId: SetSourceIdFunc,
setPosition: SetPosition,
onConnect: OnConnectFunc,
isTarget: boolean,
isValidConnection: ValidConnectionFunc,
connectionMode: ConnectionMode,
elementEdgeUpdaterType?: HandleType,
onEdgeUpdateEnd?: (evt: MouseEvent) => void,
onConnectStart?: OnConnectStartFunc,
onConnectStop?: OnConnectStopFunc,
onConnectEnd?: OnConnectEndFunc
): void {
const reactFlowNode = (event.target as Element).closest('.react-flow');
// when react-flow is used inside a shadow root we can't use document
const doc = getHostForElement(event.target as HTMLElement);
console.log(doc);
if (!doc) {
return;
}
const elementBelow = doc.elementFromPoint(event.clientX, event.clientY);
const elementBelowIsTarget = elementBelow?.classList.contains('target');
const elementBelowIsSource = elementBelow?.classList.contains('source');
if (!reactFlowNode || (!elementBelowIsTarget && !elementBelowIsSource && !elementEdgeUpdaterType)) {
return;
}
const handleType = elementEdgeUpdaterType ? elementEdgeUpdaterType : elementBelowIsTarget ? 'target' : 'source';
const containerBounds = reactFlowNode.getBoundingClientRect();
let recentHoveredHandle: Element;
setPosition({
x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top
});
setConnectionNodeId({ connectionNodeId: nodeId, connectionHandleId: handleId, connectionHandleType: handleType });
onConnectStart?.(event, { nodeId, handleId, handleType });
function onMouseMove(event: MouseEvent) {
setPosition({
x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top
});
const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid(
event,
connectionMode,
isTarget,
nodeId,
handleId,
isValidConnection,
doc
);
if (!isHoveringHandle) {
return resetRecentHandle(recentHoveredHandle);
}
const isOwnHandle = connection.source === connection.target;
if (!isOwnHandle && elementBelow) {
recentHoveredHandle = elementBelow;
elementBelow.classList.add('react-flow__handle-connecting');
elementBelow.classList.toggle('react-flow__handle-valid', isValid);
}
}
function onMouseUp(event: MouseEvent) {
const { connection, isValid } = checkElementBelowIsValid(
event,
connectionMode,
isTarget,
nodeId,
handleId,
isValidConnection,
doc
);
onConnectStop?.(event);
if (isValid) {
onConnect?.(connection);
}
onConnectEnd?.(event);
if (elementEdgeUpdaterType && onEdgeUpdateEnd) {
onEdgeUpdateEnd(event);
}
resetRecentHandle(recentHoveredHandle);
setConnectionNodeId({ connectionNodeId: null, connectionHandleId: null, connectionHandleType: null });
doc.removeEventListener('mousemove', onMouseMove as EventListenerOrEventListenerObject);
doc.removeEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject);
}
doc.addEventListener('mousemove', onMouseMove as EventListenerOrEventListenerObject);
doc.addEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject);
}
+97
View File
@@ -0,0 +1,97 @@
import { Connection, ElementId, Position } from '../../types';
import { onMouseDown, ValidConnectionFunc } from './handler';
import { defineComponent, inject, PropType } from 'vue';
import store from '../../store';
const alwaysValid = () => true;
const Handle = defineComponent({
props: {
type: {
type: String,
required: false,
default: 'source'
},
position: {
type: String as PropType<Position>,
required: false,
default: Position.Top
},
isValidConnection: {
type: Function as PropType<ValidConnectionFunc>,
required: false,
default: alwaysValid
},
isConnectable: {
type: Boolean,
required: false,
default: true
},
id: {
type: String,
required: false,
default: undefined
},
onConnect: {
type: Function,
required: false,
default: undefined
}
},
setup(props, { slots }) {
const pinia = store();
const nodeId = inject<ElementId>('NodeIdContext') as ElementId;
const handleId = props.id || null;
const isTarget = props.type === 'target';
const onConnectExtended = (params: Connection) => {
pinia.onConnect?.(params);
props.onConnect?.(params);
};
const onMouseDownHandler = (event: MouseEvent) => {
onMouseDown(
event,
handleId,
nodeId,
pinia.setConnectionNodeId,
pinia.setConnectionPosition,
onConnectExtended,
isTarget,
props.isValidConnection,
pinia.connectionMode,
undefined,
undefined,
pinia.onConnectStart,
pinia.onConnectStop,
pinia.onConnectEnd
);
};
const handleClasses = [
'react-flow__handle',
`react-flow__handle-${props.position}`,
'nodrag',
{
source: !isTarget,
target: isTarget,
connectable: props.isConnectable
}
];
return () => (
<div
data-handleid={handleId}
data-nodeid={nodeId}
data-handlepos={props.position}
class={handleClasses}
onMousedown={onMouseDownHandler}
>
{slots.default ? slots.default() : ''}
</div>
);
}
});
export default Handle;
-64
View File
@@ -1,64 +0,0 @@
<template>
<h1>{{ msg }}</h1>
<p>
Recommended IDE setup:
<a href="https://code.visualstudio.com/" target="_blank">VSCode</a>
+
<a href="https://marketplace.visualstudio.com/items?itemName=octref.vetur" target="_blank"> Vetur </a>
or
<a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>
(if using
<code>&lt;script setup&gt;</code>)
</p>
<p>See <code>README.md</code> for more information.</p>
<p>
<a href="https://vitejs.dev/guide/features.html" target="_blank"> Vite Docs </a>
|
<a href="https://v3.vuejs.org/" target="_blank">Vue 3 Docs</a>
</p>
<button type="button" @click="count++">count is: {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test hot module replacement.
</p>
</template>
<script lang="ts">
import { ref, defineComponent } from 'vue';
export default defineComponent({
name: 'HelloWorld',
props: {
msg: {
type: String,
required: true
}
},
setup: () => {
const count = ref(0);
return { count };
}
});
</script>
<style scoped>
a {
color: #42b983;
}
label {
margin: 0 0.5em;
font-weight: bold;
}
code {
background-color: #eee;
padding: 2px 4px;
border-radius: 4px;
color: #304455;
}
</style>
+41
View File
@@ -0,0 +1,41 @@
import Handle from '../../components/Handle';
import { NodeProps, Position } from '../../types';
import { defineComponent, PropType } from 'vue';
const DefaultNode = defineComponent({
name: 'DefaultNode',
components: { Handle },
props: {
data: {
type: Object as PropType<NodeProps['data']>,
required: false,
default: undefined as any
},
isConnectable: {
type: Boolean as PropType<NodeProps['isConnectable']>,
required: false,
default: undefined
},
targetPosition: {
type: String as PropType<NodeProps['targetPosition']>,
required: false,
default: Position.Top
},
sourcePosition: {
type: String as PropType<NodeProps['sourcePosition']>,
required: false,
default: Position.Bottom
}
},
setup(props) {
return () => (
<>
<Handle type="target" position={props.targetPosition} isConnectable={props.isConnectable} />
{props.data?.label}
<Handle type="source" position={props.sourcePosition} isConnectable={props.isConnectable} />
</>
);
}
});
export default DefaultNode;
+35
View File
@@ -0,0 +1,35 @@
import Handle from '../../components/Handle';
import { NodeProps, Position } from '../../types';
import { defineComponent, PropType } from 'vue';
const InputNode = defineComponent({
name: 'InputNode',
components: { Handle },
props: {
data: {
type: Object as PropType<NodeProps['data']>,
required: false,
default: undefined as any
},
isConnectable: {
type: Boolean as PropType<NodeProps['isConnectable']>,
required: false,
default: false
},
sourcePosition: {
type: String as PropType<NodeProps['sourcePosition']>,
required: false,
default: Position.Bottom
}
},
setup(props) {
return () => (
<>
{props.data?.label}
<Handle type="source" position={props.sourcePosition} isConnectable={props.isConnectable} />
</>
);
}
});
export default InputNode;
+35
View File
@@ -0,0 +1,35 @@
import Handle from '../../components/Handle';
import { NodeProps, Position } from '../../types';
import { defineComponent, PropType } from 'vue';
const OutputNode = defineComponent({
name: 'OutputNode',
components: { Handle },
props: {
data: {
type: Object as PropType<NodeProps['data']>,
required: false,
default: undefined as any
},
isConnectable: {
type: Boolean as PropType<NodeProps['isConnectable']>,
required: false,
default: false
},
targetPosition: {
type: String as PropType<NodeProps['targetPosition']>,
required: false,
default: Position.Bottom
}
},
setup(props) {
return () => (
<>
{props.data?.label}
<Handle type="source" position={props.targetPosition} isConnectable={props.isConnectable} />
</>
);
}
});
export default OutputNode;
+41
View File
@@ -0,0 +1,41 @@
import { HandleElement, Position } from '../../types';
import { getDimensions } from '../../utils';
export const getHandleBounds = (nodeElement: HTMLDivElement, scale: number) => {
const bounds = nodeElement.getBoundingClientRect();
return {
source: getHandleBoundsByHandleType('.source', nodeElement, bounds, scale),
target: getHandleBoundsByHandleType('.target', nodeElement, bounds, scale)
};
};
export const getHandleBoundsByHandleType = (
selector: string,
nodeElement: HTMLDivElement,
parentBounds: ClientRect | DOMRect,
k: number
): HandleElement[] | null => {
const handles = nodeElement.querySelectorAll(selector);
if (!handles || !handles.length) {
return null;
}
const handlesArray = Array.from(handles) as HTMLDivElement[];
return handlesArray.map((handle): HandleElement => {
const bounds = handle.getBoundingClientRect();
const dimensions = getDimensions(handle);
const handleId = handle.getAttribute('data-handleid');
const handlePosition = handle.getAttribute('data-handlepos') as unknown as Position;
return {
id: handleId,
position: handlePosition,
x: (bounds.left - parentBounds.left) / k,
y: (bounds.top - parentBounds.top) / k,
...dimensions
};
});
};
+201
View File
@@ -0,0 +1,201 @@
import { Node, WrapNodeProps } from '../../types';
import { computed, CSSProperties, defineComponent, onMounted, provide, ref } from 'vue';
import store from '../../store';
export default (NodeComponent: any) => {
return defineComponent({
props: WrapNodeProps,
setup(props) {
const pinia = store();
provide('NodeIdContext', props.id);
const nodeElement = ref<HTMLDivElement | null>(null);
const node = computed(() => ({
id: props.id,
type: props.type,
position: { x: props.xPos, y: props.yPos },
data: props.data
}));
// const grid = computed(() => (props.snapToGrid ? props.snapGrid : [1, 1])! as [number, number]);
const nodeStyle = computed<CSSProperties>(() => ({
zIndex: props.selected ? 10 : 3,
transform: `translate(${props.xPos}px,${props.yPos}px)`,
pointerEvents:
props.isSelectable ||
props.isDraggable ||
props.onClick ||
props.onMouseEnter ||
props.onMouseMove ||
props.onMouseLeave
? 'all'
: 'none',
// prevents jumping of nodes on start
opacity: props.isInitialized ? 1 : 0,
...props.style
}));
const onMouseEnterHandler = () => {
if (!props.onMouseEnter || props.isDragging) {
return;
}
return (event: MouseEvent) => props.onMouseEnter && props.onMouseEnter(event, node.value as Node);
};
const onMouseMoveHandler = () => {
if (!props.onMouseMove || props.isDragging) {
return;
}
return (event: MouseEvent) => props.onMouseMove && props.onMouseMove(event, node.value as Node);
};
const onMouseLeaveHandler = () => {
if (!props.onMouseLeave || props.isDragging) {
return;
}
return (event: MouseEvent) => props.onMouseLeave && props.onMouseLeave(event, node.value as Node);
};
const onContextMenuHandler = () => {
if (!props.onContextMenu) {
return;
}
return (event: MouseEvent) => props.onContextMenu && props.onContextMenu(event, node.value as Node);
};
const onSelectNodeHandler = (event: MouseEvent) => {
if (!props.isDraggable) {
if (props.isSelectable) {
pinia.unsetNodesSelection();
if (!props.selected) {
pinia.addSelectedElements([node.value as Node]);
}
}
props.onClick?.(event, node.value as Node);
}
};
/**
const onDragStart = (event: DraggableEvent) => {
onNodeDragStart?.(event as MouseEvent, node);
if (selectNodesOnDrag && isSelectable) {
unsetNodesSelection();
if (!selected) {
addSelectedElements(node);
}
} else if (!selectNodesOnDrag && !selected && isSelectable) {
unsetNodesSelection();
addSelectedElements([]);
}
};
const onDrag = useCallback(
(event: DraggableEvent, draggableData: DraggableData) => {
if (onNodeDrag) {
node.position.x += draggableData.deltaX;
node.position.y += draggableData.deltaY;
onNodeDrag(event as MouseEvent, node);
}
updateNodePosDiff({
id,
diff: {
x: draggableData.deltaX,
y: draggableData.deltaY
},
isDragging: true
});
},
[id, node, onNodeDrag]
);
const onDragStop = useCallback(
(event: DraggableEvent) => {
// onDragStop also gets called when user just clicks on a node.
// Because of that we set dragging to true inside the onDrag handler and handle the click here
if (!isDragging) {
if (isSelectable && !selectNodesOnDrag && !selected) {
addSelectedElements(node);
}
onClick?.(event as MouseEvent, node);
return;
}
updateNodePosDiff({
id: node.id,
isDragging: false
});
onNodeDragStop?.(event as MouseEvent, node);
},
[node, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, isDragging, selected]
);
*/
onMounted(() => {
if (nodeElement.value && !props.isHidden) {
pinia.updateNodeDimensions([{ id: props.id || '', nodeElement: nodeElement.value, forceUpdate: true }]);
}
});
onMounted(() => {
if (nodeElement.value && typeof props.resizeObserver?.observe === 'function') {
const currNode = nodeElement.value;
props.resizeObserver?.observe(currNode);
return () => props.resizeObserver?.unobserve(currNode);
}
});
if (props.isHidden) {
return null;
}
const nodeClasses = [
'react-flow__node',
`react-flow__node-${props.type}`,
{
selected: props.selected,
selectable: props.isSelectable
}
];
return () => (
<div
class={nodeClasses}
ref={nodeElement}
style={nodeStyle.value}
onMouseenter={onMouseEnterHandler}
onMousemove={onMouseMoveHandler}
onMouseleave={onMouseLeaveHandler}
onContextmenu={onContextMenuHandler}
onClick={onSelectNodeHandler}
data-id={props.id}
>
<NodeComponent
id={props.id}
data={props.data}
type={props.type}
xPos={props.xPos}
yPos={props.yPos}
selected={props.selected}
isConnectable={props.isConnectable}
sourcePosition={props.sourcePosition}
targetPosition={props.targetPosition}
isDragging={props.isDragging}
/>
</div>
);
}
});
};
+123
View File
@@ -0,0 +1,123 @@
import { isNode } from '../../utils/graph';
import { Node } from '../../types';
export interface NodesSelectionProps {
onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void;
onSelectionDrag?: (event: MouseEvent, nodes: Node[]) => void;
onSelectionDragStop?: (event: MouseEvent, nodes: Node[]) => void;
onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void;
}
export default ({ onSelectionDragStart, onSelectionDrag, onSelectionDragStop, onSelectionContextMenu }: NodesSelectionProps) => {
const [tX, tY, tScale] = useStoreState((state) => state.transform);
const selectedNodesBbox = useStoreState((state) => state.selectedNodesBbox);
const selectionActive = useStoreState((state) => state.selectionActive);
const selectedElements = useStoreState((state) => state.selectedElements);
const snapToGrid = useStoreState((state) => state.snapToGrid);
const snapGrid = useStoreState((state) => state.snapGrid);
const nodes = useStoreState((state) => state.nodes);
const updateNodePosDiff = useStoreActions((actions) => actions.updateNodePosDiff);
const nodeRef = useRef(null);
const grid = useMemo(() => (snapToGrid ? snapGrid : [1, 1])! as [number, number], [snapToGrid, snapGrid]);
const selectedNodes = useMemo(
() =>
selectedElements
? selectedElements.filter(isNode).map((selectedNode) => {
const matchingNode = nodes.find((node) => node.id === selectedNode.id);
return {
...matchingNode,
position: matchingNode?.__rf.position
} as Node;
})
: [],
[selectedElements, nodes]
);
const style = useMemo(
() => ({
transform: `translate(${tX}px,${tY}px) scale(${tScale})`
}),
[tX, tY, tScale]
);
const innerStyle = useMemo(
() => ({
width: selectedNodesBbox.width,
height: selectedNodesBbox.height,
top: selectedNodesBbox.y,
left: selectedNodesBbox.x
}),
[selectedNodesBbox]
);
const onStart = useCallback(
(event: MouseEvent) => {
onSelectionDragStart?.(event, selectedNodes);
},
[onSelectionDragStart, selectedNodes]
);
const onDrag = useCallback(
(event: MouseEvent, data: DraggableData) => {
if (onSelectionDrag) {
onSelectionDrag(event, selectedNodes);
}
updateNodePosDiff({
diff: {
x: data.deltaX,
y: data.deltaY
},
isDragging: true
});
},
[onSelectionDrag, selectedNodes, updateNodePosDiff]
);
const onStop = useCallback(
(event: MouseEvent) => {
updateNodePosDiff({
isDragging: false
});
onSelectionDragStop?.(event, selectedNodes);
},
[selectedNodes, onSelectionDragStop]
);
const onContextMenu = useCallback(
(event: MouseEvent) => {
const selectedNodes = selectedElements
? selectedElements.filter(isNode).map((selectedNode) => nodes.find((node) => node.id === selectedNode.id)!)
: [];
onSelectionContextMenu?.(event, selectedNodes);
},
[onSelectionContextMenu]
);
if (!selectedElements || selectionActive) {
return null;
}
return (
<div className="react-flow__nodesselection" style={style}>
<ReactDraggable
scale={tScale}
grid={grid}
onStart={(event) => onStart(event as MouseEvent)}
onDrag={(event, data) => onDrag(event as MouseEvent, data)}
onStop={(event) => onStop(event as MouseEvent)}
nodeRef={nodeRef}
enableUserSelectHack={false}
>
<div ref={nodeRef} className="react-flow__nodesselection-rect" onContextMenu={onContextMenu} style={innerStyle} />
</ReactDraggable>
</div>
);
};
+104
View File
@@ -0,0 +1,104 @@
/**
* The user selection rectangle gets displayed when a user drags the mouse while pressing shift
*/
import { XYPosition } from '../../types';
import { defineComponent, PropType } from 'vue';
import store from '../../store';
type UserSelectionProps = {
selectionKeyPressed: boolean;
};
function getMousePosition(event: MouseEvent): XYPosition | void {
const reactFlowNode = (event.target as Element).closest('.react-flow');
if (!reactFlowNode) {
return;
}
const containerBounds = reactFlowNode.getBoundingClientRect();
return {
x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top
};
}
const SelectionRect = defineComponent({
setup() {
const pinia = store();
if (!pinia.userSelectionRect.draw) {
return null;
}
return () => (
<div
class="react-flow__selection"
style={{
width: pinia.userSelectionRect.width,
height: pinia.userSelectionRect.height,
transform: `translate(${pinia.userSelectionRect.x}px, ${pinia.userSelectionRect.y}px)`
}}
/>
);
}
});
export default defineComponent({
components: { SelectionRect },
props: {
selectionKeyPressed: {
type: Boolean as PropType<UserSelectionProps['selectionKeyPressed']>,
required: true
}
},
setup(props) {
const pinia = store();
const renderUserSelectionPane = pinia.selectionActive || props.selectionKeyPressed;
if (!pinia.elementsSelectable || !renderUserSelectionPane) {
return null;
}
const onMouseDown = (event: MouseEvent): void => {
const mousePos = getMousePosition(event);
if (!mousePos) {
return;
}
pinia.setUserSelection(mousePos);
};
const onMouseMove = (event: MouseEvent): void => {
if (!props.selectionKeyPressed || !pinia.selectionActive) {
return;
}
const mousePos = getMousePosition(event);
if (!mousePos) {
return;
}
pinia.updateUserSelection(mousePos);
};
const onMouseUp = () => pinia.unsetUserSelection();
const onMouseLeave = () => {
pinia.unsetUserSelection();
pinia.unsetNodesSelection();
};
return () => (
<div
class="react-flow__selectionpane"
onMousedown={onMouseDown}
onMousemove={onMouseMove}
onMouseup={onMouseUp}
onMouseleave={onMouseLeave}
>
<SelectionRect />
</div>
);
}
});