From 08e61f31113b0f166af1fe760f37f27b64ee23a3 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 16 Nov 2023 15:41:30 +0100 Subject: [PATCH 01/11] refactor(edges): simplify edge rendering --- .../react/src/components/Edges/wrapEdge.tsx | 163 +++++++++--------- .../src/container/EdgeRenderer/index.tsx | 134 +++++++------- packages/react/src/hooks/useVisibleEdges.ts | 21 +-- .../components/EdgeWrapper/EdgeWrapper.svelte | 79 +++++---- .../EdgeRenderer/EdgeRenderer.svelte | 96 +++++------ packages/svelte/src/lib/store/index.ts | 4 +- .../svelte/src/lib/store/initial-store.ts | 2 +- .../store/{edge-tree.ts => visible-edges.ts} | 21 ++- packages/svelte/src/lib/types/edges.ts | 1 + packages/system/src/styles/init.css | 5 - packages/system/src/styles/style.css | 6 + packages/system/src/utils/edges/general.ts | 61 ++----- 12 files changed, 275 insertions(+), 318 deletions(-) rename packages/svelte/src/lib/store/{edge-tree.ts => visible-edges.ts} (78%) diff --git a/packages/react/src/components/Edges/wrapEdge.tsx b/packages/react/src/components/Edges/wrapEdge.tsx index b0a876b0..afdeead5 100644 --- a/packages/react/src/components/Edges/wrapEdge.tsx +++ b/packages/react/src/components/Edges/wrapEdge.tsx @@ -26,6 +26,7 @@ export default (EdgeComponent: ComponentType) => { labelBgPadding, labelBgBorderRadius, style, + zIndex, source, target, isSelectable, @@ -201,86 +202,88 @@ export default (EdgeComponent: ComponentType) => { }; return ( - - {!updating && ( - - )} - {isUpdatable && ( - <> - {(isUpdatable === 'source' || isUpdatable === true) && ( - - )} - {(isUpdatable === 'target' || isUpdatable === true) && ( - - )} - - )} - + + + {!updating && ( + + )} + {isUpdatable && ( + <> + {(isUpdatable === 'source' || isUpdatable === true) && ( + + )} + {(isUpdatable === 'target' || isUpdatable === true) && ( + + )} + + )} + + ); }; diff --git a/packages/react/src/container/EdgeRenderer/index.tsx b/packages/react/src/container/EdgeRenderer/index.tsx index 24825fe6..35137619 100644 --- a/packages/react/src/container/EdgeRenderer/index.tsx +++ b/packages/react/src/container/EdgeRenderer/index.tsx @@ -63,82 +63,76 @@ const EdgeRenderer = ({ children, }: EdgeRendererProps) => { const { edgesFocusable, edgesUpdatable, elementsSelectable, onError } = useStore(selector, shallow); - // we are grouping edges by zIndex here in order to be able to render them in the correct order - // each zIndex gets its own svg element - const edgeTree = useVisibleEdges(onlyRenderVisibleElements, elevateEdgesOnSelect); + const edges = useVisibleEdges(onlyRenderVisibleElements, elevateEdgesOnSelect); return ( - <> - {edgeTree.map(({ level, edges, isMaxLevel }) => ( - - {isMaxLevel && } - <> - {edges.map((edge) => { - let edgeType = edge.type || 'default'; +
+ + + - if (!edgeTypes[edgeType]) { - onError?.('011', errorMessages['error011'](edgeType)); - edgeType = 'default'; - } + {edges.map((edge) => { + let edgeType = edge.type || 'default'; - const EdgeComponent = edgeTypes[edgeType]; - 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') - ); + if (!edgeTypes[edgeType]) { + onError?.('011', errorMessages['error011'](edgeType)); + edgeType = 'default'; + } + + const EdgeComponent = edgeTypes[edgeType]; + 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 ( +
); }; diff --git a/packages/react/src/hooks/useVisibleEdges.ts b/packages/react/src/hooks/useVisibleEdges.ts index 67d02c92..ca035fbb 100644 --- a/packages/react/src/hooks/useVisibleEdges.ts +++ b/packages/react/src/hooks/useVisibleEdges.ts @@ -1,11 +1,11 @@ import { useCallback } from 'react'; -import { GroupedEdges, groupEdgesByZLevel, isEdgeVisible } from '@xyflow/system'; +import { shallow } from 'zustand/shallow'; +import { adjustEdgeZIndex, isEdgeVisible } from '@xyflow/system'; import { useStore } from '../hooks/useStore'; -import { Edge, type ReactFlowState } from '../types'; -import { shallow } from 'zustand/shallow'; +import { type Edge, type ReactFlowState } from '../types'; -function useVisibleEdges(onlyRenderVisible: boolean, elevateEdgesOnSelect: boolean): GroupedEdges[] { +function useVisibleEdges(onlyRenderVisible: boolean, elevateEdgesOnSelect: boolean): Edge[] { const edges = useStore( useCallback( (s: ReactFlowState) => { @@ -29,20 +29,11 @@ function useVisibleEdges(onlyRenderVisible: boolean, elevateEdgesOnSelect: boole }) : s.edges; - return groupEdgesByZLevel(visibleEdges, s.nodeLookup, elevateEdgesOnSelect); + return visibleEdges.map((edge) => adjustEdgeZIndex(edge, 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; - } + shallow ); return edges; diff --git a/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte b/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte index 7a8df68c..763c6efb 100644 --- a/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte +++ b/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte @@ -18,6 +18,7 @@ export let target: $$Props['target'] = ''; export let data: $$Props['data'] = {}; export let style: $$Props['style'] = undefined; + export let zIndex: $$Props['zIndex'] = undefined; export let animated: $$Props['animated'] = false; export let selected: $$Props['selected'] = false; @@ -95,42 +96,44 @@ {#if !hidden} - - - + + + + + {/if} diff --git a/packages/svelte/src/lib/container/EdgeRenderer/EdgeRenderer.svelte b/packages/svelte/src/lib/container/EdgeRenderer/EdgeRenderer.svelte index 27bc7d72..6dcc16fa 100644 --- a/packages/svelte/src/lib/container/EdgeRenderer/EdgeRenderer.svelte +++ b/packages/svelte/src/lib/container/EdgeRenderer/EdgeRenderer.svelte @@ -9,7 +9,7 @@ const { elementsSelectable, - edgeTree, + visibleEdges, edges: { setDefaultOptions } } = useStore(); @@ -18,57 +18,47 @@ }); -{#each $edgeTree as group (group.level)} - - {#if group.isMaxLevel} {/if} - - {#each group.edges as edge (edge.id)} - {@const edgeType = edge.type || 'default'} - {@const selectable = !!( - edge.selectable || - ($elementsSelectable && typeof edge.selectable === 'undefined') - )} - - - {/each} - +
+ + -{/each} - + {#each $visibleEdges as edge (edge.id)} + {@const edgeType = edge.type || 'default'} + {@const selectable = !!( + edge.selectable || + ($elementsSelectable && typeof edge.selectable === 'undefined') + )} + +
diff --git a/packages/svelte/src/lib/store/index.ts b/packages/svelte/src/lib/store/index.ts index 625191e0..a16844d2 100644 --- a/packages/svelte/src/lib/store/index.ts +++ b/packages/svelte/src/lib/store/index.ts @@ -24,7 +24,7 @@ import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions, ConnectionData } import { initialEdgeTypes, initialNodeTypes, getInitialStore } from './initial-store'; import type { SvelteFlowStore } from './types'; import { syncNodeStores, syncEdgeStores, syncViewportStores } from './utils'; -import { getEdgeTree } from './edge-tree'; +import { getVisibleEdges } from './visible-edges'; import { getVisibleNodes } from './visible-nodes'; import { getDerivedConnectionProps } from './derived-connection-props'; @@ -349,8 +349,8 @@ export function createStore({ ...store, // derived state - edgeTree: getEdgeTree(store), connection: getDerivedConnectionProps(store, currentConnection), + visibleEdges: getVisibleEdges(store), visibleNodes: getVisibleNodes(store), markers: derived( [store.edges, store.defaultMarkerColor, store.flowId], diff --git a/packages/svelte/src/lib/store/initial-store.ts b/packages/svelte/src/lib/store/initial-store.ts index d6b621d9..3821023a 100644 --- a/packages/svelte/src/lib/store/initial-store.ts +++ b/packages/svelte/src/lib/store/initial-store.ts @@ -91,7 +91,7 @@ export const getInitialStore = ({ nodeLookup: readable>(nodeLookup), visibleNodes: readable([]), edges: createEdgesStore(edges), - edgeTree: readable[]>([]), + visibleEdges: readable([]), height: writable(500), width: writable(500), minZoom: writable(0.5), diff --git a/packages/svelte/src/lib/store/edge-tree.ts b/packages/svelte/src/lib/store/visible-edges.ts similarity index 78% rename from packages/svelte/src/lib/store/edge-tree.ts rename to packages/svelte/src/lib/store/visible-edges.ts index cd5448ab..d9903261 100644 --- a/packages/svelte/src/lib/store/edge-tree.ts +++ b/packages/svelte/src/lib/store/visible-edges.ts @@ -1,10 +1,10 @@ import { derived } from 'svelte/store'; -import { groupEdgesByZLevel, isEdgeVisible, getEdgePosition } from '@xyflow/system'; +import { isEdgeVisible, getEdgePosition, adjustEdgeZIndex } from '@xyflow/system'; import type { EdgeLayouted } from '$lib/types'; import type { SvelteFlowStoreState } from './types'; -export function getEdgeTree(store: SvelteFlowStoreState) { +export function getVisibleEdges(store: SvelteFlowStoreState) { const visibleEdges = derived( [ store.edges, @@ -41,8 +41,15 @@ export function getEdgeTree(store: SvelteFlowStoreState) { ); return derived( - [visibleEdges, store.nodes, store.nodeLookup, store.connectionMode, store.onerror], - ([visibleEdges, , nodeLookup, connectionMode, onerror]) => { + [ + visibleEdges, + store.nodes, + store.nodeLookup, + store.connectionMode, + store.onlyRenderVisibleElements, + store.onerror + ], + ([visibleEdges, , nodeLookup, connectionMode, onlyRenderVisibleElements, onerror]) => { const layoutedEdges = visibleEdges.reduce((res, edge) => { const sourceNode = nodeLookup.get(edge.source); const targetNode = nodeLookup.get(edge.target); @@ -63,7 +70,7 @@ export function getEdgeTree(store: SvelteFlowStoreState) { if (edgePosition) { res.push({ - ...edge, + ...adjustEdgeZIndex(edge, nodeLookup, onlyRenderVisibleElements), ...edgePosition }); } @@ -71,9 +78,7 @@ export function getEdgeTree(store: SvelteFlowStoreState) { return res; }, []); - const groupedEdges = groupEdgesByZLevel(layoutedEdges, nodeLookup, false); - - return groupedEdges; + return layoutedEdges; } ); } diff --git a/packages/svelte/src/lib/types/edges.ts b/packages/svelte/src/lib/types/edges.ts index 33f0d908..4ae172e6 100644 --- a/packages/svelte/src/lib/types/edges.ts +++ b/packages/svelte/src/lib/types/edges.ts @@ -70,6 +70,7 @@ export type EdgeLayouted = Pick< | 'ariaLabel' | 'hidden' | 'class' + | 'zIndex' > & EdgePosition & { sourceNode?: Node; diff --git a/packages/system/src/styles/init.css b/packages/system/src/styles/init.css index 9fed0d7b..26530e9b 100644 --- a/packages/system/src/styles/init.css +++ b/packages/system/src/styles/init.css @@ -57,11 +57,6 @@ outline: none; } -.xy-flow .xy-flow__edges { - pointer-events: none; - overflow: visible; -} - .xy-flow__edge-path { stroke: var(--edge-stroke, var(--edge-stroke-default)); stroke-width: var(--edge-stroke-width, var(--edge-stroke-width-default)); diff --git a/packages/system/src/styles/style.css b/packages/system/src/styles/style.css index 99fa8876..2e6a95c8 100644 --- a/packages/system/src/styles/style.css +++ b/packages/system/src/styles/style.css @@ -20,6 +20,12 @@ --controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, 0.08); } +.xy-flow__edgewrapper { + overflow: visible; + position: absolute; + pointer-events: none; +} + .xy-flow__edge { &.updating { .xy-flow__edge-path { diff --git a/packages/system/src/utils/edges/general.ts b/packages/system/src/utils/edges/general.ts index bdffbbd8..ca1dd390 100644 --- a/packages/system/src/utils/edges/general.ts +++ b/packages/system/src/utils/edges/general.ts @@ -23,63 +23,32 @@ export function getEdgeCenter({ return [centerX, centerY, xOffset, yOffset]; } -const defaultEdgeTree = [{ level: 0, isMaxLevel: true, edges: [] }]; - export type GroupedEdges = { edges: EdgeType[]; level: number; isMaxLevel: boolean; }; -export function groupEdgesByZLevel( - edges: EdgeType[], - nodeLookup: Map, - elevateEdgesOnSelect = false -): GroupedEdges[] { - let maxLevel = -1; +export function adjustEdgeZIndex(edge: EdgeBase, nodeLookup: Map, elevateEdgesOnSelect: boolean) { + const hasZIndex = isNumeric(edge.zIndex); + let z = hasZIndex ? edge.zIndex! : 0; - const levelLookup = edges.reduce>((tree, edge) => { - const hasZIndex = isNumeric(edge.zIndex); - let z = hasZIndex ? edge.zIndex! : 0; + if (elevateEdgesOnSelect) { + const targetNode = nodeLookup.get(edge.target)!; + const sourceNode = nodeLookup.get(edge.source)!; + const edgeOrConnectedNodeSelected = edge.selected || targetNode?.selected || sourceNode?.selected; + const selectedZIndex = Math.max(sourceNode?.[internalsSymbol]?.z || 0, targetNode?.[internalsSymbol]?.z || 0, 1000); + z = (hasZIndex ? edge.zIndex! : 0) + (edgeOrConnectedNodeSelected ? selectedZIndex : 0); - if (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 (edge.zIndex !== z) { + return { + ...edge, + zIndex: z, + }; } - - 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; + return edge; } type IsEdgeVisibleParams = { From 2fe9d57d2a33e4e3b5e9d39ea289415668875d59 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 16 Nov 2023 15:45:36 +0100 Subject: [PATCH 02/11] chore(edges): remove group type --- packages/svelte/src/lib/store/initial-store.ts | 1 - packages/system/src/utils/edges/general.ts | 6 ------ 2 files changed, 7 deletions(-) diff --git a/packages/svelte/src/lib/store/initial-store.ts b/packages/svelte/src/lib/store/initial-store.ts index 3821023a..b9f5a524 100644 --- a/packages/svelte/src/lib/store/initial-store.ts +++ b/packages/svelte/src/lib/store/initial-store.ts @@ -10,7 +10,6 @@ import { type PanZoomInstance, type CoordinateExtent, type IsValidConnection, - type GroupedEdges, type NodeOrigin, type OnError, devWarn, diff --git a/packages/system/src/utils/edges/general.ts b/packages/system/src/utils/edges/general.ts index ca1dd390..518f6963 100644 --- a/packages/system/src/utils/edges/general.ts +++ b/packages/system/src/utils/edges/general.ts @@ -23,12 +23,6 @@ export function getEdgeCenter({ return [centerX, centerY, xOffset, yOffset]; } -export type GroupedEdges = { - edges: EdgeType[]; - level: number; - isMaxLevel: boolean; -}; - export function adjustEdgeZIndex(edge: EdgeBase, nodeLookup: Map, elevateEdgesOnSelect: boolean) { const hasZIndex = isNumeric(edge.zIndex); let z = hasZIndex ? edge.zIndex! : 0; From 622c987e46395d8d91a58530b44bd398113db2f1 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 16 Nov 2023 15:52:57 +0100 Subject: [PATCH 03/11] refactor(edges): cleanup markup --- packages/react/src/components/Edges/wrapEdge.tsx | 2 +- packages/react/src/container/EdgeRenderer/index.tsx | 2 +- .../svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte | 2 +- .../svelte/src/lib/container/EdgeRenderer/EdgeRenderer.svelte | 2 +- packages/system/src/styles/style.css | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/react/src/components/Edges/wrapEdge.tsx b/packages/react/src/components/Edges/wrapEdge.tsx index afdeead5..7d3f92e6 100644 --- a/packages/react/src/components/Edges/wrapEdge.tsx +++ b/packages/react/src/components/Edges/wrapEdge.tsx @@ -202,7 +202,7 @@ export default (EdgeComponent: ComponentType) => { }; return ( - + - + diff --git a/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte b/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte index 763c6efb..fb90ca8b 100644 --- a/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte +++ b/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte @@ -96,7 +96,7 @@ {#if !hidden} - +
- + diff --git a/packages/system/src/styles/style.css b/packages/system/src/styles/style.css index 2e6a95c8..1ba81595 100644 --- a/packages/system/src/styles/style.css +++ b/packages/system/src/styles/style.css @@ -20,7 +20,7 @@ --controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, 0.08); } -.xy-flow__edgewrapper { +.xy-flow__edges svg { overflow: visible; position: absolute; pointer-events: none; From 2bc02547f6e70e159d818bce22447ab6dd991ce6 Mon Sep 17 00:00:00 2001 From: moklick Date: Sun, 17 Dec 2023 11:56:21 +0100 Subject: [PATCH 04/11] refactor(edges): optimize edge zIndex calculation --- .../src/components/EdgeWrapper/index.tsx | 48 +++++++++++++------ .../src/container/EdgeRenderer/index.tsx | 3 +- packages/react/src/hooks/useVisibleEdges.ts | 8 ++-- packages/react/src/types/edges.ts | 1 + .../svelte/src/lib/store/visible-edges.ts | 16 ++----- packages/system/src/utils/edges/general.ts | 27 +++++------ 6 files changed, 58 insertions(+), 45 deletions(-) diff --git a/packages/react/src/components/EdgeWrapper/index.tsx b/packages/react/src/components/EdgeWrapper/index.tsx index de387ce5..0b20af69 100644 --- a/packages/react/src/components/EdgeWrapper/index.tsx +++ b/packages/react/src/components/EdgeWrapper/index.tsx @@ -8,13 +8,14 @@ import { type Connection, 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 } from '../../types'; +import type { EdgeWrapperProps, Node } from '../../types'; import { builtinEdgeTypes } from './utils'; function EdgeWrapper({ @@ -56,7 +57,8 @@ function EdgeWrapper({ pathOptions, interactionWidth, edgeTypes, - zIndex, + zIndex: edgeZIndex, + elevateEdgesOnSelect, onError, }: EdgeWrapperProps): JSX.Element | null { let edgeType = type || 'default'; @@ -72,27 +74,45 @@ function EdgeWrapper({ const [updateHover, setUpdateHover] = useState(false); const [updating, setUpdating] = useState(false); const store = useStoreApi(); - const edgePosition = useStore( + const prevSourceNode = useRef(); + const prevTargetNode = useRef(); + const prevZIndex = useRef(edgeZIndex); + const prevEdgePosition = useRef | null>(null); + + const { edgePosition, zIndex } = useStore( useCallback( (state) => { const sourceNode = state.nodeLookup.get(source); const targetNode = state.nodeLookup.get(target); if (!sourceNode || !targetNode) { - return null; + return { edgePosition: null, zIndex: edgeZIndex }; } - return getEdgePosition({ - id, - sourceNode, - targetNode, - sourceHandle: sourceHandleId || null, - targetHandle: targetHandleId || null, - connectionMode: state.connectionMode, - onError: state.onError, - }); + const nodesChanged = prevSourceNode.current !== sourceNode || prevTargetNode.current !== targetNode; + + 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(selected, edgeZIndex, sourceNode, targetNode, elevateEdgesOnSelect); + + return { + edgePosition: prevEdgePosition.current, + zIndex: prevZIndex.current, + }; }, - [source, target] + [source, target, selected, edgeZIndex] ), shallow ); diff --git a/packages/react/src/container/EdgeRenderer/index.tsx b/packages/react/src/container/EdgeRenderer/index.tsx index a5e591f1..e1ae6a20 100644 --- a/packages/react/src/container/EdgeRenderer/index.tsx +++ b/packages/react/src/container/EdgeRenderer/index.tsx @@ -63,7 +63,7 @@ const EdgeRenderer = ({ children, }: EdgeRendererProps) => { const { edgesFocusable, edgesUpdatable, elementsSelectable, onError } = useStore(selector, shallow); - const edges = useVisibleEdges(onlyRenderVisibleElements, elevateEdgesOnSelect); + const edges = useVisibleEdges(onlyRenderVisibleElements); return (
@@ -121,6 +121,7 @@ const EdgeRenderer = ({ interactionWidth={edge.interactionWidth} onError={onError} edgeTypes={edgeTypes} + elevateEdgesOnSelect={elevateEdgesOnSelect} /> ); })} diff --git a/packages/react/src/hooks/useVisibleEdges.ts b/packages/react/src/hooks/useVisibleEdges.ts index ca035fbb..f7ebe606 100644 --- a/packages/react/src/hooks/useVisibleEdges.ts +++ b/packages/react/src/hooks/useVisibleEdges.ts @@ -1,11 +1,11 @@ import { useCallback } from 'react'; import { shallow } from 'zustand/shallow'; -import { adjustEdgeZIndex, isEdgeVisible } from '@xyflow/system'; +import { isEdgeVisible } from '@xyflow/system'; import { useStore } from '../hooks/useStore'; import { type Edge, type ReactFlowState } from '../types'; -function useVisibleEdges(onlyRenderVisible: boolean, elevateEdgesOnSelect: boolean): Edge[] { +function useVisibleEdges(onlyRenderVisible: boolean): Edge[] { const edges = useStore( useCallback( (s: ReactFlowState) => { @@ -29,9 +29,9 @@ function useVisibleEdges(onlyRenderVisible: boolean, elevateEdgesOnSelect: boole }) : s.edges; - return visibleEdges.map((edge) => adjustEdgeZIndex(edge, s.nodeLookup, elevateEdgesOnSelect)); + return visibleEdges; }, - [onlyRenderVisible, elevateEdgesOnSelect] + [onlyRenderVisible] ), shallow ); diff --git a/packages/react/src/types/edges.ts b/packages/react/src/types/edges.ts index 5af61d1e..2c0d3742 100644 --- a/packages/react/src/types/edges.ts +++ b/packages/react/src/types/edges.ts @@ -78,6 +78,7 @@ export type EdgeWrapperProps = Omit, 'sourceHandle' | 'targetHa pathOptions?: BezierPathOptions | SmoothStepPathOptions; edgeTypes?: EdgeTypes; onError?: OnError; + elevateEdgesOnSelect?: boolean; }; export type DefaultEdgeOptions = DefaultEdgeOptionsBase; diff --git a/packages/svelte/src/lib/store/visible-edges.ts b/packages/svelte/src/lib/store/visible-edges.ts index d9903261..1dd4a956 100644 --- a/packages/svelte/src/lib/store/visible-edges.ts +++ b/packages/svelte/src/lib/store/visible-edges.ts @@ -1,5 +1,5 @@ import { derived } from 'svelte/store'; -import { isEdgeVisible, getEdgePosition, adjustEdgeZIndex } from '@xyflow/system'; +import { isEdgeVisible, getEdgePosition, getEdgeZIndex } from '@xyflow/system'; import type { EdgeLayouted } from '$lib/types'; import type { SvelteFlowStoreState } from './types'; @@ -41,15 +41,8 @@ export function getVisibleEdges(store: SvelteFlowStoreState) { ); return derived( - [ - visibleEdges, - store.nodes, - store.nodeLookup, - store.connectionMode, - store.onlyRenderVisibleElements, - store.onerror - ], - ([visibleEdges, , nodeLookup, connectionMode, onlyRenderVisibleElements, onerror]) => { + [visibleEdges, store.nodes, store.nodeLookup, store.connectionMode, store.onerror], + ([visibleEdges, , nodeLookup, connectionMode, onerror]) => { const layoutedEdges = visibleEdges.reduce((res, edge) => { const sourceNode = nodeLookup.get(edge.source); const targetNode = nodeLookup.get(edge.target); @@ -70,7 +63,8 @@ export function getVisibleEdges(store: SvelteFlowStoreState) { if (edgePosition) { res.push({ - ...adjustEdgeZIndex(edge, nodeLookup, onlyRenderVisibleElements), + ...edge, + zIndex: getEdgeZIndex(edge.selected, edge.zIndex, sourceNode, targetNode, false), ...edgePosition }); } diff --git a/packages/system/src/utils/edges/general.ts b/packages/system/src/utils/edges/general.ts index fcde5f8b..73cbd7f9 100644 --- a/packages/system/src/utils/edges/general.ts +++ b/packages/system/src/utils/edges/general.ts @@ -23,26 +23,23 @@ export function getEdgeCenter({ return [centerX, centerY, xOffset, yOffset]; } -export function adjustEdgeZIndex(edge: EdgeBase, nodeLookup: Map, elevateEdgesOnSelect: boolean) { - const hasZIndex = isNumeric(edge.zIndex); - let z = hasZIndex ? edge.zIndex! : 0; +export function getEdgeZIndex( + selected: boolean | undefined, + zIndex: number | undefined, + sourceNode: NodeBase, + targetNode: NodeBase, + elevateEdgesOnSelect?: boolean +) { + const hasZIndex = isNumeric(zIndex); + let nextZIndex = hasZIndex ? 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 edgeOrConnectedNodeSelected = 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 (edge.zIndex !== z) { - return { - ...edge, - zIndex: z, - }; - } + nextZIndex = (hasZIndex ? zIndex! : 0) + (edgeOrConnectedNodeSelected ? selectedZIndex : 0); } - return edge; + return nextZIndex; } type IsEdgeVisibleParams = { From 4316e233094c4c53e5713562a9a67d570717456d Mon Sep 17 00:00:00 2001 From: moklick Date: Sun, 17 Dec 2023 13:53:07 +0100 Subject: [PATCH 05/11] 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 (