Merge pull request #4166 from xyflow/refactor/xy-drag-use-map
Refactor/xy drag use map
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
## Patch changes
|
||||
|
||||
- use correct positions for intersection helpers
|
||||
- fix minimap interaction for touch devices
|
||||
|
||||
## 12.0.0-next.14
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user