Skip to content

Commit

Permalink
Add drag pan type (#256)
Browse files Browse the repository at this point in the history
* Add drag pan type

* Add wheel scroll zoom with drag pan

* Hide scroll bars on drag pan

* Remove react-zoom-pan-pinch

* Use react-gesture onWheel for zooming
  • Loading branch information
ghsteff authored Jul 8, 2024
1 parent 6701938 commit c4db788
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 122 deletions.
45 changes: 0 additions & 45 deletions docs/Advanced/Plugins.mdx

This file was deleted.

22 changes: 0 additions & 22 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@
"prettier": "^3.2.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-zoom-pan-pinch": "^3.1.0",
"rollup-plugin-peer-deps-external": "2.2.4",
"storybook": "^7.6.17",
"typescript": "^4.9.5",
Expand Down
9 changes: 9 additions & 0 deletions src/Canvas.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,12 @@
.dragNode {
pointer-events: none;
}

.hideScrollbars {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* Internet Explorer 10+ */
}

.hideScrollbars::-webkit-scrollbar {
display: none; /* WebKit */
}
50 changes: 46 additions & 4 deletions src/Canvas.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { FC, ReactElement, Ref, useImperativeHandle, forwardRef, useLayoutEffect, useRef, Fragment, useMemo, useState, useCallback, useEffect } from 'react';
import { useId, CloneElement } from 'reablocks';
import { useGesture } from 'react-use-gesture';
import { Node, NodeDragType, NodeProps } from './symbols/Node';
import { Edge, EdgeProps } from './symbols/Edge';
import { ElkRoot, CanvasDirection, LayoutResult, ElkCanvasLayoutOptions } from './layout';
Expand Down Expand Up @@ -38,6 +39,11 @@ export interface CanvasContainerProps extends CanvasProps {
*/
pannable?: boolean;

/**
* Type of interaction to use for panning.
*/
panType?: 'scroll' | 'drag';

/**
* Whether the canvas is zoomable or not.
*/
Expand Down Expand Up @@ -180,7 +186,7 @@ export type CanvasRef = LayoutResult & ZoomResult;

const InternalCanvas: FC<CanvasProps & { ref?: Ref<CanvasRef> }> = forwardRef(({ className, height = '100%', width = '100%', readonly, disabled = false, animated = true, arrow = <MarkerArrow />, node = <Node />, edge = <Edge />, dragNode = <Node />, dragEdge = <Edge />, onMouseEnter = () => undefined, onMouseLeave = () => undefined, onCanvasClick = () => undefined }, ref: Ref<CanvasRef>) => {
const id = useId();
const { pannable, dragCoords, dragNode: canvasDragNode, layout, containerRef, svgRef, canvasHeight, canvasWidth, xy, zoom, setZoom, observe, zoomIn, zoomOut, positionCanvas, fitCanvas, setScrollXY, ...rest } = useCanvas();
const { pannable, dragCoords, dragNode: canvasDragNode, layout, containerRef, svgRef, canvasHeight, canvasWidth, xy, zoom, setZoom, observe, zoomIn, zoomOut, positionCanvas, fitCanvas, setScrollXY, panType, ...rest } = useCanvas();
const [dragType, setDragType] = useState<null | NodeDragType>(null);

useImperativeHandle(ref, () => ({
Expand All @@ -202,6 +208,8 @@ const InternalCanvas: FC<CanvasProps & { ref?: Ref<CanvasRef> }> = forwardRef(({
}));

const mount = useRef<boolean>(false);
const panStartScrollPosition = useRef<{ x: number; y: number }>({ x: 0, y: 0 });

const dragNodeData = useMemo(() => getDragNodeData(canvasDragNode, layout?.children), [canvasDragNode, layout?.children]);
const [dragNodeDataWithChildren, setDragNodeDataWithChildren] = useState<{
[key: string]: any;
Expand All @@ -213,6 +221,39 @@ const InternalCanvas: FC<CanvasProps & { ref?: Ref<CanvasRef> }> = forwardRef(({
}
}, [layout, xy]);

useGesture(
{
onDrag: ({ movement: [mx, my] }) => {
// Update container scroll position during drag
if (containerRef.current) {
containerRef.current.scrollLeft = panStartScrollPosition.current.x - mx;
containerRef.current.scrollTop = panStartScrollPosition.current.y - my;
}
},
onDragStart: () => {
// Store the initial scroll position of the container when drag starts
panStartScrollPosition.current = {
x: containerRef.current?.scrollLeft || 0,
y: containerRef.current?.scrollTop || 0
};
},
onWheel: ({ event, delta, last }) => {
!last && event.preventDefault();

if (delta[1] > 0) {
zoomOut();
} else {
zoomIn();
}
}
},
{
enabled: pannable && panType === 'drag',
eventOptions: { passive: false },
domTarget: containerRef
}
);

const onDragStart = useCallback((event) => {
setDragType(event.dragType);
}, []);
Expand Down Expand Up @@ -248,7 +289,8 @@ const InternalCanvas: FC<CanvasProps & { ref?: Ref<CanvasRef> }> = forwardRef(({
<div
style={{ height, width }}
className={classNames(css.container, className, {
[css.pannable]: pannable
[css.pannable]: pannable,
[css.hideScrollbars]: panType === 'drag'
})}
ref={(el) => {
// Really not a fan of this API change...
Expand Down Expand Up @@ -343,8 +385,8 @@ const InternalCanvas: FC<CanvasProps & { ref?: Ref<CanvasRef> }> = forwardRef(({
);
});

export const Canvas: FC<CanvasContainerProps & { ref?: Ref<CanvasRef> }> = forwardRef(({ selections = [], readonly = false, fit = false, nodes = [], edges = [], maxHeight = 2000, maxWidth = 2000, direction = 'DOWN', pannable = true, zoom = 1, defaultPosition = CanvasPosition.CENTER, zoomable = true, minZoom = -0.5, maxZoom = 1, onNodeLink = () => undefined, onNodeLinkCheck = () => undefined, onLayoutChange = () => undefined, onZoomChange = () => undefined, layoutOptions, ...rest }, ref: Ref<CanvasRef>) => (
<CanvasProvider layoutOptions={layoutOptions} nodes={nodes} edges={edges} zoom={zoom} defaultPosition={defaultPosition} minZoom={minZoom} maxZoom={maxZoom} fit={fit} maxHeight={maxHeight} maxWidth={maxWidth} direction={direction} pannable={pannable} zoomable={zoomable} readonly={readonly} onLayoutChange={onLayoutChange} selections={selections} onZoomChange={onZoomChange} onNodeLink={onNodeLink} onNodeLinkCheck={onNodeLinkCheck}>
export const Canvas: FC<CanvasContainerProps & { ref?: Ref<CanvasRef> }> = forwardRef(({ selections = [], readonly = false, fit = false, nodes = [], edges = [], maxHeight = 2000, maxWidth = 2000, direction = 'DOWN', pannable = true, panType = 'scroll', zoom = 1, defaultPosition = CanvasPosition.CENTER, zoomable = true, minZoom = -0.5, maxZoom = 1, onNodeLink = () => undefined, onNodeLinkCheck = () => undefined, onLayoutChange = () => undefined, onZoomChange = () => undefined, layoutOptions, ...rest }, ref: Ref<CanvasRef>) => (
<CanvasProvider layoutOptions={layoutOptions} nodes={nodes} edges={edges} zoom={zoom} defaultPosition={defaultPosition} minZoom={minZoom} maxZoom={maxZoom} fit={fit} maxHeight={maxHeight} maxWidth={maxWidth} direction={direction} pannable={pannable} panType={panType} zoomable={zoomable} readonly={readonly} onLayoutChange={onLayoutChange} selections={selections} onZoomChange={onZoomChange} onNodeLink={onNodeLink} onNodeLinkCheck={onNodeLinkCheck}>
<InternalCanvas ref={ref} {...rest} />
</CanvasProvider>
));
4 changes: 4 additions & 0 deletions src/utils/CanvasProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface CanvasProviderValue
selections?: string[];
readonly?: boolean;
pannable: boolean;
panType: 'scroll' | 'drag';
}

export const CanvasContext = createContext<CanvasProviderValue>({} as any);
Expand Down Expand Up @@ -43,6 +44,7 @@ export const CanvasProvider = ({
direction,
layoutOptions,
pannable,
panType,
defaultPosition,
zoomable,
zoom,
Expand All @@ -67,6 +69,7 @@ export const CanvasProvider = ({
maxWidth,
direction,
pannable,
panType,
defaultPosition,
fit,
layoutOptions,
Expand All @@ -86,6 +89,7 @@ export const CanvasProvider = ({
selections,
readonly,
pannable,
panType,
...layoutProps,
...zoomProps,
...dragProps
Expand Down
85 changes: 35 additions & 50 deletions stories/Controls.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { useRef, useState } from 'react';
import { Canvas, CanvasRef } from '../src/Canvas';
import { Node, Edge, MarkerArrow, Port, Icon, Arrow, Label, Remove, Add } from '../src/symbols';
import { TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch';
import { CanvasPosition } from '../src/types';

export default {
Expand Down Expand Up @@ -266,52 +265,38 @@ export const Zoom = () => {
);
};

export const ZoomExternal = () => {
const ref = useRef<CanvasRef | null>(null);

return (
<TransformWrapper
wheel={{step: 40}}
options={{
maxScale: 4,
limitToBounds: false,
}}
>
<TransformComponent>
<Canvas
ref={ref}
zoomable={false}
maxWidth={800}
maxHeight={800}
fit={true}
nodes={[
{
id: '1',
text: 'Node 1'
},
{
id: '2',
text: 'Node 2'
},
{
id: '3',
text: 'Node 3'
}
]}
edges={[
{
id: '1-2',
from: '1',
to: '2'
},
{
id: '1-3',
from: '1',
to: '3'
}
]}
/>
</TransformComponent>
</TransformWrapper>
);
};
export const DragPan = () => (
<div style={{ position: 'absolute', top: 0, bottom: 0, left: 0, right: 0 }}>
<Canvas
fit={true}
panType="drag"
nodes={[
{
id: '1',
text: 'Node 1'
},
{
id: '2',
text: 'Node 2'
},
{
id: '3',
text: 'Node 3'
}
]}
edges={[
{
id: '1-2',
from: '1',
to: '2'
},
{
id: '1-3',
from: '1',
to: '3'
}
]}
onLayoutChange={(layout) => console.log('Layout', layout)}
/>
</div>
);

0 comments on commit c4db788

Please sign in to comment.