Merge pull request #4426 from xyflow/next

React Flow 12.0.0-next.28 and Svelte Flow 0.1.10
This commit is contained in:
Moritz Klack
2024-07-09 11:24:55 +02:00
committed by GitHub
27 changed files with 175 additions and 14 deletions

View File

@@ -1,6 +1,7 @@
import Basic from '../examples/Basic';
import Backgrounds from '../examples/Backgrounds';
import ColorMode from '../examples/ColorMode';
import ClickDistance from '../examples/ClickDistance';
import ControlledUncontrolled from '../examples/ControlledUncontrolled';
import ControlledViewport from '../examples/ControlledViewport';
import CustomConnectionLine from '../examples/CustomConnectionLine';
@@ -85,6 +86,11 @@ const routes: IRoute[] = [
path: 'cancel-connection',
component: CancelConnection,
},
{
name: 'Click Distance',
path: 'click-distance',
component: ClickDistance,
},
{
name: 'Controlled/Uncontrolled',
path: 'controlled-uncontrolled',

View File

@@ -0,0 +1,72 @@
import { useCallback, useState } from 'react';
import { ReactFlow, addEdge, Node, Connection, Edge, useNodesState, useEdgesState, Panel } from '@xyflow/react';
const initNodes: Node[] = [
{
id: '1a',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
className: 'light',
ariaLabel: 'Input Node 1',
},
{
id: '2a',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
className: 'light',
ariaLabel: 'Default Node 2',
},
{
id: '3a',
data: { label: 'Node 3' },
position: { x: 400, y: 100 },
className: 'light',
},
{
id: '4a',
data: { label: 'Node 4' },
position: { x: 400, y: 200 },
className: 'light',
},
];
const initEdges: Edge[] = [
{ id: 'e1-2', source: '1a', target: '2a', ariaLabel: undefined },
{ id: 'e1-3', source: '1a', target: '3a' },
];
const onPaneClick = () => console.log('pane click');
const BasicFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(initNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
const [paneClickDistance, setPaneClickDistance] = useState(0);
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
paneClickDistance={paneClickDistance}
onPaneClick={onPaneClick}
>
<Panel position="top-right">
<input
type="range"
min={0}
max={100}
value={paneClickDistance}
onChange={(evt) => setPaneClickDistance(+evt.target.value)}
/>
click distance: {paneClickDistance}
</Panel>
</ReactFlow>
);
};
export default BasicFlow;

View File

@@ -1,5 +1,10 @@
# @xyflow/react
## 12.0.0-next.28
- add `paneDistanceClick` prop (max distance between mousedown/up that will trigger a click)
- returned nodes in `onNodeDragStop` are set to `dragging=false`
## 12.0.0-next.27
- return Promises for `setViewport`, `fitView`, `fitBounds` and `zoomTo` to be able to await viewport update

View File

