From a893cddf8f7397c71980808bdd6d3dd4c38edf72 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Wed, 8 Jan 2025 16:05:28 +0100 Subject: [PATCH 1/4] Port enhanced selection logic from svelte to react --- .changeset/perfect-fans-grow.md | 5 + packages/react/src/container/Pane/index.tsx | 105 +++++++++++--------- 2 files changed, 62 insertions(+), 48 deletions(-) create mode 100644 .changeset/perfect-fans-grow.md diff --git a/.changeset/perfect-fans-grow.md b/.changeset/perfect-fans-grow.md new file mode 100644 index 00000000..2d4c738e --- /dev/null +++ b/.changeset/perfect-fans-grow.md @@ -0,0 +1,5 @@ +--- +'@xyflow/react': patch +--- + +Optimize selections and take into account if edges connected to selected nodes are actually selectable. diff --git a/packages/react/src/container/Pane/index.tsx b/packages/react/src/container/Pane/index.tsx index 48c23eba..99d81da3 100644 --- a/packages/react/src/container/Pane/index.tsx +++ b/packages/react/src/container/Pane/index.tsx @@ -39,6 +39,20 @@ type PaneProps = { > >; +function areSetsEqual(a: Set, b: Set) { + if (a.size !== b.size) { + return false; + } + + for (const item of a) { + if (!b.has(item)) { + return false; + } + } + + return true; +} + const wrapHandler = ( handler: React.MouseEventHandler | undefined, containerRef: React.MutableRefObject @@ -73,27 +87,20 @@ export function Pane({ onPaneMouseLeave, children, }: PaneProps) { - const container = useRef(null); const store = useStoreApi(); - const prevSelectedNodesCount = useRef(0); - const prevSelectedEdgesCount = useRef(0); - const containerBounds = useRef(); - const edgeIdLookup = useRef>>(new Map()); - const { userSelectionActive, elementsSelectable, dragging } = useStore(selector, shallow); const hasActiveSelection = elementsSelectable && (isSelecting || userSelectionActive); + const container = useRef(null); + const containerBounds = useRef(); + + const selectedNodeIds = useRef>(new Set()); + const selectedEdgeIds = useRef>(new Set()); + // Used to prevent click events when the user lets go of the selectionKey during a selection const selectionInProgress = useRef(false); const selectionStarted = useRef(false); - const resetUserSelection = () => { - store.setState({ userSelectionActive: false, userSelectionRect: null }); - - prevSelectedNodesCount.current = 0; - prevSelectedEdgesCount.current = 0; - }; - const onClick = (event: ReactMouseEvent) => { // We prevent click events when the user let go of the selectionKey during a selection if (selectionInProgress.current) { @@ -135,12 +142,6 @@ export function Pane({ selectionStarted.current = true; selectionInProgress.current = false; - edgeIdLookup.current = new Map(); - - for (const [id, edge] of edgeLookup) { - edgeIdLookup.current.set(edge.source, edgeIdLookup.current.get(edge.source)?.add(id) || new Set([id])); - edgeIdLookup.current.set(edge.target, edgeIdLookup.current.get(edge.target)?.add(id) || new Set([id])); - } const { x, y } = getEventPosition(event.nativeEvent, containerBounds.current); @@ -161,8 +162,16 @@ export function Pane({ }; const onPointerMove = (event: ReactPointerEvent): void => { - const { userSelectionRect, edgeLookup, transform, nodeLookup, triggerNodeChanges, triggerEdgeChanges } = - store.getState(); + const { + userSelectionRect, + transform, + nodeLookup, + edgeLookup, + connectionLookup, + triggerNodeChanges, + triggerEdgeChanges, + defaultEdgeOptions, + } = store.getState(); if (!containerBounds.current || !userSelectionRect) { return; @@ -182,38 +191,37 @@ export function Pane({ height: Math.abs(mouseY - startY), }; - const selectedNodes = getNodesInside( - nodeLookup, - nextUserSelectRect, - transform, - selectionMode === SelectionMode.Partial, - true + const prevSelectedNodeIds = selectedNodeIds.current; + const prevSelectedEdgeIds = selectedEdgeIds.current; + + selectedNodeIds.current = new Set( + getNodesInside(nodeLookup, nextUserSelectRect, transform, selectionMode === SelectionMode.Partial, true).map( + (node) => node.id + ) ); - const selectedEdgeIds = new Set(); - const selectedNodeIds = new Set(); + let edgesSelectable = defaultEdgeOptions?.selectable ?? true; + selectedEdgeIds.current = new Set(); - for (const selectedNode of selectedNodes) { - selectedNodeIds.add(selectedNode.id); - - const edgeIds = edgeIdLookup.current.get(selectedNode.id); - - if (edgeIds) { - for (const edgeId of edgeIds) { - selectedEdgeIds.add(edgeId); + // We look for all edges connected to the selected nodes + for (let nodeId of selectedNodeIds.current) { + let connections = connectionLookup.get(nodeId); + if (!connections) continue; + for (let { edgeId } of connections.values()) { + let edge = edgeLookup.get(edgeId); + if (edge && (edge.selectable ?? edgesSelectable)) { + selectedEdgeIds.current.add(edgeId); } } } - if (prevSelectedNodesCount.current !== selectedNodeIds.size) { - prevSelectedNodesCount.current = selectedNodeIds.size; - const changes = getSelectionChanges(nodeLookup, selectedNodeIds, true) as NodeChange[]; + if (areSetsEqual(prevSelectedNodeIds, selectedNodeIds.current)) { + const changes = getSelectionChanges(nodeLookup, selectedNodeIds.current, true) as NodeChange[]; triggerNodeChanges(changes); } - if (prevSelectedEdgesCount.current !== selectedEdgeIds.size) { - prevSelectedEdgesCount.current = selectedEdgeIds.size; - const changes = getSelectionChanges(edgeLookup, selectedEdgeIds) as EdgeChange[]; + if (areSetsEqual(prevSelectedEdgeIds, selectedEdgeIds.current)) { + const changes = getSelectionChanges(edgeLookup, selectedEdgeIds.current) as EdgeChange[]; triggerEdgeChanges(changes); } @@ -231,17 +239,18 @@ export function Pane({ (event.target as Element)?.releasePointerCapture?.(event.pointerId); const { userSelectionRect } = store.getState(); + // We only want to trigger click functions when in selection mode if // the user did not move the mouse. if (!userSelectionActive && userSelectionRect && event.target === container.current) { onClick?.(event); } - if (prevSelectedNodesCount.current > 0) { - store.setState({ nodesSelectionActive: true }); - } - - resetUserSelection(); + store.setState({ + userSelectionActive: false, + userSelectionRect: null, + nodesSelectionActive: selectedNodeIds.current.size > 0, + }); onSelectionEnd?.(event); // If the user kept holding the selectionKey during the selection, From 4947f683b7530f8e6684865ab53ea38633de0f4d Mon Sep 17 00:00:00 2001 From: peterkogo Date: Wed, 8 Jan 2025 16:09:20 +0100 Subject: [PATCH 2/4] Move areSetsEqual to system --- ...ct-fans-grow.md => twelve-bottles-turn.md} | 1 + packages/react/src/container/Pane/index.tsx | 23 +++++++------------ packages/system/src/utils/general.ts | 14 +++++++++++ 3 files changed, 23 insertions(+), 15 deletions(-) rename .changeset/{perfect-fans-grow.md => twelve-bottles-turn.md} (85%) diff --git a/.changeset/perfect-fans-grow.md b/.changeset/twelve-bottles-turn.md similarity index 85% rename from .changeset/perfect-fans-grow.md rename to .changeset/twelve-bottles-turn.md index 2d4c738e..89b28a52 100644 --- a/.changeset/perfect-fans-grow.md +++ b/.changeset/twelve-bottles-turn.md @@ -1,5 +1,6 @@ --- '@xyflow/react': patch +'@xyflow/system': patch --- Optimize selections and take into account if edges connected to selected nodes are actually selectable. diff --git a/packages/react/src/container/Pane/index.tsx b/packages/react/src/container/Pane/index.tsx index 99d81da3..3b7c94bb 100644 --- a/packages/react/src/container/Pane/index.tsx +++ b/packages/react/src/container/Pane/index.tsx @@ -10,7 +10,14 @@ import { } from 'react'; import { shallow } from 'zustand/shallow'; import cc from 'classcat'; -import { getNodesInside, getEventPosition, SelectionMode, type NodeChange, type EdgeChange } from '@xyflow/system'; +import { + getNodesInside, + getEventPosition, + SelectionMode, + areSetsEqual, + type NodeChange, + type EdgeChange, +} from '@xyflow/system'; import { UserSelection } from '../../components/UserSelection'; import { containerStyle } from '../../styles/utils'; @@ -39,20 +46,6 @@ type PaneProps = { > >; -function areSetsEqual(a: Set, b: Set) { - if (a.size !== b.size) { - return false; - } - - for (const item of a) { - if (!b.has(item)) { - return false; - } - } - - return true; -} - const wrapHandler = ( handler: React.MouseEventHandler | undefined, containerRef: React.MutableRefObject diff --git a/packages/system/src/utils/general.ts b/packages/system/src/utils/general.ts index 088209a3..7f631a59 100644 --- a/packages/system/src/utils/general.ts +++ b/packages/system/src/utils/general.ts @@ -263,3 +263,17 @@ export function evaluateAbsolutePosition( return positionAbsolute; } + +export function areSetsEqual(a: Set, b: Set) { + if (a.size !== b.size) { + return false; + } + + for (const item of a) { + if (!b.has(item)) { + return false; + } + } + + return true; +} From 228ea770a327fb613d96f74283492879c1710a82 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Wed, 8 Jan 2025 17:01:57 +0100 Subject: [PATCH 3/4] let let be const --- packages/react/src/container/Pane/index.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/react/src/container/Pane/index.tsx b/packages/react/src/container/Pane/index.tsx index 3b7c94bb..ac66b665 100644 --- a/packages/react/src/container/Pane/index.tsx +++ b/packages/react/src/container/Pane/index.tsx @@ -193,15 +193,15 @@ export function Pane({ ) ); - let edgesSelectable = defaultEdgeOptions?.selectable ?? true; selectedEdgeIds.current = new Set(); + const edgesSelectable = defaultEdgeOptions?.selectable ?? true; // We look for all edges connected to the selected nodes - for (let nodeId of selectedNodeIds.current) { - let connections = connectionLookup.get(nodeId); + for (const nodeId of selectedNodeIds.current) { + const connections = connectionLookup.get(nodeId); if (!connections) continue; - for (let { edgeId } of connections.values()) { - let edge = edgeLookup.get(edgeId); + for (const { edgeId } of connections.values()) { + const edge = edgeLookup.get(edgeId); if (edge && (edge.selectable ?? edgesSelectable)) { selectedEdgeIds.current.add(edgeId); } From 748c140146f94303393473c4ed2f0e85e66f2ea0 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Wed, 8 Jan 2025 17:12:12 +0100 Subject: [PATCH 4/4] fixes --- packages/react/src/container/Pane/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react/src/container/Pane/index.tsx b/packages/react/src/container/Pane/index.tsx index ac66b665..3432247b 100644 --- a/packages/react/src/container/Pane/index.tsx +++ b/packages/react/src/container/Pane/index.tsx @@ -118,7 +118,7 @@ export function Pane({ const onWheel = onPaneScroll ? (event: React.WheelEvent) => onPaneScroll(event) : undefined; const onPointerDown = (event: ReactPointerEvent): void => { - const { resetSelectedElements, domNode, edgeLookup } = store.getState(); + const { resetSelectedElements, domNode } = store.getState(); containerBounds.current = domNode?.getBoundingClientRect(); if ( @@ -208,12 +208,12 @@ export function Pane({ } } - if (areSetsEqual(prevSelectedNodeIds, selectedNodeIds.current)) { + if (!areSetsEqual(prevSelectedNodeIds, selectedNodeIds.current)) { const changes = getSelectionChanges(nodeLookup, selectedNodeIds.current, true) as NodeChange[]; triggerNodeChanges(changes); } - if (areSetsEqual(prevSelectedEdgeIds, selectedEdgeIds.current)) { + if (!areSetsEqual(prevSelectedEdgeIds, selectedEdgeIds.current)) { const changes = getSelectionChanges(edgeLookup, selectedEdgeIds.current) as EdgeChange[]; triggerEdgeChanges(changes); }