feat(handle): add type and position
This commit is contained in:
@@ -1,21 +1,21 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import Graph, { isEdge, removeElements, getOutgoers, SourceHandle, TargetHandle, MiniMap } from '../src';
|
||||
// import Graph, { isEdge, removeElements, getOutgoers, SourceHandle, TargetHandle } from '../dist/ReactGraph';
|
||||
import Graph, { isEdge, removeElements, getOutgoers, Handle, MiniMap } from '../src';
|
||||
// import Graph, { isEdge, removeElements, getOutgoers, Handle } from '../dist/ReactGraph';
|
||||
|
||||
const SpecialNode = ({ data, styles }) => (
|
||||
<div
|
||||
style={{ background: '#FFCC00', padding: 10, borderRadius: 2, ...styles }}
|
||||
>
|
||||
<TargetHandle id="a" style={{ left: 10, background: '#999' }} />
|
||||
<TargetHandle id="b" style={{ left: 30, background: '#999' }} />
|
||||
<Handle type="target" position="top" id="a" style={{ left: 10, background: '#999' }} />
|
||||
<Handle type="target" position="top" id="b" style={{ left: 30, background: '#999' }} />
|
||||
<div>I am <strong>special</strong>!<br />{data.label}</div>
|
||||
<select onChange={(e) => data.onChange(e.target.value, data)}>
|
||||
<option value="1">1</option>
|
||||
<option value="2">2</option>
|
||||
<option value="3">3</option>
|
||||
</select>
|
||||
<SourceHandle style={{ left: 10, background: '#999' }} />
|
||||
<Handle type="source" position="bottom" style={{ left: 10, background: '#999' }} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -23,10 +23,10 @@ const InputNode = ({ data, styles }) => (
|
||||
<div
|
||||
style={{ background: '#FFCC00', padding: 10, borderRadius: 2, ...styles }}
|
||||
>
|
||||
<TargetHandle style={{ left: 10, background: '#999' }} />
|
||||
<Handle type="target" position="left" style={{ background: '#999' }} />
|
||||
<div>{data.input}</div>
|
||||
<input onChange={(e) => data.onChange(e.target.value, data)} />
|
||||
<SourceHandle style={{ left: 10, background: '#999' }} />
|
||||
<Handle type="source" position="right" style={{ background: '#999' }} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -72,9 +72,11 @@ class App extends PureComponent {
|
||||
{ id: '5', type: 'default', data: { label: '5 Another node'}, position: { x: 400, y: 300 } },
|
||||
{ id: '6', type: 'special', data: { onChange, label: '6 no option selected' }, position: { x: 425, y: 375 } },
|
||||
{ id: '7', type: 'output', data: { label: '7 output' }, position: { x: 250, y: 500 } },
|
||||
{ id: '8', type: 'text', data: { onChange: onChangeInput, input: 'write something' }, position: { x: 300, y: 100 } },
|
||||
{ id: '8', type: 'text', data: { onChange: onChangeInput, input: 'write something' }, position: { x: 350, y: 100 } },
|
||||
{ id: '9', type: 'text', data: { label: 'right' }, position: { x: 600, y: 100 } },
|
||||
{ source: '1', target: '2', animated: true },
|
||||
{ source: '1', target: '8', animated: true },
|
||||
{ source: '8', target: '9', animated: true },
|
||||
{ source: '2', target: '3' },
|
||||
{ source: '3', target: '4', type: 'step' },
|
||||
{ source: '3', target: '5' },
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
export default memo((props) => {
|
||||
const {
|
||||
sourceX, sourceY, targetX, targetY, style = {}
|
||||
} = props;
|
||||
|
||||
export default memo(({
|
||||
sourceX, sourceY, targetX, targetY,
|
||||
sourcePosition, targetPosition, style = {}
|
||||
}) => {
|
||||
const yOffset = Math.abs(targetY - sourceY) / 2;
|
||||
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
|
||||
const dAttr = `M${sourceX},${sourceY} C${sourceX},${centerY} ${targetX},${centerY} ${targetX},${targetY}`;
|
||||
|
||||
let dAttr = `M${sourceX},${sourceY} C${sourceX},${centerY} ${targetX},${centerY} ${targetX},${targetY}`;
|
||||
|
||||
if (['left', 'right'].includes(sourcePosition) && ['left', 'right'].includes(targetPosition)) {
|
||||
const xOffset = Math.abs(targetX - sourceX) / 2;
|
||||
const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset;
|
||||
|
||||
dAttr = `M${sourceX},${sourceY} C${centerX},${sourceY} ${centerX},${targetY} ${targetX},${targetY}`;
|
||||
} else if (['left', 'right'].includes(sourcePosition) || ['left', 'right'].includes(targetPosition)) {
|
||||
dAttr = `M${sourceX},${sourceY} C${sourceX},${targetY} ${sourceX},${targetY} ${targetX},${targetY}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<path
|
||||
|
||||
@@ -4,52 +4,74 @@ import { useStoreState } from 'easy-peasy';
|
||||
import ConnectionLine from '../ConnectionLine';
|
||||
import { isEdge } from '../graph-utils';
|
||||
|
||||
function getEdgePositions({ sourceNode, targetNode, sourceHandleId, targetHandleId }) {
|
||||
const hasSourceHandle = !!sourceNode.__rg.handleBounds.source;
|
||||
const hasTargetHandle = !!targetNode.__rg.handleBounds.target;
|
||||
|
||||
let sourceHandle = null;
|
||||
let targetHandle = null;
|
||||
|
||||
if (hasSourceHandle) {
|
||||
// there is no sourceHandleId when there are no multiple handles/ handles with ids
|
||||
// so we just pick the first one
|
||||
if (sourceNode.__rg.handleBounds.source.length === 1 || !sourceHandleId) {
|
||||
sourceHandle = sourceNode.__rg.handleBounds.source[0];
|
||||
} else if (sourceHandleId) {
|
||||
sourceHandle = sourceNode.__rg.handleBounds.source.find(d => d.id === sourceHandleId);
|
||||
function getHandlePosition(position, node, handle = null) {
|
||||
if (!handle) {
|
||||
switch (position) {
|
||||
case 'top': return {
|
||||
x: node.__rg.width / 2,
|
||||
y: 0
|
||||
};
|
||||
case 'right': return {
|
||||
x: node.__rg.width,
|
||||
y: node.__rg.height / 2
|
||||
};
|
||||
case 'bottom': return {
|
||||
x: node.__rg.width / 2,
|
||||
y: node.__rg.height
|
||||
};
|
||||
case 'left': return {
|
||||
x: 0,
|
||||
y: node.__rg.height / 2
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (hasTargetHandle) {
|
||||
if (targetNode.__rg.handleBounds.target.length === 1 || !targetHandleId) {
|
||||
targetHandle = targetNode.__rg.handleBounds.target[0];
|
||||
} else if (targetHandleId) {
|
||||
targetHandle = targetNode.__rg.handleBounds.target.find(d => d.id === targetHandleId);
|
||||
}
|
||||
switch (position) {
|
||||
case 'top': return {
|
||||
x: handle.x + (handle.width / 2),
|
||||
y: handle.y
|
||||
};
|
||||
case 'right': return {
|
||||
x: handle.x + handle.width,
|
||||
y: handle.y + (handle.height / 2)
|
||||
};
|
||||
case 'bottom': return {
|
||||
x: handle.x + (handle.width / 2),
|
||||
y: handle.y + handle.height
|
||||
};
|
||||
case 'left': return {
|
||||
x: handle.x,
|
||||
y: handle.y + (handle.height / 2)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getHandle(bounds, handleId) {
|
||||
let handle = null;
|
||||
|
||||
if (!bounds) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sourceHandleX = hasSourceHandle ?
|
||||
sourceHandle.x + (sourceHandle.width / 2) :
|
||||
sourceNode.__rg.width / 2;
|
||||
// there is no handleId when there are no multiple handles/ handles with ids
|
||||
// so we just pick the first one
|
||||
if (bounds.length === 1 || !handleId) {
|
||||
handle = bounds[0];
|
||||
} else if (handleId) {
|
||||
handle = bounds.find(d => d.id === handleId);
|
||||
}
|
||||
|
||||
const sourceHandleY = hasSourceHandle ?
|
||||
sourceHandle.y + (sourceHandle.height / 2) :
|
||||
sourceNode.__rg.height;
|
||||
return handle;
|
||||
}
|
||||
|
||||
const sourceX = sourceNode.__rg.position.x + sourceHandleX;
|
||||
const sourceY = sourceNode.__rg.position.y + sourceHandleY;
|
||||
function getEdgePositions({ sourceNode, sourceHandle, sourcePosition, targetNode, targetHandle, targetPosition }) {
|
||||
const sourceHandlePos = getHandlePosition(sourcePosition, sourceNode, sourceHandle)
|
||||
const sourceX = sourceNode.__rg.position.x + sourceHandlePos.x;
|
||||
const sourceY = sourceNode.__rg.position.y + sourceHandlePos.y;
|
||||
|
||||
const targetHandleX = hasTargetHandle ?
|
||||
targetHandle.x + (targetHandle.width / 2) :
|
||||
targetNode.__rg.width / 2;
|
||||
|
||||
const targetHandleY = hasTargetHandle ?
|
||||
targetHandle.y + (targetHandle.height / 2) :
|
||||
0;
|
||||
|
||||
const targetX = targetNode.__rg.position.x + targetHandleX;
|
||||
const targetY = targetNode.__rg.position.y + targetHandleY;
|
||||
const targetHandlePos = getHandlePosition(targetPosition, targetNode, targetHandle);
|
||||
const targetX = targetNode.__rg.position.x + targetHandlePos.x;
|
||||
const targetY = targetNode.__rg.position.y + targetHandlePos.y;
|
||||
|
||||
return {
|
||||
sourceX, sourceY, targetX, targetY
|
||||
@@ -80,7 +102,15 @@ function renderEdge(e, props, state) {
|
||||
}
|
||||
|
||||
const EdgeComponent = props.edgeTypes[edgeType] || props.edgeTypes.default;
|
||||
const { sourceX, sourceY, targetX, targetY } = getEdgePositions({ sourceNode, targetNode, sourceHandleId, targetHandleId });
|
||||
const sourceHandle = getHandle(sourceNode.__rg.handleBounds.source, sourceHandleId);
|
||||
const targetHandle = getHandle(targetNode.__rg.handleBounds.target, targetHandleId);
|
||||
const sourcePosition = sourceHandle ? sourceHandle.position : 'bottom';
|
||||
const targetPosition = targetHandle ? targetHandle.position : 'top';
|
||||
|
||||
const { sourceX, sourceY, targetX, targetY } = getEdgePositions({
|
||||
sourceNode, sourceHandle, sourcePosition,
|
||||
targetNode, targetHandle, targetPosition
|
||||
});
|
||||
const selected = state.selectedElements
|
||||
.filter(isEdge)
|
||||
.find(elm => elm.source === sourceId && elm.target === targetId);
|
||||
@@ -102,6 +132,8 @@ function renderEdge(e, props, state) {
|
||||
sourceY={sourceY}
|
||||
targetX={targetX}
|
||||
targetY={targetY}
|
||||
sourcePosition={sourcePosition}
|
||||
targetPosition={targetPosition}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,23 +40,26 @@ function onMouseDown(evt, { nodeId, setSourceId, setPosition, onConnect, isTarg
|
||||
}
|
||||
|
||||
const BaseHandle = memo(({
|
||||
source, target, nodeId, onConnect,
|
||||
type, nodeId, onConnect, position,
|
||||
setSourceId, setPosition, className,
|
||||
id = false, ...rest
|
||||
}) => {
|
||||
const isTarget = type === 'target';
|
||||
const handleClasses = cx(
|
||||
'react-graph__handle',
|
||||
className,
|
||||
{ source, target }
|
||||
position,
|
||||
{ source: !isTarget, target: isTarget }
|
||||
);
|
||||
|
||||
const handleId = id ? `${nodeId}__${id}` : nodeId;
|
||||
const nodeIdWithHandleId = id ? `${nodeId}__${id}` : nodeId;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-nodeid={handleId}
|
||||
data-nodeid={nodeIdWithHandleId}
|
||||
data-handlepos={position}
|
||||
className={handleClasses}
|
||||
onMouseDown={evt => onMouseDown(evt, { nodeId: handleId, setSourceId, setPosition, onConnect, isTarget: target })}
|
||||
onMouseDown={evt => onMouseDown(evt, { nodeId: nodeIdWithHandleId, setSourceId, setPosition, onConnect, isTarget })}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
39
src/NodeRenderer/HandleTypes/Handle.js
Normal file
39
src/NodeRenderer/HandleTypes/Handle.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import React, { memo, useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useStoreActions, useStoreState } from 'easy-peasy';
|
||||
|
||||
import BaseHandle from './BaseHandle';
|
||||
import NodeIdContext from '../NodeIdContext'
|
||||
|
||||
const Handle = memo((props) => {
|
||||
const nodeId = useContext(NodeIdContext);
|
||||
const { setPosition, setSourceId } = useStoreActions(a => ({
|
||||
setPosition: a.setConnectionPosition,
|
||||
setSourceId: a.setConnectionSourceId
|
||||
}));
|
||||
const onConnect = useStoreState(s => s.onConnect);
|
||||
|
||||
return (
|
||||
<BaseHandle
|
||||
nodeId={nodeId}
|
||||
setPosition={setPosition}
|
||||
setSourceId={setSourceId}
|
||||
onConnect={onConnect}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
Handle.displayName = 'Handle';
|
||||
|
||||
Handle.propTypes = {
|
||||
type: PropTypes.oneOf(['source', 'target']),
|
||||
position: PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
|
||||
};
|
||||
|
||||
Handle.defaultProps = {
|
||||
type: 'source',
|
||||
position: 'top'
|
||||
};
|
||||
|
||||
export default Handle;
|
||||
@@ -14,7 +14,8 @@ const SourceHandle = memo((props) => {
|
||||
|
||||
return (
|
||||
<BaseHandle
|
||||
source
|
||||
type="source"
|
||||
position="bottom"
|
||||
nodeId={nodeId}
|
||||
setPosition={setPosition}
|
||||
setSourceId={setSourceId}
|
||||
|
||||
@@ -14,7 +14,8 @@ const TargetHandle = memo((props) => {
|
||||
|
||||
return (
|
||||
<BaseHandle
|
||||
target
|
||||
type="target"
|
||||
position="top"
|
||||
nodeId={nodeId}
|
||||
setPosition={setPosition}
|
||||
setSourceId={setSourceId}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import TargetHandle from '../HandleTypes/TargetHandle';
|
||||
import SourceHandle from '../HandleTypes/SourceHandle';
|
||||
import Handle from '../HandleTypes/Handle';
|
||||
|
||||
const nodeStyles = {
|
||||
background: '#ff6060',
|
||||
@@ -12,8 +11,8 @@ const nodeStyles = {
|
||||
|
||||
export default ({ data, style }) => (
|
||||
<div style={{ ...nodeStyles, ...style }}>
|
||||
<TargetHandle />
|
||||
<Handle type="target" position="top" />
|
||||
{data.label}
|
||||
<SourceHandle />
|
||||
<Handle type="source" position="bottom" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import SourceHandle from '../HandleTypes/SourceHandle';
|
||||
import Handle from '../HandleTypes/Handle';
|
||||
|
||||
const nodeStyles = {
|
||||
background: '#9999ff',
|
||||
@@ -15,6 +15,6 @@ export default ({ data, style }) => (
|
||||
className="react-graph__node-inner"
|
||||
>
|
||||
{data.label}
|
||||
<SourceHandle />
|
||||
<Handle type="source" position="bottom" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import TargetHandle from '../HandleTypes/TargetHandle';
|
||||
import Handle from '../HandleTypes/TargetHandle';
|
||||
|
||||
const nodeStyles = {
|
||||
background: '#55dd99',
|
||||
@@ -11,7 +11,7 @@ const nodeStyles = {
|
||||
|
||||
export default ({ data, style }) => (
|
||||
<div style={{ ...nodeStyles, ...style }}>
|
||||
<TargetHandle />
|
||||
<Handle type="target" position="top" />
|
||||
{data.label}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -23,6 +23,7 @@ const getHandleBounds = (sel, nodeElement, parentBounds, k) => {
|
||||
const bounds = handle.getBoundingClientRect();
|
||||
const dimensions = getDimensions(handle);
|
||||
const nodeIdAttr = handle.getAttribute('data-nodeid');
|
||||
const handlePosition = handle.getAttribute('data-handlepos');
|
||||
const nodeIdSplitted = nodeIdAttr.split('__');
|
||||
|
||||
let handleId = null;
|
||||
@@ -33,6 +34,7 @@ const getHandleBounds = (sel, nodeElement, parentBounds, k) => {
|
||||
|
||||
return {
|
||||
id: handleId,
|
||||
position: handlePosition,
|
||||
x: (bounds.x - parentBounds.x) * (1 / k),
|
||||
y: (bounds.y - parentBounds.y) * (1 / k),
|
||||
...dimensions
|
||||
|
||||
@@ -2,8 +2,7 @@ import ReactGraph from './ReactGraph';
|
||||
|
||||
export default ReactGraph;
|
||||
|
||||
export { default as SourceHandle } from './NodeRenderer/HandleTypes/SourceHandle';
|
||||
export { default as TargetHandle } from './NodeRenderer/HandleTypes/TargetHandle';
|
||||
export { default as Handle } from './NodeRenderer/HandleTypes/Handle';
|
||||
export { default as MiniMap } from './Plugins/MiniMap';
|
||||
|
||||
export {
|
||||
|
||||
@@ -104,21 +104,33 @@
|
||||
width: 10px;
|
||||
height: 8px;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
cursor: crosshair;
|
||||
|
||||
&.source {
|
||||
&.bottom {
|
||||
top: auto;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
transform: translate(-50%, 0);
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
&.target {
|
||||
&.top {
|
||||
left: 50%;
|
||||
top: 0;
|
||||
cursor: crosshair;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
&.left {
|
||||
top: 50%;
|
||||
left: 0;
|
||||
transform: translate(0, -50%);
|
||||
|
||||
}
|
||||
|
||||
&.right {
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
.react-graph__nodesselection {
|
||||
|
||||
Reference in New Issue
Block a user