From 801f773e9a1b5c926f6e537d594035a77a896ad5 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 10 Sep 2020 22:31:40 +0200 Subject: [PATCH 1/6] style(example): provider mobile layout --- example/src/Provider/provider.css | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/example/src/Provider/provider.css b/example/src/Provider/provider.css index 8bb8ebf2..aa719acc 100644 --- a/example/src/Provider/provider.css +++ b/example/src/Provider/provider.css @@ -1,14 +1,12 @@ .providerflow { + flex-direction: column; display: flex; height: 100%; } .providerflow aside { - width: 20%; - max-width: 250px; - flex-grow: 1; border-right: 1px solid #eee; - padding: 10px; + padding: 15px 10px; font-size: 12px; background: #fcfcfc; } @@ -33,4 +31,15 @@ .providerflow .selectall { margin-top: 10px; +} + +@media screen and (min-width: 768px) { + .providerflow { + flex-direction: row; + } + + .providerflow aside { + width: 20%; + max-width: 250px; + } } \ No newline at end of file From 7f64ffe1733fd2f28d56c04b8aaa32e8c9a7e9f9 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 10 Sep 2020 23:54:53 +0200 Subject: [PATCH 2/6] feat(utils): add getIncomers function closes #461 --- README.md | 6 ++++++ cypress/integration/flow/graph-utils.spec.js | 13 +++++++++++-- src/utils/graph.ts | 12 +++++++++++- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 802887c5..f6436671 100644 --- a/README.md +++ b/README.md @@ -581,6 +581,12 @@ Returns all direct child nodes of the passed node. `getOutgoers = (node: Node, elements: Elements): Node[]` +### getIncomers + +Returns all direct incoming nodes of the passed node. + +`getOutgoers = (node: Node, elements: Elements): Node[]` + ### getConnectedEdges Returns all edges that are connected to the passed nodes. diff --git a/cypress/integration/flow/graph-utils.spec.js b/cypress/integration/flow/graph-utils.spec.js index 128f3e6d..00e355a7 100644 --- a/cypress/integration/flow/graph-utils.spec.js +++ b/cypress/integration/flow/graph-utils.spec.js @@ -1,4 +1,4 @@ -import { isNode, isEdge, getOutgoers, removeElements, addEdge } from '../../../src/utils/graph.ts'; +import { isNode, isEdge, getOutgoers, getIncomers, removeElements, addEdge } from '../../../src/utils/graph.ts'; const nodes = [ { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }, @@ -10,6 +10,7 @@ const nodes = [ const edges = [ { id: 'e1-2', source: '1', target: '2', animated: true }, { id: 'e1-3', source: '1', target: '3' }, + { id: 'e2-3', source: '2', target: '3' }, ]; const elements = [...nodes, ...edges]; @@ -29,10 +30,18 @@ describe('Graph Utils Testing', () => { const outgoers = getOutgoers(nodes[0], elements); expect(outgoers.length).to.be.equal(2); - const noOutgoers = getOutgoers(nodes[1], elements); + const noOutgoers = getOutgoers(nodes[2], elements); expect(noOutgoers.length).to.be.equal(0); }); + it('tests getIncomers function', () => { + const incomers = getIncomers(nodes[2], elements); + expect(incomers.length).to.be.equal(2); + + const noIncomers = getIncomers(nodes[0], elements); + expect(noIncomers.length).to.be.equal(0); + }); + describe('tests addEdge function', () => { it('adds edge', () => { const newEdge = { source: '2', target: '3' }; diff --git a/src/utils/graph.ts b/src/utils/graph.ts index 1d104749..6f341ac9 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -1,6 +1,7 @@ +import { Store } from 'easy-peasy'; + import store, { StoreModel } from '../store'; import { ElementId, Node, Edge, Elements, Transform, XYPosition, Rect, FitViewParams, Box, Connection } from '../types'; -import { Store } from 'easy-peasy'; export const isEdge = (element: Node | Connection | Edge): element is Edge => 'id' in element && 'source' in element && 'target' in element; @@ -17,6 +18,15 @@ export const getOutgoers = (node: Node, elements: Elements): Node[] => { return elements.filter((e) => outgoerIds.includes(e.id)) as Node[]; }; +export const getIncomers = (node: Node, elements: Elements): Node[] => { + if (!isNode(node)) { + return []; + } + + const incomersIds = elements.filter((e) => isEdge(e) && e.target === node.id).map((e) => (e as Edge).source); + return elements.filter((e) => incomersIds.includes(e.id)) as Node[]; +}; + export const removeElements = (elementsToRemove: Elements, elements: Elements): Elements => { const nodeIdsToRemove = elementsToRemove.map((n) => n.id); From fe3dd87d24f09d2a5158317b0c5127ee60065f0e Mon Sep 17 00:00:00 2001 From: moklick Date: Fri, 11 Sep 2020 01:39:40 +0200 Subject: [PATCH 3/6] fix(selection): handle snap grid correctly --- src/components/NodesSelection/index.tsx | 63 +++++-------------------- src/store/index.ts | 16 +++++++ src/types/index.ts | 5 ++ 3 files changed, 34 insertions(+), 50 deletions(-) diff --git a/src/components/NodesSelection/index.tsx b/src/components/NodesSelection/index.tsx index 1aaa2053..fddd652a 100644 --- a/src/components/NodesSelection/index.tsx +++ b/src/components/NodesSelection/index.tsx @@ -3,29 +3,12 @@ * made a selectio with on or several nodes */ -import React, { useState, MouseEvent } from 'react'; -import ReactDraggable from 'react-draggable'; +import React, { MouseEvent } from 'react'; +import ReactDraggable, { DraggableData } from 'react-draggable'; import { useStoreState, useStoreActions } from '../../store/hooks'; import { isNode } from '../../utils/graph'; -import { Node, XYPosition } from '../../types'; - -type StartPositions = { [key: string]: XYPosition }; - -function getStartPositions(nodes: Node[]): StartPositions { - const startPositions: StartPositions = {}; - - return nodes.reduce((res, node) => { - const startPosition = { - x: node.__rf.position.x || node.position.x, - y: node.__rf.position.y || node.position.y, - }; - - res[node.id] = startPosition; - - return res; - }, startPositions); -} +import { Node } from '../../types'; export interface NodesSelectionProps { onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void; @@ -34,9 +17,6 @@ export interface NodesSelectionProps { } export default ({ onSelectionDragStart, onSelectionDrag, onSelectionDragStop }: NodesSelectionProps) => { - const [offset, setOffset] = useState({ x: 0, y: 0 }); - const [startPositions, setStartPositions] = useState({}); - const [tX, tY, tScale] = useStoreState((state) => state.transform); const selectedNodesBbox = useStoreState((state) => state.selectedNodesBbox); const selectedElements = useStoreState((state) => state.selectedElements); @@ -44,7 +24,7 @@ export default ({ onSelectionDragStart, onSelectionDrag, onSelectionDragStop }: const snapGrid = useStoreState((state) => state.snapGrid); const nodes = useStoreState((state) => state.nodes); - const updateNodePos = useStoreActions((actions) => actions.updateNodePos); + const updateNodePosDiff = useStoreActions((actions) => actions.updateNodePosDiff); const grid = (snapToGrid ? snapGrid : [1, 1])! as [number, number]; @@ -53,36 +33,18 @@ export default ({ onSelectionDragStart, onSelectionDrag, onSelectionDragStop }: } const onStart = (event: MouseEvent) => { - const scaledClient: XYPosition = { - x: event.clientX / tScale, - y: event.clientY / tScale, - }; - const offsetX: number = scaledClient.x - selectedNodesBbox.x - tX; - const offsetY: number = scaledClient.y - selectedNodesBbox.y - tY; const selectedNodes = selectedElements ? selectedElements .filter(isNode) .map((selectedNode) => nodes.find((node) => node.id === selectedNode.id)! as Node) : []; - const nextStartPositions = getStartPositions(selectedNodes); - if (onSelectionDragStart) { onSelectionDragStart(event, selectedNodes); } - - if (nextStartPositions) { - setOffset({ x: offsetX, y: offsetY }); - setStartPositions(nextStartPositions); - } }; - const onDrag = (event: MouseEvent) => { - const scaledClient: XYPosition = { - x: event.clientX / tScale, - y: event.clientY / tScale, - }; - + const onDrag = (event: MouseEvent, data: DraggableData) => { if (selectedElements) { const selectedNodes = selectedElements ? selectedElements.filter(isNode) : []; @@ -95,12 +57,13 @@ export default ({ onSelectionDragStart, onSelectionDrag, onSelectionDragStop }: } selectedNodes.forEach((node) => { - const pos: XYPosition = { - x: startPositions[node.id].x + scaledClient.x - selectedNodesBbox.x - offset.x - tX, - y: startPositions[node.id].y + scaledClient.y - selectedNodesBbox.y - offset.y - tY, - }; - - updateNodePos({ id: node.id, pos }); + updateNodePosDiff({ + id: node.id, + diff: { + x: data.deltaX, + y: data.deltaY, + }, + }); }); } }; @@ -126,7 +89,7 @@ export default ({ onSelectionDragStart, onSelectionDrag, onSelectionDragStop }: scale={tScale} grid={grid} onStart={(event) => onStart(event as MouseEvent)} - onDrag={(event) => onDrag(event as MouseEvent)} + onDrag={(event, data) => onDrag(event as MouseEvent, data)} onStop={(event) => onStop(event as MouseEvent)} >
; updateNodePos: Action; + updateNodePosDiff: Action; setSelection: Action; @@ -255,6 +257,20 @@ export const storeModel: StoreModel = { }); }), + updateNodePosDiff: action((state, { id, diff }) => { + state.elements.forEach((n) => { + if (n.id === id && isNode(n)) { + n.__rf = { + ...n.__rf, + position: { + x: n.__rf.position.x + diff.x, + y: n.__rf.position.y + diff.y, + }, + }; + } + }); + }), + setUserSelection: action((state, mousePos) => { state.userSelectionRect = { width: 0, diff --git a/src/types/index.ts b/src/types/index.ts index 7d02712a..a9471d46 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -267,6 +267,11 @@ export type NodePosUpdate = { pos: XYPosition; }; +export type NodeDiffUpdate = { + id: ElementId; + diff: XYPosition; +}; + export type FlowTransform = { x: number; y: number; From 121952bba73839ae31216548a179e914f44c6044 Mon Sep 17 00:00:00 2001 From: moklick Date: Fri, 11 Sep 2020 02:01:33 +0200 Subject: [PATCH 4/6] feat(selection): add context menu handler closes #478 --- README.md | 1 + example/src/Overview/index.js | 5 ++ src/components/NodesSelection/index.tsx | 106 ++++++++++++++---------- src/container/GraphView/index.tsx | 3 + src/container/ReactFlow/index.tsx | 3 + 5 files changed, 74 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index f6436671..ea3ff932 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ const BasicFlow = () => ; - `onSelectionDragStart(evt: MouseEvent, nodes: Node[])`: called when user starts to drag a selection - `onSelectionDrag(evt: MouseEvent, nodes: Node[])`: called when user drags a selection - `onSelectionDragStop(evt: MouseEvent, nodes: Node[])`: called when user stops to drag a selection +- `onSelectionContextMenu(event: MouseEvent, nodes: Node[])`: called when user does a right-click on a selection - `onPaneClick(event: MouseEvent)`: called when user clicks directly on the canvas - `onPaneContextMenu(event: MouseEvent)`: called when user does a right-click on the canvas - `onPaneScroll(event: WheelEvent)`: called when user scrolls pane (only works when `zoomOnScroll` is set to `false) diff --git a/example/src/Overview/index.js b/example/src/Overview/index.js index 045d5684..20246191 100644 --- a/example/src/Overview/index.js +++ b/example/src/Overview/index.js @@ -7,6 +7,10 @@ const onNodeDragStop = (event, node) => console.log('drag stop', node); const onSelectionDrag = (event, nodes) => console.log('selection drag', nodes); const onSelectionDragStart = (event, nodes) => console.log('selection drag start', nodes); const onSelectionDragStop = (event, nodes) => console.log('selection drag stop', nodes); +const onSelectionContextMenu = (event, nodes) => { + event.preventDefault(); + console.log('selection context menu', nodes); +}; const onElementClick = (event, element) => console.log(`${isNode(element) ? 'node' : 'edge'} click:`, element); const onSelectionChange = (elements) => console.log('selection change', elements); const onLoad = (reactFlowInstance) => { @@ -126,6 +130,7 @@ const OverviewFlow = () => { onSelectionDragStart={onSelectionDragStart} onSelectionDrag={onSelectionDrag} onSelectionDragStop={onSelectionDragStop} + onSelectionContextMenu={onSelectionContextMenu} onSelectionChange={onSelectionChange} onMoveEnd={onMoveEnd} onLoad={onLoad} diff --git a/src/components/NodesSelection/index.tsx b/src/components/NodesSelection/index.tsx index fddd652a..a2f6d1d2 100644 --- a/src/components/NodesSelection/index.tsx +++ b/src/components/NodesSelection/index.tsx @@ -3,7 +3,7 @@ * made a selectio with on or several nodes */ -import React, { MouseEvent } from 'react'; +import React, { useMemo, useCallback, MouseEvent } from 'react'; import ReactDraggable, { DraggableData } from 'react-draggable'; import { useStoreState, useStoreActions } from '../../store/hooks'; @@ -14,9 +14,15 @@ export interface NodesSelectionProps { onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void; onSelectionDrag?: (event: MouseEvent, nodes: Node[]) => void; onSelectionDragStop?: (event: MouseEvent, nodes: Node[]) => void; + onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void; } -export default ({ onSelectionDragStart, onSelectionDrag, onSelectionDragStop }: NodesSelectionProps) => { +export default ({ + onSelectionDragStart, + onSelectionDrag, + onSelectionDragStop, + onSelectionContextMenu, +}: NodesSelectionProps) => { const [tX, tY, tScale] = useStoreState((state) => state.transform); const selectedNodesBbox = useStoreState((state) => state.selectedNodesBbox); const selectedElements = useStoreState((state) => state.selectedElements); @@ -32,31 +38,30 @@ export default ({ onSelectionDragStart, onSelectionDrag, onSelectionDragStop }: return null; } - const onStart = (event: MouseEvent) => { - const selectedNodes = selectedElements - ? selectedElements - .filter(isNode) - .map((selectedNode) => nodes.find((node) => node.id === selectedNode.id)! as Node) - : []; + const selectedNodes = useMemo( + () => + selectedElements + ? selectedElements + .filter(isNode) + .map((selectedNode) => nodes.find((node) => node.id === selectedNode.id)! as Node) + : [], + [selectedElements] + ); - if (onSelectionDragStart) { - onSelectionDragStart(event, selectedNodes); - } - }; - - const onDrag = (event: MouseEvent, data: DraggableData) => { - if (selectedElements) { - const selectedNodes = selectedElements ? selectedElements.filter(isNode) : []; + const onStart = useCallback( + (event: MouseEvent) => { + onSelectionDragStart?.(event, selectedNodes); + }, + [onSelectionDragStart, selectedNodes] + ); + const onDrag = useCallback( + (event: MouseEvent, data: DraggableData) => { if (onSelectionDrag) { - const selectionNodes = selectedNodes.map( - (selectedNode) => nodes.find((node) => node.id === selectedNode.id)! as Node - ); - - onSelectionDrag(event, selectionNodes); + onSelectionDrag(event, selectedNodes); } - selectedNodes.forEach((node) => { + selectedNodes?.forEach((node) => { updateNodePosDiff({ id: node.id, diff: { @@ -65,26 +70,47 @@ export default ({ onSelectionDragStart, onSelectionDrag, onSelectionDragStop }: }, }); }); - } - }; + }, + [onSelectionDrag, selectedNodes, updateNodePosDiff] + ); - const onStop = (event: MouseEvent) => { - if (selectedElements && onSelectionDragStop) { + const onStop = useCallback( + (event: MouseEvent) => { + onSelectionDragStop?.(event, selectedNodes); + }, + [selectedNodes, onSelectionDragStop] + ); + + const onContextMenu = useCallback( + (event: MouseEvent) => { const selectedNodes = selectedElements ? selectedElements.filter(isNode).map((selectedNode) => nodes.find((node) => node.id === selectedNode.id)!) : []; - onSelectionDragStop(event, selectedNodes); - } - }; + onSelectionContextMenu?.(event, selectedNodes); + }, + [onSelectionContextMenu] + ); + + const style = useMemo( + () => ({ + transform: `translate(${tX}px,${tY}px) scale(${tScale})`, + }), + [tX, tY, tScale] + ); + + const innerStyle = useMemo( + () => ({ + width: selectedNodesBbox.width, + height: selectedNodesBbox.height, + top: selectedNodesBbox.y, + left: selectedNodesBbox.x, + }), + [selectedNodesBbox] + ); return ( -
+
onDrag(event as MouseEvent, data)} onStop={(event) => onStop(event as MouseEvent)} > -
+
); diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index 88e2013c..1d806186 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -51,6 +51,7 @@ export interface GraphViewProps { onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void; onSelectionDrag?: (event: MouseEvent, nodes: Node[]) => void; onSelectionDragStop?: (event: MouseEvent, nodes: Node[]) => void; + onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void; selectionKeyCode: number; nodeTypes: NodeTypesType; edgeTypes: EdgeTypesType; @@ -93,6 +94,7 @@ const GraphView = ({ onSelectionDragStart, onSelectionDrag, onSelectionDragStop, + onSelectionContextMenu, connectionLineType, connectionLineStyle, selectionKeyCode, @@ -292,6 +294,7 @@ const GraphView = ({ onSelectionDragStart={onSelectionDragStart} onSelectionDrag={onSelectionDrag} onSelectionDragStop={onSelectionDragStop} + onSelectionContextMenu={onSelectionContextMenu} /> )}
, 'on onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void; onSelectionDrag?: (event: MouseEvent, nodes: Node[]) => void; onSelectionDragStop?: (event: MouseEvent, nodes: Node[]) => void; + onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void; onPaneScroll?: (event?: WheelEvent) => void; onPaneClick?: (event: MouseEvent) => void; onPaneContextMenu?: (event: MouseEvent) => void; @@ -112,6 +113,7 @@ const ReactFlow = ({ onSelectionDragStart, onSelectionDrag, onSelectionDragStop, + onSelectionContextMenu, connectionLineType, connectionLineStyle, deleteKeyCode, @@ -191,6 +193,7 @@ const ReactFlow = ({ onSelectionDragStart={onSelectionDragStart} onSelectionDrag={onSelectionDrag} onSelectionDragStop={onSelectionDragStop} + onSelectionContextMenu={onSelectionContextMenu} /> {onSelectionChange && } {children} From 1425b8ffde53e270f32a67aec804145942efde62 Mon Sep 17 00:00:00 2001 From: moklick Date: Fri, 11 Sep 2020 10:54:04 +0200 Subject: [PATCH 5/6] refactor(element-parsing): handle elements with integer ids closes #500 --- src/hooks/useElementUpdater.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/useElementUpdater.ts b/src/hooks/useElementUpdater.ts index f0109bf5..f3f7fdc1 100644 --- a/src/hooks/useElementUpdater.ts +++ b/src/hooks/useElementUpdater.ts @@ -11,7 +11,7 @@ const useElementUpdater = (propElements: Elements): void => { useEffect(() => { const nextElements: Elements = propElements.map((propElement) => { - const existingElement = stateElements.find((el) => el.id === propElement.id); + const existingElement = stateElements.find((el) => el.id === propElement.id?.toString()); if (existingElement) { const data = !isEqual(existingElement.data, propElement.data) @@ -41,7 +41,7 @@ const useElementUpdater = (propElements: Elements): void => { if (typeof propElement.isHidden !== 'undefined') { elementProps.isHidden = propElement.isHidden; } - + if (typeof propElement.type !== 'undefined') { elementProps.type = propElement.type; } From 4cc6b0af02949c934342b3f95a8ed40b8613eb65 Mon Sep 17 00:00:00 2001 From: moklick Date: Fri, 11 Sep 2020 11:04:38 +0200 Subject: [PATCH 6/6] fix(selection): dont render selection when another node gets selected --- src/components/Nodes/wrapNode.tsx | 3 +++ src/components/NodesSelection/index.tsx | 9 +++++---- src/store/index.ts | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index 54be5214..4634eb90 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -51,6 +51,7 @@ export default (NodeComponent: ComponentType) => { const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions); const setSelectedElements = useStoreActions((actions) => actions.setSelectedElements); const updateNodePos = useStoreActions((actions) => actions.updateNodePos); + const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); const nodeElement = useRef(null); const [offset, setOffset] = useState({ x: 0, y: 0 }); @@ -104,6 +105,7 @@ export default (NodeComponent: ComponentType) => { (event: MouseEvent) => { if (!isDraggable) { if (isSelectable) { + unsetNodesSelection(); setSelectedElements({ id: node.id, type: node.type } as Node); } @@ -132,6 +134,7 @@ export default (NodeComponent: ComponentType) => { } if (selectNodesOnDrag && isSelectable) { + unsetNodesSelection(); setSelectedElements({ id: node.id, type: node.type } as Node); } }, diff --git a/src/components/NodesSelection/index.tsx b/src/components/NodesSelection/index.tsx index a2f6d1d2..c46e7b9e 100644 --- a/src/components/NodesSelection/index.tsx +++ b/src/components/NodesSelection/index.tsx @@ -25,6 +25,7 @@ export default ({ }: NodesSelectionProps) => { const [tX, tY, tScale] = useStoreState((state) => state.transform); const selectedNodesBbox = useStoreState((state) => state.selectedNodesBbox); + const selectionActive = useStoreState((state) => state.selectionActive); const selectedElements = useStoreState((state) => state.selectedElements); const snapToGrid = useStoreState((state) => state.snapToGrid); const snapGrid = useStoreState((state) => state.snapGrid); @@ -34,10 +35,6 @@ export default ({ const grid = (snapToGrid ? snapGrid : [1, 1])! as [number, number]; - if (!selectedElements) { - return null; - } - const selectedNodes = useMemo( () => selectedElements @@ -109,6 +106,10 @@ export default ({ [selectedNodesBbox] ); + if (!selectedElements || selectionActive) { + return null; + } + return (
{ - const selectedNodes = getNodesInside(state.nodes, state.userSelectionRect, state.transform); + const selectedNodes = state.selectedElements?.filter(isNode); - if (!selectedNodes.length) { + if (!selectedNodes) { state.selectionActive = false; state.userSelectionRect = { ...state.userSelectionRect, draw: false }; state.nodesSelectionActive = false;