refactor pan/zoom behaviour, implement pan on scroll #359, edit interaction example

This commit is contained in:
Christopher Möller
2020-10-29 17:57:31 +01:00
parent 2280ed39af
commit d6a6eb82d8
6 changed files with 96 additions and 33 deletions

View File

@@ -26,6 +26,7 @@ const InteractionFlow = () => {
const [isDraggable, setIsDraggable] = useState(false);
const [isConnectable, setIsConnectable] = useState(false);
const [zoomOnScroll, setZoomOnScroll] = useState(false);
const [panOnScroll, setPanOnScroll] = useState(true);
const [zoomOnDoubleClick, setZoomOnDoubleClick] = useState(false);
const [paneMoveable, setPaneMoveable] = useState(true);
const [captureZoomClick, setCaptureZoomClick] = useState(false);
@@ -39,6 +40,7 @@ const InteractionFlow = () => {
nodesConnectable={isConnectable}
nodesDraggable={isDraggable}
zoomOnScroll={zoomOnScroll}
panOnScroll={panOnScroll}
zoomOnDoubleClick={zoomOnDoubleClick}
onConnect={onConnect}
onElementClick={captureElementClick ? onElementClick : undefined}
@@ -55,7 +57,7 @@ const InteractionFlow = () => {
<div style={{ position: 'absolute', left: 10, top: 10, zIndex: 4 }}>
<div>
<label htmlFor="draggable">
draggable
nodesDraggable
<input
id="draggable"
type="checkbox"
@@ -67,7 +69,7 @@ const InteractionFlow = () => {
</div>
<div>
<label htmlFor="connectable">
connectable
nodesConnectable
<input
id="connectable"
type="checkbox"
@@ -79,7 +81,7 @@ const InteractionFlow = () => {
</div>
<div>
<label htmlFor="selectable">
selectable
elementsSelectable
<input
id="selectable"
type="checkbox"
@@ -91,7 +93,7 @@ const InteractionFlow = () => {
</div>
<div>
<label htmlFor="zoomonscroll">
zoom on scroll
zoomOnScroll
<input
id="zoomonscroll"
type="checkbox"
@@ -101,9 +103,21 @@ const InteractionFlow = () => {
/>
</label>
</div>
<div>
<label htmlFor="panonscroll">
panOnScroll
<input
id="panonscroll"
type="checkbox"
checked={panOnScroll}
onChange={(event) => setPanOnScroll(event.target.checked)}
className="react-flow__panonscroll"
/>
</label>
</div>
<div>
<label htmlFor="zoomondbl">
zoom on double click
zoomOnDoubleClick
<input
id="zoomondbl"
type="checkbox"
@@ -115,7 +129,7 @@ const InteractionFlow = () => {
</div>
<div>
<label htmlFor="panemoveable">
pane moveable
paneMoveable
<input
id="panemoveable"
type="checkbox"
@@ -127,7 +141,7 @@ const InteractionFlow = () => {
</div>
<div>
<label htmlFor="capturezoompaneclick">
capture zoom pane click
capture onPaneClick
<input
id="capturezoompaneclick"
type="checkbox"
@@ -139,7 +153,7 @@ const InteractionFlow = () => {
</div>
<div>
<label htmlFor="capturezoompanescroll">
capture zoom pane scroll
capture onPaneScroll
<input
id="capturezoompanescroll"
type="checkbox"
@@ -151,7 +165,7 @@ const InteractionFlow = () => {
</div>
<div>
<label htmlFor="captureelementclick">
capture element click
capture onElementClick
<input
id="captureelementclick"
type="checkbox"

View File

@@ -36,7 +36,9 @@ const FlowRenderer = ({
onMoveStart,
onMoveEnd,
selectionKeyCode,
elementsSelectable,
zoomOnScroll,
panOnScroll,
zoomOnDoubleClick,
paneMoveable,
defaultPosition,
@@ -61,7 +63,9 @@ const FlowRenderer = ({
onMoveStart,
onMoveEnd,
selectionKeyPressed,
elementsSelectable,
zoomOnScroll,
panOnScroll,
zoomOnDoubleClick,
paneMoveable,
defaultPosition,

View File

@@ -70,6 +70,7 @@ export interface GraphViewProps {
arrowHeadColor: string;
markerEndId?: string;
zoomOnScroll?: boolean;
panOnScroll?: boolean;
zoomOnDoubleClick?: boolean;
paneMoveable?: boolean;
}
@@ -118,6 +119,7 @@ const GraphView = ({
arrowHeadColor,
markerEndId,
zoomOnScroll,
panOnScroll,
zoomOnDoubleClick,
paneMoveable,
onPaneClick,
@@ -245,11 +247,13 @@ const GraphView = ({
onElementsRemove={onElementsRemove}
deleteKeyCode={deleteKeyCode}
selectionKeyCode={selectionKeyCode}
elementsSelectable={elementsSelectable}
onMove={onMove}
onMoveStart={onMoveStart}
onMoveEnd={onMoveEnd}
zoomOnScroll={zoomOnScroll}
zoomOnDoubleClick={zoomOnDoubleClick}
panOnScroll={panOnScroll}
paneMoveable={paneMoveable}
defaultPosition={defaultPosition}
defaultZoom={defaultZoom}

View File

@@ -98,6 +98,7 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'on
arrowHeadColor?: string;
markerEndId?: string;
zoomOnScroll?: boolean;
panOnScroll?: boolean;
zoomOnDoubleClick?: boolean;
}
@@ -147,6 +148,7 @@ const ReactFlow = ({
arrowHeadColor = '#b1b1b7',
markerEndId,
zoomOnScroll = true,
panOnScroll = false,
zoomOnDoubleClick = true,
paneMoveable = true,
onPaneClick,
@@ -203,6 +205,7 @@ const ReactFlow = ({
markerEndId={markerEndId}
zoomOnScroll={zoomOnScroll}
zoomOnDoubleClick={zoomOnDoubleClick}
panOnScroll={panOnScroll}
paneMoveable={paneMoveable}
onPaneClick={onPaneClick}
onPaneScroll={onPaneScroll}

View File

@@ -6,7 +6,9 @@ import { FlowTransform, TranslateExtent } from '../types';
interface UseD3ZoomParams {
zoomPane: MutableRefObject<Element | null>;
selectionKeyPressed: boolean;
elementsSelectable?: boolean;
zoomOnScroll?: boolean;
panOnScroll?: boolean;
zoomOnDoubleClick?: boolean;
paneMoveable?: boolean;
defaultPosition?: [number, number];
@@ -34,8 +36,10 @@ export default ({
onMoveStart,
onMoveEnd,
zoomOnScroll = true,
panOnScroll = false,
zoomOnDoubleClick = true,
selectionKeyPressed,
elementsSelectable,
paneMoveable = true,
defaultPosition = [0, 0],
defaultZoom = 1,
@@ -46,6 +50,7 @@ export default ({
const initD3 = useStoreActions((actions) => actions.initD3);
const updateTransform = useStoreActions((actions) => actions.updateTransform);
const updateTransformDelta = useStoreActions((actions) => actions.updateTransformDelta);
useEffect(() => {
if (zoomPane.current) {
@@ -55,20 +60,22 @@ export default ({
useEffect(() => {
if (d3Zoom) {
if (selectionKeyPressed) {
d3Zoom.on('zoom', null);
} else {
d3Zoom.on('zoom', (event: any) => {
d3Zoom.on('zoom', (event: any) => {
if (panOnScroll && event.sourceEvent.type === 'wheel') {
const { deltaX, deltaY } = event.sourceEvent;
updateTransformDelta({ x: deltaX, y: deltaY });
} else {
updateTransform(event.transform);
// todo: should we move this into the store or into a wrapper component with useEffect?
if (onMove) {
const flowTransform = eventToFlowTransform(event.transform);
onMove(flowTransform);
}
});
}
}
});
}
}, [selectionKeyPressed, d3Zoom, updateTransform, onMove]);
}, [selectionKeyPressed, panOnScroll, d3Zoom, updateTransform, updateTransformDelta, onMove]);
useEffect(() => {
if (d3Zoom) {
@@ -107,34 +114,44 @@ export default ({
useEffect(() => {
if (d3Zoom) {
d3Zoom.filter((event: any) => {
if (selectionKeyPressed) {
// if all interactions are disabled, we prevent all zoom events
if (!paneMoveable && !zoomOnScroll && !panOnScroll && !zoomOnDoubleClick) {
return false;
}
// only allow zoom on nodes
if (event.target.closest('.react-flow__node') && event.type !== 'wheel') {
return false;
}
// only allow zoom on user selection
if (event.target.closest('.react-flow__nodesselection') && event.type !== 'wheel') {
return false;
}
if (!paneMoveable) {
return false;
}
if (!zoomOnScroll && event.type === 'wheel') {
// during a selection we prevent all other interactions
if (elementsSelectable && selectionKeyPressed) {
return false;
}
// if zoom on double click is disabled, we prevent the double click event
if (!zoomOnDoubleClick && event.type === 'dblclick') {
return false;
}
// when the target element is a node, we still allow zooming
if (event.target.closest('.react-flow__node') && event.type !== 'wheel') {
return false;
}
// when the target element is a node selection, we still allow zooming
if (event.target.closest('.react-flow__nodesselection') && event.type !== 'wheel') {
return false;
}
// when there is no scroll handling enabled, we prevent all wheel events
if (!zoomOnScroll && !panOnScroll && event.type === 'wheel') {
return false;
}
// if the pane is not movable, we prevent dragging it with the mouse
if (!paneMoveable && event.type === 'mousedown') {
return false;
}
// default filter for d3-zoom, prevents zooming on buttons and when ctrl is pressed
return !event.ctrlKey && !event.button;
});
}
}, [d3Zoom, zoomOnScroll, zoomOnDoubleClick, paneMoveable, selectionKeyPressed]);
}, [d3Zoom, zoomOnScroll, panOnScroll, zoomOnDoubleClick, paneMoveable, selectionKeyPressed, elementsSelectable]);
};

View File

@@ -37,6 +37,12 @@ type TransformXYK = {
k: number;
};
type TransformDeltas = {
x?: number;
y?: number;
k?: number;
};
type NodeDimensionUpdate = {
id: ElementId;
nodeElement: HTMLDivElement;
@@ -109,6 +115,7 @@ export interface StoreModel {
setSelectedElements: Action<StoreModel, Elements | Node | Edge>;
updateTransform: Action<StoreModel, TransformXYK>;
updateTransformDelta: Action<StoreModel, TransformDeltas>;
setInitTransform: Action<StoreModel, TransformXYK>;
@@ -351,6 +358,20 @@ export const storeModel: StoreModel = {
state.transform[2] = transform.k;
}),
updateTransformDelta: action((state, deltas) => {
const { x = 0, y = 0, k = 0 } = deltas;
state.transform[0] += x;
state.transform[1] += y;
state.transform[2] += k;
if (state.d3Selection) {
const updatedTransform = zoomIdentity.translate(state.transform[0], state.transform[1]).scale(state.transform[2]);
// we need to sync the d3 zoom transform with the updated transform
state.d3Selection.property('__zoom', updatedTransform);
}
}),
setInitTransform: action((state, transform) => {
state.transform[0] = transform.x;
state.transform[1] = transform.y;