Merge pull request #3632 from xyflow/refactor/edge-rendering
refactor(edges): simplify edge rendering
This commit is contained in:
@@ -6,7 +6,6 @@ import {
|
||||
addEdge,
|
||||
applyNodeChanges,
|
||||
applyEdgeChanges,
|
||||
ReactFlowInstance,
|
||||
Connection,
|
||||
Edge,
|
||||
Node,
|
||||
@@ -97,7 +96,6 @@ const initialEdges: Edge[] = [
|
||||
{ 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) =>
|
||||
console.log(`start update ${handleType} handle`, edge);
|
||||
const onEdgeUpdateEnd = (_: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) =>
|
||||
@@ -111,7 +109,6 @@ const UpdatableEdge = () => {
|
||||
const onConnect = (connection: Connection) => setEdges((els) => addEdge(connection, els));
|
||||
|
||||
const onNodesChange = useCallback((changes: NodeChange[]) => {
|
||||
console.log(changes);
|
||||
setNodes((ns) => applyNodeChanges(changes, ns));
|
||||
}, []);
|
||||
|
||||
@@ -125,12 +122,12 @@ const UpdatableEdge = () => {
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onInit={onInit}
|
||||
snapToGrid={true}
|
||||
onEdgeUpdate={onEdgeUpdate}
|
||||
onConnect={onConnect}
|
||||
onEdgeUpdateStart={onEdgeUpdateStart}
|
||||
onEdgeUpdateEnd={onEdgeUpdateEnd}
|
||||
fitView
|
||||
>
|
||||
<Controls />
|
||||
</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 {
|
||||
getMarkerId,
|
||||
elementSelectionKeys,
|
||||
XYHandle,
|
||||
type Connection,
|
||||
getEdgePosition,
|
||||
errorMessages,
|
||||
getElevatedEdgeZIndex,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { useStoreApi, useStore } from '../../hooks/useStore';
|
||||
import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions';
|
||||
import { EdgeAnchor } from '../Edges/EdgeAnchor';
|
||||
import { getMouseHandler } from '../Edges/utils';
|
||||
import type { EdgeWrapperProps } from '../../types';
|
||||
import { builtinEdgeTypes } from './utils';
|
||||
import { builtinEdgeTypes, nullPosition } from './utils';
|
||||
import EdgeUpdateAnchors from './EdgeUpdateAnchors';
|
||||
|
||||
function EdgeWrapper({
|
||||
id,
|
||||
className,
|
||||
type,
|
||||
data,
|
||||
edgesFocusable,
|
||||
edgesUpdatable,
|
||||
elementsSelectable,
|
||||
onClick,
|
||||
onEdgeDoubleClick,
|
||||
selected,
|
||||
animated,
|
||||
label,
|
||||
labelStyle,
|
||||
labelShowBg,
|
||||
labelBgStyle,
|
||||
labelBgPadding,
|
||||
labelBgBorderRadius,
|
||||
style,
|
||||
source,
|
||||
target,
|
||||
isSelectable,
|
||||
hidden,
|
||||
onDoubleClick,
|
||||
sourceHandleId,
|
||||
targetHandleId,
|
||||
onContextMenu,
|
||||
@@ -47,18 +32,17 @@ function EdgeWrapper({
|
||||
onEdgeUpdate,
|
||||
onEdgeUpdateStart,
|
||||
onEdgeUpdateEnd,
|
||||
markerEnd,
|
||||
markerStart,
|
||||
rfId,
|
||||
ariaLabel,
|
||||
isFocusable,
|
||||
isUpdatable,
|
||||
pathOptions,
|
||||
interactionWidth,
|
||||
edgeTypes,
|
||||
elevateEdgesOnSelect,
|
||||
noPanClassName,
|
||||
onError,
|
||||
}: 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];
|
||||
|
||||
if (EdgeComponent === undefined) {
|
||||
@@ -67,49 +51,74 @@ function EdgeWrapper({
|
||||
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 [updateHover, setUpdateHover] = useState<boolean>(false);
|
||||
const [updating, setUpdating] = useState<boolean>(false);
|
||||
const store = useStoreApi();
|
||||
const edgePosition = useStore(
|
||||
|
||||
const { zIndex, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition } = useStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
const sourceNode = state.nodeLookup.get(source);
|
||||
const targetNode = state.nodeLookup.get(target);
|
||||
(store) => {
|
||||
const sourceNode = store.nodeLookup.get(edge.source);
|
||||
const targetNode = store.nodeLookup.get(edge.target);
|
||||
|
||||
if (!sourceNode || !targetNode) {
|
||||
return null;
|
||||
return {
|
||||
zIndex: edge.zIndex,
|
||||
...nullPosition,
|
||||
};
|
||||
}
|
||||
|
||||
return getEdgePosition({
|
||||
const edgePosition = getEdgePosition({
|
||||
id,
|
||||
sourceNode,
|
||||
targetNode,
|
||||
sourceHandle: sourceHandleId || null,
|
||||
targetHandle: targetHandleId || null,
|
||||
connectionMode: state.connectionMode,
|
||||
onError: state.onError,
|
||||
connectionMode: store.connectionMode,
|
||||
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
|
||||
);
|
||||
|
||||
const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart, rfId)})`, [markerStart, rfId]);
|
||||
const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd, rfId)})`, [markerEnd, rfId]);
|
||||
const markerStartUrl = useMemo(
|
||||
() => (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;
|
||||
}
|
||||
|
||||
const onEdgeClick = (event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
|
||||
const { edges, addSelectedEdges, unselectNodesAndEdges, multiSelectionActive } = store.getState();
|
||||
const edge = edges.find((e) => e.id === id);
|
||||
|
||||
if (!edge) {
|
||||
return;
|
||||
}
|
||||
const { addSelectedEdges, unselectNodesAndEdges, multiSelectionActive } = store.getState();
|
||||
|
||||
if (isSelectable) {
|
||||
store.setState({ nodesSelectionActive: false });
|
||||
@@ -127,91 +136,40 @@ function EdgeWrapper({
|
||||
}
|
||||
};
|
||||
|
||||
const onEdgeDoubleClickHandler = getMouseHandler(id, store.getState, onEdgeDoubleClick);
|
||||
const onEdgeContextMenu = getMouseHandler(id, store.getState, onContextMenu);
|
||||
const onEdgeMouseEnter = getMouseHandler(id, store.getState, onMouseEnter);
|
||||
const onEdgeMouseMove = getMouseHandler(id, store.getState, onMouseMove);
|
||||
const onEdgeMouseLeave = getMouseHandler(id, store.getState, onMouseLeave);
|
||||
|
||||
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,
|
||||
edges,
|
||||
isValidConnection,
|
||||
connectionMode,
|
||||
connectionRadius,
|
||||
lib,
|
||||
onConnectStart,
|
||||
onConnectEnd,
|
||||
cancelConnection,
|
||||
nodes,
|
||||
panBy,
|
||||
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 onEdgeDoubleClick = onDoubleClick
|
||||
? (event: React.MouseEvent) => {
|
||||
onDoubleClick(event, { ...edge });
|
||||
}
|
||||
: undefined;
|
||||
const onEdgeContextMenu = onContextMenu
|
||||
? (event: React.MouseEvent) => {
|
||||
onContextMenu(event, { ...edge });
|
||||
}
|
||||
: undefined;
|
||||
const onEdgeMouseEnter = onMouseEnter
|
||||
? (event: React.MouseEvent) => {
|
||||
onMouseEnter(event, { ...edge });
|
||||
}
|
||||
: undefined;
|
||||
const onEdgeMouseMove = onMouseMove
|
||||
? (event: React.MouseEvent) => {
|
||||
onMouseMove(event, { ...edge });
|
||||
}
|
||||
: undefined;
|
||||
const onEdgeMouseLeave = onMouseLeave
|
||||
? (event: React.MouseEvent) => {
|
||||
onMouseLeave(event, { ...edge });
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (elementSelectionKeys.includes(event.key) && isSelectable) {
|
||||
const { unselectNodesAndEdges, addSelectedEdges, edges } = store.getState();
|
||||
const { unselectNodesAndEdges, addSelectedEdges } = store.getState();
|
||||
const unselect = event.key === 'Escape';
|
||||
|
||||
if (unselect) {
|
||||
edgeRef.current?.blur();
|
||||
unselectNodesAndEdges({ edges: [edges.find((e) => e.id === id)!] });
|
||||
unselectNodesAndEdges({ edges: [edge] });
|
||||
} else {
|
||||
addSelectedEdges([id]);
|
||||
}
|
||||
@@ -219,86 +177,88 @@ function EdgeWrapper({
|
||||
};
|
||||
|
||||
return (
|
||||
<g
|
||||
className={cc([
|
||||
'react-flow__edge',
|
||||
`react-flow__edge-${type}`,
|
||||
className,
|
||||
{ selected, animated, inactive, updating: updateHover },
|
||||
])}
|
||||
onClick={onEdgeClick}
|
||||
onDoubleClick={onEdgeDoubleClickHandler}
|
||||
onContextMenu={onEdgeContextMenu}
|
||||
onMouseEnter={onEdgeMouseEnter}
|
||||
onMouseMove={onEdgeMouseMove}
|
||||
onMouseLeave={onEdgeMouseLeave}
|
||||
onKeyDown={isFocusable ? onKeyDown : undefined}
|
||||
tabIndex={isFocusable ? 0 : undefined}
|
||||
role={isFocusable ? 'button' : 'img'}
|
||||
data-id={id}
|
||||
data-testid={`rf__edge-${id}`}
|
||||
aria-label={ariaLabel === null ? undefined : ariaLabel ? ariaLabel : `Edge from ${source} to ${target}`}
|
||||
aria-describedby={isFocusable ? `${ARIA_EDGE_DESC_KEY}-${rfId}` : undefined}
|
||||
ref={edgeRef}
|
||||
>
|
||||
{!updating && (
|
||||
<EdgeComponent
|
||||
id={id}
|
||||
source={source}
|
||||
target={target}
|
||||
selected={selected}
|
||||
animated={animated}
|
||||
label={label}
|
||||
labelStyle={labelStyle}
|
||||
labelShowBg={labelShowBg}
|
||||
labelBgStyle={labelBgStyle}
|
||||
labelBgPadding={labelBgPadding}
|
||||
labelBgBorderRadius={labelBgBorderRadius}
|
||||
data={data}
|
||||
style={style}
|
||||
sourceX={edgePosition.sourceX}
|
||||
sourceY={edgePosition.sourceY}
|
||||
targetX={edgePosition.targetX}
|
||||
targetY={edgePosition.targetY}
|
||||
sourcePosition={edgePosition.sourcePosition}
|
||||
targetPosition={edgePosition.targetPosition}
|
||||
sourceHandleId={sourceHandleId}
|
||||
targetHandleId={targetHandleId}
|
||||
markerStart={markerStartUrl}
|
||||
markerEnd={markerEndUrl}
|
||||
pathOptions={pathOptions}
|
||||
interactionWidth={interactionWidth}
|
||||
/>
|
||||
)}
|
||||
{isUpdatable && (
|
||||
<>
|
||||
{(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"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</g>
|
||||
<svg style={{ zIndex }}>
|
||||
<g
|
||||
className={cc([
|
||||
'react-flow__edge',
|
||||
`react-flow__edge-${edgeType}`,
|
||||
edge.className,
|
||||
noPanClassName,
|
||||
{
|
||||
selected: edge.selected,
|
||||
animated: edge.animated,
|
||||
inactive: !isSelectable && !onClick,
|
||||
updating: updateHover,
|
||||
},
|
||||
])}
|
||||
onClick={onEdgeClick}
|
||||
onDoubleClick={onEdgeDoubleClick}
|
||||
onContextMenu={onEdgeContextMenu}
|
||||
onMouseEnter={onEdgeMouseEnter}
|
||||
onMouseMove={onEdgeMouseMove}
|
||||
onMouseLeave={onEdgeMouseLeave}
|
||||
onKeyDown={isFocusable ? onKeyDown : undefined}
|
||||
tabIndex={isFocusable ? 0 : undefined}
|
||||
role={isFocusable ? 'button' : 'img'}
|
||||
data-id={id}
|
||||
data-testid={`rf__edge-${id}`}
|
||||
aria-label={
|
||||
edge.ariaLabel === null ? undefined : edge.ariaLabel || `Edge from ${edge.source} to ${edge.target}`
|
||||
}
|
||||
aria-describedby={isFocusable ? `${ARIA_EDGE_DESC_KEY}-${rfId}` : undefined}
|
||||
ref={edgeRef}
|
||||
>
|
||||
{!updating && (
|
||||
<EdgeComponent
|
||||
id={id}
|
||||
source={edge.source}
|
||||
target={edge.target}
|
||||
selected={edge.selected}
|
||||
animated={edge.animated}
|
||||
label={edge.label}
|
||||
labelStyle={edge.labelStyle}
|
||||
labelShowBg={edge.labelShowBg}
|
||||
labelBgStyle={edge.labelBgStyle}
|
||||
labelBgPadding={edge.labelBgPadding}
|
||||
labelBgBorderRadius={edge.labelBgBorderRadius}
|
||||
sourceX={sourceX}
|
||||
sourceY={sourceY}
|
||||
targetX={targetX}
|
||||
targetY={targetY}
|
||||
sourcePosition={sourcePosition}
|
||||
targetPosition={targetPosition}
|
||||
data={edge.data}
|
||||
style={edge.style}
|
||||
sourceHandleId={sourceHandleId}
|
||||
targetHandleId={targetHandleId}
|
||||
markerStart={markerStartUrl}
|
||||
markerEnd={markerEndUrl}
|
||||
pathOptions={'pathOptions' in edge ? edge.pathOptions : undefined}
|
||||
interactionWidth={edge.interactionWidth}
|
||||
/>
|
||||
)}
|
||||
{isUpdatable && (
|
||||
<EdgeUpdateAnchors
|
||||
edge={edge}
|
||||
isUpdatable={isUpdatable}
|
||||
edgeUpdaterRadius={edgeUpdaterRadius}
|
||||
onEdgeUpdate={onEdgeUpdate}
|
||||
onEdgeUpdateStart={onEdgeUpdateStart}
|
||||
onEdgeUpdateEnd={onEdgeUpdateEnd}
|
||||
sourceX={sourceX}
|
||||
sourceY={sourceY}
|
||||
targetX={targetX}
|
||||
targetY={targetY}
|
||||
sourcePosition={sourcePosition}
|
||||
targetPosition={targetPosition}
|
||||
setUpdateHover={setUpdateHover}
|
||||
setUpdating={setUpdating}
|
||||
sourceHandleId={sourceHandleId}
|
||||
targetHandleId={targetHandleId}
|
||||
/>
|
||||
)}
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,3 +15,12 @@ export const builtinEdgeTypes: EdgeTypes = {
|
||||
smoothstep: SmoothStepEdgeInternal 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 type { NodeWrapperProps } from '../../types';
|
||||
import { arrowKeyDiffs, builtinNodeTypes } from './utils';
|
||||
|
||||
import { shallow } from 'zustand/shallow';
|
||||
const NodeWrapper = ({
|
||||
id,
|
||||
onClick,
|
||||
@@ -40,7 +40,23 @@ const NodeWrapper = ({
|
||||
nodeOrigin,
|
||||
onError,
|
||||
}: 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 NodeComponent = nodeTypes?.[nodeType] || builtinNodeTypes[nodeType];
|
||||
@@ -110,31 +126,22 @@ const NodeWrapper = ({
|
||||
const height = node.height ?? undefined;
|
||||
const computedWidth = node.computed?.width;
|
||||
const computedHeight = node.computed?.height;
|
||||
const positionAbsolute = nodeExtent
|
||||
? clampPosition(node.computed?.positionAbsolute, nodeExtent)
|
||||
: node.computed?.positionAbsolute || { x: 0, y: 0 };
|
||||
|
||||
const positionAbsoluteOrigin = getPositionWithOrigin({
|
||||
x: positionAbsolute.x,
|
||||
y: positionAbsolute.y,
|
||||
x: positionAbsoluteX,
|
||||
y: positionAbsoluteY,
|
||||
width: computedWidth ?? width ?? 0,
|
||||
height: computedHeight ?? height ?? 0,
|
||||
origin: node.origin || nodeOrigin,
|
||||
});
|
||||
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 onMouseEnterHandler =
|
||||
onMouseEnter === undefined ? undefined : (event: MouseEvent) => onMouseEnter(event, { ...node });
|
||||
const onMouseMoveHandler =
|
||||
onMouseMove === undefined ? undefined : (event: MouseEvent) => onMouseMove(event, { ...node });
|
||||
const onMouseLeaveHandler =
|
||||
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 onMouseEnterHandler = onMouseEnter ? (event: MouseEvent) => onMouseEnter(event, { ...node }) : undefined;
|
||||
const onMouseMoveHandler = onMouseMove ? (event: MouseEvent) => onMouseMove(event, { ...node }) : undefined;
|
||||
const onMouseLeaveHandler = onMouseLeave ? (event: MouseEvent) => onMouseLeave(event, { ...node }) : undefined;
|
||||
const onContextMenuHandler = onContextMenu ? (event: MouseEvent) => onContextMenu(event, { ...node }) : undefined;
|
||||
const onDoubleClickHandler = onDoubleClick ? (event: MouseEvent) => onDoubleClick(event, { ...node }) : undefined;
|
||||
|
||||
const onSelectNodeHandler = (event: MouseEvent) => {
|
||||
const { selectNodesOnDrag, nodeDragThreshold } = store.getState();
|
||||
@@ -177,7 +184,7 @@ const NodeWrapper = ({
|
||||
store.setState({
|
||||
ariaLiveMessage: `Moved selected node ${event.key
|
||||
.replace('Arrow', '')
|
||||
.toLowerCase()}. New position, x: ${~~positionAbsolute.x}, y: ${~~positionAbsolute.y}`,
|
||||
.toLowerCase()}. New position, x: ${~~positionAbsoluteX}, y: ${~~positionAbsoluteY}`,
|
||||
});
|
||||
|
||||
updatePositions({
|
||||
@@ -236,8 +243,8 @@ const NodeWrapper = ({
|
||||
type={nodeType}
|
||||
width={computedWidth}
|
||||
height={computedHeight}
|
||||
positionAbsoluteX={positionAbsolute.x}
|
||||
positionAbsoluteY={positionAbsolute.y}
|
||||
positionAbsoluteX={positionAbsoluteX}
|
||||
positionAbsoluteY={positionAbsoluteY}
|
||||
selected={node.selected}
|
||||
isConnectable={isConnectable}
|
||||
sourcePosition={node.sourcePosition}
|
||||
|
||||
@@ -51,32 +51,38 @@ const markerSelector =
|
||||
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 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
|
||||
const MarkerDefinitions = ({ defaultColor, rfId }: MarkerDefinitionsProps) => {
|
||||
const markers = useStore(
|
||||
useCallback(markerSelector({ defaultColor, rfId }), [defaultColor, rfId]),
|
||||
// the id includes all marker options, so we just need to look at that part of the marker
|
||||
(a, b) => !(a.length !== b.length || a.some((m, i) => m.id !== b[i].id))
|
||||
);
|
||||
const markers = useStore(useCallback(markerSelector({ defaultColor, rfId }), [defaultColor, rfId]), markersEqual);
|
||||
|
||||
if (!markers.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<defs>
|
||||
{markers.map((marker: MarkerProps) => (
|
||||
<Marker
|
||||
id={marker.id}
|
||||
key={marker.id}
|
||||
type={marker.type}
|
||||
color={marker.color}
|
||||
width={marker.width}
|
||||
height={marker.height}
|
||||
markerUnits={marker.markerUnits}
|
||||
strokeWidth={marker.strokeWidth}
|
||||
orient={marker.orient}
|
||||
/>
|
||||
))}
|
||||
</defs>
|
||||
<svg className="react-flow__marker">
|
||||
<defs>
|
||||
{markers.map((marker: MarkerProps) => (
|
||||
<Marker
|
||||
id={marker.id}
|
||||
key={marker.id}
|
||||
type={marker.type}
|
||||
color={marker.color}
|
||||
width={marker.width}
|
||||
height={marker.height}
|
||||
markerUnits={marker.markerUnits}
|
||||
strokeWidth={marker.strokeWidth}
|
||||
orient={marker.orient}
|
||||
/>
|
||||
))}
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { memo, ReactNode } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import cc from 'classcat';
|
||||
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import useVisibleEdges from '../../hooks/useVisibleEdges';
|
||||
import useVisibleEdgeIds from '../../hooks/useVisibleEdgeIds';
|
||||
import MarkerDefinitions from './MarkerDefinitions';
|
||||
import { GraphViewProps } from '../GraphView';
|
||||
import type { ReactFlowState } from '../../types';
|
||||
import EdgeWrapper from '../../components/EdgeWrapper';
|
||||
import type { ReactFlowState } from '../../types';
|
||||
|
||||
type EdgeRendererProps = Pick<
|
||||
GraphViewProps,
|
||||
@@ -29,7 +28,6 @@ type EdgeRendererProps = Pick<
|
||||
| 'disableKeyboardA11y'
|
||||
| 'edgeTypes'
|
||||
> & {
|
||||
elevateEdgesOnSelect: boolean;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
@@ -63,76 +61,40 @@ const EdgeRenderer = ({
|
||||
children,
|
||||
}: EdgeRendererProps) => {
|
||||
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
|
||||
// each zIndex gets its own svg element
|
||||
const edgeTree = useVisibleEdges(onlyRenderVisibleElements, elevateEdgesOnSelect);
|
||||
const edgeIds = useVisibleEdgeIds(onlyRenderVisibleElements);
|
||||
|
||||
return (
|
||||
<>
|
||||
{edgeTree.map(({ level, edges, isMaxLevel }) => (
|
||||
<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')
|
||||
);
|
||||
<div className="react-flow__edges">
|
||||
<MarkerDefinitions defaultColor={defaultMarkerColor} rfId={rfId} />
|
||||
|
||||
return (
|
||||
<EdgeWrapper
|
||||
key={edge.id}
|
||||
id={edge.id}
|
||||
className={cc([edge.className, noPanClassName])}
|
||||
type={edge.type}
|
||||
data={edge.data}
|
||||
selected={!!edge.selected}
|
||||
animated={!!edge.animated}
|
||||
hidden={!!edge.hidden}
|
||||
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}
|
||||
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>
|
||||
))}
|
||||
{edgeIds.map((id) => {
|
||||
return (
|
||||
<EdgeWrapper
|
||||
key={id}
|
||||
id={id}
|
||||
edgesFocusable={edgesFocusable}
|
||||
edgesUpdatable={edgesUpdatable}
|
||||
elementsSelectable={elementsSelectable}
|
||||
noPanClassName={noPanClassName}
|
||||
onEdgeUpdate={onEdgeUpdate}
|
||||
onContextMenu={onEdgeContextMenu}
|
||||
onMouseEnter={onEdgeMouseEnter}
|
||||
onMouseMove={onEdgeMouseMove}
|
||||
onMouseLeave={onEdgeMouseLeave}
|
||||
onClick={onEdgeClick}
|
||||
edgeUpdaterRadius={edgeUpdaterRadius}
|
||||
onDoubleClick={onEdgeDoubleClick}
|
||||
onEdgeUpdateStart={onEdgeUpdateStart}
|
||||
onEdgeUpdateEnd={onEdgeUpdateEnd}
|
||||
rfId={rfId}
|
||||
onError={onError}
|
||||
edgeTypes={edgeTypes}
|
||||
elevateEdgesOnSelect={elevateEdgesOnSelect}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{children}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { memo } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import useVisibleNodesIds from '../../hooks/useVisibleNodes';
|
||||
import useVisibleNodesIds from '../../hooks/useVisibleNodeIds';
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import { containerStyle } from '../../styles/utils';
|
||||
import { GraphViewProps } from '../GraphView';
|
||||
|
||||
@@ -10,7 +10,7 @@ import { getNodesInside, getEventPosition, SelectionMode } from '@xyflow/system'
|
||||
import UserSelection from '../../components/UserSelection';
|
||||
import { containerStyle } from '../../styles/utils';
|
||||
import { useStore, useStoreApi } from '../../hooks/useStore';
|
||||
import { getSelectionChanges, getConnectedEdges } from '../../utils';
|
||||
import { getSelectionChanges } from '../../utils';
|
||||
import type { ReactFlowProps, ReactFlowState, NodeChange, EdgeChange } from '../../types';
|
||||
|
||||
type PaneProps = {
|
||||
@@ -156,19 +156,30 @@ const Pane = memo(
|
||||
true,
|
||||
nodeOrigin
|
||||
);
|
||||
const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id);
|
||||
const selectedNodeIds = selectedNodes.map((n) => n.id);
|
||||
|
||||
if (prevSelectedNodesCount.current !== selectedNodeIds.length) {
|
||||
prevSelectedNodesCount.current = selectedNodeIds.length;
|
||||
const changes = getSelectionChanges(nodes, selectedNodeIds) as NodeChange[];
|
||||
const selectedEdgeIds = new Set<string>();
|
||||
const selectedNodeIds = new Set<string>();
|
||||
|
||||
for (const selectedNode of selectedNodes) {
|
||||
selectedNodeIds.add(selectedNode.id);
|
||||
|
||||
for (const edge of edges) {
|
||||
if (edge.source === selectedNode.id || edge.target === selectedNode.id) {
|
||||
selectedEdgeIds.add(edge.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (prevSelectedNodesCount.current !== selectedNodeIds.size) {
|
||||
prevSelectedNodesCount.current = selectedNodeIds.size;
|
||||
const changes = getSelectionChanges(nodes, selectedNodeIds, true) as NodeChange[];
|
||||
if (changes.length) {
|
||||
onNodesChange?.(changes);
|
||||
}
|
||||
}
|
||||
|
||||
if (prevSelectedEdgesCount.current !== selectedEdgeIds.length) {
|
||||
prevSelectedEdgesCount.current = selectedEdgeIds.length;
|
||||
if (prevSelectedEdgesCount.current !== selectedEdgeIds.size) {
|
||||
prevSelectedEdgesCount.current = selectedEdgeIds.size;
|
||||
const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[];
|
||||
if (changes.length) {
|
||||
onEdgesChange?.(changes);
|
||||
|
||||
@@ -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
-1
@@ -1,7 +1,7 @@
|
||||
import { getNodesInside } from '@xyflow/system';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { useStore } from '../hooks/useStore';
|
||||
import { useStore } from './useStore';
|
||||
import type { Node, ReactFlowState } from '../types';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
@@ -77,6 +77,7 @@ export {
|
||||
type CoordinateExtent,
|
||||
type ColorMode,
|
||||
type ColorModeClass,
|
||||
type HandleType,
|
||||
} from '@xyflow/system';
|
||||
|
||||
// system utils
|
||||
|
||||
@@ -55,12 +55,11 @@ const createRFStore = ({
|
||||
set({ nodes: nodesWithInternalData });
|
||||
},
|
||||
setEdges: (edges: Edge[]) => {
|
||||
const { defaultEdgeOptions = {}, connectionLookup } = get();
|
||||
const nextEdges = edges.map((e) => ({ ...defaultEdgeOptions, ...e }));
|
||||
const { connectionLookup, edgeLookup } = get();
|
||||
|
||||
updateConnectionLookup(connectionLookup, nextEdges);
|
||||
updateConnectionLookup(connectionLookup, edgeLookup, edges);
|
||||
|
||||
set({ edges: nextEdges });
|
||||
set({ edges });
|
||||
},
|
||||
// when the user works with an uncontrolled flow,
|
||||
// we set a flag `hasDefaultNodes` / `hasDefaultEdges`
|
||||
@@ -79,13 +78,16 @@ const createRFStore = ({
|
||||
};
|
||||
|
||||
if (hasDefaultNodes) {
|
||||
const { nodeLookup } = get();
|
||||
const { nodeLookup, nodeOrigin, elevateNodesOnSelect } = get();
|
||||
nextState.nodes = adoptUserProvidedNodes(nodes, nodeLookup, {
|
||||
nodeOrigin: get().nodeOrigin,
|
||||
elevateNodesOnSelect: get().elevateNodesOnSelect,
|
||||
nodeOrigin,
|
||||
elevateNodesOnSelect,
|
||||
});
|
||||
}
|
||||
if (hasDefaultEdges) {
|
||||
const { connectionLookup, edgeLookup } = get();
|
||||
updateConnectionLookup(connectionLookup, edgeLookup, edges);
|
||||
|
||||
nextState.edges = edges;
|
||||
}
|
||||
|
||||
@@ -193,8 +195,8 @@ const createRFStore = ({
|
||||
if (multiSelectionActive) {
|
||||
changedNodes = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true)) as NodeSelectionChange[];
|
||||
} else {
|
||||
changedNodes = getSelectionChanges(nodes, selectedNodeIds);
|
||||
changedEdges = getSelectionChanges(edges, []);
|
||||
changedNodes = getSelectionChanges(nodes, new Set([...selectedNodeIds]), true);
|
||||
changedEdges = getSelectionChanges(edges);
|
||||
}
|
||||
|
||||
updateNodesAndEdgesSelections({
|
||||
@@ -212,8 +214,8 @@ const createRFStore = ({
|
||||
if (multiSelectionActive) {
|
||||
changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true)) as EdgeSelectionChange[];
|
||||
} else {
|
||||
changedEdges = getSelectionChanges(edges, selectedEdgeIds);
|
||||
changedNodes = getSelectionChanges(nodes, []);
|
||||
changedEdges = getSelectionChanges(edges, new Set([...selectedEdgeIds]));
|
||||
changedNodes = getSelectionChanges(nodes, new Set(), true);
|
||||
}
|
||||
|
||||
updateNodesAndEdgesSelections({
|
||||
|
||||
@@ -24,7 +24,10 @@ const getInitialState = ({
|
||||
fitView?: boolean;
|
||||
} = {}): ReactFlowStore => {
|
||||
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, {
|
||||
nodeOrigin: [0, 0],
|
||||
elevateNodesOnSelect: false,
|
||||
@@ -46,7 +49,8 @@ const getInitialState = ({
|
||||
transform,
|
||||
nodes: nextNodes,
|
||||
nodeLookup,
|
||||
edges: edges,
|
||||
edges,
|
||||
edgeLookup,
|
||||
connectionLookup,
|
||||
onNodesChange: null,
|
||||
onEdgesChange: null,
|
||||
|
||||
@@ -34,9 +34,8 @@ export type EdgeUpdatable = boolean | HandleType;
|
||||
export type DefaultEdge<EdgeData = any> = EdgeBase<EdgeData> & {
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
sourceNode?: Node;
|
||||
targetNode?: Node;
|
||||
updatable?: EdgeUpdatable;
|
||||
focusable?: boolean;
|
||||
} & EdgeLabelOptions;
|
||||
|
||||
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 EdgeWrapperProps<T = any> = Omit<Edge<T>, 'sourceHandle' | 'targetHandle'> & {
|
||||
export type EdgeWrapperProps = {
|
||||
id: string;
|
||||
edgesFocusable: boolean;
|
||||
edgesUpdatable: boolean;
|
||||
elementsSelectable: boolean;
|
||||
noPanClassName: string;
|
||||
onClick?: EdgeMouseHandler;
|
||||
onEdgeDoubleClick?: EdgeMouseHandler;
|
||||
onDoubleClick?: EdgeMouseHandler;
|
||||
sourceHandleId?: string | null;
|
||||
targetHandleId?: string | null;
|
||||
onEdgeUpdate?: OnEdgeUpdateFunc;
|
||||
@@ -72,12 +76,9 @@ export type EdgeWrapperProps<T = any> = Omit<Edge<T>, 'sourceHandle' | 'targetHa
|
||||
onEdgeUpdateStart?: (event: ReactMouseEvent, edge: Edge, handleType: HandleType) => void;
|
||||
onEdgeUpdateEnd?: (event: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) => void;
|
||||
rfId?: string;
|
||||
isFocusable: boolean;
|
||||
isUpdatable: EdgeUpdatable;
|
||||
isSelectable: boolean;
|
||||
pathOptions?: BezierPathOptions | SmoothStepPathOptions;
|
||||
edgeTypes?: EdgeTypes;
|
||||
onError?: OnError;
|
||||
elevateEdgesOnSelect?: boolean;
|
||||
};
|
||||
|
||||
export type DefaultEdgeOptions = DefaultEdgeOptionsBase<Edge>;
|
||||
@@ -93,13 +94,14 @@ export type EdgeProps<T = any> = Pick<
|
||||
Edge<T>,
|
||||
'id' | 'animated' | 'data' | 'style' | 'selected' | 'source' | 'target'
|
||||
> &
|
||||
Pick<EdgeWrapperProps, 'sourceHandleId' | 'targetHandleId' | 'interactionWidth'> &
|
||||
Pick<EdgeWrapperProps, 'sourceHandleId' | 'targetHandleId'> &
|
||||
EdgePosition &
|
||||
EdgeLabelOptions & {
|
||||
markerStart?: string;
|
||||
markerEnd?: string;
|
||||
// @TODO: how can we get better types for pathOptions?
|
||||
pathOptions?: any;
|
||||
interactionWidth?: number;
|
||||
};
|
||||
|
||||
export type BaseEdgeProps = Pick<EdgeProps, 'style' | 'markerStart' | 'markerEnd' | 'interactionWidth'> &
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
|
||||
import type { CoordinateExtent, NodeBase, NodeOrigin, OnError } from '@xyflow/system';
|
||||
import { NodeTypes } from './general';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type Node<NodeData = any, NodeType extends string | undefined = string | undefined> = NodeBase<
|
||||
NodeData,
|
||||
NodeType
|
||||
|
||||
@@ -24,7 +24,9 @@ import {
|
||||
type OnMoveEnd,
|
||||
type IsValidConnection,
|
||||
type UpdateConnection,
|
||||
Connection,
|
||||
type EdgeLookup,
|
||||
type ConnectionLookup,
|
||||
type NodeLookup,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type {
|
||||
@@ -48,10 +50,10 @@ export type ReactFlowStore = {
|
||||
height: number;
|
||||
transform: Transform;
|
||||
nodes: Node[];
|
||||
nodeLookup: Map<string, Node>;
|
||||
nodeLookup: NodeLookup<Node>;
|
||||
edges: Edge[];
|
||||
connectionLookup: Map<string, Map<string, Connection>>;
|
||||
|
||||
edgeLookup: EdgeLookup<Edge>;
|
||||
connectionLookup: ConnectionLookup;
|
||||
onNodesChange: OnNodesChange | null;
|
||||
onEdgesChange: OnEdgesChange | null;
|
||||
hasDefaultNodes: boolean;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type { Node, Edge, EdgeChange, NodeChange } from '../types';
|
||||
import type { Node, Edge, EdgeChange, NodeChange, NodeSelectionChange, EdgeSelectionChange } from '../types';
|
||||
|
||||
export function handleParentExpand(res: any[], updateItem: any) {
|
||||
const parent = res.find((e) => e.id === updateItem.parentNode);
|
||||
@@ -57,12 +57,11 @@ function applyChanges(changes: any[], elements: any[]): any[] {
|
||||
let remainingChanges = changes;
|
||||
const updatedElements: any[] = [];
|
||||
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
for (const item of elements) {
|
||||
const nextChanges: any[] = [];
|
||||
const _remainingChanges: any[] = [];
|
||||
const item = elements[i];
|
||||
|
||||
remainingChanges.forEach((c) => {
|
||||
for (const c of remainingChanges) {
|
||||
if (c.type === 'add') {
|
||||
updatedElements.push(c.item);
|
||||
} else if (c.id === item.id) {
|
||||
@@ -70,7 +69,8 @@ function applyChanges(changes: any[], elements: any[]): any[] {
|
||||
} else {
|
||||
_remainingChanges.push(c);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
remainingChanges = _remainingChanges;
|
||||
|
||||
if (nextChanges.length === 0) {
|
||||
@@ -150,24 +150,33 @@ export function applyEdgeChanges<EdgeData = any>(changes: EdgeChange[], edges: E
|
||||
return applyChanges(changes, edges) as Edge<EdgeData>[];
|
||||
}
|
||||
|
||||
export const createSelectionChange = (id: string, selected: boolean) => ({
|
||||
export const createSelectionChange = (id: string, selected: boolean): NodeSelectionChange | EdgeSelectionChange => ({
|
||||
id,
|
||||
type: 'select',
|
||||
selected,
|
||||
});
|
||||
|
||||
export function getSelectionChanges(items: any[], selectedIds: string[]) {
|
||||
return items.reduce((res, item) => {
|
||||
const willBeSelected = selectedIds.includes(item.id);
|
||||
export function getSelectionChanges(
|
||||
items: any[],
|
||||
selectedIds: Set<string> = new Set(),
|
||||
mutateItem = false
|
||||
): NodeSelectionChange[] | EdgeSelectionChange[] {
|
||||
const changes: NodeSelectionChange[] | EdgeSelectionChange[] = [];
|
||||
|
||||
if (!item.selected && willBeSelected) {
|
||||
item.selected = true;
|
||||
res.push(createSelectionChange(item.id, true));
|
||||
} else if (item.selected && !willBeSelected) {
|
||||
item.selected = false;
|
||||
res.push(createSelectionChange(item.id, false));
|
||||
for (const item of items) {
|
||||
const willBeSelected = selectedIds.has(item.id);
|
||||
|
||||
// we don't want to set all items to selected=false on the first selection
|
||||
if (!(item.selected === undefined && !willBeSelected) && item.selected !== willBeSelected) {
|
||||
if (mutateItem) {
|
||||
// this hack is needed for nodes. When the user dragged a node, it's selected.
|
||||
// When another node gets dragged, we need to deselect the previous one,
|
||||
// in order to have only one selected node at a time - the onNodesChange callback comes too late here :/
|
||||
item.selected = willBeSelected;
|
||||
}
|
||||
changes.push(createSelectionChange(item.id, willBeSelected));
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}, []);
|
||||
return changes;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
export let target: $$Props['target'] = '';
|
||||
export let data: $$Props['data'] = {};
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let zIndex: $$Props['zIndex'] = undefined;
|
||||
|
||||
export let animated: $$Props['animated'] = false;
|
||||
export let selected: $$Props['selected'] = false;
|
||||
@@ -95,42 +96,44 @@
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
{#if !hidden}
|
||||
<g
|
||||
class={cc(['svelte-flow__edge', className])}
|
||||
class:animated
|
||||
class:selected
|
||||
data-id={id}
|
||||
on:click={onClick}
|
||||
on:contextmenu={onContextMenu}
|
||||
aria-label={ariaLabel === null
|
||||
? undefined
|
||||
: ariaLabel
|
||||
? ariaLabel
|
||||
: `Edge from ${source} to ${target}`}
|
||||
role="img"
|
||||
>
|
||||
<svelte:component
|
||||
this={edgeComponent}
|
||||
{id}
|
||||
{source}
|
||||
{target}
|
||||
{sourceX}
|
||||
{sourceY}
|
||||
{targetX}
|
||||
{targetY}
|
||||
{sourcePosition}
|
||||
{targetPosition}
|
||||
{animated}
|
||||
{selected}
|
||||
{label}
|
||||
{labelStyle}
|
||||
{data}
|
||||
{style}
|
||||
{interactionWidth}
|
||||
sourceHandleId={sourceHandle}
|
||||
targetHandleId={targetHandle}
|
||||
markerStart={markerStartUrl}
|
||||
markerEnd={markerEndUrl}
|
||||
/>
|
||||
</g>
|
||||
<svg style:zIndex>
|
||||
<g
|
||||
class={cc(['svelte-flow__edge', className])}
|
||||
class:animated
|
||||
class:selected
|
||||
data-id={id}
|
||||
on:click={onClick}
|
||||
on:contextmenu={onContextMenu}
|
||||
aria-label={ariaLabel === null
|
||||
? undefined
|
||||
: ariaLabel
|
||||
? ariaLabel
|
||||
: `Edge from ${source} to ${target}`}
|
||||
role="img"
|
||||
>
|
||||
<svelte:component
|
||||
this={edgeComponent}
|
||||
{id}
|
||||
{source}
|
||||
{target}
|
||||
{sourceX}
|
||||
{sourceY}
|
||||
{targetX}
|
||||
{targetY}
|
||||
{sourcePosition}
|
||||
{targetPosition}
|
||||
{animated}
|
||||
{selected}
|
||||
{label}
|
||||
{labelStyle}
|
||||
{data}
|
||||
{style}
|
||||
{interactionWidth}
|
||||
sourceHandleId={sourceHandle}
|
||||
targetHandleId={targetHandle}
|
||||
markerStart={markerStartUrl}
|
||||
markerEnd={markerEndUrl}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
{/if}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
const {
|
||||
elementsSelectable,
|
||||
edgeTree,
|
||||
visibleEdges,
|
||||
edges: { setDefaultOptions }
|
||||
} = useStore();
|
||||
|
||||
@@ -18,57 +18,47 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
{#each $edgeTree as group (group.level)}
|
||||
<svg style="z-index: {group.level}" class="svelte-flow__edges">
|
||||
{#if group.isMaxLevel} <MarkerDefinition />{/if}
|
||||
<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>
|
||||
<div class="svelte-flow__edges">
|
||||
<svg class="svelte-flow__marker">
|
||||
<MarkerDefinition />
|
||||
</svg>
|
||||
{/each}
|
||||
|
||||
<style>
|
||||
.svelte-flow__edges {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
{#each $visibleEdges 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}
|
||||
zIndex={edge.zIndex}
|
||||
{selectable}
|
||||
on:edgeclick
|
||||
on:edgecontextmenu
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@ import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions, ConnectionData }
|
||||
import { initialEdgeTypes, initialNodeTypes, getInitialStore } from './initial-store';
|
||||
import type { SvelteFlowStore } from './types';
|
||||
import { syncNodeStores, syncEdgeStores, syncViewportStores } from './utils';
|
||||
import { getEdgeTree } from './edge-tree';
|
||||
import { getVisibleEdges } from './visible-edges';
|
||||
import { getVisibleNodes } from './visible-nodes';
|
||||
import { getDerivedConnectionProps } from './derived-connection-props';
|
||||
|
||||
@@ -357,8 +357,8 @@ export function createStore({
|
||||
...store,
|
||||
|
||||
// derived state
|
||||
edgeTree: getEdgeTree(store),
|
||||
connection: getDerivedConnectionProps(store, currentConnection),
|
||||
visibleEdges: getVisibleEdges(store),
|
||||
visibleNodes: getVisibleNodes(store),
|
||||
markers: derived(
|
||||
[store.edges, store.defaultMarkerColor, store.flowId],
|
||||
|
||||
@@ -4,25 +4,25 @@ import {
|
||||
SelectionMode,
|
||||
ConnectionMode,
|
||||
ConnectionLineType,
|
||||
devWarn,
|
||||
adoptUserProvidedNodes,
|
||||
getNodesBounds,
|
||||
getViewportForBounds,
|
||||
updateConnectionLookup,
|
||||
type SelectionRect,
|
||||
type SnapGrid,
|
||||
type MarkerProps,
|
||||
type PanZoomInstance,
|
||||
type CoordinateExtent,
|
||||
type IsValidConnection,
|
||||
type GroupedEdges,
|
||||
type NodeOrigin,
|
||||
type OnError,
|
||||
devWarn,
|
||||
type Viewport,
|
||||
adoptUserProvidedNodes,
|
||||
getNodesBounds,
|
||||
getViewportForBounds,
|
||||
updateConnectionLookup,
|
||||
type ConnectionLookup,
|
||||
type OnConnect,
|
||||
type OnConnectStart,
|
||||
type OnConnectEnd
|
||||
type OnConnectEnd,
|
||||
type NodeLookup
|
||||
} from '@xyflow/system';
|
||||
|
||||
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
|
||||
@@ -82,7 +82,9 @@ export const getInitialStore = ({
|
||||
nodeOrigin: [0, 0],
|
||||
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 };
|
||||
|
||||
@@ -95,10 +97,10 @@ export const getInitialStore = ({
|
||||
return {
|
||||
flowId: writable<string | null>(null),
|
||||
nodes: createNodesStore(nextNodes, nodeLookup),
|
||||
nodeLookup: readable<Map<string, Node>>(nodeLookup),
|
||||
nodeLookup: readable<NodeLookup>(nodeLookup),
|
||||
visibleNodes: readable<Node[]>([]),
|
||||
edges: createEdgesStore(edges, connectionLookup),
|
||||
edgeTree: readable<GroupedEdges<EdgeLayouted>[]>([]),
|
||||
edges: createEdgesStore(edges, connectionLookup, edgeLookup),
|
||||
visibleEdges: readable<EdgeLayouted[]>([]),
|
||||
connectionLookup: readable<ConnectionLookup>(connectionLookup),
|
||||
height: writable<number>(500),
|
||||
width: writable<number>(500),
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import {
|
||||
writable,
|
||||
get,
|
||||
type Unsubscriber,
|
||||
type Subscriber,
|
||||
type Updater,
|
||||
type Writable,
|
||||
get
|
||||
type Writable
|
||||
} from 'svelte/store';
|
||||
import {
|
||||
adoptUserProvidedNodes,
|
||||
updateConnectionLookup,
|
||||
type Viewport,
|
||||
type PanZoomInstance,
|
||||
type ConnectionLookup
|
||||
type ConnectionLookup,
|
||||
type EdgeLookup,
|
||||
type NodeLookup
|
||||
} from '@xyflow/system';
|
||||
|
||||
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.
|
||||
export const createNodesStore = (
|
||||
nodes: Node[],
|
||||
nodeLookup: Map<string, Node>
|
||||
nodeLookup: NodeLookup<Node>
|
||||
): {
|
||||
subscribe: (this: void, run: Subscriber<Node[]>) => Unsubscriber;
|
||||
update: (this: void, updater: Updater<Node[]>) => void;
|
||||
@@ -175,6 +177,7 @@ export const createNodesStore = (
|
||||
export const createEdgesStore = (
|
||||
edges: Edge[],
|
||||
connectionLookup: ConnectionLookup,
|
||||
edgeLookup: EdgeLookup<Edge>,
|
||||
defaultOptions?: DefaultEdgeOptions
|
||||
): Writable<Edge[]> & { setDefaultOptions: (opts: DefaultEdgeOptions) => void } => {
|
||||
const { subscribe, set, update } = writable<Edge[]>([]);
|
||||
@@ -184,7 +187,7 @@ export const createEdgesStore = (
|
||||
const _set: typeof set = (eds: Edge[]) => {
|
||||
const nextEdges = defaults ? eds.map((edge) => ({ ...defaults, ...edge })) : eds;
|
||||
|
||||
updateConnectionLookup(connectionLookup, nextEdges);
|
||||
updateConnectionLookup(connectionLookup, edgeLookup, nextEdges);
|
||||
|
||||
value = nextEdges;
|
||||
set(value);
|
||||
|
||||
+10
-5
@@ -1,10 +1,10 @@
|
||||
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 { SvelteFlowStoreState } from './types';
|
||||
|
||||
export function getEdgeTree(store: SvelteFlowStoreState) {
|
||||
export function getVisibleEdges(store: SvelteFlowStoreState) {
|
||||
const visibleEdges = derived(
|
||||
[
|
||||
store.edges,
|
||||
@@ -64,6 +64,13 @@ export function getEdgeTree(store: SvelteFlowStoreState) {
|
||||
if (edgePosition) {
|
||||
res.push({
|
||||
...edge,
|
||||
zIndex: getElevatedEdgeZIndex({
|
||||
selected: edge.selected,
|
||||
zIndex: edge.zIndex,
|
||||
sourceNode,
|
||||
targetNode,
|
||||
elevateOnSelect: false
|
||||
}),
|
||||
...edgePosition
|
||||
});
|
||||
}
|
||||
@@ -71,9 +78,7 @@ export function getEdgeTree(store: SvelteFlowStoreState) {
|
||||
return res;
|
||||
}, []);
|
||||
|
||||
const groupedEdges = groupEdgesByZLevel<EdgeLayouted>(layoutedEdges, nodeLookup, false);
|
||||
|
||||
return groupedEdges;
|
||||
return layoutedEdges;
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import type {
|
||||
|
||||
import type { Node } from '$lib/types';
|
||||
|
||||
export type DefaultEdge<EdgeData = any> = Omit<EdgeBase<EdgeData>, 'focusable'> & {
|
||||
export type DefaultEdge<EdgeData = any> = EdgeBase<EdgeData> & {
|
||||
label?: string;
|
||||
labelStyle?: string;
|
||||
style?: string;
|
||||
@@ -75,7 +75,7 @@ export type StraightEdgeProps<T = any> = Omit<
|
||||
|
||||
export type EdgeTypes = Record<string, ComponentType<SvelteComponent<EdgeProps>>>;
|
||||
|
||||
export type DefaultEdgeOptions = Omit<DefaultEdgeOptionsBase<Edge>, 'focusable'>;
|
||||
export type DefaultEdgeOptions = DefaultEdgeOptionsBase<Edge>;
|
||||
|
||||
export type EdgeLayouted = Pick<
|
||||
Edge,
|
||||
@@ -98,6 +98,7 @@ export type EdgeLayouted = Pick<
|
||||
| 'ariaLabel'
|
||||
| 'hidden'
|
||||
| 'class'
|
||||
| 'zIndex'
|
||||
> &
|
||||
EdgePosition & {
|
||||
sourceNode?: Node;
|
||||
|
||||
@@ -92,11 +92,6 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.xy-flow .xy-flow__edges {
|
||||
pointer-events: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.xy-flow__edge-path {
|
||||
stroke: var(--xy-edge-stroke, var(--xy-edge-stroke-default));
|
||||
stroke-width: var(--xy-edge-stroke-width, var(--xy-edge-stroke-width-default));
|
||||
|
||||
@@ -43,6 +43,12 @@
|
||||
--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 {
|
||||
&.updating {
|
||||
.xy-flow__edge-path {
|
||||
|
||||
@@ -19,7 +19,6 @@ export type EdgeBase<EdgeData = any> = {
|
||||
zIndex?: number;
|
||||
ariaLabel?: string;
|
||||
interactionWidth?: number;
|
||||
focusable?: boolean;
|
||||
};
|
||||
|
||||
export type SmoothStepPathOptions = {
|
||||
@@ -37,7 +36,7 @@ export type BezierPathOptions = {
|
||||
|
||||
export type DefaultEdgeOptionsBase<EdgeType extends EdgeBase> = Omit<
|
||||
EdgeType,
|
||||
'id' | 'source' | 'target' | 'sourceHandle' | 'targetHandle' | 'sourceNode' | 'targetNode'
|
||||
'id' | 'source' | 'target' | 'sourceHandle' | 'targetHandle' | 'selected'
|
||||
>;
|
||||
|
||||
export enum ConnectionLineType {
|
||||
@@ -77,3 +76,5 @@ export type EdgePosition = {
|
||||
sourcePosition: Position;
|
||||
targetPosition: Position;
|
||||
};
|
||||
|
||||
export type EdgeLookup<EdgeType extends EdgeBase = EdgeBase> = Map<string, EdgeType>;
|
||||
|
||||
@@ -107,3 +107,5 @@ export type OnSelectionDrag = (event: MouseEvent, nodes: NodeBase[]) => void;
|
||||
export type NodeHandle = Optional<HandleElement, 'width' | 'height'>;
|
||||
|
||||
export type Align = 'center' | 'start' | 'end';
|
||||
|
||||
export type NodeLookup<NodeType extends NodeBase = NodeBase> = Map<string, NodeType>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Connection, Transform, errorMessages, internalsSymbol, isEdgeBase } from '../..';
|
||||
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)
|
||||
export function getEdgeCenter({
|
||||
@@ -23,63 +23,29 @@ export function getEdgeCenter({
|
||||
return [centerX, centerY, xOffset, yOffset];
|
||||
}
|
||||
|
||||
const defaultEdgeTree = [{ level: 0, isMaxLevel: true, edges: [] }];
|
||||
|
||||
export type GroupedEdges<EdgeType extends EdgeBase> = {
|
||||
edges: EdgeType[];
|
||||
level: number;
|
||||
isMaxLevel: boolean;
|
||||
export type GetEdgeZIndexParams = {
|
||||
sourceNode: NodeBase;
|
||||
targetNode: NodeBase;
|
||||
selected?: boolean;
|
||||
zIndex?: number;
|
||||
elevateOnSelect?: boolean;
|
||||
};
|
||||
|
||||
export function groupEdgesByZLevel<EdgeType extends EdgeBase>(
|
||||
edges: EdgeType[],
|
||||
nodeLookup: Map<string, NodeBase>,
|
||||
elevateEdgesOnSelect = false
|
||||
): GroupedEdges<EdgeType>[] {
|
||||
let maxLevel = -1;
|
||||
|
||||
const levelLookup = edges.reduce<Record<string, EdgeType[]>>((tree, edge) => {
|
||||
const hasZIndex = isNumeric(edge.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;
|
||||
export function getElevatedEdgeZIndex({
|
||||
sourceNode,
|
||||
targetNode,
|
||||
selected = false,
|
||||
zIndex = 0,
|
||||
elevateOnSelect = false,
|
||||
}: GetEdgeZIndexParams): number {
|
||||
if (!elevateOnSelect) {
|
||||
return zIndex;
|
||||
}
|
||||
|
||||
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 = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { EdgePosition } from '../../types/edges';
|
||||
import { ConnectionMode, OnError } from '../../types/general';
|
||||
import { NodeBase, NodeHandle, NodeHandleBounds } from '../../types/nodes';
|
||||
import { Position, Rect, XYPosition } from '../../types/utils';
|
||||
import { NodeBase, NodeHandle } from '../../types/nodes';
|
||||
import { Position } from '../../types/utils';
|
||||
import { errorMessages, internalsSymbol } from '../../constants';
|
||||
import { HandleElement } from '../../types';
|
||||
|
||||
@@ -15,21 +15,28 @@ export type GetEdgePositionParams = {
|
||||
onError?: OnError;
|
||||
};
|
||||
|
||||
export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | null {
|
||||
const [sourceNodeRect, sourceHandleBounds, isSourceValid] = getHandleDataByNode(params.sourceNode);
|
||||
const [targetNodeRect, targetHandleBounds, isTargetValid] = getHandleDataByNode(params.targetNode);
|
||||
function isNodeInitialized(node: NodeBase): boolean {
|
||||
return !!node?.[internalsSymbol]?.handleBounds && !!node?.computed?.width;
|
||||
}
|
||||
|
||||
if (!isSourceValid || !isTargetValid) {
|
||||
export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | null {
|
||||
const { sourceNode, targetNode } = params;
|
||||
|
||||
if (!isNodeInitialized(sourceNode) || !isNodeInitialized(targetNode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// when connection type is loose we can define all handles as sources and connect source -> source
|
||||
const targetNodeHandles =
|
||||
const sourceHandleBounds = sourceNode[internalsSymbol]?.handleBounds || toHandleBounds(sourceNode.handles);
|
||||
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
|
||||
? targetHandleBounds!.target
|
||||
: (targetHandleBounds!.target ?? []).concat(targetHandleBounds!.source ?? []);
|
||||
const sourceHandle = getHandle(sourceHandleBounds!.source!, params.sourceHandle);
|
||||
const targetHandle = getHandle(targetNodeHandles!, params.targetHandle);
|
||||
? targetHandleBounds?.target ?? []
|
||||
: (targetHandleBounds?.target ?? []).concat(targetHandleBounds?.source ?? []),
|
||||
params.targetHandle
|
||||
);
|
||||
const sourcePosition = sourceHandle?.position || Position.Bottom;
|
||||
const targetPosition = targetHandle?.position || Position.Top;
|
||||
|
||||
@@ -46,8 +53,8 @@ export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | n
|
||||
return null;
|
||||
}
|
||||
|
||||
const { x: sourceX, y: sourceY } = getHandlePosition(sourcePosition, sourceNodeRect, sourceHandle);
|
||||
const { x: targetX, y: targetY } = getHandlePosition(targetPosition, targetNodeRect, targetHandle);
|
||||
const [sourceX, sourceY] = getHandlePosition(sourcePosition, sourceNode, sourceHandle);
|
||||
const [targetX, targetY] = getHandlePosition(targetPosition, targetNode, targetHandle);
|
||||
|
||||
return {
|
||||
sourceX,
|
||||
@@ -64,79 +71,41 @@ function toHandleBounds(handles?: NodeHandle[]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return handles.reduce<NodeHandleBounds>(
|
||||
(res, item) => {
|
||||
item.width = item.width || 1;
|
||||
item.height = item.height || 1;
|
||||
const source = [];
|
||||
const target = [];
|
||||
|
||||
if (item.type === 'source') {
|
||||
res.source?.push(item as HandleElement);
|
||||
}
|
||||
for (const handle of handles) {
|
||||
handle.width = handle.width || 1;
|
||||
handle.height = handle.height || 1;
|
||||
|
||||
if (item.type === 'target') {
|
||||
res.target?.push(item as HandleElement);
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
{
|
||||
source: [],
|
||||
target: [],
|
||||
if (handle.type === 'source') {
|
||||
source.push(handle as HandleElement);
|
||||
} else if (handle.type === 'target') {
|
||||
target.push(handle as HandleElement);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
source,
|
||||
target,
|
||||
};
|
||||
}
|
||||
|
||||
function getHandleDataByNode(node?: NodeBase): [Rect, NodeHandleBounds | null, boolean] {
|
||||
const handleBounds = node?.[internalsSymbol]?.handleBounds || toHandleBounds(node?.handles) || null;
|
||||
const nodeWidth = node?.computed?.width || node?.width;
|
||||
const nodeHeight = node?.computed?.height || node?.height;
|
||||
|
||||
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;
|
||||
function getHandlePosition(position: Position, node: NodeBase, handle: HandleElement | null = null): number[] {
|
||||
const x = (handle?.x ?? 0) + (node.computed?.positionAbsolute?.x ?? 0);
|
||||
const y = (handle?.y ?? 0) + (node.computed?.positionAbsolute?.y ?? 0);
|
||||
const width = handle?.width || (node?.computed?.width ?? node?.width ?? 0);
|
||||
const height = handle?.height || (node?.computed?.height ?? node?.height ?? 0);
|
||||
|
||||
switch (position) {
|
||||
case Position.Top:
|
||||
return {
|
||||
x: x + width / 2,
|
||||
y,
|
||||
};
|
||||
return [x + width / 2, y];
|
||||
case Position.Right:
|
||||
return {
|
||||
x: x + width,
|
||||
y: y + height / 2,
|
||||
};
|
||||
return [x + width, y + height / 2];
|
||||
case Position.Bottom:
|
||||
return {
|
||||
x: x + width / 2,
|
||||
y: y + height,
|
||||
};
|
||||
return [x + width / 2, y + height];
|
||||
case Position.Left:
|
||||
return {
|
||||
x,
|
||||
y: y + height / 2,
|
||||
};
|
||||
return [x, y + height / 2];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
XYZPosition,
|
||||
ConnectionLookup,
|
||||
EdgeBase,
|
||||
EdgeLookup,
|
||||
} from '../types';
|
||||
import { getDimensions, getHandleBounds } from './dom';
|
||||
import { isNumeric } from './general';
|
||||
@@ -42,10 +43,13 @@ export function updateAbsolutePositions<NodeType extends NodeBase>(
|
||||
parentNode?.origin || nodeOrigin
|
||||
);
|
||||
|
||||
node.computed!.positionAbsolute = {
|
||||
x,
|
||||
y,
|
||||
};
|
||||
const positionChanged = x !== node.computed?.positionAbsolute?.x || y !== node.computed?.positionAbsolute?.y;
|
||||
node.computed!.positionAbsolute = positionChanged
|
||||
? {
|
||||
x,
|
||||
y,
|
||||
}
|
||||
: node.computed?.positionAbsolute;
|
||||
|
||||
node[internalsSymbol]!.z = z;
|
||||
|
||||
@@ -244,22 +248,22 @@ export function panBy({
|
||||
return transformChanged;
|
||||
}
|
||||
|
||||
export function updateConnectionLookup(lookup: ConnectionLookup, edges: EdgeBase[]) {
|
||||
lookup.clear();
|
||||
export function updateConnectionLookup(connectionLookup: ConnectionLookup, edgeLookup: EdgeLookup, edges: EdgeBase[]) {
|
||||
connectionLookup.clear();
|
||||
edgeLookup.clear();
|
||||
|
||||
edges.forEach(({ source, target, sourceHandle = null, targetHandle = null }) => {
|
||||
if (source && target) {
|
||||
const sourceKey = `${source}-source-${sourceHandle}`;
|
||||
const targetKey = `${target}-target-${targetHandle}`;
|
||||
for (const edge of edges) {
|
||||
const { source, target, sourceHandle = null, targetHandle = null } = edge;
|
||||
|
||||
const prevSource = lookup.get(sourceKey) || new Map();
|
||||
const prevTarget = lookup.get(targetKey) || new Map();
|
||||
const connection = { source, target, sourceHandle, targetHandle };
|
||||
const sourceKey = `${source}-source-${sourceHandle}`;
|
||||
const targetKey = `${target}-target-${targetHandle}`;
|
||||
|
||||
lookup.set(sourceKey, prevSource.set(`${target}-${targetHandle}`, connection));
|
||||
lookup.set(targetKey, prevTarget.set(`${source}-${sourceHandle}`, connection));
|
||||
}
|
||||
});
|
||||
const prevSource = connectionLookup.get(sourceKey) || new Map();
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user