Merge pull request #1771 from wbkd/feat/mobile-support

Feat/mobile support
This commit is contained in:
Moritz Klack
2021-12-17 10:52:57 +01:00
committed by GitHub
20 changed files with 227 additions and 385 deletions
@@ -1,20 +1,18 @@
import React, { useState, MouseEvent, CSSProperties } from 'react'; import { MouseEvent, CSSProperties } from 'react';
import ReactFlow, { import ReactFlow, {
removeElements,
addEdge, addEdge,
MiniMap, MiniMap,
Controls, Controls,
Background, Background,
isNode,
Node, Node,
Elements,
FlowElement,
OnLoadParams,
FlowTransform, FlowTransform,
SnapGrid, SnapGrid,
Connection, Connection,
Edge, Edge,
ReactFlowInstance,
useNodesState,
useEdgesState,
OnSelectionChangeParams,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
const onNodeDragStart = (_: MouseEvent, node: Node) => console.log('drag start', node); const onNodeDragStart = (_: MouseEvent, node: Node) => console.log('drag start', node);
@@ -31,10 +29,10 @@ const onSelectionContextMenu = (event: MouseEvent, nodes: Node[]) => {
event.preventDefault(); event.preventDefault();
console.log('selection context menu', nodes); console.log('selection context menu', nodes);
}; };
const onElementClick = (_: MouseEvent, element: FlowElement) => const onNodeClick = (_: MouseEvent, node: Node) => console.log('node click:', node);
console.log(`${isNode(element) ? 'node' : 'edge'} click:`, element);
const onSelectionChange = (elements: Elements | null) => console.log('selection change', elements); const onSelectionChange = ({ nodes, edges }: OnSelectionChangeParams) => console.log('selection change', nodes, edges);
const onLoad = (reactFlowInstance: OnLoadParams) => { const onPaneReady = (reactFlowInstance: ReactFlowInstance) => {
console.log('flow loaded:', reactFlowInstance); console.log('flow loaded:', reactFlowInstance);
reactFlowInstance.fitView(); reactFlowInstance.fitView();
}; };
@@ -47,7 +45,7 @@ const onEdgeMouseMove = (_: MouseEvent, edge: Edge) => console.log('edge mouse m
const onEdgeMouseLeave = (_: MouseEvent, edge: Edge) => console.log('edge mouse leave', edge); const onEdgeMouseLeave = (_: MouseEvent, edge: Edge) => console.log('edge mouse leave', edge);
const onEdgeDoubleClick = (_: MouseEvent, edge: Edge) => console.log('edge double click', edge); const onEdgeDoubleClick = (_: MouseEvent, edge: Edge) => console.log('edge double click', edge);
const initialElements: Elements = [ const initialNodes: Node[] = [
{ {
id: '1', id: '1',
type: 'input', type: 'input',
@@ -121,6 +119,9 @@ const initialElements: Elements = [
position: { x: 100, y: 480 }, position: { x: 100, y: 480 },
}, },
{ id: '7', type: 'output', data: { label: 'Another output node' }, position: { x: 400, y: 450 } }, { id: '7', type: 'output', data: { label: 'Another output node' }, position: { x: 400, y: 450 } },
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', label: 'this is an edge label' }, { id: 'e1-2', source: '1', target: '2', label: 'this is an edge label' },
{ id: 'e1-3', source: '1', target: '3' }, { id: 'e1-3', source: '1', target: '3' },
{ id: 'e3-4', source: '3', target: '4', animated: true, label: 'animated edge' }, { id: 'e3-4', source: '3', target: '4', animated: true, label: 'animated edge' },
@@ -157,15 +158,17 @@ const nodeColor = (n: Node): string => {
}; };
const OverviewFlow = () => { const OverviewFlow = () => {
const [elements, setElements] = useState(initialElements); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
return ( return (
<ReactFlow <ReactFlow
elements={elements} nodes={nodes}
onElementClick={onElementClick} edges={edges}
onElementsRemove={onElementsRemove} onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onConnect={onConnect} onConnect={onConnect}
onPaneClick={onPaneClick} onPaneClick={onPaneClick}
onPaneScroll={onPaneScroll} onPaneScroll={onPaneScroll}
@@ -181,7 +184,7 @@ const OverviewFlow = () => {
onSelectionChange={onSelectionChange} onSelectionChange={onSelectionChange}
onMoveStart={onMoveStart} onMoveStart={onMoveStart}
onMoveEnd={onMoveEnd} onMoveEnd={onMoveEnd}
onLoad={onLoad} onPaneReady={onPaneReady}
connectionLineStyle={connectionLineStyle} connectionLineStyle={connectionLineStyle}
snapToGrid={true} snapToGrid={true}
snapGrid={snapGrid} snapGrid={snapGrid}
+50
View File
@@ -0,0 +1,50 @@
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 (
<ReactFlow
nodes={nodes}
edges={edges}
onConnect={onConnect}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
className="touchdevice-flow"
/>
);
};
export default TouchDeviceFlow;
+19
View File
@@ -0,0 +1,19 @@
.touchdevice-flow .react-flow__handle {
width: 20px;
height: 20px;
border-radius: 3px;
background-color: #9f7aea;
}
.touchdevice-flow .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);
}
}
+10
View File
@@ -2,6 +2,7 @@ import { ChangeEvent } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route, Switch, withRouter } from 'react-router-dom'; import { BrowserRouter as Router, Route, Switch, withRouter } from 'react-router-dom';
import Overview from './Overview';
import Basic from './Basic'; import Basic from './Basic';
import UpdateNode from './UpdateNode'; import UpdateNode from './UpdateNode';
import Stress from './Stress'; import Stress from './Stress';
@@ -11,12 +12,17 @@ import Layouting from './Layouting';
import NestedNodes from './NestedNodes'; import NestedNodes from './NestedNodes';
import Hidden from './Hidden'; import Hidden from './Hidden';
import UpdatableEdge from './UpdatableEdge'; import UpdatableEdge from './UpdatableEdge';
import TouchDevice from './TouchDevice';
import './index.css'; import './index.css';
const routes = [ const routes = [
{ {
path: '/', path: '/',
component: Overview,
},
{
path: '/basic',
component: Basic, component: Basic,
}, },
{ {
@@ -51,6 +57,10 @@ const routes = [
path: '/updatable-edge', path: '/updatable-edge',
component: UpdatableEdge, component: UpdatableEdge,
}, },
{
path: '/touch-device',
component: TouchDevice,
},
]; ];
const Header = withRouter(({ history, location }) => { const Header = withRouter(({ history, location }) => {
-98
View File
@@ -1,98 +0,0 @@
import React, { useState, MouseEvent } from 'react';
import ReactFlow, {
removeElements,
addEdge,
isNode,
Background,
Elements,
BackgroundVariant,
FlowElement,
Node,
Edge,
Connection,
OnLoadParams,
} from 'react-flow-renderer';
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element);
const initialNodes: Node[] = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' },
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
];
const BasicFlow = () => {
const [rfInstance, setRfInstance] = useState<OnLoadParams | null>(null);
const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
// const onElementsRemove = (elementsToRemove: Elements) => setNodes((els) => removeElements(elementsToRemove, els));
// const onConnect = (params: Edge | Connection) => setNodes((els) => addEdge(params, els));
const onLoad = (reactFlowInstance: OnLoadParams) => setRfInstance(reactFlowInstance);
const updatePos = () => {
setNodes((nds) => {
return nds.map((n) => {
n.position = {
x: Math.random() * 400,
y: Math.random() * 400,
};
return n;
});
});
};
const logToObject = () => console.log(rfInstance?.toObject());
const resetTransform = () => rfInstance?.setTransform({ x: 0, y: 0, zoom: 1 });
const toggleClassnames = () => {
setNodes((nds) => {
return nds.map((n) => {
n.className = n.className === 'light' ? 'dark' : 'light';
return n;
});
});
};
return (
<ReactFlow
nodes={nodes}
edges={edges}
onLoad={onLoad}
onElementClick={onElementClick}
// onElementsRemove={onElementsRemove}
// onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
className="react-flow-basic-example"
defaultZoom={1.5}
minZoom={0.2}
maxZoom={4}
>
<Background variant={BackgroundVariant.Lines} />
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
<button onClick={resetTransform} style={{ marginRight: 5 }}>
reset transform
</button>
<button onClick={updatePos} style={{ marginRight: 5 }}>
change pos
</button>
<button onClick={toggleClassnames} style={{ marginRight: 5 }}>
toggle classnames
</button>
<button onClick={logToObject}>toObject</button>
</div>
</ReactFlow>
);
};
export default BasicFlow;
@@ -1,25 +0,0 @@
import React, { memo, FC, CSSProperties } from 'react';
import { Handle, Position, NodeProps, Connection, Edge } from 'react-flow-renderer';
const targetHandleStyle: CSSProperties = { background: '#555' };
const sourceHandleStyleA: CSSProperties = { ...targetHandleStyle, top: 10 };
const sourceHandleStyleB: CSSProperties = { ...targetHandleStyle, bottom: 10, top: 'auto' };
const onConnect = (params: Connection | Edge) => console.log('handle onConnect', params);
const ColorSelectorNode: FC<NodeProps> = ({ data, isConnectable }) => {
return (
<>
<Handle type="target" position={Position.Left} style={targetHandleStyle} onConnect={onConnect} />
<div>
Custom Color Picker Node: <strong>{data.color}</strong>
</div>
<input className="nodrag" type="color" onChange={data.onChange} defaultValue={data.color} />
<Handle type="source" position={Position.Right} id="a" style={sourceHandleStyleA} isConnectable={isConnectable} />
<Handle type="source" position={Position.Right} id="b" style={sourceHandleStyleB} isConnectable={isConnectable} />
</>
);
};
export default memo(ColorSelectorNode);
-135
View File
@@ -1,135 +0,0 @@
import React, { useState, useEffect, MouseEvent } from 'react';
import { ChangeEvent } from 'react';
import ReactFlow, {
isEdge,
removeElements,
addEdge,
MiniMap,
Controls,
Node,
FlowElement,
OnLoadParams,
Elements,
Position,
SnapGrid,
Connection,
Edge,
} from 'react-flow-renderer';
import ColorSelectorNode from './ColorSelectorNode';
const onLoad = (reactFlowInstance: OnLoadParams) => console.log('flow loaded:', reactFlowInstance);
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element);
const initBgColor = '#1A192B';
const connectionLineStyle = { stroke: '#fff' };
const snapGrid: SnapGrid = [16, 16];
const nodeTypes = {
selectorNode: ColorSelectorNode,
};
const CustomNodeFlow = () => {
const [elements, setElements] = useState<Elements>([]);
const [bgColor, setBgColor] = useState<string>(initBgColor);
useEffect(() => {
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
setElements((els) =>
els.map((e) => {
if (isEdge(e) || e.id !== '2') {
return e;
}
const color = event.target.value;
setBgColor(color);
return {
...e,
data: {
...e.data,
color,
},
};
})
);
};
setElements([
{
id: '1',
type: 'input',
data: { label: 'An input node' },
position: { x: 0, y: 50 },
sourcePosition: Position.Right,
},
{
id: '2',
type: 'selectorNode',
data: { onChange: onChange, color: initBgColor },
style: { border: '1px solid #777', padding: 10 },
position: { x: 250, y: 50 },
},
{
id: '3',
type: 'output',
data: { label: 'Output A' },
position: { x: 550, y: 25 },
targetPosition: Position.Left,
},
{
id: '4',
type: 'output',
data: { label: 'Output B' },
position: { x: 550, y: 100 },
targetPosition: Position.Left,
},
{ id: 'e1-2', source: '1', target: '2', animated: true, style: { stroke: '#fff' } },
{ id: 'e2a-3', source: '2', sourceHandle: 'a', target: '3', animated: true, style: { stroke: '#fff' } },
{ id: 'e2b-4', source: '2', sourceHandle: 'b', target: '4', animated: true, style: { stroke: '#fff' } },
]);
}, []);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
const onConnect = (params: Connection | Edge) =>
setElements((els) => addEdge({ ...params, animated: true, style: { stroke: '#fff' } }, els));
return (
<ReactFlow
elements={elements}
onElementClick={onElementClick}
onElementsRemove={onElementsRemove}
onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
style={{ background: bgColor }}
onLoad={onLoad}
nodeTypes={nodeTypes}
connectionLineStyle={connectionLineStyle}
snapToGrid={true}
snapGrid={snapGrid}
defaultZoom={1.5}
>
<MiniMap
nodeStrokeColor={(n: Node): string => {
if (n.type === 'input') return '#0041d0';
if (n.type === 'selectorNode') return bgColor;
if (n.type === 'output') return '#ff0072';
return '#eee';
}}
nodeColor={(n: Node): string => {
if (n.type === 'selectorNode') return bgColor;
return '#fff';
}}
/>
<Controls />
</ReactFlow>
);
};
export default CustomNodeFlow;
-70
View File
@@ -1,70 +0,0 @@
import React, { useState, CSSProperties } from 'react';
import ReactFlow, {
removeElements,
addEdge,
MiniMap,
isNode,
Controls,
Background,
OnLoadParams,
Elements,
Connection,
Edge,
} from 'react-flow-renderer';
import { getElements } from './utils';
const buttonWrapperStyles: CSSProperties = { position: 'absolute', right: 10, top: 10, zIndex: 4 };
const onLoad = (reactFlowInstance: OnLoadParams) => {
reactFlowInstance.fitView();
console.log(reactFlowInstance.getElements());
};
const initialElements: Elements = getElements(30, 30);
const StressFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
const updatePos = () => {
setElements((elms) => {
return elms.map((el) => {
if (isNode(el)) {
return {
...el,
position: {
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
},
};
}
return el;
});
});
};
const updateElements = () => {
const grid = Math.ceil(Math.random() * 10);
setElements(getElements(grid, grid));
};
return (
<ReactFlow elements={elements} onLoad={onLoad} onElementsRemove={onElementsRemove} onConnect={onConnect}>
<MiniMap />
<Controls />
<Background />
<div style={buttonWrapperStyles}>
<button onClick={updatePos} style={{ marginRight: 5 }}>
change pos
</button>
<button onClick={updateElements}>update elements</button>
</div>
</ReactFlow>
);
};
export default StressFlow;
-30
View File
@@ -1,30 +0,0 @@
import { Elements } from 'react-flow-renderer';
export function getElements(xElements: number = 10, yElements: number = 10): Elements {
const initialElements = [];
let nodeId = 1;
let recentNodeId = null;
for (let y = 0; y < yElements; y++) {
for (let x = 0; x < xElements; x++) {
const position = { x: x * 100, y: y * 50 };
const data = { label: `Node ${nodeId}` };
const node = {
id: nodeId.toString(),
style: { width: 50, fontSize: 11 },
data,
position,
};
initialElements.push(node);
if (recentNodeId && nodeId <= xElements * yElements) {
initialElements.push({ id: `${x}-${y}`, source: recentNodeId.toString(), target: nodeId.toString() });
}
recentNodeId = nodeId;
nodeId++;
}
}
return initialElements;
}
+25
View File
@@ -23,6 +23,7 @@
"@babel/preset-env": "^7.16.4", "@babel/preset-env": "^7.16.4",
"@babel/preset-react": "^7.16.0", "@babel/preset-react": "^7.16.0",
"@babel/preset-typescript": "^7.16.0", "@babel/preset-typescript": "^7.16.0",
"@rollup/plugin-alias": "^3.1.8",
"@rollup/plugin-babel": "^5.3.0", "@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-commonjs": "^21.0.1", "@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-node-resolve": "^13.0.6", "@rollup/plugin-node-resolve": "^13.0.6",
@@ -2103,6 +2104,21 @@
"@octokit/openapi-types": "^11.2.0" "@octokit/openapi-types": "^11.2.0"
} }
}, },
"node_modules/@rollup/plugin-alias": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-3.1.8.tgz",
"integrity": "sha512-tf7HeSs/06wO2LPqKNY3Ckbvy0JRe7Jyn98bXnt/gfrxbe+AJucoNJlsEVi9sdgbQtXemjbakCpO/76JVgnHpA==",
"dev": true,
"dependencies": {
"slash": "^3.0.0"
},
"engines": {
"node": ">=8.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0"
}
},
"node_modules/@rollup/plugin-babel": { "node_modules/@rollup/plugin-babel": {
"version": "5.3.0", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz",
@@ -12374,6 +12390,15 @@
"@octokit/openapi-types": "^11.2.0" "@octokit/openapi-types": "^11.2.0"
} }
}, },
"@rollup/plugin-alias": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-3.1.8.tgz",
"integrity": "sha512-tf7HeSs/06wO2LPqKNY3Ckbvy0JRe7Jyn98bXnt/gfrxbe+AJucoNJlsEVi9sdgbQtXemjbakCpO/76JVgnHpA==",
"dev": true,
"requires": {
"slash": "^3.0.0"
}
},
"@rollup/plugin-babel": { "@rollup/plugin-babel": {
"version": "5.3.0", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz",
+1
View File
@@ -45,6 +45,7 @@
"@babel/preset-env": "^7.16.4", "@babel/preset-env": "^7.16.4",
"@babel/preset-react": "^7.16.0", "@babel/preset-react": "^7.16.0",
"@babel/preset-typescript": "^7.16.0", "@babel/preset-typescript": "^7.16.0",
"@rollup/plugin-alias": "^3.1.8",
"@rollup/plugin-babel": "^5.3.0", "@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-commonjs": "^21.0.1", "@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-node-resolve": "^13.0.6", "@rollup/plugin-node-resolve": "^13.0.6",
+4
View File
@@ -4,6 +4,7 @@ import babel from '@rollup/plugin-babel';
import postcss from 'rollup-plugin-postcss'; import postcss from 'rollup-plugin-postcss';
import bundleSize from 'rollup-plugin-bundle-size'; import bundleSize from 'rollup-plugin-bundle-size';
import replace from '@rollup/plugin-replace'; import replace from '@rollup/plugin-replace';
import alias from '@rollup/plugin-alias';
import svgr from '@svgr/rollup'; import svgr from '@svgr/rollup';
import typescript from 'rollup-plugin-typescript2'; import typescript from 'rollup-plugin-typescript2';
import { DEFAULT_EXTENSIONS as DEFAULT_BABEL_EXTENSIONS } from '@babel/core'; import { DEFAULT_EXTENSIONS as DEFAULT_BABEL_EXTENSIONS } from '@babel/core';
@@ -38,6 +39,9 @@ export const baseConfig = ({ mainFile = pkg.main, moduleFile = pkg.module, injec
}, },
], ],
plugins: [ plugins: [
alias({
entries: [{ find: 'd3-color', replacement: __dirname + '/src/d3-color-alias' }],
}),
replace({ replace({
__ENV__: JSON.stringify(processEnv), __ENV__: JSON.stringify(processEnv),
__REACT_FLOW_VERSION__: JSON.stringify(pkg.version), __REACT_FLOW_VERSION__: JSON.stringify(pkg.version),
+1 -1
View File
@@ -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 } // 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, event: MouseEvent,
connectionMode: ConnectionMode, connectionMode: ConnectionMode,
isTarget: boolean, isTarget: boolean,
+59 -5
View File
@@ -5,7 +5,8 @@ import shallow from 'zustand/shallow';
import { useStore, useStoreApi } from '../../store'; import { useStore, useStoreApi } from '../../store';
import NodeIdContext from '../../contexts/NodeIdContext'; import NodeIdContext from '../../contexts/NodeIdContext';
import { HandleProps, Connection, ReactFlowState, Position } from '../../types'; import { HandleProps, Connection, ReactFlowState, Position } from '../../types';
import { onMouseDown } from './handler'; import { checkElementBelowIsValid, onMouseDown } from './handler';
import { getHostForElement } from '../../utils';
const alwaysValid = () => true; const alwaysValid = () => true;
@@ -17,6 +18,8 @@ const selector = (s: ReactFlowState) => ({
onConnectStop: s.onConnectStop, onConnectStop: s.onConnectStop,
onConnectEnd: s.onConnectEnd, onConnectEnd: s.onConnectEnd,
connectionMode: s.connectionMode, connectionMode: s.connectionMode,
connectionStartHandle: s.connectionStartHandle,
connectOnClick: s.connectOnClick,
}); });
const Handle = forwardRef<HTMLDivElement, HandleComponentProps>( const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
@@ -36,10 +39,15 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
) => { ) => {
const store = useStoreApi(); const store = useStoreApi();
const nodeId = useContext(NodeIdContext) as string; const nodeId = useContext(NodeIdContext) as string;
const { onConnectAction, onConnectStart, onConnectStop, onConnectEnd, connectionMode } = useStore( const {
selector, onConnectAction,
shallow onConnectStart,
); onConnectStop,
onConnectEnd,
connectionMode,
connectionStartHandle,
connectOnClick,
} = useStore(selector, shallow);
const handleId = id || null; const handleId = id || null;
const isTarget = type === 'target'; const isTarget = type === 'target';
@@ -83,6 +91,47 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
] ]
); );
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([ const handleClasses = cc([
'react-flow__handle', 'react-flow__handle',
`react-flow__handle-${position}`, `react-flow__handle-${position}`,
@@ -92,6 +141,10 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
source: !isTarget, source: !isTarget,
target: isTarget, target: isTarget,
connectable: isConnectable, connectable: isConnectable,
connecting:
connectionStartHandle?.nodeId === nodeId &&
connectionStartHandle?.handleId === handleId &&
connectionStartHandle?.type === type,
}, },
]); ]);
@@ -102,6 +155,7 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
data-handlepos={position} data-handlepos={position}
className={handleClasses} className={handleClasses}
onMouseDown={onMouseDownHandler} onMouseDown={onMouseDownHandler}
onClick={connectOnClick ? onClick : undefined}
ref={ref} ref={ref}
{...rest} {...rest}
> >
+3
View File
@@ -38,6 +38,7 @@ interface StoreUpdaterProps {
snapGrid?: SnapGrid; snapGrid?: SnapGrid;
translateExtent?: CoordinateExtent; translateExtent?: CoordinateExtent;
fitViewOnInit: boolean; fitViewOnInit: boolean;
connectOnClick: boolean;
} }
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
@@ -87,6 +88,7 @@ const StoreUpdater = ({
snapToGrid, snapToGrid,
translateExtent, translateExtent,
fitViewOnInit, fitViewOnInit,
connectOnClick,
}: StoreUpdaterProps) => { }: StoreUpdaterProps) => {
const { setNodes, setEdges, setMinZoom, setMaxZoom, setTranslateExtent, setNodeExtent, reset } = useStore( const { setNodes, setEdges, setMinZoom, setMaxZoom, setTranslateExtent, setNodeExtent, reset } = useStore(
selector, selector,
@@ -120,6 +122,7 @@ const StoreUpdater = ({
useDirectStoreUpdater('snapGrid', snapGrid, store.setState); useDirectStoreUpdater('snapGrid', snapGrid, store.setState);
useDirectStoreUpdater('onNodesChange', onNodesChange, store.setState); useDirectStoreUpdater('onNodesChange', onNodesChange, store.setState);
useDirectStoreUpdater('onEdgesChange', onEdgesChange, store.setState); useDirectStoreUpdater('onEdgesChange', onEdgesChange, store.setState);
useDirectStoreUpdater('connectOnClick', connectOnClick, store.setState);
return null; return null;
}; };
+3
View File
@@ -134,6 +134,7 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'on
noWheelClassName?: string; noWheelClassName?: string;
noPanClassName?: string; noPanClassName?: string;
fitViewOnInit?: boolean; fitViewOnInit?: boolean;
connectOnClick?: boolean;
} }
export type ReactFlowRefType = HTMLDivElement; export type ReactFlowRefType = HTMLDivElement;
@@ -221,6 +222,7 @@ const ReactFlow: FunctionComponent<ReactFlowProps> = forwardRef<ReactFlowRefType
noWheelClassName = 'nowheel', noWheelClassName = 'nowheel',
noPanClassName = 'nopan', noPanClassName = 'nopan',
fitViewOnInit = false, fitViewOnInit = false,
connectOnClick = true,
...rest ...rest
}, },
ref ref
@@ -309,6 +311,7 @@ const ReactFlow: FunctionComponent<ReactFlowProps> = forwardRef<ReactFlowRefType
connectionMode={connectionMode} connectionMode={connectionMode}
translateExtent={translateExtent} translateExtent={translateExtent}
fitViewOnInit={fitViewOnInit} fitViewOnInit={fitViewOnInit}
connectOnClick={connectOnClick}
/> />
{onSelectionChange && <SelectionListener onSelectionChange={onSelectionChange} />} {onSelectionChange && <SelectionListener onSelectionChange={onSelectionChange} />}
{children} {children}
+10
View File
@@ -0,0 +1,10 @@
// this helps us to reduce bundle size by ~10kb. d3-color is not used but can't be treeshaked in this case.
const obj = {};
export const rgb = {};
export const color = {};
export const hsl = {};
export const lab = {};
export const hcl = {};
export const cubehelix = {};
export default obj;
+3
View File
@@ -42,6 +42,9 @@ const initialState: ReactFlowStore = {
fitViewOnInit: false, fitViewOnInit: false,
fitViewOnInitDone: false, fitViewOnInitDone: false,
connectionStartHandle: null,
connectOnClick: true,
}; };
export default initialState; export default initialState;
+11 -2
View File
@@ -5,7 +5,7 @@ import { XYPosition, Rect, Transform, CoordinateExtent } from './utils';
import { NodeChange, EdgeChange } from './changes'; import { NodeChange, EdgeChange } from './changes';
import { Node, NodeInternals, NodeDimensionUpdate, NodeDiffUpdate } from './nodes'; import { Node, NodeInternals, NodeDimensionUpdate, NodeDiffUpdate } from './nodes';
import { Edge } from './edges'; import { Edge } from './edges';
import { HandleType } from './handles'; import { HandleType, StartHandle } from './handles';
export type FlowElement<T = any> = Node<T> | Edge<T>; export type FlowElement<T = any> = Node<T> | Edge<T>;
@@ -178,10 +178,14 @@ export type ReactFlowStore = {
fitViewOnInit: boolean; fitViewOnInit: boolean;
fitViewOnInitDone: boolean; fitViewOnInitDone: boolean;
connectionStartHandle: StartHandle | null;
onConnect?: OnConnect; onConnect?: OnConnect;
onConnectStart?: OnConnectStart; onConnectStart?: OnConnectStart;
onConnectStop?: OnConnectStop; onConnectStop?: OnConnectStop;
onConnectEnd?: OnConnectEnd; onConnectEnd?: OnConnectEnd;
connectOnClick: boolean;
}; };
export type ReactFlowActions = { export type ReactFlowActions = {
@@ -204,4 +208,9 @@ export type ReactFlowState = ReactFlowStore & ReactFlowActions;
export type UpdateNodeInternals = (nodeId: string) => void; export type UpdateNodeInternals = (nodeId: string) => void;
export type OnSelectionChangeFunc = (params: { nodes: Node[]; edges: Edge[] }) => void; export type OnSelectionChangeParams = {
nodes: Node[];
edges: Edge[];
};
export type OnSelectionChangeFunc = (params: OnSelectionChangeParams) => void;
+6
View File
@@ -8,6 +8,12 @@ export interface HandleElement extends XYPosition, Dimensions {
position: Position; position: Position;
} }
export interface StartHandle {
nodeId: string;
type: HandleType;
handleId?: string | null;
}
export interface HandleProps { export interface HandleProps {
type: HandleType; type: HandleType;
position: Position; position: Position;