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,
|
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';
|
||||||
|
|||||||
@@ -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
-1
@@ -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';
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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'> &
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
+10
-5
@@ -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;
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user