feat(handles): validate connection

This commit is contained in:
moklick
2019-10-07 19:56:47 +02:00
parent 0e67f5fefa
commit ae1bb70882
7 changed files with 89 additions and 21 deletions
+6
View File
@@ -19,6 +19,12 @@
left: 10px; left: 10px;
z-index: 4; z-index: 4;
} }
.react-flow__handle.connecting {
background: orange;
}
.react-flow__handle.valid {
background: green;
}
</style> </style>
</head> </head>
<body> <body>
+6 -1
View File
@@ -9,6 +9,11 @@ export default({ data, styles }) => (
<Handle type="target" position="left" style={{ background: '#999' }} /> <Handle type="target" position="left" style={{ background: '#999' }} />
<div>{data.input}</div> <div>{data.input}</div>
<input onChange={(e) => data.onChange(e.target.value, data)} /> <input onChange={(e) => data.onChange(e.target.value, data)} />
<Handle type="source" position="right" style={{ background: '#999' }} /> <Handle
type="source"
position="right"
style={{ background: '#999' }}
isValidConnection={connection => +connection.target % 2 === 0}
/>
</div> </div>
); );
+5 -1
View File
@@ -26,7 +26,11 @@ export default ({ data, styles }) => {
<option value="2">2</option> <option value="2">2</option>
<option value="3">3</option> <option value="3">3</option>
</select> </select>
<Handle type="source" position="bottom" style={{ left: 10, background: '#999' }} /> <Handle
type="source"
position="bottom"
style={{ left: 10, background: '#999' }}
/>
</div> </div>
); );
}; };
+63 -12
View File
@@ -1,8 +1,9 @@
import React, { memo } from 'react'; import React, { memo } from 'react';
import cx from 'classnames'; import cx from 'classnames';
function onMouseDown(evt, { nodeId, setSourceId, setPosition, onConnect, isTarget }) { function onMouseDown(evt, { nodeId, setSourceId, setPosition, onConnect, isTarget, isValidConnection }) {
const containerBounds = document.querySelector('.react-flow').getBoundingClientRect(); const containerBounds = document.querySelector('.react-flow').getBoundingClientRect();
let recentHoveredHandle = null;
setPosition({ setPosition({
x: evt.clientX - containerBounds.x, x: evt.clientX - containerBounds.x,
@@ -10,24 +11,71 @@ function onMouseDown(evt, { nodeId, setSourceId, setPosition, onConnect, isTarg
}); });
setSourceId(nodeId); setSourceId(nodeId);
// checks if element below mouse is a handle and returns connection in form of an object { source: 123, target: 312 }
function checkElementBelowIsValid(evt) {
const elementBelow = document.elementFromPoint(evt.clientX, evt.clientY);
const result = {
elementBelow,
isValid: false,
connection: null,
isHoveringHandle: false
};
if (elementBelow && (elementBelow.classList.contains('target') || elementBelow.classList.contains('source'))) {
let connection = null;
if (isTarget) {
const sourceId = elementBelow.getAttribute('data-nodeid');
connection = { source: sourceId, target: nodeId };
} else {
const targetId = elementBelow.getAttribute('data-nodeid');
connection = { source: nodeId, target: targetId };
}
const isValid = isValidConnection(connection);
result.connection = connection;
result.isValid = isValid;
result.isHoveringHandle = true;
}
return result;
}
function onMouseMove(evt) { function onMouseMove(evt) {
setPosition({ setPosition({
x: evt.clientX - containerBounds.x, x: evt.clientX - containerBounds.x,
y: evt.clientY - containerBounds.y, y: evt.clientY - containerBounds.y,
}); });
const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid(evt);
if (!isHoveringHandle) {
if (recentHoveredHandle) {
recentHoveredHandle.classList.remove('valid');
recentHoveredHandle.classList.remove('connecting');
}
return false;
}
const isOwnHandle = connection.source === connection.target;
if (!isOwnHandle) {
recentHoveredHandle = elementBelow;
elementBelow.classList.add('connecting');
elementBelow.classList.toggle('valid', isValid);
}
} }
function onMouseUp(evt) { function onMouseUp(evt) {
const elementBelow = document.elementFromPoint(evt.clientX, evt.clientY); const { connection, isValid } = checkElementBelowIsValid(evt);
if (elementBelow && (elementBelow.classList.contains('target') || elementBelow.classList.contains('source'))) { if (isValid) {
if (isTarget) { onConnect(connection);
const sourceId = elementBelow.getAttribute('data-nodeid'); }
onConnect({ source: sourceId, target: nodeId });
} else { if (recentHoveredHandle) {
const targetId = elementBelow.getAttribute('data-nodeid'); recentHoveredHandle.classList.remove('valid');
onConnect({ source: nodeId, target: targetId });
}
} }
setSourceId(null); setSourceId(null);
@@ -42,7 +90,7 @@ function onMouseDown(evt, { nodeId, setSourceId, setPosition, onConnect, isTarg
const BaseHandle = memo(({ const BaseHandle = memo(({
type, nodeId, onConnect, position, type, nodeId, onConnect, position,
setSourceId, setPosition, className, setSourceId, setPosition, className,
id = false, ...rest id = false, isValidConnection, ...rest
}) => { }) => {
const isTarget = type === 'target'; const isTarget = type === 'target';
const handleClasses = cx( const handleClasses = cx(
@@ -59,7 +107,10 @@ const BaseHandle = memo(({
data-nodeid={nodeIdWithHandleId} data-nodeid={nodeIdWithHandleId}
data-handlepos={position} data-handlepos={position}
className={handleClasses} className={handleClasses}
onMouseDown={evt => onMouseDown(evt, { nodeId: nodeIdWithHandleId, setSourceId, setPosition, onConnect, isTarget })} onMouseDown={evt => onMouseDown(evt, {
nodeId: nodeIdWithHandleId, setSourceId, setPosition,
onConnect, isTarget, isValidConnection
})}
{...rest} {...rest}
/> />
); );
+4 -2
View File
@@ -33,13 +33,15 @@ Handle.displayName = 'Handle';
Handle.propTypes = { Handle.propTypes = {
type: PropTypes.oneOf(['source', 'target']), type: PropTypes.oneOf(['source', 'target']),
position: PropTypes.oneOf(['top', 'right', 'bottom', 'left']), position: PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
onConnect: PropTypes.func onConnect: PropTypes.func,
isValidConnection: PropTypes.func
}; };
Handle.defaultProps = { Handle.defaultProps = {
type: 'source', type: 'source',
position: 'top', position: 'top',
onConnect: () => {} onConnect: () => {},
isValidConnection: () => true
}; };
export default Handle; export default Handle;
@@ -21,7 +21,7 @@ import store from '../../store';
import '../../style.css'; import '../../style.css';
const Editor = ({ const ReactFlow = ({
style, onElementClick, elements, children, style, onElementClick, elements, children,
nodeTypes, edgeTypes, onLoad, onMove, nodeTypes, edgeTypes, onLoad, onMove,
onElementsRemove, onConnect, onNodeDragStop, connectionLineType, onElementsRemove, onConnect, onNodeDragStop, connectionLineType,
@@ -54,9 +54,9 @@ const Editor = ({
); );
}; };
Editor.displayName = 'Editor'; ReactFlow.displayName = 'ReactFlow';
Editor.defaultProps = { ReactFlow.defaultProps = {
onElementClick: () => {}, onElementClick: () => {},
onElementsRemove: () => {}, onElementsRemove: () => {},
onNodeDragStop: () => {}, onNodeDragStop: () => {},
@@ -79,4 +79,4 @@ Editor.defaultProps = {
selectionKeyCode: 16 selectionKeyCode: 16
}; };
export default Editor; export default ReactFlow;
+1 -1
View File
@@ -1,4 +1,4 @@
import ReactFlow from './container/Editor'; import ReactFlow from './container/ReactFlow';
export default ReactFlow; export default ReactFlow;