feat(react,svelte): add nodeDragThreshold
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,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",
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user