Merge pull request #4348 from xyflow/next

React Flow 12.0.0-next.20 and Svelte Flow 0.1.5
This commit is contained in:
Moritz Klack
2024-06-06 17:34:14 +02:00
committed by GitHub
18 changed files with 7273 additions and 5743 deletions
@@ -28,10 +28,15 @@ for (let i = 0; i < 100; i++) {
}); });
} }
const initEdges: Edge[] = []; const initEdges: Edge[] = initNodes.reduce<Edge[]>((res, node, index) => {
if (index > 0) {
res.push({ id: `${index - 1}-${index}`, source: (index - 1).toString(), target: index.toString() });
}
return res;
}, []);
const CustomNodeFlow = () => { const CustomNodeFlow = () => {
const { setNodes, updateNodeData } = useReactFlow(); const { setNodes, updateNodeData, updateEdge } = useReactFlow();
const [nodes, , onNodesChange] = useNodesState(initNodes); const [nodes, , onNodesChange] = useNodesState(initNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges); const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
@@ -55,6 +60,10 @@ const CustomNodeFlow = () => {
nodes.forEach((node) => updateNodeData(node.id, { label: 'node update' })); nodes.forEach((node) => updateNodeData(node.id, { label: 'node update' }));
}; };
const multiUpdateEdges = () => {
edges.forEach((edge) => updateEdge(edge.id, { label: 'edge update' }));
};
return ( return (
<ReactFlow <ReactFlow
nodes={nodes} nodes={nodes}
@@ -70,6 +79,7 @@ const CustomNodeFlow = () => {
<Panel> <Panel>
<button onClick={multiSetNodes}>set nodes</button> <button onClick={multiSetNodes}>set nodes</button>
<button onClick={multiUpdateNodes}>update nodes</button> <button onClick={multiUpdateNodes}>update nodes</button>
<button onClick={multiUpdateEdges}>update edges</button>
</Panel> </Panel>
</ReactFlow> </ReactFlow>
); );
+8
View File
@@ -1,5 +1,13 @@
# @xyflow/react # @xyflow/react
## 12.0.0-next.20
- add `updateEdge` and `updateEdgeData` helpers to `useReactFlow`
- enable dynamic edge label updates
- prevent zooming on mobile if zoomOnPinch is false
- add straight edge to path built-in-types
- abort drag when multiple touches are detected
## 12.0.0-next.19 ## 12.0.0-next.19
- update internals on node resizer updates - update internals on node resizer updates
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/react", "name": "@xyflow/react",
"version": "12.0.0-next.19", "version": "12.0.0-next.20",
"description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.", "description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.",
"keywords": [ "keywords": [
"react", "react",
@@ -1,4 +1,4 @@
import { memo, useState, useCallback } from 'react'; import { memo, useState, useEffect, useRef } from 'react';
import cc from 'classcat'; import cc from 'classcat';
import type { Rect } from '@xyflow/system'; import type { Rect } from '@xyflow/system';
@@ -19,19 +19,20 @@ function EdgeTextComponent({
}: EdgeTextProps) { }: EdgeTextProps) {
const [edgeTextBbox, setEdgeTextBbox] = useState<Rect>({ x: 1, y: 0, width: 0, height: 0 }); const [edgeTextBbox, setEdgeTextBbox] = useState<Rect>({ x: 1, y: 0, width: 0, height: 0 });
const edgeTextClasses = cc(['react-flow__edge-textwrapper', className]); const edgeTextClasses = cc(['react-flow__edge-textwrapper', className]);
const edgeTextRef = useRef<SVGTextElement | null>(null);
const onEdgeTextRefChange = useCallback((edgeRef: SVGTextElement) => { useEffect(() => {
if (edgeRef === null) return; if (edgeTextRef.current) {
const textBbox = edgeTextRef.current.getBBox();
const textBbox = edgeRef.getBBox(); setEdgeTextBbox({
x: textBbox.x,
setEdgeTextBbox({ y: textBbox.y,
x: textBbox.x, width: textBbox.width,
y: textBbox.y, height: textBbox.height,
width: textBbox.width, });
height: textBbox.height, }
}); }, [label]);
}, []);
if (typeof label === 'undefined' || !label) { if (typeof label === 'undefined' || !label) {
return null; return null;
@@ -60,7 +61,7 @@ function EdgeTextComponent({
className="react-flow__edge-text" className="react-flow__edge-text"
y={edgeTextBbox.height / 2} y={edgeTextBbox.height / 2}
dy="0.3em" dy="0.3em"
ref={onEdgeTextRefChange} ref={edgeTextRef}
style={labelStyle} style={labelStyle}
> >
{label} {label}
+34 -4
View File
@@ -13,7 +13,7 @@ import {
import useViewportHelper from './useViewportHelper'; import useViewportHelper from './useViewportHelper';
import { useStore, useStoreApi } from './useStore'; import { useStore, useStoreApi } from './useStore';
import { useBatchContext } from '../components/BatchProvider'; import { useBatchContext } from '../components/BatchProvider';
import { elementToRemoveChange, isNode } from '../utils'; import { elementToRemoveChange, isEdge, isNode } from '../utils';
import type { ReactFlowInstance, Node, Edge, InternalNode, ReactFlowState, GeneralHelpers } from '../types'; import type { ReactFlowInstance, Node, Edge, InternalNode, ReactFlowState, GeneralHelpers } from '../types';
const selector = (s: ReactFlowState) => !!s.panZoom; const selector = (s: ReactFlowState) => !!s.panZoom;
@@ -41,6 +41,10 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
batchContext.nodeQueue.push(payload as NodeType[]); batchContext.nodeQueue.push(payload as NodeType[]);
}; };
const setEdges: GeneralHelpers<NodeType, EdgeType>['setEdges'] = (payload) => {
batchContext.edgeQueue.push(payload as EdgeType[]);
};
const getNodeRect = (node: NodeType | { id: string }): Rect | null => { const getNodeRect = (node: NodeType | { id: string }): Rect | null => {
const { nodeLookup, nodeOrigin } = store.getState(); const { nodeLookup, nodeOrigin } = store.getState();
@@ -77,6 +81,23 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
); );
}; };
const updateEdge: GeneralHelpers<NodeType, EdgeType>['updateEdge'] = (
id,
edgeUpdate,
options = { replace: false }
) => {
setEdges((prevEdges) =>
prevEdges.map((edge) => {
if (edge.id === id) {
const nextEdge = typeof edgeUpdate === 'function' ? edgeUpdate(edge as EdgeType) : edgeUpdate;
return options.replace && isEdge(nextEdge) ? (nextEdge as EdgeType) : { ...edge, ...nextEdge };
}
return edge;
})
);
};
return { return {
getNodes: () => store.getState().nodes.map((n) => ({ ...n })) as NodeType[], getNodes: () => store.getState().nodes.map((n) => ({ ...n })) as NodeType[],
getNode: (id) => getInternalNode(id)?.internals.userNode as NodeType, getNode: (id) => getInternalNode(id)?.internals.userNode as NodeType,
@@ -87,9 +108,7 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
}, },
getEdge: (id) => store.getState().edgeLookup.get(id) as EdgeType, getEdge: (id) => store.getState().edgeLookup.get(id) as EdgeType,
setNodes, setNodes,
setEdges: (payload) => { setEdges,
batchContext.edgeQueue.push(payload as EdgeType[]);
},
addNodes: (payload) => { addNodes: (payload) => {
const newNodes = Array.isArray(payload) ? payload : [payload]; const newNodes = Array.isArray(payload) ? payload : [payload];
batchContext.nodeQueue.push((nodes) => [...nodes, ...newNodes]); batchContext.nodeQueue.push((nodes) => [...nodes, ...newNodes]);
@@ -200,6 +219,17 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
options options
); );
}, },
updateEdge,
updateEdgeData: (id, dataUpdate, options = { replace: false }) => {
updateEdge(
id,
(edge) => {
const nextData = typeof dataUpdate === 'function' ? dataUpdate(edge) : dataUpdate;
return options.replace ? { ...edge, data: nextData } : { ...edge, data: { ...edge.data, ...nextData } };
},
options
);
},
}; };
}, []); }, []);
+3 -1
View File
@@ -60,7 +60,9 @@ type StepEdge<EdgeData extends Record<string, unknown> = Record<string, unknown>
pathOptions?: StepPathOptions; pathOptions?: StepPathOptions;
}; };
export type BuiltInEdge = SmoothStepEdge | BezierEdge | StepEdge; type StraightEdge<EdgeData extends Record<string, unknown> = Record<string, unknown>> = Edge<EdgeData, 'straight'>;
export type BuiltInEdge = SmoothStepEdge | BezierEdge | StepEdge | StraightEdge;
export type EdgeMouseHandler<EdgeType extends Edge = Edge> = (event: ReactMouseEvent, edge: EdgeType) => void; export type EdgeMouseHandler<EdgeType extends Edge = Edge> = (event: ReactMouseEvent, edge: EdgeType) => void;
+30
View File
@@ -143,6 +143,36 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
dataUpdate: Partial<NodeType['data']> | ((node: NodeType) => Partial<NodeType['data']>), dataUpdate: Partial<NodeType['data']> | ((node: NodeType) => Partial<NodeType['data']>),
options?: { replace: boolean } options?: { replace: boolean }
) => void; ) => void;
/**
* Updates an edge.
*
* @param id - id of the edge to update
* @param edgeUpdate - the edge update as an object or a function that receives the current edge and returns the edge update
* @param options.replace - if true, the edge is replaced with the edge update, otherwise the changes get merged
*
* @example
* updateEdge('edge-1', (edge) => ({ label: 'A new label' }));
*/
updateEdge: (
id: string,
edgeUpdate: Partial<EdgeType> | ((edge: EdgeType) => Partial<EdgeType>),
options?: { replace: boolean }
) => void;
/**
* Updates the data attribute of a edge.
*
* @param id - id of the edge to update
* @param dataUpdate - the data update as an object or a function that receives the current data and returns the data update
* @param options.replace - if true, the data is replaced with the data update, otherwise the changes get merged
*
* @example
* updateEdgeData('edge-1', { label: 'A new label' });
*/
updateEdgeData: (
id: string,
dataUpdate: Partial<EdgeType['data']> | ((edge: EdgeType) => Partial<EdgeType['data']>),
options?: { replace: boolean }
) => void;
}; };
export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edge = Edge> = GeneralHelpers< export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edge = Edge> = GeneralHelpers<
+4 -1
View File
@@ -2,5 +2,8 @@
"extends": "@xyflow/tsconfig/react.json", "extends": "@xyflow/tsconfig/react.json",
"display": "@xyflow/react", "display": "@xyflow/react",
"include": ["**/*.ts", "**/*.tsx"], "include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist"],
"compilerOptions": {
"outDir": "dist"
}
} }
+7
View File
@@ -1,5 +1,12 @@
# @xyflow/svelte # @xyflow/svelte
## 0.1.5
- prevent zooming on mobile if zoomOnPinch is false
- add straight edge to path built-in-types
- abort drag when multiple touches are detected
- fix merge_styles error
## 0.1.4 ## 0.1.4
- add `selectable`, `deletable` and `draggable` to node and edge props - add `selectable`, `deletable` and `draggable` to node and edge props
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/svelte", "name": "@xyflow/svelte",
"version": "0.1.4", "version": "0.1.5",
"description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.", "description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.",
"keywords": [ "keywords": [
"svelte", "svelte",
@@ -19,7 +19,7 @@
<div <div
class={cc(['svelte-flow__panel', className, ...positionClasses])} class={cc(['svelte-flow__panel', className, ...positionClasses])}
{style} {style}
style:pointer-events={$selectionRectMode ? 'none' : undefined} style:pointer-events={$selectionRectMode ? 'none' : ''}
{...$$restProps} {...$$restProps}
> >
<slot /> <slot />
+6 -1
View File
@@ -44,7 +44,12 @@ type StepEdge<EdgeData extends Record<string, unknown> = Record<string, unknown>
pathOptions?: StepPathOptions; pathOptions?: StepPathOptions;
}; };
export type BuiltInEdge = SmoothStepEdge | BezierEdge | StepEdge; type StraightEdge<EdgeData extends Record<string, unknown> = Record<string, unknown>> = Edge<
EdgeData,
'straight'
>;
export type BuiltInEdge = SmoothStepEdge | BezierEdge | StepEdge | StraightEdge;
/** /**
* Custom edge component props. * Custom edge component props.
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/system", "name": "@xyflow/system",
"version": "0.0.27", "version": "0.0.28",
"description": "xyflow core system that powers React Flow and Svelte Flow.", "description": "xyflow core system that powers React Flow and Svelte Flow.",
"keywords": [ "keywords": [
"node-based UI", "node-based UI",
+5 -6
View File
@@ -61,6 +61,7 @@ export const getEventPosition = (event: MouseEvent | TouchEvent, bounds?: DOMRec
export const getHandleBounds = ( export const getHandleBounds = (
selector: string, selector: string,
nodeElement: HTMLDivElement, nodeElement: HTMLDivElement,
nodeBounds: DOMRect,
zoom: number, zoom: number,
nodeOrigin: NodeOrigin = [0, 0] nodeOrigin: NodeOrigin = [0, 0]
): HandleElement[] | null => { ): HandleElement[] | null => {
@@ -72,11 +73,9 @@ export const getHandleBounds = (
const handlesArray = Array.from(handles) as HTMLDivElement[]; const handlesArray = Array.from(handles) as HTMLDivElement[];
// @todo can't we use the node dimensions here?
const nodeBounds = nodeElement.getBoundingClientRect();
const nodeOffset = { const nodeOffset = {
x: nodeBounds.width * nodeOrigin[0], x: nodeBounds.left - nodeBounds.width * nodeOrigin[0],
y: nodeBounds.height * nodeOrigin[1], y: nodeBounds.top - nodeBounds.height * nodeOrigin[1],
}; };
return handlesArray.map((handle): HandleElement => { return handlesArray.map((handle): HandleElement => {
@@ -85,8 +84,8 @@ export const getHandleBounds = (
return { return {
id: handle.getAttribute('data-handleid'), id: handle.getAttribute('data-handleid'),
position: handle.getAttribute('data-handlepos') as unknown as Position, position: handle.getAttribute('data-handlepos') as unknown as Position,
x: (handleBounds.left - nodeBounds.left - nodeOffset.x) / zoom, x: (handleBounds.left - nodeOffset.x) / zoom,
y: (handleBounds.top - nodeBounds.top - nodeOffset.y) / zoom, y: (handleBounds.top - nodeOffset.y) / zoom,
...getDimensions(handle), ...getDimensions(handle),
}; };
}); });
+3 -2
View File
@@ -269,12 +269,13 @@ export function updateNodeInternals<NodeType extends InternalNodeBase>(
); );
if (doUpdate) { if (doUpdate) {
const nodeBounds = update.nodeElement.getBoundingClientRect();
node.measured = dimensions; node.measured = dimensions;
node.internals = { node.internals = {
...node.internals, ...node.internals,
handleBounds: { handleBounds: {
source: getHandleBounds('.source', update.nodeElement, zoom, node.origin || nodeOrigin), source: getHandleBounds('.source', update.nodeElement, nodeBounds, zoom, node.origin || nodeOrigin),
target: getHandleBounds('.target', update.nodeElement, zoom, node.origin || nodeOrigin), target: getHandleBounds('.target', update.nodeElement, nodeBounds, zoom, node.origin || nodeOrigin),
}, },
}; };
+12 -1
View File
@@ -100,6 +100,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
let containerBounds: DOMRect | null = null; let containerBounds: DOMRect | null = null;
let dragStarted = false; let dragStarted = false;
let d3Selection: Selection<Element, unknown, null, undefined> | null = null; let d3Selection: Selection<Element, unknown, null, undefined> | null = null;
let abortDrag = false; // prevents unintentional dragging on multitouch
// public functions // public functions
function update({ noDragClassName, handleSelector, domNode, isSelectable, nodeId }: DragUpdateParams) { function update({ noDragClassName, handleSelector, domNode, isSelectable, nodeId }: DragUpdateParams) {
@@ -264,6 +265,8 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
.on('start', (event: UseDragEvent) => { .on('start', (event: UseDragEvent) => {
const { domNode, nodeDragThreshold, transform, snapGrid, snapToGrid } = getStoreItems(); const { domNode, nodeDragThreshold, transform, snapGrid, snapToGrid } = getStoreItems();
abortDrag = false;
if (nodeDragThreshold === 0) { if (nodeDragThreshold === 0) {
startDrag(event); startDrag(event);
} }
@@ -277,6 +280,14 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
const { autoPanOnNodeDrag, transform, snapGrid, snapToGrid, nodeDragThreshold } = getStoreItems(); const { autoPanOnNodeDrag, transform, snapGrid, snapToGrid, nodeDragThreshold } = getStoreItems();
const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
if (event.sourceEvent.type === 'touchmove' && event.sourceEvent.touches.length > 1) {
abortDrag = true;
}
if (abortDrag) {
return;
}
if (!autoPanStarted && autoPanOnNodeDrag && dragStarted) { if (!autoPanStarted && autoPanOnNodeDrag && dragStarted) {
autoPanStarted = true; autoPanStarted = true;
autoPan(); autoPan();
@@ -301,7 +312,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
} }
}) })
.on('end', (event: UseDragEvent) => { .on('end', (event: UseDragEvent) => {
if (!dragStarted) { if (!dragStarted || abortDrag) {
return; return;
} }
+5
View File
@@ -65,6 +65,11 @@ export function createFilter({
return false; return false;
} }
if (!zoomOnPinch && event.type === 'touchstart' && event.touches?.length > 1) {
event.preventDefault(); // if you manage to start with 2 touches, we prevent native zoom
return false;
}
// when there is no scroll handling enabled, we prevent all wheel events // when there is no scroll handling enabled, we prevent all wheel events
if (!zoomScroll && !panOnScroll && !pinchZoom && event.type === 'wheel') { if (!zoomScroll && !panOnScroll && !pinchZoom && event.type === 'wheel') {
return false; return false;
+7126 -5708
View File
File diff suppressed because it is too large Load Diff