diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index 4634eb90..6a13af1a 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -11,16 +11,12 @@ import React, { } from 'react'; import { DraggableCore } from 'react-draggable'; import cc from 'classcat'; - import { ResizeObserver } from 'resize-observer'; -import { useStoreActions } from '../../store/hooks'; +import { useStoreActions } from '../../store/hooks'; import { Provider } from '../../contexts/NodeIdContext'; import { Node, NodeComponentProps, WrapNodeProps } from '../../types'; -const getMouseEvent = (event: MouseEvent | TouchEvent) => - typeof TouchEvent !== 'undefined' && event instanceof TouchEvent ? event.touches[0] : (event as MouseEvent); - export default (NodeComponent: ComponentType) => { const NodeWrapper = ({ id, @@ -47,18 +43,20 @@ export default (NodeComponent: ComponentType) => { targetPosition, isHidden, isInitialized, + snapToGrid, + snapGrid, }: WrapNodeProps) => { const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions); const setSelectedElements = useStoreActions((actions) => actions.setSelectedElements); - const updateNodePos = useStoreActions((actions) => actions.updateNodePos); + const updateNodePosDiff = useStoreActions((actions) => actions.updateNodePosDiff); const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); const nodeElement = useRef(null); - const [offset, setOffset] = useState({ x: 0, y: 0 }); const [isDragging, setDragging] = useState(false); - const position = useMemo(() => ({ x: xPos, y: yPos }), [xPos, yPos]); - const node = useMemo(() => ({ id, type, position, data }), [id, type, position, data]); + const node = useMemo(() => ({ id, type, position: { x: xPos, y: yPos }, data }), [id, type, xPos, yPos, data]); + const grid = useMemo(() => (snapToGrid ? snapGrid : [1, 1])! as [number, number], [snapToGrid, snapGrid]); + const nodeStyle: CSSProperties = useMemo( () => ({ zIndex: selected ? 10 : 3, @@ -109,9 +107,7 @@ export default (NodeComponent: ComponentType) => { setSelectedElements({ id: node.id, type: node.type } as Node); } - if (onClick) { - onClick(event, node); - } + onClick?.(event, node); } }, [isSelectable, isDraggable, onClick, node] @@ -119,66 +115,51 @@ export default (NodeComponent: ComponentType) => { const onDragStart = useCallback( (event) => { - const startEvent = getMouseEvent(event); - - const scaledClientX = startEvent.clientX / transform[2]; - const scaledClientY = startEvent.clientY / transform[2]; - - const offsetX = scaledClientX - position.x - transform[0]; - const offsetY = scaledClientY - position.y - transform[1]; - - setOffset({ x: offsetX, y: offsetY }); - - if (onNodeDragStart) { - onNodeDragStart(event as MouseEvent, node); - } + onNodeDragStart?.(event as MouseEvent, node); if (selectNodesOnDrag && isSelectable) { unsetNodesSelection(); setSelectedElements({ id: node.id, type: node.type } as Node); } }, - [node, transform, position, selectNodesOnDrag, isSelectable, onNodeDragStart] + [node, transform, selectNodesOnDrag, isSelectable, onNodeDragStart] ); const onDrag = useCallback( - (event) => { - const dragEvent = getMouseEvent(event); - - const scaledClientX = dragEvent.clientX / transform[2]; - const scaledClientY = dragEvent.clientY / transform[2]; - + (_, data) => { setDragging(true); - updateNodePos({ + updateNodePosDiff({ id, - pos: { - x: scaledClientX - transform[0] - offset.x, - y: scaledClientY - transform[1] - offset.y, + diff: { + x: data.deltaX, + y: data.deltaY, }, }); }, - [id, transform, offset] + [id, transform] ); const onDragStop = useCallback( (event) => { + // onDragStop also gets called when user just clicks on a node. + // Because of that we set dragging to true inside the onDrag handler and handle the click here if (!isDragging) { if (isSelectable && !selectNodesOnDrag) { setSelectedElements({ id: node.id, type: node.type } as Node); } - if (onClick) { - onClick(event as MouseEvent, node); - } + onClick?.(event as MouseEvent, node); return; } setDragging(false); + updateNodePosDiff({ + id, + isDragging: false, + }); - if (onNodeDragStop) { - onNodeDragStop(event as MouseEvent, node); - } + onNodeDragStop?.(event as MouseEvent, node); }, [node, isDragging, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop] ); @@ -187,11 +168,9 @@ export default (NodeComponent: ComponentType) => { if (nodeElement.current) { updateNodeDimensions({ id, nodeElement: nodeElement.current }); - const resizeObserver = new ResizeObserver((entries) => { - for (let _ of entries) { - if (nodeElement.current) { - updateNodeDimensions({ id, nodeElement: nodeElement.current }); - } + const resizeObserver = new ResizeObserver(() => { + if (nodeElement.current) { + updateNodeDimensions({ id, nodeElement: nodeElement.current }); } }); @@ -230,6 +209,7 @@ export default (NodeComponent: ComponentType) => { disabled={!isDraggable} cancel=".nodrag" nodeRef={nodeElement} + grid={grid} >
{ + selectedNodes?.forEach((node) => { + updateNodePosDiff({ + id: node.id, + isDragging: false, + }); + }); + onSelectionDragStop?.(event, selectedNodes); }, [selectedNodes, onSelectionDragStop] diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index 1d806186..b73a24ef 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -279,6 +279,8 @@ const GraphView = ({ onNodeDragStart={onNodeDragStart} onlyRenderVisibleNodes={onlyRenderVisibleNodes} selectNodesOnDrag={selectNodesOnDrag} + snapToGrid={snapToGrid} + snapGrid={snapGrid} /> void; onNodeDragStop?: (event: MouseEvent, node: Node) => void; onlyRenderVisibleNodes?: boolean; + snapToGrid?: boolean; + snapGrid?: [number, number]; } function renderNode( @@ -47,6 +49,7 @@ function renderNode( data={node.data} xPos={node.__rf.position.x} yPos={node.__rf.position.y} + isDragging={node.__rf.isDragging} onClick={props.onElementClick} onMouseEnter={props.onNodeMouseEnter} onMouseMove={props.onNodeMouseMove} @@ -66,6 +69,8 @@ function renderNode( selectNodesOnDrag={props.selectNodesOnDrag} isHidden={node.isHidden} isInitialized={isInitialized} + snapGrid={props.snapGrid} + snapToGrid={props.snapToGrid} /> ); } diff --git a/src/store/index.ts b/src/store/index.ts index ec39cac5..85e08fba 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -227,11 +227,9 @@ export const storeModel: StoreModel = { state.elements.forEach((n) => { if (n.id === id && isNode(n)) { - n.__rf = { - ...n.__rf, - ...dimensions, - handleBounds, - }; + n.__rf.width = dimensions.width; + n.__rf.height = dimensions.height; + n.__rf.handleBounds = handleBounds; } }); }), @@ -249,24 +247,21 @@ export const storeModel: StoreModel = { state.elements.forEach((n) => { if (n.id === id && isNode(n)) { - n.__rf = { - ...n.__rf, - position, - }; + n.__rf.position = position; } }); }), - updateNodePosDiff: action((state, { id, diff }) => { + updateNodePosDiff: action((state, { id, diff = null, isDragging = true }) => { state.elements.forEach((n) => { if (n.id === id && isNode(n)) { - n.__rf = { - ...n.__rf, - position: { + if (diff) { + n.__rf.position = { x: n.__rf.position.x + diff.x, y: n.__rf.position.y + diff.y, - }, - }; + }; + } + n.__rf.isDragging = isDragging; } }); }), @@ -316,7 +311,7 @@ export const storeModel: StoreModel = { if (!selectedNodes) { state.selectionActive = false; - state.userSelectionRect = { ...state.userSelectionRect, draw: false }; + state.userSelectionRect.draw = false; state.nodesSelectionActive = false; state.selectedElements = null; @@ -328,7 +323,7 @@ export const storeModel: StoreModel = { state.nodesSelectionActive = true; state.selectedNodesBbox = selectedNodesBbox; - state.userSelectionRect = { ...state.userSelectionRect, draw: false }; + state.userSelectionRect.draw = false; state.selectionActive = false; }), @@ -454,7 +449,9 @@ export const storeModel: StoreModel = { // we need to sync the d3 zoom transform with the fitted transform d3Selection.property('__zoom', fittedTransform); - state.transform = [fittedTransform.x, fittedTransform.y, fittedTransform.k]; + state.transform[0] = fittedTransform.x; + state.transform[1] = fittedTransform.y; + state.transform[2] = fittedTransform.k; }), zoomTo: action((state, zoomLevel) => { diff --git a/src/types/index.ts b/src/types/index.ts index a9471d46..96730e61 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -172,6 +172,9 @@ export interface WrapNodeProps { targetPosition?: Position; isHidden?: boolean; isInitialized?: boolean; + snapToGrid?: boolean; + snapGrid?: [number, number]; + isDragging?: boolean; } export type FitViewParams = { @@ -269,7 +272,8 @@ export type NodePosUpdate = { export type NodeDiffUpdate = { id: ElementId; - diff: XYPosition; + diff?: XYPosition; + isDragging?: boolean; }; export type FlowTransform = { diff --git a/src/utils/graph.ts b/src/utils/graph.ts index 6f341ac9..d31c00cb 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -126,6 +126,7 @@ export const parseElement = (element: Node | Edge): Node | Edge => { width: null, height: null, handleBounds: {}, + isDragging: false, }, } as Node; }; @@ -182,13 +183,13 @@ export const getNodesInside = ( height: rect.height / tScale, }); - return nodes.filter(({ __rf: { position, width, height } }) => { + return nodes.filter(({ __rf: { position, width, height, isDragging } }) => { const nBox = rectToBox({ ...position, width, height }); const xOverlap = Math.max(0, Math.min(rBox.x2, nBox.x2) - Math.max(rBox.x, nBox.x)); const yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y)); const overlappingArea = xOverlap * yOverlap; - if (width === null || height === null) { + if (width === null || height === null || isDragging) { // at the beginnning all nodes have width & height === 0 return true; }