refactor(utils): add getElementsToRemove and use in react and svelte packages
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
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 useViewportHelper from './useViewportHelper';
|
||||
import { useStoreApi } from '../hooks/useStore';
|
||||
import { getConnectedEdges } from '../utils';
|
||||
import type {
|
||||
ReactFlowInstance,
|
||||
Instance,
|
||||
@@ -16,6 +15,7 @@ import type {
|
||||
EdgeRemoveChange,
|
||||
NodeChange,
|
||||
Node,
|
||||
Edge,
|
||||
} from '../types';
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
@@ -127,38 +127,23 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
|
||||
onNodesChange,
|
||||
onEdgesChange,
|
||||
} = store.getState();
|
||||
const nodeIds = (nodesDeleted || []).map((node) => node.id);
|
||||
const edgeIds = (edgesDeleted || []).map((edge) => edge.id);
|
||||
const nodesToRemove = getNodes().reduce<Node[]>((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));
|
||||
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;
|
||||
}, []);
|
||||
const { matchingNodes, matchingEdges } = getElementsToRemove<Node, Edge>({
|
||||
nodesToRemove: nodesDeleted || [],
|
||||
edgesToRemove: edgesDeleted || [],
|
||||
nodes: getNodes(),
|
||||
edges,
|
||||
});
|
||||
|
||||
if (matchingNodes.length || matchingEdges.length) {
|
||||
if (hasDefaultEdges || hasDefaultNodes) {
|
||||
if (hasDefaultEdges) {
|
||||
store.setState({
|
||||
edges: edges.filter((e) => !edgeIdsToRemove.includes(e.id)),
|
||||
edges: edges.filter((e) => !matchingEdges.some((mE) => mE.id === e.id)),
|
||||
});
|
||||
}
|
||||
|
||||
if (hasDefaultNodes) {
|
||||
nodesToRemove.forEach((node) => {
|
||||
matchingNodes.forEach((node) => {
|
||||
nodeInternals.delete(node.id);
|
||||
});
|
||||
|
||||
@@ -168,24 +153,24 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
|
||||
}
|
||||
}
|
||||
|
||||
if (edgeIdsToRemove.length > 0) {
|
||||
onEdgesDelete?.(edgesToRemove);
|
||||
if (matchingEdges.length > 0) {
|
||||
onEdgesDelete?.(matchingEdges);
|
||||
|
||||
if (onEdgesChange) {
|
||||
onEdgesChange(
|
||||
edgeIdsToRemove.map((id) => ({
|
||||
id,
|
||||
matchingEdges.map((edge) => ({
|
||||
id: edge.id,
|
||||
type: 'remove',
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (nodesToRemove.length > 0) {
|
||||
onNodesDelete?.(nodesToRemove);
|
||||
if (matchingNodes.length > 0) {
|
||||
onNodesDelete?.(matchingNodes as Node[]);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,9 +40,6 @@
|
||||
|
||||
function onConnectExtended(params: Connection) {
|
||||
addEdge(params);
|
||||
// @todo add props
|
||||
// onConnectAction?.(edgeParams);
|
||||
// onConnect?.(edgeParams);
|
||||
dispatchEvent('connect')
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
$: hasActiveSelection = elementsSelectable && (isSelecting || $selectionRectMode === 'user');
|
||||
|
||||
function onClick(event: MouseEvent) {
|
||||
dispatch('pane:click');
|
||||
dispatch('pane:click', event);
|
||||
|
||||
resetSelectedElements();
|
||||
selectionRectMode.set(null);
|
||||
@@ -164,7 +164,7 @@
|
||||
// return;
|
||||
// }
|
||||
|
||||
dispatch('pane:contextmenu');
|
||||
dispatch('pane:contextmenu', event);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
@@ -28,11 +28,12 @@
|
||||
export let edgeTypes: $$Props['edgeTypes'] = undefined;
|
||||
export let selectionKey: $$Props['selectionKey'] = 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 style: $$Props['style'] = undefined;
|
||||
let className: $$Props['class'] = undefined;
|
||||
export { className as class };
|
||||
const dispatch = createEventDispatcher<{ 'on:nodeclick': number }>();
|
||||
|
||||
|
||||
let domNode: HTMLDivElement;
|
||||
|
||||
@@ -41,7 +42,6 @@
|
||||
nodeTypes
|
||||
});
|
||||
|
||||
|
||||
setContext(key, {
|
||||
getStore: () => store
|
||||
});
|
||||
@@ -66,19 +66,15 @@
|
||||
const updatableProps = {
|
||||
id,
|
||||
connectionLineType,
|
||||
connectionRadius,
|
||||
defaultEdgeOptions
|
||||
};
|
||||
|
||||
Object.keys(updatableProps).forEach(prop => {
|
||||
// @ts-ignore
|
||||
if (updatableProps[prop] !== undefined) {
|
||||
// @ts-ignore
|
||||
if (!store[prop]) {
|
||||
// @ts-ignore
|
||||
console.warn(store[prop], prop, 'ups')
|
||||
} else {
|
||||
// @ts-ignore
|
||||
store[prop].set(updatableProps[prop]);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -111,9 +107,10 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={domNode}
|
||||
style={style}
|
||||
class={cc(['svelte-flow', className])}
|
||||
data-testid="rf__wrapper"
|
||||
bind:this={domNode}
|
||||
>
|
||||
<KeyHandler {selectionKey} {deleteKey} />
|
||||
<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 {
|
||||
Edge,
|
||||
@@ -24,6 +30,7 @@ export type SvelteFlowProps = {
|
||||
maxZoom?: number;
|
||||
initialViewport?: Viewport;
|
||||
defaultEdgeOptions?: DefaultEdgeOptions;
|
||||
connectionRadius?: number;
|
||||
|
||||
class?: string;
|
||||
style?: string;
|
||||
@@ -37,9 +44,9 @@ export type SvelteFlowEvents = {
|
||||
'node:mousemove': CustomEvent<Node>;
|
||||
'node:mouseleave': CustomEvent<Node>;
|
||||
'edge:click': CustomEvent<Edge>;
|
||||
'connect:start': CustomEvent<number>;
|
||||
connect: CustomEvent<number>;
|
||||
'connect:end': CustomEvent<number>;
|
||||
'connect:start': CustomEvent<OnConnectStartParams>;
|
||||
connect: CustomEvent<Connection>;
|
||||
'connect:end': CustomEvent<OnConnectStartParams>;
|
||||
'pane:click': CustomEvent;
|
||||
'pane:contextmenu': CustomEvent;
|
||||
};
|
||||
|
||||
@@ -12,7 +12,12 @@ import {
|
||||
type XYPosition,
|
||||
type CoordinateExtent
|
||||
} 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 type { EdgeTypes, NodeTypes, Node, Edge, ConnectionData } from '$lib/types';
|
||||
@@ -59,12 +64,16 @@ export function createStore({ fitView: fitViewOnInit = false }: CreateStoreProps
|
||||
}
|
||||
|
||||
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) {
|
||||
const edges = get(store.edges);
|
||||
store.edges.set(addEdgeUtil(edgeParams, edges));
|
||||
setEdges(addEdgeUtil(edgeParams, edges));
|
||||
}
|
||||
|
||||
function setNodes(nodes: Node[]) {
|
||||
@@ -229,38 +238,20 @@ export function createStore({ fitView: fitViewOnInit = false }: CreateStoreProps
|
||||
const selectedNodes = nodes.filter((node) => node.selected);
|
||||
const selectedEdges = edges.filter((edge) => edge.selected);
|
||||
|
||||
// @todo can we put this stuff into @reactflow/utils?
|
||||
const nodeIds = selectedNodes.map((node) => node.id);
|
||||
const edgeIds = selectedEdges.map((edge) => edge.id);
|
||||
const nodesToRemove = nodes.reduce<Node[]>((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);
|
||||
}
|
||||
const { matchingNodes, matchingEdges } = getElementsToRemove<Node, Edge>({
|
||||
nodesToRemove: selectedNodes,
|
||||
edgesToRemove: selectedEdges,
|
||||
nodes,
|
||||
edges
|
||||
});
|
||||
|
||||
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;
|
||||
}, []);
|
||||
|
||||
store.nodes.update((nds) => nds.filter((node) => !nodeIds.includes(node.id)));
|
||||
store.edges.update((eds) => eds.filter((edge) => !edgeIdsToRemove.includes(edge.id)));
|
||||
if (matchingNodes.length || matchingEdges.length) {
|
||||
store.nodes.update((nds) =>
|
||||
nds.filter((node) => !matchingNodes.some((mN) => mN.id === node.id))
|
||||
);
|
||||
store.edges.update((eds) =>
|
||||
eds.filter((edge) => !matchingEdges.some((mE) => mE.id === edge.id))
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -16,7 +16,15 @@ import OutputNode from '$lib/components/nodes/OutputNode.svelte';
|
||||
import BezierEdge from '$lib/components/edges/BezierEdge.svelte';
|
||||
import StraightEdge from '$lib/components/edges/StraightEdge.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 = {
|
||||
nodeId: null,
|
||||
@@ -52,6 +60,7 @@ export const initialStoreState = {
|
||||
zoom: null,
|
||||
selection: null
|
||||
}),
|
||||
defaultEdgeOptions: writable<DefaultEdgeOptions | null>(null),
|
||||
dragging: writable<boolean>(false),
|
||||
selectionRect: writable<SelectionRect | null>(null),
|
||||
selectionKeyPressed: writable<boolean>(false),
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
minZoom={0.1}
|
||||
maxZoom={2.5}
|
||||
initialViewport={{ x: 100, y: 100, zoom: 2 }}
|
||||
defaultEdgeOptions={{ animated: true }}
|
||||
on:node:click={(event) => console.log('on node click', event)}
|
||||
on:node:mouseenter={(event) => console.log('on node enter', 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:end={(event) => console.log('on connect end', 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 />
|
||||
<Background variant={BackgroundVariant.Dots} />
|
||||
|
||||
@@ -3,7 +3,8 @@ import type {
|
||||
MouseEvent as ReactMouseEvent,
|
||||
TouchEvent as ReactTouchEvent,
|
||||
} 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 => ({
|
||||
width: node.offsetWidth,
|
||||
@@ -130,3 +131,43 @@ export const infiniteExtent: CoordinateExtent = [
|
||||
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user