Merge pull request #3741 from xyflow/feat/on-before-delete

Feat(deletions): add onBeforeDelete handler
This commit is contained in:
Moritz Klack
2023-12-23 15:20:37 +01:00
committed by GitHub
19 changed files with 174 additions and 122 deletions

View File

@@ -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}
>
<MiniMap nodeBorderRadius={2} />

View File

@@ -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 (
<ReactFlow
nodes={nodes}
@@ -99,6 +104,7 @@ const ValidationFlow = () => {
onConnectEnd={onConnectEnd}
onEdgeUpdate={onEdgeUpdate}
isValidConnection={isValidConnection}
onBeforeDelete={onBeforeDelete}
fitView
>
<ConnectionStatus />

View File

@@ -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}

View File

@@ -64,6 +64,7 @@ const reactFlowFieldsToTrack = [
'isValidConnection',
'selectNodesOnDrag',
'nodeDragThreshold',
'onBeforeDelete',
] as const;
type ReactFlowFieldsToTrack = (typeof reactFlowFieldsToTrack)[number];

View File

@@ -70,6 +70,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
onSelectionContextMenu,
onSelectionStart,
onSelectionEnd,
onBeforeDelete,
connectionMode,
connectionLineType = ConnectionLineType.Bezier,
connectionLineStyle,
@@ -284,6 +285,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
isValidConnection={isValidConnection}
selectNodesOnDrag={selectNodesOnDrag}
nodeDragThreshold={nodeDragThreshold}
onBeforeDelete={onBeforeDelete}
/>
<SelectionListener onSelectionChange={onSelectionChange} />
{children}

View File

@@ -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]);

View File

