refactor(app): folder structure, components cleanup

This commit is contained in:
moklick
2019-10-06 19:01:02 +02:00
parent 845bae284f
commit cc2775dd78
25 changed files with 822 additions and 981 deletions
+49
View File
@@ -0,0 +1,49 @@
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-graph__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>
);
};
+27
View File
@@ -0,0 +1,27 @@
import React, { memo } from 'react';
export default memo(({
sourceX, sourceY, targetX, targetY,
sourcePosition, targetPosition, style = {}
}) => {
const yOffset = Math.abs(targetY - sourceY) / 2;
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
let dAttr = `M${sourceX},${sourceY} C${sourceX},${centerY} ${targetX},${centerY} ${targetX},${targetY}`;
if (['left', 'right'].includes(sourcePosition) && ['left', 'right'].includes(targetPosition)) {
const xOffset = Math.abs(targetX - sourceX) / 2;
const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset;
dAttr = `M${sourceX},${sourceY} C${centerX},${sourceY} ${centerX},${targetY} ${targetX},${targetY}`;
} else if (['left', 'right'].includes(sourcePosition) || ['left', 'right'].includes(targetPosition)) {
dAttr = `M${sourceX},${sourceY} C${sourceX},${targetY} ${sourceX},${targetY} ${targetX},${targetY}`;
}
return (
<path
{...style}
d={dAttr}
/>
);
});
+17
View File
@@ -0,0 +1,17 @@
import React, { memo } from 'react';
export default memo((props) => {
const {
sourceX, sourceY, targetX, targetY, style = {}
} = props;
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}`}
/>
);
});
+14
View File
@@ -0,0 +1,14 @@
import React, { memo } from 'react';
export default memo((props) => {
const {
sourceX, sourceY, targetX, targetY, style = {}
} = props;
return (
<path
{...style}
d={`M ${sourceX},${sourceY}L ${targetX},${targetY}`}
/>
);
});
+37
View File
@@ -0,0 +1,37 @@
import React, { memo } from 'react';
import cx from 'classnames';
import { isInputNode } 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-graph__edge', { selected, animated });
const onEdgeClick = (evt) => {
if (isInputNode(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;
};
+71
View File
@@ -0,0 +1,71 @@
import React, { memo } from 'react';
import cx from 'classnames';
function onMouseDown(evt, { nodeId, setSourceId, setPosition, onConnect, isTarget }) {
const containerBounds = document.querySelector('.react-graph').getBoundingClientRect();
setPosition({
x: evt.clientX - containerBounds.x,
y: evt.clientY - containerBounds.y,
});
setSourceId(nodeId);
function onMouseMove(evt) {
setPosition({
x: evt.clientX - containerBounds.x,
y: evt.clientY - containerBounds.y,
});
}
function onMouseUp(evt) {
const elementBelow = document.elementFromPoint(evt.clientX, evt.clientY);
if (elementBelow && (elementBelow.classList.contains('target') || elementBelow.classList.contains('source'))) {
if (isTarget) {
const sourceId = elementBelow.getAttribute('data-nodeid');
onConnect({ source: sourceId, target: nodeId });
} else {
const targetId = elementBelow.getAttribute('data-nodeid');
onConnect({ source: nodeId, target: targetId });
}
}
setSourceId(null);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
}
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp)
}
const BaseHandle = memo(({
type, nodeId, onConnect, position,
setSourceId, setPosition, className,
id = false, ...rest
}) => {
const isTarget = type === 'target';
const handleClasses = cx(
'react-graph__handle',
className,
position,
{ source: !isTarget, target: isTarget }
);
const nodeIdWithHandleId = id ? `${nodeId}__${id}` : nodeId;
return (
<div
data-nodeid={nodeIdWithHandleId}
data-handlepos={position}
className={handleClasses}
onMouseDown={evt => onMouseDown(evt, { nodeId: nodeIdWithHandleId, setSourceId, setPosition, onConnect, isTarget })}
{...rest}
/>
);
});
BaseHandle.displayName = 'BaseHandle';
BaseHandle.whyDidYouRender = false;
export default BaseHandle;
+45
View File
@@ -0,0 +1,45 @@
import React, { memo, useContext } from 'react';
import PropTypes from 'prop-types';
import { useStoreActions, useStoreState } from 'easy-peasy';
import BaseHandle from './BaseHandle';
import NodeIdContext from '../../contexts/NodeIdContext'
const Handle = memo(({ onConnect, ...rest }) => {
const nodeId = useContext(NodeIdContext);
const { setPosition, setSourceId } = useStoreActions(a => ({
setPosition: a.setConnectionPosition,
setSourceId: a.setConnectionSourceId
}));
const onConnectAction = useStoreState(s => s.onConnect);
const onConnectExtended = (params) => {
onConnectAction(params);
onConnect(params);
};
return (
<BaseHandle
nodeId={nodeId}
setPosition={setPosition}
setSourceId={setSourceId}
onConnect={onConnectExtended}
{...rest}
/>
);
});
Handle.displayName = 'Handle';
Handle.propTypes = {
type: PropTypes.oneOf(['source', 'target']),
position: PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
onConnect: PropTypes.func
};
Handle.defaultProps = {
type: 'source',
position: 'top',
onConnect: () => {}
};
export default Handle;
+18
View File
@@ -0,0 +1,18 @@
import React from 'react';
import Handle from '../../components/Handle';
const nodeStyles = {
background: '#ff6060',
padding: 10,
borderRadius: 5,
width: 150
};
export default ({ data, style }) => (
<div style={{ ...nodeStyles, ...style }}>
<Handle type="target" position="top" />
{data.label}
<Handle type="source" position="bottom" />
</div>
);
+17
View File
@@ -0,0 +1,17 @@
import React from 'react';
import Handle from '../../components/Handle';
const nodeStyles = {
background: '#9999ff',
padding: 10,
borderRadius: 5,
width: 150
};
export default ({ data, style }) => (
<div style={{ ...nodeStyles, ...style }}>
{data.label}
<Handle type="source" position="bottom" />
</div>
);
+17
View File
@@ -0,0 +1,17 @@
import React from 'react';
import Handle from '../../components/Handle';
const nodeStyles = {
background: '#55dd99',
padding: 10,
borderRadius: 5,
width: 150
};
export default ({ data, style }) => (
<div style={{ ...nodeStyles, ...style }}>
<Handle type="target" position="top" />
{data.label}
</div>
);
+166
View File
@@ -0,0 +1,166 @@
import React, { useEffect, useRef, useState, memo } from 'react';
import ReactDraggable from 'react-draggable';
import cx from 'classnames';
import { getDimensions, isInputNode } 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 (isInputNode(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-graph__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;
};
+85
View File
@@ -0,0 +1,85 @@
import React, { useState, memo } from 'react';
import ReactDraggable from 'react-draggable';
import { useStoreState, useStoreActions } from 'easy-peasy';
import { isNode } from '../../graph-utils';
function getStartPositions(elements) {
return elements
.filter(isNode)
.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;
}, {});
}
export default memo(() => {
const [offset, setOffset] = useState({ x: 0, y: 0 });
const [startPositions, setStartPositions] = useState({});
const state = useStoreState(s => ({
transform: s.transform,
selectedNodesBbox: s.selectedNodesBbox,
selectedElements: s.selectedElements
}));
const updateNodePos = useStoreActions(a => a.updateNodePos);
const [x, y, k] = state.transform;
const position = state.selectedNodesBbox;
const onStart = (evt) => {
const scaledClient = {
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 startPositions = getStartPositions(state.selectedElements);
setOffset({ x: offsetX, y: offsetY });
setStartPositions(startPositions);
};
const onDrag = (evt) => {
const scaledClient = {
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
}});
});
};
return (
<div
className="react-graph__nodesselection"
style={{
transform: `translate(${x}px,${y}px) scale(${k})`
}}
>
<ReactDraggable
scale={k}
onStart={onStart}
onDrag={onDrag}
>
<div
className="react-graph__nodesselection-rect"
style={{
width: state.selectedNodesBbox.width,
height: state.selectedNodesBbox.height,
top: state.selectedNodesBbox.y,
left: state.selectedNodesBbox.x
}}
/>
</ReactDraggable>
</div>
);
});
+109
View File
@@ -0,0 +1,109 @@
import React, { useEffect, useRef, useState, memo } from 'react';
import { useStoreActions } from 'easy-peasy';
const initialRect = {
startX: 0,
startY: 0,
x: 0,
y: 0,
width: 0,
height: 0,
draw: false
};
function getMousePosition(evt) {
const containerBounds = document.querySelector('.react-graph').getBoundingClientRect();
return {
x: evt.clientX - containerBounds.left,
y: evt.clientY - containerBounds.top,
};
}
export default memo(() => {
const selectionPane = useRef(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) {
const mousePos = getMousePosition(evt);
setRect((currentRect) => ({
...currentRect,
startX: mousePos.x,
startY: mousePos.y,
x: mousePos.x,
y: mousePos.y,
draw: true
}));
setSelection(true);
}
function onMouseMove(evt) {
setRect((currentRect) => {
if (!currentRect.draw) {
return currentRect;
}
const mousePos = getMousePosition(evt);
const negativeX = mousePos.x < currentRect.startX;
const negativeY = mousePos.y < currentRect.startY;
const nextRect = {
...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,
};
updateSelection(nextRect);
return nextRect;
});
}
function onMouseUp() {
setRect((currentRect) => {
setNodesSelection({ isActive: true, selection: currentRect });
setSelection(false);
return {
...currentRect,
draw: false
};
});
}
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 (
<div
className="react-graph__selectionpane"
ref={selectionPane}
>
{(rect.draw || rect.fixed) && (
<div
className="react-graph__selection"
style={{
width: rect.width,
height: rect.height,
transform: `translate(${rect.x}px, ${rect.y}px)`
}}
/>
)}
</div>
);
});