@@ -12,7 +12,7 @@ React Flow is a library for building node-based graphs. You can easily implement
|
|||||||
- [Options](#options-1)
|
- [Options](#options-1)
|
||||||
- [Node Types & Custom Nodes](#node-types--custom-nodes)
|
- [Node Types & Custom Nodes](#node-types--custom-nodes)
|
||||||
- [Handle Component](#handle-component)
|
- [Handle Component](#handle-component)
|
||||||
- [Edges](#nodes)
|
- [Edges](#edges)
|
||||||
- [Options](#options)
|
- [Options](#options)
|
||||||
- [Edge Types & Custom Edges](#edge-types--custom-edges)
|
- [Edge Types & Custom Edges](#edge-types--custom-edges)
|
||||||
- [Components](#components)
|
- [Components](#components)
|
||||||
|
|||||||
+3
-2
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"baseUrl": "http://localhost:3000",
|
"baseUrl": "http://localhost:3000",
|
||||||
"viewportWidth": 1280,
|
"viewportWidth": 1280,
|
||||||
"viewportHeight": 720
|
"viewportHeight": 720,
|
||||||
}
|
"video": false
|
||||||
|
}
|
||||||
+2
-2
@@ -18,7 +18,7 @@
|
|||||||
"build:example": "npm install && npm run build && cd example && npm install && npm run build",
|
"build:example": "npm install && npm run build && cd example && npm install && npm run build",
|
||||||
"cy:open": "cypress open",
|
"cy:open": "cypress open",
|
||||||
"cypress": "npm run dev:wait cy:open",
|
"cypress": "npm run dev:wait cy:open",
|
||||||
"test:chrome": "cypress run --browser chrome",
|
"test:chrome": "cypress run --browser chrome --headless",
|
||||||
"test:firefox": "cypress run --browser firefox",
|
"test:firefox": "cypress run --browser firefox",
|
||||||
"test:all": "npm run test:chrome && npm run test:firefox",
|
"test:all": "npm run test:chrome && npm run test:firefox",
|
||||||
"test": "npm run dev:wait test:chrome",
|
"test": "npm run dev:wait test:chrome",
|
||||||
@@ -74,4 +74,4 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -95,8 +95,8 @@ const onDrag = ({ evt, setDragging, id, offset, transform, updateNodePos }: OnDr
|
|||||||
const dragEvt = getMouseEvent(evt);
|
const dragEvt = getMouseEvent(evt);
|
||||||
|
|
||||||
const scaledClient = {
|
const scaledClient = {
|
||||||
x: dragEvt.clientX * (1 / transform[2]),
|
x: dragEvt.clientX / transform[2],
|
||||||
y: dragEvt.clientY * (1 / transform[2]),
|
y: dragEvt.clientY / transform[2],
|
||||||
};
|
};
|
||||||
|
|
||||||
setDragging(true);
|
setDragging(true);
|
||||||
|
|||||||
@@ -30,16 +30,16 @@ function getStartPositions(nodes: Node[]): StartPositions {
|
|||||||
export default () => {
|
export default () => {
|
||||||
const [offset, setOffset] = useState<XYPosition>({ x: 0, y: 0 });
|
const [offset, setOffset] = useState<XYPosition>({ x: 0, y: 0 });
|
||||||
const [startPositions, setStartPositions] = useState<StartPositions>({});
|
const [startPositions, setStartPositions] = useState<StartPositions>({});
|
||||||
const [tX, tY, tScale] = useStoreState((s) => s.transform);
|
|
||||||
const selectedNodesBbox = useStoreState((s) => s.selectedNodesBbox);
|
|
||||||
const selectedElements = useStoreState((s) => s.selectedElements);
|
|
||||||
const snapToGrid = useStoreState((s) => s.snapToGrid);
|
|
||||||
const snapGrid = useStoreState((s) => s.snapGrid);
|
|
||||||
const nodes = useStoreState((s) => s.nodes);
|
|
||||||
|
|
||||||
const updateNodePos = useStoreActions((a) => a.updateNodePos);
|
const [tX, tY, tScale] = useStoreState((state) => state.transform);
|
||||||
|
const selectedNodesBbox = useStoreState((state) => state.selectedNodesBbox);
|
||||||
|
const selectedElements = useStoreState((state) => state.selectedElements);
|
||||||
|
const snapToGrid = useStoreState((state) => state.snapToGrid);
|
||||||
|
const snapGrid = useStoreState((state) => state.snapGrid);
|
||||||
|
const nodes = useStoreState((state) => state.nodes);
|
||||||
|
|
||||||
|
const updateNodePos = useStoreActions((actions) => actions.updateNodePos);
|
||||||
|
|
||||||
const position = selectedNodesBbox;
|
|
||||||
const grid = (snapToGrid ? snapGrid : [1, 1])! as [number, number];
|
const grid = (snapToGrid ? snapGrid : [1, 1])! as [number, number];
|
||||||
|
|
||||||
if (!selectedElements) {
|
if (!selectedElements) {
|
||||||
@@ -51,8 +51,8 @@ export default () => {
|
|||||||
x: evt.clientX / tScale,
|
x: evt.clientX / tScale,
|
||||||
y: evt.clientY / tScale,
|
y: evt.clientY / tScale,
|
||||||
};
|
};
|
||||||
const offsetX: number = scaledClient.x - position.x - tX;
|
const offsetX: number = scaledClient.x - selectedNodesBbox.x - tX;
|
||||||
const offsetY: number = scaledClient.y - position.y - tY;
|
const offsetY: number = scaledClient.y - selectedNodesBbox.y - tY;
|
||||||
const selectedNodes = selectedElements
|
const selectedNodes = selectedElements
|
||||||
? selectedElements
|
? selectedElements
|
||||||
.filter(isNode)
|
.filter(isNode)
|
||||||
@@ -76,8 +76,8 @@ export default () => {
|
|||||||
if (selectedElements) {
|
if (selectedElements) {
|
||||||
selectedElements.filter(isNode).forEach((node) => {
|
selectedElements.filter(isNode).forEach((node) => {
|
||||||
const pos: XYPosition = {
|
const pos: XYPosition = {
|
||||||
x: startPositions[node.id].x + scaledClient.x - position.x - offset.x - tX,
|
x: startPositions[node.id].x + scaledClient.x - selectedNodesBbox.x - offset.x - tX,
|
||||||
y: startPositions[node.id].y + scaledClient.y - position.y - offset.y - tY,
|
y: startPositions[node.id].y + scaledClient.y - selectedNodesBbox.y - offset.y - tY,
|
||||||
};
|
};
|
||||||
|
|
||||||
updateNodePos({ id: node.id, pos });
|
updateNodePos({ id: node.id, pos });
|
||||||
|
|||||||
@@ -155,9 +155,7 @@ function renderEdge(
|
|||||||
targetPosition
|
targetPosition
|
||||||
);
|
);
|
||||||
|
|
||||||
const isSelected = selectedElements
|
const isSelected = selectedElements ? selectedElements.some((elm) => isEdge(elm) && elm.id === edge.id) : false;
|
||||||
? (selectedElements as Edge[]).some((elm) => isEdge(elm) && elm.source === sourceId && elm.target === targetId)
|
|
||||||
: false;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EdgeComponent
|
<EdgeComponent
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ const GraphView = ({
|
|||||||
const rendererNode = useRef<HTMLDivElement>(null);
|
const rendererNode = useRef<HTMLDivElement>(null);
|
||||||
const d3Initialised = useStoreState((state) => state.d3Initialised);
|
const d3Initialised = useStoreState((state) => state.d3Initialised);
|
||||||
const nodesSelectionActive = useStoreState((state) => state.nodesSelectionActive);
|
const nodesSelectionActive = useStoreState((state) => state.nodesSelectionActive);
|
||||||
const setNodesSelection = useStoreActions((actions) => actions.setNodesSelection);
|
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
|
||||||
const setOnConnect = useStoreActions((actions) => actions.setOnConnect);
|
const setOnConnect = useStoreActions((actions) => actions.setOnConnect);
|
||||||
const setOnConnectStart = useStoreActions((actions) => actions.setOnConnectStart);
|
const setOnConnectStart = useStoreActions((actions) => actions.setOnConnectStart);
|
||||||
const setOnConnectStop = useStoreActions((actions) => actions.setOnConnectStop);
|
const setOnConnectStop = useStoreActions((actions) => actions.setOnConnectStop);
|
||||||
@@ -127,7 +127,7 @@ const GraphView = ({
|
|||||||
|
|
||||||
const onZoomPaneClick = useCallback(() => {
|
const onZoomPaneClick = useCallback(() => {
|
||||||
onPaneClick?.();
|
onPaneClick?.();
|
||||||
setNodesSelection({ isActive: false });
|
unsetNodesSelection();
|
||||||
}, [onPaneClick]);
|
}, [onPaneClick]);
|
||||||
|
|
||||||
useResizeHandler(rendererNode);
|
useResizeHandler(rendererNode);
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ interface HookParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default ({ deleteKeyCode, onElementsRemove }: HookParams): void => {
|
export default ({ deleteKeyCode, onElementsRemove }: HookParams): void => {
|
||||||
const selectedElements = useStoreState((s) => s.selectedElements);
|
const selectedElements = useStoreState((state) => state.selectedElements);
|
||||||
const edges = useStoreState((s) => s.edges);
|
const edges = useStoreState((state) => state.edges);
|
||||||
|
|
||||||
const setNodesSelection = useStoreActions((a) => a.setNodesSelection);
|
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
|
||||||
const deleteKeyPressed = useKeyPress(deleteKeyCode);
|
const deleteKeyPressed = useKeyPress(deleteKeyCode);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -29,7 +29,7 @@ export default ({ deleteKeyCode, onElementsRemove }: HookParams): void => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onElementsRemove(elementsToRemove);
|
onElementsRemove(elementsToRemove);
|
||||||
setNodesSelection({ isActive: false });
|
unsetNodesSelection();
|
||||||
}
|
}
|
||||||
}, [deleteKeyPressed]);
|
}, [deleteKeyPressed]);
|
||||||
};
|
};
|
||||||
|
|||||||
+11
-38
@@ -38,11 +38,6 @@ type NodeDimensionUpdate = {
|
|||||||
nodeElement: HTMLDivElement;
|
nodeElement: HTMLDivElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SelectionUpdate = {
|
|
||||||
isActive: boolean;
|
|
||||||
selection?: SelectionRect;
|
|
||||||
};
|
|
||||||
|
|
||||||
type SetMinMaxZoom = {
|
type SetMinMaxZoom = {
|
||||||
minZoom: number;
|
minZoom: number;
|
||||||
maxZoom: number;
|
maxZoom: number;
|
||||||
@@ -72,7 +67,6 @@ export interface StoreModel {
|
|||||||
|
|
||||||
nodesSelectionActive: boolean;
|
nodesSelectionActive: boolean;
|
||||||
selectionActive: boolean;
|
selectionActive: boolean;
|
||||||
selection: SelectionRect | null;
|
|
||||||
|
|
||||||
userSelectionRect: SelectionRect;
|
userSelectionRect: SelectionRect;
|
||||||
|
|
||||||
@@ -105,7 +99,7 @@ export interface StoreModel {
|
|||||||
|
|
||||||
setSelection: Action<StoreModel, boolean>;
|
setSelection: Action<StoreModel, boolean>;
|
||||||
|
|
||||||
setNodesSelection: Action<StoreModel, SelectionUpdate>;
|
unsetNodesSelection: Action<StoreModel>;
|
||||||
|
|
||||||
setSelectedElements: Action<StoreModel, Elements | Node | Edge>;
|
setSelectedElements: Action<StoreModel, Elements | Node | Edge>;
|
||||||
|
|
||||||
@@ -159,7 +153,6 @@ export const storeModel: StoreModel = {
|
|||||||
|
|
||||||
nodesSelectionActive: false,
|
nodesSelectionActive: false,
|
||||||
selectionActive: false,
|
selectionActive: false,
|
||||||
selection: null,
|
|
||||||
|
|
||||||
userSelectionRect: {
|
userSelectionRect: {
|
||||||
startX: 0,
|
startX: 0,
|
||||||
@@ -198,7 +191,6 @@ export const storeModel: StoreModel = {
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
updateNodeDimensions: action((state, { id, nodeElement }) => {
|
updateNodeDimensions: action((state, { id, nodeElement }) => {
|
||||||
const bounds = nodeElement.getBoundingClientRect();
|
|
||||||
const dimensions = getDimensions(nodeElement);
|
const dimensions = getDimensions(nodeElement);
|
||||||
const matchingNode = state.nodes.find((n) => n.id === id);
|
const matchingNode = state.nodes.find((n) => n.id === id);
|
||||||
|
|
||||||
@@ -210,6 +202,7 @@ export const storeModel: StoreModel = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bounds = nodeElement.getBoundingClientRect();
|
||||||
const handleBounds = {
|
const handleBounds = {
|
||||||
source: getHandleBounds('.source', nodeElement, bounds, state.transform[2]),
|
source: getHandleBounds('.source', nodeElement, bounds, state.transform[2]),
|
||||||
target: getHandleBounds('.target', nodeElement, bounds, state.transform[2]),
|
target: getHandleBounds('.target', nodeElement, bounds, state.transform[2]),
|
||||||
@@ -231,7 +224,6 @@ export const storeModel: StoreModel = {
|
|||||||
|
|
||||||
if (state.snapToGrid) {
|
if (state.snapToGrid) {
|
||||||
const [gridSizeX, gridSizeY] = state.snapGrid;
|
const [gridSizeX, gridSizeY] = state.snapGrid;
|
||||||
|
|
||||||
position = {
|
position = {
|
||||||
x: gridSizeX * Math.round(pos.x / gridSizeX),
|
x: gridSizeX * Math.round(pos.x / gridSizeX),
|
||||||
y: gridSizeY * Math.round(pos.y / gridSizeY),
|
y: gridSizeY * Math.round(pos.y / gridSizeY),
|
||||||
@@ -281,7 +273,6 @@ export const storeModel: StoreModel = {
|
|||||||
const nextSelectedElements = [...selectedNodes, ...selectedEdges];
|
const nextSelectedElements = [...selectedNodes, ...selectedEdges];
|
||||||
const selectedElementsUpdated = !isEqual(nextSelectedElements, state.selectedElements);
|
const selectedElementsUpdated = !isEqual(nextSelectedElements, state.selectedElements);
|
||||||
|
|
||||||
state.selection = nextRect;
|
|
||||||
state.userSelectionRect = nextRect;
|
state.userSelectionRect = nextRect;
|
||||||
|
|
||||||
if (selectedElementsUpdated) {
|
if (selectedElementsUpdated) {
|
||||||
@@ -303,7 +294,6 @@ export const storeModel: StoreModel = {
|
|||||||
|
|
||||||
const selectedNodesBbox = getRectOfNodes(selectedNodes);
|
const selectedNodesBbox = getRectOfNodes(selectedNodes);
|
||||||
|
|
||||||
state.selection = state.userSelectionRect;
|
|
||||||
state.nodesSelectionActive = true;
|
state.nodesSelectionActive = true;
|
||||||
state.selectedNodesBbox = selectedNodesBbox;
|
state.selectedNodesBbox = selectedNodesBbox;
|
||||||
|
|
||||||
@@ -315,27 +305,9 @@ export const storeModel: StoreModel = {
|
|||||||
state.selectionActive = isActive;
|
state.selectionActive = isActive;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setNodesSelection: action((state, { isActive, selection }) => {
|
unsetNodesSelection: action((state) => {
|
||||||
if (!isActive || typeof selection === 'undefined') {
|
state.nodesSelectionActive = false;
|
||||||
state.nodesSelectionActive = false;
|
state.selectedElements = null;
|
||||||
state.selectedElements = null;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const selectedNodes = getNodesInside(state.nodes, selection, state.transform);
|
|
||||||
|
|
||||||
if (!selectedNodes.length) {
|
|
||||||
state.nodesSelectionActive = false;
|
|
||||||
state.selectedElements = null;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedNodesBbox = getRectOfNodes(selectedNodes);
|
|
||||||
|
|
||||||
state.selection = selection;
|
|
||||||
state.nodesSelectionActive = true;
|
|
||||||
state.selectedNodesBbox = selectedNodesBbox;
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setSelectedElements: action((state, elements) => {
|
setSelectedElements: action((state, elements) => {
|
||||||
@@ -424,19 +396,20 @@ export const storeModel: StoreModel = {
|
|||||||
|
|
||||||
fitView: action((state, payload = { padding: 0.1 }) => {
|
fitView: action((state, payload = { padding: 0.1 }) => {
|
||||||
const { padding } = payload;
|
const { padding } = payload;
|
||||||
const { nodes, width, height, d3Selection, d3Zoom } = state;
|
const { nodes, width, height, d3Selection, minZoom, maxZoom } = state;
|
||||||
|
|
||||||
if (!d3Selection || !d3Zoom || !nodes.length) {
|
if (!d3Selection || !nodes.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bounds = getRectOfNodes(nodes);
|
const bounds = getRectOfNodes(nodes);
|
||||||
const maxBoundsSize = Math.max(bounds.width, bounds.height);
|
const maxBoundsSize = Math.max(bounds.width, bounds.height);
|
||||||
const k = Math.min(width, height) / (maxBoundsSize + maxBoundsSize * padding);
|
const zoom = Math.min(width, height) / (maxBoundsSize + maxBoundsSize * padding);
|
||||||
|
const clampedZoom = clamp(zoom, minZoom, maxZoom);
|
||||||
const boundsCenterX = bounds.x + bounds.width / 2;
|
const boundsCenterX = bounds.x + bounds.width / 2;
|
||||||
const boundsCenterY = bounds.y + bounds.height / 2;
|
const boundsCenterY = bounds.y + bounds.height / 2;
|
||||||
const transform = [width / 2 - boundsCenterX * k, height / 2 - boundsCenterY * k];
|
const transform = [width / 2 - boundsCenterX * clampedZoom, height / 2 - boundsCenterY * clampedZoom];
|
||||||
const fittedTransform = zoomIdentity.translate(transform[0], transform[1]).scale(k);
|
const fittedTransform = zoomIdentity.translate(transform[0], transform[1]).scale(clampedZoom);
|
||||||
|
|
||||||
// we need to sync the d3 zoom transform with the fitted transform
|
// we need to sync the d3 zoom transform with the fitted transform
|
||||||
d3Selection.property('__zoom', fittedTransform);
|
d3Selection.property('__zoom', fittedTransform);
|
||||||
|
|||||||
Reference in New Issue
Block a user