refactor(edge-renderer): work with edge ids instead of edges, like node renderer

This commit is contained in:
moklick
2023-12-17 13:53:07 +01:00
parent 2bc02547f6
commit 4316e23309
15 changed files with 300 additions and 269 deletions
@@ -6,7 +6,6 @@ import {
addEdge, addEdge,
applyNodeChanges, applyNodeChanges,
applyEdgeChanges, applyEdgeChanges,
ReactFlowInstance,
Connection, Connection,
Edge, Edge,
Node, Node,
@@ -97,7 +96,6 @@ const initialEdges: Edge[] = [
{ id: 'e5-6', source: '5', target: '6', label: 'This edge can be updated from both sides' }, { id: 'e5-6', source: '5', target: '6', label: 'This edge can be updated from both sides' },
]; ];
const onInit = (reactFlowInstance: ReactFlowInstance) => reactFlowInstance.fitView();
const onEdgeUpdateStart = (_: ReactMouseEvent, edge: Edge, handleType: HandleType) => const onEdgeUpdateStart = (_: ReactMouseEvent, edge: Edge, handleType: HandleType) =>
console.log(`start update ${handleType} handle`, edge); console.log(`start update ${handleType} handle`, edge);
const onEdgeUpdateEnd = (_: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) => const onEdgeUpdateEnd = (_: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) =>
@@ -111,7 +109,6 @@ const UpdatableEdge = () => {
const onConnect = (connection: Connection) => setEdges((els) => addEdge(connection, els)); const onConnect = (connection: Connection) => setEdges((els) => addEdge(connection, els));
const onNodesChange = useCallback((changes: NodeChange[]) => { const onNodesChange = useCallback((changes: NodeChange[]) => {
console.log(changes);
setNodes((ns) => applyNodeChanges(changes, ns)); setNodes((ns) => applyNodeChanges(changes, ns));
}, []); }, []);
@@ -125,12 +122,12 @@ const UpdatableEdge = () => {
edges={edges} edges={edges}
onNodesChange={onNodesChange} onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
onInit={onInit}
snapToGrid={true} snapToGrid={true}
onEdgeUpdate={onEdgeUpdate} onEdgeUpdate={onEdgeUpdate}
onConnect={onConnect} onConnect={onConnect}
onEdgeUpdateStart={onEdgeUpdateStart} onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd} onEdgeUpdateEnd={onEdgeUpdateEnd}
fitView
> >
<Controls /> <Controls />
</ReactFlow> </ReactFlow>
@@ -0,0 +1,133 @@
// Updatable edges have a anchors around their handles to update the edge.
import { XYHandle, type Connection, getEdgePosition } from '@xyflow/system';
import { EdgeAnchor } from '../Edges/EdgeAnchor';
import type { EdgeWrapperProps, Edge } from '../../types/edges';
import { useStoreApi } from '../../hooks/useStore';
type EdgeUpdateAnchorsProps = {
edgePosition: NonNullable<ReturnType<typeof getEdgePosition>>;
edge: Edge;
isUpdatable: boolean | 'source' | 'target';
edgeUpdaterRadius: EdgeWrapperProps['edgeUpdaterRadius'];
sourceHandleId: EdgeWrapperProps['sourceHandleId'];
targetHandleId: EdgeWrapperProps['targetHandleId'];
onEdgeUpdate: EdgeWrapperProps['onEdgeUpdate'];
onEdgeUpdateStart: EdgeWrapperProps['onEdgeUpdateStart'];
onEdgeUpdateEnd: EdgeWrapperProps['onEdgeUpdateEnd'];
setUpdateHover: (hover: boolean) => void;
setUpdating: (updating: boolean) => void;
};
function EdgeUpdateAnchors({
isUpdatable,
edgeUpdaterRadius,
onEdgeUpdate,
onEdgeUpdateStart,
onEdgeUpdateEnd,
setUpdating,
setUpdateHover,
edgePosition,
edge,
targetHandleId,
sourceHandleId,
}: EdgeUpdateAnchorsProps) {
const store = useStoreApi();
const handleEdgeUpdater = (event: React.MouseEvent<SVGGElement, MouseEvent>, isSourceHandle: boolean) => {
// avoid triggering edge updater if mouse btn is not left
if (event.button !== 0) {
return;
}
const {
autoPanOnConnect,
domNode,
isValidConnection,
connectionMode,
connectionRadius,
lib,
onConnectStart,
onConnectEnd,
cancelConnection,
nodes,
panBy,
updateConnection,
} = store.getState();
const nodeId = isSourceHandle ? edge.target : edge.source;
const handleId = (isSourceHandle ? targetHandleId : sourceHandleId) || null;
const handleType = isSourceHandle ? 'target' : 'source';
const isTarget = isSourceHandle;
setUpdating(true);
onEdgeUpdateStart?.(event, edge, handleType);
const _onEdgeUpdateEnd = (evt: MouseEvent | TouchEvent) => {
setUpdating(false);
onEdgeUpdateEnd?.(evt, edge, handleType);
};
const onConnectEdge = (connection: Connection) => onEdgeUpdate?.(edge, connection);
XYHandle.onPointerDown(event.nativeEvent, {
autoPanOnConnect,
connectionMode,
connectionRadius,
domNode,
handleId,
nodeId,
nodes,
isTarget,
edgeUpdaterType: handleType,
lib,
cancelConnection,
panBy,
isValidConnection,
onConnect: onConnectEdge,
onConnectStart,
onConnectEnd,
onEdgeUpdateEnd: _onEdgeUpdateEnd,
updateConnection,
getTransform: () => store.getState().transform,
});
};
const onEdgeUpdaterSourceMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>
handleEdgeUpdater(event, true);
const onEdgeUpdaterTargetMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>
handleEdgeUpdater(event, false);
const onEdgeUpdaterMouseEnter = () => setUpdateHover(true);
const onEdgeUpdaterMouseOut = () => setUpdateHover(false);
return (
<>
{(isUpdatable === 'source' || isUpdatable === true) && (
<EdgeAnchor
position={edgePosition.sourcePosition}
centerX={edgePosition.sourceX}
centerY={edgePosition.sourceY}
radius={edgeUpdaterRadius}
onMouseDown={onEdgeUpdaterSourceMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter}
onMouseOut={onEdgeUpdaterMouseOut}
type="source"
/>
)}
{(isUpdatable === 'target' || isUpdatable === true) && (
<EdgeAnchor
position={edgePosition.targetPosition}
centerX={edgePosition.targetX}
centerY={edgePosition.targetY}
radius={edgeUpdaterRadius}
onMouseDown={onEdgeUpdaterTargetMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter}
onMouseOut={onEdgeUpdaterMouseOut}
type="target"
/>
)}
</>
);
}
export default EdgeUpdateAnchors;
@@ -1,43 +1,21 @@
import { memo, useState, useMemo, useRef, type KeyboardEvent, useCallback } from 'react'; import { memo, useState, useMemo, useRef, type KeyboardEvent, useCallback } from 'react';
import cc from 'classcat'; import cc from 'classcat';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import { import { getMarkerId, elementSelectionKeys, getEdgePosition, errorMessages, getEdgeZIndex } from '@xyflow/system';
getMarkerId,
elementSelectionKeys,
XYHandle,
type Connection,
getEdgePosition,
errorMessages,
getEdgeZIndex,
} from '@xyflow/system';
import { useStoreApi, useStore } from '../../hooks/useStore'; import { useStoreApi, useStore } from '../../hooks/useStore';
import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions'; import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions';
import { EdgeAnchor } from '../Edges/EdgeAnchor';
import { getMouseHandler } from '../Edges/utils';
import type { EdgeWrapperProps, Node } from '../../types'; import type { EdgeWrapperProps, Node } from '../../types';
import { builtinEdgeTypes } from './utils'; import { builtinEdgeTypes } from './utils';
import EdgeUpdateAnchors from './EdgeUpdateAnchors';
function EdgeWrapper({ function EdgeWrapper({
id, id,
className, edgesFocusable,
type, edgesUpdatable,
data, elementsSelectable,
onClick, onClick,
onEdgeDoubleClick, onDoubleClick,
selected,
animated,
label,
labelStyle,
labelShowBg,
labelBgStyle,
labelBgPadding,
labelBgBorderRadius,
style,
source,
target,
isSelectable,
hidden,
sourceHandleId, sourceHandleId,
targetHandleId, targetHandleId,
onContextMenu, onContextMenu,
@@ -48,20 +26,15 @@ function EdgeWrapper({
onEdgeUpdate, onEdgeUpdate,
onEdgeUpdateStart, onEdgeUpdateStart,
onEdgeUpdateEnd, onEdgeUpdateEnd,
markerEnd,
markerStart,
rfId, rfId,
ariaLabel,
isFocusable,
isUpdatable,
pathOptions,
interactionWidth,
edgeTypes, edgeTypes,
zIndex: edgeZIndex,
elevateEdgesOnSelect, elevateEdgesOnSelect,
noPanClassName,
onError, onError,
}: EdgeWrapperProps): JSX.Element | null { }: EdgeWrapperProps): JSX.Element | null {
let edgeType = type || 'default'; const edge = useStore((s) => s.edgeLookup.get(id)!);
let edgeType = edge.type || 'default';
let EdgeComponent = edgeTypes?.[edgeType] || builtinEdgeTypes[edgeType]; let EdgeComponent = edgeTypes?.[edgeType] || builtinEdgeTypes[edgeType];
if (EdgeComponent === undefined) { if (EdgeComponent === undefined) {
@@ -70,23 +43,29 @@ function EdgeWrapper({
EdgeComponent = builtinEdgeTypes.default; EdgeComponent = builtinEdgeTypes.default;
} }
const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined'));
const isUpdatable =
typeof onEdgeUpdate !== 'undefined' &&
(edge.updatable || (edgesUpdatable && typeof edge.updatable === 'undefined'));
const isSelectable = !!(edge.selectable || (elementsSelectable && typeof edge.selectable === 'undefined'));
const edgeRef = useRef<SVGGElement>(null); const edgeRef = useRef<SVGGElement>(null);
const [updateHover, setUpdateHover] = useState<boolean>(false); const [updateHover, setUpdateHover] = useState<boolean>(false);
const [updating, setUpdating] = useState<boolean>(false); const [updating, setUpdating] = useState<boolean>(false);
const store = useStoreApi(); const store = useStoreApi();
const prevSourceNode = useRef<Node | undefined>(); const prevSourceNode = useRef<Node | undefined>();
const prevTargetNode = useRef<Node | undefined>(); const prevTargetNode = useRef<Node | undefined>();
const prevZIndex = useRef<number | undefined>(edgeZIndex); const prevZIndex = useRef<number | undefined>(edge.zIndex);
const prevEdgePosition = useRef<ReturnType<typeof getEdgePosition> | null>(null); const prevEdgePosition = useRef<ReturnType<typeof getEdgePosition> | null>(null);
const { edgePosition, zIndex } = useStore( const { edgePosition, zIndex } = useStore(
useCallback( useCallback(
(state) => { (state) => {
const sourceNode = state.nodeLookup.get(source); const sourceNode = state.nodeLookup.get(edge.source);
const targetNode = state.nodeLookup.get(target); const targetNode = state.nodeLookup.get(edge.target);
if (!sourceNode || !targetNode) { if (!sourceNode || !targetNode) {
return { edgePosition: null, zIndex: edgeZIndex }; return { edgePosition: null, zIndex: edge.zIndex };
} }
const nodesChanged = prevSourceNode.current !== sourceNode || prevTargetNode.current !== targetNode; const nodesChanged = prevSourceNode.current !== sourceNode || prevTargetNode.current !== targetNode;
@@ -105,28 +84,33 @@ function EdgeWrapper({
onError: state.onError, onError: state.onError,
}) })
: prevEdgePosition.current; : prevEdgePosition.current;
prevZIndex.current = getEdgeZIndex(selected, edgeZIndex, sourceNode, targetNode, elevateEdgesOnSelect); prevZIndex.current = getEdgeZIndex(edge.selected, edge.zIndex, sourceNode, targetNode, elevateEdgesOnSelect);
return { return {
edgePosition: prevEdgePosition.current, edgePosition: prevEdgePosition.current,
zIndex: prevZIndex.current, zIndex: prevZIndex.current,
}; };
}, },
[source, target, selected, edgeZIndex] [edge.source, edge.target, edge.selected, edge.zIndex]
), ),
shallow shallow
); );
const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart, rfId)})`, [markerStart, rfId]); const markerStartUrl = useMemo(
const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd, rfId)})`, [markerEnd, rfId]); () => (edge.markerStart ? `url(#${getMarkerId(edge.markerStart, rfId)})` : undefined),
[edge.markerStart, rfId]
);
const markerEndUrl = useMemo(
() => (edge.markerEnd ? `url(#${getMarkerId(edge.markerEnd, rfId)})` : undefined),
[edge.markerEnd, rfId]
);
if (hidden || !edgePosition) { if (edge.hidden || !edgePosition) {
return null; return null;
} }
const onEdgeClick = (event: React.MouseEvent<SVGGElement, MouseEvent>): void => { const onEdgeClick = (event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
const { edges, addSelectedEdges, unselectNodesAndEdges, multiSelectionActive } = store.getState(); const { addSelectedEdges, unselectNodesAndEdges, multiSelectionActive } = store.getState();
const edge = edges.find((e) => e.id === id);
if (!edge) { if (!edge) {
return; return;
@@ -148,82 +132,31 @@ function EdgeWrapper({
} }
}; };
const onEdgeDoubleClickHandler = getMouseHandler(id, store.getState, onEdgeDoubleClick); const onEdgeDoubleClick = onDoubleClick
const onEdgeContextMenu = getMouseHandler(id, store.getState, onContextMenu); ? (event: React.MouseEvent) => {
const onEdgeMouseEnter = getMouseHandler(id, store.getState, onMouseEnter); onDoubleClick(event, { ...edge });
const onEdgeMouseMove = getMouseHandler(id, store.getState, onMouseMove); }
const onEdgeMouseLeave = getMouseHandler(id, store.getState, onMouseLeave); : undefined;
const onEdgeContextMenu = onContextMenu
const handleEdgeUpdater = (event: React.MouseEvent<SVGGElement, MouseEvent>, isSourceHandle: boolean) => { ? (event: React.MouseEvent) => {
// avoid triggering edge updater if mouse btn is not left onContextMenu(event, { ...edge });
if (event.button !== 0) { }
return; : undefined;
} const onEdgeMouseEnter = onMouseEnter
? (event: React.MouseEvent) => {
const { onMouseEnter(event, { ...edge });
autoPanOnConnect, }
domNode, : undefined;
edges, const onEdgeMouseMove = onMouseMove
isValidConnection, ? (event: React.MouseEvent) => {
connectionMode, onMouseMove(event, { ...edge });
connectionRadius, }
lib, : undefined;
onConnectStart, const onEdgeMouseLeave = onMouseLeave
onConnectEnd, ? (event: React.MouseEvent) => {
cancelConnection, onMouseLeave(event, { ...edge });
nodes, }
panBy, : undefined;
updateConnection,
} = store.getState();
const nodeId = isSourceHandle ? target : source;
const handleId = (isSourceHandle ? targetHandleId : sourceHandleId) || null;
const handleType = isSourceHandle ? 'target' : 'source';
const isTarget = isSourceHandle;
const edge = edges.find((e) => e.id === id)!;
setUpdating(true);
onEdgeUpdateStart?.(event, edge, handleType);
const _onEdgeUpdateEnd = (evt: MouseEvent | TouchEvent) => {
setUpdating(false);
onEdgeUpdateEnd?.(evt, edge, handleType);
};
const onConnectEdge = (connection: Connection) => onEdgeUpdate?.(edge, connection);
XYHandle.onPointerDown(event.nativeEvent, {
autoPanOnConnect,
connectionMode,
connectionRadius,
domNode,
handleId,
nodeId,
nodes,
isTarget,
edgeUpdaterType: handleType,
lib,
cancelConnection,
panBy,
isValidConnection,
onConnect: onConnectEdge,
onConnectStart,
onConnectEnd,
onEdgeUpdateEnd: _onEdgeUpdateEnd,
updateConnection,
getTransform: () => store.getState().transform,
});
};
const onEdgeUpdaterSourceMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>
handleEdgeUpdater(event, true);
const onEdgeUpdaterTargetMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>
handleEdgeUpdater(event, false);
const onEdgeUpdaterMouseEnter = () => setUpdateHover(true);
const onEdgeUpdaterMouseOut = () => setUpdateHover(false);
const inactive = !isSelectable && !onClick;
const onKeyDown = (event: KeyboardEvent) => { const onKeyDown = (event: KeyboardEvent) => {
if (elementSelectionKeys.includes(event.key) && isSelectable) { if (elementSelectionKeys.includes(event.key) && isSelectable) {
@@ -244,12 +177,18 @@ function EdgeWrapper({
<g <g
className={cc([ className={cc([
'react-flow__edge', 'react-flow__edge',
`react-flow__edge-${type}`, `react-flow__edge-${edgeType}`,
className, edge.className,
{ selected, animated, inactive, updating: updateHover }, noPanClassName,
{
selected: edge.selected,
animated: edge.animated,
inactive: !isSelectable && !onClick,
updating: updateHover,
},
])} ])}
onClick={onEdgeClick} onClick={onEdgeClick}
onDoubleClick={onEdgeDoubleClickHandler} onDoubleClick={onEdgeDoubleClick}
onContextMenu={onEdgeContextMenu} onContextMenu={onEdgeContextMenu}
onMouseEnter={onEdgeMouseEnter} onMouseEnter={onEdgeMouseEnter}
onMouseMove={onEdgeMouseMove} onMouseMove={onEdgeMouseMove}
@@ -259,25 +198,27 @@ function EdgeWrapper({
role={isFocusable ? 'button' : 'img'} role={isFocusable ? 'button' : 'img'}
data-id={id} data-id={id}
data-testid={`rf__edge-${id}`} data-testid={`rf__edge-${id}`}
aria-label={ariaLabel === null ? undefined : ariaLabel ? ariaLabel : `Edge from ${source} to ${target}`} aria-label={
edge.ariaLabel === null ? undefined : edge.ariaLabel || `Edge from ${edge.source} to ${edge.target}`
}
aria-describedby={isFocusable ? `${ARIA_EDGE_DESC_KEY}-${rfId}` : undefined} aria-describedby={isFocusable ? `${ARIA_EDGE_DESC_KEY}-${rfId}` : undefined}
ref={edgeRef} ref={edgeRef}
> >
{!updating && ( {!updating && (
<EdgeComponent <EdgeComponent
id={id} id={id}
source={source} source={edge.source}
target={target} target={edge.target}
selected={selected} selected={edge.selected}
animated={animated} animated={edge.animated}
label={label} label={edge.label}
labelStyle={labelStyle} labelStyle={edge.labelStyle}
labelShowBg={labelShowBg} labelShowBg={edge.labelShowBg}
labelBgStyle={labelBgStyle} labelBgStyle={edge.labelBgStyle}
labelBgPadding={labelBgPadding} labelBgPadding={edge.labelBgPadding}
labelBgBorderRadius={labelBgBorderRadius} labelBgBorderRadius={edge.labelBgBorderRadius}
data={data} data={edge.data}
style={style} style={edge.style}
sourceX={edgePosition.sourceX} sourceX={edgePosition.sourceX}
sourceY={edgePosition.sourceY} sourceY={edgePosition.sourceY}
targetX={edgePosition.targetX} targetX={edgePosition.targetX}
@@ -288,37 +229,24 @@ function EdgeWrapper({
targetHandleId={targetHandleId} targetHandleId={targetHandleId}
markerStart={markerStartUrl} markerStart={markerStartUrl}
markerEnd={markerEndUrl} markerEnd={markerEndUrl}
pathOptions={pathOptions} pathOptions={'pathOptions' in edge ? edge.pathOptions : undefined}
interactionWidth={interactionWidth} interactionWidth={edge.interactionWidth}
/> />
)} )}
{isUpdatable && ( {isUpdatable && (
<> <EdgeUpdateAnchors
{(isUpdatable === 'source' || isUpdatable === true) && ( edge={edge}
<EdgeAnchor isUpdatable={isUpdatable}
position={edgePosition.sourcePosition} edgeUpdaterRadius={edgeUpdaterRadius}
centerX={edgePosition.sourceX} onEdgeUpdate={onEdgeUpdate}
centerY={edgePosition.sourceY} onEdgeUpdateStart={onEdgeUpdateStart}
radius={edgeUpdaterRadius} onEdgeUpdateEnd={onEdgeUpdateEnd}
onMouseDown={onEdgeUpdaterSourceMouseDown} edgePosition={edgePosition}
onMouseEnter={onEdgeUpdaterMouseEnter} setUpdateHover={setUpdateHover}
onMouseOut={onEdgeUpdaterMouseOut} setUpdating={setUpdating}
type="source" sourceHandleId={sourceHandleId}
/> targetHandleId={targetHandleId}
)} />
{(isUpdatable === 'target' || isUpdatable === true) && (
<EdgeAnchor
position={edgePosition.targetPosition}
centerX={edgePosition.targetX}
centerY={edgePosition.targetY}
radius={edgeUpdaterRadius}
onMouseDown={onEdgeUpdaterTargetMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter}
onMouseOut={onEdgeUpdaterMouseOut}
type="target"
/>
)}
</>
)} )}
</g> </g>
</svg> </svg>
@@ -1,20 +0,0 @@
import type { MouseEvent as ReactMouseEvent } from 'react';
import type { StoreApi } from 'zustand';
import type { Edge, ReactFlowState } from '../../types';
export function getMouseHandler(
id: string,
getState: StoreApi<ReactFlowState>['getState'],
handler?: (event: ReactMouseEvent<SVGGElement, MouseEvent>, edge: Edge) => void
) {
return handler === undefined
? handler
: (event: ReactMouseEvent<SVGGElement, MouseEvent>) => {
const edge = getState().edges.find((e) => e.id === id);
if (edge) {
handler(event, { ...edge });
}
};
}
@@ -1,9 +1,8 @@
import { memo, ReactNode } from 'react'; import { memo, ReactNode } from 'react';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import cc from 'classcat';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import useVisibleEdges from '../../hooks/useVisibleEdges'; import useVisibleEdgeIds from '../../hooks/useVisibleEdges';
import MarkerDefinitions from './MarkerDefinitions'; import MarkerDefinitions from './MarkerDefinitions';
import { GraphViewProps } from '../GraphView'; import { GraphViewProps } from '../GraphView';
import type { ReactFlowState } from '../../types'; import type { ReactFlowState } from '../../types';
@@ -63,7 +62,7 @@ const EdgeRenderer = ({
children, children,
}: EdgeRendererProps) => { }: EdgeRendererProps) => {
const { edgesFocusable, edgesUpdatable, elementsSelectable, onError } = useStore(selector, shallow); const { edgesFocusable, edgesUpdatable, elementsSelectable, onError } = useStore(selector, shallow);
const edges = useVisibleEdges(onlyRenderVisibleElements); const edgeIds = useVisibleEdgeIds(onlyRenderVisibleElements);
return ( return (
<div className="react-flow__edges"> <div className="react-flow__edges">
@@ -71,38 +70,15 @@ const EdgeRenderer = ({
<MarkerDefinitions defaultColor={defaultMarkerColor} rfId={rfId} /> <MarkerDefinitions defaultColor={defaultMarkerColor} rfId={rfId} />
</svg> </svg>
{edges.map((edge) => { {edgeIds.map((id) => {
const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined'));
const isUpdatable =
typeof onEdgeUpdate !== 'undefined' &&
(edge.updatable || (edgesUpdatable && typeof edge.updatable === 'undefined'));
const isSelectable = !!(edge.selectable || (elementsSelectable && typeof edge.selectable === 'undefined'));
return ( return (
<EdgeWrapper <EdgeWrapper
key={edge.id} key={id}
id={edge.id} id={id}
className={cc([edge.className, noPanClassName])} edgesFocusable={edgesFocusable}
type={edge.type} edgesUpdatable={edgesUpdatable}
data={edge.data} elementsSelectable={elementsSelectable}
selected={!!edge.selected} noPanClassName={noPanClassName}
animated={!!edge.animated}
hidden={!!edge.hidden}
zIndex={edge.zIndex}
label={edge.label}
labelStyle={edge.labelStyle}
labelShowBg={edge.labelShowBg}
labelBgStyle={edge.labelBgStyle}
labelBgPadding={edge.labelBgPadding}
labelBgBorderRadius={edge.labelBgBorderRadius}
style={edge.style}
source={edge.source}
target={edge.target}
sourceHandleId={edge.sourceHandle}
targetHandleId={edge.targetHandle}
markerEnd={edge.markerEnd}
markerStart={edge.markerStart}
isSelectable={isSelectable}
onEdgeUpdate={onEdgeUpdate} onEdgeUpdate={onEdgeUpdate}
onContextMenu={onEdgeContextMenu} onContextMenu={onEdgeContextMenu}
onMouseEnter={onEdgeMouseEnter} onMouseEnter={onEdgeMouseEnter}
@@ -110,15 +86,10 @@ const EdgeRenderer = ({
onMouseLeave={onEdgeMouseLeave} onMouseLeave={onEdgeMouseLeave}
onClick={onEdgeClick} onClick={onEdgeClick}
edgeUpdaterRadius={edgeUpdaterRadius} edgeUpdaterRadius={edgeUpdaterRadius}
onEdgeDoubleClick={onEdgeDoubleClick} onDoubleClick={onEdgeDoubleClick}
onEdgeUpdateStart={onEdgeUpdateStart} onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd} onEdgeUpdateEnd={onEdgeUpdateEnd}
rfId={rfId} rfId={rfId}
ariaLabel={edge.ariaLabel}
isFocusable={isFocusable}
isUpdatable={isUpdatable}
pathOptions={'pathOptions' in edge ? edge.pathOptions : undefined}
interactionWidth={edge.interactionWidth}
onError={onError} onError={onError}
edgeTypes={edgeTypes} edgeTypes={edgeTypes}
elevateEdgesOnSelect={elevateEdgesOnSelect} elevateEdgesOnSelect={elevateEdgesOnSelect}
+6 -6
View File
@@ -3,10 +3,10 @@ import { shallow } from 'zustand/shallow';
import { isEdgeVisible } from '@xyflow/system'; import { isEdgeVisible } from '@xyflow/system';
import { useStore } from '../hooks/useStore'; import { useStore } from '../hooks/useStore';
import { type Edge, type ReactFlowState } from '../types'; import { type ReactFlowState } from '../types';
function useVisibleEdges(onlyRenderVisible: boolean): Edge[] { function useVisibleEdgeIds(onlyRenderVisible: boolean): string[] {
const edges = useStore( const edgeIds = useStore(
useCallback( useCallback(
(s: ReactFlowState) => { (s: ReactFlowState) => {
const visibleEdges = const visibleEdges =
@@ -29,14 +29,14 @@ function useVisibleEdges(onlyRenderVisible: boolean): Edge[] {
}) })
: s.edges; : s.edges;
return visibleEdges; return visibleEdges.map((edge) => edge.id);
}, },
[onlyRenderVisible] [onlyRenderVisible]
), ),
shallow shallow
); );
return edges; return edgeIds;
} }
export default useVisibleEdges; export default useVisibleEdgeIds;
+1
View File
@@ -77,6 +77,7 @@ export {
type CoordinateExtent, type CoordinateExtent,
type ColorMode, type ColorMode,
type ColorModeClass, type ColorModeClass,
type HandleType,
} from '@xyflow/system'; } from '@xyflow/system';
// system utils // system utils
+8 -5
View File
@@ -55,10 +55,10 @@ const createRFStore = ({
set({ nodes: nodesWithInternalData }); set({ nodes: nodesWithInternalData });
}, },
setEdges: (edges: Edge[]) => { setEdges: (edges: Edge[]) => {
const { defaultEdgeOptions = {}, connectionLookup } = get(); const { defaultEdgeOptions = {}, connectionLookup, edgeLookup } = get();
const nextEdges = edges.map((e) => ({ ...defaultEdgeOptions, ...e })); const nextEdges = edges.map((e) => ({ ...defaultEdgeOptions, ...e }));
updateConnectionLookup(connectionLookup, nextEdges); updateConnectionLookup(connectionLookup, edgeLookup, nextEdges);
set({ edges: nextEdges }); set({ edges: nextEdges });
}, },
@@ -79,13 +79,16 @@ const createRFStore = ({
}; };
if (hasDefaultNodes) { if (hasDefaultNodes) {
const { nodeLookup } = get(); const { nodeLookup, nodeOrigin, elevateNodesOnSelect } = get();
nextState.nodes = adoptUserProvidedNodes(nodes, nodeLookup, { nextState.nodes = adoptUserProvidedNodes(nodes, nodeLookup, {
nodeOrigin: get().nodeOrigin, nodeOrigin,
elevateNodesOnSelect: get().elevateNodesOnSelect, elevateNodesOnSelect,
}); });
} }
if (hasDefaultEdges) { if (hasDefaultEdges) {
const { connectionLookup, edgeLookup } = get();
updateConnectionLookup(connectionLookup, edgeLookup, edges);
nextState.edges = edges; nextState.edges = edges;
} }
+5 -1
View File
@@ -24,7 +24,10 @@ const getInitialState = ({
fitView?: boolean; fitView?: boolean;
} = {}): ReactFlowStore => { } = {}): ReactFlowStore => {
const nodeLookup = new Map(); const nodeLookup = new Map();
const connectionLookup = updateConnectionLookup(new Map(), edges); const connectionLookup = new Map();
const edgeLookup = new Map();
updateConnectionLookup(connectionLookup, edgeLookup, edges);
const nextNodes = adoptUserProvidedNodes(nodes, nodeLookup, { const nextNodes = adoptUserProvidedNodes(nodes, nodeLookup, {
nodeOrigin: [0, 0], nodeOrigin: [0, 0],
elevateNodesOnSelect: false, elevateNodesOnSelect: false,
@@ -47,6 +50,7 @@ const getInitialState = ({
nodes: nextNodes, nodes: nextNodes,
nodeLookup, nodeLookup,
edges: edges, edges: edges,
edgeLookup,
connectionLookup, connectionLookup,
onNodesChange: null, onNodesChange: null,
onEdgesChange: null, onEdgesChange: null,
+9 -7
View File
@@ -58,9 +58,14 @@ export type Edge<T = any> = DefaultEdge<T> | SmoothStepEdgeType<T> | BezierEdgeT
export type EdgeMouseHandler = (event: ReactMouseEvent, edge: Edge) => void; export type EdgeMouseHandler = (event: ReactMouseEvent, edge: Edge) => void;
export type EdgeWrapperProps<T = any> = Omit<Edge<T>, 'sourceHandle' | 'targetHandle'> & { export type EdgeWrapperProps = {
id: string;
edgesFocusable: boolean;
edgesUpdatable: boolean;
elementsSelectable: boolean;
noPanClassName: string;
onClick?: EdgeMouseHandler; onClick?: EdgeMouseHandler;
onEdgeDoubleClick?: EdgeMouseHandler; onDoubleClick?: EdgeMouseHandler;
sourceHandleId?: string | null; sourceHandleId?: string | null;
targetHandleId?: string | null; targetHandleId?: string | null;
onEdgeUpdate?: OnEdgeUpdateFunc; onEdgeUpdate?: OnEdgeUpdateFunc;
@@ -72,10 +77,6 @@ export type EdgeWrapperProps<T = any> = Omit<Edge<T>, 'sourceHandle' | 'targetHa
onEdgeUpdateStart?: (event: ReactMouseEvent, edge: Edge, handleType: HandleType) => void; onEdgeUpdateStart?: (event: ReactMouseEvent, edge: Edge, handleType: HandleType) => void;
onEdgeUpdateEnd?: (event: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) => void; onEdgeUpdateEnd?: (event: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) => void;
rfId?: string; rfId?: string;
isFocusable: boolean;
isUpdatable: EdgeUpdatable;
isSelectable: boolean;
pathOptions?: BezierPathOptions | SmoothStepPathOptions;
edgeTypes?: EdgeTypes; edgeTypes?: EdgeTypes;
onError?: OnError; onError?: OnError;
elevateEdgesOnSelect?: boolean; elevateEdgesOnSelect?: boolean;
@@ -94,13 +95,14 @@ export type EdgeProps<T = any> = Pick<
Edge<T>, Edge<T>,
'id' | 'animated' | 'data' | 'style' | 'selected' | 'source' | 'target' 'id' | 'animated' | 'data' | 'style' | 'selected' | 'source' | 'target'
> & > &
Pick<EdgeWrapperProps, 'sourceHandleId' | 'targetHandleId' | 'interactionWidth'> & Pick<EdgeWrapperProps, 'sourceHandleId' | 'targetHandleId'> &
EdgePosition & EdgePosition &
EdgeLabelOptions & { EdgeLabelOptions & {
markerStart?: string; markerStart?: string;
markerEnd?: string; markerEnd?: string;
// @TODO: how can we get better types for pathOptions? // @TODO: how can we get better types for pathOptions?
pathOptions?: any; pathOptions?: any;
interactionWidth?: number;
}; };
export type BaseEdgeProps = Pick<EdgeProps, 'style' | 'markerStart' | 'markerEnd' | 'interactionWidth'> & export type BaseEdgeProps = Pick<EdgeProps, 'style' | 'markerStart' | 'markerEnd' | 'interactionWidth'> &
+4 -2
View File
@@ -24,7 +24,8 @@ import {
type OnMoveEnd, type OnMoveEnd,
type IsValidConnection, type IsValidConnection,
type UpdateConnection, type UpdateConnection,
Connection, EdgeLookup,
ConnectionLookup,
} from '@xyflow/system'; } from '@xyflow/system';
import type { import type {
@@ -50,7 +51,8 @@ export type ReactFlowStore = {
nodes: Node[]; nodes: Node[];
nodeLookup: Map<string, Node>; nodeLookup: Map<string, Node>;
edges: Edge[]; edges: Edge[];
connectionLookup: Map<string, Map<string, Connection>>; edgeLookup: EdgeLookup<Edge>;
connectionLookup: ConnectionLookup;
onNodesChange: OnNodesChange | null; onNodesChange: OnNodesChange | null;
onEdgesChange: OnEdgesChange | null; onEdgesChange: OnEdgesChange | null;
@@ -81,7 +81,9 @@ export const getInitialStore = ({
nodeOrigin: [0, 0], nodeOrigin: [0, 0],
elevateNodesOnSelect: false elevateNodesOnSelect: false
}); });
const connectionLookup = updateConnectionLookup(new Map(), edges); const connectionLookup = new Map();
const edgeLookup = new Map();
updateConnectionLookup(connectionLookup, edgeLookup, edges);
let viewport: Viewport = { x: 0, y: 0, zoom: 1 }; let viewport: Viewport = { x: 0, y: 0, zoom: 1 };
@@ -96,7 +98,7 @@ export const getInitialStore = ({
nodes: createNodesStore(nextNodes, nodeLookup), nodes: createNodesStore(nextNodes, nodeLookup),
nodeLookup: readable<Map<string, Node>>(nodeLookup), nodeLookup: readable<Map<string, Node>>(nodeLookup),
visibleNodes: readable<Node[]>([]), visibleNodes: readable<Node[]>([]),
edges: createEdgesStore(edges, connectionLookup), edges: createEdgesStore(edges, connectionLookup, edgeLookup),
visibleEdges: readable<EdgeLayouted[]>([]), visibleEdges: readable<EdgeLayouted[]>([]),
connectionLookup: readable<ConnectionLookup>(connectionLookup), connectionLookup: readable<ConnectionLookup>(connectionLookup),
height: writable<number>(500), height: writable<number>(500),
+4 -2
View File
@@ -11,7 +11,8 @@ import {
updateConnectionLookup, updateConnectionLookup,
type Viewport, type Viewport,
type PanZoomInstance, type PanZoomInstance,
type ConnectionLookup type ConnectionLookup,
type EdgeLookup
} from '@xyflow/system'; } from '@xyflow/system';
import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, Node } from '$lib/types'; import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, Node } from '$lib/types';
@@ -175,6 +176,7 @@ export const createNodesStore = (
export const createEdgesStore = ( export const createEdgesStore = (
edges: Edge[], edges: Edge[],
connectionLookup: ConnectionLookup, connectionLookup: ConnectionLookup,
edgeLookup: EdgeLookup,
defaultOptions?: DefaultEdgeOptions defaultOptions?: DefaultEdgeOptions
): Writable<Edge[]> & { setDefaultOptions: (opts: DefaultEdgeOptions) => void } => { ): Writable<Edge[]> & { setDefaultOptions: (opts: DefaultEdgeOptions) => void } => {
const { subscribe, set, update } = writable<Edge[]>([]); const { subscribe, set, update } = writable<Edge[]>([]);
@@ -184,7 +186,7 @@ export const createEdgesStore = (
const _set: typeof set = (eds: Edge[]) => { const _set: typeof set = (eds: Edge[]) => {
const nextEdges = defaults ? eds.map((edge) => ({ ...defaults, ...edge })) : eds; const nextEdges = defaults ? eds.map((edge) => ({ ...defaults, ...edge })) : eds;
updateConnectionLookup(connectionLookup, nextEdges); updateConnectionLookup(connectionLookup, edgeLookup, nextEdges);
value = nextEdges; value = nextEdges;
set(value); set(value);
+2
View File
@@ -77,3 +77,5 @@ export type EdgePosition = {
sourcePosition: Position; sourcePosition: Position;
targetPosition: Position; targetPosition: Position;
}; };
export type EdgeLookup = Map<string, EdgeBase>;
+16 -12
View File
@@ -11,6 +11,7 @@ import {
XYZPosition, XYZPosition,
ConnectionLookup, ConnectionLookup,
EdgeBase, EdgeBase,
EdgeLookup,
} from '../types'; } from '../types';
import { getDimensions, getHandleBounds } from './dom'; import { getDimensions, getHandleBounds } from './dom';
import { isNumeric } from './general'; import { isNumeric } from './general';
@@ -244,22 +245,25 @@ export function panBy({
return transformChanged; return transformChanged;
} }
export function updateConnectionLookup(lookup: ConnectionLookup, edges: EdgeBase[]) { export function updateConnectionLookup(lookup: ConnectionLookup, edgeLookup: EdgeLookup, edges: EdgeBase[]) {
lookup.clear(); lookup.clear();
edgeLookup.clear();
edges.forEach(({ source, target, sourceHandle = null, targetHandle = null }) => { for (const edge of edges) {
if (source && target) { edgeLookup.set(edge.id, edge);
const sourceKey = `${source}-source-${sourceHandle}`;
const targetKey = `${target}-target-${targetHandle}`;
const prevSource = lookup.get(sourceKey) || new Map(); const { source, target, sourceHandle = null, targetHandle = null } = edge;
const prevTarget = lookup.get(targetKey) || new Map();
const connection = { source, target, sourceHandle, targetHandle };
lookup.set(sourceKey, prevSource.set(`${target}-${targetHandle}`, connection)); const sourceKey = `${source}-source-${sourceHandle}`;
lookup.set(targetKey, prevTarget.set(`${source}-${sourceHandle}`, connection)); const targetKey = `${target}-target-${targetHandle}`;
}
}); const prevSource = lookup.get(sourceKey) || new Map();
const prevTarget = lookup.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));
}
return lookup; return lookup;
} }