Merge pull request #3632 from xyflow/refactor/edge-rendering

refactor(edges): simplify edge rendering
This commit is contained in:
Moritz Klack
2023-12-18 17:21:41 +01:00
committed by GitHub
34 changed files with 752 additions and 717 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,137 @@
// Updatable edges have a anchors around their handles to update the edge.
import { XYHandle, type Connection, EdgePosition } from '@xyflow/system';
import { EdgeAnchor } from '../Edges/EdgeAnchor';
import type { EdgeWrapperProps, Edge } from '../../types/edges';
import { useStoreApi } from '../../hooks/useStore';
type EdgeUpdateAnchorsProps = {
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;
} & EdgePosition;
function EdgeUpdateAnchors({
isUpdatable,
edgeUpdaterRadius,
edge,
targetHandleId,
sourceHandleId,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
onEdgeUpdate,
onEdgeUpdateStart,
onEdgeUpdateEnd,
setUpdating,
setUpdateHover,
}: 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={sourcePosition}
centerX={sourceX}
centerY={sourceY}
radius={edgeUpdaterRadius}
onMouseDown={onEdgeUpdaterSourceMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter}
onMouseOut={onEdgeUpdaterMouseOut}
type="source"
/>
)}
{(isUpdatable === 'target' || isUpdatable === true) && (
<EdgeAnchor
position={targetPosition}
centerX={targetX}
centerY={targetY}
radius={edgeUpdaterRadius}
onMouseDown={onEdgeUpdaterTargetMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter}
onMouseOut={onEdgeUpdaterMouseOut}
type="target"
/>
)}
</>
);
}
export default EdgeUpdateAnchors;
@@ -4,39 +4,24 @@ import { shallow } from 'zustand/shallow';
import { import {
getMarkerId, getMarkerId,
elementSelectionKeys, elementSelectionKeys,
XYHandle,
type Connection,
getEdgePosition, getEdgePosition,
errorMessages, errorMessages,
getElevatedEdgeZIndex,
} from '@xyflow/system'; } 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 } from '../../types'; import type { EdgeWrapperProps } from '../../types';
import { builtinEdgeTypes } from './utils'; import { builtinEdgeTypes, nullPosition } 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,
@@ -47,18 +32,17 @@ function EdgeWrapper({
onEdgeUpdate, onEdgeUpdate,
onEdgeUpdateStart, onEdgeUpdateStart,
onEdgeUpdateEnd, onEdgeUpdateEnd,
markerEnd,
markerStart,
rfId, rfId,
ariaLabel,
isFocusable,
isUpdatable,
pathOptions,
interactionWidth,
edgeTypes, edgeTypes,
elevateEdgesOnSelect,
noPanClassName,
onError, onError,
}: EdgeWrapperProps): JSX.Element | null { }: EdgeWrapperProps): JSX.Element | null {
let edgeType = type || 'default'; 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]; let EdgeComponent = edgeTypes?.[edgeType] || builtinEdgeTypes[edgeType];
if (EdgeComponent === undefined) { if (EdgeComponent === undefined) {
@@ -67,49 +51,74 @@ 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 edgePosition = useStore(
const { zIndex, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition } = useStore(
useCallback( useCallback(
(state) => { (store) => {
const sourceNode = state.nodeLookup.get(source); const sourceNode = store.nodeLookup.get(edge.source);
const targetNode = state.nodeLookup.get(target); const targetNode = store.nodeLookup.get(edge.target);
if (!sourceNode || !targetNode) { if (!sourceNode || !targetNode) {
return null; return {
zIndex: edge.zIndex,
...nullPosition,
};
} }
return getEdgePosition({ const edgePosition = getEdgePosition({
id, id,
sourceNode, sourceNode,
targetNode, targetNode,
sourceHandle: sourceHandleId || null, sourceHandle: sourceHandleId || null,
targetHandle: targetHandleId || null, targetHandle: targetHandleId || null,
connectionMode: state.connectionMode, connectionMode: store.connectionMode,
onError: state.onError, onError,
}); });
const zIndex = getElevatedEdgeZIndex({
selected: edge.selected,
zIndex: edge.zIndex,
sourceNode,
targetNode,
elevateOnSelect: elevateEdgesOnSelect,
});
return {
zIndex,
...(edgePosition || nullPosition),
};
}, },
[source, target] [edge.source, edge.target, edge.selected, edge.zIndex, elevateEdgesOnSelect]
), ),
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]
);
if (hidden || !edgePosition) { const markerEndUrl = useMemo(
() => (edge.markerEnd ? `url(#${getMarkerId(edge.markerEnd, rfId)})` : undefined),
[edge.markerEnd, rfId]
);
if (edge.hidden || !sourceX || !sourceY || !targetX || !targetY) {
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) {
return;
}
if (isSelectable) { if (isSelectable) {
store.setState({ nodesSelectionActive: false }); store.setState({ nodesSelectionActive: false });
@@ -127,91 +136,40 @@ 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) {
const { unselectNodesAndEdges, addSelectedEdges, edges } = store.getState(); const { unselectNodesAndEdges, addSelectedEdges } = store.getState();
const unselect = event.key === 'Escape'; const unselect = event.key === 'Escape';
if (unselect) { if (unselect) {
edgeRef.current?.blur(); edgeRef.current?.blur();
unselectNodesAndEdges({ edges: [edges.find((e) => e.id === id)!] }); unselectNodesAndEdges({ edges: [edge] });
} else { } else {
addSelectedEdges([id]); addSelectedEdges([id]);
} }
@@ -219,86 +177,88 @@ function EdgeWrapper({
}; };
return ( return (
<g <svg style={{ zIndex }}>
className={cc([ <g
'react-flow__edge', className={cc([
`react-flow__edge-${type}`, 'react-flow__edge',
className, `react-flow__edge-${edgeType}`,
{ selected, animated, inactive, updating: updateHover }, edge.className,
])} noPanClassName,
onClick={onEdgeClick} {
onDoubleClick={onEdgeDoubleClickHandler} selected: edge.selected,
onContextMenu={onEdgeContextMenu} animated: edge.animated,
onMouseEnter={onEdgeMouseEnter} inactive: !isSelectable && !onClick,
onMouseMove={onEdgeMouseMove} updating: updateHover,
onMouseLeave={onEdgeMouseLeave} },
onKeyDown={isFocusable ? onKeyDown : undefined} ])}
tabIndex={isFocusable ? 0 : undefined} onClick={onEdgeClick}
role={isFocusable ? 'button' : 'img'} onDoubleClick={onEdgeDoubleClick}
data-id={id} onContextMenu={onEdgeContextMenu}
data-testid={`rf__edge-${id}`} onMouseEnter={onEdgeMouseEnter}
aria-label={ariaLabel === null ? undefined : ariaLabel ? ariaLabel : `Edge from ${source} to ${target}`} onMouseMove={onEdgeMouseMove}
aria-describedby={isFocusable ? `${ARIA_EDGE_DESC_KEY}-${rfId}` : undefined} onMouseLeave={onEdgeMouseLeave}
ref={edgeRef} onKeyDown={isFocusable ? onKeyDown : undefined}
> tabIndex={isFocusable ? 0 : undefined}
{!updating && ( role={isFocusable ? 'button' : 'img'}
<EdgeComponent data-id={id}
id={id} data-testid={`rf__edge-${id}`}
source={source} aria-label={
target={target} edge.ariaLabel === null ? undefined : edge.ariaLabel || `Edge from ${edge.source} to ${edge.target}`
selected={selected} }
animated={animated} aria-describedby={isFocusable ? `${ARIA_EDGE_DESC_KEY}-${rfId}` : undefined}
label={label} ref={edgeRef}
labelStyle={labelStyle} >
labelShowBg={labelShowBg} {!updating && (
labelBgStyle={labelBgStyle} <EdgeComponent
labelBgPadding={labelBgPadding} id={id}
labelBgBorderRadius={labelBgBorderRadius} source={edge.source}
data={data} target={edge.target}
style={style} selected={edge.selected}
sourceX={edgePosition.sourceX} animated={edge.animated}
sourceY={edgePosition.sourceY} label={edge.label}
targetX={edgePosition.targetX} labelStyle={edge.labelStyle}
targetY={edgePosition.targetY} labelShowBg={edge.labelShowBg}
sourcePosition={edgePosition.sourcePosition} labelBgStyle={edge.labelBgStyle}
targetPosition={edgePosition.targetPosition} labelBgPadding={edge.labelBgPadding}
sourceHandleId={sourceHandleId} labelBgBorderRadius={edge.labelBgBorderRadius}
targetHandleId={targetHandleId} sourceX={sourceX}
markerStart={markerStartUrl} sourceY={sourceY}
markerEnd={markerEndUrl} targetX={targetX}
pathOptions={pathOptions} targetY={targetY}
interactionWidth={interactionWidth} sourcePosition={sourcePosition}
/> targetPosition={targetPosition}
)} data={edge.data}
{isUpdatable && ( style={edge.style}
<> sourceHandleId={sourceHandleId}
{(isUpdatable === 'source' || isUpdatable === true) && ( targetHandleId={targetHandleId}
<EdgeAnchor markerStart={markerStartUrl}
position={edgePosition.sourcePosition} markerEnd={markerEndUrl}
centerX={edgePosition.sourceX} pathOptions={'pathOptions' in edge ? edge.pathOptions : undefined}
centerY={edgePosition.sourceY} interactionWidth={edge.interactionWidth}
radius={edgeUpdaterRadius} />
onMouseDown={onEdgeUpdaterSourceMouseDown} )}
onMouseEnter={onEdgeUpdaterMouseEnter} {isUpdatable && (
onMouseOut={onEdgeUpdaterMouseOut} <EdgeUpdateAnchors
type="source" edge={edge}
/> isUpdatable={isUpdatable}
)} edgeUpdaterRadius={edgeUpdaterRadius}
{(isUpdatable === 'target' || isUpdatable === true) && ( onEdgeUpdate={onEdgeUpdate}
<EdgeAnchor onEdgeUpdateStart={onEdgeUpdateStart}
position={edgePosition.targetPosition} onEdgeUpdateEnd={onEdgeUpdateEnd}
centerX={edgePosition.targetX} sourceX={sourceX}
centerY={edgePosition.targetY} sourceY={sourceY}
radius={edgeUpdaterRadius} targetX={targetX}
onMouseDown={onEdgeUpdaterTargetMouseDown} targetY={targetY}
onMouseEnter={onEdgeUpdaterMouseEnter} sourcePosition={sourcePosition}
onMouseOut={onEdgeUpdaterMouseOut} targetPosition={targetPosition}
type="target" setUpdateHover={setUpdateHover}
/> setUpdating={setUpdating}
)} sourceHandleId={sourceHandleId}
</> targetHandleId={targetHandleId}
)} />
</g> )}
</g>
</svg>
); );
} }
@@ -15,3 +15,12 @@ export const builtinEdgeTypes: EdgeTypes = {
smoothstep: SmoothStepEdgeInternal as ComponentType<EdgeProps>, smoothstep: SmoothStepEdgeInternal as ComponentType<EdgeProps>,
simplebezier: SimpleBezierEdgeInternal as ComponentType<EdgeProps>, simplebezier: SimpleBezierEdgeInternal as ComponentType<EdgeProps>,
}; };
export const nullPosition = {
sourceX: null,
sourceY: null,
targetX: null,
targetY: null,
sourcePosition: null,
targetPosition: null,
};
@@ -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 });
}
};
}
@@ -17,7 +17,7 @@ import useUpdateNodePositions from '../../hooks/useUpdateNodePositions';
import { handleNodeClick } from '../Nodes/utils'; import { handleNodeClick } from '../Nodes/utils';
import type { NodeWrapperProps } from '../../types'; import type { NodeWrapperProps } from '../../types';
import { arrowKeyDiffs, builtinNodeTypes } from './utils'; import { arrowKeyDiffs, builtinNodeTypes } from './utils';
import { shallow } from 'zustand/shallow';
const NodeWrapper = ({ const NodeWrapper = ({
id, id,
onClick, onClick,
@@ -40,7 +40,23 @@ const NodeWrapper = ({
nodeOrigin, nodeOrigin,
onError, onError,
}: NodeWrapperProps) => { }: NodeWrapperProps) => {
const node = useStore((s) => s.nodeLookup.get(id)!); const { node, positionAbsoluteX, positionAbsoluteY, zIndex, isParent } = useStore((s) => {
const node = s.nodeLookup.get(id)!;
const positionAbsolute = nodeExtent
? clampPosition(node.computed?.positionAbsolute, nodeExtent)
: node.computed?.positionAbsolute || { x: 0, y: 0 };
return {
node,
// we are mutating positionAbsolute, z and isParent attributes for sub flows
// so we we need to force a re-render when some change
positionAbsoluteX: positionAbsolute.x,
positionAbsoluteY: positionAbsolute.y,
zIndex: node[internalsSymbol]?.z ?? 0,
isParent: !!node[internalsSymbol]?.isParent,
};
}, shallow);
let nodeType = node.type || 'default'; let nodeType = node.type || 'default';
let NodeComponent = nodeTypes?.[nodeType] || builtinNodeTypes[nodeType]; let NodeComponent = nodeTypes?.[nodeType] || builtinNodeTypes[nodeType];
@@ -110,31 +126,22 @@ const NodeWrapper = ({
const height = node.height ?? undefined; const height = node.height ?? undefined;
const computedWidth = node.computed?.width; const computedWidth = node.computed?.width;
const computedHeight = node.computed?.height; const computedHeight = node.computed?.height;
const positionAbsolute = nodeExtent
? clampPosition(node.computed?.positionAbsolute, nodeExtent)
: node.computed?.positionAbsolute || { x: 0, y: 0 };
const positionAbsoluteOrigin = getPositionWithOrigin({ const positionAbsoluteOrigin = getPositionWithOrigin({
x: positionAbsolute.x, x: positionAbsoluteX,
y: positionAbsolute.y, y: positionAbsoluteY,
width: computedWidth ?? width ?? 0, width: computedWidth ?? width ?? 0,
height: computedHeight ?? height ?? 0, height: computedHeight ?? height ?? 0,
origin: node.origin || nodeOrigin, origin: node.origin || nodeOrigin,
}); });
const initialized = (!!computedWidth && !!computedHeight) || (!!width && !!height); const initialized = (!!computedWidth && !!computedHeight) || (!!width && !!height);
const zIndex = node[internalsSymbol]?.z ?? 0;
const isParent = !!node[internalsSymbol]?.isParent;
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave; const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
const onMouseEnterHandler = const onMouseEnterHandler = onMouseEnter ? (event: MouseEvent) => onMouseEnter(event, { ...node }) : undefined;
onMouseEnter === undefined ? undefined : (event: MouseEvent) => onMouseEnter(event, { ...node }); const onMouseMoveHandler = onMouseMove ? (event: MouseEvent) => onMouseMove(event, { ...node }) : undefined;
const onMouseMoveHandler = const onMouseLeaveHandler = onMouseLeave ? (event: MouseEvent) => onMouseLeave(event, { ...node }) : undefined;
onMouseMove === undefined ? undefined : (event: MouseEvent) => onMouseMove(event, { ...node }); const onContextMenuHandler = onContextMenu ? (event: MouseEvent) => onContextMenu(event, { ...node }) : undefined;
const onMouseLeaveHandler = const onDoubleClickHandler = onDoubleClick ? (event: MouseEvent) => onDoubleClick(event, { ...node }) : undefined;
onMouseLeave === undefined ? undefined : (event: MouseEvent) => onMouseLeave(event, { ...node });
const onContextMenuHandler =
onContextMenu === undefined ? undefined : (event: MouseEvent) => onContextMenu(event, { ...node });
const onDoubleClickHandler =
onDoubleClick === undefined ? undefined : (event: MouseEvent) => onDoubleClick(event, { ...node });
const onSelectNodeHandler = (event: MouseEvent) => { const onSelectNodeHandler = (event: MouseEvent) => {
const { selectNodesOnDrag, nodeDragThreshold } = store.getState(); const { selectNodesOnDrag, nodeDragThreshold } = store.getState();
@@ -177,7 +184,7 @@ const NodeWrapper = ({
store.setState({ store.setState({
ariaLiveMessage: `Moved selected node ${event.key ariaLiveMessage: `Moved selected node ${event.key
.replace('Arrow', '') .replace('Arrow', '')
.toLowerCase()}. New position, x: ${~~positionAbsolute.x}, y: ${~~positionAbsolute.y}`, .toLowerCase()}. New position, x: ${~~positionAbsoluteX}, y: ${~~positionAbsoluteY}`,
}); });
updatePositions({ updatePositions({
@@ -236,8 +243,8 @@ const NodeWrapper = ({
type={nodeType} type={nodeType}
width={computedWidth} width={computedWidth}
height={computedHeight} height={computedHeight}
positionAbsoluteX={positionAbsolute.x} positionAbsoluteX={positionAbsoluteX}
positionAbsoluteY={positionAbsolute.y} positionAbsoluteY={positionAbsoluteY}
selected={node.selected} selected={node.selected}
isConnectable={isConnectable} isConnectable={isConnectable}
sourcePosition={node.sourcePosition} sourcePosition={node.sourcePosition}
@@ -51,32 +51,38 @@ const markerSelector =
return markers; return markers;
}; };
const markersEqual = (a: MarkerProps[], b: MarkerProps[]) =>
// the id includes all marker options, so we just need to look at that part of the marker
!(a.length !== b.length || a.some((m, i) => m.id !== b[i].id));
// when you have multiple flows on a page and you hide the first one, the other ones have no markers anymore // when you have multiple flows on a page and you hide the first one, the other ones have no markers anymore
// when they do have markers with the same ids. To prevent this the user can pass a unique id to the react flow wrapper // when they do have markers with the same ids. To prevent this the user can pass a unique id to the react flow wrapper
// that we can then use for creating our unique marker ids // that we can then use for creating our unique marker ids
const MarkerDefinitions = ({ defaultColor, rfId }: MarkerDefinitionsProps) => { const MarkerDefinitions = ({ defaultColor, rfId }: MarkerDefinitionsProps) => {
const markers = useStore( const markers = useStore(useCallback(markerSelector({ defaultColor, rfId }), [defaultColor, rfId]), markersEqual);
useCallback(markerSelector({ defaultColor, rfId }), [defaultColor, rfId]),
// the id includes all marker options, so we just need to look at that part of the marker if (!markers.length) {
(a, b) => !(a.length !== b.length || a.some((m, i) => m.id !== b[i].id)) return null;
); }
return ( return (
<defs> <svg className="react-flow__marker">
{markers.map((marker: MarkerProps) => ( <defs>
<Marker {markers.map((marker: MarkerProps) => (
id={marker.id} <Marker
key={marker.id} id={marker.id}
type={marker.type} key={marker.id}
color={marker.color} type={marker.type}
width={marker.width} color={marker.color}
height={marker.height} width={marker.width}
markerUnits={marker.markerUnits} height={marker.height}
strokeWidth={marker.strokeWidth} markerUnits={marker.markerUnits}
orient={marker.orient} strokeWidth={marker.strokeWidth}
/> orient={marker.orient}
))} />
</defs> ))}
</defs>
</svg>
); );
}; };
@@ -1,13 +1,12 @@
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/useVisibleEdgeIds';
import MarkerDefinitions from './MarkerDefinitions'; import MarkerDefinitions from './MarkerDefinitions';
import { GraphViewProps } from '../GraphView'; import { GraphViewProps } from '../GraphView';
import type { ReactFlowState } from '../../types';
import EdgeWrapper from '../../components/EdgeWrapper'; import EdgeWrapper from '../../components/EdgeWrapper';
import type { ReactFlowState } from '../../types';
type EdgeRendererProps = Pick< type EdgeRendererProps = Pick<
GraphViewProps, GraphViewProps,
@@ -29,7 +28,6 @@ type EdgeRendererProps = Pick<
| 'disableKeyboardA11y' | 'disableKeyboardA11y'
| 'edgeTypes' | 'edgeTypes'
> & { > & {
elevateEdgesOnSelect: boolean;
children: ReactNode; children: ReactNode;
}; };
@@ -63,76 +61,40 @@ const EdgeRenderer = ({
children, children,
}: EdgeRendererProps) => { }: EdgeRendererProps) => {
const { edgesFocusable, edgesUpdatable, elementsSelectable, onError } = useStore(selector, shallow); const { edgesFocusable, edgesUpdatable, elementsSelectable, onError } = useStore(selector, shallow);
// we are grouping edges by zIndex here in order to be able to render them in the correct order const edgeIds = useVisibleEdgeIds(onlyRenderVisibleElements);
// each zIndex gets its own svg element
const edgeTree = useVisibleEdges(onlyRenderVisibleElements, elevateEdgesOnSelect);
return ( return (
<> <div className="react-flow__edges">
{edgeTree.map(({ level, edges, isMaxLevel }) => ( <MarkerDefinitions defaultColor={defaultMarkerColor} rfId={rfId} />
<svg key={level} style={{ zIndex: level }} className="react-flow__edges react-flow__container">
{isMaxLevel && <MarkerDefinitions defaultColor={defaultMarkerColor} rfId={rfId} />}
<>
{edges.map((edge) => {
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 ( {edgeIds.map((id) => {
<EdgeWrapper return (
key={edge.id} <EdgeWrapper
id={edge.id} key={id}
className={cc([edge.className, noPanClassName])} id={id}
type={edge.type} edgesFocusable={edgesFocusable}
data={edge.data} edgesUpdatable={edgesUpdatable}
selected={!!edge.selected} elementsSelectable={elementsSelectable}
animated={!!edge.animated} noPanClassName={noPanClassName}
hidden={!!edge.hidden} onEdgeUpdate={onEdgeUpdate}
label={edge.label} onContextMenu={onEdgeContextMenu}
labelStyle={edge.labelStyle} onMouseEnter={onEdgeMouseEnter}
labelShowBg={edge.labelShowBg} onMouseMove={onEdgeMouseMove}
labelBgStyle={edge.labelBgStyle} onMouseLeave={onEdgeMouseLeave}
labelBgPadding={edge.labelBgPadding} onClick={onEdgeClick}
labelBgBorderRadius={edge.labelBgBorderRadius} edgeUpdaterRadius={edgeUpdaterRadius}
style={edge.style} onDoubleClick={onEdgeDoubleClick}
source={edge.source} onEdgeUpdateStart={onEdgeUpdateStart}
target={edge.target} onEdgeUpdateEnd={onEdgeUpdateEnd}
sourceHandleId={edge.sourceHandle} rfId={rfId}
targetHandleId={edge.targetHandle} onError={onError}
markerEnd={edge.markerEnd} edgeTypes={edgeTypes}
markerStart={edge.markerStart} elevateEdgesOnSelect={elevateEdgesOnSelect}
isSelectable={isSelectable} />
onEdgeUpdate={onEdgeUpdate} );
onContextMenu={onEdgeContextMenu} })}
onMouseEnter={onEdgeMouseEnter}
onMouseMove={onEdgeMouseMove}
onMouseLeave={onEdgeMouseLeave}
onClick={onEdgeClick}
edgeUpdaterRadius={edgeUpdaterRadius}
onEdgeDoubleClick={onEdgeDoubleClick}
onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd}
rfId={rfId}
ariaLabel={edge.ariaLabel}
isFocusable={isFocusable}
isUpdatable={isUpdatable}
pathOptions={'pathOptions' in edge ? edge.pathOptions : undefined}
interactionWidth={edge.interactionWidth}
onError={onError}
edgeTypes={edgeTypes}
/>
);
})}
</>
</svg>
))}
{children} {children}
</> </div>
); );
}; };
@@ -1,7 +1,7 @@
import { memo } from 'react'; import { memo } from 'react';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import useVisibleNodesIds from '../../hooks/useVisibleNodes'; import useVisibleNodesIds from '../../hooks/useVisibleNodeIds';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import { containerStyle } from '../../styles/utils'; import { containerStyle } from '../../styles/utils';
import { GraphViewProps } from '../GraphView'; import { GraphViewProps } from '../GraphView';
+19 -8
View File
@@ -10,7 +10,7 @@ import { getNodesInside, getEventPosition, SelectionMode } from '@xyflow/system'
import UserSelection from '../../components/UserSelection'; import UserSelection from '../../components/UserSelection';
import { containerStyle } from '../../styles/utils'; import { containerStyle } from '../../styles/utils';
import { useStore, useStoreApi } from '../../hooks/useStore'; import { useStore, useStoreApi } from '../../hooks/useStore';
import { getSelectionChanges, getConnectedEdges } from '../../utils'; import { getSelectionChanges } from '../../utils';
import type { ReactFlowProps, ReactFlowState, NodeChange, EdgeChange } from '../../types'; import type { ReactFlowProps, ReactFlowState, NodeChange, EdgeChange } from '../../types';
type PaneProps = { type PaneProps = {
@@ -156,19 +156,30 @@ const Pane = memo(
true, true,
nodeOrigin nodeOrigin
); );
const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id);
const selectedNodeIds = selectedNodes.map((n) => n.id);
if (prevSelectedNodesCount.current !== selectedNodeIds.length) { const selectedEdgeIds = new Set<string>();
prevSelectedNodesCount.current = selectedNodeIds.length; const selectedNodeIds = new Set<string>();
const changes = getSelectionChanges(nodes, selectedNodeIds) as NodeChange[];
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) { if (changes.length) {
onNodesChange?.(changes); onNodesChange?.(changes);
} }
} }
if (prevSelectedEdgesCount.current !== selectedEdgeIds.length) { if (prevSelectedEdgesCount.current !== selectedEdgeIds.size) {
prevSelectedEdgesCount.current = selectedEdgeIds.length; prevSelectedEdgesCount.current = selectedEdgeIds.size;
const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[]; const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[];
if (changes.length) { if (changes.length) {
onEdgesChange?.(changes); onEdgesChange?.(changes);
@@ -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;
@@ -1,51 +0,0 @@
import { useCallback } from 'react';
import { GroupedEdges, groupEdgesByZLevel, isEdgeVisible } from '@xyflow/system';
import { useStore } from '../hooks/useStore';
import { Edge, type ReactFlowState } from '../types';
import { shallow } from 'zustand/shallow';
function useVisibleEdges(onlyRenderVisible: boolean, elevateEdgesOnSelect: boolean): GroupedEdges<Edge>[] {
const edges = 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 groupEdgesByZLevel(visibleEdges, s.nodeLookup, elevateEdgesOnSelect);
},
[onlyRenderVisible, elevateEdgesOnSelect]
),
(groupA, groupB) => {
const unEqual = groupA.some(
(item, index) =>
item.isMaxLevel !== groupB[index].isMaxLevel ||
item.level !== groupB[index].level ||
!shallow(item.edges, groupB[index].edges)
);
return !unEqual;
}
);
return edges;
}
export default useVisibleEdges;
@@ -1,7 +1,7 @@
import { getNodesInside } from '@xyflow/system'; import { getNodesInside } from '@xyflow/system';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import { useStore } from '../hooks/useStore'; import { useStore } from './useStore';
import type { Node, ReactFlowState } from '../types'; import type { Node, ReactFlowState } from '../types';
import { useCallback } from 'react'; import { useCallback } from 'react';
+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
+13 -11
View File
@@ -55,12 +55,11 @@ const createRFStore = ({
set({ nodes: nodesWithInternalData }); set({ nodes: nodesWithInternalData });
}, },
setEdges: (edges: Edge[]) => { setEdges: (edges: Edge[]) => {
const { defaultEdgeOptions = {}, connectionLookup } = get(); const { connectionLookup, edgeLookup } = get();
const nextEdges = edges.map((e) => ({ ...defaultEdgeOptions, ...e }));
updateConnectionLookup(connectionLookup, nextEdges); updateConnectionLookup(connectionLookup, edgeLookup, edges);
set({ edges: nextEdges }); set({ edges });
}, },
// when the user works with an uncontrolled flow, // when the user works with an uncontrolled flow,
// we set a flag `hasDefaultNodes` / `hasDefaultEdges` // we set a flag `hasDefaultNodes` / `hasDefaultEdges`
@@ -79,13 +78,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;
} }
@@ -193,8 +195,8 @@ const createRFStore = ({
if (multiSelectionActive) { if (multiSelectionActive) {
changedNodes = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true)) as NodeSelectionChange[]; changedNodes = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true)) as NodeSelectionChange[];
} else { } else {
changedNodes = getSelectionChanges(nodes, selectedNodeIds); changedNodes = getSelectionChanges(nodes, new Set([...selectedNodeIds]), true);
changedEdges = getSelectionChanges(edges, []); changedEdges = getSelectionChanges(edges);
} }
updateNodesAndEdgesSelections({ updateNodesAndEdgesSelections({
@@ -212,8 +214,8 @@ const createRFStore = ({
if (multiSelectionActive) { if (multiSelectionActive) {
changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true)) as EdgeSelectionChange[]; changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true)) as EdgeSelectionChange[];
} else { } else {
changedEdges = getSelectionChanges(edges, selectedEdgeIds); changedEdges = getSelectionChanges(edges, new Set([...selectedEdgeIds]));
changedNodes = getSelectionChanges(nodes, []); changedNodes = getSelectionChanges(nodes, new Set(), true);
} }
updateNodesAndEdgesSelections({ updateNodesAndEdgesSelections({
+6 -2
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,
@@ -46,7 +49,8 @@ const getInitialState = ({
transform, transform,
nodes: nextNodes, nodes: nextNodes,
nodeLookup, nodeLookup,
edges: edges, edges,
edgeLookup,
connectionLookup, connectionLookup,
onNodesChange: null, onNodesChange: null,
onEdgesChange: null, onEdgesChange: null,
+11 -9
View File
@@ -34,9 +34,8 @@ export type EdgeUpdatable = boolean | HandleType;
export type DefaultEdge<EdgeData = any> = EdgeBase<EdgeData> & { export type DefaultEdge<EdgeData = any> = EdgeBase<EdgeData> & {
style?: CSSProperties; style?: CSSProperties;
className?: string; className?: string;
sourceNode?: Node;
targetNode?: Node;
updatable?: EdgeUpdatable; updatable?: EdgeUpdatable;
focusable?: boolean;
} & EdgeLabelOptions; } & EdgeLabelOptions;
type SmoothStepEdgeType<T> = DefaultEdge<T> & { type SmoothStepEdgeType<T> = DefaultEdge<T> & {
@@ -58,9 +57,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,12 +76,9 @@ 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;
}; };
export type DefaultEdgeOptions = DefaultEdgeOptionsBase<Edge>; export type DefaultEdgeOptions = DefaultEdgeOptionsBase<Edge>;
@@ -93,13 +94,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'> &
+1
View File
@@ -2,6 +2,7 @@ import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
import type { CoordinateExtent, NodeBase, NodeOrigin, OnError } from '@xyflow/system'; import type { CoordinateExtent, NodeBase, NodeOrigin, OnError } from '@xyflow/system';
import { NodeTypes } from './general'; import { NodeTypes } from './general';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Node<NodeData = any, NodeType extends string | undefined = string | undefined> = NodeBase< export type Node<NodeData = any, NodeType extends string | undefined = string | undefined> = NodeBase<
NodeData, NodeData,
NodeType NodeType
+6 -4
View File
@@ -24,7 +24,9 @@ import {
type OnMoveEnd, type OnMoveEnd,
type IsValidConnection, type IsValidConnection,
type UpdateConnection, type UpdateConnection,
Connection, type EdgeLookup,
type ConnectionLookup,
type NodeLookup,
} from '@xyflow/system'; } from '@xyflow/system';
import type { import type {
@@ -48,10 +50,10 @@ export type ReactFlowStore = {
height: number; height: number;
transform: Transform; transform: Transform;
nodes: Node[]; nodes: Node[];
nodeLookup: Map<string, Node>; nodeLookup: NodeLookup<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;
hasDefaultNodes: boolean; hasDefaultNodes: boolean;
+26 -17
View File
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* 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) { export function handleParentExpand(res: any[], updateItem: any) {
const parent = res.find((e) => e.id === updateItem.parentNode); const parent = res.find((e) => e.id === updateItem.parentNode);
@@ -57,12 +57,11 @@ function applyChanges(changes: any[], elements: any[]): any[] {
let remainingChanges = changes; let remainingChanges = changes;
const updatedElements: any[] = []; const updatedElements: any[] = [];
for (let i = 0; i < elements.length; i++) { for (const item of elements) {
const nextChanges: any[] = []; const nextChanges: any[] = [];
const _remainingChanges: any[] = []; const _remainingChanges: any[] = [];
const item = elements[i];
remainingChanges.forEach((c) => { for (const c of remainingChanges) {
if (c.type === 'add') { if (c.type === 'add') {
updatedElements.push(c.item); updatedElements.push(c.item);
} else if (c.id === item.id) { } else if (c.id === item.id) {
@@ -70,7 +69,8 @@ function applyChanges(changes: any[], elements: any[]): any[] {
} else { } else {
_remainingChanges.push(c); _remainingChanges.push(c);
} }
}); }
remainingChanges = _remainingChanges; remainingChanges = _remainingChanges;
if (nextChanges.length === 0) { if (nextChanges.length === 0) {
@@ -150,24 +150,33 @@ export function applyEdgeChanges<EdgeData = any>(changes: EdgeChange[], edges: E
return applyChanges(changes, edges) as Edge<EdgeData>[]; return applyChanges(changes, edges) as Edge<EdgeData>[];
} }
export const createSelectionChange = (id: string, selected: boolean) => ({ export const createSelectionChange = (id: string, selected: boolean): NodeSelectionChange | EdgeSelectionChange => ({
id, id,
type: 'select', type: 'select',
selected, selected,
}); });
export function getSelectionChanges(items: any[], selectedIds: string[]) { export function getSelectionChanges(
return items.reduce((res, item) => { items: any[],
const willBeSelected = selectedIds.includes(item.id); selectedIds: Set<string> = new Set(),
mutateItem = false
): NodeSelectionChange[] | EdgeSelectionChange[] {
const changes: NodeSelectionChange[] | EdgeSelectionChange[] = [];
if (!item.selected && willBeSelected) { for (const item of items) {
item.selected = true; const willBeSelected = selectedIds.has(item.id);
res.push(createSelectionChange(item.id, true));
} else if (item.selected && !willBeSelected) { // we don't want to set all items to selected=false on the first selection
item.selected = false; if (!(item.selected === undefined && !willBeSelected) && item.selected !== willBeSelected) {
res.push(createSelectionChange(item.id, false)); 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;
}, []);
} }
@@ -18,6 +18,7 @@
export let target: $$Props['target'] = ''; export let target: $$Props['target'] = '';
export let data: $$Props['data'] = {}; export let data: $$Props['data'] = {};
export let style: $$Props['style'] = undefined; export let style: $$Props['style'] = undefined;
export let zIndex: $$Props['zIndex'] = undefined;
export let animated: $$Props['animated'] = false; export let animated: $$Props['animated'] = false;
export let selected: $$Props['selected'] = false; export let selected: $$Props['selected'] = false;
@@ -95,42 +96,44 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions --> <!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
{#if !hidden} {#if !hidden}
<g <svg style:zIndex>
class={cc(['svelte-flow__edge', className])} <g
class:animated class={cc(['svelte-flow__edge', className])}
class:selected class:animated
data-id={id} class:selected
on:click={onClick} data-id={id}
on:contextmenu={onContextMenu} on:click={onClick}
aria-label={ariaLabel === null on:contextmenu={onContextMenu}
? undefined aria-label={ariaLabel === null
: ariaLabel ? undefined
? ariaLabel : ariaLabel
: `Edge from ${source} to ${target}`} ? ariaLabel
role="img" : `Edge from ${source} to ${target}`}
> role="img"
<svelte:component >
this={edgeComponent} <svelte:component
{id} this={edgeComponent}
{source} {id}
{target} {source}
{sourceX} {target}
{sourceY} {sourceX}
{targetX} {sourceY}
{targetY} {targetX}
{sourcePosition} {targetY}
{targetPosition} {sourcePosition}
{animated} {targetPosition}
{selected} {animated}
{label} {selected}
{labelStyle} {label}
{data} {labelStyle}
{style} {data}
{interactionWidth} {style}
sourceHandleId={sourceHandle} {interactionWidth}
targetHandleId={targetHandle} sourceHandleId={sourceHandle}
markerStart={markerStartUrl} targetHandleId={targetHandle}
markerEnd={markerEndUrl} markerStart={markerStartUrl}
/> markerEnd={markerEndUrl}
</g> />
</g>
</svg>
{/if} {/if}
@@ -9,7 +9,7 @@
const { const {
elementsSelectable, elementsSelectable,
edgeTree, visibleEdges,
edges: { setDefaultOptions } edges: { setDefaultOptions }
} = useStore(); } = useStore();
@@ -18,57 +18,47 @@
}); });
</script> </script>
{#each $edgeTree as group (group.level)} <div class="svelte-flow__edges">
<svg style="z-index: {group.level}" class="svelte-flow__edges"> <svg class="svelte-flow__marker">
{#if group.isMaxLevel} <MarkerDefinition />{/if} <MarkerDefinition />
<g>
{#each group.edges as edge (edge.id)}
{@const edgeType = edge.type || 'default'}
{@const selectable = !!(
edge.selectable ||
($elementsSelectable && typeof edge.selectable === 'undefined')
)}
<EdgeWrapper
id={edge.id}
source={edge.source}
target={edge.target}
data={edge.data}
style={edge.style}
animated={edge.animated}
selected={edge.selected}
hidden={edge.hidden}
label={edge.label}
labelStyle={edge.labelStyle}
markerStart={edge.markerStart}
markerEnd={edge.markerEnd}
sourceHandle={edge.sourceHandle}
targetHandle={edge.targetHandle}
sourceX={edge.sourceX}
sourceY={edge.sourceY}
targetX={edge.targetX}
targetY={edge.targetY}
sourcePosition={edge.sourcePosition}
targetPosition={edge.targetPosition}
ariaLabel={edge.ariaLabel}
interactionWidth={edge.interactionWidth}
class={edge.class}
type={edgeType}
{selectable}
on:edgeclick
on:edgecontextmenu
/>
{/each}
</g>
</svg> </svg>
{/each}
<style> {#each $visibleEdges as edge (edge.id)}
.svelte-flow__edges { {@const edgeType = edge.type || 'default'}
width: 100%; {@const selectable = !!(
height: 100%; edge.selectable ||
position: absolute; ($elementsSelectable && typeof edge.selectable === 'undefined')
top: 0; )}
left: 0;
} <EdgeWrapper
</style> id={edge.id}
source={edge.source}
target={edge.target}
data={edge.data}
style={edge.style}
animated={edge.animated}
selected={edge.selected}
hidden={edge.hidden}
label={edge.label}
labelStyle={edge.labelStyle}
markerStart={edge.markerStart}
markerEnd={edge.markerEnd}
sourceHandle={edge.sourceHandle}
targetHandle={edge.targetHandle}
sourceX={edge.sourceX}
sourceY={edge.sourceY}
targetX={edge.targetX}
targetY={edge.targetY}
sourcePosition={edge.sourcePosition}
targetPosition={edge.targetPosition}
ariaLabel={edge.ariaLabel}
interactionWidth={edge.interactionWidth}
class={edge.class}
type={edgeType}
zIndex={edge.zIndex}
{selectable}
on:edgeclick
on:edgecontextmenu
/>
{/each}
</div>
+2 -2
View File
@@ -24,7 +24,7 @@ import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions, ConnectionData }
import { initialEdgeTypes, initialNodeTypes, getInitialStore } from './initial-store'; import { initialEdgeTypes, initialNodeTypes, getInitialStore } from './initial-store';
import type { SvelteFlowStore } from './types'; import type { SvelteFlowStore } from './types';
import { syncNodeStores, syncEdgeStores, syncViewportStores } from './utils'; import { syncNodeStores, syncEdgeStores, syncViewportStores } from './utils';
import { getEdgeTree } from './edge-tree'; import { getVisibleEdges } from './visible-edges';
import { getVisibleNodes } from './visible-nodes'; import { getVisibleNodes } from './visible-nodes';
import { getDerivedConnectionProps } from './derived-connection-props'; import { getDerivedConnectionProps } from './derived-connection-props';
@@ -357,8 +357,8 @@ export function createStore({
...store, ...store,
// derived state // derived state
edgeTree: getEdgeTree(store),
connection: getDerivedConnectionProps(store, currentConnection), connection: getDerivedConnectionProps(store, currentConnection),
visibleEdges: getVisibleEdges(store),
visibleNodes: getVisibleNodes(store), visibleNodes: getVisibleNodes(store),
markers: derived( markers: derived(
[store.edges, store.defaultMarkerColor, store.flowId], [store.edges, store.defaultMarkerColor, store.flowId],
+13 -11
View File
@@ -4,25 +4,25 @@ import {
SelectionMode, SelectionMode,
ConnectionMode, ConnectionMode,
ConnectionLineType, ConnectionLineType,
devWarn,
adoptUserProvidedNodes,
getNodesBounds,
getViewportForBounds,
updateConnectionLookup,
type SelectionRect, type SelectionRect,
type SnapGrid, type SnapGrid,
type MarkerProps, type MarkerProps,
type PanZoomInstance, type PanZoomInstance,
type CoordinateExtent, type CoordinateExtent,
type IsValidConnection, type IsValidConnection,
type GroupedEdges,
type NodeOrigin, type NodeOrigin,
type OnError, type OnError,
devWarn,
type Viewport, type Viewport,
adoptUserProvidedNodes,
getNodesBounds,
getViewportForBounds,
updateConnectionLookup,
type ConnectionLookup, type ConnectionLookup,
type OnConnect, type OnConnect,
type OnConnectStart, type OnConnectStart,
type OnConnectEnd type OnConnectEnd,
type NodeLookup
} from '@xyflow/system'; } from '@xyflow/system';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte'; import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
@@ -82,7 +82,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 };
@@ -95,10 +97,10 @@ export const getInitialStore = ({
return { return {
flowId: writable<string | null>(null), flowId: writable<string | null>(null),
nodes: createNodesStore(nextNodes, nodeLookup), nodes: createNodesStore(nextNodes, nodeLookup),
nodeLookup: readable<Map<string, Node>>(nodeLookup), nodeLookup: readable<NodeLookup>(nodeLookup),
visibleNodes: readable<Node[]>([]), visibleNodes: readable<Node[]>([]),
edges: createEdgesStore(edges, connectionLookup), edges: createEdgesStore(edges, connectionLookup, edgeLookup),
edgeTree: readable<GroupedEdges<EdgeLayouted>[]>([]), visibleEdges: readable<EdgeLayouted[]>([]),
connectionLookup: readable<ConnectionLookup>(connectionLookup), connectionLookup: readable<ConnectionLookup>(connectionLookup),
height: writable<number>(500), height: writable<number>(500),
width: writable<number>(500), width: writable<number>(500),
+8 -5
View File
@@ -1,17 +1,19 @@
import { import {
writable, writable,
get,
type Unsubscriber, type Unsubscriber,
type Subscriber, type Subscriber,
type Updater, type Updater,
type Writable, type Writable
get
} from 'svelte/store'; } from 'svelte/store';
import { import {
adoptUserProvidedNodes, adoptUserProvidedNodes,
updateConnectionLookup, updateConnectionLookup,
type Viewport, type Viewport,
type PanZoomInstance, type PanZoomInstance,
type ConnectionLookup type ConnectionLookup,
type EdgeLookup,
type NodeLookup
} from '@xyflow/system'; } from '@xyflow/system';
import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, Node } from '$lib/types'; import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, Node } from '$lib/types';
@@ -125,7 +127,7 @@ export type NodeStoreOptions = {
// The user only passes in relative positions, so we need to calculate the absolute positions based on the parent nodes. // The user only passes in relative positions, so we need to calculate the absolute positions based on the parent nodes.
export const createNodesStore = ( export const createNodesStore = (
nodes: Node[], nodes: Node[],
nodeLookup: Map<string, Node> nodeLookup: NodeLookup<Node>
): { ): {
subscribe: (this: void, run: Subscriber<Node[]>) => Unsubscriber; subscribe: (this: void, run: Subscriber<Node[]>) => Unsubscriber;
update: (this: void, updater: Updater<Node[]>) => void; update: (this: void, updater: Updater<Node[]>) => void;
@@ -175,6 +177,7 @@ export const createNodesStore = (
export const createEdgesStore = ( export const createEdgesStore = (
edges: Edge[], edges: Edge[],
connectionLookup: ConnectionLookup, connectionLookup: ConnectionLookup,
edgeLookup: EdgeLookup<Edge>,
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 +187,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);
@@ -1,10 +1,10 @@
import { derived } from 'svelte/store'; import { derived } from 'svelte/store';
import { groupEdgesByZLevel, isEdgeVisible, getEdgePosition } from '@xyflow/system'; import { isEdgeVisible, getEdgePosition, getElevatedEdgeZIndex } from '@xyflow/system';
import type { EdgeLayouted } from '$lib/types'; import type { EdgeLayouted } from '$lib/types';
import type { SvelteFlowStoreState } from './types'; import type { SvelteFlowStoreState } from './types';
export function getEdgeTree(store: SvelteFlowStoreState) { export function getVisibleEdges(store: SvelteFlowStoreState) {
const visibleEdges = derived( const visibleEdges = derived(
[ [
store.edges, store.edges,
@@ -64,6 +64,13 @@ export function getEdgeTree(store: SvelteFlowStoreState) {
if (edgePosition) { if (edgePosition) {
res.push({ res.push({
...edge, ...edge,
zIndex: getElevatedEdgeZIndex({
selected: edge.selected,
zIndex: edge.zIndex,
sourceNode,
targetNode,
elevateOnSelect: false
}),
...edgePosition ...edgePosition
}); });
} }
@@ -71,9 +78,7 @@ export function getEdgeTree(store: SvelteFlowStoreState) {
return res; return res;
}, []); }, []);
const groupedEdges = groupEdgesByZLevel<EdgeLayouted>(layoutedEdges, nodeLookup, false); return layoutedEdges;
return groupedEdges;
} }
); );
} }
+3 -2
View File
@@ -12,7 +12,7 @@ import type {
import type { Node } from '$lib/types'; import type { Node } from '$lib/types';
export type DefaultEdge<EdgeData = any> = Omit<EdgeBase<EdgeData>, 'focusable'> & { export type DefaultEdge<EdgeData = any> = EdgeBase<EdgeData> & {
label?: string; label?: string;
labelStyle?: string; labelStyle?: string;
style?: string; style?: string;
@@ -75,7 +75,7 @@ export type StraightEdgeProps<T = any> = Omit<
export type EdgeTypes = Record<string, ComponentType<SvelteComponent<EdgeProps>>>; export type EdgeTypes = Record<string, ComponentType<SvelteComponent<EdgeProps>>>;
export type DefaultEdgeOptions = Omit<DefaultEdgeOptionsBase<Edge>, 'focusable'>; export type DefaultEdgeOptions = DefaultEdgeOptionsBase<Edge>;
export type EdgeLayouted = Pick< export type EdgeLayouted = Pick<
Edge, Edge,
@@ -98,6 +98,7 @@ export type EdgeLayouted = Pick<
| 'ariaLabel' | 'ariaLabel'
| 'hidden' | 'hidden'
| 'class' | 'class'
| 'zIndex'
> & > &
EdgePosition & { EdgePosition & {
sourceNode?: Node; sourceNode?: Node;
-5
View File
@@ -92,11 +92,6 @@
outline: none; outline: none;
} }
.xy-flow .xy-flow__edges {
pointer-events: none;
overflow: visible;
}
.xy-flow__edge-path { .xy-flow__edge-path {
stroke: var(--xy-edge-stroke, var(--xy-edge-stroke-default)); stroke: var(--xy-edge-stroke, var(--xy-edge-stroke-default));
stroke-width: var(--xy-edge-stroke-width, var(--xy-edge-stroke-width-default)); stroke-width: var(--xy-edge-stroke-width, var(--xy-edge-stroke-width-default));
+6
View File
@@ -43,6 +43,12 @@
--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, 0.08); --xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, 0.08);
} }
.xy-flow__edges svg {
overflow: visible;
position: absolute;
pointer-events: none;
}
.xy-flow__edge { .xy-flow__edge {
&.updating { &.updating {
.xy-flow__edge-path { .xy-flow__edge-path {
+3 -2
View File
@@ -19,7 +19,6 @@ export type EdgeBase<EdgeData = any> = {
zIndex?: number; zIndex?: number;
ariaLabel?: string; ariaLabel?: string;
interactionWidth?: number; interactionWidth?: number;
focusable?: boolean;
}; };
export type SmoothStepPathOptions = { export type SmoothStepPathOptions = {
@@ -37,7 +36,7 @@ export type BezierPathOptions = {
export type DefaultEdgeOptionsBase<EdgeType extends EdgeBase> = Omit< export type DefaultEdgeOptionsBase<EdgeType extends EdgeBase> = Omit<
EdgeType, EdgeType,
'id' | 'source' | 'target' | 'sourceHandle' | 'targetHandle' | 'sourceNode' | 'targetNode' 'id' | 'source' | 'target' | 'sourceHandle' | 'targetHandle' | 'selected'
>; >;
export enum ConnectionLineType { export enum ConnectionLineType {
@@ -77,3 +76,5 @@ export type EdgePosition = {
sourcePosition: Position; sourcePosition: Position;
targetPosition: Position; targetPosition: Position;
}; };
export type EdgeLookup<EdgeType extends EdgeBase = EdgeBase> = Map<string, EdgeType>;
+2
View File
@@ -107,3 +107,5 @@ export type OnSelectionDrag = (event: MouseEvent, nodes: NodeBase[]) => void;
export type NodeHandle = Optional<HandleElement, 'width' | 'height'>; export type NodeHandle = Optional<HandleElement, 'width' | 'height'>;
export type Align = 'center' | 'start' | 'end'; export type Align = 'center' | 'start' | 'end';
export type NodeLookup<NodeType extends NodeBase = NodeBase> = Map<string, NodeType>;
+20 -54
View File
@@ -1,6 +1,6 @@
import { Connection, Transform, errorMessages, internalsSymbol, isEdgeBase } from '../..'; import { Connection, Transform, errorMessages, internalsSymbol, isEdgeBase } from '../..';
import { EdgeBase, NodeBase } from '../../types'; import { EdgeBase, NodeBase } from '../../types';
import { isNumeric, getOverlappingArea, boxToRect, nodeToBox, getBoundsOfBoxes, devWarn } from '../general'; import { getOverlappingArea, boxToRect, nodeToBox, getBoundsOfBoxes, devWarn } from '../general';
// this is used for straight edges and simple smoothstep edges (LTR, RTL, BTT, TTB) // this is used for straight edges and simple smoothstep edges (LTR, RTL, BTT, TTB)
export function getEdgeCenter({ export function getEdgeCenter({
@@ -23,63 +23,29 @@ export function getEdgeCenter({
return [centerX, centerY, xOffset, yOffset]; return [centerX, centerY, xOffset, yOffset];
} }
const defaultEdgeTree = [{ level: 0, isMaxLevel: true, edges: [] }]; export type GetEdgeZIndexParams = {
sourceNode: NodeBase;
export type GroupedEdges<EdgeType extends EdgeBase> = { targetNode: NodeBase;
edges: EdgeType[]; selected?: boolean;
level: number; zIndex?: number;
isMaxLevel: boolean; elevateOnSelect?: boolean;
}; };
export function groupEdgesByZLevel<EdgeType extends EdgeBase>( export function getElevatedEdgeZIndex({
edges: EdgeType[], sourceNode,
nodeLookup: Map<string, NodeBase>, targetNode,
elevateEdgesOnSelect = false selected = false,
): GroupedEdges<EdgeType>[] { zIndex = 0,
let maxLevel = -1; elevateOnSelect = false,
}: GetEdgeZIndexParams): number {
const levelLookup = edges.reduce<Record<string, EdgeType[]>>((tree, edge) => { if (!elevateOnSelect) {
const hasZIndex = isNumeric(edge.zIndex); return zIndex;
let z = hasZIndex ? edge.zIndex! : 0;
if (elevateEdgesOnSelect) {
const targetNode = nodeLookup.get(edge.target);
const sourceNode = nodeLookup.get(edge.source);
const edgeOrConnectedNodeSelected = edge.selected || targetNode?.selected || sourceNode?.selected;
const selectedZIndex = Math.max(
sourceNode?.[internalsSymbol]?.z || 0,
targetNode?.[internalsSymbol]?.z || 0,
1000
);
z = (hasZIndex ? edge.zIndex! : 0) + (edgeOrConnectedNodeSelected ? selectedZIndex : 0);
}
if (tree[z]) {
tree[z].push(edge);
} else {
tree[z] = [edge];
}
maxLevel = z > maxLevel ? z : maxLevel;
return tree;
}, {});
const edgeTree = Object.entries(levelLookup).map(([key, edges]) => {
const level = +key;
return {
edges,
level,
isMaxLevel: level === maxLevel,
};
});
if (edgeTree.length === 0) {
return defaultEdgeTree;
} }
return edgeTree; const edgeOrConnectedNodeSelected = selected || targetNode.selected || sourceNode.selected;
const selectedZIndex = Math.max(sourceNode[internalsSymbol]?.z || 0, targetNode[internalsSymbol]?.z || 0, 1000);
return zIndex + (edgeOrConnectedNodeSelected ? selectedZIndex : 0);
} }
type IsEdgeVisibleParams = { type IsEdgeVisibleParams = {
+45 -76
View File
@@ -1,7 +1,7 @@
import { EdgePosition } from '../../types/edges'; import { EdgePosition } from '../../types/edges';
import { ConnectionMode, OnError } from '../../types/general'; import { ConnectionMode, OnError } from '../../types/general';
import { NodeBase, NodeHandle, NodeHandleBounds } from '../../types/nodes'; import { NodeBase, NodeHandle } from '../../types/nodes';
import { Position, Rect, XYPosition } from '../../types/utils'; import { Position } from '../../types/utils';
import { errorMessages, internalsSymbol } from '../../constants'; import { errorMessages, internalsSymbol } from '../../constants';
import { HandleElement } from '../../types'; import { HandleElement } from '../../types';
@@ -15,21 +15,28 @@ export type GetEdgePositionParams = {
onError?: OnError; onError?: OnError;
}; };
export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | null { function isNodeInitialized(node: NodeBase): boolean {
const [sourceNodeRect, sourceHandleBounds, isSourceValid] = getHandleDataByNode(params.sourceNode); return !!node?.[internalsSymbol]?.handleBounds && !!node?.computed?.width;
const [targetNodeRect, targetHandleBounds, isTargetValid] = getHandleDataByNode(params.targetNode); }
if (!isSourceValid || !isTargetValid) { export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | null {
const { sourceNode, targetNode } = params;
if (!isNodeInitialized(sourceNode) || !isNodeInitialized(targetNode)) {
return null; return null;
} }
// when connection type is loose we can define all handles as sources and connect source -> source const sourceHandleBounds = sourceNode[internalsSymbol]?.handleBounds || toHandleBounds(sourceNode.handles);
const targetNodeHandles = const targetHandleBounds = targetNode[internalsSymbol]?.handleBounds || toHandleBounds(targetNode.handles);
const sourceHandle = getHandle(sourceHandleBounds?.source ?? [], params.sourceHandle);
const targetHandle = getHandle(
// when connection type is loose we can define all handles as sources and connect source -> source
params.connectionMode === ConnectionMode.Strict params.connectionMode === ConnectionMode.Strict
? targetHandleBounds!.target ? targetHandleBounds?.target ?? []
: (targetHandleBounds!.target ?? []).concat(targetHandleBounds!.source ?? []); : (targetHandleBounds?.target ?? []).concat(targetHandleBounds?.source ?? []),
const sourceHandle = getHandle(sourceHandleBounds!.source!, params.sourceHandle); params.targetHandle
const targetHandle = getHandle(targetNodeHandles!, params.targetHandle); );
const sourcePosition = sourceHandle?.position || Position.Bottom; const sourcePosition = sourceHandle?.position || Position.Bottom;
const targetPosition = targetHandle?.position || Position.Top; const targetPosition = targetHandle?.position || Position.Top;
@@ -46,8 +53,8 @@ export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | n
return null; return null;
} }
const { x: sourceX, y: sourceY } = getHandlePosition(sourcePosition, sourceNodeRect, sourceHandle); const [sourceX, sourceY] = getHandlePosition(sourcePosition, sourceNode, sourceHandle);
const { x: targetX, y: targetY } = getHandlePosition(targetPosition, targetNodeRect, targetHandle); const [targetX, targetY] = getHandlePosition(targetPosition, targetNode, targetHandle);
return { return {
sourceX, sourceX,
@@ -64,79 +71,41 @@ function toHandleBounds(handles?: NodeHandle[]) {
return null; return null;
} }
return handles.reduce<NodeHandleBounds>( const source = [];
(res, item) => { const target = [];
item.width = item.width || 1;
item.height = item.height || 1;
if (item.type === 'source') { for (const handle of handles) {
res.source?.push(item as HandleElement); handle.width = handle.width || 1;
} handle.height = handle.height || 1;
if (item.type === 'target') { if (handle.type === 'source') {
res.target?.push(item as HandleElement); source.push(handle as HandleElement);
} } else if (handle.type === 'target') {
target.push(handle as HandleElement);
return res;
},
{
source: [],
target: [],
} }
); }
return {
source,
target,
};
} }
function getHandleDataByNode(node?: NodeBase): [Rect, NodeHandleBounds | null, boolean] { function getHandlePosition(position: Position, node: NodeBase, handle: HandleElement | null = null): number[] {
const handleBounds = node?.[internalsSymbol]?.handleBounds || toHandleBounds(node?.handles) || null; const x = (handle?.x ?? 0) + (node.computed?.positionAbsolute?.x ?? 0);
const nodeWidth = node?.computed?.width || node?.width; const y = (handle?.y ?? 0) + (node.computed?.positionAbsolute?.y ?? 0);
const nodeHeight = node?.computed?.height || node?.height; const width = handle?.width || (node?.computed?.width ?? node?.width ?? 0);
const height = handle?.height || (node?.computed?.height ?? node?.height ?? 0);
const isValid =
handleBounds &&
nodeWidth &&
nodeHeight &&
typeof node?.computed?.positionAbsolute?.x !== 'undefined' &&
typeof node?.computed?.positionAbsolute?.y !== 'undefined';
return [
{
x: node?.computed?.positionAbsolute?.x || 0,
y: node?.computed?.positionAbsolute?.y || 0,
width: nodeWidth || 0,
height: nodeHeight || 0,
},
handleBounds,
!!isValid,
];
}
function getHandlePosition(position: Position, nodeRect: Rect, handle: HandleElement | null = null): XYPosition {
const x = (handle?.x || 0) + nodeRect.x;
const y = (handle?.y || 0) + nodeRect.y;
const width = handle?.width || nodeRect.width;
const height = handle?.height || nodeRect.height;
switch (position) { switch (position) {
case Position.Top: case Position.Top:
return { return [x + width / 2, y];
x: x + width / 2,
y,
};
case Position.Right: case Position.Right:
return { return [x + width, y + height / 2];
x: x + width,
y: y + height / 2,
};
case Position.Bottom: case Position.Bottom:
return { return [x + width / 2, y + height];
x: x + width / 2,
y: y + height,
};
case Position.Left: case Position.Left:
return { return [x, y + height / 2];
x,
y: y + height / 2,
};
} }
} }
+22 -18
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';
@@ -42,10 +43,13 @@ export function updateAbsolutePositions<NodeType extends NodeBase>(
parentNode?.origin || nodeOrigin parentNode?.origin || nodeOrigin
); );
node.computed!.positionAbsolute = { const positionChanged = x !== node.computed?.positionAbsolute?.x || y !== node.computed?.positionAbsolute?.y;
x, node.computed!.positionAbsolute = positionChanged
y, ? {
}; x,
y,
}
: node.computed?.positionAbsolute;
node[internalsSymbol]!.z = z; node[internalsSymbol]!.z = z;
@@ -244,22 +248,22 @@ export function panBy({
return transformChanged; return transformChanged;
} }
export function updateConnectionLookup(lookup: ConnectionLookup, edges: EdgeBase[]) { export function updateConnectionLookup(connectionLookup: ConnectionLookup, edgeLookup: EdgeLookup, edges: EdgeBase[]) {
lookup.clear(); connectionLookup.clear();
edgeLookup.clear();
edges.forEach(({ source, target, sourceHandle = null, targetHandle = null }) => { for (const edge of edges) {
if (source && target) { 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 sourceKey = `${source}-source-${sourceHandle}`;
const prevTarget = lookup.get(targetKey) || new Map(); const targetKey = `${target}-target-${targetHandle}`;
const connection = { source, target, sourceHandle, targetHandle };
lookup.set(sourceKey, prevSource.set(`${target}-${targetHandle}`, connection)); const prevSource = connectionLookup.get(sourceKey) || new Map();
lookup.set(targetKey, prevTarget.set(`${source}-${sourceHandle}`, connection)); const prevTarget = connectionLookup.get(targetKey) || new Map();
} const connection = { source, target, sourceHandle, targetHandle };
});
return lookup; edgeLookup.set(edge.id, edge);
connectionLookup.set(sourceKey, prevSource.set(`${target}-${targetHandle}`, connection));
connectionLookup.set(targetKey, prevTarget.set(`${source}-${sourceHandle}`, connection));
}
} }