* refactor(ts): add ReactFlowProps

* Refactor/grid.tsx (#24)

* chore(deps-dev): bump start-server-and-test from 1.10.4 to 1.10.5

Bumps [start-server-and-test](https://github.com/bahmutov/start-server-and-test) from 1.10.4 to 1.10.5.
- [Release notes](https://github.com/bahmutov/start-server-and-test/releases)
- [Commits](https://github.com/bahmutov/start-server-and-test/compare/v1.10.4...v1.10.5)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

* chore(deps-dev): bump typescript from 3.6.3 to 3.6.4

Bumps [typescript](https://github.com/Microsoft/TypeScript) from 3.6.3 to 3.6.4.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v3.6.3...v3.6.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

* refactor: grid.js -> grid.tsx

* refactor(bg): remove unused renderer

* refactor(connectionline): use ts

* refactor(ts): edges

* chore(build): update

* Refactor/typescript (WIP) (#25)

* refactor(store): use ts

* refactor(edgewrapper): use ts

* fix(handle): provide onConnect default func

* refactor(nodeselection): use ts

* refactor(userselction): use ts

* refactor(plugins): use ts

* refactor(hooks): use ts

* refactor(nodes): use ts

* refactor(edgerenderer): use ts

* refactor(graphview): use ts

* refactor(utils): rename js to ts

* refactor(app): fix ts errors

* fix(ts): errors

* fix(app): ts errors

* refactor(app): ts erros

* refactor(app): ts errors

* fix(utils): removeElements

* feat(example): add empty renderer closes #34

* fix(connect): dont drag node on connect

* chore(build): update
This commit is contained in:
Moritz
2019-10-15 22:44:10 +02:00
committed by GitHub
parent 1651ed332a
commit c5632323c3
55 changed files with 4399 additions and 13047 deletions
+85
View File
@@ -0,0 +1,85 @@
import React, {memo} from 'react';
import classnames from 'classnames';
import { useStoreState } from '../../store/hooks';
import { GridType } from '../../types';
interface GridProps {
backgroundType?: GridType;
gap?: number;
color?: string;
size?: number;
style?: React.CSSProperties;
className?: string | null;
};
const baseStyles: React.CSSProperties = {
position: 'absolute',
top: 0,
left: 0,
};
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}`);
return [...xValues, ...yValues].join(' ');
};
const createGridDots = (
width: number,
height: number,
xOffset: number,
yOffset: number,
gap: number,
size: number
): string => {
const lineCountX = Math.ceil(width / gap) + 1;
const lineCountY = Math.ceil(height / gap) + 1;
const values = Array.from({length: lineCountX}, (_, col) => {
const x = col * gap + xOffset;
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`;
}).join(' ');
});
return values.join(' ');
};
const Grid = memo(
({gap = 24, color = '#aaa', size = 0.5, style = {}, className = null, backgroundType = GridType.Dots}: GridProps) => {
const {
width,
height,
transform: [x, y, scale],
} = useStoreState(s => s);
const gridClasses = classnames('react-flow__grid', className);
const scaledGap = gap * scale;
const xOffset = x % scaledGap;
const yOffset = y % scaledGap;
const isLines = backgroundType === 'lines';
const path = isLines
? createGridLines(width, height, xOffset, yOffset, scaledGap)
: createGridDots(width, height, xOffset, yOffset, scaledGap, size);
const fill = isLines ? 'none' : color;
const stroke = isLines ? color : 'none';
return (
<svg width={width} height={height} style={{...baseStyles, ...style}} className={gridClasses}>
<path fill={fill} stroke={stroke} strokeWidth={size} d={path} />
</svg>
);
}
);
Grid.displayName = 'Grid';
export default Grid;
-49
View File
@@ -1,49 +0,0 @@
import React, { useEffect, useState } from 'react';
import cx from 'classnames';
export default (props) => {
const [sourceNode, setSourceNode] = useState(null);
const hasHandleId = props.connectionSourceId.includes('__');
const sourceIdSplitted = props.connectionSourceId.split('__');
const nodeId = sourceIdSplitted[0];
const handleId = hasHandleId ? sourceIdSplitted[1] : null;
useEffect(() => {
setSourceNode(props.nodes.find(n => n.id === nodeId));
}, []);
if (!sourceNode) {
return null;
}
const style = props.connectionLineStyle || {};
const className = cx('react-flow__edge', 'connection', props.className);
const sourceHandle = handleId ? sourceNode.__rg.handleBounds.source.find(d => 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;
const targetX = (props.connectionPositionX - props.transform[0]) * (1 / props.transform[2]);
const targetY = (props.connectionPositionY - props.transform[1]) * (1 / props.transform[2]);
let dAttr = '';
if (props.connectionLineType === 'bezier') {
const yOffset = Math.abs(targetY - sourceY) / 2;
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
dAttr = `M${sourceX},${sourceY} C${sourceX},${centerY} ${targetX},${centerY} ${targetX},${targetY}`;
} else {
dAttr = `M${sourceX},${sourceY} ${targetX},${targetY}`;
}
return (
<g className={className}>
<path
d={dAttr}
{...style}
/>
</g>
);
};
+67
View File
@@ -0,0 +1,67 @@
import React, { useEffect, useState, SVGAttributes } from 'react';
import cx from 'classnames';
import { ElementId, Node, Transform, HandleElement } from '../../types';
interface ConnectionLineProps {
connectionSourceId: ElementId;
connectionPositionX: number;
connectionPositionY: number;
connectionLineType?: string | null;
nodes: Node[];
transform: Transform;
connectionLineStyle?: SVGAttributes<{}>;
className?: string;
};
export default ({
connectionSourceId, connectionLineStyle = {}, connectionPositionX, connectionPositionY,
connectionLineType, nodes = [], className, transform
}: ConnectionLineProps) => {
const [sourceNode, setSourceNode] = useState<Node | null>(null);
const hasHandleId = connectionSourceId.includes('__');
const sourceIdSplitted = connectionSourceId.split('__');
const nodeId = sourceIdSplitted[0];
const handleId = hasHandleId ? sourceIdSplitted[1] : null;
useEffect(() => {
const nextSourceNode = nodes.find(n => n.id === nodeId) || null;
setSourceNode(nextSourceNode);
}, []);
if (!sourceNode) {
return null;
}
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 sourceX = sourceNode.__rg.position.x + sourceHandleX;
const sourceY = sourceNode.__rg.position.y + sourceHandleY;
const targetX = (connectionPositionX - transform[0]) * (1 / transform[2]);
const targetY = (connectionPositionY - transform[1]) * (1 / transform[2]);
let dAttr: string = '';
if (connectionLineType === 'bezier') {
const yOffset = Math.abs(targetY - sourceY) / 2;
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
dAttr = `M${sourceX},${sourceY} C${sourceX},${centerY} ${targetX},${centerY} ${targetX},${targetY}`;
} else {
dAttr = `M${sourceX},${sourceY} ${targetX},${targetY}`;
}
return (
<g className={edgeClasses}>
<path
d={dAttr}
{...connectionLineStyle}
/>
</g>
);
};
@@ -1,9 +1,11 @@
import React, { memo } from 'react';
import { EdgeBezierProps } from '../../types';
export default memo(({
sourceX, sourceY, targetX, targetY,
sourcePosition, targetPosition, style = {}
}) => {
sourcePosition = 'bottom', targetPosition = 'top', style = {}
}: EdgeBezierProps) => {
const yOffset = Math.abs(targetY - sourceY) / 2;
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
@@ -1,10 +1,10 @@
import React, { memo } from 'react';
export default memo((props) => {
const {
sourceX, sourceY, targetX, targetY, style = {}
} = props;
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;
@@ -1,10 +1,10 @@
import React, { memo } from 'react';
export default memo((props) => {
const {
sourceX, sourceY, targetX, targetY, style = {}
} = props;
import { EdgeProps } from '../../types';
export default memo(({
sourceX, sourceY, targetX, targetY, style = {}
}: EdgeProps) => {
return (
<path
{...style}
-37
View File
@@ -1,37 +0,0 @@
import React, { memo } from 'react';
import cx from 'classnames';
import { inInputDOMNode } from '../../utils';
import store from '../../store';
export default EdgeComponent => {
const EdgeWrapper = memo((props) => {
const {
id, source, target, type,
animated, selected, onClick
} = props;
const edgeClasses = cx('react-flow__edge', { selected, animated });
const onEdgeClick = (evt) => {
if (inInputDOMNode(evt)) {
return false;
}
store.dispatch.setSelectedElements({ id, source, target });
onClick({ id, source, target, type });
};
return (
<g
className={edgeClasses}
onClick={onEdgeClick}
>
<EdgeComponent {...props} />
</g>
);
});
EdgeWrapper.displayName = 'EdgeWrapper';
EdgeWrapper.whyDidYouRender = false;
return EdgeWrapper;
};
+46
View File
@@ -0,0 +1,46 @@
import React, { memo, MouseEvent, ComponentType } from 'react';
import cx from 'classnames';
import { isInputDOMNode } from '../../utils';
import store from '../../store';
import { EdgeWrapperProps } 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;
}
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';
return EdgeWrapper;
};
@@ -1,35 +1,69 @@
import React, { memo } from 'react';
import React, { memo, MouseEvent as ReactMouseEvent } from 'react';
import cx from 'classnames';
function onMouseDown(evt, { nodeId, setSourceId, setPosition, onConnect, isTarget, isValidConnection }) {
const containerBounds = document.querySelector('.react-flow').getBoundingClientRect();
let recentHoveredHandle = null;
import { HandleType, ElementId, Position, XYPosition, OnConnectFunc, Connection } from '../../types';
type ValidConnectionFunc = (connection: Connection) => boolean;
interface BaseHandleProps {
type: HandleType;
nodeId: ElementId;
onConnect: OnConnectFunc;
position: Position;
setSourceId: (nodeId: ElementId) => void;
setPosition: (pos: XYPosition) => void;
isValidConnection: ValidConnectionFunc;
id?: ElementId | boolean;
className?: string;
};
type Result = {
elementBelow: Element;
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
): void {
const reactFlowNode = document.querySelector('.react-flow');
if (!reactFlowNode) {
return null;
}
const containerBounds = reactFlowNode.getBoundingClientRect();
let recentHoveredHandle: Element = null;
setPosition({
x: evt.clientX - containerBounds.x,
y: evt.clientY - containerBounds.y,
x: evt.clientX - containerBounds.left,
y: evt.clientY - containerBounds.top,
});
setSourceId(nodeId);
function resetRecentHandle() {
if (recentHoveredHandle) {
recentHoveredHandle.classList.remove('valid');
recentHoveredHandle.classList.remove('connecting');
if (!recentHoveredHandle) {
return false;
}
recentHoveredHandle.classList.remove('valid');
recentHoveredHandle.classList.remove('connecting');
}
// checks if element below mouse is a handle and returns connection in form of an object { source: 123, target: 312 }
function checkElementBelowIsValid(evt) {
function checkElementBelowIsValid(evt: MouseEvent) {
const elementBelow = document.elementFromPoint(evt.clientX, evt.clientY);
const result = {
const result: Result = {
elementBelow,
isValid: false,
connection: null,
connection: { source: null, target: null },
isHoveringHandle: false
};
if (elementBelow && (elementBelow.classList.contains('target') || elementBelow.classList.contains('source'))) {
let connection = null;
let connection: Connection = { source: null, target: null };
if (isTarget) {
const sourceId = elementBelow.getAttribute('data-nodeid');
@@ -49,10 +83,10 @@ function onMouseDown(evt, { nodeId, setSourceId, setPosition, onConnect, isTarg
return result;
}
function onMouseMove(evt) {
function onMouseMove(evt: MouseEvent) {
setPosition({
x: evt.clientX - containerBounds.x,
y: evt.clientY - containerBounds.y,
x: evt.clientX - containerBounds.left,
y: evt.clientY - containerBounds.top,
});
const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid(evt);
@@ -70,7 +104,7 @@ function onMouseDown(evt, { nodeId, setSourceId, setPosition, onConnect, isTarg
}
}
function onMouseUp(evt) {
function onMouseUp(evt: MouseEvent) {
const { connection, isValid } = checkElementBelowIsValid(evt);
if (isValid) {
@@ -92,7 +126,7 @@ 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',
@@ -108,16 +142,15 @@ const BaseHandle = memo(({
data-nodeid={nodeIdWithHandleId}
data-handlepos={position}
className={handleClasses}
onMouseDown={evt => onMouseDown(evt, {
nodeId: nodeIdWithHandleId, setSourceId, setPosition,
onMouseDown={evt => onMouseDown(evt,
nodeIdWithHandleId, setSourceId, setPosition,
onConnect, isTarget, isValidConnection
})}
)}
{...rest}
/>
);
});
BaseHandle.displayName = 'BaseHandle';
BaseHandle.whyDidYouRender = false;
export default BaseHandle;
@@ -1,18 +1,29 @@
import React, { memo, useContext } from 'react';
import PropTypes from 'prop-types';
import { useStoreActions, useStoreState } from 'easy-peasy';
import { useStoreActions, useStoreState } from '../../store/hooks';
import BaseHandle from './BaseHandle';
import NodeIdContext from '../../contexts/NodeIdContext'
const Handle = memo(({ onConnect, ...rest }) => {
const nodeId = useContext(NodeIdContext);
import { HandleType, ElementId, Position, OnConnectParams, OnConnectFunc } from '../../types';
interface HandleProps {
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) => {
const onConnectExtended = (params: OnConnectParams) => {
onConnectAction(params);
onConnect(params);
};
@@ -23,6 +34,9 @@ const Handle = memo(({ onConnect, ...rest }) => {
setPosition={setPosition}
setSourceId={setSourceId}
onConnect={onConnectExtended}
type={type}
position={position}
isValidConnection={isValidConnection}
{...rest}
/>
);
@@ -30,18 +44,4 @@ const Handle = memo(({ onConnect, ...rest }) => {
Handle.displayName = 'Handle';
Handle.propTypes = {
type: PropTypes.oneOf(['source', 'target']),
position: PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
onConnect: PropTypes.func,
isValidConnection: PropTypes.func
};
Handle.defaultProps = {
type: 'source',
position: 'top',
onConnect: () => {},
isValidConnection: () => true
};
export default Handle;
@@ -1,15 +1,16 @@
import React from 'react';
import React, { CSSProperties } from 'react';
import Handle from '../../components/Handle';
import { NodeProps } from '../../types';
const nodeStyles = {
const nodeStyles: CSSProperties = {
background: '#ff6060',
padding: 10,
borderRadius: 5,
width: 150
};
export default ({ data, style }) => (
export default ({ data, style }: NodeProps) => (
<div style={{ ...nodeStyles, ...style }}>
<Handle type="target" position="top" />
{data.label}
@@ -1,15 +1,16 @@
import React from 'react';
import React, { CSSProperties } from 'react';
import Handle from '../../components/Handle';
import { NodeProps } from '../../types';
const nodeStyles = {
const nodeStyles: CSSProperties = {
background: '#9999ff',
padding: 10,
borderRadius: 5,
width: 150
};
export default ({ data, style }) => (
export default ({ data, style }: NodeProps) => (
<div style={{ ...nodeStyles, ...style }}>
{data.label}
<Handle type="source" position="bottom" />
@@ -1,15 +1,16 @@
import React from 'react';
import React, { CSSProperties } from 'react';
import Handle from '../../components/Handle';
import { NodeProps } from '../../types';
const nodeStyles = {
const nodeStyles: CSSProperties = {
background: '#55dd99',
padding: 10,
borderRadius: 5,
width: 150
};
export default ({ data, style }) => (
export default ({ data, style }: NodeProps) => (
<div style={{ ...nodeStyles, ...style }}>
<Handle type="target" position="top" />
{data.label}
-166
View File
@@ -1,166 +0,0 @@
import React, { useEffect, useRef, useState, memo } from 'react';
import ReactDraggable from 'react-draggable';
import cx from 'classnames';
import { getDimensions, inInputDOMNode } from '../../utils';
import { Provider } from '../../contexts/NodeIdContext';
import store from '../../store';
const isHandle = e => (
e.target.className &&
e.target.className.includes &&
(e.target.className.includes('source') || e.target.className.includes('target'))
);
const hasResizeObserver = !!window.ResizeObserver;
const getHandleBounds = (sel, nodeElement, parentBounds, k) => {
const handles = nodeElement.querySelectorAll(sel);
if (!handles || !handles.length) {
return null;
}
return [].map.call(handles, (handle) => {
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;
if (nodeIdSplitted) {
handleId = nodeIdSplitted.length ? nodeIdSplitted[1] : nodeIdSplitted;
}
return {
id: handleId,
position: handlePosition,
x: (bounds.x - parentBounds.x) * (1 / k),
y: (bounds.y - parentBounds.y) * (1 / k),
...dimensions
};
});
};
const onStart = (evt, { setOffset, onClick, id, type, data, position, transform }) => {
if (inInputDOMNode(evt) || isHandle(evt)) {
return false;
}
const scaledClient = {
x: evt.clientX * (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 };
store.dispatch.setSelectedElements({ id, type });
setOffset({ x: offsetX, y: offsetY });
onClick(node);
};
const onDrag = (evt, { setDragging, id, offset, transform }) => {
const scaledClient = {
x: evt.clientX * (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
}});
};
const onStop = ({ onNodeDragStop, setDragging, isDragging, id, type, position, data }) => {
if (!isDragging) {
return false;
}
setDragging(false);
onNodeDragStop({
id, type, position, data
});
};
export default NodeComponent => {
const NodeWrapper = memo((props) => {
const nodeElement = useRef(null);
const [offset, setOffset] = useState({ x: 0, y: 0 });
const [isDragging, setDragging] = useState(false);
const {
id, type, data, transform, xPos, yPos, selected,
onClick, onNodeDragStop, style
} = props;
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 = () => {
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(() => {
updateNode();
let resizeObserver = null;
if (hasResizeObserver) {
resizeObserver = new ResizeObserver(entries => {
for (let entry of entries) {
updateNode();
}
});
resizeObserver.observe(nodeElement.current);
}
return () => {
if (hasResizeObserver && resizeObserver) {
resizeObserver.unobserve(nodeElement.current);
}
}
}, []);
return (
<ReactDraggable.DraggableCore
onStart={evt => onStart(evt, { onClick, id, type, data, setOffset, transform, position })}
onDrag={evt => onDrag(evt, { setDragging, id, offset, transform })}
onStop={() => onStop({ onNodeDragStop, isDragging, setDragging, id, type, position, data })}
scale={transform[2]}
>
<div
className={nodeClasses}
ref={nodeElement}
style={nodeStyle}
>
<Provider value={id}>
<NodeComponent
id={id}
data={data}
type={type}
style={style}
selected={selected}
/>
</Provider>
</div>
</ReactDraggable.DraggableCore>
);
});
NodeWrapper.displayName = 'NodeWrapper';
NodeWrapper.whyDidYouRender = false;
return NodeWrapper;
};
+180
View File
@@ -0,0 +1,180 @@
import React, { useEffect, useRef, useState, memo, ComponentType } from 'react';
import { DraggableCore, DraggableEvent } from 'react-draggable';
import cx from 'classnames';
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';
const isHandle = (evt: MouseEvent | DraggableEvent) => {
const target = evt.target as HTMLElement;
return (
target.className &&
target.className.includes &&
(target.className.includes('source') || target.className.includes('target'))
);
};
const getHandleBounds = (
selector: string, nodeElement: HTMLDivElement, parentBounds: ClientRect | DOMRect, k: number
): HandleElement => {
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('__');
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
};
});
};
const onStart = (
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])
};
const offsetX = scaledClient.x - position.x - transform[0];
const offsetY = scaledClient.y - position.y - transform[1];
const node = { id, type, position, data };
store.dispatch.setSelectedElements({ id, type });
setOffset({ x: offsetX, y: offsetY });
onClick(node);
};
const onDrag = (
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])
};
setDragging(true);
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
): void => {
if (isDragging) {
setDragging(false);
onNodeDragStop({
id, type, position, data
});
}
};
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 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])
};
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);
}
}
}
}, [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}
>
<Provider value={id}>
<NodeComponent
id={id}
data={data}
type={type}
style={style}
selected={selected}
/>
</Provider>
</div>
</DraggableCore>
);
});
NodeWrapper.displayName = 'NodeWrapper';
return NodeWrapper;
};
@@ -1,13 +1,14 @@
import React, { useState, memo } from 'react';
import ReactDraggable from 'react-draggable';
import { useStoreState, useStoreActions } from 'easy-peasy';
import { useStoreState, useStoreActions } from '../../store/hooks';
import { isNode } from '../../utils/graph';
import { Node, Elements, XYPosition } from '../../types';
function getStartPositions(elements) {
function getStartPositions(elements: Elements) {
return elements
.filter(isNode)
.reduce((res, node) => {
.reduce((res, node: Node) => {
const startPosition = {
x: node.__rg.position.x || node.position.x,
y: node.__rg.position.y || node.position.x
@@ -31,31 +32,33 @@ export default memo(() => {
const [x, y, k] = state.transform;
const position = state.selectedNodesBbox;
const onStart = (evt) => {
const scaledClient = {
const onStart = (evt: MouseEvent) => {
const scaledClient: XYPosition = {
x: evt.clientX * (1 / k),
y: evt.clientY * (1 / k)
};
const offsetX = scaledClient.x - position.x - x;
const offsetY = scaledClient.y - position.y - y;
const offsetX: number = scaledClient.x - position.x - x;
const offsetY: number = scaledClient.y - position.y - y;
const startPositions = getStartPositions(state.selectedElements);
setOffset({ x: offsetX, y: offsetY });
setStartPositions(startPositions);
};
const onDrag = (evt) => {
const scaledClient = {
const onDrag = (evt: MouseEvent) => {
const scaledClient: XYPosition = {
x: evt.clientX * (1 / k),
y: evt.clientY * (1 / k)
};
state.selectedElements.filter(isNode).forEach(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)
.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
}});
});
};
return (
@@ -67,8 +70,8 @@ export default memo(() => {
>
<ReactDraggable
scale={k}
onStart={onStart}
onDrag={onDrag}
onStart={(evt: MouseEvent) => onStart(evt)}
onDrag={(evt: MouseEvent) => onDrag(evt)}
>
<div
className="react-flow__nodesselection-rect"
@@ -1,7 +1,9 @@
import React, { useEffect, useRef, useState, memo } from 'react';
import { useStoreActions } from 'easy-peasy';
import React, { useEffect, useRef, useState, memo, MouseEvent } from 'react';
const initialRect = {
import { useStoreActions } from '../../store/hooks';
import { SelectionRect } from '../../types';
const initialRect: SelectionRect = {
startX: 0,
startY: 0,
x: 0,
@@ -11,8 +13,13 @@ const initialRect = {
draw: false
};
function getMousePosition(evt) {
const containerBounds = document.querySelector('.react-flow').getBoundingClientRect();
function getMousePosition(evt: MouseEvent) {
const reactFlowNode = document.querySelector('.react-flow');
if (!reactFlowNode) {
return false;
}
const containerBounds = reactFlowNode.getBoundingClientRect();
return {
x: evt.clientX - containerBounds.left,
@@ -28,8 +35,11 @@ export default memo(() => {
const setNodesSelection = useStoreActions(a => a.setNodesSelection);
useEffect(() => {
function onMouseDown(evt) {
function onMouseDown(evt: MouseEvent) {
const mousePos = getMousePosition(evt);
if (!mousePos) {
return false;
}
setRect((currentRect) => ({
...currentRect,
@@ -43,13 +53,17 @@ export default memo(() => {
setSelection(true);
}
function onMouseMove(evt) {
function onMouseMove(evt: MouseEvent) {
setRect((currentRect) => {
if (!currentRect.draw) {
return currentRect;
}
const mousePos = getMousePosition(evt);
if (!mousePos) {
return currentRect;
}
const negativeX = mousePos.x < currentRect.startX;
const negativeY = mousePos.y < currentRect.startY;
const nextRect = {
@@ -94,7 +108,7 @@ export default memo(() => {
className="react-flow__selectionpane"
ref={selectionPane}
>
{(rect.draw || rect.fixed) && (
{rect.draw && (
<div
className="react-flow__selection"
style={{