From 02bcd1be11f9a0ce073b112ef1cfec6e869f5d17 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 2 Jun 2020 14:21:21 +0200 Subject: [PATCH] feat(options): add minZoom, maxZoom, defaultZoom closes #273 --- README.md | 3 +++ cypress/integration/flow/basic.spec.js | 4 ++-- example/src/Basic/index.js | 3 +++ src/container/GraphView/index.tsx | 17 +++++++++++++++++ src/container/ReactFlow/index.tsx | 12 ++++++++++++ src/hooks/useD3Zoom.ts | 21 +++++++++++---------- src/store/index.ts | 23 +++++++++++++++++++++++ 7 files changed, 71 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 9dac10e3..f176c9a5 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,9 @@ const BasicFlow = () => ; - `onlyRenderVisibleNodes`: default: `true` - `isInteractive`: default: `true`. If the graph is not interactive you can't drag any nodes - `selectNodesOnDrag`: default: `true` +- `minZoom`: default: `0.5` +- `maxZoom`: default: `2` +- `defaultZoom`: default: `1` ## React Flow Instance diff --git a/cypress/integration/flow/basic.spec.js b/cypress/integration/flow/basic.spec.js index e160f369..afe43550 100644 --- a/cypress/integration/flow/basic.spec.js +++ b/cypress/integration/flow/basic.spec.js @@ -127,8 +127,8 @@ describe('Basic Graph Rendering', () => { cy.window().then((win) => { cy.get('.react-flow__zoompane') - .trigger('mousedown', { which: 1, view: win }) - .trigger('mousemove', newPosition) + .trigger('mousedown', 'topLeft', { which: 1, view: win }) + .trigger('mousemove', 'bottomLeft') .trigger('mouseup', { force: true, view: win }) .then(() => { const styleAfterDrag = Cypress.$('.react-flow__nodes').css('transform'); diff --git a/example/src/Basic/index.js b/example/src/Basic/index.js index da5cfe53..2cb4f068 100644 --- a/example/src/Basic/index.js +++ b/example/src/Basic/index.js @@ -48,6 +48,9 @@ const BasicFlow = () => { onConnect={onConnect} onNodeDragStop={onNodeDragStop} className="react-flow-basic-example" + defaultZoom={1.5} + minZoom={0.2} + maxZoom={4} > diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index 1dca125c..36ab0396 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -43,6 +43,9 @@ export interface GraphViewProps { onlyRenderVisibleNodes: boolean; isInteractive: boolean; selectNodesOnDrag: boolean; + minZoom: number; + maxZoom: number; + defaultZoom: number; } const GraphView = memo( @@ -66,6 +69,9 @@ const GraphView = memo( onlyRenderVisibleNodes, isInteractive, selectNodesOnDrag, + minZoom, + maxZoom, + defaultZoom, }: GraphViewProps) => { const zoomPane = useRef(null); const rendererNode = useRef(null); @@ -79,6 +85,9 @@ const GraphView = memo( const setOnConnect = useStoreActions((a) => a.setOnConnect); const setSnapGrid = useStoreActions((actions) => actions.setSnapGrid); const setInteractive = useStoreActions((actions) => actions.setInteractive); + const updateTransform = useStoreActions((actions) => actions.updateTransform); + const setMinMaxZoom = useStoreActions((actions) => actions.setMinMaxZoom); + const selectionKeyPressed = useKeyPress(selectionKeyCode); const rendererClasses = classnames('react-flow__renderer', { 'is-interactive': isInteractive }); @@ -106,6 +115,10 @@ const GraphView = memo( setOnConnect(onConnect); } + if (defaultZoom !== 1) { + updateTransform({ x: 0, y: 0, k: defaultZoom }); + } + return () => { window.onresize = null; }; @@ -132,6 +145,10 @@ const GraphView = memo( setInteractive(isInteractive); }, [isInteractive]); + useEffect(() => { + setMinMaxZoom({ minZoom, maxZoom }); + }, [minZoom, maxZoom]); + useGlobalKeyHandler({ onElementsRemove, deleteKeyCode }); useElementUpdater(elements); diff --git a/src/container/ReactFlow/index.tsx b/src/container/ReactFlow/index.tsx index 1339529e..de742138 100644 --- a/src/container/ReactFlow/index.tsx +++ b/src/container/ReactFlow/index.tsx @@ -54,6 +54,9 @@ export interface ReactFlowProps extends Omit, 'on onlyRenderVisibleNodes: boolean; isInteractive: boolean; selectNodesOnDrag: boolean; + minZoom: number; + maxZoom: number; + defaultZoom: number; } const ReactFlow = ({ @@ -80,6 +83,9 @@ const ReactFlow = ({ onlyRenderVisibleNodes, isInteractive, selectNodesOnDrag, + minZoom, + maxZoom, + defaultZoom, }: ReactFlowProps) => { const nodeTypesParsed = useMemo(() => createNodeTypes(nodeTypes), []); const edgeTypesParsed = useMemo(() => createEdgeTypes(edgeTypes), []); @@ -107,6 +113,9 @@ const ReactFlow = ({ onlyRenderVisibleNodes={onlyRenderVisibleNodes} isInteractive={isInteractive} selectNodesOnDrag={selectNodesOnDrag} + minZoom={minZoom} + maxZoom={maxZoom} + defaultZoom={defaultZoom} /> {onSelectionChange && } {children} @@ -136,6 +145,9 @@ ReactFlow.defaultProps = { onlyRenderVisibleNodes: true, isInteractive: true, selectNodesOnDrag: true, + minZoom: 0.5, + maxZoom: 2, + defaultZoom: 1, }; export default ReactFlow; diff --git a/src/hooks/useD3Zoom.ts b/src/hooks/useD3Zoom.ts index 0cd64333..95f3a0d9 100644 --- a/src/hooks/useD3Zoom.ts +++ b/src/hooks/useD3Zoom.ts @@ -10,10 +10,6 @@ interface UseD3ZoomParams { onMove?: () => void; } -const d3ZoomInstance = zoom() - .scaleExtent([0.5, 2]) - .filter(() => !event.button); - export default ({ zoomPane, onMove, selectionKeyPressed }: UseD3ZoomParams): void => { const transform = useStoreState((s) => s.transform); const d3Selection = useStoreState((s) => s.d3Selection); @@ -24,16 +20,21 @@ export default ({ zoomPane, onMove, selectionKeyPressed }: UseD3ZoomParams): voi useEffect(() => { if (zoomPane.current) { - const selection = select(zoomPane.current).call(d3ZoomInstance); - initD3({ zoom: d3ZoomInstance, selection }); + const nextD3ZoomInstance = zoom(); + const selection = select(zoomPane.current).call(nextD3ZoomInstance); + initD3({ zoom: nextD3ZoomInstance, selection }); } }, []); useEffect(() => { + if (!d3Zoom) { + return; + } + if (selectionKeyPressed) { - d3ZoomInstance.on('zoom', null); + d3Zoom.on('zoom', null); } else { - d3ZoomInstance.on('zoom', () => { + d3Zoom.on('zoom', () => { if (event.sourceEvent && event.sourceEvent.target !== zoomPane.current) { return; } @@ -53,7 +54,7 @@ export default ({ zoomPane, onMove, selectionKeyPressed }: UseD3ZoomParams): voi } return () => { - d3ZoomInstance.on('zoom', null); + d3Zoom.on('zoom', null); }; - }, [selectionKeyPressed]); + }, [selectionKeyPressed, d3Zoom]); }; diff --git a/src/store/index.ts b/src/store/index.ts index 9fec04bb..b0d00dc8 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -47,6 +47,11 @@ type D3Init = { selection: D3Selection; }; +type SetMinMaxZoom = { + minZoom: number; + maxZoom: number; +}; + type SetSnapGrid = { snapToGrid: boolean; snapGrid: [number, number]; @@ -64,6 +69,8 @@ export interface StoreModel { d3Zoom: ZoomBehavior | null; d3Selection: D3Selection | null; d3Initialised: boolean; + minZoom: number; + maxZoom: number; nodesSelectionActive: boolean; selectionActive: boolean; @@ -104,6 +111,8 @@ export interface StoreModel { initD3: Action; + setMinMaxZoom: Action; + setSnapGrid: Action; setConnectionPosition: Action; @@ -129,6 +138,8 @@ const storeModel: StoreModel = { d3Zoom: null, d3Selection: null, d3Initialised: false, + minZoom: 0.5, + maxZoom: 2, nodesSelectionActive: false, selectionActive: false, @@ -326,10 +337,22 @@ const storeModel: StoreModel = { initD3: action((state, { zoom, selection }) => { state.d3Zoom = zoom; + + state.d3Zoom.scaleExtent([state.minZoom, state.maxZoom]); + state.d3Selection = selection; state.d3Initialised = true; }), + setMinMaxZoom: action((state, { minZoom, maxZoom }) => { + state.minZoom = minZoom; + state.maxZoom = maxZoom; + + if (state.d3Zoom) { + state.d3Zoom.scaleExtent([minZoom, maxZoom]); + } + }), + setConnectionPosition: action((state, position) => { state.connectionPosition = position; }),