From 4316e233094c4c53e5713562a9a67d570717456d Mon Sep 17 00:00:00 2001 From: moklick Date: Sun, 17 Dec 2023 13:53:07 +0100 Subject: [PATCH] refactor(edge-renderer): work with edge ids instead of edges, like node renderer --- .../src/examples/UpdatableEdge/index.tsx | 5 +- .../EdgeWrapper/EdgeUpdateAnchors.tsx | 133 +++++++++ .../src/components/EdgeWrapper/index.tsx | 266 +++++++----------- packages/react/src/components/Edges/utils.ts | 20 -- .../src/container/EdgeRenderer/index.tsx | 49 +--- packages/react/src/hooks/useVisibleEdges.ts | 12 +- packages/react/src/index.ts | 1 + packages/react/src/store/index.ts | 13 +- packages/react/src/store/initialState.ts | 6 +- packages/react/src/types/edges.ts | 16 +- packages/react/src/types/store.ts | 6 +- .../svelte/src/lib/store/initial-store.ts | 6 +- packages/svelte/src/lib/store/utils.ts | 6 +- packages/system/src/types/edges.ts | 2 + packages/system/src/utils/store.ts | 28 +- 15 files changed, 300 insertions(+), 269 deletions(-) create mode 100644 packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx delete mode 100644 packages/react/src/components/Edges/utils.ts diff --git a/examples/react/src/examples/UpdatableEdge/index.tsx b/examples/react/src/examples/UpdatableEdge/index.tsx index 754efa56..5dacdc9c 100644 --- a/examples/react/src/examples/UpdatableEdge/index.tsx +++ b/examples/react/src/examples/UpdatableEdge/index.tsx @@ -6,7 +6,6 @@ import { addEdge, applyNodeChanges, applyEdgeChanges, - ReactFlowInstance, Connection, Edge, Node, @@ -97,7 +96,6 @@ const initialEdges: Edge[] = [ { id: 'e5-6', source: '5', target: '6', label: 'This edge can be updated from both sides' }, ]; -const onInit = (reactFlowInstance: ReactFlowInstance) => reactFlowInstance.fitView(); const onEdgeUpdateStart = (_: ReactMouseEvent, edge: Edge, handleType: HandleType) => console.log(`start update ${handleType} handle`, edge); const onEdgeUpdateEnd = (_: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) => @@ -111,7 +109,6 @@ const UpdatableEdge = () => { const onConnect = (connection: Connection) => setEdges((els) => addEdge(connection, els)); const onNodesChange = useCallback((changes: NodeChange[]) => { - console.log(changes); setNodes((ns) => applyNodeChanges(changes, ns)); }, []); @@ -125,12 +122,12 @@ const UpdatableEdge = () => { edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} - onInit={onInit} snapToGrid={true} onEdgeUpdate={onEdgeUpdate} onConnect={onConnect} onEdgeUpdateStart={onEdgeUpdateStart} onEdgeUpdateEnd={onEdgeUpdateEnd} + fitView > diff --git a/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx b/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx new file mode 100644 index 00000000..02576559 --- /dev/null +++ b/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx @@ -0,0 +1,133 @@ +// Updatable edges have a anchors around their handles to update the edge. +import { XYHandle, type Connection, getEdgePosition } from '@xyflow/system'; + +import { EdgeAnchor } from '../Edges/EdgeAnchor'; +import type { EdgeWrapperProps, Edge } from '../../types/edges'; +import { useStoreApi } from '../../hooks/useStore'; + +type EdgeUpdateAnchorsProps = { + edgePosition: NonNullable>; + edge: Edge; + isUpdatable: boolean | 'source' | 'target'; + edgeUpdaterRadius: EdgeWrapperProps['edgeUpdaterRadius']; + sourceHandleId: EdgeWrapperProps['sourceHandleId']; + targetHandleId: EdgeWrapperProps['targetHandleId']; + onEdgeUpdate: EdgeWrapperProps['onEdgeUpdate']; + onEdgeUpdateStart: EdgeWrapperProps['onEdgeUpdateStart']; + onEdgeUpdateEnd: EdgeWrapperProps['onEdgeUpdateEnd']; + setUpdateHover: (hover: boolean) => void; + setUpdating: (updating: boolean) => void; +}; + +function EdgeUpdateAnchors({ + isUpdatable, + edgeUpdaterRadius, + onEdgeUpdate, + onEdgeUpdateStart, + onEdgeUpdateEnd, + setUpdating, + setUpdateHover, + edgePosition, + edge, + targetHandleId, + sourceHandleId, +}: EdgeUpdateAnchorsProps) { + const store = useStoreApi(); + + const handleEdgeUpdater = (event: React.MouseEvent, 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): void => + handleEdgeUpdater(event, true); + const onEdgeUpdaterTargetMouseDown = (event: React.MouseEvent): void => + handleEdgeUpdater(event, false); + const onEdgeUpdaterMouseEnter = () => setUpdateHover(true); + const onEdgeUpdaterMouseOut = () => setUpdateHover(false); + + return ( + <> + {(isUpdatable === 'source' || isUpdatable === true) && ( + + )} + {(isUpdatable === 'target' || isUpdatable === true) && ( + + )} + + ); +} + +export default EdgeUpdateAnchors; diff --git a/packages/react/src/components/EdgeWrapper/index.tsx b/packages/react/src/components/EdgeWrapper/index.tsx index 0b20af69..e82268ac 100644 --- a/packages/react/src/components/EdgeWrapper/index.tsx +++ b/packages/react/src/components/EdgeWrapper/index.tsx @@ -1,43 +1,21 @@ import { memo, useState, useMemo, useRef, type KeyboardEvent, useCallback } from 'react'; import cc from 'classcat'; import { shallow } from 'zustand/shallow'; -import { - getMarkerId, - elementSelectionKeys, - XYHandle, - type Connection, - getEdgePosition, - errorMessages, - getEdgeZIndex, -} from '@xyflow/system'; +import { getMarkerId, elementSelectionKeys, getEdgePosition, errorMessages, getEdgeZIndex } from '@xyflow/system'; import { useStoreApi, useStore } from '../../hooks/useStore'; import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions'; -import { EdgeAnchor } from '../Edges/EdgeAnchor'; -import { getMouseHandler } from '../Edges/utils'; import type { EdgeWrapperProps, Node } from '../../types'; import { builtinEdgeTypes } from './utils'; +import EdgeUpdateAnchors from './EdgeUpdateAnchors'; function EdgeWrapper({ id, - className, - type, - data, + edgesFocusable, + edgesUpdatable, + elementsSelectable, onClick, - onEdgeDoubleClick, - selected, - animated, - label, - labelStyle, - labelShowBg, - labelBgStyle, - labelBgPadding, - labelBgBorderRadius, - style, - source, - target, - isSelectable, - hidden, + onDoubleClick, sourceHandleId, targetHandleId, onContextMenu, @@ -48,20 +26,15 @@ function EdgeWrapper({ onEdgeUpdate, onEdgeUpdateStart, onEdgeUpdateEnd, - markerEnd, - markerStart, rfId, - ariaLabel, - isFocusable, - isUpdatable, - pathOptions, - interactionWidth, edgeTypes, - zIndex: edgeZIndex, elevateEdgesOnSelect, + noPanClassName, onError, }: EdgeWrapperProps): JSX.Element | null { - let edgeType = type || 'default'; + const edge = useStore((s) => s.edgeLookup.get(id)!); + + let edgeType = edge.type || 'default'; let EdgeComponent = edgeTypes?.[edgeType] || builtinEdgeTypes[edgeType]; if (EdgeComponent === undefined) { @@ -70,23 +43,29 @@ function EdgeWrapper({ EdgeComponent = builtinEdgeTypes.default; } + const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined')); + const isUpdatable = + typeof onEdgeUpdate !== 'undefined' && + (edge.updatable || (edgesUpdatable && typeof edge.updatable === 'undefined')); + const isSelectable = !!(edge.selectable || (elementsSelectable && typeof edge.selectable === 'undefined')); + const edgeRef = useRef(null); const [updateHover, setUpdateHover] = useState(false); const [updating, setUpdating] = useState(false); const store = useStoreApi(); const prevSourceNode = useRef(); const prevTargetNode = useRef(); - const prevZIndex = useRef(edgeZIndex); + const prevZIndex = useRef(edge.zIndex); const prevEdgePosition = useRef | null>(null); const { edgePosition, zIndex } = useStore( useCallback( (state) => { - const sourceNode = state.nodeLookup.get(source); - const targetNode = state.nodeLookup.get(target); + const sourceNode = state.nodeLookup.get(edge.source); + const targetNode = state.nodeLookup.get(edge.target); if (!sourceNode || !targetNode) { - return { edgePosition: null, zIndex: edgeZIndex }; + return { edgePosition: null, zIndex: edge.zIndex }; } const nodesChanged = prevSourceNode.current !== sourceNode || prevTargetNode.current !== targetNode; @@ -105,28 +84,33 @@ function EdgeWrapper({ onError: state.onError, }) : prevEdgePosition.current; - prevZIndex.current = getEdgeZIndex(selected, edgeZIndex, sourceNode, targetNode, elevateEdgesOnSelect); + prevZIndex.current = getEdgeZIndex(edge.selected, edge.zIndex, sourceNode, targetNode, elevateEdgesOnSelect); return { edgePosition: prevEdgePosition.current, zIndex: prevZIndex.current, }; }, - [source, target, selected, edgeZIndex] + [edge.source, edge.target, edge.selected, edge.zIndex] ), shallow ); - const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart, rfId)})`, [markerStart, rfId]); - const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd, rfId)})`, [markerEnd, rfId]); + const markerStartUrl = useMemo( + () => (edge.markerStart ? `url(#${getMarkerId(edge.markerStart, rfId)})` : undefined), + [edge.markerStart, rfId] + ); + const markerEndUrl = useMemo( + () => (edge.markerEnd ? `url(#${getMarkerId(edge.markerEnd, rfId)})` : undefined), + [edge.markerEnd, rfId] + ); - if (hidden || !edgePosition) { + if (edge.hidden || !edgePosition) { return null; } const onEdgeClick = (event: React.MouseEvent): void => { - const { edges, addSelectedEdges, unselectNodesAndEdges, multiSelectionActive } = store.getState(); - const edge = edges.find((e) => e.id === id); + const { addSelectedEdges, unselectNodesAndEdges, multiSelectionActive } = store.getState(); if (!edge) { return; @@ -148,82 +132,31 @@ function EdgeWrapper({ } }; - const onEdgeDoubleClickHandler = getMouseHandler(id, store.getState, onEdgeDoubleClick); - const onEdgeContextMenu = getMouseHandler(id, store.getState, onContextMenu); - const onEdgeMouseEnter = getMouseHandler(id, store.getState, onMouseEnter); - const onEdgeMouseMove = getMouseHandler(id, store.getState, onMouseMove); - const onEdgeMouseLeave = getMouseHandler(id, store.getState, onMouseLeave); - - const handleEdgeUpdater = (event: React.MouseEvent, isSourceHandle: boolean) => { - // avoid triggering edge updater if mouse btn is not left - if (event.button !== 0) { - return; - } - - const { - autoPanOnConnect, - domNode, - edges, - isValidConnection, - connectionMode, - connectionRadius, - lib, - onConnectStart, - onConnectEnd, - cancelConnection, - nodes, - panBy, - updateConnection, - } = store.getState(); - const nodeId = isSourceHandle ? target : source; - const handleId = (isSourceHandle ? targetHandleId : sourceHandleId) || null; - const handleType = isSourceHandle ? 'target' : 'source'; - - const isTarget = isSourceHandle; - const edge = edges.find((e) => e.id === id)!; - - setUpdating(true); - onEdgeUpdateStart?.(event, edge, handleType); - - const _onEdgeUpdateEnd = (evt: MouseEvent | TouchEvent) => { - setUpdating(false); - onEdgeUpdateEnd?.(evt, edge, handleType); - }; - - const onConnectEdge = (connection: Connection) => onEdgeUpdate?.(edge, connection); - - XYHandle.onPointerDown(event.nativeEvent, { - autoPanOnConnect, - connectionMode, - connectionRadius, - domNode, - handleId, - nodeId, - nodes, - isTarget, - edgeUpdaterType: handleType, - lib, - cancelConnection, - panBy, - isValidConnection, - onConnect: onConnectEdge, - onConnectStart, - onConnectEnd, - onEdgeUpdateEnd: _onEdgeUpdateEnd, - updateConnection, - getTransform: () => store.getState().transform, - }); - }; - - const onEdgeUpdaterSourceMouseDown = (event: React.MouseEvent): void => - handleEdgeUpdater(event, true); - const onEdgeUpdaterTargetMouseDown = (event: React.MouseEvent): void => - handleEdgeUpdater(event, false); - - const onEdgeUpdaterMouseEnter = () => setUpdateHover(true); - const onEdgeUpdaterMouseOut = () => setUpdateHover(false); - - const inactive = !isSelectable && !onClick; + const onEdgeDoubleClick = onDoubleClick + ? (event: React.MouseEvent) => { + onDoubleClick(event, { ...edge }); + } + : undefined; + const onEdgeContextMenu = onContextMenu + ? (event: React.MouseEvent) => { + onContextMenu(event, { ...edge }); + } + : undefined; + const onEdgeMouseEnter = onMouseEnter + ? (event: React.MouseEvent) => { + onMouseEnter(event, { ...edge }); + } + : undefined; + const onEdgeMouseMove = onMouseMove + ? (event: React.MouseEvent) => { + onMouseMove(event, { ...edge }); + } + : undefined; + const onEdgeMouseLeave = onMouseLeave + ? (event: React.MouseEvent) => { + onMouseLeave(event, { ...edge }); + } + : undefined; const onKeyDown = (event: KeyboardEvent) => { if (elementSelectionKeys.includes(event.key) && isSelectable) { @@ -244,12 +177,18 @@ function EdgeWrapper({ {!updating && ( )} {isUpdatable && ( - <> - {(isUpdatable === 'source' || isUpdatable === true) && ( - - )} - {(isUpdatable === 'target' || isUpdatable === true) && ( - - )} - + )} diff --git a/packages/react/src/components/Edges/utils.ts b/packages/react/src/components/Edges/utils.ts deleted file mode 100644 index a51b7613..00000000 --- a/packages/react/src/components/Edges/utils.ts +++ /dev/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['getState'], - handler?: (event: ReactMouseEvent, edge: Edge) => void -) { - return handler === undefined - ? handler - : (event: ReactMouseEvent) => { - const edge = getState().edges.find((e) => e.id === id); - - if (edge) { - handler(event, { ...edge }); - } - }; -} diff --git a/packages/react/src/container/EdgeRenderer/index.tsx b/packages/react/src/container/EdgeRenderer/index.tsx index e1ae6a20..550bb487 100644 --- a/packages/react/src/container/EdgeRenderer/index.tsx +++ b/packages/react/src/container/EdgeRenderer/index.tsx @@ -1,9 +1,8 @@ import { memo, ReactNode } from 'react'; import { shallow } from 'zustand/shallow'; -import cc from 'classcat'; import { useStore } from '../../hooks/useStore'; -import useVisibleEdges from '../../hooks/useVisibleEdges'; +import useVisibleEdgeIds from '../../hooks/useVisibleEdges'; import MarkerDefinitions from './MarkerDefinitions'; import { GraphViewProps } from '../GraphView'; import type { ReactFlowState } from '../../types'; @@ -63,7 +62,7 @@ const EdgeRenderer = ({ children, }: EdgeRendererProps) => { const { edgesFocusable, edgesUpdatable, elementsSelectable, onError } = useStore(selector, shallow); - const edges = useVisibleEdges(onlyRenderVisibleElements); + const edgeIds = useVisibleEdgeIds(onlyRenderVisibleElements); return (
@@ -71,38 +70,15 @@ const EdgeRenderer = ({ - {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')); - + {edgeIds.map((id) => { return (