refactor(edge-wrapper): cleanup

This commit is contained in:
moklick
2023-12-18 11:00:27 +01:00
parent 1401bcae64
commit 5b455a0b2d
13 changed files with 180 additions and 175 deletions
@@ -1,12 +1,11 @@
// Updatable edges have a anchors around their handles to update the edge.
import { XYHandle, type Connection, getEdgePosition } from '@xyflow/system';
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 = {
edgePosition: NonNullable<ReturnType<typeof getEdgePosition>>;
edge: Edge;
isUpdatable: boolean | 'source' | 'target';
edgeUpdaterRadius: EdgeWrapperProps['edgeUpdaterRadius'];
@@ -17,20 +16,25 @@ type EdgeUpdateAnchorsProps = {
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,
edgePosition,
edge,
targetHandleId,
sourceHandleId,
}: EdgeUpdateAnchorsProps) {
const store = useStoreApi();
@@ -104,9 +108,9 @@ function EdgeUpdateAnchors({
<>
{(isUpdatable === 'source' || isUpdatable === true) && (
<EdgeAnchor
position={edgePosition.sourcePosition}
centerX={edgePosition.sourceX}
centerY={edgePosition.sourceY}
position={sourcePosition}
centerX={sourceX}
centerY={sourceY}
radius={edgeUpdaterRadius}
onMouseDown={onEdgeUpdaterSourceMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter}
@@ -116,9 +120,9 @@ function EdgeUpdateAnchors({
)}
{(isUpdatable === 'target' || isUpdatable === true) && (
<EdgeAnchor
position={edgePosition.targetPosition}
centerX={edgePosition.targetX}
centerY={edgePosition.targetY}
position={targetPosition}
centerX={targetX}
centerY={targetY}
radius={edgeUpdaterRadius}
onMouseDown={onEdgeUpdaterTargetMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter}
@@ -1,12 +1,18 @@
import { memo, useState, useMemo, useRef, type KeyboardEvent, useCallback } from 'react';
import cc from 'classcat';
import { shallow } from 'zustand/shallow';
import { getMarkerId, elementSelectionKeys, getEdgePosition, errorMessages, getEdgeZIndex } from '@xyflow/system';
import {
getMarkerId,
elementSelectionKeys,
getEdgePosition,
errorMessages,
getElevatedEdgeZIndex,
} from '@xyflow/system';
import { useStoreApi, useStore } from '../../hooks/useStore';
import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions';
import type { EdgeWrapperProps, Node } from '../../types';
import { builtinEdgeTypes } from './utils';
import type { EdgeWrapperProps } from '../../types';
import { builtinEdgeTypes, nullPosition } from './utils';
import EdgeUpdateAnchors from './EdgeUpdateAnchors';
function EdgeWrapper({
@@ -53,45 +59,44 @@ function EdgeWrapper({
const [updateHover, setUpdateHover] = useState<boolean>(false);
const [updating, setUpdating] = useState<boolean>(false);
const store = useStoreApi();
const prevSourceNode = useRef<Node | undefined>();
const prevTargetNode = useRef<Node | undefined>();
const prevZIndex = useRef<number | undefined>(edge.zIndex);
const prevEdgePosition = useRef<ReturnType<typeof getEdgePosition> | null>(null);
const { edgePosition, zIndex } = useStore(
const { zIndex, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition } = useStore(
useCallback(
(state) => {
const sourceNode = state.nodeLookup.get(edge.source);
const targetNode = state.nodeLookup.get(edge.target);
(store) => {
const sourceNode = store.nodeLookup.get(edge.source);
const targetNode = store.nodeLookup.get(edge.target);
if (!sourceNode || !targetNode) {
return { edgePosition: null, zIndex: edge.zIndex };
return {
zIndex: edge.zIndex,
...nullPosition,
};
}
const nodesChanged = prevSourceNode.current !== sourceNode || prevTargetNode.current !== targetNode;
const edgePosition = getEdgePosition({
id,
sourceNode,
targetNode,
sourceHandle: sourceHandleId || null,
targetHandle: targetHandleId || null,
connectionMode: store.connectionMode,
onError,
});
prevSourceNode.current = sourceNode;
prevTargetNode.current = targetNode;
prevEdgePosition.current = nodesChanged
? getEdgePosition({
id,
sourceNode,
targetNode,
sourceHandle: sourceHandleId || null,
targetHandle: targetHandleId || null,
connectionMode: state.connectionMode,
onError: state.onError,
})
: prevEdgePosition.current;
prevZIndex.current = getEdgeZIndex(edge.selected, edge.zIndex, sourceNode, targetNode, elevateEdgesOnSelect);
const zIndex = getElevatedEdgeZIndex({
selected: edge.selected,
zIndex: edge.zIndex,
sourceNode,
targetNode,
elevateOnSelect: elevateEdgesOnSelect,
});
return {
edgePosition: prevEdgePosition.current,
zIndex: prevZIndex.current,
zIndex,
...(edgePosition || nullPosition),
};
},
[edge.source, edge.target, edge.selected, edge.zIndex]
[edge.source, edge.target, edge.selected, edge.zIndex, elevateEdgesOnSelect]
),
shallow
);
@@ -100,12 +105,13 @@ function EdgeWrapper({
() => (edge.markerStart ? `url(#${getMarkerId(edge.markerStart, rfId)})` : undefined),
[edge.markerStart, rfId]
);
const markerEndUrl = useMemo(
() => (edge.markerEnd ? `url(#${getMarkerId(edge.markerEnd, rfId)})` : undefined),
[edge.markerEnd, rfId]
);
if (edge.hidden || !edgePosition) {
if (edge.hidden || !sourceX || !sourceY || !targetX || !targetY) {
return null;
}
@@ -213,14 +219,14 @@ function EdgeWrapper({
labelBgStyle={edge.labelBgStyle}
labelBgPadding={edge.labelBgPadding}
labelBgBorderRadius={edge.labelBgBorderRadius}
sourceX={sourceX}
sourceY={sourceY}
targetX={targetX}
targetY={targetY}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
data={edge.data}
style={edge.style}
sourceX={edgePosition.sourceX}
sourceY={edgePosition.sourceY}
targetX={edgePosition.targetX}
targetY={edgePosition.targetY}
sourcePosition={edgePosition.sourcePosition}
targetPosition={edgePosition.targetPosition}
sourceHandleId={sourceHandleId}
targetHandleId={targetHandleId}
markerStart={markerStartUrl}
@@ -237,7 +243,12 @@ function EdgeWrapper({
onEdgeUpdate={onEdgeUpdate}
onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd}
edgePosition={edgePosition}
sourceX={sourceX}
sourceY={sourceY}
targetX={targetX}
targetY={targetY}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
setUpdateHover={setUpdateHover}
setUpdating={setUpdating}
sourceHandleId={sourceHandleId}
@@ -15,3 +15,12 @@ export const builtinEdgeTypes: EdgeTypes = {
smoothstep: SmoothStepEdgeInternal as ComponentType<EdgeProps>,
simplebezier: SimpleBezierEdgeInternal as ComponentType<EdgeProps>,
};
export const nullPosition = {
sourceX: null,
sourceY: null,
targetX: null,
targetY: null,
sourcePosition: null,
targetPosition: null,
};
@@ -125,16 +125,11 @@ const NodeWrapper = ({
const isParent = !!node[internalsSymbol]?.isParent;
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
const onMouseEnterHandler =
onMouseEnter === undefined ? undefined : (event: MouseEvent) => onMouseEnter(event, { ...node });
const onMouseMoveHandler =
onMouseMove === undefined ? undefined : (event: MouseEvent) => onMouseMove(event, { ...node });
const onMouseLeaveHandler =
onMouseLeave === undefined ? undefined : (event: MouseEvent) => onMouseLeave(event, { ...node });
const onContextMenuHandler =
onContextMenu === undefined ? undefined : (event: MouseEvent) => onContextMenu(event, { ...node });
const onDoubleClickHandler =
onDoubleClick === undefined ? undefined : (event: MouseEvent) => onDoubleClick(event, { ...node });
const onMouseEnterHandler = onMouseEnter ? (event: MouseEvent) => onMouseEnter(event, { ...node }) : undefined;
const onMouseMoveHandler = onMouseMove ? (event: MouseEvent) => onMouseMove(event, { ...node }) : undefined;
const onMouseLeaveHandler = onMouseLeave ? (event: MouseEvent) => onMouseLeave(event, { ...node }) : undefined;
const onContextMenuHandler = onContextMenu ? (event: MouseEvent) => onContextMenu(event, { ...node }) : undefined;
const onDoubleClickHandler = onDoubleClick ? (event: MouseEvent) => onDoubleClick(event, { ...node }) : undefined;
const onSelectNodeHandler = (event: MouseEvent) => {
const { selectNodesOnDrag, nodeDragThreshold } = store.getState();
+4 -3
View File
@@ -24,8 +24,9 @@ import {
type OnMoveEnd,
type IsValidConnection,
type UpdateConnection,
EdgeLookup,
ConnectionLookup,
type EdgeLookup,
type ConnectionLookup,
type NodeLookup,
} from '@xyflow/system';
import type {
@@ -49,7 +50,7 @@ export type ReactFlowStore = {
height: number;
transform: Transform;
nodes: Node[];
nodeLookup: Map<string, Node>;
nodeLookup: NodeLookup<Node>;
edges: Edge[];
edgeLookup: EdgeLookup<Edge>;
connectionLookup: ConnectionLookup;
+4 -4
View File
@@ -57,12 +57,11 @@ function applyChanges(changes: any[], elements: any[]): any[] {
let remainingChanges = changes;
const updatedElements: any[] = [];
for (let i = 0; i < elements.length; i++) {
for (const item of elements) {
const nextChanges: any[] = [];
const _remainingChanges: any[] = [];
const item = elements[i];
remainingChanges.forEach((c) => {
for (const c of remainingChanges) {
if (c.type === 'add') {
updatedElements.push(c.item);
} else if (c.id === item.id) {
@@ -70,7 +69,8 @@ function applyChanges(changes: any[], elements: any[]): any[] {
} else {
_remainingChanges.push(c);
}
});
}
remainingChanges = _remainingChanges;
if (nextChanges.length === 0) {
@@ -4,6 +4,11 @@ import {
SelectionMode,
ConnectionMode,
ConnectionLineType,
devWarn,
adoptUserProvidedNodes,
getNodesBounds,
getViewportForBounds,
updateConnectionLookup,
type SelectionRect,
type SnapGrid,
type MarkerProps,
@@ -12,16 +17,12 @@ import {
type IsValidConnection,
type NodeOrigin,
type OnError,
devWarn,
type Viewport,
adoptUserProvidedNodes,
getNodesBounds,
getViewportForBounds,
updateConnectionLookup,
type ConnectionLookup,
type OnConnect,
type OnConnectStart,
type OnConnectEnd
type OnConnectEnd,
type NodeLookup
} from '@xyflow/system';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
@@ -96,7 +97,7 @@ export const getInitialStore = ({
return {
flowId: writable<string | null>(null),
nodes: createNodesStore(nextNodes, nodeLookup),
nodeLookup: readable<Map<string, Node>>(nodeLookup),
nodeLookup: readable<NodeLookup>(nodeLookup),
visibleNodes: readable<Node[]>([]),
edges: createEdgesStore(edges, connectionLookup, edgeLookup),
visibleEdges: readable<EdgeLayouted[]>([]),
+6 -5
View File
@@ -1,10 +1,10 @@
import {
writable,
get,
type Unsubscriber,
type Subscriber,
type Updater,
type Writable,
get
type Writable
} from 'svelte/store';
import {
adoptUserProvidedNodes,
@@ -12,7 +12,8 @@ import {
type Viewport,
type PanZoomInstance,
type ConnectionLookup,
type EdgeLookup
type EdgeLookup,
type NodeLookup
} from '@xyflow/system';
import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, Node } from '$lib/types';
@@ -126,7 +127,7 @@ export type NodeStoreOptions = {
// The user only passes in relative positions, so we need to calculate the absolute positions based on the parent nodes.
export const createNodesStore = (
nodes: Node[],
nodeLookup: Map<string, Node>
nodeLookup: NodeLookup<Node>
): {
subscribe: (this: void, run: Subscriber<Node[]>) => Unsubscriber;
update: (this: void, updater: Updater<Node[]>) => void;
@@ -176,7 +177,7 @@ export const createNodesStore = (
export const createEdgesStore = (
edges: Edge[],
connectionLookup: ConnectionLookup,
edgeLookup: EdgeLookup,
edgeLookup: EdgeLookup<Edge>,
defaultOptions?: DefaultEdgeOptions
): Writable<Edge[]> & { setDefaultOptions: (opts: DefaultEdgeOptions) => void } => {
const { subscribe, set, update } = writable<Edge[]>([]);
@@ -1,5 +1,5 @@
import { derived } from 'svelte/store';
import { isEdgeVisible, getEdgePosition, getEdgeZIndex } from '@xyflow/system';
import { isEdgeVisible, getEdgePosition, getElevatedEdgeZIndex } from '@xyflow/system';
import type { EdgeLayouted } from '$lib/types';
import type { SvelteFlowStoreState } from './types';
@@ -64,7 +64,13 @@ export function getVisibleEdges(store: SvelteFlowStoreState) {
if (edgePosition) {
res.push({
...edge,
zIndex: getEdgeZIndex(edge.selected, edge.zIndex, sourceNode, targetNode, false),
zIndex: getElevatedEdgeZIndex({
selected: edge.selected,
zIndex: edge.zIndex,
sourceNode,
targetNode,
elevateOnSelect: false
}),
...edgePosition
});
}
+1 -1
View File
@@ -78,4 +78,4 @@ export type EdgePosition = {
targetPosition: Position;
};
export type EdgeLookup = Map<string, EdgeBase>;
export type EdgeLookup<EdgeType extends EdgeBase = EdgeBase> = Map<string, EdgeType>;
+2
View File
@@ -107,3 +107,5 @@ export type OnSelectionDrag = (event: MouseEvent, nodes: NodeBase[]) => void;
export type NodeHandle = Optional<HandleElement, 'width' | 'height'>;
export type Align = 'center' | 'start' | 'end';
export type NodeLookup<NodeType extends NodeBase = NodeBase> = Map<string, NodeType>;
+21 -15
View File
@@ -1,6 +1,6 @@
import { Connection, Transform, errorMessages, internalsSymbol, isEdgeBase } from '../..';
import { EdgeBase, NodeBase } from '../../types';
import { isNumeric, getOverlappingArea, boxToRect, nodeToBox, getBoundsOfBoxes, devWarn } from '../general';
import { getOverlappingArea, boxToRect, nodeToBox, getBoundsOfBoxes, devWarn } from '../general';
// this is used for straight edges and simple smoothstep edges (LTR, RTL, BTT, TTB)
export function getEdgeCenter({
@@ -23,23 +23,29 @@ export function getEdgeCenter({
return [centerX, centerY, xOffset, yOffset];
}
export function getEdgeZIndex(
selected: boolean | undefined,
zIndex: number | undefined,
sourceNode: NodeBase,
targetNode: NodeBase,
elevateEdgesOnSelect?: boolean
) {
const hasZIndex = isNumeric(zIndex);
let nextZIndex = hasZIndex ? zIndex! : 0;
export type GetEdgeZIndexParams = {
sourceNode: NodeBase;
targetNode: NodeBase;
selected?: boolean;
zIndex?: number;
elevateOnSelect?: boolean;
};
if (elevateEdgesOnSelect) {
const edgeOrConnectedNodeSelected = selected || targetNode?.selected || sourceNode?.selected;
const selectedZIndex = Math.max(sourceNode?.[internalsSymbol]?.z || 0, targetNode?.[internalsSymbol]?.z || 0, 1000);
nextZIndex = (hasZIndex ? zIndex! : 0) + (edgeOrConnectedNodeSelected ? selectedZIndex : 0);
export function getElevatedEdgeZIndex({
sourceNode,
targetNode,
selected = false,
zIndex = 0,
elevateOnSelect = false,
}: GetEdgeZIndexParams): number {
if (!elevateOnSelect) {
return zIndex;
}
return nextZIndex;
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 = {
+45 -76
View File
@@ -1,7 +1,7 @@
import { EdgePosition } from '../../types/edges';
import { ConnectionMode, OnError } from '../../types/general';
import { NodeBase, NodeHandle, NodeHandleBounds } from '../../types/nodes';
import { Position, Rect, XYPosition } from '../../types/utils';
import { NodeBase, NodeHandle } from '../../types/nodes';
import { Position } from '../../types/utils';
import { errorMessages, internalsSymbol } from '../../constants';
import { HandleElement } from '../../types';
@@ -15,21 +15,28 @@ export type GetEdgePositionParams = {
onError?: OnError;
};
export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | null {
const [sourceNodeRect, sourceHandleBounds, isSourceValid] = getHandleDataByNode(params.sourceNode);
const [targetNodeRect, targetHandleBounds, isTargetValid] = getHandleDataByNode(params.targetNode);
function isNodeInitialized(node: NodeBase): boolean {
return !!node?.[internalsSymbol]?.handleBounds && !!node?.computed?.width;
}
if (!isSourceValid || !isTargetValid) {
export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | null {
const { sourceNode, targetNode } = params;
if (!isNodeInitialized(sourceNode) || !isNodeInitialized(targetNode)) {
return null;
}
// when connection type is loose we can define all handles as sources and connect source -> source
const targetNodeHandles =
const sourceHandleBounds = sourceNode[internalsSymbol]?.handleBounds || toHandleBounds(sourceNode.handles);
const targetHandleBounds = targetNode[internalsSymbol]?.handleBounds || toHandleBounds(targetNode.handles);
const sourceHandle = getHandle(sourceHandleBounds?.source ?? [], params.sourceHandle);
const targetHandle = getHandle(
// when connection type is loose we can define all handles as sources and connect source -> source
params.connectionMode === ConnectionMode.Strict
? targetHandleBounds!.target
: (targetHandleBounds!.target ?? []).concat(targetHandleBounds!.source ?? []);
const sourceHandle = getHandle(sourceHandleBounds!.source!, params.sourceHandle);
const targetHandle = getHandle(targetNodeHandles!, params.targetHandle);
? targetHandleBounds?.target ?? []
: (targetHandleBounds?.target ?? []).concat(targetHandleBounds?.source ?? []),
params.targetHandle
);
const sourcePosition = sourceHandle?.position || Position.Bottom;
const targetPosition = targetHandle?.position || Position.Top;
@@ -46,8 +53,8 @@ export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | n
return null;
}
const { x: sourceX, y: sourceY } = getHandlePosition(sourcePosition, sourceNodeRect, sourceHandle);
const { x: targetX, y: targetY } = getHandlePosition(targetPosition, targetNodeRect, targetHandle);
const [sourceX, sourceY] = getHandlePosition(sourcePosition, sourceNode, sourceHandle);
const [targetX, targetY] = getHandlePosition(targetPosition, targetNode, targetHandle);
return {
sourceX,
@@ -64,79 +71,41 @@ function toHandleBounds(handles?: NodeHandle[]) {
return null;
}
return handles.reduce<NodeHandleBounds>(
(res, item) => {
item.width = item.width || 1;
item.height = item.height || 1;
const source = [];
const target = [];
if (item.type === 'source') {
res.source?.push(item as HandleElement);
}
for (const handle of handles) {
handle.width = handle.width || 1;
handle.height = handle.height || 1;
if (item.type === 'target') {
res.target?.push(item as HandleElement);
}
return res;
},
{
source: [],
target: [],
if (handle.type === 'source') {
source.push(handle as HandleElement);
} else if (handle.type === 'target') {
target.push(handle as HandleElement);
}
);
}
return {
source,
target,
};
}
function getHandleDataByNode(node?: NodeBase): [Rect, NodeHandleBounds | null, boolean] {
const handleBounds = node?.[internalsSymbol]?.handleBounds || toHandleBounds(node?.handles) || null;
const nodeWidth = node?.computed?.width || node?.width;
const nodeHeight = node?.computed?.height || node?.height;
const isValid =
handleBounds &&
nodeWidth &&
nodeHeight &&
typeof node?.computed?.positionAbsolute?.x !== 'undefined' &&
typeof node?.computed?.positionAbsolute?.y !== 'undefined';
return [
{
x: node?.computed?.positionAbsolute?.x || 0,
y: node?.computed?.positionAbsolute?.y || 0,
width: nodeWidth || 0,
height: nodeHeight || 0,
},
handleBounds,
!!isValid,
];
}
function getHandlePosition(position: Position, nodeRect: Rect, handle: HandleElement | null = null): XYPosition {
const x = (handle?.x || 0) + nodeRect.x;
const y = (handle?.y || 0) + nodeRect.y;
const width = handle?.width || nodeRect.width;
const height = handle?.height || nodeRect.height;
function getHandlePosition(position: Position, node: NodeBase, handle: HandleElement | null = null): number[] {
const x = (handle?.x ?? 0) + (node.computed?.positionAbsolute?.x ?? 0);
const y = (handle?.y ?? 0) + (node.computed?.positionAbsolute?.y ?? 0);
const width = handle?.width || (node?.computed?.width ?? node?.width ?? 0);
const height = handle?.height || (node?.computed?.height ?? node?.height ?? 0);
switch (position) {
case Position.Top:
return {
x: x + width / 2,
y,
};
return [x + width / 2, y];
case Position.Right:
return {
x: x + width,
y: y + height / 2,
};
return [x + width, y + height / 2];
case Position.Bottom:
return {
x: x + width / 2,
y: y + height,
};
return [x + width / 2, y + height];
case Position.Left:
return {
x,
y: y + height / 2,
};
return [x, y + height / 2];
}
}