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
+1
View File
@@ -5,6 +5,7 @@
## Patch changes ## Patch changes
- use correct positions for intersection helpers - use correct positions for intersection helpers
- fix minimap interaction for touch devices
## 12.0.0-next.14 ## 12.0.0-next.14
@@ -19,7 +19,7 @@ export function useMoveSelectedNodes() {
const moveSelectedNodes = useCallback((params: { direction: XYPosition; factor: number }) => { const moveSelectedNodes = useCallback((params: { direction: XYPosition; factor: number }) => {
const { nodeExtent, snapToGrid, snapGrid, nodesDraggable, onError, updateNodePositions, nodeLookup, nodeOrigin } = const { nodeExtent, snapToGrid, snapGrid, nodesDraggable, onError, updateNodePositions, nodeLookup, nodeOrigin } =
store.getState(); store.getState();
const nodeUpdates = []; const nodeUpdates = new Map();
const isSelected = selectedAndDraggable(nodesDraggable); const isSelected = selectedAndDraggable(nodesDraggable);
// by default a node moves 5px on each key press // by default a node moves 5px on each key press
@@ -56,7 +56,7 @@ export function useMoveSelectedNodes() {
node.position = position; node.position = position;
node.internals.positionAbsolute = positionAbsolute; node.internals.positionAbsolute = positionAbsolute;
nodeUpdates.push(node); nodeUpdates.set(node.id, node);
} }
updateNodePositions(nodeUpdates); updateNodePositions(nodeUpdates);
+14 -14
View File
@@ -16,7 +16,7 @@ import {
import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
import getInitialState from './initialState'; 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 = ({ const createRFStore = ({
nodes, nodes,
@@ -124,27 +124,26 @@ const createRFStore = ({
} }
}, },
updateNodePositions: (nodeDragItems, dragging = false) => { updateNodePositions: (nodeDragItems, dragging = false) => {
const { nodeLookup, parentLookup } = get();
const parentExpandChildren: ParentExpandChild[] = []; 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 // @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 = { const change: NodeChange = {
id: node.id, id,
type: 'position', type: 'position',
position: node.position, position: dragItem.position,
dragging, dragging,
}; };
if (internalNode?.expandParent && internalNode?.parentId && change.position) { if (dragItem?.expandParent && dragItem?.parentId && change.position) {
parentExpandChildren.push({ parentExpandChildren.push({
id: internalNode.id, id,
parentId: internalNode.parentId, parentId: dragItem.parentId,
rect: { rect: {
...node.internals.positionAbsolute, ...dragItem.internals.positionAbsolute,
width: internalNode.measured.width!, width: dragItem.measured.width!,
height: internalNode.measured.height!, height: dragItem.measured.height!,
}, },
}); });
@@ -152,10 +151,11 @@ const createRFStore = ({
change.position.y = Math.max(0, change.position.y); change.position.y = Math.max(0, change.position.y);
} }
return change; changes.push(change);
}); }
if (parentExpandChildren.length > 0) { if (parentExpandChildren.length > 0) {
const { nodeLookup, parentLookup } = get();
const parentExpandChanges = handleExpandParent(parentExpandChildren, nodeLookup, parentLookup); const parentExpandChanges = handleExpandParent(parentExpandChildren, nodeLookup, parentLookup);
changes.push(...parentExpandChanges); changes.push(...parentExpandChanges);
} }
+3 -3
View File
@@ -64,14 +64,14 @@ export function createStore({
const updateNodePositions: UpdateNodePositions = (nodeDragItems, dragging = false) => { const updateNodePositions: UpdateNodePositions = (nodeDragItems, dragging = false) => {
const nodeLookup = get(store.nodeLookup); const nodeLookup = get(store.nodeLookup);
for (const nodeDragItem of nodeDragItems) { for (const [id, dragItem] of nodeDragItems) {
const node = nodeLookup.get(nodeDragItem.id)?.internals.userNode; const node = nodeLookup.get(id)?.internals.userNode;
if (!node) { if (!node) {
continue; continue;
} }
node.position = nodeDragItem.position; node.position = dragItem.position;
node.dragging = dragging; node.dragging = dragging;
} }
+1 -1
View File
@@ -127,7 +127,7 @@ export type SelectionRect = Rect & {
export type OnError = (id: string, message: string) => void; 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 PanBy = (delta: XYPosition) => boolean;
export type UpdateConnection = (params: { export type UpdateConnection = (params: {
+2 -2
View File
@@ -120,8 +120,8 @@ export type NodeDragItem = {
// distance from the mouse cursor to the node when start dragging // distance from the mouse cursor to the node when start dragging
distance: XYPosition; distance: XYPosition;
measured: { measured: {
width: number | null; width: number;
height: number | null; height: number;
}; };
internals: { internals: {
positionAbsolute: XYPosition; positionAbsolute: XYPosition;
+7 -3
View File
@@ -205,9 +205,13 @@ export function isCoordinateExtent(extent?: CoordinateExtent | 'parent'): extent
return extent !== undefined && extent !== 'parent'; return extent !== undefined && extent !== 'parent';
} }
export function getNodeDimensions<NodeType extends NodeBase = NodeBase>( export function getNodeDimensions(node: {
node: NodeType measured?: { width?: number; height?: number };
): { width: number; height: number } { width?: number;
height?: number;
initialWidth?: number;
initialHeight?: number;
}): { width: number; height: number } {
return { return {
width: node.measured?.width ?? node.width ?? node.initialWidth ?? 0, width: node.measured?.width ?? node.width ?? node.initialWidth ?? 0,
height: node.measured?.height ?? node.height ?? node.initialHeight ?? 0, height: node.measured?.height ?? node.height ?? node.initialHeight ?? 0,
+17 -6
View File
@@ -10,6 +10,7 @@ import {
getViewportForBounds, getViewportForBounds,
isCoordinateExtent, isCoordinateExtent,
getNodeDimensions, getNodeDimensions,
getPositionWithOrigin,
} from './general'; } from './general';
import { import {
type Transform, type Transform,
@@ -25,6 +26,7 @@ import {
OnBeforeDeleteBase, OnBeforeDeleteBase,
NodeLookup, NodeLookup,
InternalNodeBase, InternalNodeBase,
NodeDragItem,
} from '../types'; } from '../types';
import { errorMessages } from '../constants'; import { errorMessages } from '../constants';
@@ -186,7 +188,7 @@ export const getNodesBounds = (
export type GetInternalNodesBoundsParams = { export type GetInternalNodesBoundsParams = {
nodeOrigin?: NodeOrigin; nodeOrigin?: NodeOrigin;
useRelativePosition?: boolean; useRelativePosition?: boolean;
filter?: (node: NodeBase) => boolean; filter?: (node: NodeBase | NodeDragItem) => boolean;
}; };
/** /**
@@ -194,10 +196,9 @@ export type GetInternalNodesBoundsParams = {
* @internal * @internal
*/ */
export const getInternalNodesBounds = ( export const getInternalNodesBounds = (
nodeLookup: NodeLookup, nodeLookup: NodeLookup | Map<string, NodeDragItem>,
params: GetInternalNodesBoundsParams = { params: GetInternalNodesBoundsParams = {
nodeOrigin: [0, 0], nodeOrigin: [0, 0],
useRelativePosition: false,
} }
): Rect => { ): Rect => {
if (nodeLookup.size === 0) { if (nodeLookup.size === 0) {
@@ -208,12 +209,22 @@ export const getInternalNodesBounds = (
nodeLookup.forEach((node) => { nodeLookup.forEach((node) => {
if (params.filter == undefined || params.filter(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 = getBoundsOfBoxes(
box, box,
rectToBox({ rectToBox({
...nodePos[params.useRelativePosition ? 'position' : 'positionAbsolute'], x,
...getNodeDimensions(node), y,
width,
height,
}) })
); );
} }
+27 -27
View File
@@ -7,10 +7,10 @@ import {
getPointerPosition, getPointerPosition,
calculateNodePosition, calculateNodePosition,
snapPosition, snapPosition,
getNodesBounds, getInternalNodesBounds,
rectToBox, rectToBox,
} from '../utils'; } from '../utils';
import { getDragItems, getEventHandlerParams, hasSelector, wrapSelectionDragFunc } from './utils'; import { getDragItems, getEventHandlerParams, hasSelector } from './utils';
import type { import type {
NodeBase, NodeBase,
NodeDragItem, NodeDragItem,
@@ -29,7 +29,12 @@ import type {
InternalNodeBase, InternalNodeBase,
} from '../types'; } 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> = { type StoreItems<OnNodeDrag> = {
nodes: NodeBase[]; nodes: NodeBase[];
@@ -89,7 +94,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
}: XYDragParams<OnNodeDrag>): XYDragInstance { }: XYDragParams<OnNodeDrag>): XYDragInstance {
let lastPos: { x: number | null; y: number | null } = { x: null, y: null }; let lastPos: { x: number | null; y: number | null } = { x: null, y: null };
let autoPanId = 0; let autoPanId = 0;
let dragItems: NodeDragItem[] = []; let dragItems = new Map<string, NodeDragItem>();
let autoPanStarted = false; let autoPanStarted = false;
let mousePosition: XYPosition = { x: 0, y: 0 }; let mousePosition: XYPosition = { x: 0, y: 0 };
let containerBounds: DOMRect | null = null; 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 hasChange = false;
let nodesBox: Box = { x: 0, y: 0, x2: 0, y2: 0 }; let nodesBox: Box = { x: 0, y: 0, x2: 0, y2: 0 };
if (dragItems.length > 1 && nodeExtent) { if (dragItems.size > 1 && nodeExtent) {
const rect = getNodesBounds(dragItems as unknown as NodeBase[], { nodeOrigin }); const rect = getInternalNodesBounds(dragItems, { nodeOrigin });
nodesBox = rectToBox(rect); nodesBox = rectToBox(rect);
} }
dragItems = dragItems.map((n) => { for (const [id, dragItem] of dragItems) {
let nextPosition = { x: x - n.distance.x, y: y - n.distance.y }; let nextPosition = { x: x - dragItem.distance.x, y: y - dragItem.distance.y };
if (snapToGrid) { if (snapToGrid) {
nextPosition = snapPosition(nextPosition, snapGrid); 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]], [nodeExtent[1][0], nodeExtent[1][1]],
]; ];
if (dragItems.length > 1 && nodeExtent && !n.extent) { if (dragItems.size > 1 && nodeExtent && !dragItem.extent) {
const { positionAbsolute } = n.internals; const { positionAbsolute } = dragItem.internals;
const x1 = positionAbsolute.x - nodesBox.x + nodeExtent[0][0]; 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 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 = [ adjustedNodeExtent = [
[x1, y1], [x1, y1],
@@ -151,7 +156,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
} }
const { position, positionAbsolute } = calculateNodePosition({ const { position, positionAbsolute } = calculateNodePosition({
nodeId: n.id, nodeId: id,
nextPosition, nextPosition,
nodeLookup, nodeLookup,
nodeExtent: adjustedNodeExtent, 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 // 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; dragItem.position = position;
n.internals.positionAbsolute = positionAbsolute; dragItem.internals.positionAbsolute = positionAbsolute;
}
return n;
});
if (!hasChange) { if (!hasChange) {
return; return;
@@ -185,8 +188,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
onNodeDrag?.(dragEvent, currentNode, currentNodes); onNodeDrag?.(dragEvent, currentNode, currentNodes);
if (!nodeId) { if (!nodeId) {
const _onSelectionDrag = wrapSelectionDragFunc(onSelectionDrag); onSelectionDrag?.(dragEvent, currentNodes);
_onSelectionDrag(dragEvent, currentNode, currentNodes);
} }
} }
} }
@@ -242,7 +244,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
lastPos = pointerPos; lastPos = pointerPos;
dragItems = getDragItems(nodeLookup, nodesDraggable, pointerPos, nodeId); 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({ const [currentNode, currentNodes] = getEventHandlerParams({
nodeId, nodeId,
dragItems, dragItems,
@@ -253,8 +255,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
onNodeDragStart?.(event.sourceEvent as MouseEvent, currentNode, currentNodes); onNodeDragStart?.(event.sourceEvent as MouseEvent, currentNode, currentNodes);
if (!nodeId) { if (!nodeId) {
const _onSelectionDragStart = wrapSelectionDragFunc(onSelectionDragStart); onSelectionDragStart?.(event.sourceEvent as MouseEvent, currentNodes);
_onSelectionDragStart(event.sourceEvent as MouseEvent, currentNode, currentNodes);
} }
} }
} }
@@ -308,7 +309,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
dragStarted = false; dragStarted = false;
cancelAnimationFrame(autoPanId); cancelAnimationFrame(autoPanId);
if (dragItems.length > 0) { if (dragItems.size > 0) {
const { nodeLookup, updateNodePositions, onNodeDragStop, onSelectionDragStop } = getStoreItems(); const { nodeLookup, updateNodePositions, onNodeDragStop, onSelectionDragStop } = getStoreItems();
updateNodePositions(dragItems, false); 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); onNodeDragStop?.(event.sourceEvent as MouseEvent, currentNode, currentNodes);
if (!nodeId) { if (!nodeId) {
const _onSelectionDragStop = wrapSelectionDragFunc(onSelectionDragStop); onSelectionDragStop?.(event.sourceEvent as MouseEvent, currentNodes);
_onSelectionDragStop(event.sourceEvent as MouseEvent, currentNode, currentNodes);
} }
} }
} }
+49 -38
View File
@@ -1,9 +1,5 @@
import { type NodeDragItem, type XYPosition, InternalNodeBase, NodeBase, NodeLookup } from '../types'; 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 { export function isParentSelected<NodeType extends NodeBase>(node: NodeType, nodeLookup: NodeLookup): boolean {
if (!node.parentId) { if (!node.parentId) {
return false; return false;
@@ -40,8 +36,8 @@ export function getDragItems<NodeType extends NodeBase>(
nodesDraggable: boolean, nodesDraggable: boolean,
mousePos: XYPosition, mousePos: XYPosition,
nodeId?: string nodeId?: string
): NodeDragItem[] { ): Map<string, NodeDragItem> {
const dragItems: NodeDragItem[] = []; const dragItems = new Map<string, NodeDragItem>();
for (const [id, node] of nodeLookup) { for (const [id, node] of nodeLookup) {
if ( if (
@@ -49,29 +45,32 @@ export function getDragItems<NodeType extends NodeBase>(
(!node.parentId || !isParentSelected(node, nodeLookup)) && (!node.parentId || !isParentSelected(node, nodeLookup)) &&
(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined')) (node.draggable || (nodesDraggable && typeof node.draggable === 'undefined'))
) { ) {
const internalNode = nodeLookup.get(id)!; const internalNode = nodeLookup.get(id);
dragItems.push({ if (internalNode) {
id: internalNode.id, dragItems.set(id, {
position: internalNode.position || { x: 0, y: 0 }, id,
distance: { position: internalNode.position || { x: 0, y: 0 },
x: mousePos.x - internalNode.internals.positionAbsolute.x, distance: {
y: mousePos.y - internalNode.internals.positionAbsolute.y, x: mousePos.x - internalNode.internals.positionAbsolute.x,
}, y: mousePos.y - internalNode.internals.positionAbsolute.y,
extent: internalNode.extent, },
parentId: internalNode.parentId, extent: internalNode.extent,
origin: internalNode.origin, parentId: internalNode.parentId,
expandParent: internalNode.expandParent, origin: internalNode.origin,
internals: { expandParent: internalNode.expandParent,
positionAbsolute: internalNode.internals.positionAbsolute || { x: 0, y: 0 }, internals: {
}, positionAbsolute: internalNode.internals.positionAbsolute || { x: 0, y: 0 },
measured: { },
width: internalNode.measured.width || 0, measured: {
height: internalNode.measured.height || 0, width: internalNode.measured.width ?? 0,
}, height: internalNode.measured.height ?? 0,
}); },
});
}
} }
} }
return dragItems; return dragItems;
} }
@@ -84,20 +83,32 @@ export function getEventHandlerParams<NodeType extends NodeBase>({
nodeLookup, nodeLookup,
}: { }: {
nodeId?: string; nodeId?: string;
dragItems: NodeDragItem[]; dragItems: Map<string, NodeDragItem>;
nodeLookup: Map<string, NodeType>; nodeLookup: Map<string, NodeType>;
}): [NodeType, NodeType[]] { }): [NodeType, NodeType[]] {
const nodesFromDragItems: NodeType[] = dragItems.map((n) => { const nodesFromDragItems: NodeType[] = [];
const node = nodeLookup.get(n.id)!;
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, ...node,
position: n.position, position: dragItems.get(nodeId)?.position || node.position,
measured: { },
...n.measured, nodesFromDragItems,
}, ];
};
});
return [nodeId ? nodesFromDragItems.find((n) => n.id === nodeId)! : nodesFromDragItems[0], nodesFromDragItems];
} }