feat(react,svelte): add nodeDragThreshold

This commit is contained in:
moklick
2023-09-26 13:08:27 +02:00
parent b1aeac0828
commit 348f311fc7
15 changed files with 94 additions and 47 deletions

View File

@@ -1,9 +1,6 @@
import { MouseEvent, useCallback } from 'react';
import { ReactFlow, addEdge, Node, Connection, Edge, useNodesState, useEdgesState } from '@xyflow/react';
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const nodesA: Node[] = [
{
id: '1a',
@@ -83,7 +80,10 @@ const edgesB: Edge[] = [
{ id: 'e4b', source: 'inputb', target: '4b' },
];
const onNodeDragStart = (_: MouseEvent, node: Node) => console.log('drag start', node);
const onNodeDrag = (_: MouseEvent, node: Node) => console.log('drag', node.position);
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const BasicFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(nodesA);
@@ -99,8 +99,10 @@ const BasicFlow = () => {
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
onNodeDragStart={onNodeDragStart}
onNodeDrag={onNodeDrag}
onNodeDragStop={onNodeDragStop}
nodeDragThreshold={10}
>
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
<button

View File

@@ -145,7 +145,7 @@
]);
</script>
<SvelteFlow {nodes} {edges} fitView>
<SvelteFlow {nodes} {edges} fitView nodeDragThreshold={2}>
<Controls />
<Background variant={BackgroundVariant.Dots} />
<MiniMap />

View File

@@ -56,6 +56,7 @@ type StoreUpdaterProps = Pick<
| 'connectionRadius'
| 'isValidConnection'
| 'selectNodesOnDrag'
| 'nodeDragThreshold'
> & { rfId: string };
const selector = (s: ReactFlowState) => ({
@@ -140,6 +141,7 @@ const StoreUpdater = ({
connectionRadius,
isValidConnection,
selectNodesOnDrag,
nodeDragThreshold,
}: StoreUpdaterProps) => {
const {
setNodes,
@@ -203,6 +205,7 @@ const StoreUpdater = ({
useDirectStoreUpdater('connectionRadius', connectionRadius, store.setState);
useDirectStoreUpdater('isValidConnection', isValidConnection, store.setState);
useDirectStoreUpdater('selectNodesOnDrag', selectNodesOnDrag, store.setState);
useDirectStoreUpdater('nodeDragThreshold', nodeDragThreshold, store.setState);
useStoreUpdater<Node[]>(nodes, setNodes);
useStoreUpdater<Edge[]>(edges, setEdges);

View File

@@ -163,6 +163,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
onError,
style,
id,
nodeDragThreshold,
...rest
},
ref
@@ -292,6 +293,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
connectionRadius={connectionRadius}
isValidConnection={isValidConnection}
selectNodesOnDrag={selectNodesOnDrag}
nodeDragThreshold={nodeDragThreshold}
/>
<SelectionListener onSelectionChange={onSelectionChange} />
{children}

View File

@@ -28,6 +28,7 @@ const initialState: ReactFlowStore = {
paneDragging: false,
noPanClassName: 'nopan',
nodeOrigin: [0, 0],
nodeDragThreshold: 0,
snapGrid: [15, 15],
snapToGrid: false,

View File

@@ -148,6 +148,7 @@ export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & {
connectionRadius?: number;
onError?: OnError;
isValidConnection?: IsValidConnection;
nodeDragThreshold?: number;
};
export type ReactFlowRefType = HTMLDivElement;

View File

@@ -61,6 +61,7 @@ export type ReactFlowStore = {
translateExtent: CoordinateExtent;
nodeExtent: CoordinateExtent;
nodeOrigin: NodeOrigin;
nodeDragThreshold: number;
nodesSelectionActive: boolean;
userSelectionActive: boolean;

View File

@@ -1,5 +1,8 @@
## WIP
## 0.0.22
- add `nodeDragThreshold` prop
- add `fitViewOptions` prop
- add `defaultEdgeOptions` prop
- add `on:edgecontextmenu` event handler prop
- add `connectionMode` prop
- add `attributionPosition` prop

View File

@@ -38,6 +38,7 @@ export default function drag(domNode: Element, params: UseDragParams) {
autoPanOnNodeDrag: get(store.autoPanOnNodeDrag),
nodesDraggable: get(store.nodesDraggable),
selectNodesOnDrag: get(store.selectNodesOnDrag),
nodeDragThreshold: get(store.nodeDragThreshold),
unselectNodesAndEdges: store.unselectNodesAndEdges,
updateNodePositions: store.updateNodePositions,
panBy: store.panBy

View File

@@ -34,6 +34,7 @@
export let panActivationKey: $$Props['panActivationKey'] = undefined;
export let nodesDraggable: $$Props['nodesDraggable'] = undefined;
export let nodesConnectable: $$Props['nodesConnectable'] = undefined;
export let nodeDragThreshold: $$Props['nodeDragThreshold'] = undefined;
export let elementsSelectable: $$Props['elementsSelectable'] = undefined;
export let snapGrid: $$Props['snapGrid'] = undefined;
export let deleteKey: $$Props['deleteKey'] = undefined;
@@ -118,7 +119,8 @@
autoPanOnConnect,
autoPanOnNodeDrag,
onError,
connectionMode
connectionMode,
nodeDragThreshold
};
updateStoreByKeys(store, updatableProps);

View File

@@ -43,6 +43,7 @@ export type SvelteFlowProps = DOMAttributes<HTMLDivElement> & {
fitView?: boolean;
fitViewOptions?: FitViewOptions;
nodeOrigin?: NodeOrigin;
nodeDragThreshold?: number;
minZoom?: number;
maxZoom?: number;
initialViewport?: Viewport;

View File

@@ -63,6 +63,7 @@ export type UpdatableStoreProps = {
autoPanOnNodeDrag?: UnwrapWritable<SvelteFlowStore['autoPanOnNodeDrag']>;
connectionMode?: UnwrapWritable<SvelteFlowStore['connectionMode']>;
onError?: UnwrapWritable<SvelteFlowStore['onError']>;
nodeDragThreshold?: UnwrapWritable<SvelteFlowStore['nodeDragThreshold']>;
};
export function updateStoreByKeys(store: SvelteFlowStore, keys: UpdatableStoreProps) {

View File

@@ -65,6 +65,7 @@ export const getInitialStore = () => ({
minZoom: writable<number>(0.5),
maxZoom: writable<number>(2),
nodeOrigin: writable<NodeOrigin>([0, 0]),
nodeDragThreshold: writable<number>(0),
nodeExtent: writable<CoordinateExtent>(infiniteExtent),
translateExtent: writable<CoordinateExtent>(infiniteExtent),
autoPanOnNodeDrag: writable<boolean>(true),

View File

@@ -1,6 +1,6 @@
{
"name": "@xyflow/system",
"version": "0.0.6",
"version": "0.0.7",
"description": "xyflow core system that powers React Flow and Svelte Flow.",
"keywords": [
"node-based UI",

View File

@@ -44,6 +44,7 @@ type StoreItems = {
autoPanOnNodeDrag: boolean;
nodesDraggable: boolean;
selectNodesOnDrag: boolean;
nodeDragThreshold: number;
panBy: PanBy;
unselectNodesAndEdges: () => void;
onError?: OnError;
@@ -93,6 +94,7 @@ export function XYDrag({
let mousePosition: XYPosition = { x: 0, y: 0 };
let dragEvent: MouseEvent | null = null;
let containerBounds: DOMRect | null = null;
let dragStarted = false;
const d3Selection = select(domNode);
@@ -192,63 +194,84 @@ export function XYDrag({
autoPanId = requestAnimationFrame(autoPan);
}
function startDrag(event: UseDragEvent) {
const {
nodes,
multiSelectionActive,
nodesDraggable,
transform,
snapGrid,
snapToGrid,
selectNodesOnDrag,
onNodeDragStart,
onSelectionDragStart,
unselectNodesAndEdges,
} = getStoreItems();
dragStarted = true;
if ((!selectNodesOnDrag || !isSelectable) && !multiSelectionActive && nodeId) {
if (!nodes.find((n) => n.id === nodeId)?.selected) {
// we need to reset selected nodes when selectNodesOnDrag=false
unselectNodesAndEdges();
}
}
if (isSelectable && selectNodesOnDrag) {
onNodeClick?.();
}
const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
lastPos = pointerPos;
dragItems = getDragItems(nodes, nodesDraggable, pointerPos, nodeId);
const onNodeOrSelectionDragStart = nodeId ? onNodeDragStart : wrapSelectionDragFunc(onSelectionDragStart);
if (dragItems) {
const [currentNode, currentNodes] = getEventHandlerParams({
nodeId,
dragItems,
nodes,
});
onDragStart?.(event.sourceEvent as MouseEvent, dragItems, currentNode, currentNodes);
onNodeOrSelectionDragStart?.(event.sourceEvent as MouseEvent, currentNode, currentNodes);
}
}
const d3DragInstance = drag()
.on('start', (event: UseDragEvent) => {
const {
nodes,
multiSelectionActive,
domNode,
nodesDraggable,
transform,
snapGrid,
snapToGrid,
selectNodesOnDrag,
onNodeDragStart,
onSelectionDragStart,
unselectNodesAndEdges,
} = getStoreItems();
const { domNode, nodeDragThreshold, transform, snapGrid, snapToGrid } = getStoreItems();
if ((!selectNodesOnDrag || !isSelectable) && !multiSelectionActive && nodeId) {
if (!nodes.find((n) => n.id === nodeId)?.selected) {
// we need to reset selected nodes when selectNodesOnDrag=false
unselectNodesAndEdges();
}
}
if (isSelectable && selectNodesOnDrag) {
onNodeClick?.();
if (nodeDragThreshold === 0) {
startDrag(event);
}
const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
lastPos = pointerPos;
dragItems = getDragItems(nodes, nodesDraggable, pointerPos, nodeId);
const onNodeOrSelectionDragStart = nodeId ? onNodeDragStart : wrapSelectionDragFunc(onSelectionDragStart);
if (dragItems) {
const [currentNode, currentNodes] = getEventHandlerParams({
nodeId,
dragItems,
nodes,
});
onDragStart?.(event.sourceEvent as MouseEvent, dragItems, currentNode, currentNodes);
onNodeOrSelectionDragStart?.(event.sourceEvent as MouseEvent, currentNode, currentNodes);
}
containerBounds = domNode?.getBoundingClientRect() || null;
mousePosition = getEventPosition(event.sourceEvent, containerBounds!);
})
.on('drag', (event: UseDragEvent) => {
const { autoPanOnNodeDrag, transform, snapGrid, snapToGrid } = getStoreItems();
const { autoPanOnNodeDrag, transform, snapGrid, snapToGrid, nodeDragThreshold } = getStoreItems();
const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
if (!autoPanStarted && autoPanOnNodeDrag) {
if (!autoPanStarted && autoPanOnNodeDrag && dragStarted) {
autoPanStarted = true;
autoPan();
}
if (!dragStarted) {
const x = pointerPos.xSnapped - (lastPos.x ?? 0);
const y = pointerPos.ySnapped - (lastPos.y ?? 0);
const distance = Math.sqrt(x * x + y * y);
if (distance > nodeDragThreshold) {
startDrag(event);
}
}
// skip events without movement
if ((lastPos.x !== pointerPos.xSnapped || lastPos.y !== pointerPos.ySnapped) && dragItems) {
if ((lastPos.x !== pointerPos.xSnapped || lastPos.y !== pointerPos.ySnapped) && dragItems && dragStarted) {
dragEvent = event.sourceEvent as MouseEvent;
mousePosition = getEventPosition(event.sourceEvent, containerBounds!);
@@ -256,7 +279,12 @@ export function XYDrag({
}
})
.on('end', (event: UseDragEvent) => {
if (!dragStarted) {
return;
}
autoPanStarted = false;
dragStarted = false;
cancelAnimationFrame(autoPanId);
if (dragItems) {