diff --git a/examples/react/src/examples/Overview/index.tsx b/examples/react/src/examples/Overview/index.tsx index e4879ace..3f5c72d0 100644 --- a/examples/react/src/examples/Overview/index.tsx +++ b/examples/react/src/examples/Overview/index.tsx @@ -15,6 +15,8 @@ import { Background, MiniMap, ConnectionMode, + OnBeforeDelete, + OnDelete, } from '@xyflow/react'; const onNodeDragStart = (_: ReactMouseEvent, node: Node, nodes: Node[]) => console.log('drag start', node, nodes); @@ -45,8 +47,12 @@ const onEdgeMouseEnter = (_: ReactMouseEvent, edge: Edge) => console.log('edge m const onEdgeMouseMove = (_: ReactMouseEvent, edge: Edge) => console.log('edge mouse move', edge); const onEdgeMouseLeave = (_: ReactMouseEvent, edge: Edge) => console.log('edge mouse leave', edge); const onEdgeDoubleClick = (_: ReactMouseEvent, edge: Edge) => console.log('edge double click', edge); -const onNodesDelete = (nodes: Node[]) => console.log('nodes delete', nodes); -const onEdgesDelete = (edges: Edge[]) => console.log('edges delete', edges); +const onBeforeDelete: OnBeforeDelete = async ({ nodes, edges }) => { + console.log('on before delete', nodes, edges); + const deleteElements = confirm('Do you want to remove the selected elements?'); + return deleteElements; +}; +const onDelete: OnDelete = ({ nodes, edges }) => console.log('on delete', nodes, edges); const onPaneMouseMove = (e: ReactMouseEvent) => console.log('pane move', e.clientX, e.clientY); const initialNodes: Node[] = [ @@ -226,8 +232,8 @@ const OverviewFlow = () => { fitViewOptions={{ padding: 0.1 /*nodes: [{ id: '1' }]*/ }} attributionPosition="top-right" maxZoom={Infinity} - onNodesDelete={onNodesDelete} - onEdgesDelete={onEdgesDelete} + onBeforeDelete={onBeforeDelete} + onDelete={onDelete} onPaneMouseMove={onPaneMouseMove} > diff --git a/examples/react/src/examples/Validation/index.tsx b/examples/react/src/examples/Validation/index.tsx index 864c3c08..3e89b3f8 100644 --- a/examples/react/src/examples/Validation/index.tsx +++ b/examples/react/src/examples/Validation/index.tsx @@ -16,6 +16,7 @@ import { updateEdge, Edge, IsValidConnection, + OnBeforeDelete, } from '@xyflow/react'; import ConnectionStatus from './ConnectionStatus'; @@ -85,6 +86,10 @@ const ValidationFlow = () => { [setEdges] ); + const onBeforeDelete: OnBeforeDelete = useCallback(async () => { + return true; + }, []); + return ( { onConnectEnd={onConnectEnd} onEdgeUpdate={onEdgeUpdate} isValidConnection={isValidConnection} + onBeforeDelete={onBeforeDelete} fitView > diff --git a/examples/svelte/src/routes/examples/overview/+page.svelte b/examples/svelte/src/routes/examples/overview/+page.svelte index d8293666..44d54544 100644 --- a/examples/svelte/src/routes/examples/overview/+page.svelte +++ b/examples/svelte/src/routes/examples/overview/+page.svelte @@ -177,6 +177,11 @@ }} on:selectionclick={(event) => console.log('on selection click', event)} on:selectioncontextmenu={(event) => console.log('on selection contextmenu', event)} + onbeforedelete={async ({ nodes, edges }) => { + console.log('on before delete', nodes, edges); + const deleteElements = confirm('Are you sure you want to delete the selected elements?'); + return deleteElements; + }} autoPanOnConnect autoPanOnNodeDrag connectionMode={ConnectionMode.Strict} diff --git a/packages/react/src/components/StoreUpdater/index.tsx b/packages/react/src/components/StoreUpdater/index.tsx index 948c50fc..344c718f 100644 --- a/packages/react/src/components/StoreUpdater/index.tsx +++ b/packages/react/src/components/StoreUpdater/index.tsx @@ -64,6 +64,7 @@ const reactFlowFieldsToTrack = [ 'isValidConnection', 'selectNodesOnDrag', 'nodeDragThreshold', + 'onBeforeDelete', ] as const; type ReactFlowFieldsToTrack = (typeof reactFlowFieldsToTrack)[number]; diff --git a/packages/react/src/container/ReactFlow/index.tsx b/packages/react/src/container/ReactFlow/index.tsx index 48ff13c6..bfe3530c 100644 --- a/packages/react/src/container/ReactFlow/index.tsx +++ b/packages/react/src/container/ReactFlow/index.tsx @@ -70,6 +70,7 @@ const ReactFlow = forwardRef( onSelectionContextMenu, onSelectionStart, onSelectionEnd, + onBeforeDelete, connectionMode, connectionLineType = ConnectionLineType.Bezier, connectionLineStyle, @@ -284,6 +285,7 @@ const ReactFlow = forwardRef( isValidConnection={isValidConnection} selectNodesOnDrag={selectNodesOnDrag} nodeDragThreshold={nodeDragThreshold} + onBeforeDelete={onBeforeDelete} /> {children} diff --git a/packages/react/src/hooks/useGlobalKeyHandler.ts b/packages/react/src/hooks/useGlobalKeyHandler.ts index 786ed870..2409558f 100644 --- a/packages/react/src/hooks/useGlobalKeyHandler.ts +++ b/packages/react/src/hooks/useGlobalKeyHandler.ts @@ -30,8 +30,8 @@ export default ({ useEffect(() => { if (deleteKeyPressed) { - const { edges, nodes } = store.getState(); - deleteElements({ nodes: nodes.filter(selected), edges: edges.filter(selected) }); + const { edges, nodes, onBeforeDelete } = store.getState(); + deleteElements({ nodes: nodes.filter(selected), edges: edges.filter(selected), onBeforeDelete }); store.setState({ nodesSelectionActive: false }); } }, [deleteKeyPressed]); diff --git a/packages/react/src/hooks/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts index 271e0ebf..eb07cc6a 100644 --- a/packages/react/src/hooks/useReactFlow.ts +++ b/packages/react/src/hooks/useReactFlow.ts @@ -12,7 +12,6 @@ import type { EdgeResetChange, NodeRemoveChange, EdgeRemoveChange, - NodeChange, Node, Edge, } from '../types'; @@ -118,67 +117,65 @@ export default function useReactFlow(({ nodes: nodesDeleted, edges: edgesDeleted }) => { - const { - nodes, - edges, - hasDefaultNodes, - hasDefaultEdges, - onNodesDelete, - onEdgesDelete, - onNodesChange, - onEdgesChange, - onDelete, - } = store.getState(); - const { matchingNodes, matchingEdges } = getElementsToRemove({ - nodesToRemove: nodesDeleted || [], - edgesToRemove: edgesDeleted || [], - nodes, - edges, - }); + const deleteElements = useCallback( + async ({ nodes: nodesToRemove = [], edges: edgesToRemove = [], onBeforeDelete }) => { + const { + nodes, + edges, + hasDefaultNodes, + hasDefaultEdges, + onNodesDelete, + onEdgesDelete, + onNodesChange, + onEdgesChange, + onDelete, + } = store.getState(); + const { nodes: matchingNodes, edges: matchingEdges } = await getElementsToRemove({ + nodesToRemove, + edgesToRemove, + nodes, + edges, + onBeforeDelete, + }); - if (matchingNodes.length || matchingEdges.length) { - if (hasDefaultEdges || hasDefaultNodes) { + const hasMatchingEdges = matchingEdges.length > 0; + const hasMatchingNodes = matchingNodes.length > 0; + + if (hasMatchingEdges) { if (hasDefaultEdges) { store.setState({ edges: edges.filter((e) => !matchingEdges.some((mE) => mE.id === e.id)), }); } + onEdgesDelete?.(matchingEdges); + onEdgesChange?.( + matchingEdges.map((edge) => ({ + id: edge.id, + type: 'remove', + })) + ); + } + + if (hasMatchingNodes) { if (hasDefaultNodes) { store.setState({ nodes: nodes.filter((n) => !matchingNodes.some((mN) => mN.id === n.id)), }); } + + onNodesDelete?.(matchingNodes); + onNodesChange?.(matchingNodes.map((node) => ({ id: node.id, type: 'remove' }))); } - if (matchingEdges.length > 0) { - onEdgesDelete?.(matchingEdges); - - if (onEdgesChange) { - onEdgesChange( - matchingEdges.map((edge) => ({ - id: edge.id, - type: 'remove', - })) - ); - } + if (hasMatchingNodes || hasMatchingEdges) { + onDelete?.({ nodes: matchingNodes, edges: matchingEdges }); } - if (matchingNodes.length > 0) { - onNodesDelete?.(matchingNodes as Node[]); - - if (onNodesChange) { - const nodeChanges: NodeChange[] = matchingNodes.map((node) => ({ id: node.id, type: 'remove' })); - onNodesChange(nodeChanges); - } - } - - onDelete?.({ nodes: matchingNodes, edges: matchingEdges }); - } - - return { deletedNodes: matchingNodes, deletedEdges: matchingEdges }; - }, []); + return { deletedNodes: matchingNodes, deletedEdges: matchingEdges }; + }, + [] + ); const getNodeRect = useCallback( (nodeOrRect: NodeType | { id: Node['id'] } | Rect): [Rect | null, NodeType | null | undefined, boolean] => { diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index f1997caa..9f30289d 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -79,6 +79,7 @@ export { type ColorMode, type ColorModeClass, type HandleType, + type OnBeforeDelete, } from '@xyflow/system'; // system utils diff --git a/packages/react/src/types/component-props.ts b/packages/react/src/types/component-props.ts index 43dc5e90..bea7166b 100644 --- a/packages/react/src/types/component-props.ts +++ b/packages/react/src/types/component-props.ts @@ -21,6 +21,7 @@ import type { IsValidConnection, ColorMode, SnapGrid, + OnBeforeDelete, } from '@xyflow/system'; import type { @@ -100,6 +101,7 @@ export type ReactFlowProps = Omit, 'onError'> & { onPaneMouseEnter?: (event: ReactMouseEvent) => void; onPaneMouseMove?: (event: ReactMouseEvent) => void; onPaneMouseLeave?: (event: ReactMouseEvent) => void; + onBeforeDelete?: OnBeforeDelete; nodeTypes?: NodeTypes; edgeTypes?: EdgeTypes; connectionLineType?: ConnectionLineType; diff --git a/packages/react/src/types/instance.ts b/packages/react/src/types/instance.ts index 878e85a8..c9c4708d 100644 --- a/packages/react/src/types/instance.ts +++ b/packages/react/src/types/instance.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-namespace */ -import type { Rect, Viewport } from '@xyflow/system'; +import type { OnBeforeDelete, Rect, Viewport } from '@xyflow/system'; import type { Node, Edge, ViewportHelperFunctions } from '.'; export type ReactFlowJsonObject = { @@ -12,6 +12,7 @@ export type ReactFlowJsonObject; - export type DeleteElements = ({ nodes, edges }: DeleteElementsOptions) => { + export type DeleteElements = (params: DeleteElementsOptions) => Promise<{ deletedNodes: Node[]; deletedEdges: Edge[]; - }; + }>; export type GetIntersectingNodes = ( node: NodeType | { id: Node['id'] } | Rect, partially?: boolean, diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index a4cad399..b138fc9d 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -27,6 +27,7 @@ import { type EdgeLookup, type ConnectionLookup, type NodeLookup, + OnBeforeDelete, } from '@xyflow/system'; import type { @@ -132,6 +133,7 @@ export type ReactFlowStore = { onViewportChangeStart?: OnViewportChange; onViewportChange?: OnViewportChange; onViewportChangeEnd?: OnViewportChange; + onBeforeDelete?: OnBeforeDelete; onSelectionChangeHandlers: OnSelectionChangeFunc[]; diff --git a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte index 3e09a460..2d2df3a6 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte @@ -1,5 +1,6 @@