diff --git a/packages/react/src/components/EdgeWrapper/index.tsx b/packages/react/src/components/EdgeWrapper/index.tsx index 826e4178..926860ff 100644 --- a/packages/react/src/components/EdgeWrapper/index.tsx +++ b/packages/react/src/components/EdgeWrapper/index.tsx @@ -38,7 +38,9 @@ function EdgeWrapper({ noPanClassName, onError, }: EdgeWrapperProps): JSX.Element | null { - const edge = useStore((s) => s.edgeLookup.get(id)!); + let edge = useStore((s) => s.edgeLookup.get(id)!); + const defaultEdgeOptions = useStore((s) => s.defaultEdgeOptions); + edge = defaultEdgeOptions ? { ...defaultEdgeOptions, ...edge } : edge; let edgeType = edge.type || 'default'; let EdgeComponent = edgeTypes?.[edgeType] || builtinEdgeTypes[edgeType]; diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index cc593439..5db82d5b 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -18,7 +18,6 @@ import { handleNodeClick } from '../Nodes/utils'; import type { NodeWrapperProps } from '../../types'; import { arrowKeyDiffs, builtinNodeTypes } from './utils'; import { shallow } from 'zustand/shallow'; - const NodeWrapper = ({ id, onClick, diff --git a/packages/react/src/container/EdgeRenderer/index.tsx b/packages/react/src/container/EdgeRenderer/index.tsx index 8d786de6..83489d24 100644 --- a/packages/react/src/container/EdgeRenderer/index.tsx +++ b/packages/react/src/container/EdgeRenderer/index.tsx @@ -2,11 +2,11 @@ import { memo, ReactNode } from 'react'; import { shallow } from 'zustand/shallow'; import { useStore } from '../../hooks/useStore'; -import useVisibleEdgeIds from '../../hooks/useVisibleEdges'; +import useVisibleEdgeIds from '../../hooks/useVisibleEdgeIds'; import MarkerDefinitions from './MarkerDefinitions'; import { GraphViewProps } from '../GraphView'; -import type { ReactFlowState } from '../../types'; import EdgeWrapper from '../../components/EdgeWrapper'; +import type { ReactFlowState } from '../../types'; type EdgeRendererProps = Pick< GraphViewProps, diff --git a/packages/react/src/container/NodeRenderer/index.tsx b/packages/react/src/container/NodeRenderer/index.tsx index fa877984..e61b8bc5 100644 --- a/packages/react/src/container/NodeRenderer/index.tsx +++ b/packages/react/src/container/NodeRenderer/index.tsx @@ -1,7 +1,7 @@ import { memo } from 'react'; import { shallow } from 'zustand/shallow'; -import useVisibleNodesIds from '../../hooks/useVisibleNodes'; +import useVisibleNodesIds from '../../hooks/useVisibleNodeIds'; import { useStore } from '../../hooks/useStore'; import { containerStyle } from '../../styles/utils'; import { GraphViewProps } from '../GraphView'; diff --git a/packages/react/src/container/Pane/index.tsx b/packages/react/src/container/Pane/index.tsx index d5d951ca..6acd7770 100644 --- a/packages/react/src/container/Pane/index.tsx +++ b/packages/react/src/container/Pane/index.tsx @@ -10,7 +10,7 @@ import { getNodesInside, getEventPosition, SelectionMode } from '@xyflow/system' import UserSelection from '../../components/UserSelection'; import { containerStyle } from '../../styles/utils'; import { useStore, useStoreApi } from '../../hooks/useStore'; -import { getSelectionChanges, getConnectedEdges } from '../../utils'; +import { getSelectionChanges } from '../../utils'; import type { ReactFlowProps, ReactFlowState, NodeChange, EdgeChange } from '../../types'; type PaneProps = { @@ -156,19 +156,30 @@ const Pane = memo( true, nodeOrigin ); - const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id); - const selectedNodeIds = selectedNodes.map((n) => n.id); - if (prevSelectedNodesCount.current !== selectedNodeIds.length) { - prevSelectedNodesCount.current = selectedNodeIds.length; - const changes = getSelectionChanges(nodes, selectedNodeIds) as NodeChange[]; + const selectedEdgeIds = new Set(); + const selectedNodeIds = new Set(); + + for (const selectedNode of selectedNodes) { + selectedNodeIds.add(selectedNode.id); + + for (const edge of edges) { + if (edge.source === selectedNode.id || edge.target === selectedNode.id) { + selectedEdgeIds.add(edge.id); + } + } + } + + if (prevSelectedNodesCount.current !== selectedNodeIds.size) { + prevSelectedNodesCount.current = selectedNodeIds.size; + const changes = getSelectionChanges(nodes, selectedNodeIds, true) as NodeChange[]; if (changes.length) { onNodesChange?.(changes); } } - if (prevSelectedEdgesCount.current !== selectedEdgeIds.length) { - prevSelectedEdgesCount.current = selectedEdgeIds.length; + if (prevSelectedEdgesCount.current !== selectedEdgeIds.size) { + prevSelectedEdgesCount.current = selectedEdgeIds.size; const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[]; if (changes.length) { onEdgesChange?.(changes); diff --git a/packages/react/src/hooks/useVisibleEdgeIds.ts b/packages/react/src/hooks/useVisibleEdgeIds.ts new file mode 100644 index 00000000..eaa4eb2d --- /dev/null +++ b/packages/react/src/hooks/useVisibleEdgeIds.ts @@ -0,0 +1,49 @@ +import { useCallback } from 'react'; +import { shallow } from 'zustand/shallow'; +import { isEdgeVisible } from '@xyflow/system'; + +import { useStore } from './useStore'; +import { type ReactFlowState } from '../types'; + +function useVisibleEdgeIds(onlyRenderVisible: boolean): string[] { + const edgeIds = useStore( + useCallback( + (s: ReactFlowState) => { + if (!onlyRenderVisible) { + return s.edges.map((edge) => edge.id); + } + + const visibleEdgeIds = []; + + if (s.width && s.height) { + for (const edge of s.edges) { + const sourceNode = s.nodeLookup.get(edge.source); + const targetNode = s.nodeLookup.get(edge.target); + + if ( + sourceNode && + targetNode && + isEdgeVisible({ + sourceNode, + targetNode, + width: s.width, + height: s.height, + transform: s.transform, + }) + ) { + visibleEdgeIds.push(edge.id); + } + } + } + + return visibleEdgeIds; + }, + [onlyRenderVisible] + ), + shallow + ); + + return edgeIds; +} + +export default useVisibleEdgeIds; diff --git a/packages/react/src/hooks/useVisibleEdges.ts b/packages/react/src/hooks/useVisibleEdges.ts deleted file mode 100644 index 9280c27e..00000000 --- a/packages/react/src/hooks/useVisibleEdges.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { useCallback } from 'react'; -import { shallow } from 'zustand/shallow'; -import { isEdgeVisible } from '@xyflow/system'; - -import { useStore } from '../hooks/useStore'; -import { type ReactFlowState } from '../types'; - -function useVisibleEdgeIds(onlyRenderVisible: boolean): string[] { - const edgeIds = useStore( - useCallback( - (s: ReactFlowState) => { - const visibleEdges = - onlyRenderVisible && s.width && s.height - ? s.edges.filter((e) => { - const sourceNode = s.nodeLookup.get(e.source); - const targetNode = s.nodeLookup.get(e.target); - - return ( - sourceNode && - targetNode && - isEdgeVisible({ - sourceNode, - targetNode, - width: s.width, - height: s.height, - transform: s.transform, - }) - ); - }) - : s.edges; - - return visibleEdges.map((edge) => edge.id); - }, - [onlyRenderVisible] - ), - shallow - ); - - return edgeIds; -} - -export default useVisibleEdgeIds; diff --git a/packages/react/src/hooks/useVisibleNodes.ts b/packages/react/src/hooks/useVisibleNodeIds.ts similarity index 93% rename from packages/react/src/hooks/useVisibleNodes.ts rename to packages/react/src/hooks/useVisibleNodeIds.ts index 2f67af02..23b29096 100644 --- a/packages/react/src/hooks/useVisibleNodes.ts +++ b/packages/react/src/hooks/useVisibleNodeIds.ts @@ -1,7 +1,7 @@ import { getNodesInside } from '@xyflow/system'; import { shallow } from 'zustand/shallow'; -import { useStore } from '../hooks/useStore'; +import { useStore } from './useStore'; import type { Node, ReactFlowState } from '../types'; import { useCallback } from 'react'; diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 9b163f4e..13311a1b 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -55,12 +55,11 @@ const createRFStore = ({ set({ nodes: nodesWithInternalData }); }, setEdges: (edges: Edge[]) => { - const { defaultEdgeOptions = {}, connectionLookup, edgeLookup } = get(); - const nextEdges = edges.map((e) => ({ ...defaultEdgeOptions, ...e })); + const { connectionLookup, edgeLookup } = get(); - updateConnectionLookup(connectionLookup, edgeLookup, nextEdges); + updateConnectionLookup(connectionLookup, edgeLookup, edges); - set({ edges: nextEdges }); + set({ edges }); }, // when the user works with an uncontrolled flow, // we set a flag `hasDefaultNodes` / `hasDefaultEdges` @@ -196,8 +195,8 @@ const createRFStore = ({ if (multiSelectionActive) { changedNodes = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true)) as NodeSelectionChange[]; } else { - changedNodes = getSelectionChanges(nodes, selectedNodeIds); - changedEdges = getSelectionChanges(edges, []); + changedNodes = getSelectionChanges(nodes, new Set([...selectedNodeIds]), true); + changedEdges = getSelectionChanges(edges); } updateNodesAndEdgesSelections({ @@ -215,8 +214,8 @@ const createRFStore = ({ if (multiSelectionActive) { changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true)) as EdgeSelectionChange[]; } else { - changedEdges = getSelectionChanges(edges, selectedEdgeIds); - changedNodes = getSelectionChanges(nodes, []); + changedEdges = getSelectionChanges(edges, new Set([...selectedEdgeIds])); + changedNodes = getSelectionChanges(nodes, new Set(), true); } updateNodesAndEdgesSelections({ diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index e70d8598..87a4fc9e 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -49,7 +49,7 @@ const getInitialState = ({ transform, nodes: nextNodes, nodeLookup, - edges: edges, + edges, edgeLookup, connectionLookup, onNodesChange: null, diff --git a/packages/react/src/types/edges.ts b/packages/react/src/types/edges.ts index e3e2809a..a1a06725 100644 --- a/packages/react/src/types/edges.ts +++ b/packages/react/src/types/edges.ts @@ -34,9 +34,8 @@ export type EdgeUpdatable = boolean | HandleType; export type DefaultEdge = EdgeBase & { style?: CSSProperties; className?: string; - sourceNode?: Node; - targetNode?: Node; updatable?: EdgeUpdatable; + focusable?: boolean; } & EdgeLabelOptions; type SmoothStepEdgeType = DefaultEdge & { diff --git a/packages/react/src/types/nodes.ts b/packages/react/src/types/nodes.ts index 00c87704..27ad338e 100644 --- a/packages/react/src/types/nodes.ts +++ b/packages/react/src/types/nodes.ts @@ -2,6 +2,7 @@ import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react'; import type { CoordinateExtent, NodeBase, NodeOrigin, OnError } from '@xyflow/system'; import { NodeTypes } from './general'; +// eslint-disable-next-line @typescript-eslint/no-explicit-any export type Node = NodeBase< NodeData, NodeType diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index ab21c8c6..bf5a5c11 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -54,7 +54,6 @@ export type ReactFlowStore = { edges: Edge[]; edgeLookup: EdgeLookup; connectionLookup: ConnectionLookup; - onNodesChange: OnNodesChange | null; onEdgesChange: OnEdgesChange | null; hasDefaultNodes: boolean; diff --git a/packages/react/src/utils/changes.ts b/packages/react/src/utils/changes.ts index 872a0806..91209e11 100644 --- a/packages/react/src/utils/changes.ts +++ b/packages/react/src/utils/changes.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { Node, Edge, EdgeChange, NodeChange } from '../types'; +import type { Node, Edge, EdgeChange, NodeChange, NodeSelectionChange, EdgeSelectionChange } from '../types'; export function handleParentExpand(res: any[], updateItem: any) { const parent = res.find((e) => e.id === updateItem.parentNode); @@ -150,24 +150,32 @@ export function applyEdgeChanges(changes: EdgeChange[], edges: E return applyChanges(changes, edges) as Edge[]; } -export const createSelectionChange = (id: string, selected: boolean) => ({ +export const createSelectionChange = (id: string, selected: boolean): NodeSelectionChange | EdgeSelectionChange => ({ id, type: 'select', selected, }); -export function getSelectionChanges(items: any[], selectedIds: string[]) { - return items.reduce((res, item) => { - const willBeSelected = selectedIds.includes(item.id); +export function getSelectionChanges( + items: any[], + selectedIds: Set = new Set(), + mutateItem = false +): NodeSelectionChange[] | EdgeSelectionChange[] { + const changes: NodeSelectionChange[] | EdgeSelectionChange[] = []; - if (!item.selected && willBeSelected) { - item.selected = true; - res.push(createSelectionChange(item.id, true)); - } else if (item.selected && !willBeSelected) { - item.selected = false; - res.push(createSelectionChange(item.id, false)); + for (const item of items) { + const willBeSelected = selectedIds.has(item.id); + + if (item.selected !== willBeSelected) { + if (mutateItem) { + // this hack is needed for nodes. When the user dragged a node, it's selected. + // When another node gets dragged, we need to deselect the previous one, + // in order to have only one selected node at a time - the onNodesChange callback comes too late here :/ + item.selected = willBeSelected; + } + changes.push(createSelectionChange(item.id, willBeSelected)); } + } - return res; - }, []); + return changes; } diff --git a/packages/svelte/src/lib/types/edges.ts b/packages/svelte/src/lib/types/edges.ts index 611d5190..ecff808c 100644 --- a/packages/svelte/src/lib/types/edges.ts +++ b/packages/svelte/src/lib/types/edges.ts @@ -12,7 +12,7 @@ import type { import type { Node } from '$lib/types'; -export type DefaultEdge = Omit, 'focusable'> & { +export type DefaultEdge = EdgeBase & { label?: string; labelStyle?: string; style?: string; @@ -75,7 +75,7 @@ export type StraightEdgeProps = Omit< export type EdgeTypes = Record>>; -export type DefaultEdgeOptions = Omit, 'focusable'>; +export type DefaultEdgeOptions = DefaultEdgeOptionsBase; export type EdgeLayouted = Pick< Edge, diff --git a/packages/system/src/types/edges.ts b/packages/system/src/types/edges.ts index 6ddeb6d9..c2318f0c 100644 --- a/packages/system/src/types/edges.ts +++ b/packages/system/src/types/edges.ts @@ -19,7 +19,6 @@ export type EdgeBase = { zIndex?: number; ariaLabel?: string; interactionWidth?: number; - focusable?: boolean; }; export type SmoothStepPathOptions = { @@ -37,7 +36,7 @@ export type BezierPathOptions = { export type DefaultEdgeOptionsBase = Omit< EdgeType, - 'id' | 'source' | 'target' | 'sourceHandle' | 'targetHandle' | 'sourceNode' | 'targetNode' + 'id' | 'source' | 'target' | 'sourceHandle' | 'targetHandle' | 'selected' >; export enum ConnectionLineType { diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index 68051aaa..8efb84c2 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -43,10 +43,13 @@ export function updateAbsolutePositions( parentNode?.origin || nodeOrigin ); - node.computed!.positionAbsolute = { - x, - y, - }; + const positionChanged = x !== node.computed?.positionAbsolute?.x || y !== node.computed?.positionAbsolute?.y; + node.computed!.positionAbsolute = positionChanged + ? { + x, + y, + } + : node.computed?.positionAbsolute; node[internalsSymbol]!.z = z; @@ -245,25 +248,22 @@ export function panBy({ return transformChanged; } -export function updateConnectionLookup(lookup: ConnectionLookup, edgeLookup: EdgeLookup, edges: EdgeBase[]) { - lookup.clear(); +export function updateConnectionLookup(connectionLookup: ConnectionLookup, edgeLookup: EdgeLookup, edges: EdgeBase[]) { + connectionLookup.clear(); edgeLookup.clear(); for (const edge of edges) { - edgeLookup.set(edge.id, edge); - const { source, target, sourceHandle = null, targetHandle = null } = edge; const sourceKey = `${source}-source-${sourceHandle}`; const targetKey = `${target}-target-${targetHandle}`; - const prevSource = lookup.get(sourceKey) || new Map(); - const prevTarget = lookup.get(targetKey) || new Map(); + const prevSource = connectionLookup.get(sourceKey) || new Map(); + const prevTarget = connectionLookup.get(targetKey) || new Map(); const connection = { source, target, sourceHandle, targetHandle }; - lookup.set(sourceKey, prevSource.set(`${target}-${targetHandle}`, connection)); - lookup.set(targetKey, prevTarget.set(`${source}-${sourceHandle}`, connection)); + edgeLookup.set(edge.id, edge); + connectionLookup.set(sourceKey, prevSource.set(`${target}-${targetHandle}`, connection)); + connectionLookup.set(targetKey, prevTarget.set(`${source}-${sourceHandle}`, connection)); } - - return lookup; }