Merge pull request #4166 from xyflow/refactor/xy-drag-use-map

Refactor/xy drag use map
This commit is contained in:
Moritz Klack
2024-04-16 18:33:59 +02:00
committed by GitHub
10 changed files with 123 additions and 96 deletions

View File

@@ -5,6 +5,7 @@
## Patch changes
- use correct positions for intersection helpers
- fix minimap interaction for touch devices
## 12.0.0-next.14

View File

@@ -19,7 +19,7 @@ export function useMoveSelectedNodes() {
const moveSelectedNodes = useCallback((params: { direction: XYPosition; factor: number }) => {
const { nodeExtent, snapToGrid, snapGrid, nodesDraggable, onError, updateNodePositions, nodeLookup, nodeOrigin } =
store.getState();
const nodeUpdates = [];
const nodeUpdates = new Map();
const isSelected = selectedAndDraggable(nodesDraggable);
// by default a node moves 5px on each key press
@@ -56,7 +56,7 @@ export function useMoveSelectedNodes() {
node.position = position;
node.internals.positionAbsolute = positionAbsolute;
nodeUpdates.push(node);
nodeUpdates.set(node.id, node);
}
updateNodePositions(nodeUpdates);

View File

@@ -16,7 +16,7 @@ import {
import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
import getInitialState from './initialState';
import type { ReactFlowState, Node, Edge, UnselectNodesAndEdgesParams, FitViewOptions, InternalNode } from '../types';
import type { ReactFlowState, Node, Edge, UnselectNodesAndEdgesParams, FitViewOptions } from '../types';
const createRFStore = ({
nodes,
@@ -124,27 +124,26 @@ const createRFStore = ({
}
},
updateNodePositions: (nodeDragItems, dragging = false) => {
const { nodeLookup, parentLookup } = get();
const parentExpandChildren: ParentExpandChild[] = [];
const changes = [];
const changes: NodeChange[] = nodeDragItems.map((node) => {
for (const [id, dragItem] of nodeDragItems) {
// @todo add expandParent to drag item so that we can get rid of the look up here
const internalNode = nodeLookup.get(node.id);
const change: NodeChange = {
id: node.id,
id,
type: 'position',
position: node.position,
position: dragItem.position,
dragging,
};
if (internalNode?.expandParent && internalNode?.parentId && change.position) {
if (dragItem?.expandParent && dragItem?.parentId && change.position) {
parentExpandChildren.push({
id: internalNode.id,
parentId: internalNode.parentId,
id,
parentId: dragItem.parentId,
rect: {
...node.internals.positionAbsolute,
width: internalNode.measured.width!,
height: internalNode.measured.height!,
...dragItem.internals.positionAbsolute,
width: dragItem.measured.width!,
height: dragItem.measured.height!,
},
});
@@ -152,10 +151,11 @@ const createRFStore = ({
change.position.y = Math.max(0, change.position.y);
}
return change;
});
changes.push(change);
}
if (parentExpandChildren.length > 0) {
const { nodeLookup, parentLookup } = get();
const parentExpandChanges = handleExpandParent(parentExpandChildren, nodeLookup, parentLookup);
changes.push(...parentExpandChanges);
}

View File

@@ -64,14 +64,14 @@ export function createStore({
const updateNodePositions: UpdateNodePositions = (nodeDragItems, dragging = false) => {
const nodeLookup = get(store.nodeLookup);
for (const nodeDragItem of nodeDragItems) {
const node = nodeLookup.get(nodeDragItem.id)?.internals.userNode;
for (const [id, dragItem] of nodeDragItems) {
const node = nodeLookup.get(id)?.internals.userNode;
if (!node) {
continue;
}
node.position = nodeDragItem.position;
node.position = dragItem.position;
node.dragging = dragging;
}

View File

@@ -127,7 +127,7 @@ export type SelectionRect = Rect & {
export type OnError = (id: string, message: string) => void;
export type UpdateNodePositions = (dragItems: NodeDragItem[] | InternalNodeBase[], dragging?: boolean) => void;
export type UpdateNodePositions = (dragItems: Map<string, NodeDragItem | InternalNodeBase>, dragging?: boolean) => void;
export type PanBy = (delta: XYPosition) => boolean;
export type UpdateConnection = (params: {

View File

@@ -120,8 +120,8 @@ export type NodeDragItem = {
// distance from the mouse cursor to the node when start dragging
distance: XYPosition;
measured: {
width: number | null;
height: number | null;
width: number;
height: number;
};
internals: {
positionAbsolute: XYPosition;

View File

@@ -205,9 +205,13 @@ export function isCoordinateExtent(extent?: CoordinateExtent | 'parent'): extent
return extent !== undefined && extent !== 'parent';
}
export function getNodeDimensions<NodeType extends NodeBase = NodeBase>(
node: NodeType
): { width: number; height: number } {
export function getNodeDimensions(node: {
measured?: { width?: number; height?: number };
width?: number;
height?: number;
initialWidth?: number;
initialHeight?: number;
}): { width: number; height: number } {
return {
width: node.measured?.width ?? node.width ?? node.initialWidth ?? 0,
height: node.measured?.height ?? node.height ?? node.initialHeight ?? 0,

View File

@@ -10,6 +10,7 @@ import {
getViewportForBounds,
isCoordinateExtent,
getNodeDimensions,
getPositionWithOrigin,
} from './general';
import {
type Transform,
@@ -25,6 +26,7 @@ import {
OnBeforeDeleteBase,
NodeLookup,
InternalNodeBase,
NodeDragItem,
} from '../types';
import { errorMessages } from '../constants';
@@ -186,7 +188,7 @@ export const getNodesBounds = (
export type GetInternalNodesBoundsParams = {
nodeOrigin?: NodeOrigin;
useRelativePosition?: boolean;
filter?: (node: NodeBase) => boolean;
filter?: (node: NodeBase | NodeDragItem) => boolean;
};
/**
@@ -194,10 +196,9 @@ export type GetInternalNodesBoundsParams = {
* @internal
*/
export const getInternalNodesBounds = (
nodeLookup: NodeLookup,
nodeLookup: NodeLookup | Map<string, NodeDragItem>,
params: GetInternalNodesBoundsParams = {
nodeOrigin: [0, 0],
useRelativePosition: false,
}
): Rect => {
if (nodeLookup.size === 0) {
@@ -208,12 +209,22 @@ export const getInternalNodesBounds = (
nodeLookup.forEach((node) => {
if (params.filter == undefined || params.filter(node)) {
const nodePos = getNodePositionWithOrigin(node, node.origin || params.nodeOrigin);
const { width, height } = getNodeDimensions(node);
const { x, y } = getPositionWithOrigin({
x: node.internals.positionAbsolute.x,
y: node.internals.positionAbsolute.x,
width,
height,
origin: node.origin || params.nodeOrigin,
});
box = getBoundsOfBoxes(
box,
rectToBox({
...nodePos[params.useRelativePosition ? 'position' : 'positionAbsolute'],
...getNodeDimensions(node),
x,
y,
width,
height,
})
);
}

View File

@@ -7,10 +7,10 @@ import {
getPointerPosition,
calculateNodePosition,
snapPosition,
getNodesBounds,
getInternalNodesBounds,
rectToBox,
} from '../utils';
import { getDragItems, getEventHandlerParams, hasSelector, wrapSelectionDragFunc } from './utils';
import { getDragItems, getEventHandlerParams, hasSelector } from './utils';
import type {
NodeBase,
NodeDragItem,
@@ -29,7 +29,12 @@ import type {
InternalNodeBase,
} from '../types';
export type OnDrag = (event: MouseEvent, dragItems: NodeDragItem[], node: NodeBase, nodes: NodeBase[]) => void;
export type OnDrag = (
event: MouseEvent,
dragItems: Map<string, NodeDragItem>,
node: NodeBase,
nodes: NodeBase[]
) => void;
type StoreItems<OnNodeDrag> = {
nodes: NodeBase[];
@@ -89,7 +94,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
}: XYDragParams<OnNodeDrag>): XYDragInstance {
let lastPos: { x: number | null; y: number | null } = { x: null, y: null };
let autoPanId = 0;
let dragItems: NodeDragItem[] = [];
let dragItems = new Map<string, NodeDragItem>();
let autoPanStarted = false;
let mousePosition: XYPosition = { x: 0, y: 0 };
let containerBounds: DOMRect | null = null;
@@ -117,13 +122,13 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
let hasChange = false;
let nodesBox: Box = { x: 0, y: 0, x2: 0, y2: 0 };
if (dragItems.length > 1 && nodeExtent) {
const rect = getNodesBounds(dragItems as unknown as NodeBase[], { nodeOrigin });
if (dragItems.size > 1 && nodeExtent) {
const rect = getInternalNodesBounds(dragItems, { nodeOrigin });
nodesBox = rectToBox(rect);
}
dragItems = dragItems.map((n) => {
let nextPosition = { x: x - n.distance.x, y: y - n.distance.y };
for (const [id, dragItem] of dragItems) {
let nextPosition = { x: x - dragItem.distance.x, y: y - dragItem.distance.y };
if (snapToGrid) {
nextPosition = snapPosition(nextPosition, snapGrid);
@@ -136,13 +141,13 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
[nodeExtent[1][0], nodeExtent[1][1]],
];
if (dragItems.length > 1 && nodeExtent && !n.extent) {
const { positionAbsolute } = n.internals;
if (dragItems.size > 1 && nodeExtent && !dragItem.extent) {
const { positionAbsolute } = dragItem.internals;
const x1 = positionAbsolute.x - nodesBox.x + nodeExtent[0][0];
const x2 = positionAbsolute.x + (n.measured?.width ?? 0) - nodesBox.x2 + nodeExtent[1][0];
const x2 = positionAbsolute.x + dragItem.measured.width - nodesBox.x2 + nodeExtent[1][0];
const y1 = positionAbsolute.y - nodesBox.y + nodeExtent[0][1];
const y2 = positionAbsolute.y + (n.measured?.height ?? 0) - nodesBox.y2 + nodeExtent[1][1];
const y2 = positionAbsolute.y + dragItem.measured.height - nodesBox.y2 + nodeExtent[1][1];
adjustedNodeExtent = [
[x1, y1],
@@ -151,7 +156,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
}
const { position, positionAbsolute } = calculateNodePosition({
nodeId: n.id,
nodeId: id,
nextPosition,
nodeLookup,
nodeExtent: adjustedNodeExtent,
@@ -160,13 +165,11 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
});
// we want to make sure that we only fire a change event when there is a change
hasChange = hasChange || n.position.x !== position.x || n.position.y !== position.y;
hasChange = hasChange || dragItem.position.x !== position.x || dragItem.position.y !== position.y;
n.position = position;
n.internals.positionAbsolute = positionAbsolute;
return n;
});
dragItem.position = position;
dragItem.internals.positionAbsolute = positionAbsolute;
}
if (!hasChange) {
return;
@@ -185,8 +188,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
onNodeDrag?.(dragEvent, currentNode, currentNodes);
if (!nodeId) {
const _onSelectionDrag = wrapSelectionDragFunc(onSelectionDrag);
_onSelectionDrag(dragEvent, currentNode, currentNodes);
onSelectionDrag?.(dragEvent, currentNodes);
}
}
}
@@ -242,7 +244,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
lastPos = pointerPos;
dragItems = getDragItems(nodeLookup, nodesDraggable, pointerPos, nodeId);
if (dragItems.length > 0 && (onDragStart || onNodeDragStart || (!nodeId && onSelectionDragStart))) {
if (dragItems.size > 0 && (onDragStart || onNodeDragStart || (!nodeId && onSelectionDragStart))) {
const [currentNode, currentNodes] = getEventHandlerParams({
nodeId,
dragItems,
@@ -253,8 +255,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
onNodeDragStart?.(event.sourceEvent as MouseEvent, currentNode, currentNodes);
if (!nodeId) {
const _onSelectionDragStart = wrapSelectionDragFunc(onSelectionDragStart);
_onSelectionDragStart(event.sourceEvent as MouseEvent, currentNode, currentNodes);
onSelectionDragStart?.(event.sourceEvent as MouseEvent, currentNodes);
}
}
}
@@ -308,7 +309,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
dragStarted = false;
cancelAnimationFrame(autoPanId);
if (dragItems.length > 0) {
if (dragItems.size > 0) {
const { nodeLookup, updateNodePositions, onNodeDragStop, onSelectionDragStop } = getStoreItems();
updateNodePositions(dragItems, false);
@@ -324,8 +325,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
onNodeDragStop?.(event.sourceEvent as MouseEvent, currentNode, currentNodes);
if (!nodeId) {
const _onSelectionDragStop = wrapSelectionDragFunc(onSelectionDragStop);
_onSelectionDragStop(event.sourceEvent as MouseEvent, currentNode, currentNodes);
onSelectionDragStop?.(event.sourceEvent as MouseEvent, currentNodes);
}
}
}

View File

@@ -1,9 +1,5 @@
import { type NodeDragItem, type XYPosition, InternalNodeBase, NodeBase, NodeLookup } from '../types';
export function wrapSelectionDragFunc(selectionFunc?: (event: MouseEvent, nodes: NodeBase[]) => void) {
return (event: MouseEvent, _: NodeBase, nodes: NodeBase[]) => selectionFunc?.(event, nodes);
}
export function isParentSelected<NodeType extends NodeBase>(node: NodeType, nodeLookup: NodeLookup): boolean {
if (!node.parentId) {
return false;
@@ -40,8 +36,8 @@ export function getDragItems<NodeType extends NodeBase>(
nodesDraggable: boolean,
mousePos: XYPosition,
nodeId?: string
): NodeDragItem[] {
const dragItems: NodeDragItem[] = [];
): Map<string, NodeDragItem> {
const dragItems = new Map<string, NodeDragItem>();
for (const [id, node] of nodeLookup) {
if (
@@ -49,29 +45,32 @@ export function getDragItems<NodeType extends NodeBase>(
(!node.parentId || !isParentSelected(node, nodeLookup)) &&
(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined'))
) {
const internalNode = nodeLookup.get(id)!;
const internalNode = nodeLookup.get(id);
dragItems.push({
id: internalNode.id,
position: internalNode.position || { x: 0, y: 0 },
distance: {
x: mousePos.x - internalNode.internals.positionAbsolute.x,
y: mousePos.y - internalNode.internals.positionAbsolute.y,
},
extent: internalNode.extent,
parentId: internalNode.parentId,
origin: internalNode.origin,
expandParent: internalNode.expandParent,
internals: {
positionAbsolute: internalNode.internals.positionAbsolute || { x: 0, y: 0 },
},
measured: {
width: internalNode.measured.width || 0,
height: internalNode.measured.height || 0,
},
});
if (internalNode) {
dragItems.set(id, {
id,
position: internalNode.position || { x: 0, y: 0 },
distance: {
x: mousePos.x - internalNode.internals.positionAbsolute.x,
y: mousePos.y - internalNode.internals.positionAbsolute.y,
},
extent: internalNode.extent,
parentId: internalNode.parentId,
origin: internalNode.origin,
expandParent: internalNode.expandParent,
internals: {
positionAbsolute: internalNode.internals.positionAbsolute || { x: 0, y: 0 },
},
measured: {
width: internalNode.measured.width ?? 0,
height: internalNode.measured.height ?? 0,
},
});
}
}
}
return dragItems;
}
@@ -84,20 +83,32 @@ export function getEventHandlerParams<NodeType extends NodeBase>({
nodeLookup,
}: {
nodeId?: string;
dragItems: NodeDragItem[];
dragItems: Map<string, NodeDragItem>;
nodeLookup: Map<string, NodeType>;
}): [NodeType, NodeType[]] {
const nodesFromDragItems: NodeType[] = dragItems.map((n) => {
const node = nodeLookup.get(n.id)!;
const nodesFromDragItems: NodeType[] = [];
return {
for (const [id, dragItem] of dragItems) {
const node = nodeLookup.get(id);
if (node) {
nodesFromDragItems.push({
...node,
position: dragItem.position,
});
}
}
if (!nodeId) {
return [nodesFromDragItems[0], nodesFromDragItems];
}
const node = nodeLookup.get(nodeId)!;
return [
{
...node,
position: n.position,
measured: {
...n.measured,
},
};
});
return [nodeId ? nodesFromDragItems.find((n) => n.id === nodeId)! : nodesFromDragItems[0], nodesFromDragItems];
position: dragItems.get(nodeId)?.position || node.position,
},
nodesFromDragItems,
];
}