refactor(handles): reduce re-renderings, handles on top of each other #3010

This commit is contained in:
moklick
2023-05-09 17:08:14 +02:00
parent e67b1fbb13
commit c428b906b7
5 changed files with 38 additions and 38 deletions
@@ -2,8 +2,9 @@ import { Handle, NodeProps, Position, ReactFlowState, useStore } from 'reactflow
const connectionNodeIdSelector = (state: ReactFlowState) => state.connectionNodeId; const connectionNodeIdSelector = (state: ReactFlowState) => state.connectionNodeId;
export default function CustomNode({ id, isConnectable }: NodeProps) { export default function CustomNode({ id }: NodeProps) {
const connectionNodeId = useStore(connectionNodeIdSelector); const connectionNodeId = useStore(connectionNodeIdSelector);
const isConnecting = !!connectionNodeId;
const isTarget = connectionNodeId && connectionNodeId !== id; const isTarget = connectionNodeId && connectionNodeId !== id;
const targetHandleStyle = { zIndex: isTarget ? 3 : 1 }; const targetHandleStyle = { zIndex: isTarget ? 3 : 1 };
@@ -18,19 +19,16 @@ export default function CustomNode({ id, isConnectable }: NodeProps) {
backgroundColor: isTarget ? '#ffcce3' : '#ccd9f6', backgroundColor: isTarget ? '#ffcce3' : '#ccd9f6',
}} }}
> >
{!isConnecting && (
<Handle className="customHandle" style={{ zIndex: 2 }} position={Position.Right} type="source" />
)}
<Handle <Handle
className="targetHandle" className="customHandle"
style={{ zIndex: 2 }}
position={Position.Right}
type="source"
isConnectable={isConnectable}
/>
<Handle
className="targetHandle"
style={targetHandleStyle} style={targetHandleStyle}
position={Position.Left} position={Position.Left}
type="target" type="target"
isConnectable={isConnectable} isConnectableStart={false}
/> />
{label} {label}
</div> </div>
@@ -28,19 +28,7 @@
border: 2px solid #222138; border: 2px solid #222138;
} }
div.sourceHandle { div.customHandle {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
border-radius: 0;
transform: none;
border: none;
opacity: 0;
}
div.targetHandle {
width: 100%; width: 100%;
height: 100%; height: 100%;
background: blue; background: blue;
+10 -10
View File
@@ -51,7 +51,7 @@ export function handlePointerDown({
cancelConnection, cancelConnection,
} = getState(); } = getState();
let autoPanId = 0; let autoPanId = 0;
let prevClosestHandle: ConnectionHandle | null; let closestHandle: ConnectionHandle | null;
const { x, y } = getEventPosition(event); const { x, y } = getEventPosition(event);
const clickedHandle = doc?.elementFromPoint(x, y); const clickedHandle = doc?.elementFromPoint(x, y);
@@ -106,9 +106,9 @@ export function handlePointerDown({
function onPointerMove(event: MouseEvent | TouchEvent) { function onPointerMove(event: MouseEvent | TouchEvent) {
const { transform } = getState(); const { transform } = getState();
connectionPosition = getEventPosition(event, containerBounds);
prevClosestHandle = getClosestHandle( connectionPosition = getEventPosition(event, containerBounds);
closestHandle = getClosestHandle(
pointToRendererPoint(connectionPosition, transform, false, [1, 1]), pointToRendererPoint(connectionPosition, transform, false, [1, 1]),
connectionRadius, connectionRadius,
handleLookup handleLookup
@@ -121,7 +121,7 @@ export function handlePointerDown({
const result = isValidHandle( const result = isValidHandle(
event, event,
prevClosestHandle, closestHandle,
connectionMode, connectionMode,
nodeId, nodeId,
handleId, handleId,
@@ -136,20 +136,20 @@ export function handlePointerDown({
setState({ setState({
connectionPosition: connectionPosition:
prevClosestHandle && isValid closestHandle && isValid
? rendererPointToPoint( ? rendererPointToPoint(
{ {
x: prevClosestHandle.x, x: closestHandle.x,
y: prevClosestHandle.y, y: closestHandle.y,
}, },
transform transform
) )
: connectionPosition, : connectionPosition,
connectionStatus: getConnectionStatus(!!prevClosestHandle, isValid), connectionStatus: getConnectionStatus(!!closestHandle, isValid),
connectionEndHandle: result.endHandle, connectionEndHandle: result.endHandle,
}); });
if (!prevClosestHandle && !isValid && !handleDomNode) { if (!closestHandle && !isValid && !handleDomNode) {
return resetRecentHandle(prevActiveHandle); return resetRecentHandle(prevActiveHandle);
} }
@@ -164,7 +164,7 @@ export function handlePointerDown({
} }
function onPointerUp(event: MouseEvent | TouchEvent) { function onPointerUp(event: MouseEvent | TouchEvent) {
if ((prevClosestHandle || handleDomNode) && connection && isValid) { if ((closestHandle || handleDomNode) && connection && isValid) {
onConnect?.(connection); onConnect?.(connection);
} }
@@ -62,7 +62,7 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
const store = useStoreApi(); const store = useStoreApi();
const nodeId = useNodeId(); const nodeId = useNodeId();
const { connectOnClick, noPanClassName } = useStore(selector, shallow); const { connectOnClick, noPanClassName } = useStore(selector, shallow);
const { connecting, clickConnecting } = useStore(connectingSelector(nodeId, handleId, type)); const { connecting, clickConnecting } = useStore(connectingSelector(nodeId, handleId, type), shallow);
if (!nodeId) { if (!nodeId) {
store.getState().onError?.('010', errorMessages['error010']()); store.getState().onError?.('010', errorMessages['error010']());
+18 -4
View File
@@ -41,18 +41,30 @@ export function getClosestHandle(
connectionRadius: number, connectionRadius: number,
handles: ConnectionHandle[] handles: ConnectionHandle[]
): ConnectionHandle | null { ): ConnectionHandle | null {
let closestHandle: ConnectionHandle | null = null; let closestHandles: ConnectionHandle[] = [];
let minDistance = Infinity; let minDistance = Infinity;
handles.forEach((handle) => { handles.forEach((handle) => {
const distance = Math.sqrt(Math.pow(handle.x - pos.x, 2) + Math.pow(handle.y - pos.y, 2)); const distance = Math.sqrt(Math.pow(handle.x - pos.x, 2) + Math.pow(handle.y - pos.y, 2));
if (distance <= connectionRadius && distance < minDistance) { if (distance <= connectionRadius) {
if (distance < minDistance) {
closestHandles = [handle];
} else if (distance === minDistance) {
// when multiple handles are on the same distance we collect all of them
closestHandles.push(handle);
}
minDistance = distance; minDistance = distance;
closestHandle = handle;
} }
}); });
return closestHandle; if (!closestHandles.length) {
return null;
}
return closestHandles.length === 1
? closestHandles[0]
: // if multiple handles are layouted on top of each other we take the one with type = target because it's more likely that the user wants to connect to this one
closestHandles.find((handle) => handle.type === 'target') || closestHandles[0];
} }
type Result = { type Result = {
@@ -81,6 +93,8 @@ export function isValidHandle(
); );
const { x, y } = getEventPosition(event); const { x, y } = getEventPosition(event);
const handleBelow = doc.elementFromPoint(x, y); const handleBelow = doc.elementFromPoint(x, y);
// we always want to prioritize the handle below the mouse cursor over the closest distance handle,
// because it could be that the center of another handle is closer to the mouse pointer than the handle below the cursor
const handleToCheck = handleBelow?.classList.contains('react-flow__handle') ? handleBelow : handleDomNode; const handleToCheck = handleBelow?.classList.contains('react-flow__handle') ? handleBelow : handleDomNode;
const result: Result = { const result: Result = {