Merge pull request #5572 from xyflow/fix/pane-events

Fix/pane events
This commit is contained in:
Moritz Klack
2025-10-28 16:14:54 +01:00
committed by GitHub
23 changed files with 190 additions and 153 deletions

View File

@@ -0,0 +1,5 @@
---
'@xyflow/svelte': patch
---
Fix showing selection box when no nodes are selected

View File

@@ -0,0 +1,7 @@
---
'@xyflow/react': patch
'@xyflow/svelte': patch
'@xyflow/system': patch
---
Fix onPaneClick events being suppressed when selectionOnDrag=true

View File

@@ -0,0 +1,5 @@
---
'@xyflow/svelte': patch
---
Remove focus outline from selection box

View File

@@ -30,10 +30,11 @@ const BasicFlow = () => {
<ReactFlow
defaultNodes={initialNodes}
defaultEdges={initialEdges}
selectionOnDrag
selectionOnDrag={true}
selectionMode={SelectionMode.Partial}
panOnDrag={panOnDrag}
panOnScroll
paneClickDistance={100}
zoomActivationKeyCode="Meta"
multiSelectionKeyCode={MULTI_SELECT_KEY}
onPaneContextMenu={onPaneContextMenu}
@@ -43,6 +44,12 @@ const BasicFlow = () => {
onMoveStart={onMoveStart}
onMove={onMove}
onMoveEnd={onMoveEnd}
onPaneClick={(e) => console.log('pane click', e)}
onSelectionStart={(e) => console.log('on selection start', e)}
onSelectionEnd={(e) => console.log('on selection end', e)}
onPointerDown={(e) => console.log('pointer down', e)}
onPointerUp={(e) => console.log('pointer up', e)}
onClick={(e) => console.log('click', e)}
>
<Background variant={BackgroundVariant.Cross} />
<Controls />

View File

@@ -9,11 +9,6 @@
import '@xyflow/svelte/dist/style.css';
const onPaneContextMenu = (e: any) => {
e.preventDefault();
console.log('context menu');
};
const panOnDrag = [1, 2];
const onmovestart = (e: any) => console.log('move start', e);
@@ -46,10 +41,14 @@
selectionMode={SelectionMode.Partial}
selectionOnDrag
panOnScroll
paneClickDistance={100}
{panOnDrag}
{onmovestart}
{onmove}
{onmoveend}
onpaneclick={(e) => console.log('on pane click', e)}
onselectionend={(e) => console.log('on selection end', e)}
onselectionstart={(e) => console.log('on selection start', e)}
>
<Controls />
<Background variant={BackgroundVariant.Dots} />

View File

@@ -69,7 +69,6 @@ const reactFlowFieldsToTrack = [
'onBeforeDelete',
'debug',
'autoPanSpeed',
'paneClickDistance',
'ariaLabelConfig',
] as const;
@@ -93,7 +92,6 @@ const selector = (s: ReactFlowState) => ({
setNodeExtent: s.setNodeExtent,
reset: s.reset,
setDefaultNodesAndEdges: s.setDefaultNodesAndEdges,
setPaneClickDistance: s.setPaneClickDistance,
});
const initPrevValues = {
@@ -109,7 +107,6 @@ const initPrevValues = {
elementsSelectable: true,
noPanClassName: 'nopan',
rfId: '1',
paneClickDistance: 0,
};
export function StoreUpdater<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
@@ -124,7 +121,6 @@ export function StoreUpdater<NodeType extends Node = Node, EdgeType extends Edge
setNodeExtent,
reset,
setDefaultNodesAndEdges,
setPaneClickDistance,
} = useStore(selector, shallow);
const store = useStoreApi<NodeType, EdgeType>();
@@ -155,7 +151,6 @@ export function StoreUpdater<NodeType extends Node = Node, EdgeType extends Edge
else if (fieldName === 'maxZoom') setMaxZoom(fieldValue as number);
else if (fieldName === 'translateExtent') setTranslateExtent(fieldValue as CoordinateExtent);
else if (fieldName === 'nodeExtent') setNodeExtent(fieldValue as CoordinateExtent);
else if (fieldName === 'paneClickDistance') setPaneClickDistance(fieldValue as number);
else if (fieldName === 'ariaLabelConfig')
store.setState({ ariaLabelConfig: mergeAriaLabelConfig(fieldValue as AriaLabelConfig) });
// Renamed fields

View File

@@ -105,6 +105,7 @@ function FlowRendererComponent<NodeType extends Node = Node>({
onViewportChange={onViewportChange}
isControlledViewport={isControlledViewport}
paneClickDistance={paneClickDistance}
selectionOnDrag={_selectionOnDrag}
>
<Pane
onSelectionStart={onSelectionStart}
@@ -119,6 +120,7 @@ function FlowRendererComponent<NodeType extends Node = Node>({
isSelecting={!!isSelecting}
selectionMode={selectionMode}
selectionKeyPressed={selectionKeyPressed}
paneClickDistance={paneClickDistance}
selectionOnDrag={_selectionOnDrag}
>
{children}

View File

@@ -1,19 +1,15 @@
import {
useRef,
type MouseEventHandler,
type MutableRefObject,
type MouseEvent as ReactMouseEvent,
type PointerEvent as ReactPointerEvent,
type WheelEvent as ReactWheelEvent,
type ReactNode,
} from 'react';
import { shallow } from 'zustand/shallow';
import cc from 'classcat';
import {
getNodesInside,
getEventPosition,
SelectionMode,
areSetsEqual,
type NodeChange,
type EdgeChange,
} from '@xyflow/system';
import { getNodesInside, getEventPosition, SelectionMode, areSetsEqual } from '@xyflow/system';
import { UserSelection } from '../../components/UserSelection';
import { containerStyle } from '../../styles/utils';
@@ -25,6 +21,7 @@ type PaneProps = {
isSelecting: boolean;
selectionKeyPressed: boolean;
children: ReactNode;
paneClickDistance: number;
} & Partial<
Pick<
ReactFlowProps,
@@ -43,9 +40,9 @@ type PaneProps = {
>;
const wrapHandler = (
handler: React.MouseEventHandler | undefined,
containerRef: React.MutableRefObject<HTMLDivElement | null>
): React.MouseEventHandler => {
handler: MouseEventHandler | undefined,
containerRef: MutableRefObject<HTMLDivElement | null>
): MouseEventHandler => {
return (event: ReactMouseEvent) => {
if (event.target !== containerRef.current) {
return;
@@ -66,6 +63,7 @@ export function Pane({
selectionKeyPressed,
selectionMode = SelectionMode.Full,
panOnDrag,
paneClickDistance,
selectionOnDrag,
onSelectionStart,
onSelectionEnd,
@@ -79,17 +77,15 @@ export function Pane({
}: PaneProps) {
const store = useStoreApi();
const { userSelectionActive, elementsSelectable, dragging, connectionInProgress } = useStore(selector, shallow);
const hasActiveSelection = elementsSelectable && (isSelecting || userSelectionActive);
const isSelectionEnabled = elementsSelectable && (isSelecting || userSelectionActive);
const container = useRef<HTMLDivElement | null>(null);
const containerBounds = useRef<DOMRect>();
const selectedNodeIds = useRef<Set<string>>(new Set());
const selectedEdgeIds = useRef<Set<string>>(new Set());
// Used to prevent click events when the user lets go of the selectionKey during a selection
const selectionInProgress = useRef<boolean>(false);
const selectionStarted = useRef<boolean>(false);
const onClick = (event: ReactMouseEvent) => {
// We prevent click events when the user let go of the selectionKey during a selection
@@ -113,17 +109,13 @@ export function Pane({
onPaneContextMenu?.(event);
};
const onWheel = onPaneScroll ? (event: React.WheelEvent) => onPaneScroll(event) : undefined;
const onWheel = onPaneScroll ? (event: ReactWheelEvent) => onPaneScroll(event) : undefined;
const onClickCapture = (event: ReactMouseEvent) => {
const isSelectionOnDragActive =
(selectionOnDrag && container.current === event.target) || !selectionOnDrag || selectionKeyPressed;
if (!isSelectionOnDragActive) {
return;
if (selectionInProgress.current) {
event.stopPropagation();
selectionInProgress.current = false;
}
event.stopPropagation();
};
// We are using capture here in order to prevent other pointer events
@@ -131,35 +123,23 @@ export function Pane({
const onPointerDownCapture = (event: ReactPointerEvent): void => {
const { resetSelectedElements, domNode } = store.getState();
containerBounds.current = domNode?.getBoundingClientRect();
if (!containerBounds.current) return;
const isNoKeyEvent = event.target !== container.current && !!(event.target as HTMLElement).closest('.nokey');
const isSelectionActive =
(selectionOnDrag && container.current === event.target) || !selectionOnDrag || selectionKeyPressed;
const eventTargetIsContainer = event.target === container.current;
// if a child element has the 'nokey' class, we don't want to swallow the event and don't start a selection
const isNoKeyEvent = !eventTargetIsContainer && !!(event.target as HTMLElement).closest('.nokey');
const isSelectionActive = (selectionOnDrag && eventTargetIsContainer) || selectionKeyPressed;
if (
!elementsSelectable ||
!isSelecting ||
event.button !== 0 ||
!containerBounds.current ||
isNoKeyEvent ||
!isSelectionActive ||
!event.isPrimary
) {
if (isNoKeyEvent || !isSelecting || !isSelectionActive || event.button !== 0 || !event.isPrimary) {
return;
}
event.stopPropagation();
event.preventDefault();
(event.target as Partial<Element>)?.setPointerCapture?.(event.pointerId);
selectionStarted.current = true;
selectionInProgress.current = false;
const { x, y } = getEventPosition(event.nativeEvent, containerBounds.current);
resetSelectedElements();
store.setState({
userSelectionRect: {
width: 0,
@@ -171,7 +151,17 @@ export function Pane({
},
});
onSelectionStart?.(event);
if (!eventTargetIsContainer) {
event.stopPropagation();
event.preventDefault();
}
if (paneClickDistance === 0 || selectionKeyPressed) {
resetSelectedElements();
onSelectionStart?.(event);
selectionInProgress.current = true;
}
};
const onPointerMove = (event: ReactPointerEvent): void => {
@@ -184,17 +174,32 @@ export function Pane({
triggerNodeChanges,
triggerEdgeChanges,
defaultEdgeOptions,
resetSelectedElements,
} = store.getState();
if (!containerBounds.current || !userSelectionRect) {
return;
}
selectionInProgress.current = true;
const { x: mouseX, y: mouseY } = getEventPosition(event.nativeEvent, containerBounds.current);
const { startX, startY } = userSelectionRect;
if (
!selectionInProgress.current &&
event.target === container.current &&
!selectionKeyPressed &&
paneClickDistance > 0
) {
const distance = Math.hypot(mouseX - startX, mouseY - startY);
if (distance <= paneClickDistance) {
return;
}
resetSelectedElements();
onSelectionStart?.(event);
}
selectionInProgress.current = true;
const nextUserSelectRect = {
startX,
startY,
@@ -229,12 +234,12 @@ export function Pane({
}
if (!areSetsEqual(prevSelectedNodeIds, selectedNodeIds.current)) {
const changes = getSelectionChanges(nodeLookup, selectedNodeIds.current, true) as NodeChange[];
const changes = getSelectionChanges(nodeLookup, selectedNodeIds.current, true);
triggerNodeChanges(changes);
}
if (!areSetsEqual(prevSelectedEdgeIds, selectedEdgeIds.current)) {
const changes = getSelectionChanges(edgeLookup, selectedEdgeIds.current) as EdgeChange[];
const changes = getSelectionChanges(edgeLookup, selectedEdgeIds.current);
triggerEdgeChanges(changes);
}
@@ -246,18 +251,17 @@ export function Pane({
};
const onPointerUp = (event: ReactPointerEvent) => {
if (event.button !== 0 || !selectionStarted.current) {
if (event.button !== 0) {
return;
}
(event.target as Partial<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) {
if (!userSelectionActive && event.target === container.current && store.getState().userSelectionRect) {
onClick?.(event);
}
@@ -266,17 +270,10 @@ export function Pane({
userSelectionRect: null,
nodesSelectionActive: selectedNodeIds.current.size > 0,
});
onSelectionEnd?.(event);
/*
* If the user kept holding the selectionKey during the selection,
* we need to reset the selectionInProgress, so the next click event is not prevented
*/
if (selectionKeyPressed || selectionOnDrag) {
selectionInProgress.current = false;
if (selectionInProgress.current) {
onSelectionEnd?.(event);
}
selectionStarted.current = false;
};
const draggable = panOnDrag === true || (Array.isArray(panOnDrag) && panOnDrag.includes(0));
@@ -284,14 +281,14 @@ export function Pane({
return (
<div
className={cc(['react-flow__pane', { draggable, dragging, selection: isSelecting }])}
onClick={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
onClick={isSelectionEnabled ? undefined : wrapHandler(onClick, container)}
onContextMenu={wrapHandler(onContextMenu, container)}
onWheel={wrapHandler(onWheel, container)}
onPointerEnter={hasActiveSelection ? undefined : onPaneMouseEnter}
onPointerMove={hasActiveSelection ? onPointerMove : onPaneMouseMove}
onPointerUp={hasActiveSelection ? onPointerUp : undefined}
onPointerDownCapture={hasActiveSelection ? onPointerDownCapture : undefined}
onClickCapture={hasActiveSelection ? onClickCapture : undefined}
onPointerEnter={isSelectionEnabled ? undefined : onPaneMouseEnter}
onPointerMove={isSelectionEnabled ? onPointerMove : onPaneMouseMove}
onPointerUp={isSelectionEnabled ? onPointerUp : undefined}
onPointerDownCapture={isSelectionEnabled ? onPointerDownCapture : undefined}
onClickCapture={isSelectionEnabled ? onClickCapture : undefined}
onPointerLeave={onPaneMouseLeave}
ref={container}
style={containerStyle}

View File

@@ -309,7 +309,6 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
nodeDragThreshold={nodeDragThreshold}
connectionDragThreshold={connectionDragThreshold}
onBeforeDelete={onBeforeDelete}
paneClickDistance={paneClickDistance}
debug={debug}
ariaLabelConfig={ariaLabelConfig}
/>

View File

@@ -12,12 +12,7 @@ import type { ReactFlowState } from '../../types';
type ZoomPaneProps = Omit<
FlowRendererProps,
| 'deleteKeyCode'
| 'selectionKeyCode'
| 'multiSelectionKeyCode'
| 'noDragClassName'
| 'disableKeyboardA11y'
| 'selectionOnDrag'
'deleteKeyCode' | 'selectionKeyCode' | 'multiSelectionKeyCode' | 'noDragClassName' | 'disableKeyboardA11y'
> & {
isControlledViewport: boolean;
};
@@ -49,6 +44,7 @@ export function ZoomPane({
onViewportChange,
isControlledViewport,
paneClickDistance,
selectionOnDrag,
}: ZoomPaneProps) {
const store = useStoreApi();
const zoomPane = useRef<HTMLDivElement>(null);
@@ -77,7 +73,6 @@ export function ZoomPane({
maxZoom,
translateExtent,
viewport: defaultViewport,
paneClickDistance,
onDraggingChange: (paneDragging: boolean) => store.setState({ paneDragging }),
onPanZoomStart: (event, vp) => {
const { onViewportChangeStart, onMoveStart } = store.getState();
@@ -128,6 +123,8 @@ export function ZoomPane({
lib,
onTransformChange,
connectionInProgress,
selectionOnDrag,
paneClickDistance,
});
}, [
onPaneContextMenu,
@@ -146,6 +143,8 @@ export function ZoomPane({
lib,
onTransformChange,
connectionInProgress,
selectionOnDrag,
paneClickDistance,
]);
return (

View File

@@ -312,9 +312,6 @@ const createStore = ({
set({ translateExtent });
},
setPaneClickDistance: (clickDistance) => {
get().panZoom?.setClickDistance(clickDistance);
},
resetSelectedElements: () => {
const { edges, nodes, triggerNodeChanges, triggerEdgeChanges, elementsSelectable } = get();

View File

@@ -175,7 +175,6 @@ export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
triggerEdgeChanges: (changes: EdgeChange<EdgeType>[]) => void;
panBy: PanBy;
setCenter: SetCenter;
setPaneClickDistance: (distance: number) => void;
};
export type ReactFlowState<NodeType extends Node = Node, EdgeType extends Edge = Edge> = ReactFlowStore<

View File

@@ -35,6 +35,7 @@ type ZoomParams = {
userSelectionActive: boolean;
lib: string;
paneClickDistance: number;
selectionOnDrag?: boolean;
onTransformChange: (transform: Transform) => void;
onDraggingChange: (dragging: boolean) => void;
connectionInProgress: boolean;
@@ -49,7 +50,6 @@ export default function zoom(domNode: Element, params: ZoomParams) {
onPanZoom,
onPanZoomEnd,
translateExtent,
paneClickDistance,
setPanZoomInstance,
onDraggingChange,
onTransformChange
@@ -61,7 +61,6 @@ export default function zoom(domNode: Element, params: ZoomParams) {
maxZoom,
translateExtent,
viewport: initialViewport,
paneClickDistance,
onPanZoom,
onPanZoomStart,
onPanZoomEnd,

View File

@@ -31,7 +31,12 @@
if (store.selectionRectMode === 'nodes') {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
store.nodes;
return getInternalNodesBounds(store.nodeLookup, { filter: (node) => !!node.selected });
const nodeBounds = getInternalNodesBounds(store.nodeLookup, {
filter: (node) => !!node.selected
});
if (nodeBounds.width > 0 && nodeBounds.height > 0) {
return nodeBounds;
}
}
return null;
});
@@ -92,4 +97,9 @@
z-index: 2000;
pointer-events: all;
}
.svelte-flow__selection-wrapper:focus,
.svelte-flow__selection-wrapper:focus-visible {
outline: none;
}
</style>

View File

@@ -47,6 +47,7 @@
let {
store = $bindable(),
panOnDrag = true,
paneClickDistance = 1,
selectionOnDrag,
onpaneclick,
onpanecontextmenu,
@@ -67,61 +68,44 @@
let panOnDragActive = $derived(store.panActivationKeyPressed || panOnDrag);
let isSelecting = $derived(
store.selectionKeyPressed ||
store.selectionRect ||
!!store.selectionRect ||
(selectionOnDrag && panOnDragActive !== true)
);
let hasActiveSelection = $derived(
let isSelectionEnabled = $derived(
store.elementsSelectable && (isSelecting || store.selectionRectMode === 'user')
);
// Used to prevent click events when the user lets go of the selectionKey during a selection
let selectionInProgress = false;
function onClick(event: MouseEvent) {
// We prevent click events when the user let go of the selectionKey during a selection
// We also prevent click events when a connection is in progress
if (selectionInProgress || store.connection.inProgress) {
selectionInProgress = false;
return;
}
onpaneclick?.({ event });
store.unselectNodesAndEdges();
store.selectionRectMode = null;
}
// We start the selection process when the user clicks down on the pane
function onPointerDownCapture(event: PointerEvent) {
containerBounds = container?.getBoundingClientRect();
if (!containerBounds) return;
const eventTargetIsContainer = event.target === container;
const isNoKeyEvent =
event.target !== container && !!(event.target as HTMLElement).closest('.nokey');
!eventTargetIsContainer && !!(event.target as HTMLElement).closest('.nokey');
const isSelectionActive =
(selectionOnDrag && container === event.target) ||
!selectionOnDrag ||
store.selectionKeyPressed;
(selectionOnDrag && eventTargetIsContainer) || store.selectionKeyPressed;
if (
!store.elementsSelectable ||
!isSelecting ||
event.button !== 0 ||
!containerBounds ||
isNoKeyEvent ||
!isSelecting ||
!isSelectionActive ||
event.button !== 0 ||
!event.isPrimary
) {
return;
}
event.stopPropagation();
event.preventDefault();
(event.target as Partial<Element>)?.setPointerCapture?.(event.pointerId);
const { x, y } = getEventPosition(event, containerBounds);
selectionInProgress = false;
store.unselectNodesAndEdges();
const { x, y } = getEventPosition(event, containerBounds);
store.selectionRect = {
width: 0,
@@ -132,7 +116,17 @@
y
};
onselectionstart?.(event);
if (!eventTargetIsContainer) {
event.stopPropagation();
event.preventDefault();
}
if (paneClickDistance === 0 || store.selectionKeyPressed) {
store.unselectNodesAndEdges();
onselectionstart?.(event);
selectionInProgress = true;
}
}
function onPointerMove(event: PointerEvent) {
@@ -140,11 +134,25 @@
return;
}
selectionInProgress = true;
const mousePos = getEventPosition(event, containerBounds);
const { startX = 0, startY = 0 } = store.selectionRect;
if (
!selectionInProgress &&
event.target === container &&
!store.selectionKeyPressed &&
paneClickDistance > 0
) {
const distance = Math.hypot(mousePos.x - startX, mousePos.y - startY);
if (distance <= paneClickDistance) {
return;
}
store.unselectNodesAndEdges();
onselectionstart?.(event);
}
selectionInProgress = true;
const nextUserSelectRect = {
...store.selectionRect,
x: mousePos.x < startX ? mousePos.x : startX,
@@ -203,22 +211,20 @@
// We only want to trigger click functions when in selection mode if
// the user did not move the mouse.
if (!isSelecting && store.selectionRectMode === 'user' && event.target === container) {
if (!selectionInProgress && event.target === container) {
onClick?.(event);
}
store.selectionRect = null;
if (selectedNodeIds.size > 0) {
store.selectionRectMode = 'nodes';
if (selectionInProgress) {
store.selectionRectMode = selectedNodeIds.size > 0 ? 'nodes' : null;
}
// If the user kept holding the selectionKey during the selection,
// we need to reset the selectionInProgress, so the next click event is not prevented
if (store.selectionKeyPressed) {
selectionInProgress = false;
if (selectionInProgress) {
onselectionend?.(event);
}
onselectionend?.(event);
}
const onContextMenu = (event: MouseEvent) => {
@@ -231,17 +237,25 @@
};
const onClickCapture = (event: MouseEvent) => {
const isSelectionActive =
(selectionOnDrag && container === event.target) ||
!selectionOnDrag ||
store.selectionKeyPressed;
if (selectionInProgress) {
event.stopPropagation();
selectionInProgress = false;
}
};
if (!isSelectionActive) {
function onClick(event: MouseEvent) {
// We prevent click events when the user let go of the selectionKey during a selection
// We also prevent click events when a connection is in progress
if (selectionInProgress || store.connection.inProgress) {
selectionInProgress = false;
return;
}
event.stopPropagation();
};
onpaneclick?.({ event });
store.unselectNodesAndEdges();
store.selectionRectMode = null;
store.selectionRect = null;
}
</script>
<!-- svelte-ignore a11y_no_static_element_interactions -->
@@ -252,12 +266,12 @@
class:draggable={panOnDrag === true || (Array.isArray(panOnDrag) && panOnDrag.includes(0))}
class:dragging={store.dragging}
class:selection={isSelecting}
onclick={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
onpointerdowncapture={hasActiveSelection ? onPointerDownCapture : undefined}
onpointermove={hasActiveSelection ? onPointerMove : undefined}
onpointerup={hasActiveSelection ? onPointerUp : undefined}
onclick={isSelectionEnabled ? undefined : wrapHandler(onClick, container)}
onpointerdowncapture={isSelectionEnabled ? onPointerDownCapture : undefined}
onpointermove={isSelectionEnabled ? onPointerMove : undefined}
onpointerup={isSelectionEnabled ? onPointerUp : undefined}
oncontextmenu={wrapHandler(onContextMenu, container)}
onclickcapture={hasActiveSelection ? onClickCapture : undefined}
onclickcapture={isSelectionEnabled ? onClickCapture : undefined}
>
{@render children()}
</div>

View File

@@ -5,6 +5,7 @@ import type { SvelteFlowStore } from '$lib/store/types';
export type PaneProps<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
store: SvelteFlowStore<NodeType, EdgeType>;
panOnDrag?: boolean | number[];
paneClickDistance: number;
selectionOnDrag?: boolean;
onselectionstart?: (event: PointerEvent) => void;
onselectionend?: (event: PointerEvent) => void;

View File

@@ -156,6 +156,7 @@
{panOnScrollSpeed}
{panOnDrag}
{paneClickDistance}
{selectionOnDrag}
{onmovestart}
{onmove}
{onmoveend}
@@ -168,6 +169,7 @@
{onselectionstart}
{onselectionend}
{panOnDrag}
{paneClickDistance}
{selectionOnDrag}
>
<ViewportComponent bind:store>

View File

@@ -16,6 +16,7 @@
panOnScroll,
panOnScrollSpeed,
paneClickDistance,
selectionOnDrag,
onmovestart,
onmove,
onmoveend,
@@ -69,6 +70,7 @@
translateExtent: store.translateExtent,
lib: 'svelte',
paneClickDistance,
selectionOnDrag,
onTransformChange: (transform: Transform) => {
store.viewport = { x: transform[0], y: transform[1], zoom: transform[2] };
},

View File

@@ -14,6 +14,7 @@ export type ZoomProps<NodeType extends Node = Node, EdgeType extends Edge = Edge
panOnScroll: boolean;
panOnDrag: boolean | number[];
paneClickDistance: number;
selectionOnDrag?: boolean;
onmove?: OnMove;
onmovestart?: OnMoveStart;
onmoveend?: OnMoveEnd;

View File

@@ -191,10 +191,6 @@ export function createStore<NodeType extends Node = Node, EdgeType extends Edge
}
}
function setPaneClickDistance(distance: number) {
store.panZoom?.setClickDistance(distance);
}
function deselect<T extends Node | Edge>(
elements: T[],
elementsToDeselect: Set<string> | null = null
@@ -393,7 +389,6 @@ export function createStore<NodeType extends Node = Node, EdgeType extends Edge
setMinZoom,
setMaxZoom,
setTranslateExtent,
setPaneClickDistance,
unselectNodesAndEdges,
addSelectedNodes,
addSelectedEdges,

View File

@@ -23,7 +23,6 @@ export type SvelteFlowStoreActions<NodeType extends Node = Node, EdgeType extend
setMinZoom: (minZoom: number) => void;
setMaxZoom: (maxZoom: number) => void;
setTranslateExtent: (extent: CoordinateExtent) => void;
setPaneClickDistance: (distance: number) => void;
fitView: (options?: FitViewOptions) => Promise<boolean>;
setCenter: SetCenter;
updateNodePositions: UpdateNodePositions;

View File

@@ -9,7 +9,6 @@ export type PanZoomParams = {
domNode: Element;
minZoom: number;
maxZoom: number;
paneClickDistance: number;
viewport: Viewport;
translateExtent: CoordinateExtent;
onDraggingChange: OnDraggingChange;
@@ -43,6 +42,8 @@ export type PanZoomUpdateOptions = {
lib: string;
onTransformChange: OnTransformChange;
connectionInProgress: boolean;
paneClickDistance: number;
selectionOnDrag?: boolean;
};
export type PanZoomInstance = {

View File

@@ -37,7 +37,6 @@ export function XYPanZoom({
domNode,
minZoom,
maxZoom,
paneClickDistance,
translateExtent,
viewport,
onPanZoom,
@@ -55,10 +54,7 @@ export function XYPanZoom({
isPanScrolling: false,
};
const bbox = domNode.getBoundingClientRect();
const d3ZoomInstance = zoom()
.clickDistance(!isNumeric(paneClickDistance) || paneClickDistance < 0 ? 0 : paneClickDistance)
.scaleExtent([minZoom, maxZoom])
.translateExtent(translateExtent);
const d3ZoomInstance = zoom().scaleExtent([minZoom, maxZoom]).translateExtent(translateExtent);
const d3Selection = select(domNode).call(d3ZoomInstance);
setViewportConstrained(
@@ -109,6 +105,8 @@ export function XYPanZoom({
lib,
onTransformChange,
connectionInProgress,
paneClickDistance,
selectionOnDrag,
}: PanZoomUpdateOptions) {
if (userSelectionActive && !zoomPanValues.isZoomingOrPanning) {
destroy();
@@ -116,6 +114,10 @@ export function XYPanZoom({
const isPanOnScroll = panOnScroll && !zoomActivationKeyPressed && !userSelectionActive;
d3ZoomInstance.clickDistance(
selectionOnDrag ? Infinity : !isNumeric(paneClickDistance) || paneClickDistance < 0 ? 0 : paneClickDistance
);
const wheelHandler = isPanOnScroll
? createPanOnScrollHandler({
zoomPanValues,