feat(react/svelte): controlled viewport (#3461)
* feat(react): controlled viewport * refactor(react): cleanup viewport * feat(svelte): two way data binding for viewport, cleanup transform <> viewport stuff * refactor(svelte): viewport stuff
This commit is contained in:
@@ -24,6 +24,7 @@ export type FlowRendererProps = Omit<
|
||||
| 'rfId'
|
||||
| 'nodeOrigin'
|
||||
> & {
|
||||
isControlledViewport: boolean;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
@@ -63,6 +64,8 @@ const FlowRenderer = ({
|
||||
noWheelClassName,
|
||||
noPanClassName,
|
||||
disableKeyboardA11y,
|
||||
onViewportChange,
|
||||
isControlledViewport,
|
||||
}: FlowRendererProps) => {
|
||||
const nodesSelectionActive = useStore(selector);
|
||||
const selectionKeyPressed = useKeyPress(selectionKeyCode);
|
||||
@@ -92,6 +95,8 @@ const FlowRenderer = ({
|
||||
preventScrolling={preventScrolling}
|
||||
noWheelClassName={noWheelClassName}
|
||||
noPanClassName={noPanClassName}
|
||||
onViewportChange={onViewportChange}
|
||||
isControlledViewport={isControlledViewport}
|
||||
>
|
||||
<Pane
|
||||
onSelectionStart={onSelectionStart}
|
||||
|
||||
@@ -5,8 +5,9 @@ import NodeRenderer from '../NodeRenderer';
|
||||
import EdgeRenderer from '../EdgeRenderer';
|
||||
import ViewportWrapper from '../Viewport';
|
||||
import useOnInitHandler from '../../hooks/useOnInitHandler';
|
||||
import useViewportSync from '../../hooks/useViewportSync';
|
||||
import ConnectionLine from '../../components/ConnectionLine';
|
||||
import type { NodeTypesWrapped, ReactFlowProps } from '../../types';
|
||||
import type { ReactFlowProps } from '../../types';
|
||||
import { createNodeTypes } from '../NodeRenderer/utils';
|
||||
import { createEdgeTypes } from '../EdgeRenderer/utils';
|
||||
import { useNodeOrEdgeTypes } from './utils';
|
||||
@@ -104,11 +105,14 @@ const GraphView = ({
|
||||
nodeOrigin,
|
||||
nodeExtent,
|
||||
rfId,
|
||||
viewport,
|
||||
onViewportChange,
|
||||
}: GraphViewProps) => {
|
||||
const nodeTypesWrapped = useNodeOrEdgeTypes(nodeTypes, createNodeTypes);
|
||||
const edgeTypesWrapped = useNodeOrEdgeTypes(edgeTypes, createEdgeTypes);
|
||||
|
||||
useOnInitHandler(onInit);
|
||||
useViewportSync(viewport);
|
||||
|
||||
return (
|
||||
<FlowRenderer
|
||||
@@ -145,6 +149,8 @@ const GraphView = ({
|
||||
noWheelClassName={noWheelClassName}
|
||||
noPanClassName={noPanClassName}
|
||||
disableKeyboardA11y={disableKeyboardA11y}
|
||||
onViewportChange={onViewportChange}
|
||||
isControlledViewport={!!viewport}
|
||||
>
|
||||
<ViewportWrapper>
|
||||
<EdgeRenderer
|
||||
|
||||
@@ -164,6 +164,8 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
|
||||
style,
|
||||
id,
|
||||
nodeDragThreshold,
|
||||
viewport,
|
||||
onViewportChange,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
@@ -242,6 +244,8 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
|
||||
disableKeyboardA11y={disableKeyboardA11y}
|
||||
nodeOrigin={nodeOrigin}
|
||||
nodeExtent={nodeExtent}
|
||||
viewport={viewport}
|
||||
onViewportChange={onViewportChange}
|
||||
/>
|
||||
<StoreUpdater
|
||||
nodes={nodes}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ReactNode } from 'react';
|
||||
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import type { ReactFlowState } from '../../types';
|
||||
import { Viewport } from '@xyflow/system';
|
||||
|
||||
const selector = (s: ReactFlowState) => `translate(${s.transform[0]}px,${s.transform[1]}px) scale(${s.transform[2]})`;
|
||||
|
||||
|
||||
@@ -18,7 +18,9 @@ type ZoomPaneProps = Omit<
|
||||
| 'noDragClassName'
|
||||
| 'disableKeyboardA11y'
|
||||
| 'selectionOnDrag'
|
||||
>;
|
||||
> & {
|
||||
isControlledViewport: boolean;
|
||||
};
|
||||
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
userSelectionActive: s.userSelectionActive,
|
||||
@@ -43,6 +45,8 @@ const ZoomPane = ({
|
||||
children,
|
||||
noWheelClassName,
|
||||
noPanClassName,
|
||||
onViewportChange,
|
||||
isControlledViewport,
|
||||
}: ZoomPaneProps) => {
|
||||
const store = useStoreApi();
|
||||
const zoomPane = useRef<HTMLDivElement>(null);
|
||||
@@ -60,7 +64,13 @@ const ZoomPane = ({
|
||||
maxZoom,
|
||||
translateExtent,
|
||||
viewport: defaultViewport,
|
||||
onTransformChange: (transform: Transform) => store.setState({ transform }),
|
||||
onTransformChange: (transform: Transform) => {
|
||||
onViewportChange?.({ x: transform[0], y: transform[1], zoom: transform[2] });
|
||||
|
||||
if (!isControlledViewport) {
|
||||
store.setState({ transform });
|
||||
}
|
||||
},
|
||||
onDraggingChange: (paneDragging: boolean) => store.setState({ paneDragging }),
|
||||
onPanZoomStart: (event, vp) => {
|
||||
const { onViewportChangeStart, onMoveStart } = store.getState();
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useEffect } from 'react';
|
||||
import type { Viewport } from '@xyflow/system';
|
||||
|
||||
import { useStore, useStoreApi } from './useStore';
|
||||
import type { ReactFlowState } from '../types';
|
||||
|
||||
const selector = (state: ReactFlowState) => state.panZoom?.syncViewport;
|
||||
|
||||
export default function useViewportSync(viewport?: Viewport) {
|
||||
const syncViewport = useStore(selector);
|
||||
const store = useStoreApi();
|
||||
|
||||
useEffect(() => {
|
||||
if (viewport) {
|
||||
syncViewport?.(viewport);
|
||||
store.setState({ transform: [viewport.x, viewport.y, viewport.zoom] });
|
||||
}
|
||||
}, [viewport, syncViewport]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -120,7 +120,9 @@ export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & {
|
||||
panOnDrag?: boolean | number[];
|
||||
minZoom?: number;
|
||||
maxZoom?: number;
|
||||
viewport?: Viewport;
|
||||
defaultViewport?: Viewport;
|
||||
onViewportChange?: (viewport: Viewport) => void;
|
||||
translateExtent?: CoordinateExtent;
|
||||
preventScrolling?: boolean;
|
||||
nodeExtent?: CoordinateExtent;
|
||||
|
||||
Reference in New Issue
Block a user