develop (#43)
* fix(ts): use strict mode strictNullChecks etc * chore: Use extended React.HTMLAttributes<> (#41) * refactor(code-format): add prettier closes #42 * feat(renderer): add snap to grid option closes #20 * chore(dependabot): use develop as target branch
This commit is contained in:
@@ -1,30 +1,40 @@
|
||||
import React, {memo} from 'react';
|
||||
import React, { memo, HTMLAttributes, CSSProperties } from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { useStoreState } from '../../store/hooks';
|
||||
import { GridType } from '../../types';
|
||||
|
||||
interface GridProps {
|
||||
interface GridProps extends HTMLAttributes<SVGElement> {
|
||||
backgroundType?: GridType;
|
||||
gap?: number;
|
||||
color?: string;
|
||||
size?: number;
|
||||
style?: React.CSSProperties;
|
||||
className?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
const baseStyles: React.CSSProperties = {
|
||||
const baseStyles: CSSProperties = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
};
|
||||
|
||||
const createGridLines = (width: number, height: number, xOffset: number, yOffset: number, gap: number): string => {
|
||||
const createGridLines = (
|
||||
width: number,
|
||||
height: number,
|
||||
xOffset: number,
|
||||
yOffset: number,
|
||||
gap: number
|
||||
): string => {
|
||||
const lineCountX = Math.ceil(width / gap) + 1;
|
||||
const lineCountY = Math.ceil(height / gap) + 1;
|
||||
|
||||
const xValues = Array.from({length: lineCountX}, (_, i) => `M${i * gap + xOffset} 0 V${height}`);
|
||||
const yValues = Array.from({length: lineCountY}, (_, i) => `M0 ${i * gap + yOffset} H${width}`);
|
||||
const xValues = Array.from(
|
||||
{ length: lineCountX },
|
||||
(_, i) => `M${i * gap + xOffset} 0 V${height}`
|
||||
);
|
||||
const yValues = Array.from(
|
||||
{ length: lineCountY },
|
||||
(_, i) => `M0 ${i * gap + yOffset} H${width}`
|
||||
);
|
||||
|
||||
return [...xValues, ...yValues].join(' ');
|
||||
};
|
||||
@@ -40,11 +50,12 @@ const createGridDots = (
|
||||
const lineCountX = Math.ceil(width / gap) + 1;
|
||||
const lineCountY = Math.ceil(height / gap) + 1;
|
||||
|
||||
const values = Array.from({length: lineCountX}, (_, col) => {
|
||||
const values = Array.from({ length: lineCountX }, (_, col) => {
|
||||
const x = col * gap + xOffset;
|
||||
return Array.from({length: lineCountY}, (_, row) => {
|
||||
return Array.from({ length: lineCountY }, (_, row) => {
|
||||
const y = row * gap + yOffset;
|
||||
return `M${x} ${y - size} l${size} ${size} l${-size} ${size} l${-size} ${-size}z`;
|
||||
return `M${x} ${y -
|
||||
size} l${size} ${size} l${-size} ${size} l${-size} ${-size}z`;
|
||||
}).join(' ');
|
||||
});
|
||||
|
||||
@@ -52,7 +63,14 @@ const createGridDots = (
|
||||
};
|
||||
|
||||
const Grid = memo(
|
||||
({gap = 24, color = '#aaa', size = 0.5, style = {}, className = null, backgroundType = GridType.Dots}: GridProps) => {
|
||||
({
|
||||
gap = 24,
|
||||
color = '#aaa',
|
||||
size = 0.5,
|
||||
style = {},
|
||||
className = '',
|
||||
backgroundType = GridType.Dots,
|
||||
}: GridProps) => {
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
@@ -73,7 +91,12 @@ const Grid = memo(
|
||||
const stroke = isLines ? color : 'none';
|
||||
|
||||
return (
|
||||
<svg width={width} height={height} style={{...baseStyles, ...style}} className={gridClasses}>
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
style={{ ...baseStyles, ...style }}
|
||||
className={gridClasses}
|
||||
>
|
||||
<path fill={fill} stroke={stroke} strokeWidth={size} d={path} />
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -7,16 +7,22 @@ interface ConnectionLineProps {
|
||||
connectionSourceId: ElementId;
|
||||
connectionPositionX: number;
|
||||
connectionPositionY: number;
|
||||
connectionLineType?: string | null;
|
||||
connectionLineType?: string | null;
|
||||
nodes: Node[];
|
||||
transform: Transform;
|
||||
connectionLineStyle?: SVGAttributes<{}>;
|
||||
className?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default ({
|
||||
connectionSourceId, connectionLineStyle = {}, connectionPositionX, connectionPositionY,
|
||||
connectionLineType, nodes = [], className, transform
|
||||
connectionSourceId,
|
||||
connectionLineStyle = {},
|
||||
connectionPositionX,
|
||||
connectionPositionY,
|
||||
connectionLineType,
|
||||
nodes = [],
|
||||
className,
|
||||
transform,
|
||||
}: ConnectionLineProps) => {
|
||||
const [sourceNode, setSourceNode] = useState<Node | null>(null);
|
||||
const hasHandleId = connectionSourceId.includes('__');
|
||||
@@ -25,7 +31,7 @@ export default ({
|
||||
const handleId = hasHandleId ? sourceIdSplitted[1] : null;
|
||||
|
||||
useEffect(() => {
|
||||
const nextSourceNode = nodes.find(n => n.id === nodeId) || null;
|
||||
const nextSourceNode = nodes.find(n => n.id === nodeId) || null;
|
||||
setSourceNode(nextSourceNode);
|
||||
}, []);
|
||||
|
||||
@@ -35,11 +41,17 @@ export default ({
|
||||
|
||||
const edgeClasses: string = cx('react-flow__edge', 'connection', className);
|
||||
|
||||
const sourceHandle = handleId ?
|
||||
sourceNode.__rg.handleBounds.source.find((d: HandleElement) => d.id === handleId) :
|
||||
sourceNode.__rg.handleBounds.source[0];
|
||||
const sourceHandleX = sourceHandle ? sourceHandle.x + (sourceHandle.width / 2) : sourceNode.__rg.width / 2;
|
||||
const sourceHandleY = sourceHandle ? sourceHandle.y + (sourceHandle.height / 2) : sourceNode.__rg.height;
|
||||
const sourceHandle = handleId
|
||||
? sourceNode.__rg.handleBounds.source.find(
|
||||
(d: HandleElement) => d.id === handleId
|
||||
)
|
||||
: sourceNode.__rg.handleBounds.source[0];
|
||||
const sourceHandleX = sourceHandle
|
||||
? sourceHandle.x + sourceHandle.width / 2
|
||||
: sourceNode.__rg.width / 2;
|
||||
const sourceHandleY = sourceHandle
|
||||
? sourceHandle.y + sourceHandle.height / 2
|
||||
: sourceNode.__rg.height;
|
||||
const sourceX = sourceNode.__rg.position.x + sourceHandleX;
|
||||
const sourceY = sourceNode.__rg.position.y + sourceHandleY;
|
||||
|
||||
@@ -58,10 +70,7 @@ export default ({
|
||||
|
||||
return (
|
||||
<g className={edgeClasses}>
|
||||
<path
|
||||
d={dAttr}
|
||||
{...connectionLineStyle}
|
||||
/>
|
||||
<path d={dAttr} {...connectionLineStyle} />
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,28 +2,36 @@ import React, { memo } from 'react';
|
||||
|
||||
import { EdgeBezierProps } from '../../types';
|
||||
|
||||
export default memo(({
|
||||
sourceX, sourceY, targetX, targetY,
|
||||
sourcePosition = 'bottom', targetPosition = 'top', style = {}
|
||||
}: EdgeBezierProps) => {
|
||||
const yOffset = Math.abs(targetY - sourceY) / 2;
|
||||
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
|
||||
export default memo(
|
||||
({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition = 'bottom',
|
||||
targetPosition = 'top',
|
||||
style = {},
|
||||
}: EdgeBezierProps) => {
|
||||
const yOffset = Math.abs(targetY - sourceY) / 2;
|
||||
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
|
||||
|
||||
let 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;
|
||||
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}`;
|
||||
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 {...style} d={dAttr} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<path
|
||||
{...style}
|
||||
d={dAttr}
|
||||
/>
|
||||
);
|
||||
});
|
||||
);
|
||||
|
||||
@@ -2,16 +2,16 @@ import React, { memo } from 'react';
|
||||
|
||||
import { EdgeProps } from '../../types';
|
||||
|
||||
export default memo(({
|
||||
sourceX, sourceY, targetX, targetY, style = {}
|
||||
} : EdgeProps) => {
|
||||
const yOffset = Math.abs(targetY - sourceY) / 2;
|
||||
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
|
||||
export default memo(
|
||||
({ sourceX, sourceY, targetX, targetY, style = {} }: EdgeProps) => {
|
||||
const yOffset = Math.abs(targetY - sourceY) / 2;
|
||||
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
|
||||
|
||||
return (
|
||||
<path
|
||||
{...style}
|
||||
d={`M ${sourceX},${sourceY}L ${sourceX},${centerY}L ${targetX},${centerY}L ${targetX},${targetY}`}
|
||||
/>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<path
|
||||
{...style}
|
||||
d={`M ${sourceX},${sourceY}L ${sourceX},${centerY}L ${targetX},${centerY}L ${targetX},${targetY}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -2,13 +2,10 @@ import React, { memo } from 'react';
|
||||
|
||||
import { EdgeProps } from '../../types';
|
||||
|
||||
export default memo(({
|
||||
sourceX, sourceY, targetX, targetY, style = {}
|
||||
}: EdgeProps) => {
|
||||
return (
|
||||
<path
|
||||
{...style}
|
||||
d={`M ${sourceX},${sourceY}L ${targetX},${targetY}`}
|
||||
/>
|
||||
);
|
||||
});
|
||||
export default memo(
|
||||
({ sourceX, sourceY, targetX, targetY, style = {} }: EdgeProps) => {
|
||||
return (
|
||||
<path {...style} d={`M ${sourceX},${sourceY}L ${targetX},${targetY}`} />
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -3,42 +3,56 @@ import cx from 'classnames';
|
||||
|
||||
import { isInputDOMNode } from '../../utils';
|
||||
import store from '../../store';
|
||||
import { EdgeWrapperProps } from '../../types';
|
||||
import { ElementId, Edge, EdgeCompProps } from '../../types';
|
||||
|
||||
export default (EdgeComponent: ComponentType<EdgeWrapperProps>) => {
|
||||
const EdgeWrapper = memo(({
|
||||
id, source, target, type,
|
||||
animated, selected, onClick,
|
||||
...rest
|
||||
}: EdgeWrapperProps) => {
|
||||
const edgeClasses = cx('react-flow__edge', { selected, animated });
|
||||
const onEdgeClick = (evt: MouseEvent) => {
|
||||
if (isInputDOMNode(evt)) {
|
||||
return false;
|
||||
}
|
||||
interface EdgeWrapperProps {
|
||||
id: ElementId;
|
||||
source: ElementId;
|
||||
target: ElementId;
|
||||
type: any;
|
||||
onClick: (edge: Edge) => void;
|
||||
animated: boolean;
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
store.dispatch.setSelectedElements({ id, source, target });
|
||||
onClick({ id, source, target, type });
|
||||
};
|
||||
export default (EdgeComponent: ComponentType<EdgeCompProps>) => {
|
||||
const EdgeWrapper = memo(
|
||||
({
|
||||
id,
|
||||
source,
|
||||
target,
|
||||
type,
|
||||
animated,
|
||||
selected,
|
||||
onClick,
|
||||
...rest
|
||||
}: EdgeWrapperProps) => {
|
||||
const edgeClasses = cx('react-flow__edge', { selected, animated });
|
||||
const onEdgeClick = (evt: MouseEvent): void => {
|
||||
if (isInputDOMNode(evt)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<g
|
||||
className={edgeClasses}
|
||||
onClick={onEdgeClick}
|
||||
>
|
||||
<EdgeComponent
|
||||
id={id}
|
||||
source={source}
|
||||
target={target}
|
||||
type={type}
|
||||
animated={animated}
|
||||
selected={selected}
|
||||
onClick={onClick}
|
||||
{...rest}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
});
|
||||
store.dispatch.setSelectedElements({ id, source, target });
|
||||
onClick({ id, source, target, type });
|
||||
};
|
||||
|
||||
return (
|
||||
<g className={edgeClasses} onClick={onEdgeClick}>
|
||||
<EdgeComponent
|
||||
id={id}
|
||||
source={source}
|
||||
target={target}
|
||||
type={type}
|
||||
animated={animated}
|
||||
selected={selected}
|
||||
onClick={onClick}
|
||||
{...rest}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
EdgeWrapper.displayName = 'EdgeWrapper';
|
||||
|
||||
|
||||
@@ -1,41 +1,54 @@
|
||||
import React, { memo, MouseEvent as ReactMouseEvent } from 'react';
|
||||
import cx from 'classnames';
|
||||
|
||||
import { HandleType, ElementId, Position, XYPosition, OnConnectFunc, Connection } from '../../types';
|
||||
import {
|
||||
HandleType,
|
||||
ElementId,
|
||||
Position,
|
||||
XYPosition,
|
||||
OnConnectFunc,
|
||||
Connection,
|
||||
} from '../../types';
|
||||
|
||||
type ValidConnectionFunc = (connection: Connection) => boolean;
|
||||
type SetSourceIdFunc = (nodeId: ElementId | null) => void;
|
||||
|
||||
interface BaseHandleProps {
|
||||
type: HandleType;
|
||||
nodeId: ElementId;
|
||||
onConnect: OnConnectFunc;
|
||||
position: Position;
|
||||
setSourceId: (nodeId: ElementId) => void;
|
||||
setSourceId: SetSourceIdFunc;
|
||||
setPosition: (pos: XYPosition) => void;
|
||||
isValidConnection: ValidConnectionFunc;
|
||||
id?: ElementId | boolean;
|
||||
id?: ElementId | boolean;
|
||||
className?: string;
|
||||
};
|
||||
}
|
||||
|
||||
type Result = {
|
||||
elementBelow: Element;
|
||||
elementBelow: Element | null;
|
||||
isValid: boolean;
|
||||
connection: Connection;
|
||||
isHoveringHandle: boolean;
|
||||
};
|
||||
|
||||
function onMouseDown(
|
||||
evt: ReactMouseEvent, nodeId: ElementId, setSourceId: (nodeId: ElementId) => void, setPosition: (pos: XYPosition) => any,
|
||||
onConnect: OnConnectFunc, isTarget: boolean, isValidConnection: ValidConnectionFunc
|
||||
evt: ReactMouseEvent,
|
||||
nodeId: ElementId,
|
||||
setSourceId: SetSourceIdFunc,
|
||||
setPosition: (pos: XYPosition) => any,
|
||||
onConnect: OnConnectFunc,
|
||||
isTarget: boolean,
|
||||
isValidConnection: ValidConnectionFunc
|
||||
): void {
|
||||
const reactFlowNode = document.querySelector('.react-flow');
|
||||
|
||||
if (!reactFlowNode) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
const containerBounds = reactFlowNode.getBoundingClientRect();
|
||||
let recentHoveredHandle: Element = null;
|
||||
let recentHoveredHandle: Element;
|
||||
|
||||
setPosition({
|
||||
x: evt.clientX - containerBounds.left,
|
||||
@@ -43,9 +56,9 @@ function onMouseDown(
|
||||
});
|
||||
setSourceId(nodeId);
|
||||
|
||||
function resetRecentHandle() {
|
||||
function resetRecentHandle(): void {
|
||||
if (!recentHoveredHandle) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
recentHoveredHandle.classList.remove('valid');
|
||||
@@ -55,14 +68,19 @@ function onMouseDown(
|
||||
// checks if element below mouse is a handle and returns connection in form of an object { source: 123, target: 312 }
|
||||
function checkElementBelowIsValid(evt: MouseEvent) {
|
||||
const elementBelow = document.elementFromPoint(evt.clientX, evt.clientY);
|
||||
|
||||
const result: Result = {
|
||||
elementBelow,
|
||||
isValid: false,
|
||||
connection: { source: null, target: null },
|
||||
isHoveringHandle: false
|
||||
isHoveringHandle: false,
|
||||
};
|
||||
|
||||
if (elementBelow && (elementBelow.classList.contains('target') || elementBelow.classList.contains('source'))) {
|
||||
if (
|
||||
elementBelow &&
|
||||
(elementBelow.classList.contains('target') ||
|
||||
elementBelow.classList.contains('source'))
|
||||
) {
|
||||
let connection: Connection = { source: null, target: null };
|
||||
|
||||
if (isTarget) {
|
||||
@@ -89,7 +107,12 @@ function onMouseDown(
|
||||
y: evt.clientY - containerBounds.top,
|
||||
});
|
||||
|
||||
const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid(evt);
|
||||
const {
|
||||
connection,
|
||||
elementBelow,
|
||||
isValid,
|
||||
isHoveringHandle,
|
||||
} = checkElementBelowIsValid(evt);
|
||||
|
||||
if (!isHoveringHandle) {
|
||||
return resetRecentHandle();
|
||||
@@ -97,7 +120,7 @@ function onMouseDown(
|
||||
|
||||
const isOwnHandle = connection.source === connection.target;
|
||||
|
||||
if (!isOwnHandle) {
|
||||
if (!isOwnHandle && elementBelow) {
|
||||
recentHoveredHandle = elementBelow;
|
||||
elementBelow.classList.add('connecting');
|
||||
elementBelow.classList.toggle('valid', isValid);
|
||||
@@ -105,7 +128,7 @@ function onMouseDown(
|
||||
}
|
||||
|
||||
function onMouseUp(evt: MouseEvent) {
|
||||
const { connection, isValid } = checkElementBelowIsValid(evt);
|
||||
const { connection, isValid } = checkElementBelowIsValid(evt);
|
||||
|
||||
if (isValid) {
|
||||
onConnect(connection);
|
||||
@@ -119,37 +142,51 @@ function onMouseDown(
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp)
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
}
|
||||
|
||||
const BaseHandle = memo(({
|
||||
type, nodeId, onConnect, position,
|
||||
setSourceId, setPosition, className,
|
||||
id = false, isValidConnection, ...rest
|
||||
}: BaseHandleProps) => {
|
||||
const isTarget = type === 'target';
|
||||
const handleClasses = cx(
|
||||
'react-flow__handle',
|
||||
className,
|
||||
const BaseHandle = memo(
|
||||
({
|
||||
type,
|
||||
nodeId,
|
||||
onConnect,
|
||||
position,
|
||||
{ source: !isTarget, target: isTarget }
|
||||
);
|
||||
setSourceId,
|
||||
setPosition,
|
||||
className,
|
||||
id = false,
|
||||
isValidConnection,
|
||||
...rest
|
||||
}: BaseHandleProps) => {
|
||||
const isTarget = type === 'target';
|
||||
const handleClasses = cx('react-flow__handle', className, position, {
|
||||
source: !isTarget,
|
||||
target: isTarget,
|
||||
});
|
||||
|
||||
const nodeIdWithHandleId = id ? `${nodeId}__${id}` : nodeId;
|
||||
const nodeIdWithHandleId = id ? `${nodeId}__${id}` : nodeId;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-nodeid={nodeIdWithHandleId}
|
||||
data-handlepos={position}
|
||||
className={handleClasses}
|
||||
onMouseDown={evt => onMouseDown(evt,
|
||||
nodeIdWithHandleId, setSourceId, setPosition,
|
||||
onConnect, isTarget, isValidConnection
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div
|
||||
data-nodeid={nodeIdWithHandleId}
|
||||
data-handlepos={position}
|
||||
className={handleClasses}
|
||||
onMouseDown={evt =>
|
||||
onMouseDown(
|
||||
evt,
|
||||
nodeIdWithHandleId,
|
||||
setSourceId,
|
||||
setPosition,
|
||||
onConnect,
|
||||
isTarget,
|
||||
isValidConnection
|
||||
)
|
||||
}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
BaseHandle.displayName = 'BaseHandle';
|
||||
|
||||
|
||||
@@ -2,45 +2,56 @@ import React, { memo, useContext } from 'react';
|
||||
|
||||
import { useStoreActions, useStoreState } from '../../store/hooks';
|
||||
import BaseHandle from './BaseHandle';
|
||||
import NodeIdContext from '../../contexts/NodeIdContext'
|
||||
import NodeIdContext from '../../contexts/NodeIdContext';
|
||||
|
||||
import { HandleType, ElementId, Position, OnConnectParams, OnConnectFunc } from '../../types';
|
||||
import {
|
||||
HandleType,
|
||||
ElementId,
|
||||
Position,
|
||||
Connection,
|
||||
OnConnectFunc,
|
||||
} from '../../types';
|
||||
|
||||
interface HandleProps {
|
||||
type: HandleType,
|
||||
position: Position,
|
||||
onConnect?: OnConnectFunc,
|
||||
isValidConnection?: () => boolean
|
||||
};
|
||||
type: HandleType;
|
||||
position: Position;
|
||||
onConnect?: OnConnectFunc;
|
||||
isValidConnection?: () => boolean;
|
||||
}
|
||||
|
||||
const Handle = memo(({
|
||||
onConnect = _ => {}, type = 'source', position = 'top', isValidConnection = () => true,
|
||||
...rest
|
||||
}: HandleProps) => {
|
||||
const nodeId = useContext(NodeIdContext) as ElementId;
|
||||
const { setPosition, setSourceId } = useStoreActions(a => ({
|
||||
setPosition: a.setConnectionPosition,
|
||||
setSourceId: a.setConnectionSourceId
|
||||
}));
|
||||
const onConnectAction = useStoreState(s => s.onConnect);
|
||||
const onConnectExtended = (params: OnConnectParams) => {
|
||||
onConnectAction(params);
|
||||
onConnect(params);
|
||||
};
|
||||
const Handle = memo(
|
||||
({
|
||||
onConnect = _ => {},
|
||||
type = 'source',
|
||||
position = 'top',
|
||||
isValidConnection = () => true,
|
||||
...rest
|
||||
}: HandleProps) => {
|
||||
const nodeId = useContext(NodeIdContext) as ElementId;
|
||||
const { setPosition, setSourceId } = useStoreActions(a => ({
|
||||
setPosition: a.setConnectionPosition,
|
||||
setSourceId: a.setConnectionSourceId,
|
||||
}));
|
||||
const onConnectAction = useStoreState(s => s.onConnect);
|
||||
const onConnectExtended = (params: Connection) => {
|
||||
onConnectAction(params);
|
||||
onConnect(params);
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseHandle
|
||||
nodeId={nodeId}
|
||||
setPosition={setPosition}
|
||||
setSourceId={setSourceId}
|
||||
onConnect={onConnectExtended}
|
||||
type={type}
|
||||
position={position}
|
||||
isValidConnection={isValidConnection}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<BaseHandle
|
||||
nodeId={nodeId}
|
||||
setPosition={setPosition}
|
||||
setSourceId={setSourceId}
|
||||
onConnect={onConnectExtended}
|
||||
type={type}
|
||||
position={position}
|
||||
isValidConnection={isValidConnection}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Handle.displayName = 'Handle';
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ const nodeStyles: CSSProperties = {
|
||||
background: '#ff6060',
|
||||
padding: 10,
|
||||
borderRadius: 5,
|
||||
width: 150
|
||||
width: 150,
|
||||
};
|
||||
|
||||
export default ({ data, style }: NodeProps) => (
|
||||
|
||||
@@ -7,7 +7,7 @@ const nodeStyles: CSSProperties = {
|
||||
background: '#9999ff',
|
||||
padding: 10,
|
||||
borderRadius: 5,
|
||||
width: 150
|
||||
width: 150,
|
||||
};
|
||||
|
||||
export default ({ data, style }: NodeProps) => (
|
||||
|
||||
@@ -7,7 +7,7 @@ const nodeStyles: CSSProperties = {
|
||||
background: '#55dd99',
|
||||
padding: 10,
|
||||
borderRadius: 5,
|
||||
width: 150
|
||||
width: 150,
|
||||
};
|
||||
|
||||
export default ({ data, style }: NodeProps) => (
|
||||
|
||||
+208
-106
@@ -1,4 +1,11 @@
|
||||
import React, { useEffect, useRef, useState, memo, ComponentType } from 'react';
|
||||
import React, {
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
memo,
|
||||
ComponentType,
|
||||
CSSProperties,
|
||||
} from 'react';
|
||||
import { DraggableCore, DraggableEvent } from 'react-draggable';
|
||||
import cx from 'classnames';
|
||||
import { ResizeObserver } from 'resize-observer';
|
||||
@@ -6,7 +13,28 @@ import { ResizeObserver } from 'resize-observer';
|
||||
import { getDimensions, isInputDOMNode } from '../../utils';
|
||||
import { Provider } from '../../contexts/NodeIdContext';
|
||||
import store from '../../store';
|
||||
import { NodeComponentProps, Node, XYPosition, HandleElement, Position, Transform, ElementId } from '../../types';
|
||||
import {
|
||||
Node,
|
||||
XYPosition,
|
||||
HandleElement,
|
||||
Position,
|
||||
Transform,
|
||||
ElementId,
|
||||
NodeComponentProps,
|
||||
} from '../../types';
|
||||
|
||||
interface WrapNodeProps {
|
||||
id: ElementId;
|
||||
type: string;
|
||||
data: any;
|
||||
selected: boolean;
|
||||
transform: Transform;
|
||||
xPos: number;
|
||||
yPos: number;
|
||||
onClick: (node: Node) => void | undefined;
|
||||
onNodeDragStop: (node: Node) => void;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
const isHandle = (evt: MouseEvent | DraggableEvent) => {
|
||||
const target = evt.target as HTMLElement;
|
||||
@@ -14,165 +42,239 @@ const isHandle = (evt: MouseEvent | DraggableEvent) => {
|
||||
return (
|
||||
target.className &&
|
||||
target.className.includes &&
|
||||
(target.className.includes('source') || target.className.includes('target'))
|
||||
(target.className.includes('source') || target.className.includes('target'))
|
||||
);
|
||||
};
|
||||
|
||||
const getHandleBounds = (
|
||||
selector: string, nodeElement: HTMLDivElement, parentBounds: ClientRect | DOMRect, k: number
|
||||
): HandleElement => {
|
||||
selector: string,
|
||||
nodeElement: HTMLDivElement,
|
||||
parentBounds: ClientRect | DOMRect,
|
||||
k: number
|
||||
): HandleElement[] | null => {
|
||||
const handles = nodeElement.querySelectorAll(selector);
|
||||
|
||||
if (!handles || !handles.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [].map.call(handles, (handle: HTMLDivElement): HandleElement => {
|
||||
const bounds = handle.getBoundingClientRect();
|
||||
const dimensions = getDimensions(handle);
|
||||
const nodeIdAttr = handle.getAttribute('data-nodeid');
|
||||
const handlePosition = handle.getAttribute('data-handlepos') as unknown as Position;
|
||||
const nodeIdSplitted = nodeIdAttr.split('__');
|
||||
const handlesArray = Array.from(handles) as HTMLDivElement[];
|
||||
|
||||
let handleId = null;
|
||||
return handlesArray.map(
|
||||
(handle): HandleElement => {
|
||||
const bounds = handle.getBoundingClientRect();
|
||||
const dimensions = getDimensions(handle);
|
||||
const nodeIdAttr = handle.getAttribute('data-nodeid');
|
||||
const handlePosition = (handle.getAttribute(
|
||||
'data-handlepos'
|
||||
) as unknown) as Position;
|
||||
const nodeIdSplitted = nodeIdAttr ? nodeIdAttr.split('__') : null;
|
||||
|
||||
if (nodeIdSplitted) {
|
||||
handleId = (nodeIdSplitted.length ? nodeIdSplitted[1] : nodeIdSplitted) as string;
|
||||
let handleId = null;
|
||||
|
||||
if (nodeIdSplitted) {
|
||||
handleId = (nodeIdSplitted.length
|
||||
? nodeIdSplitted[1]
|
||||
: nodeIdSplitted) as string;
|
||||
}
|
||||
|
||||
return {
|
||||
id: handleId,
|
||||
position: handlePosition,
|
||||
x: (bounds.left - parentBounds.left) * (1 / k),
|
||||
y: (bounds.top - parentBounds.top) * (1 / k),
|
||||
...dimensions,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: handleId,
|
||||
position: handlePosition,
|
||||
x: (bounds.left - parentBounds.left) * (1 / k),
|
||||
y: (bounds.top - parentBounds.top) * (1 / k),
|
||||
...dimensions
|
||||
};
|
||||
});
|
||||
);
|
||||
};
|
||||
|
||||
const onStart = (
|
||||
evt: MouseEvent, onClick: (node: Node) => void, id: ElementId, type: string,
|
||||
data: any, setOffset: (pos: XYPosition) => void, transform: Transform, position: XYPosition
|
||||
): false | void => {
|
||||
evt: MouseEvent,
|
||||
onClick: (node: Node) => void,
|
||||
id: ElementId,
|
||||
type: string,
|
||||
data: any,
|
||||
setOffset: (pos: XYPosition) => void,
|
||||
transform: Transform,
|
||||
position: XYPosition
|
||||
): false | void => {
|
||||
if (isInputDOMNode(evt) || isHandle(evt)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const scaledClient: XYPosition = {
|
||||
x: evt.clientX * (1 / transform[2]),
|
||||
y: evt.clientY * (1 / transform[2])
|
||||
y: evt.clientY * (1 / transform[2]),
|
||||
};
|
||||
const offsetX = scaledClient.x - position.x - transform[0];
|
||||
const offsetY = scaledClient.y - position.y - transform[1];
|
||||
const node = { id, type, position, data };
|
||||
const node = { id, type, position, data };
|
||||
|
||||
store.dispatch.setSelectedElements({ id, type });
|
||||
store.dispatch.setSelectedElements({ id, type } as Node);
|
||||
setOffset({ x: offsetX, y: offsetY });
|
||||
onClick(node);
|
||||
};
|
||||
|
||||
const onDrag = (
|
||||
evt: MouseEvent, setDragging: (isDragging: boolean) => void, id: ElementId, offset: XYPosition,
|
||||
evt: MouseEvent,
|
||||
setDragging: (isDragging: boolean) => void,
|
||||
id: ElementId,
|
||||
offset: XYPosition,
|
||||
transform: Transform
|
||||
): void => {
|
||||
const scaledClient = {
|
||||
x: evt.clientX * (1 / transform[2]),
|
||||
y: evt.clientY * (1 / transform[2])
|
||||
y: evt.clientY * (1 / transform[2]),
|
||||
};
|
||||
|
||||
setDragging(true);
|
||||
store.dispatch.updateNodePos({ id, pos: {
|
||||
x: scaledClient.x - transform[0] - offset.x,
|
||||
y: scaledClient.y - transform[1] - offset.y
|
||||
}});
|
||||
store.dispatch.updateNodePos({
|
||||
id,
|
||||
pos: {
|
||||
x: scaledClient.x - transform[0] - offset.x,
|
||||
y: scaledClient.y - transform[1] - offset.y,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onStop = (
|
||||
onNodeDragStop: (params: Node) => void, isDragging: boolean, setDragging: (isDragging: boolean) => void, id: ElementId,
|
||||
type: string, position: XYPosition, data: any
|
||||
onNodeDragStop: (params: Node) => void,
|
||||
isDragging: boolean,
|
||||
setDragging: (isDragging: boolean) => void,
|
||||
id: ElementId,
|
||||
type: string,
|
||||
position: XYPosition,
|
||||
data: any
|
||||
): void => {
|
||||
if (isDragging) {
|
||||
setDragging(false);
|
||||
onNodeDragStop({
|
||||
id, type, position, data
|
||||
});
|
||||
id,
|
||||
type,
|
||||
position,
|
||||
data,
|
||||
} as Node);
|
||||
}
|
||||
};
|
||||
|
||||
export default (NodeComponent: ComponentType<NodeComponentProps>) => {
|
||||
const NodeWrapper = memo(({
|
||||
id, type, data, transform,
|
||||
xPos, yPos, selected, onClick,
|
||||
onNodeDragStop, style
|
||||
}: NodeComponentProps) => {
|
||||
const nodeElement = useRef<HTMLDivElement>(null);
|
||||
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
||||
const [isDragging, setDragging] = useState(false);
|
||||
const NodeWrapper = memo(
|
||||
({
|
||||
id,
|
||||
type,
|
||||
data,
|
||||
transform,
|
||||
xPos,
|
||||
yPos,
|
||||
selected,
|
||||
onClick,
|
||||
onNodeDragStop,
|
||||
style,
|
||||
}: WrapNodeProps) => {
|
||||
const nodeElement = useRef<HTMLDivElement>(null);
|
||||
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
||||
const [isDragging, setDragging] = useState(false);
|
||||
|
||||
const position = { x: xPos, y: yPos };
|
||||
const nodeClasses = cx('react-flow__node', { selected });
|
||||
const nodeStyle = { zIndex: selected ? 10 : 3, transform: `translate(${xPos}px,${yPos}px)` };
|
||||
|
||||
const updateNode = () => {
|
||||
if (!nodeElement.current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const storeState = store.getState()
|
||||
const bounds = nodeElement.current.getBoundingClientRect();
|
||||
const dimensions = getDimensions(nodeElement.current);
|
||||
const handleBounds = {
|
||||
source: getHandleBounds('.source', nodeElement.current, bounds, storeState.transform[2]),
|
||||
target: getHandleBounds('.target', nodeElement.current, bounds, storeState.transform[2])
|
||||
const position = { x: xPos, y: yPos };
|
||||
const nodeClasses = cx('react-flow__node', { selected });
|
||||
const nodeStyle = {
|
||||
zIndex: selected ? 10 : 3,
|
||||
transform: `translate(${xPos}px,${yPos}px)`,
|
||||
};
|
||||
store.dispatch.updateNodeData({ id, ...dimensions, handleBounds });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (nodeElement.current) {
|
||||
updateNode();
|
||||
|
||||
const resizeObserver = new ResizeObserver(entries => {
|
||||
for (let _ of entries) {
|
||||
updateNode();
|
||||
}
|
||||
});
|
||||
|
||||
resizeObserver.observe(nodeElement.current);
|
||||
|
||||
return () => {
|
||||
if (resizeObserver && nodeElement.current) {
|
||||
resizeObserver.unobserve(nodeElement.current);
|
||||
}
|
||||
const updateNode = (): void => {
|
||||
if (!nodeElement.current) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [nodeElement.current]);
|
||||
|
||||
return (
|
||||
<DraggableCore
|
||||
onStart={evt => onStart(evt as MouseEvent, onClick, id, type, data, setOffset, transform, position)}
|
||||
onDrag={evt => onDrag(evt as MouseEvent, setDragging, id, offset, transform)}
|
||||
onStop={() => onStop(onNodeDragStop, isDragging, setDragging, id, type, position, data)}
|
||||
scale={transform[2]}
|
||||
>
|
||||
<div
|
||||
className={nodeClasses}
|
||||
ref={nodeElement}
|
||||
style={nodeStyle}
|
||||
const storeState = store.getState();
|
||||
const bounds = nodeElement.current.getBoundingClientRect();
|
||||
const dimensions = getDimensions(nodeElement.current);
|
||||
const handleBounds = {
|
||||
source: getHandleBounds(
|
||||
'.source',
|
||||
nodeElement.current,
|
||||
bounds,
|
||||
storeState.transform[2]
|
||||
),
|
||||
target: getHandleBounds(
|
||||
'.target',
|
||||
nodeElement.current,
|
||||
bounds,
|
||||
storeState.transform[2]
|
||||
),
|
||||
};
|
||||
store.dispatch.updateNodeData({ id, ...dimensions, handleBounds });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (nodeElement.current) {
|
||||
updateNode();
|
||||
|
||||
const resizeObserver = new ResizeObserver(entries => {
|
||||
for (let _ of entries) {
|
||||
updateNode();
|
||||
}
|
||||
});
|
||||
|
||||
resizeObserver.observe(nodeElement.current);
|
||||
|
||||
return () => {
|
||||
if (resizeObserver && nodeElement.current) {
|
||||
resizeObserver.unobserve(nodeElement.current);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return;
|
||||
}, [nodeElement.current]);
|
||||
|
||||
return (
|
||||
<DraggableCore
|
||||
onStart={evt =>
|
||||
onStart(
|
||||
evt as MouseEvent,
|
||||
onClick,
|
||||
id,
|
||||
type,
|
||||
data,
|
||||
setOffset,
|
||||
transform,
|
||||
position
|
||||
)
|
||||
}
|
||||
onDrag={evt =>
|
||||
onDrag(evt as MouseEvent, setDragging, id, offset, transform)
|
||||
}
|
||||
onStop={() =>
|
||||
onStop(
|
||||
onNodeDragStop,
|
||||
isDragging,
|
||||
setDragging,
|
||||
id,
|
||||
type,
|
||||
position,
|
||||
data
|
||||
)
|
||||
}
|
||||
scale={transform[2]}
|
||||
>
|
||||
<Provider value={id}>
|
||||
<NodeComponent
|
||||
id={id}
|
||||
data={data}
|
||||
type={type}
|
||||
style={style}
|
||||
selected={selected}
|
||||
/>
|
||||
</Provider>
|
||||
</div>
|
||||
</DraggableCore>
|
||||
);
|
||||
});
|
||||
<div className={nodeClasses} ref={nodeElement} style={nodeStyle}>
|
||||
<Provider value={id}>
|
||||
<NodeComponent
|
||||
id={id}
|
||||
data={data}
|
||||
type={type}
|
||||
style={style}
|
||||
selected={selected}
|
||||
/>
|
||||
</Provider>
|
||||
</div>
|
||||
</DraggableCore>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
NodeWrapper.displayName = 'NodeWrapper';
|
||||
|
||||
|
||||
@@ -5,28 +5,30 @@ import { useStoreState, useStoreActions } from '../../store/hooks';
|
||||
import { isNode } from '../../utils/graph';
|
||||
import { Node, Elements, XYPosition } from '../../types';
|
||||
|
||||
function getStartPositions(elements: Elements) {
|
||||
return elements
|
||||
.filter(isNode)
|
||||
.reduce((res, node: Node) => {
|
||||
const startPosition = {
|
||||
x: node.__rg.position.x || node.position.x,
|
||||
y: node.__rg.position.y || node.position.x
|
||||
};
|
||||
type StartPositions = { [key: string]: XYPosition };
|
||||
|
||||
res[node.id] = startPosition;
|
||||
function getStartPositions(elements: Elements): StartPositions {
|
||||
const startPositions: StartPositions = {};
|
||||
|
||||
return res;
|
||||
}, {});
|
||||
return (elements.filter(isNode) as Node[]).reduce((res, node) => {
|
||||
const startPosition = {
|
||||
x: node.__rg.position.x || node.position.x,
|
||||
y: node.__rg.position.y || node.position.x,
|
||||
};
|
||||
|
||||
res[node.id] = startPosition;
|
||||
|
||||
return res;
|
||||
}, startPositions);
|
||||
}
|
||||
|
||||
export default memo(() => {
|
||||
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
||||
const [startPositions, setStartPositions] = useState({});
|
||||
const [offset, setOffset] = useState<XYPosition>({ x: 0, y: 0 });
|
||||
const [startPositions, setStartPositions] = useState<StartPositions>({});
|
||||
const state = useStoreState(s => ({
|
||||
transform: s.transform,
|
||||
selectedNodesBbox: s.selectedNodesBbox,
|
||||
selectedElements: s.selectedElements
|
||||
selectedElements: s.selectedElements,
|
||||
}));
|
||||
const updateNodePos = useStoreActions(a => a.updateNodePos);
|
||||
const [x, y, k] = state.transform;
|
||||
@@ -35,43 +37,55 @@ export default memo(() => {
|
||||
const onStart = (evt: MouseEvent) => {
|
||||
const scaledClient: XYPosition = {
|
||||
x: evt.clientX * (1 / k),
|
||||
y: evt.clientY * (1 / k)
|
||||
y: evt.clientY * (1 / k),
|
||||
};
|
||||
const offsetX: number = scaledClient.x - position.x - x;
|
||||
const offsetY: number = scaledClient.y - position.y - y;
|
||||
const startPositions = getStartPositions(state.selectedElements);
|
||||
const nextStartPositions = getStartPositions(state.selectedElements);
|
||||
|
||||
setOffset({ x: offsetX, y: offsetY });
|
||||
setStartPositions(startPositions);
|
||||
if (nextStartPositions) {
|
||||
setOffset({ x: offsetX, y: offsetY });
|
||||
setStartPositions(nextStartPositions);
|
||||
}
|
||||
};
|
||||
|
||||
const onDrag = (evt: MouseEvent) => {
|
||||
const scaledClient: XYPosition = {
|
||||
x: evt.clientX * (1 / k),
|
||||
y: evt.clientY * (1 / k)
|
||||
y: evt.clientY * (1 / k),
|
||||
};
|
||||
|
||||
state.selectedElements
|
||||
.filter(isNode)
|
||||
.forEach((node: Node) => {
|
||||
updateNodePos({ id: node.id, pos: {
|
||||
x: startPositions[node.id].x + scaledClient.x - position.x - offset.x - x ,
|
||||
y: startPositions[node.id].y + scaledClient.y - position.y - offset.y - y
|
||||
}});
|
||||
});
|
||||
(state.selectedElements.filter(isNode) as Node[]).forEach(node => {
|
||||
const pos: XYPosition = {
|
||||
x:
|
||||
startPositions[node.id].x +
|
||||
scaledClient.x -
|
||||
position.x -
|
||||
offset.x -
|
||||
x,
|
||||
y:
|
||||
startPositions[node.id].y +
|
||||
scaledClient.y -
|
||||
position.y -
|
||||
offset.y -
|
||||
y,
|
||||
};
|
||||
|
||||
updateNodePos({ id: node.id, pos });
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="react-flow__nodesselection"
|
||||
style={{
|
||||
transform: `translate(${x}px,${y}px) scale(${k})`
|
||||
transform: `translate(${x}px,${y}px) scale(${k})`,
|
||||
}}
|
||||
>
|
||||
<ReactDraggable
|
||||
scale={k}
|
||||
onStart={(evt: MouseEvent) => onStart(evt)}
|
||||
onDrag={(evt: MouseEvent) => onDrag(evt)}
|
||||
onStart={evt => onStart(evt as MouseEvent)}
|
||||
onDrag={evt => onDrag(evt as MouseEvent)}
|
||||
>
|
||||
<div
|
||||
className="react-flow__nodesselection-rect"
|
||||
@@ -79,7 +93,7 @@ export default memo(() => {
|
||||
width: state.selectedNodesBbox.width,
|
||||
height: state.selectedNodesBbox.height,
|
||||
top: state.selectedNodesBbox.y,
|
||||
left: state.selectedNodesBbox.x
|
||||
left: state.selectedNodesBbox.x,
|
||||
}}
|
||||
/>
|
||||
</ReactDraggable>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef, useState, memo, MouseEvent } from 'react';
|
||||
import React, { useEffect, useRef, useState, memo } from 'react';
|
||||
|
||||
import { useStoreActions } from '../../store/hooks';
|
||||
import { SelectionRect } from '../../types';
|
||||
@@ -10,7 +10,7 @@ const initialRect: SelectionRect = {
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
draw: false
|
||||
draw: false,
|
||||
};
|
||||
|
||||
function getMousePosition(evt: MouseEvent) {
|
||||
@@ -28,33 +28,33 @@ function getMousePosition(evt: MouseEvent) {
|
||||
}
|
||||
|
||||
export default memo(() => {
|
||||
const selectionPane = useRef(null);
|
||||
const selectionPane = useRef<HTMLDivElement>(null);
|
||||
const [rect, setRect] = useState(initialRect);
|
||||
const setSelection = useStoreActions(a => a.setSelection);
|
||||
const updateSelection = useStoreActions(a => a.updateSelection);
|
||||
const setNodesSelection = useStoreActions(a => a.setNodesSelection);
|
||||
|
||||
useEffect(() => {
|
||||
function onMouseDown(evt: MouseEvent) {
|
||||
function onMouseDown(evt: MouseEvent): void {
|
||||
const mousePos = getMousePosition(evt);
|
||||
if (!mousePos) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
setRect((currentRect) => ({
|
||||
setRect(currentRect => ({
|
||||
...currentRect,
|
||||
startX: mousePos.x,
|
||||
startY: mousePos.y,
|
||||
x: mousePos.x,
|
||||
y: mousePos.y,
|
||||
draw: true
|
||||
draw: true,
|
||||
}));
|
||||
|
||||
setSelection(true);
|
||||
}
|
||||
|
||||
function onMouseMove(evt: MouseEvent) {
|
||||
setRect((currentRect) => {
|
||||
function onMouseMove(evt: MouseEvent): void {
|
||||
setRect(currentRect => {
|
||||
if (!currentRect.draw) {
|
||||
return currentRect;
|
||||
}
|
||||
@@ -70,8 +70,12 @@ export default memo(() => {
|
||||
...currentRect,
|
||||
x: negativeX ? mousePos.x : currentRect.x,
|
||||
y: negativeY ? mousePos.y : currentRect.y,
|
||||
width: negativeX ? currentRect.startX - mousePos.x : mousePos.x - currentRect.startX,
|
||||
height: negativeY ? currentRect.startY - mousePos.y : mousePos.y - currentRect.startY,
|
||||
width: negativeX
|
||||
? currentRect.startX - mousePos.x
|
||||
: mousePos.x - currentRect.startX,
|
||||
height: negativeY
|
||||
? currentRect.startY - mousePos.y
|
||||
: mousePos.y - currentRect.startY,
|
||||
};
|
||||
|
||||
updateSelection(nextRect);
|
||||
@@ -81,40 +85,44 @@ export default memo(() => {
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
setRect((currentRect) => {
|
||||
setRect(currentRect => {
|
||||
setNodesSelection({ isActive: true, selection: currentRect });
|
||||
setSelection(false);
|
||||
|
||||
return {
|
||||
...currentRect,
|
||||
draw: false
|
||||
draw: false,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
selectionPane.current.addEventListener('mousedown', onMouseDown);
|
||||
selectionPane.current.addEventListener('mousemove', onMouseMove);
|
||||
selectionPane.current.addEventListener('mouseup', onMouseUp);
|
||||
if (selectionPane.current) {
|
||||
selectionPane.current.addEventListener('mousedown', onMouseDown);
|
||||
selectionPane.current.addEventListener('mousemove', onMouseMove);
|
||||
selectionPane.current.addEventListener('mouseup', onMouseUp);
|
||||
|
||||
return () => {
|
||||
selectionPane.current.removeEventListener('mousedown', onMouseDown);
|
||||
selectionPane.current.removeEventListener('mousemove', onMouseMove);
|
||||
selectionPane.current.removeEventListener('mouseup', onMouseUp);
|
||||
};
|
||||
}, []);
|
||||
return () => {
|
||||
if (!selectionPane.current) {
|
||||
return;
|
||||
}
|
||||
selectionPane.current.removeEventListener('mousedown', onMouseDown);
|
||||
selectionPane.current.removeEventListener('mousemove', onMouseMove);
|
||||
selectionPane.current.removeEventListener('mouseup', onMouseUp);
|
||||
};
|
||||
}
|
||||
|
||||
return;
|
||||
}, [selectionPane.current]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="react-flow__selectionpane"
|
||||
ref={selectionPane}
|
||||
>
|
||||
<div className="react-flow__selectionpane" ref={selectionPane}>
|
||||
{rect.draw && (
|
||||
<div
|
||||
className="react-flow__selection"
|
||||
style={{
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
transform: `translate(${rect.x}px, ${rect.y}px)`
|
||||
transform: `translate(${rect.x}px, ${rect.y}px)`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user