@@ -12,7 +12,6 @@ import type {
EdgeResetChange,
NodeRemoveChange,
EdgeRemoveChange,
NodeChange,
Node,
Edge,
} from '../types';
@@ -118,67 +117,65 @@ export default function useReactFlow<NodeType extends Node = Node, EdgeType exte
};
}, []);
const deleteElements = useCallback<Instance.DeleteElements>(({ nodes: nodesDeleted, edges: edgesDeleted }) => {
const {
nodes,
edges,
hasDefaultNodes,
hasDefaultEdges,
onNodesDelete,
onEdgesDelete,
onNodesChange,
onEdgesChange,
onDelete,
} = store.getState();
const { matchingNodes, matchingEdges } = getElementsToRemove<Node, Edge>({
nodesToRemove: nodesDeleted || [],
edgesToRemove: edgesDeleted || [],
nodes,
edges,
});
const deleteElements = useCallback<Instance.DeleteElements>(
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] => {

View File

@@ -79,6 +79,7 @@ export {
type ColorMode,
type ColorModeClass,
type HandleType,
type OnBeforeDelete,
} from '@xyflow/system';
// system utils

View File

@@ -21,6 +21,7 @@ import type {
IsValidConnection,
ColorMode,
SnapGrid,
OnBeforeDelete,
} from '@xyflow/system';
import type {
@@ -100,6 +101,7 @@ export type ReactFlowProps = Omit<HTMLAttributes<HTMLDivElement>, 'onError'> & {
onPaneMouseEnter?: (event: ReactMouseEvent) => void;
onPaneMouseMove?: (event: ReactMouseEvent) => void;
onPaneMouseLeave?: (event: ReactMouseEvent) => void;
onBeforeDelete?: OnBeforeDelete;
nodeTypes?: NodeTypes;
edgeTypes?: EdgeTypes;
connectionLineType?: ConnectionLineType;

View File

@@ -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<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
@@ -12,6 +12,7 @@ export type ReactFlowJsonObject<NodeType extends Node = Node, EdgeType extends E
export type DeleteElementsOptions = {
nodes?: (Node | { id: Node['id'] })[];
edges?: (Edge | { id: Edge['id'] })[];
onBeforeDelete?: OnBeforeDelete;
};
export namespace Instance {
@@ -31,10 +32,10 @@ export namespace Instance {
NodeType,
EdgeType
>;
export type DeleteElements = ({ nodes, edges }: DeleteElementsOptions) => {
export type DeleteElements = (params: DeleteElementsOptions) => Promise<{
deletedNodes: Node[];
deletedEdges: Edge[];
};
}>;
export type GetIntersectingNodes<NodeType extends Node = Node> = (
node: NodeType | { id: Node['id'] } | Rect,
partially?: boolean,

View File

@@ -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[];

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { onMount, hasContext } from 'svelte';
import { get } from 'svelte/store';
import cc from 'classcat';
import { ConnectionMode, PanOnScrollMode, type Viewport } from '@xyflow/system';
@@ -15,13 +16,7 @@
import { Attribution } from '$lib/components/Attribution';
import { key, useStore, createStoreContext } from '$lib/store';
import type { SvelteFlowProps } from './types';
import {
updateStore,
updateStoreByKeys,
type UpdatableStoreProps,
getColorModeClass
} from './utils';
import { get } from 'svelte/store';
import { updateStore, updateStoreByKeys, type UpdatableStoreProps } from './utils';
import { useColorModeClass } from '$lib/hooks/useColorModeClass';
type $$Props = SvelteFlowProps;
@@ -81,6 +76,7 @@
export let onconnect: $$Props['onconnect'] = undefined;
export let onconnectstart: $$Props['onconnectstart'] = undefined;
export let onconnectend: $$Props['onconnectend'] = undefined;
export let onbeforedelete: $$Props['onbeforedelete'] = undefined;
export let defaultMarkerColor = '#b1b1b7';
@@ -158,7 +154,8 @@
nodeDragThreshold,
onconnect,
onconnectstart,
onconnectend
onconnectend,
onbeforedelete
};
updateStoreByKeys(store, updatableProps);

View File

@@ -18,7 +18,8 @@ import type {
ColorMode,
OnConnect,
OnConnectStart,
OnConnectEnd
OnConnectEnd,
OnBeforeDelete
} from '@xyflow/system';
import type {
@@ -93,6 +94,7 @@ export type SvelteFlowProps = DOMAttributes<HTMLDivElement> & {
onMoveEnd?: OnMoveEnd;
onerror?: OnError;
ondelete?: OnDelete;
onbeforedelete?: OnBeforeDelete;
onedgecreate?: OnEdgeCreate;

View File

@@ -1,6 +1,6 @@
import type { SvelteFlowStore } from '$lib/store/types';
import type { EdgeTypes, NodeTypes } from '$lib/types';
import type { ColorMode, CoordinateExtent } from '@xyflow/system';
import type { CoordinateExtent } from '@xyflow/system';
import type { Writable } from 'svelte/store';
// this is helper function for updating the store
@@ -69,6 +69,7 @@ export type UpdatableStoreProps = {
onconnect?: UnwrapWritable<SvelteFlowStore['onconnect']>;
onconnectstart?: UnwrapWritable<SvelteFlowStore['onconnectstart']>;
onconnectend?: UnwrapWritable<SvelteFlowStore['onconnectend']>;
onbeforedelete?: UnwrapWritable<SvelteFlowStore['onbeforedelete']>;
};
export function updateStoreByKeys(store: SvelteFlowStore, keys: UpdatableStoreProps) {
@@ -81,15 +82,3 @@ export function updateStoreByKeys(store: SvelteFlowStore, keys: UpdatableStorePr
}
});
}
export function getColorModeClass(colorMode?: ColorMode) {
if (colorMode !== 'system') {
return colorMode;
}
if (!colorMode || typeof window === 'undefined' || !window.matchMedia) {
return 'light';
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}

View File

@@ -13,7 +13,8 @@ import {
type Rect,
getViewportForBounds,
getElementsToRemove,
rendererPointToPoint
rendererPointToPoint,
type OnBeforeDelete
} from '@xyflow/system';
import { useStore } from '$lib/store';
@@ -48,8 +49,9 @@ export function useSvelteFlow(): {
fitBounds: (bounds: Rect, options?: FitBoundsOptions) => void;
deleteElements: (
nodesToRemove?: (Node | { id: Node['id'] })[],
edgesToRemove?: (Edge | { id: Edge['id'] })[]
) => { deletedNodes: Node[]; deletedEdges: Edge[] };
edgesToRemove?: (Edge | { id: Edge['id'] })[],
onBeforeDelete?: OnBeforeDelete
) => Promise<{ deletedNodes: Node[]; deletedEdges: Edge[] }>;
screenToFlowPosition: (position: XYPosition) => XYPosition;
flowToScreenPosition: (position: XYPosition) => XYPosition;
viewport: Writable<Viewport>;
@@ -69,6 +71,7 @@ export function useSvelteFlow(): {
zoomIn,
zoomOut,
fitView,
onbeforedelete,
snapGrid,
viewport,
width,
@@ -198,25 +201,29 @@ export function useSvelteFlow(): {
return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
},
deleteElements: (
deleteElements: async (
nodesToRemove: (Node | { id: Node['id'] })[] = [],
edgesToRemove: (Edge | { id: Edge['id'] })[] = []
edgesToRemove: (Edge | { id: Edge['id'] })[] = [],
onBeforeDelete?: OnBeforeDelete
) => {
const _nodes = get(nodes);
const _edges = get(edges);
const { matchingNodes, matchingEdges } = getElementsToRemove<Node, Edge>({
const { nodes: matchingNodes, edges: matchingEdges } = await getElementsToRemove({
nodesToRemove,
edgesToRemove,
nodes: _nodes,
edges: _edges
nodes: get(nodes),
edges: get(edges),
onBeforeDelete
});
if (matchingNodes) {
nodes.set(_nodes.filter((node) => !matchingNodes.some(({ id }) => id === node.id)));
nodes.update((nds) =>
nds.filter((node) => !matchingNodes.some(({ id }) => id === node.id))
);
}
if (matchingEdges) {
edges.set(_edges.filter((edge) => !matchingEdges.some(({ id }) => id === edge.id)));
edges.update((eds) =>
eds.filter((edge) => !matchingEdges.some(({ id }) => id === edge.id))
);
}
return {

View File

@@ -208,18 +208,19 @@ export function createStore({
}
}
store.deleteKeyPressed.subscribe((deleteKeyPressed) => {
store.deleteKeyPressed.subscribe(async (deleteKeyPressed) => {
if (deleteKeyPressed) {
const nodes = get(store.nodes);
const edges = get(store.edges);
const selectedNodes = nodes.filter((node) => node.selected);
const selectedEdges = edges.filter((edge) => edge.selected);
const { matchingNodes, matchingEdges } = getElementsToRemove<Node, Edge>({
const { nodes: matchingNodes, edges: matchingEdges } = await getElementsToRemove({
nodesToRemove: selectedNodes,
edgesToRemove: selectedEdges,
nodes,
edges
edges,
onBeforeDelete: get(store.onbeforedelete)
});
if (matchingNodes.length || matchingEdges.length) {

View File

@@ -22,7 +22,8 @@ import {
type OnConnect,
type OnConnectStart,
type OnConnectEnd,
type NodeLookup
type NodeLookup,
type OnBeforeDelete
} from '@xyflow/system';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
@@ -148,6 +149,7 @@ export const getInitialStore = ({
onedgecreate: writable<OnEdgeCreate>(undefined),
onconnect: writable<OnConnect>(undefined),
onconnectstart: writable<OnConnectStart>(undefined),
onconnectend: writable<OnConnectEnd>(undefined)
onconnectend: writable<OnConnectEnd>(undefined),
onbeforedelete: writable<OnBeforeDelete>(undefined)
};
};

View File

@@ -141,3 +141,11 @@ export type ColorModeClass = 'light' | 'dark';
export type ColorMode = ColorModeClass | 'system';
export type ConnectionLookup = Map<string, Map<string, Connection>>;
export type OnBeforeDelete = <NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>({
nodes,
edges,
}: {
nodes: NodeType[];
edges: EdgeType[];
}) => Promise<boolean | { nodes: NodeType[]; edges: EdgeType[] }>;

View File

@@ -22,6 +22,7 @@ import {
NodeDragItem,
CoordinateExtent,
OnError,
OnBeforeDelete,
} from '../types';
import { errorMessages } from '../constants';
@@ -332,47 +333,69 @@ export function calcNextPosition<NodeType extends NodeBase>(
* @param param.edgesToRemove - The edges to remove
* @param param.nodes - All nodes
* @param param.edges - All edges
* @returns matchingNodes: nodes that can be deleted, matchingEdges: edges that can be deleted
* @param param.onBeforeDelete - Callback to check which nodes and edges can be deleted
* @returns nodes: nodes that can be deleted, edges: edges that can be deleted
*/
export function getElementsToRemove<NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>({
nodesToRemove,
edgesToRemove,
export async function getElementsToRemove<NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>({
nodesToRemove = [],
edgesToRemove = [],
nodes,
edges,
onBeforeDelete,
}: {
nodesToRemove: Partial<NodeType>[];
edgesToRemove: Partial<EdgeType>[];
nodes: NodeType[];
edges: EdgeType[];
}): {
matchingNodes: NodeType[];
matchingEdges: EdgeType[];
} {
onBeforeDelete?: OnBeforeDelete;
}): Promise<{
nodes: NodeType[];
edges: EdgeType[];
}> {
const nodeIds = nodesToRemove.map((node) => node.id);
const matchingNodes: NodeType[] = [];
for (const node of nodes) {
if (node.deletable === false) {
continue;
}
const isIncluded = nodeIds.includes(node.id);
const parentHit = !isIncluded && node.parentNode && matchingNodes.find((n) => n.id === node.parentNode);
if (isIncluded || parentHit) {
matchingNodes.push(node);
}
}
const edgeIds = edgesToRemove.map((edge) => edge.id);
const deletableEdges = edges.filter((edge) => edge.deletable !== false);
const connectedEdges = getConnectedEdgesBase(matchingNodes, deletableEdges);
const matchingEdges: EdgeType[] = connectedEdges;
const matchingNodes = nodes.reduce<NodeType[]>((res, node) => {
const parentHit = !nodeIds.includes(node.id) && node.parentNode && res.find((n) => n.id === node.parentNode);
const deletable = typeof node.deletable === 'boolean' ? node.deletable : true;
if (deletable && (nodeIds.includes(node.id) || parentHit)) {
res.push(node);
for (const edge of deletableEdges) {
const isIncluded = edgeIds.includes(edge.id);
if (isIncluded && !matchingEdges.find((e) => e.id === edge.id)) {
matchingEdges.push(edge);
}
}
return res;
}, []);
const deletableEdges = edges.filter((e) => (typeof e.deletable === 'boolean' ? e.deletable : true));
const initialHitEdges = deletableEdges.filter((e) => edgeIds.includes(e.id));
const connectedEdges = getConnectedEdgesBase<NodeType, EdgeType>(matchingNodes, deletableEdges);
const matchingEdges = connectedEdges.reduce((res, edge) => {
if (!res.find((e) => e.id === edge.id)) {
res.push(edge);
}
if (!onBeforeDelete) {
return {
edges: matchingEdges,
nodes: matchingNodes,
};
}
return res;
}, initialHitEdges);
const onBeforeDeleteResult = await onBeforeDelete({
nodes: matchingNodes,
edges: matchingEdges,
});
return {
matchingEdges,
matchingNodes,
};
if (typeof onBeforeDeleteResult === 'boolean') {
return onBeforeDeleteResult ? { edges: matchingEdges, nodes: matchingNodes } : { edges: [], nodes: [] };
}
return onBeforeDeleteResult;
}