Files
xyflow/src/components/Handle/index.tsx
T

185 lines
4.7 KiB
TypeScript

import React, { memo, useContext, useCallback, HTMLAttributes, forwardRef } from 'react';
import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStore, useStoreApi } from '../../store';
import NodeIdContext from '../../contexts/NodeIdContext';
import { HandleProps, Connection, ReactFlowState, Position } from '../../types';
import { checkElementBelowIsValid, onMouseDown } from './handler';
import { getHostForElement } from '../../utils';
import { addEdge } from '../../utils/graph';
const alwaysValid = () => true;
export type HandleComponentProps = HandleProps & Omit<HTMLAttributes<HTMLDivElement>, 'id'>;
const selector = (s: ReactFlowState) => ({
onConnectAction: s.onConnect,
onConnectStart: s.onConnectStart,
onConnectStop: s.onConnectStop,
onConnectEnd: s.onConnectEnd,
connectionMode: s.connectionMode,
connectionStartHandle: s.connectionStartHandle,
connectOnClick: s.connectOnClick,
isControlled: s.isControlled,
});
const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
(
{
type = 'source',
position = Position.Top,
isValidConnection = alwaysValid,
isConnectable = true,
id,
onConnect,
children,
className,
...rest
},
ref
) => {
const store = useStoreApi();
const nodeId = useContext(NodeIdContext) as string;
const {
onConnectAction,
onConnectStart,
onConnectStop,
onConnectEnd,
connectionMode,
connectionStartHandle,
connectOnClick,
isControlled,
} = useStore(selector, shallow);
const handleId = id || null;
const isTarget = type === 'target';
const onConnectExtended = useCallback(
(params: Connection) => {
const { defaultEdgeOptions } = store.getState();
const edgeParams = {
...defaultEdgeOptions,
...params,
};
if (isControlled) {
const { edges } = store.getState();
store.setState({ edges: addEdge(edgeParams, edges) });
} else {
onConnectAction?.(edgeParams);
}
onConnect?.(edgeParams);
},
[isControlled, onConnectAction, onConnect]
);
const onMouseDownHandler = useCallback(
(event: React.MouseEvent) => {
onMouseDown(
event,
handleId,
nodeId,
store.setState,
onConnectExtended,
isTarget,
isValidConnection,
connectionMode,
undefined,
undefined,
onConnectStart,
onConnectStop,
onConnectEnd
);
},
[
handleId,
nodeId,
onConnectExtended,
isTarget,
isValidConnection,
connectionMode,
onConnectStart,
onConnectStop,
onConnectEnd,
]
);
const onClick = useCallback(
(event: React.MouseEvent) => {
if (!connectionStartHandle) {
onConnectStart?.(event, { nodeId, handleId, handleType: type });
store.setState({ connectionStartHandle: { nodeId, type, handleId } });
} else {
const doc = getHostForElement(event.target as HTMLElement);
const { connection, isValid } = checkElementBelowIsValid(
event as unknown as MouseEvent,
connectionMode,
connectionStartHandle.type === 'target',
connectionStartHandle.nodeId,
connectionStartHandle.handleId || null,
isValidConnection,
doc
);
onConnectStop?.(event as unknown as MouseEvent);
if (isValid) {
onConnectExtended(connection);
}
onConnectEnd?.(event as unknown as MouseEvent);
store.setState({ connectionStartHandle: null });
}
},
[
connectionStartHandle,
onConnectStart,
onConnectExtended,
onConnectStop,
onConnectEnd,
isTarget,
nodeId,
handleId,
type,
]
);
const handleClasses = cc([
'react-flow__handle',
`react-flow__handle-${position}`,
'nodrag',
className,
{
source: !isTarget,
target: isTarget,
connectable: isConnectable,
connecting:
connectionStartHandle?.nodeId === nodeId &&
connectionStartHandle?.handleId === handleId &&
connectionStartHandle?.type === type,
},
]);
return (
<div
data-handleid={handleId}
data-nodeid={nodeId}
data-handlepos={position}
className={handleClasses}
onMouseDown={onMouseDownHandler}
onClick={connectOnClick ? onClick : undefined}
ref={ref}
{...rest}
>
{children}
</div>
);
}
);
Handle.displayName = 'Handle';
export default memo(Handle);