From 3b25086149202e62a055ca42a4679ded71c8f484 Mon Sep 17 00:00:00 2001 From: moklick Date: Fri, 17 Dec 2021 10:35:53 +0100 Subject: [PATCH] feat(nodes): connect on touch device --- example/src/TouchDevice/index.tsx | 49 +++++++++++++++++++++ example/src/TouchDevice/touch-device.css | 19 ++++++++ example/src/index.tsx | 5 +++ src/components/Handle/handler.ts | 2 +- src/components/Handle/index.tsx | 56 +++++++++++++++++++++--- src/store/initialState.ts | 2 + src/types/general.ts | 4 +- src/types/handles.ts | 6 +++ 8 files changed, 136 insertions(+), 7 deletions(-) create mode 100644 example/src/TouchDevice/index.tsx create mode 100644 example/src/TouchDevice/touch-device.css diff --git a/example/src/TouchDevice/index.tsx b/example/src/TouchDevice/index.tsx new file mode 100644 index 00000000..3a3b11fd --- /dev/null +++ b/example/src/TouchDevice/index.tsx @@ -0,0 +1,49 @@ +import { useCallback } from 'react'; +import ReactFlow, { + Node, + Edge, + useNodesState, + useEdgesState, + Position, + Connection, + addEdge, +} from 'react-flow-renderer'; + +import './touch-device.css'; + +const initialNodes: Node[] = [ + { + id: '1', + data: { label: 'Node 1' }, + position: { x: 100, y: 100 }, + sourcePosition: Position.Right, + targetPosition: Position.Left, + }, + { + id: '2', + data: { label: 'Node 2' }, + position: { x: 300, y: 100 }, + sourcePosition: Position.Right, + targetPosition: Position.Left, + }, +]; + +const initialEdges: Edge[] = []; + +const TouchDeviceFlow = () => { + const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); + const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + const onConnect = useCallback((connection: Connection) => setEdges((eds) => addEdge(connection, eds)), []); + + return ( + + ); +}; + +export default TouchDeviceFlow; diff --git a/example/src/TouchDevice/touch-device.css b/example/src/TouchDevice/touch-device.css new file mode 100644 index 00000000..c1f8d4bf --- /dev/null +++ b/example/src/TouchDevice/touch-device.css @@ -0,0 +1,19 @@ +.react-flow .react-flow__handle { + width: 20px; + height: 20px; + border-radius: 3px; + background-color: #9f7aea; +} + +.react-flow__handle.connecting { + animation: bounce 1600ms infinite ease-out; +} + +@keyframes bounce { + 0% { + transform: translate(0, -50%) scale(1); + } + 50% { + transform: translate(0, -50%) scale(1.1); + } +} diff --git a/example/src/index.tsx b/example/src/index.tsx index a4e81a87..ef35a97e 100644 --- a/example/src/index.tsx +++ b/example/src/index.tsx @@ -11,6 +11,7 @@ import Layouting from './Layouting'; import NestedNodes from './NestedNodes'; import Hidden from './Hidden'; import UpdatableEdge from './UpdatableEdge'; +import TouchDevice from './TouchDevice'; import './index.css'; @@ -51,6 +52,10 @@ const routes = [ path: '/updatable-edge', component: UpdatableEdge, }, + { + path: '/touch-device', + component: TouchDevice, + }, ]; const Header = withRouter(({ history, location }) => { diff --git a/src/components/Handle/handler.ts b/src/components/Handle/handler.ts index 8e7e31ae..c695c557 100644 --- a/src/components/Handle/handler.ts +++ b/src/components/Handle/handler.ts @@ -23,7 +23,7 @@ type Result = { }; // checks if element below mouse is a handle and returns connection in form of an object { source: 123, target: 312 } -function checkElementBelowIsValid( +export function checkElementBelowIsValid( event: MouseEvent, connectionMode: ConnectionMode, isTarget: boolean, diff --git a/src/components/Handle/index.tsx b/src/components/Handle/index.tsx index 4d1e14f3..fee87e81 100644 --- a/src/components/Handle/index.tsx +++ b/src/components/Handle/index.tsx @@ -5,7 +5,8 @@ import shallow from 'zustand/shallow'; import { useStore, useStoreApi } from '../../store'; import NodeIdContext from '../../contexts/NodeIdContext'; import { HandleProps, Connection, ReactFlowState, Position } from '../../types'; -import { onMouseDown } from './handler'; +import { checkElementBelowIsValid, onMouseDown } from './handler'; +import { getHostForElement } from '../../utils'; const alwaysValid = () => true; @@ -17,6 +18,7 @@ const selector = (s: ReactFlowState) => ({ onConnectStop: s.onConnectStop, onConnectEnd: s.onConnectEnd, connectionMode: s.connectionMode, + connectionStartHandle: s.connectionStartHandle, }); const Handle = forwardRef( @@ -36,10 +38,8 @@ const Handle = forwardRef( ) => { const store = useStoreApi(); const nodeId = useContext(NodeIdContext) as string; - const { onConnectAction, onConnectStart, onConnectStop, onConnectEnd, connectionMode } = useStore( - selector, - shallow - ); + const { onConnectAction, onConnectStart, onConnectStop, onConnectEnd, connectionMode, connectionStartHandle } = + useStore(selector, shallow); const handleId = id || null; const isTarget = type === 'target'; @@ -83,6 +83,47 @@ const Handle = forwardRef( ] ); + 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}`, @@ -92,6 +133,10 @@ const Handle = forwardRef( source: !isTarget, target: isTarget, connectable: isConnectable, + connecting: + connectionStartHandle?.nodeId === nodeId && + connectionStartHandle?.handleId === handleId && + connectionStartHandle?.type === type, }, ]); @@ -102,6 +147,7 @@ const Handle = forwardRef( data-handlepos={position} className={handleClasses} onMouseDown={onMouseDownHandler} + onClick={onClick} ref={ref} {...rest} > diff --git a/src/store/initialState.ts b/src/store/initialState.ts index 98ea8c50..41366ca3 100644 --- a/src/store/initialState.ts +++ b/src/store/initialState.ts @@ -42,6 +42,8 @@ const initialState: ReactFlowStore = { fitViewOnInit: false, fitViewOnInitDone: false, + + connectionStartHandle: null, }; export default initialState; diff --git a/src/types/general.ts b/src/types/general.ts index 1f67dbc9..0c6a5a7f 100644 --- a/src/types/general.ts +++ b/src/types/general.ts @@ -5,7 +5,7 @@ import { XYPosition, Rect, Transform, CoordinateExtent } from './utils'; import { NodeChange, EdgeChange } from './changes'; import { Node, NodeInternals, NodeDimensionUpdate, NodeDiffUpdate } from './nodes'; import { Edge } from './edges'; -import { HandleType } from './handles'; +import { HandleType, StartHandle } from './handles'; export type FlowElement = Node | Edge; @@ -178,6 +178,8 @@ export type ReactFlowStore = { fitViewOnInit: boolean; fitViewOnInitDone: boolean; + connectionStartHandle: StartHandle | null; + onConnect?: OnConnect; onConnectStart?: OnConnectStart; onConnectStop?: OnConnectStop; diff --git a/src/types/handles.ts b/src/types/handles.ts index 9114b360..37d1ea41 100644 --- a/src/types/handles.ts +++ b/src/types/handles.ts @@ -8,6 +8,12 @@ export interface HandleElement extends XYPosition, Dimensions { position: Position; } +export interface StartHandle { + nodeId: string; + type: HandleType; + handleId?: string | null; +} + export interface HandleProps { type: HandleType; position: Position;