Files
xyflow/src/store/reducer.ts
T

365 lines
10 KiB
TypeScript

import isEqual from 'fast-deep-equal';
import { getDimensions } from '../utils';
import {
getNodesInside,
getConnectedEdges,
getRectOfNodes,
isNode,
isEdge,
parseNode,
parseEdge,
} from '../utils/graph';
import { getHandleBounds } from '../components/Nodes/utils';
import { ReactFlowState, Node, XYPosition, Edge } from '../types';
import * as constants from './contants';
import { ReactFlowAction } from './actions';
import { initialState } from './index';
type ElementsSplitted = {
nodeElements: Node[];
edgeElements: Edge[];
};
export default function reactFlowReducer(state = initialState, action: ReactFlowAction): ReactFlowState {
switch (action.type) {
case constants.SET_ELEMENTS: {
const propElements = action.payload;
const elementsSplitted: ElementsSplitted = {
nodeElements: [],
edgeElements: [],
};
const { nodeElements, edgeElements } = propElements.reduce((res, item): ElementsSplitted => {
if (isNode(item)) {
res.nodeElements.push(item);
} else if (isEdge(item)) {
res.edgeElements.push(item);
}
return res;
}, elementsSplitted);
const nextNodes = nodeElements.map((propNode: Node) => {
let storeNode = state.nodes.find((node) => node.id === propNode.id);
// update existing element
if (storeNode) {
const positionChanged =
storeNode.position.x !== propNode.position.x || storeNode.position.y !== propNode.position.y;
const typeChanged = typeof propNode.type !== 'undefined' && propNode.type !== storeNode.type;
storeNode = {
...storeNode,
...propNode,
};
if (positionChanged) {
storeNode.__rf.position = propNode.position;
}
if (typeChanged) {
// we reset the elements dimensions here in order to force a re-calculation of the bounds.
// When the type of a node changes it is possible that the number or positions of handles changes too.
storeNode.__rf.width = null;
}
return storeNode;
} else {
// add new element
return parseNode(propNode, state.nodeExtent);
}
});
const nextEdges = edgeElements.map((propEdge: Edge) => {
let storeEdge = state.edges.find((se) => se.id === propEdge.id);
if (storeEdge) {
return {
...storeEdge,
...propEdge,
};
} else {
return parseEdge(propEdge);
}
});
return { ...state, nodes: nextNodes, edges: nextEdges };
}
case constants.UPDATE_NODE_DIMENSIONS: {
const updatedNodes = state.nodes.map((node) => {
const update = action.payload.find((u) => u.id === node.id);
if (update) {
const dimensions = getDimensions(update.nodeElement);
if (
dimensions.width &&
dimensions.height &&
(node.__rf.width !== dimensions.width || node.__rf.height !== dimensions.height)
) {
const handleBounds = getHandleBounds(update.nodeElement, state.transform[2]);
return {
...node,
__rf: {
...node.__rf,
...dimensions,
handleBounds,
},
};
}
}
return node;
});
return {
...state,
nodes: updatedNodes,
};
}
case constants.UPDATE_NODE_POS: {
const { id, pos } = action.payload;
let position: XYPosition = pos;
if (state.snapToGrid) {
const [gridSizeX, gridSizeY] = state.snapGrid;
position = {
x: gridSizeX * Math.round(pos.x / gridSizeX),
y: gridSizeY * Math.round(pos.y / gridSizeY),
};
}
const nextNodes = state.nodes.map((node) => {
if (node.id === id) {
return {
...node,
__rf: {
...node.__rf,
position,
},
};
}
return node;
});
return { ...state, nodes: nextNodes };
}
case constants.UPDATE_NODE_POS_DIFF: {
const { id, diff, isDragging } = action.payload;
const nextNodes = state.nodes.map((node) => {
if (id === node.id || state.selectedElements?.find((sNode) => sNode.id === node.id)) {
if (diff) {
return {
...node,
__rf: {
...node.__rf,
isDragging,
position: {
x: node.__rf.position.x + diff.x,
y: node.__rf.position.y + diff.y,
},
},
};
}
return {
...node,
__rf: {
...node.__rf,
isDragging,
},
};
}
return node;
});
return { ...state, nodes: nextNodes };
}
case constants.SET_USER_SELECTION: {
const mousePos = action.payload;
const userSelectionRect = {
width: 0,
height: 0,
startX: mousePos.x,
startY: mousePos.y,
x: mousePos.x,
y: mousePos.y,
draw: true,
};
return {
...state,
userSelectionRect,
selectionActive: true,
};
}
case constants.UPDATE_USER_SELECTION: {
const mousePos = action.payload;
const startX = state.userSelectionRect.startX || 0;
const startY = state.userSelectionRect.startY || 0;
const negativeX = mousePos.x < startX;
const negativeY = mousePos.y < startY;
const nextUserSelectRect = {
...state.userSelectionRect,
x: negativeX ? mousePos.x : state.userSelectionRect.x,
y: negativeY ? mousePos.y : state.userSelectionRect.y,
width: Math.abs(mousePos.x - startX),
height: Math.abs(mousePos.y - startY),
};
const selectedNodes = getNodesInside(state.nodes, nextUserSelectRect, state.transform);
const selectedEdges = getConnectedEdges(selectedNodes, state.edges);
const nextSelectedElements = [...selectedNodes, ...selectedEdges];
const selectedElementsUpdated = !isEqual(nextSelectedElements, state.selectedElements);
if (selectedElementsUpdated) {
return {
...state,
selectedElements: nextSelectedElements.length > 0 ? nextSelectedElements : null,
userSelectionRect: nextUserSelectRect,
};
}
return {
...state,
userSelectionRect: nextUserSelectRect,
};
}
case constants.UNSET_USER_SELECTION: {
const selectedNodes = state.selectedElements?.filter((node) => isNode(node) && node.__rf) as Node[];
const selectionActive = false;
const userSelectionRect = {
...state.userSelectionRect,
draw: false,
};
if (!selectedNodes || selectedNodes.length === 0) {
return {
...state,
selectionActive,
userSelectionRect,
selectedElements: null,
nodesSelectionActive: false,
};
}
const selectedNodesBbox = getRectOfNodes(selectedNodes);
return {
...state,
selectionActive,
userSelectionRect,
selectedNodesBbox,
nodesSelectionActive: true,
};
}
case constants.SET_SELECTED_ELEMENTS: {
const elements = action.payload;
const selectedElementsArr = Array.isArray(elements) ? elements : [elements];
const selectedElementsUpdated = !isEqual(selectedElementsArr, state.selectedElements);
const selectedElements = selectedElementsUpdated ? selectedElementsArr : state.selectedElements;
return {
...state,
selectedElements,
};
}
case constants.ADD_SELECTED_ELEMENTS: {
const { multiSelectionActive, selectedElements } = state;
const elements = action.payload;
const selectedElementsArr = Array.isArray(elements) ? elements : [elements];
let nextElements = selectedElementsArr;
if (multiSelectionActive) {
nextElements = selectedElements ? [...selectedElements, ...selectedElementsArr] : selectedElementsArr;
}
const selectedElementsUpdated = !isEqual(nextElements, state.selectedElements);
const nextSelectedElements = selectedElementsUpdated ? nextElements : state.selectedElements;
return { ...state, selectedElements: nextSelectedElements };
}
case constants.INIT_D3ZOOM: {
const { d3Zoom, d3Selection, d3ZoomHandler, transform } = action.payload;
return {
...state,
d3Zoom,
d3Selection,
d3ZoomHandler,
transform,
};
}
case constants.SET_MINZOOM: {
const minZoom = action.payload;
if (state.d3Zoom) {
state.d3Zoom.scaleExtent([minZoom, state.maxZoom]);
}
return {
...state,
minZoom,
};
}
case constants.SET_MAXZOOM: {
const maxZoom = action.payload;
if (state.d3Zoom) {
state.d3Zoom.scaleExtent([state.minZoom, maxZoom]);
}
return {
...state,
maxZoom,
};
}
case constants.SET_TRANSLATEEXTENT: {
const translateExtent = action.payload;
if (state.d3Zoom) {
state.d3Zoom.translateExtent(translateExtent);
}
return {
...state,
translateExtent,
};
}
case constants.SET_ON_CONNECT:
case constants.SET_ON_CONNECT_START:
case constants.SET_ON_CONNECT_STOP:
case constants.SET_ON_CONNECT_END:
case constants.RESET_SELECTED_ELEMENTS:
case constants.UPDATE_TRANSFORM:
case constants.UPDATE_SIZE:
case constants.SET_CONNECTION_POSITION:
case constants.SET_CONNECTION_NODEID:
case constants.SET_SNAPTOGRID:
case constants.SET_SNAPGRID:
case constants.SET_INTERACTIVE:
case constants.SET_NODES_DRAGGABLE:
case constants.SET_NODES_CONNECTABLE:
case constants.SET_ELEMENTS_SELECTABLE:
case constants.SET_MULTI_SELECTION_ACTIVE:
case constants.SET_CONNECTION_MODE:
case constants.SET_NODE_EXTENT:
return { ...state, ...action.payload };
default:
return state;
}
}