* 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:
Moritz
2019-10-21 20:58:28 +02:00
committed by GitHub
parent ce210eb5dc
commit a793f8c2ff
80 changed files with 2634 additions and 1247 deletions
+37 -14
View File
@@ -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>
);
+23 -14
View File
@@ -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>
);
};
+29 -21
View File
@@ -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}
/>
);
});
);
+12 -12
View File
@@ -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}`}
/>
);
}
);
+7 -10
View File
@@ -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}`} />
);
}
);
+47 -33
View File
@@ -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';
+79 -42
View File
@@ -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';
+45 -34
View File
@@ -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';
+1 -1
View File
@@ -7,7 +7,7 @@ const nodeStyles: CSSProperties = {
background: '#ff6060',
padding: 10,
borderRadius: 5,
width: 150
width: 150,
};
export default ({ data, style }: NodeProps) => (
+1 -1
View File
@@ -7,7 +7,7 @@ const nodeStyles: CSSProperties = {
background: '#9999ff',
padding: 10,
borderRadius: 5,
width: 150
width: 150,
};
export default ({ data, style }: NodeProps) => (
+1 -1
View File
@@ -7,7 +7,7 @@ const nodeStyles: CSSProperties = {
background: '#55dd99',
padding: 10,
borderRadius: 5,
width: 150
width: 150,
};
export default ({ data, style }: NodeProps) => (
+208 -106
View File
@@ -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';
+45 -31
View File
@@ -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>
+35 -27
View File
@@ -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)`,
}}
/>
)}