5
.changeset/eight-rice-laugh.md
Normal file
5
.changeset/eight-rice-laugh.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@xyflow/svelte': patch
|
||||
---
|
||||
|
||||
Fix showing selection box when no nodes are selected
|
||||
7
.changeset/sour-jokes-argue.md
Normal file
7
.changeset/sour-jokes-argue.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@xyflow/react': patch
|
||||
'@xyflow/svelte': patch
|
||||
'@xyflow/system': patch
|
||||
---
|
||||
|
||||
Fix onPaneClick events being suppressed when selectionOnDrag=true
|
||||
5
.changeset/thirty-goats-invent.md
Normal file
5
.changeset/thirty-goats-invent.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@xyflow/svelte': patch
|
||||
---
|
||||
|
||||
Remove focus outline from selection box
|
||||
@@ -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 />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -312,9 +312,6 @@ const createStore = ({
|
||||
|
||||
set({ translateExtent });
|
||||
},
|
||||
setPaneClickDistance: (clickDistance) => {
|
||||
get().panZoom?.setClickDistance(clickDistance);
|
||||
},
|
||||
resetSelectedElements: () => {
|
||||
const { edges, nodes, triggerNodeChanges, triggerEdgeChanges, elementsSelectable } = get();
|
||||
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -156,6 +156,7 @@
|
||||
{panOnScrollSpeed}
|
||||
{panOnDrag}
|
||||
{paneClickDistance}
|
||||
{selectionOnDrag}
|
||||
{onmovestart}
|
||||
{onmove}
|
||||
{onmoveend}
|
||||
@@ -168,6 +169,7 @@
|
||||
{onselectionstart}
|
||||
{onselectionend}
|
||||
{panOnDrag}
|
||||
{paneClickDistance}
|
||||
{selectionOnDrag}
|
||||
>
|
||||
<ViewportComponent bind:store>
|
||||
|
||||
@@ -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] };
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user