diff --git a/packages/core/src/hooks/useReactFlow.ts b/packages/core/src/hooks/useReactFlow.ts index 500fa15b..6f8a72f9 100644 --- a/packages/core/src/hooks/useReactFlow.ts +++ b/packages/core/src/hooks/useReactFlow.ts @@ -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(): ReactFlo onNodesChange, onEdgesChange, } = store.getState(); - const nodeIds = (nodesDeleted || []).map((node) => node.id); - const edgeIds = (edgesDeleted || []).map((edge) => edge.id); - const nodesToRemove = getNodes().reduce((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((res, edge) => { - if (!res.includes(edge.id)) { - res.push(edge.id); - } - return res; - }, []); + const { matchingNodes, matchingEdges } = getElementsToRemove({ + 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(): 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); } } diff --git a/packages/svelte/src/lib/components/Handle/Handle.svelte b/packages/svelte/src/lib/components/Handle/Handle.svelte index 38502a92..89834704 100644 --- a/packages/svelte/src/lib/components/Handle/Handle.svelte +++ b/packages/svelte/src/lib/components/Handle/Handle.svelte @@ -40,9 +40,6 @@ function onConnectExtended(params: Connection) { addEdge(params); - // @todo add props - // onConnectAction?.(edgeParams); - // onConnect?.(edgeParams); dispatchEvent('connect') } diff --git a/packages/svelte/src/lib/container/Pane/Pane.svelte b/packages/svelte/src/lib/container/Pane/Pane.svelte index 138aadfa..bd8d9f74 100644 --- a/packages/svelte/src/lib/container/Pane/Pane.svelte +++ b/packages/svelte/src/lib/container/Pane/Pane.svelte @@ -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); }; diff --git a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte index 2a07ad32..cd385eb9 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte @@ -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 @@
diff --git a/packages/svelte/src/lib/container/SvelteFlow/types.ts b/packages/svelte/src/lib/container/SvelteFlow/types.ts index 4cefa634..2403b67a 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/types.ts +++ b/packages/svelte/src/lib/container/SvelteFlow/types.ts @@ -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:mouseleave': CustomEvent; 'edge:click': CustomEvent; - 'connect:start': CustomEvent; - connect: CustomEvent; - 'connect:end': CustomEvent; + 'connect:start': CustomEvent; + connect: CustomEvent; + 'connect:end': CustomEvent; 'pane:click': CustomEvent; 'pane:contextmenu': CustomEvent; }; diff --git a/packages/svelte/src/lib/store/index.ts b/packages/svelte/src/lib/store/index.ts index 90cfba3b..2486d852 100644 --- a/packages/svelte/src/lib/store/index.ts +++ b/packages/svelte/src/lib/store/index.ts @@ -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((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({ + 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((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)) + ); } } }); diff --git a/packages/svelte/src/lib/store/initial-store.ts b/packages/svelte/src/lib/store/initial-store.ts index 6258c0c1..289d0710 100644 --- a/packages/svelte/src/lib/store/initial-store.ts +++ b/packages/svelte/src/lib/store/initial-store.ts @@ -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(null), dragging: writable(false), selectionRect: writable(null), selectionKeyPressed: writable(false), diff --git a/packages/svelte/src/routes/+page.svelte b/packages/svelte/src/routes/+page.svelte index 2b3a95c1..c21e7221 100644 --- a/packages/svelte/src/routes/+page.svelte +++ b/packages/svelte/src/routes/+page.svelte @@ -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); }} > diff --git a/packages/utils/src/utils.ts b/packages/utils/src/utils.ts index 48146da8..e27a30ea 100644 --- a/packages/utils/src/utils.ts +++ b/packages/utils/src/utils.ts @@ -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({ + nodesToRemove, + edgesToRemove, + nodes, + edges, +}: { + nodesToRemove: Partial[]; + edgesToRemove: Partial[]; + 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((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(matchingNodes, deletableEdges); + const matchingEdges = [...initialHitEdges, ...connectedEdges]; + + return { + matchingEdges, + matchingNodes, + }; +}