@@ -1,6 +1,6 @@
{
"name": "@xyflow/react",
"version": "12.0.0-next.26",
"version": "12.0.0-next.27",
"description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.",
"keywords": [
"react",

View File

@@ -67,6 +67,7 @@ const reactFlowFieldsToTrack = [
'onBeforeDelete',
'debug',
'autoPanSpeed',
'paneClickDistance',
] as const;
type ReactFlowFieldsToTrack = (typeof reactFlowFieldsToTrack)[number];
@@ -89,6 +90,7 @@ const selector = (s: ReactFlowState) => ({
setNodeExtent: s.setNodeExtent,
reset: s.reset,
setDefaultNodesAndEdges: s.setDefaultNodesAndEdges,
setPaneClickDistance: s.setPaneClickDistance,
});
const initPrevValues = {
@@ -102,6 +104,7 @@ const initPrevValues = {
elementsSelectable: true,
noPanClassName: 'nopan',
rfId: '1',
paneClickDistance: 0,
};
export function StoreUpdater<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
@@ -116,6 +119,7 @@ export function StoreUpdater<NodeType extends Node = Node, EdgeType extends Edge
setNodeExtent,
reset,
setDefaultNodesAndEdges,
setPaneClickDistance,
} = useStore(selector, shallow);
const store = useStoreApi<NodeType, EdgeType>();
@@ -146,6 +150,7 @@ export function StoreUpdater<NodeType extends Node = Node, EdgeType extends Edge
else if (fieldName === 'maxZoom') setMaxZoom(fieldValue as number);
else if (fieldName === 'translateExtent') setTranslateExtent(fieldValue as CoordinateExtent);
else if (fieldName === 'nodeExtent') setNodeExtent(fieldValue as CoordinateExtent);
else if (fieldName === 'paneClickDistance') setPaneClickDistance(fieldValue as number);
// Renamed fields
else if (fieldName === 'fitView') store.setState({ fitViewOnInit: fieldValue as boolean });
else if (fieldName === 'fitViewOptions') store.setState({ fitViewOnInitOptions: fieldValue as FitViewOptions });

View File

@@ -41,6 +41,7 @@ function FlowRendererComponent<NodeType extends Node = Node>({
onPaneMouseLeave,
onPaneContextMenu,
onPaneScroll,
paneClickDistance,
deleteKeyCode,
selectionKeyCode,
selectionOnDrag,
@@ -101,6 +102,7 @@ function FlowRendererComponent<NodeType extends Node = Node>({
noPanClassName={noPanClassName}
onViewportChange={onViewportChange}
isControlledViewport={isControlledViewport}
paneClickDistance={paneClickDistance}
>
<Pane
onSelectionStart={onSelectionStart}

View File

@@ -32,6 +32,7 @@ export type GraphViewProps<NodeType extends Node = Node, EdgeType extends Edge =
| 'noPanClassName'
| 'defaultViewport'
| 'disableKeyboardA11y'
| 'paneClickDistance'
>
> & {
rfId: string;
@@ -84,6 +85,7 @@ function GraphViewComponent<NodeType extends Node = Node, EdgeType extends Edge
onPaneMouseLeave,
onPaneScroll,
onPaneContextMenu,
paneClickDistance,
onEdgeContextMenu,
onEdgeMouseEnter,
onEdgeMouseMove,
@@ -116,6 +118,7 @@ function GraphViewComponent<NodeType extends Node = Node, EdgeType extends Edge
onPaneMouseLeave={onPaneMouseLeave}
onPaneContextMenu={onPaneContextMenu}
onPaneScroll={onPaneScroll}
paneClickDistance={paneClickDistance}
deleteKeyCode={deleteKeyCode}
selectionKeyCode={selectionKeyCode}
selectionOnDrag={selectionOnDrag}

View File

@@ -103,6 +103,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
onPaneMouseLeave,
onPaneScroll,
onPaneContextMenu,
paneClickDistance = 0,
children,
onReconnect,
onReconnectStart,
@@ -200,6 +201,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
onPaneMouseLeave={onPaneMouseLeave}
onPaneScroll={onPaneScroll}
onPaneContextMenu={onPaneContextMenu}
paneClickDistance={paneClickDistance}
onSelectionContextMenu={onSelectionContextMenu}
onSelectionStart={onSelectionStart}
onSelectionEnd={onSelectionEnd}
@@ -277,6 +279,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
selectNodesOnDrag={selectNodesOnDrag}
nodeDragThreshold={nodeDragThreshold}
onBeforeDelete={onBeforeDelete}
paneClickDistance={paneClickDistance}
debug={debug}
/>
<SelectionListener onSelectionChange={onSelectionChange} />

View File

@@ -47,6 +47,7 @@ export function ZoomPane({
noPanClassName,
onViewportChange,
isControlledViewport,
paneClickDistance,
}: ZoomPaneProps) {
const store = useStoreApi();
const zoomPane = useRef<HTMLDivElement>(null);
@@ -64,6 +65,7 @@ export function ZoomPane({
maxZoom,
translateExtent,
viewport: defaultViewport,
paneClickDistance,
onTransformChange: (transform: Transform) => {
onViewportChange?.({ x: transform[0], y: transform[1], zoom: transform[2] });

View File

@@ -134,7 +134,6 @@ const createStore = ({
const changes = [];
for (const [id, dragItem] of nodeDragItems) {
// @todo add expandParent to drag item so that we can get rid of the look up here
const change: NodeChange = {
id,
type: 'position',
@@ -255,6 +254,9 @@ const createStore = ({
set({ translateExtent });
},
setPaneClickDistance: (clickDistance) => {
get().panZoom?.setClickDistance(clickDistance);
},
resetSelectedElements: () => {
const { edges, nodes, triggerNodeChanges, triggerEdgeChanges } = get();

View File

@@ -223,6 +223,10 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
onPaneMouseMove?: (event: ReactMouseEvent) => void;
/** This event handler gets called when mouse leaves the pane */
onPaneMouseLeave?: (event: ReactMouseEvent) => void;
/** Distance that the mouse can move between mousedown/up that will trigger a click
* @default 0
*/
paneClickDistance?: number;
/** This handler gets called before the user deletes nodes or edges and provides a way to abort the deletion by returning false. */
onBeforeDelete?: OnBeforeDelete<NodeType, EdgeType>;
/** Custom node types to be available in a flow.

View File

@@ -170,6 +170,7 @@ export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
panBy: PanBy;
fitView: (options?: FitViewOptions) => Promise<boolean>;
fitViewSync: (options?: FitViewOptions) => boolean;
setPaneClickDistance: (distance: number) => void;
};
export type ReactFlowState<NodeType extends Node = Node, EdgeType extends Edge = Edge> = ReactFlowStore<

View File

@@ -1,5 +1,10 @@
# @xyflow/svelte
## 0.1.10
- add `paneDistanceClick` prop (max distance between mousedown/up that will trigger a click)
- returned nodes in `on:nodedragstop` are set to `dragging=false`
## 0.1.9
- return Promises for `setViewport`, `fitView`, `fitBounds` and `zoomTo` to be able to await viewport update

View File

@@ -1,6 +1,6 @@
{
"name": "@xyflow/svelte",
"version": "0.1.8",
"version": "0.1.9",
"description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.",
"keywords": [
"svelte",

View File

@@ -35,11 +35,20 @@ type ZoomParams = {
noWheelClassName: string;
userSelectionActive: boolean;
lib: string;
paneClickDistance: number;
};
export default function zoom(domNode: Element, params: ZoomParams) {
const { panZoom, minZoom, maxZoom, initialViewport, viewport, dragging, translateExtent } =
params;
const {
panZoom,
minZoom,
maxZoom,
initialViewport,
viewport,
dragging,
translateExtent,
paneClickDistance
} = params;
const panZoomInstance = XYPanZoom({
domNode,
@@ -47,6 +56,7 @@ export default function zoom(domNode: Element, params: ZoomParams) {
maxZoom,
translateExtent,
viewport: initialViewport,
paneClickDistance,
onTransformChange: (transform) =>
viewport.set({ x: transform[0], y: transform[1], zoom: transform[2] }),
onDraggingChange: dragging.set

View File

@@ -79,6 +79,7 @@
export let onbeforedelete: $$Props['onbeforedelete'] = undefined;
export let oninit: $$Props['oninit'] = undefined;
export let nodeOrigin: $$Props['nodeOrigin'] = undefined;
export let paneClickDistance: $$Props['paneClickDistance'] = 0;
export let defaultMarkerColor = '#b1b1b7';
@@ -125,7 +126,8 @@
edgeTypes,
minZoom,
maxZoom,
translateExtent
translateExtent,
paneClickDistance
});
return () => {
@@ -188,7 +190,8 @@
edgeTypes,
minZoom,
maxZoom,
translateExtent
translateExtent,
paneClickDistance
});
$: colorModeClass = useColorModeClass(colorMode);
@@ -225,6 +228,7 @@
zoomOnPinch={zoomOnPinch === undefined ? true : zoomOnPinch}
panOnScroll={panOnScroll === undefined ? false : panOnScroll}
panOnDrag={panOnDrag === undefined ? true : panOnDrag}
paneClickDistance={paneClickDistance === undefined ? 0 : paneClickDistance}
>
<Pane
on:paneclick

View File

@@ -134,6 +134,10 @@ export type SvelteFlowProps = DOMAttributes<HTMLDivElement> & {
* @default 1
*/
nodeDragThreshold?: number;
/** Distance that the mouse can move between mousedown/up that will trigger a click
* @default 0
*/
paneClickDistance?: number;
/** Minimum zoom level
* @default 0.5
*/

View File

@@ -1,7 +1,8 @@
import type { Writable } from 'svelte/store';
import type { CoordinateExtent } from '@xyflow/system';
import type { SvelteFlowStore } from '$lib/store/types';
import type { EdgeTypes, NodeTypes } from '$lib/types';
import type { CoordinateExtent } from '@xyflow/system';
import type { Writable } from 'svelte/store';
// this is helper function for updating the store
// for props where we need to call a specific store action
@@ -12,13 +13,15 @@ export function updateStore(
edgeTypes,
minZoom,
maxZoom,
translateExtent
translateExtent,
paneClickDistance
}: {
nodeTypes?: NodeTypes;
edgeTypes?: EdgeTypes;
minZoom?: number;
maxZoom?: number;
translateExtent?: CoordinateExtent;
paneClickDistance?: number;
}
) {
if (nodeTypes !== undefined) {
@@ -40,6 +43,10 @@ export function updateStore(
if (translateExtent !== undefined) {
store.setTranslateExtent(translateExtent);
}
if (paneClickDistance !== undefined) {
store.setPaneClickDistance(paneClickDistance);
}
}
const getKeys = <T extends object>(obj: T) => Object.keys(obj) as Array<keyof T>;

View File

@@ -19,6 +19,7 @@
export let zoomOnPinch: $$Props['zoomOnPinch'];
export let panOnDrag: $$Props['panOnDrag'];
export let panOnScroll: $$Props['panOnScroll'];
export let paneClickDistance: $$Props['paneClickDistance'];
const {
viewport,
@@ -68,7 +69,8 @@
noWheelClassName: 'nowheel',
userSelectionActive: !!$selectionRect,
translateExtent: $translateExtent,
lib: $lib
lib: $lib,
paneClickDistance
}}
>
<slot />

View File

@@ -12,4 +12,5 @@ export type ZoomProps = {
zoomOnPinch: boolean;
panOnScroll: boolean;
panOnDrag: boolean | number[];
paneClickDistance: number;
};

View File

@@ -248,6 +248,10 @@ export function createStore({
return elementsChanged;
}
function setPaneClickDistance(distance: number) {
get(store.panZoom)?.setClickDistance(distance);
}
function unselectNodesAndEdges(params?: { nodes?: Node[]; edges?: Edge[] }) {
const resetNodes = resetSelectedElements(params?.nodes || get(store.nodes));
if (resetNodes) store.nodes.set(get(store.nodes));
@@ -448,6 +452,7 @@ export function createStore({
setMinZoom,
setMaxZoom,
setTranslateExtent,
setPaneClickDistance,
unselectNodesAndEdges,
addSelectedNodes,
addSelectedEdges,

View File

@@ -25,6 +25,7 @@ export type SvelteFlowStoreActions = {
setMinZoom: (minZoom: number) => void;
setMaxZoom: (maxZoom: number) => void;
setTranslateExtent: (extent: CoordinateExtent) => void;
setPaneClickDistance: (distance: number) => void;
fitView: (options?: FitViewOptions) => Promise<boolean>;
updateNodePositions: UpdateNodePositions;
updateNodeInternals: (updates: Map<string, InternalNodeUpdate>) => void;

View File

@@ -1,6 +1,6 @@
{
"name": "@xyflow/system",
"version": "0.0.32",
"version": "0.0.33",
"description": "xyflow core system that powers React Flow and Svelte Flow.",
"keywords": [
"node-based UI",

View File

@@ -9,6 +9,7 @@ export type PanZoomParams = {
domNode: Element;
minZoom: number;
maxZoom: number;
paneClickDistance: number;
viewport: Viewport;
translateExtent: CoordinateExtent;
onTransformChange: OnTransformChange;
@@ -56,4 +57,5 @@ export type PanZoomInstance = {
scaleTo: (scale: number, options?: PanZoomTransformOptions) => Promise<boolean>;
scaleBy: (factor: number, options?: PanZoomTransformOptions) => Promise<boolean>;
syncViewport: (viewport: Viewport) => void;
setClickDistance: (distance: number) => void;
};

View File

@@ -332,6 +332,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
nodeId,
dragItems,
nodeLookup,
dragging: false,
});
onDragStop?.(event.sourceEvent as MouseEvent, dragItems, currentNode, currentNodes);

View File

@@ -81,10 +81,12 @@ export function getEventHandlerParams<NodeType extends InternalNodeBase>({
nodeId,
dragItems,
nodeLookup,
dragging = true,
}: {
nodeId?: string;
dragItems: Map<string, NodeDragItem>;
nodeLookup: Map<string, NodeType>;
dragging?: boolean;
}): [NodeBase, NodeBase[]] {
const nodesFromDragItems: NodeBase[] = [];
@@ -95,6 +97,7 @@ export function getEventHandlerParams<NodeType extends InternalNodeBase>({
nodesFromDragItems.push({
...node,
position: dragItem.position,
dragging,
});
}
}
@@ -109,6 +112,7 @@ export function getEventHandlerParams<NodeType extends InternalNodeBase>({
{
...node,
position: dragItems.get(nodeId)?.position || node.position,
dragging,
},
nodesFromDragItems,
];

View File

@@ -9,7 +9,7 @@ import {
PanZoomParams,
PanZoomInstance,
} from '../types';
import { clamp } from '../utils';
import { clamp, isNumeric } from '../utils';
import { getD3Transition, viewportToTransform, wheelDelta } from './utils';
import {
createPanOnScrollHandler,
@@ -34,6 +34,7 @@ export function XYPanZoom({
domNode,
minZoom,
maxZoom,
paneClickDistance,
translateExtent,
viewport,
onPanZoom,
@@ -52,7 +53,10 @@ export function XYPanZoom({
isPanScrolling: false,
};
const bbox = domNode.getBoundingClientRect();
const d3ZoomInstance = zoom().scaleExtent([minZoom, maxZoom]).translateExtent(translateExtent);
const d3ZoomInstance = zoom()
.clickDistance(!isNumeric(paneClickDistance) || paneClickDistance < 0 ? 0 : paneClickDistance)
.scaleExtent([minZoom, maxZoom])
.translateExtent(translateExtent);
const d3Selection = select(domNode).call(d3ZoomInstance);
setViewportConstrained(
@@ -267,6 +271,11 @@ export function XYPanZoom({
d3ZoomInstance?.translateExtent(translateExtent);
}
function setClickDistance(distance: number) {
const validDistance = !isNumeric(distance) || distance < 0 ? 0 : distance;
d3ZoomInstance?.clickDistance(validDistance);
}
return {
update,
destroy,
@@ -278,5 +287,6 @@ export function XYPanZoom({
setScaleExtent,
setTranslateExtent,
syncViewport,
setClickDistance,
};
}