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
+6 -4
View File
@@ -1,9 +1,6 @@
import { MouseEvent, useCallback } from 'react'; import { MouseEvent, useCallback } from 'react';
import { ReactFlow, addEdge, Node, Connection, Edge, useNodesState, useEdgesState } from '@xyflow/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[] = [ const nodesA: Node[] = [
{ {
id: '1a', id: '1a',
@@ -83,7 +80,10 @@ const edgesB: Edge[] = [
{ id: 'e4b', source: 'inputb', target: '4b' }, { 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 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 BasicFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(nodesA); const [nodes, setNodes, onNodesChange] = useNodesState(nodesA);
@@ -99,8 +99,10 @@ const BasicFlow = () => {
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick} onNodeClick={onNodeClick}
onConnect={onConnect} onConnect={onConnect}
onNodeDragStop={onNodeDragStop} onNodeDragStart={onNodeDragStart}
onNodeDrag={onNodeDrag} onNodeDrag={onNodeDrag}
onNodeDragStop={onNodeDragStop}
nodeDragThreshold={10}
> >
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}> <div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
<button <button
@@ -145,7 +145,7 @@
]); ]);
</script> </script>
<SvelteFlow {nodes} {edges} fitView> <SvelteFlow {nodes} {edges} fitView nodeDragThreshold={2}>
<Controls /> <Controls />
<Background variant={BackgroundVariant.Dots} /> <Background variant={BackgroundVariant.Dots} />
<MiniMap /> <MiniMap />
@@ -56,6 +56,7 @@ type StoreUpdaterProps = Pick<
| 'connectionRadius' | 'connectionRadius'
| 'isValidConnection' | 'isValidConnection'
| 'selectNodesOnDrag' | 'selectNodesOnDrag'
| 'nodeDragThreshold'
> & { rfId: string }; > & { rfId: string };
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
@@ -140,6 +141,7 @@ const StoreUpdater = ({
connectionRadius, connectionRadius,
isValidConnection, isValidConnection,
selectNodesOnDrag, selectNodesOnDrag,
nodeDragThreshold,
}: StoreUpdaterProps) => { }: StoreUpdaterProps) => {
const { const {
setNodes, setNodes,
@@ -203,6 +205,7 @@ const StoreUpdater = ({
useDirectStoreUpdater('connectionRadius', connectionRadius, store.setState); useDirectStoreUpdater('connectionRadius', connectionRadius, store.setState);
useDirectStoreUpdater('isValidConnection', isValidConnection, store.setState); useDirectStoreUpdater('isValidConnection', isValidConnection, store.setState);
useDirectStoreUpdater('selectNodesOnDrag', selectNodesOnDrag, store.setState); useDirectStoreUpdater('selectNodesOnDrag', selectNodesOnDrag, store.setState);
useDirectStoreUpdater('nodeDragThreshold', nodeDragThreshold, store.setState);
useStoreUpdater<Node[]>(nodes, setNodes); useStoreUpdater<Node[]>(nodes, setNodes);
useStoreUpdater<Edge[]>(edges, setEdges); useStoreUpdater<Edge[]>(edges, setEdges);
@@ -163,6 +163,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
onError, onError,
style, style,
id, id,
nodeDragThreshold,
...rest ...rest
}, },
ref ref
@@ -292,6 +293,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
connectionRadius={connectionRadius} connectionRadius={connectionRadius}
isValidConnection={isValidConnection} isValidConnection={isValidConnection}
selectNodesOnDrag={selectNodesOnDrag} selectNodesOnDrag={selectNodesOnDrag}
nodeDragThreshold={nodeDragThreshold}
/> />
<SelectionListener onSelectionChange={onSelectionChange} /> <SelectionListener onSelectionChange={onSelectionChange} />
{children} {children}
+1
View File
@@ -28,6 +28,7 @@ const initialState: ReactFlowStore = {
paneDragging: false, paneDragging: false,
noPanClassName: 'nopan', noPanClassName: 'nopan',
nodeOrigin: [0, 0], nodeOrigin: [0, 0],
nodeDragThreshold: 0,
snapGrid: [15, 15], snapGrid: [15, 15],
snapToGrid: false, snapToGrid: false,
@@ -148,6 +148,7 @@ export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & {
connectionRadius?: number; connectionRadius?: number;
onError?: OnError; onError?: OnError;
isValidConnection?: IsValidConnection; isValidConnection?: IsValidConnection;
nodeDragThreshold?: number;
}; };
export type ReactFlowRefType = HTMLDivElement; export type ReactFlowRefType = HTMLDivElement;
+1
View File
@@ -61,6 +61,7 @@ export type ReactFlowStore = {
translateExtent: CoordinateExtent; translateExtent: CoordinateExtent;
nodeExtent: CoordinateExtent; nodeExtent: CoordinateExtent;
nodeOrigin: NodeOrigin; nodeOrigin: NodeOrigin;
nodeDragThreshold: number;
nodesSelectionActive: boolean; nodesSelectionActive: boolean;
userSelectionActive: boolean; userSelectionActive: boolean;
+4 -1
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 `on:edgecontextmenu` event handler prop
- add `connectionMode` prop - add `connectionMode` prop
- add `attributionPosition` prop - add `attributionPosition` prop
@@ -38,6 +38,7 @@ export default function drag(domNode: Element, params: UseDragParams) {
autoPanOnNodeDrag: get(store.autoPanOnNodeDrag), autoPanOnNodeDrag: get(store.autoPanOnNodeDrag),
nodesDraggable: get(store.nodesDraggable), nodesDraggable: get(store.nodesDraggable),
selectNodesOnDrag: get(store.selectNodesOnDrag), selectNodesOnDrag: get(store.selectNodesOnDrag),
nodeDragThreshold: get(store.nodeDragThreshold),
unselectNodesAndEdges: store.unselectNodesAndEdges, unselectNodesAndEdges: store.unselectNodesAndEdges,
updateNodePositions: store.updateNodePositions, updateNodePositions: store.updateNodePositions,
panBy: store.panBy panBy: store.panBy
@@ -34,6 +34,7 @@
export let panActivationKey: $$Props['panActivationKey'] = undefined; export let panActivationKey: $$Props['panActivationKey'] = undefined;
export let nodesDraggable: $$Props['nodesDraggable'] = undefined; export let nodesDraggable: $$Props['nodesDraggable'] = undefined;
export let nodesConnectable: $$Props['nodesConnectable'] = undefined; export let nodesConnectable: $$Props['nodesConnectable'] = undefined;
export let nodeDragThreshold: $$Props['nodeDragThreshold'] = undefined;
export let elementsSelectable: $$Props['elementsSelectable'] = undefined; export let elementsSelectable: $$Props['elementsSelectable'] = undefined;
export let snapGrid: $$Props['snapGrid'] = undefined; export let snapGrid: $$Props['snapGrid'] = undefined;
export let deleteKey: $$Props['deleteKey'] = undefined; export let deleteKey: $$Props['deleteKey'] = undefined;
@@ -118,7 +119,8 @@
autoPanOnConnect, autoPanOnConnect,
autoPanOnNodeDrag, autoPanOnNodeDrag,
onError, onError,
connectionMode connectionMode,
nodeDragThreshold
}; };
updateStoreByKeys(store, updatableProps); updateStoreByKeys(store, updatableProps);
@@ -43,6 +43,7 @@ export type SvelteFlowProps = DOMAttributes<HTMLDivElement> & {
fitView?: boolean; fitView?: boolean;
fitViewOptions?: FitViewOptions; fitViewOptions?: FitViewOptions;
nodeOrigin?: NodeOrigin; nodeOrigin?: NodeOrigin;
nodeDragThreshold?: number;
minZoom?: number; minZoom?: number;
maxZoom?: number; maxZoom?: number;
initialViewport?: Viewport; initialViewport?: Viewport;
@@ -63,6 +63,7 @@ export type UpdatableStoreProps = {
autoPanOnNodeDrag?: UnwrapWritable<SvelteFlowStore['autoPanOnNodeDrag']>; autoPanOnNodeDrag?: UnwrapWritable<SvelteFlowStore['autoPanOnNodeDrag']>;
connectionMode?: UnwrapWritable<SvelteFlowStore['connectionMode']>; connectionMode?: UnwrapWritable<SvelteFlowStore['connectionMode']>;
onError?: UnwrapWritable<SvelteFlowStore['onError']>; onError?: UnwrapWritable<SvelteFlowStore['onError']>;
nodeDragThreshold?: UnwrapWritable<SvelteFlowStore['nodeDragThreshold']>;
}; };
export function updateStoreByKeys(store: SvelteFlowStore, keys: UpdatableStoreProps) { export function updateStoreByKeys(store: SvelteFlowStore, keys: UpdatableStoreProps) {
@@ -65,6 +65,7 @@ export const getInitialStore = () => ({
minZoom: writable<number>(0.5), minZoom: writable<number>(0.5),
maxZoom: writable<number>(2), maxZoom: writable<number>(2),
nodeOrigin: writable<NodeOrigin>([0, 0]), nodeOrigin: writable<NodeOrigin>([0, 0]),
nodeDragThreshold: writable<number>(0),
nodeExtent: writable<CoordinateExtent>(infiniteExtent), nodeExtent: writable<CoordinateExtent>(infiniteExtent),
translateExtent: writable<CoordinateExtent>(infiniteExtent), translateExtent: writable<CoordinateExtent>(infiniteExtent),
autoPanOnNodeDrag: writable<boolean>(true), autoPanOnNodeDrag: writable<boolean>(true),
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/system", "name": "@xyflow/system",
"version": "0.0.6", "version": "0.0.7",
"description": "xyflow core system that powers React Flow and Svelte Flow.", "description": "xyflow core system that powers React Flow and Svelte Flow.",
"keywords": [ "keywords": [
"node-based UI", "node-based UI",
+67 -39
View File
@@ -44,6 +44,7 @@ type StoreItems = {
autoPanOnNodeDrag: boolean; autoPanOnNodeDrag: boolean;
nodesDraggable: boolean; nodesDraggable: boolean;
selectNodesOnDrag: boolean; selectNodesOnDrag: boolean;
nodeDragThreshold: number;
panBy: PanBy; panBy: PanBy;
unselectNodesAndEdges: () => void; unselectNodesAndEdges: () => void;
onError?: OnError; onError?: OnError;
@@ -93,6 +94,7 @@ export function XYDrag({
let mousePosition: XYPosition = { x: 0, y: 0 }; let mousePosition: XYPosition = { x: 0, y: 0 };
let dragEvent: MouseEvent | null = null; let dragEvent: MouseEvent | null = null;
let containerBounds: DOMRect | null = null; let containerBounds: DOMRect | null = null;
let dragStarted = false;
const d3Selection = select(domNode); const d3Selection = select(domNode);
@@ -192,63 +194,84 @@ export function XYDrag({
autoPanId = requestAnimationFrame(autoPan); 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() const d3DragInstance = drag()
.on('start', (event: UseDragEvent) => { .on('start', (event: UseDragEvent) => {
const { const { domNode, nodeDragThreshold, transform, snapGrid, snapToGrid } = getStoreItems();
nodes,
multiSelectionActive,
domNode,
nodesDraggable,
transform,
snapGrid,
snapToGrid,
selectNodesOnDrag,
onNodeDragStart,
onSelectionDragStart,
unselectNodesAndEdges,
} = getStoreItems();
if ((!selectNodesOnDrag || !isSelectable) && !multiSelectionActive && nodeId) { if (nodeDragThreshold === 0) {
if (!nodes.find((n) => n.id === nodeId)?.selected) { startDrag(event);
// we need to reset selected nodes when selectNodesOnDrag=false
unselectNodesAndEdges();
}
}
if (isSelectable && selectNodesOnDrag) {
onNodeClick?.();
} }
const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
lastPos = pointerPos; 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; containerBounds = domNode?.getBoundingClientRect() || null;
mousePosition = getEventPosition(event.sourceEvent, containerBounds!); mousePosition = getEventPosition(event.sourceEvent, containerBounds!);
}) })
.on('drag', (event: UseDragEvent) => { .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 }); const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
if (!autoPanStarted && autoPanOnNodeDrag) { if (!autoPanStarted && autoPanOnNodeDrag && dragStarted) {
autoPanStarted = true; autoPanStarted = true;
autoPan(); 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 // 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; dragEvent = event.sourceEvent as MouseEvent;
mousePosition = getEventPosition(event.sourceEvent, containerBounds!); mousePosition = getEventPosition(event.sourceEvent, containerBounds!);
@@ -256,7 +279,12 @@ export function XYDrag({
} }
}) })
.on('end', (event: UseDragEvent) => { .on('end', (event: UseDragEvent) => {
if (!dragStarted) {
return;
}
autoPanStarted = false; autoPanStarted = false;
dragStarted = false;
cancelAnimationFrame(autoPanId); cancelAnimationFrame(autoPanId);
if (dragItems) { if (dragItems) {