Merge pull request #2277 from wbkd/v10.3.9

V10.3.9
This commit is contained in:
Moritz Klack
2022-07-14 15:54:37 +02:00
committed by GitHub
13 changed files with 104 additions and 100 deletions
+2
View File
@@ -9,6 +9,7 @@ import ReactFlow, {
useReactFlow, useReactFlow,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
const onNodeDrag = (_: MouseEvent, node: Node) => console.log('drag', node);
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
@@ -61,6 +62,7 @@ const BasicFlow = () => {
defaultEdges={initialEdges} defaultEdges={initialEdges}
onNodeClick={onNodeClick} onNodeClick={onNodeClick}
onNodeDragStop={onNodeDragStop} onNodeDragStop={onNodeDragStop}
onNodeDrag={onNodeDrag}
className="react-flow-basic-example" className="react-flow-basic-example"
minZoom={0.2} minZoom={0.2}
maxZoom={4} maxZoom={4}
+7 -9
View File
@@ -1,5 +1,5 @@
import React, { memo, useCallback, Dispatch, FC } from 'react'; import React, { memo, useCallback, Dispatch, FC } from 'react';
import { useReactFlow, ReactFlowInstance, Edge, Node, ReactFlowJsonObject } from 'react-flow-renderer'; import { useReactFlow, Edge, Node, ReactFlowJsonObject } from 'react-flow-renderer';
import localforage from 'localforage'; import localforage from 'localforage';
localforage.config({ localforage.config({
@@ -12,20 +12,17 @@ const flowKey = 'example-flow';
const getNodeId = () => `randomnode_${+new Date()}`; const getNodeId = () => `randomnode_${+new Date()}`;
type ControlsProps = { type ControlsProps = {
rfInstance?: ReactFlowInstance;
setNodes: Dispatch<React.SetStateAction<Node<any>[]>>; setNodes: Dispatch<React.SetStateAction<Node<any>[]>>;
setEdges: Dispatch<React.SetStateAction<Edge<any>[]>>; setEdges: Dispatch<React.SetStateAction<Edge<any>[]>>;
}; };
const Controls: FC<ControlsProps> = ({ rfInstance, setNodes, setEdges }) => { const Controls: FC<ControlsProps> = ({ setNodes, setEdges }) => {
const { setViewport } = useReactFlow(); const { setViewport, toObject } = useReactFlow();
const onSave = useCallback(() => { const onSave = useCallback(() => {
if (rfInstance) { const flow = toObject();
const flow = rfInstance.toObject(); localforage.setItem(flowKey, flow);
localforage.setItem(flowKey, flow); }, [toObject]);
}
}, [rfInstance]);
const onRestore = useCallback(() => { const onRestore = useCallback(() => {
const restoreFlow = async () => { const restoreFlow = async () => {
@@ -33,6 +30,7 @@ const Controls: FC<ControlsProps> = ({ rfInstance, setNodes, setEdges }) => {
if (flow) { if (flow) {
const { x, y, zoom } = flow.viewport; const { x, y, zoom } = flow.viewport;
setNodes(flow.nodes || []); setNodes(flow.nodes || []);
setEdges(flow.edges || []); setEdges(flow.edges || []);
setViewport({ x, y, zoom: zoom || 0 }); setViewport({ x, y, zoom: zoom || 0 });
+3 -6
View File
@@ -1,11 +1,10 @@
import { useState } from 'react'; import { useCallback } from 'react';
import ReactFlow, { import ReactFlow, {
ReactFlowProvider, ReactFlowProvider,
Node, Node,
addEdge, addEdge,
Connection, Connection,
Edge, Edge,
ReactFlowInstance,
useNodesState, useNodesState,
useEdgesState, useEdgesState,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
@@ -22,10 +21,9 @@ const initialNodes: Node[] = [
const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }]; const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }];
const SaveRestore = () => { const SaveRestore = () => {
const [rfInstance, setRfInstance] = useState<ReactFlowInstance>();
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)); const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
return ( return (
<ReactFlowProvider> <ReactFlowProvider>
@@ -35,9 +33,8 @@ const SaveRestore = () => {
onNodesChange={onNodesChange} onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
onConnect={onConnect} onConnect={onConnect}
onInit={setRfInstance}
> >
<Controls rfInstance={rfInstance} setNodes={setNodes} setEdges={setEdges} /> <Controls setNodes={setNodes} setEdges={setEdges} />
</ReactFlow> </ReactFlow>
</ReactFlowProvider> </ReactFlowProvider>
); );
+2 -2
View File
@@ -11,7 +11,7 @@ const idStyle: CSSProperties = {
left: 2, left: 2,
}; };
const ColorSelectorNode: FC<NodeProps> = ({ zIndex, xPos, yPos, id }) => { const DebugNode: FC<NodeProps> = ({ zIndex, xPos, yPos, id }) => {
return ( return (
<> <>
<Handle type="target" position={Position.Top} /> <Handle type="target" position={Position.Top} />
@@ -24,4 +24,4 @@ const ColorSelectorNode: FC<NodeProps> = ({ zIndex, xPos, yPos, id }) => {
); );
}; };
export default memo(ColorSelectorNode); export default memo(DebugNode);
+3
View File
@@ -15,6 +15,7 @@ import ReactFlow, {
} from 'react-flow-renderer'; } from 'react-flow-renderer';
import DebugNode from './DebugNode'; import DebugNode from './DebugNode';
const onNodeDrag = (_: MouseEvent, node: Node, nodes: Node[]) => console.log('drag', node, nodes);
const onNodeDragStop = (_: MouseEvent, node: Node, nodes: Node[]) => console.log('drag stop', node, nodes); const onNodeDragStop = (_: MouseEvent, node: Node, nodes: Node[]) => console.log('drag stop', node, nodes);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge); const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge);
@@ -82,6 +83,7 @@ const initialNodes: Node[] = [
position: { x: 25, y: 50 }, position: { x: 25, y: 50 },
className: 'light', className: 'light',
parentNode: '5', parentNode: '5',
extent: 'parent',
}, },
{ {
id: '5b', id: '5b',
@@ -167,6 +169,7 @@ const Subflow = () => {
onNodeClick={onNodeClick} onNodeClick={onNodeClick}
onEdgeClick={onEdgeClick} onEdgeClick={onEdgeClick}
onConnect={onConnect} onConnect={onConnect}
onNodeDrag={onNodeDrag}
onNodeDragStop={onNodeDragStop} onNodeDragStop={onNodeDragStop}
className="react-flow-basic-example" className="react-flow-basic-example"
defaultZoom={1.5} defaultZoom={1.5}
+19 -20
View File
@@ -46,6 +46,24 @@ export function checkElementBelowIsValid(
if (elementBelow && (elementBelowIsTarget || elementBelowIsSource)) { if (elementBelow && (elementBelowIsTarget || elementBelowIsSource)) {
result.isHoveringHandle = true; result.isHoveringHandle = true;
const elementBelowNodeId = elementBelow.getAttribute('data-nodeid');
const elementBelowHandleId = elementBelow.getAttribute('data-handleid');
const connection: Connection = isTarget
? {
source: elementBelowNodeId,
sourceHandle: elementBelowHandleId,
target: nodeId,
targetHandle: handleId,
}
: {
source: nodeId,
sourceHandle: handleId,
target: elementBelowNodeId,
targetHandle: elementBelowHandleId,
};
result.connection = connection;
// in strict mode we don't allow target to target or source to source connections // in strict mode we don't allow target to target or source to source connections
const isValid = const isValid =
connectionMode === ConnectionMode.Strict connectionMode === ConnectionMode.Strict
@@ -53,23 +71,6 @@ export function checkElementBelowIsValid(
: true; : true;
if (isValid) { if (isValid) {
const elementBelowNodeId = elementBelow.getAttribute('data-nodeid');
const elementBelowHandleId = elementBelow.getAttribute('data-handleid');
const connection: Connection = isTarget
? {
source: elementBelowNodeId,
sourceHandle: elementBelowHandleId,
target: nodeId,
targetHandle: handleId,
}
: {
source: nodeId,
sourceHandle: handleId,
target: elementBelowNodeId,
targetHandle: elementBelowHandleId,
};
result.connection = connection;
result.isValid = isValidConnection(connection); result.isValid = isValidConnection(connection);
} }
} }
@@ -151,9 +152,7 @@ export function handleMouseDown(
return resetRecentHandle(recentHoveredHandle); return resetRecentHandle(recentHoveredHandle);
} }
const isOwnHandle = connection.source === connection.target; if (connection.source !== connection.target && elementBelow) {
if (!isOwnHandle && elementBelow) {
recentHoveredHandle = elementBelow; recentHoveredHandle = elementBelow;
elementBelow.classList.add('react-flow__handle-connecting'); elementBelow.classList.add('react-flow__handle-connecting');
elementBelow.classList.toggle('react-flow__handle-valid', isValid); elementBelow.classList.toggle('react-flow__handle-valid', isValid);
+17 -22
View File
@@ -4,21 +4,17 @@ import { GetState, SetState } from 'zustand';
import { HandleElement, Node, Position, ReactFlowState } from '../../types'; import { HandleElement, Node, Position, ReactFlowState } from '../../types';
import { getDimensions } from '../../utils'; import { getDimensions } from '../../utils';
export const getHandleBounds = (nodeElement: HTMLDivElement, scale: number) => { function getTranslateValues(domNode: HTMLDivElement): [number, number] {
const bounds = nodeElement.getBoundingClientRect(); if (typeof window === 'undefined' || !window.DOMMatrixReadOnly) {
return [0, 0];
}
return { const style = window.getComputedStyle(domNode);
source: getHandleBoundsByHandleType('.source', nodeElement, bounds, scale), const { m41, m42 } = new window.DOMMatrixReadOnly(style.transform);
target: getHandleBoundsByHandleType('.target', nodeElement, bounds, scale), return [m41, m42];
}; }
};
export const getHandleBoundsByHandleType = ( export const getHandleBounds = (selector: string, nodeElement: HTMLDivElement): HandleElement[] | null => {
selector: string,
nodeElement: HTMLDivElement,
parentBounds: DOMRect,
k: number
): HandleElement[] | null => {
const handles = nodeElement.querySelectorAll(selector); const handles = nodeElement.querySelectorAll(selector);
if (!handles || !handles.length) { if (!handles || !handles.length) {
@@ -28,17 +24,16 @@ export const getHandleBoundsByHandleType = (
const handlesArray = Array.from(handles) as HTMLDivElement[]; const handlesArray = Array.from(handles) as HTMLDivElement[];
return handlesArray.map((handle): HandleElement => { return handlesArray.map((handle): HandleElement => {
const bounds = handle.getBoundingClientRect(); // we don't use getBoundingClientRect here, because it includes the transform of the parent (scaled viewport)
const dimensions = getDimensions(handle); // that we would then need to calculate out again in order to get the correct position.
const handleId = handle.getAttribute('data-handleid'); const [translateX, translateY] = getTranslateValues(handle);
const handlePosition = handle.getAttribute('data-handlepos') as unknown as Position;
return { return {
id: handleId, id: handle.getAttribute('data-handleid'),
position: handlePosition, position: handle.getAttribute('data-handlepos') as unknown as Position,
x: (bounds.left - parentBounds.left) / k, x: handle.offsetLeft + nodeElement.clientLeft + translateX,
y: (bounds.top - parentBounds.top) / k, y: handle.offsetTop + nodeElement.clientTop + translateY,
...dimensions, ...getDimensions(handle),
}; };
}); });
}; };
+18 -20
View File
@@ -54,6 +54,7 @@ const ZoomPane = ({
noPanClassName, noPanClassName,
}: ZoomPaneProps) => { }: ZoomPaneProps) => {
const store = useStoreApi(); const store = useStoreApi();
const isZoomingOrPanning = useRef(false);
const zoomPane = useRef<HTMLDivElement>(null); const zoomPane = useRef<HTMLDivElement>(null);
const prevTransform = useRef<Viewport>({ x: 0, y: 0, zoom: 0 }); const prevTransform = useRef<Viewport>({ x: 0, y: 0, zoom: 0 });
const { d3Zoom, d3Selection, d3ZoomHandler } = useStore(selector, shallow); const { d3Zoom, d3Selection, d3ZoomHandler } = useStore(selector, shallow);
@@ -146,12 +147,11 @@ const ZoomPane = ({
useEffect(() => { useEffect(() => {
if (d3Zoom) { if (d3Zoom) {
if (selectionKeyPressed) { if (selectionKeyPressed && !isZoomingOrPanning.current) {
d3Zoom.on('zoom', null); d3Zoom.on('zoom', null);
} else { } else if (!selectionKeyPressed) {
d3Zoom.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => { d3Zoom.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => {
store.setState({ transform: [event.transform.x, event.transform.y, event.transform.k] }); store.setState({ transform: [event.transform.x, event.transform.y, event.transform.k] });
if (onMove) { if (onMove) {
const flowTransform = eventToFlowTransform(event.transform); const flowTransform = eventToFlowTransform(event.transform);
onMove(event.sourceEvent as MouseEvent | TouchEvent, flowTransform); onMove(event.sourceEvent as MouseEvent | TouchEvent, flowTransform);
@@ -163,33 +163,31 @@ const ZoomPane = ({
useEffect(() => { useEffect(() => {
if (d3Zoom) { if (d3Zoom) {
if (onMoveStart) { d3Zoom.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => {
d3Zoom.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => { isZoomingOrPanning.current = true;
if (onMoveStart) {
const flowTransform = eventToFlowTransform(event.transform); const flowTransform = eventToFlowTransform(event.transform);
prevTransform.current = flowTransform; prevTransform.current = flowTransform;
onMoveStart(event.sourceEvent as MouseEvent | TouchEvent, flowTransform); onMoveStart(event.sourceEvent as MouseEvent | TouchEvent, flowTransform);
}); }
} else { });
d3Zoom.on('start', null);
}
} }
}, [d3Zoom, onMoveStart]); }, [d3Zoom, onMoveStart]);
useEffect(() => { useEffect(() => {
if (d3Zoom) { if (d3Zoom) {
if (onMoveEnd) { d3Zoom.on('end', (event: D3ZoomEvent<HTMLDivElement, any>) => {
d3Zoom.on('end', (event: D3ZoomEvent<HTMLDivElement, any>) => { isZoomingOrPanning.current = false;
if (viewChanged(prevTransform.current, event.transform)) {
const flowTransform = eventToFlowTransform(event.transform);
prevTransform.current = flowTransform;
onMoveEnd(event.sourceEvent as MouseEvent | TouchEvent, flowTransform); if (onMoveEnd && viewChanged(prevTransform.current, event.transform)) {
} const flowTransform = eventToFlowTransform(event.transform);
}); prevTransform.current = flowTransform;
} else {
d3Zoom.on('end', null); onMoveEnd(event.sourceEvent as MouseEvent | TouchEvent, flowTransform);
} }
});
} }
}, [d3Zoom, onMoveEnd]); }, [d3Zoom, onMoveEnd]);
+1
View File
@@ -77,6 +77,7 @@ function useDrag({
} }
const pointerPos = getPointerPosition(event); const pointerPos = getPointerPosition(event);
lastPos.current = pointerPos;
dragItems.current = getDragItems(nodeInternals, pointerPos, nodeId); dragItems.current = getDragItems(nodeInternals, pointerPos, nodeId);
if (onStart && dragItems.current) { if (onStart && dragItems.current) {
+20 -3
View File
@@ -39,7 +39,8 @@ export function getDragItems(nodeInternals: NodeInternals, mousePos: XYPosition,
.filter((n) => (n.selected || n.id === nodeId) && (!n.parentNode || !isParentSelected(n, nodeInternals))) .filter((n) => (n.selected || n.id === nodeId) && (!n.parentNode || !isParentSelected(n, nodeInternals)))
.map((n) => ({ .map((n) => ({
id: n.id, id: n.id,
position: n.positionAbsolute || { x: 0, y: 0 }, position: n.position || { x: 0, y: 0 },
positionAbsolute: n.positionAbsolute || { x: 0, y: 0 },
distance: { distance: {
x: mousePos.x - (n.positionAbsolute?.x ?? 0), x: mousePos.x - (n.positionAbsolute?.x ?? 0),
y: mousePos.y - (n.positionAbsolute?.y ?? 0), y: mousePos.y - (n.positionAbsolute?.y ?? 0),
@@ -94,14 +95,28 @@ export function updatePosition(
]; ];
} }
dragItem.position = currentExtent ? clampPosition(nextPosition, currentExtent as CoordinateExtent) : nextPosition; let parentPosition = { x: 0, y: 0 };
if (dragItem.parentNode) {
const parentNode = nodeInternals.get(dragItem.parentNode);
parentPosition = { x: parentNode?.positionAbsolute?.x ?? 0, y: parentNode?.positionAbsolute?.y ?? 0 };
}
dragItem.positionAbsolute = currentExtent
? clampPosition(nextPosition, currentExtent as CoordinateExtent)
: nextPosition;
dragItem.position = {
x: dragItem.positionAbsolute.x - parentPosition.x,
y: dragItem.positionAbsolute.y - parentPosition.y,
};
return dragItem; return dragItem;
} }
// returns two params: // returns two params:
// 1. the dragged node (or the first of the list, if we are dragging a node selection) // 1. the dragged node (or the first of the list, if we are dragging a node selection)
// 2. array of selected nodes (handy when multi selection is active) // 2. array of selected nodes (for multi selections)
export function getEventHandlerParams({ export function getEventHandlerParams({
nodeId, nodeId,
dragItems, dragItems,
@@ -116,6 +131,8 @@ export function getEventHandlerParams({
return { return {
...node, ...node,
position: n.position,
positionAbsolute: n.positionAbsolute,
}; };
}); });
+5 -5
View File
@@ -38,12 +38,12 @@ const useViewportHelper = (): ViewportHelperFunctions => {
zoomIn: (options) => d3Zoom.scaleBy(getD3Transition(d3Selection, options?.duration), 1.2), zoomIn: (options) => d3Zoom.scaleBy(getD3Transition(d3Selection, options?.duration), 1.2),
zoomOut: (options) => d3Zoom.scaleBy(getD3Transition(d3Selection, options?.duration), 1 / 1.2), zoomOut: (options) => d3Zoom.scaleBy(getD3Transition(d3Selection, options?.duration), 1 / 1.2),
zoomTo: (zoomLevel, options) => d3Zoom.scaleTo(getD3Transition(d3Selection, options?.duration), zoomLevel), zoomTo: (zoomLevel, options) => d3Zoom.scaleTo(getD3Transition(d3Selection, options?.duration), zoomLevel),
getZoom: () => { getZoom: () => store.getState().transform[2],
const [, , zoom] = store.getState().transform;
return zoom;
},
setViewport: (transform, options) => { setViewport: (transform, options) => {
const nextTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.zoom); const [x, y, zoom] = store.getState().transform;
const nextTransform = zoomIdentity
.translate(transform.x ?? x, transform.y ?? y)
.scale(transform.zoom ?? zoom);
d3Zoom.transform(getD3Transition(d3Selection, options?.duration), nextTransform); d3Zoom.transform(getD3Transition(d3Selection, options?.duration), nextTransform);
}, },
getViewport: () => { getViewport: () => {
+6 -12
View File
@@ -43,7 +43,7 @@ const createStore = () =>
set({ nodeInternals, edges: nextEdges, hasDefaultNodes, hasDefaultEdges }); set({ nodeInternals, edges: nextEdges, hasDefaultNodes, hasDefaultEdges });
}, },
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => { updateNodeDimensions: (updates: NodeDimensionUpdate[]) => {
const { onNodesChange, transform, nodeInternals, fitViewOnInit, fitViewOnInitDone, fitViewOnInitOptions } = get(); const { onNodesChange, nodeInternals, fitViewOnInit, fitViewOnInitDone, fitViewOnInitOptions } = get();
const changes: NodeDimensionChange[] = updates.reduce<NodeDimensionChange[]>((res, update) => { const changes: NodeDimensionChange[] = updates.reduce<NodeDimensionChange[]>((res, update) => {
const node = nodeInternals.get(update.id); const node = nodeInternals.get(update.id);
@@ -57,12 +57,14 @@ const createStore = () =>
); );
if (doUpdate) { if (doUpdate) {
const handleBounds = getHandleBounds(update.nodeElement, transform[2]);
nodeInternals.set(node.id, { nodeInternals.set(node.id, {
...node, ...node,
[internalsSymbol]: { [internalsSymbol]: {
...node[internalsSymbol], ...node[internalsSymbol],
handleBounds, handleBounds: {
source: getHandleBounds('.source', update.nodeElement),
target: getHandleBounds('.target', update.nodeElement),
},
}, },
...dimensions, ...dimensions,
}); });
@@ -99,16 +101,8 @@ const createStore = () =>
}; };
if (positionChanged) { if (positionChanged) {
change.positionAbsolute = node.position; change.positionAbsolute = node.positionAbsolute;
change.position = node.position; change.position = node.position;
if (node.parentNode) {
const parentNode = nodeInternals.get(node.parentNode);
change.position = {
x: change.position.x - (parentNode?.positionAbsolute?.x ?? 0),
y: change.position.y - (parentNode?.positionAbsolute?.y ?? 0),
};
}
} }
return change; return change;
+1 -1
View File
@@ -110,8 +110,8 @@ export type NodeBounds = XYPosition & {
export type NodeDragItem = { export type NodeDragItem = {
id: string; id: string;
// relative node position
position: XYPosition; position: XYPosition;
positionAbsolute: XYPosition;
// distance from the mouse cursor to the node when start dragging // distance from the mouse cursor to the node when start dragging
distance: XYPosition; distance: XYPosition;
width?: number | null; width?: number | null;