refactor(utils): add getElementsToRemove and use in react and svelte packages

This commit is contained in:
moklick
2023-03-02 13:19:21 +01:00
parent e2961bfa8c
commit 3b83e029e6
9 changed files with 117 additions and 89 deletions
+18 -33
View File
@@ -1,10 +1,9 @@
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { getOverlappingArea, isRectObject, nodeToRect } from '@reactflow/utils'; import { getElementsToRemove, getOverlappingArea, isRectObject, nodeToRect } from '@reactflow/utils';
import type { Rect } from '@reactflow/system'; import type { Rect } from '@reactflow/system';
import useViewportHelper from './useViewportHelper'; import useViewportHelper from './useViewportHelper';
import { useStoreApi } from '../hooks/useStore'; import { useStoreApi } from '../hooks/useStore';
import { getConnectedEdges } from '../utils';
import type { import type {
ReactFlowInstance, ReactFlowInstance,
Instance, Instance,
@@ -16,6 +15,7 @@ import type {
EdgeRemoveChange, EdgeRemoveChange,
NodeChange, NodeChange,
Node, Node,
Edge,
} from '../types'; } from '../types';
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
@@ -127,38 +127,23 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
onNodesChange, onNodesChange,
onEdgesChange, onEdgesChange,
} = store.getState(); } = store.getState();
const nodeIds = (nodesDeleted || []).map((node) => node.id); const { matchingNodes, matchingEdges } = getElementsToRemove<Node, Edge>({
const edgeIds = (edgesDeleted || []).map((edge) => edge.id); nodesToRemove: nodesDeleted || [],
const nodesToRemove = getNodes().reduce<Node[]>((res, node) => { edgesToRemove: edgesDeleted || [],
const parentHit = !nodeIds.includes(node.id) && node.parentNode && res.find((n) => n.id === node.parentNode); nodes: getNodes(),
const deletable = typeof node.deletable === 'boolean' ? node.deletable : true; edges,
if (deletable && (nodeIds.includes(node.id) || parentHit)) { });
res.push(node);
}
return res;
}, []);
const deletableEdges = edges.filter((e) => (typeof e.deletable === 'boolean' ? e.deletable : true));
const initialHitEdges = deletableEdges.filter((e) => edgeIds.includes(e.id));
if (nodesToRemove || initialHitEdges) {
const connectedEdges = getConnectedEdges(nodesToRemove, deletableEdges);
const edgesToRemove = [...initialHitEdges, ...connectedEdges];
const edgeIdsToRemove = edgesToRemove.reduce<string[]>((res, edge) => {
if (!res.includes(edge.id)) {
res.push(edge.id);
}
return res;
}, []);
if (matchingNodes.length || matchingEdges.length) {
if (hasDefaultEdges || hasDefaultNodes) { if (hasDefaultEdges || hasDefaultNodes) {
if (hasDefaultEdges) { if (hasDefaultEdges) {
store.setState({ store.setState({
edges: edges.filter((e) => !edgeIdsToRemove.includes(e.id)), edges: edges.filter((e) => !matchingEdges.some((mE) => mE.id === e.id)),
}); });
} }
if (hasDefaultNodes) { if (hasDefaultNodes) {
nodesToRemove.forEach((node) => { matchingNodes.forEach((node) => {
nodeInternals.delete(node.id); nodeInternals.delete(node.id);
}); });
@@ -168,24 +153,24 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
} }
} }
if (edgeIdsToRemove.length > 0) { if (matchingEdges.length > 0) {
onEdgesDelete?.(edgesToRemove); onEdgesDelete?.(matchingEdges);
if (onEdgesChange) { if (onEdgesChange) {
onEdgesChange( onEdgesChange(
edgeIdsToRemove.map((id) => ({ matchingEdges.map((edge) => ({
id, id: edge.id,
type: 'remove', type: 'remove',
})) }))
); );
} }
} }
if (nodesToRemove.length > 0) { if (matchingNodes.length > 0) {
onNodesDelete?.(nodesToRemove); onNodesDelete?.(matchingNodes as Node[]);
if (onNodesChange) { if (onNodesChange) {
const nodeChanges: NodeChange[] = nodesToRemove.map((n) => ({ id: n.id, type: 'remove' })); const nodeChanges: NodeChange[] = matchingNodes.map((node) => ({ id: node.id, type: 'remove' }));
onNodesChange(nodeChanges); onNodesChange(nodeChanges);
} }
} }
@@ -40,9 +40,6 @@
function onConnectExtended(params: Connection) { function onConnectExtended(params: Connection) {
addEdge(params); addEdge(params);
// @todo add props
// onConnectAction?.(edgeParams);
// onConnect?.(edgeParams);
dispatchEvent('connect') dispatchEvent('connect')
} }
@@ -62,7 +62,7 @@
$: hasActiveSelection = elementsSelectable && (isSelecting || $selectionRectMode === 'user'); $: hasActiveSelection = elementsSelectable && (isSelecting || $selectionRectMode === 'user');
function onClick(event: MouseEvent) { function onClick(event: MouseEvent) {
dispatch('pane:click'); dispatch('pane:click', event);
resetSelectedElements(); resetSelectedElements();
selectionRectMode.set(null); selectionRectMode.set(null);
@@ -164,7 +164,7 @@
// return; // return;
// } // }
dispatch('pane:contextmenu'); dispatch('pane:contextmenu', event);
}; };
</script> </script>
@@ -28,11 +28,12 @@
export let edgeTypes: $$Props['edgeTypes'] = undefined; export let edgeTypes: $$Props['edgeTypes'] = undefined;
export let selectionKey: $$Props['selectionKey'] = undefined; export let selectionKey: $$Props['selectionKey'] = undefined;
export let deleteKey: $$Props['deleteKey'] = undefined; export let deleteKey: $$Props['deleteKey'] = undefined;
export let defaultEdgeOptions: $$Props['defaultEdgeOptions'] = undefined;
export let connectionRadius: $$Props['connectionRadius'] = undefined;
export let connectionLineType: $$Props['connectionLineType'] = undefined export let connectionLineType: $$Props['connectionLineType'] = undefined
export let style: $$Props['style'] = undefined;
let className: $$Props['class'] = undefined; let className: $$Props['class'] = undefined;
export { className as class }; export { className as class };
const dispatch = createEventDispatcher<{ 'on:nodeclick': number }>();
let domNode: HTMLDivElement; let domNode: HTMLDivElement;
@@ -41,7 +42,6 @@
nodeTypes nodeTypes
}); });
setContext(key, { setContext(key, {
getStore: () => store getStore: () => store
}); });
@@ -66,19 +66,15 @@
const updatableProps = { const updatableProps = {
id, id,
connectionLineType, connectionLineType,
connectionRadius,
defaultEdgeOptions
}; };
Object.keys(updatableProps).forEach(prop => { Object.keys(updatableProps).forEach(prop => {
// @ts-ignore // @ts-ignore
if (updatableProps[prop] !== undefined) { if (updatableProps[prop] !== undefined) {
// @ts-ignore
if (!store[prop]) {
// @ts-ignore
console.warn(store[prop], prop, 'ups')
} else {
// @ts-ignore // @ts-ignore
store[prop].set(updatableProps[prop]); store[prop].set(updatableProps[prop]);
}
} }
}) })
} }
@@ -111,9 +107,10 @@
</script> </script>
<div <div
bind:this={domNode}
style={style}
class={cc(['svelte-flow', className])} class={cc(['svelte-flow', className])}
data-testid="rf__wrapper" data-testid="rf__wrapper"
bind:this={domNode}
> >
<KeyHandler {selectionKey} {deleteKey} /> <KeyHandler {selectionKey} {deleteKey} />
<Zoom {initialViewport}> <Zoom {initialViewport}>
@@ -1,4 +1,10 @@
import type { ConnectionLineType, NodeOrigin, Viewport } from '@reactflow/system'; import type {
Connection,
ConnectionLineType,
NodeOrigin,
OnConnectStartParams,
Viewport
} from '@reactflow/system';
import type { import type {
Edge, Edge,
@@ -24,6 +30,7 @@ export type SvelteFlowProps = {
maxZoom?: number; maxZoom?: number;
initialViewport?: Viewport; initialViewport?: Viewport;
defaultEdgeOptions?: DefaultEdgeOptions; defaultEdgeOptions?: DefaultEdgeOptions;
connectionRadius?: number;
class?: string; class?: string;
style?: string; style?: string;
@@ -37,9 +44,9 @@ export type SvelteFlowEvents = {
'node:mousemove': CustomEvent<Node>; 'node:mousemove': CustomEvent<Node>;
'node:mouseleave': CustomEvent<Node>; 'node:mouseleave': CustomEvent<Node>;
'edge:click': CustomEvent<Edge>; 'edge:click': CustomEvent<Edge>;
'connect:start': CustomEvent<number>; 'connect:start': CustomEvent<OnConnectStartParams>;
connect: CustomEvent<number>; connect: CustomEvent<Connection>;
'connect:end': CustomEvent<number>; 'connect:end': CustomEvent<OnConnectStartParams>;
'pane:click': CustomEvent; 'pane:click': CustomEvent;
'pane:contextmenu': CustomEvent; 'pane:contextmenu': CustomEvent;
}; };
+25 -34
View File
@@ -12,7 +12,12 @@ import {
type XYPosition, type XYPosition,
type CoordinateExtent type CoordinateExtent
} from '@reactflow/system'; } from '@reactflow/system';
import { fitView as fitViewUtil, getD3Transition, getDimensions } from '@reactflow/utils'; import {
fitView as fitViewUtil,
getD3Transition,
getDimensions,
getElementsToRemove
} from '@reactflow/utils';
import { getHandleBounds, getConnectedEdges, addEdge as addEdgeUtil } from '$lib/utils'; import { getHandleBounds, getConnectedEdges, addEdge as addEdgeUtil } from '$lib/utils';
import type { EdgeTypes, NodeTypes, Node, Edge, ConnectionData } from '$lib/types'; import type { EdgeTypes, NodeTypes, Node, Edge, ConnectionData } from '$lib/types';
@@ -59,12 +64,16 @@ export function createStore({ fitView: fitViewOnInit = false }: CreateStoreProps
} }
function setEdges(edges: Edge[]) { function setEdges(edges: Edge[]) {
store.edges.set(edges); const defaultEdgeOptions = get(store.defaultEdgeOptions);
const nextEdges = defaultEdgeOptions
? edges.map((e) => ({ ...defaultEdgeOptions, ...e }))
: edges;
store.edges.set(nextEdges);
} }
function addEdge(edgeParams: Edge | Connection) { function addEdge(edgeParams: Edge | Connection) {
const edges = get(store.edges); const edges = get(store.edges);
store.edges.set(addEdgeUtil(edgeParams, edges)); setEdges(addEdgeUtil(edgeParams, edges));
} }
function setNodes(nodes: Node[]) { function setNodes(nodes: Node[]) {
@@ -229,38 +238,20 @@ export function createStore({ fitView: fitViewOnInit = false }: CreateStoreProps
const selectedNodes = nodes.filter((node) => node.selected); const selectedNodes = nodes.filter((node) => node.selected);
const selectedEdges = edges.filter((edge) => edge.selected); const selectedEdges = edges.filter((edge) => edge.selected);
// @todo can we put this stuff into @reactflow/utils? const { matchingNodes, matchingEdges } = getElementsToRemove<Node, Edge>({
const nodeIds = selectedNodes.map((node) => node.id); nodesToRemove: selectedNodes,
const edgeIds = selectedEdges.map((edge) => edge.id); edgesToRemove: selectedEdges,
const nodesToRemove = nodes.reduce<Node[]>((res, node) => { nodes,
const parentHit = edges
!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);
}
return res; if (matchingNodes.length || matchingEdges.length) {
}, []); store.nodes.update((nds) =>
const deletableEdges = edges.filter((e) => nds.filter((node) => !matchingNodes.some((mN) => mN.id === node.id))
typeof e.deletable === 'boolean' ? e.deletable : true );
); store.edges.update((eds) =>
const initialHitEdges = deletableEdges.filter((e) => edgeIds.includes(e.id)); eds.filter((edge) => !matchingEdges.some((mE) => mE.id === edge.id))
);
if (nodesToRemove || initialHitEdges) {
const connectedEdges = getConnectedEdges(nodesToRemove, deletableEdges);
const edgesToRemove = [...initialHitEdges, ...connectedEdges];
const edgeIdsToRemove = edgesToRemove.reduce<string[]>((res, edge) => {
if (!res.includes(edge.id)) {
res.push(edge.id);
}
return res;
}, []);
store.nodes.update((nds) => nds.filter((node) => !nodeIds.includes(node.id)));
store.edges.update((eds) => eds.filter((edge) => !edgeIdsToRemove.includes(edge.id)));
} }
} }
}); });
+10 -1
View File
@@ -16,7 +16,15 @@ import OutputNode from '$lib/components/nodes/OutputNode.svelte';
import BezierEdge from '$lib/components/edges/BezierEdge.svelte'; import BezierEdge from '$lib/components/edges/BezierEdge.svelte';
import StraightEdge from '$lib/components/edges/StraightEdge.svelte'; import StraightEdge from '$lib/components/edges/StraightEdge.svelte';
import SmoothStepEdge from '$lib/components/edges/SmoothStepEdge.svelte'; import SmoothStepEdge from '$lib/components/edges/SmoothStepEdge.svelte';
import type { Node, Edge, ConnectionData, NodeTypes, EdgeTypes, EdgeLayouted } from '$lib/types'; import type {
Node,
Edge,
ConnectionData,
NodeTypes,
EdgeTypes,
EdgeLayouted,
DefaultEdgeOptions
} from '$lib/types';
export const initConnectionData = { export const initConnectionData = {
nodeId: null, nodeId: null,
@@ -52,6 +60,7 @@ export const initialStoreState = {
zoom: null, zoom: null,
selection: null selection: null
}), }),
defaultEdgeOptions: writable<DefaultEdgeOptions | null>(null),
dragging: writable<boolean>(false), dragging: writable<boolean>(false),
selectionRect: writable<SelectionRect | null>(null), selectionRect: writable<SelectionRect | null>(null),
selectionKeyPressed: writable<boolean>(false), selectionKeyPressed: writable<boolean>(false),
+2 -1
View File
@@ -128,6 +128,7 @@
minZoom={0.1} minZoom={0.1}
maxZoom={2.5} maxZoom={2.5}
initialViewport={{ x: 100, y: 100, zoom: 2 }} initialViewport={{ x: 100, y: 100, zoom: 2 }}
defaultEdgeOptions={{ animated: true }}
on:node:click={(event) => console.log('on node click', event)} on:node:click={(event) => console.log('on node click', event)}
on:node:mouseenter={(event) => console.log('on node enter', event)} on:node:mouseenter={(event) => console.log('on node enter', event)}
on:node:mouseleave={(event) => console.log('on node leave', event)} on:node:mouseleave={(event) => console.log('on node leave', event)}
@@ -136,7 +137,7 @@
on:connect={(event) => console.log('on connect', event)} on:connect={(event) => console.log('on connect', event)}
on:connect:end={(event) => console.log('on connect end', event)} on:connect:end={(event) => console.log('on connect end', event)}
on:pane:click={(event) => console.log('on pane click', event)} on:pane:click={(event) => console.log('on pane click', event)}
on:pane:contextmenu={(event) => { console.log('on pane contextmenu', event); }} on:pane:contextmenu={(event) => { event.preventDefault(); console.log('on pane contextmenu', event); }}
> >
<Controls /> <Controls />
<Background variant={BackgroundVariant.Dots} /> <Background variant={BackgroundVariant.Dots} />
+42 -1
View File
@@ -3,7 +3,8 @@ import type {
MouseEvent as ReactMouseEvent, MouseEvent as ReactMouseEvent,
TouchEvent as ReactTouchEvent, TouchEvent as ReactTouchEvent,
} from 'react'; } from 'react';
import type { Dimensions, XYPosition, CoordinateExtent, Box, Rect, BaseNode } from '@reactflow/system'; import type { Dimensions, XYPosition, CoordinateExtent, Box, Rect, BaseNode, BaseEdge } from '@reactflow/system';
import { getConnectedEdgesBase } from './graph';
export const getDimensions = (node: HTMLDivElement): Dimensions => ({ export const getDimensions = (node: HTMLDivElement): Dimensions => ({
width: node.offsetWidth, width: node.offsetWidth,
@@ -130,3 +131,43 @@ export const infiniteExtent: CoordinateExtent = [
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY], [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY], [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
]; ];
// helper function to get arrays of nodes and edges that can be deleted
// you can pass in a list of nodes and edges that should be deleted
// and the function only returns elements that are deletable and also handles connected nodes and child nodes
export function getElementsToRemove<NodeType extends BaseNode = BaseNode, EdgeType extends BaseEdge = BaseEdge>({
nodesToRemove,
edgesToRemove,
nodes,
edges,
}: {
nodesToRemove: Partial<NodeType>[];
edgesToRemove: Partial<EdgeType>[];
nodes: NodeType[];
edges: EdgeType[];
}): {
matchingNodes: NodeType[];
matchingEdges: EdgeType[];
} {
const nodeIds = nodesToRemove.map((node) => node.id);
const edgeIds = edgesToRemove.map((edge) => edge.id);
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);
}
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 = [...initialHitEdges, ...connectedEdges];
return {
matchingEdges,
matchingNodes,
};
}