@eccenca/gui-elements
Version:
GUI elements based on other libraries, usable in React application, written in Typescript.
287 lines (261 loc) • 10.8 kB
text/typescript
import React, { MouseEvent as ReactMouseEvent, useCallback } from "react";
import { OnLoadParams, useStoreState } from "react-flow-renderer";
import {
Edge,
Node,
OnConnectStartFunc,
OnConnectStartParams,
OnConnectStopFunc,
Transform,
} from "react-flow-renderer/dist/types";
import { ReactFlowExtendedScrollProps } from "../ReactFlow/ReactFlow";
import { ReactFlowV9ContainerProps } from "../ReactFlow/ReactFlowV9";
interface IProps extends ReactFlowExtendedScrollProps {
/** The original react-flow props. */
reactFlowProps: ReactFlowV9ContainerProps;
}
export interface ScrollStateShared {
// The current x position of the react-flow view
currentX: number;
// The current y position of the react-flow view
currentY: number;
// The current Zoom level
currentZoom: number;
// The current scroll function callback, when scrolling is active
scrollTaskId?: NodeJS.Timeout;
// If a warning of the react-flow instance with the given ID has not been found
loggedWarning: boolean;
// If the x-axis is currently being scrolled
scrollX: boolean;
// If the y-axis is currently being scrolled
scrollY: boolean;
// Only if this is true the canvas will scroll when moving the mouse past it
draggingOperationActive: boolean;
}
interface ScrollState extends ScrollStateShared {
// The react-flow instance
reactFlowInstance?: OnLoadParams;
}
type ReturnType = Pick<
ReactFlowV9ContainerProps,
| "onLoad"
| "onNodeDragStart"
| "onNodeDragStop"
| "onConnectStart"
| "onConnectStop"
| "onSelectionDragStart"
| "onSelectionDragStop"
| "onEdgeUpdateStart"
| "onEdgeUpdateEnd"
>;
/** Handles the scrolling of the react-flow canvas on all drag operations when the mouse pointer gets near or over the borders.
* The return value contains the wrapped react-flow callback functions that need to be handed over to the react-flow component. */
export const useReactFlowScrollOnDragV9 = ({ reactFlowProps, scrollOnDrag }: IProps): ReturnType => {
/** Tracks the zoom on drag to border functionality. */
const scrollState = React.useRef<ScrollState>({
reactFlowInstance: undefined,
currentX: 0,
currentY: 0,
currentZoom: 1,
loggedWarning: false,
scrollX: false,
scrollY: false,
draggingOperationActive: false,
});
const useStoreStateInternal = (): Transform => {
try {
return useStoreState((state) => state.transform);
} catch (ex) {
if (reactFlowProps.id && scrollOnDrag) {
// eslint-disable-next-line no-console
console.warn("Scroll on drag is not correctly working. Reason: " + ex);
}
return [0, 0, 1];
}
};
/** The current position and zoom factor of the view port. */
const [currentX, currentY, currentZoom] = useStoreStateInternal();
scrollState.current.currentX = currentX;
scrollState.current.currentY = currentY;
scrollState.current.currentZoom = currentZoom;
const originalOnLoad = reactFlowProps.onLoad;
const originalOnNodeDragStart = reactFlowProps.onNodeDragStart;
const originalOnNodeDragStop = reactFlowProps.onNodeDragStop;
const originalOnConnectStart = reactFlowProps.onConnectStart;
const originalOnConnectStop = reactFlowProps.onConnectStop;
const originalOnSelectionDragStart = reactFlowProps.onSelectionDragStart;
const originalOnSelectionDragStop = reactFlowProps.onSelectionDragStop;
const originalOnEdgeUpdateStart = reactFlowProps.onEdgeUpdateStart;
const originalOnEdgeUpdateEnd = reactFlowProps.onEdgeUpdateEnd;
const scrollInterval = scrollOnDrag?.scrollInterval;
const scrollStepSize = scrollOnDrag?.scrollStepSize;
const reactFlowInstanceId = reactFlowProps.id;
const clearIntervalIfExists = React.useCallback(() => {
if (scrollState.current.scrollTaskId) {
clearInterval(scrollState.current.scrollTaskId);
}
}, []);
const setScrolling = React.useCallback(
(active: boolean) => {
scrollState.current.draggingOperationActive = active;
if (!active) {
clearIntervalIfExists();
}
},
[clearIntervalIfExists]
);
// Handle scrolling if any operation is active e.g. connecting or dragging a node
React.useEffect(() => {
if (scrollInterval && scrollStepSize && reactFlowInstanceId) {
const handleScrolling = (event: MouseEvent) => {
const state = scrollState.current;
if (!state.draggingOperationActive) {
clearIntervalIfExists();
return;
}
// Check if mouse pointer is outside of the canvas
const canvasElement = document.getElementById(reactFlowInstanceId);
if (!canvasElement) {
if (!state.loggedWarning) {
// eslint-disable-next-line no-console
console.warn("No element found with ID " + reactFlowInstanceId);
state.loggedWarning = true;
}
return;
}
const boundingRect = canvasElement.getBoundingClientRect();
const xStepSize = boundingRect.width * scrollStepSize;
const yStepSize = boundingRect.height * scrollStepSize;
if (
boundingRect.top > event.clientY ||
boundingRect.bottom < event.clientY ||
boundingRect.left > event.clientX ||
boundingRect.right < event.clientX
) {
const scrollX: number =
boundingRect.left > event.clientX
? xStepSize
: boundingRect.right < event.clientX
? -xStepSize
: 0;
const scrollY: number =
boundingRect.top > event.clientY
? yStepSize
: boundingRect.bottom < event.clientY
? -yStepSize
: 0;
if (state.scrollY === (scrollY !== 0) && state.scrollX === (scrollX !== 0)) {
// Nothing has changed, do not change interval function
return;
}
clearIntervalIfExists();
state.scrollTaskId = setInterval(() => {
state.reactFlowInstance?.setTransform({
x: state.currentX + scrollX,
y: state.currentY + scrollY,
zoom: state.currentZoom,
});
}, scrollInterval);
} else {
clearIntervalIfExists();
}
};
const disableScrollingOnMouseUp = () => {
scrollState.current.draggingOperationActive = false;
clearIntervalIfExists();
};
document.addEventListener("mousemove", handleScrolling);
document.addEventListener("mouseup", disableScrollingOnMouseUp);
return () => {
document.removeEventListener("mousemove", handleScrolling);
document.removeEventListener("mouseup", disableScrollingOnMouseUp);
};
} else {
return undefined;
}
}, [scrollInterval, scrollStepSize, reactFlowInstanceId, clearIntervalIfExists]);
const onLoad = useCallback(
(rfi: OnLoadParams) => {
scrollState.current.reactFlowInstance = rfi;
originalOnLoad?.(rfi);
},
[originalOnLoad]
);
/** Wrap original callbacks to turn scrolling on and off. */
const onConnectStart: OnConnectStartFunc = React.useCallback(
(event: ReactMouseEvent, params: OnConnectStartParams) => {
setScrolling(true);
originalOnConnectStart?.(event, params);
},
[originalOnConnectStart, setScrolling]
);
const onConnectStop: OnConnectStopFunc = React.useCallback(
(event: MouseEvent) => {
setScrolling(false);
originalOnConnectStop?.(event);
},
[originalOnConnectStop, setScrolling]
);
const onNodeDragStart = React.useCallback(
(event: ReactMouseEvent, node: Node) => {
setScrolling(true);
originalOnNodeDragStart?.(event, node);
},
[originalOnNodeDragStart, setScrolling]
);
const onNodeDragStop = React.useCallback(
(event: ReactMouseEvent, node: Node) => {
setScrolling(false);
originalOnNodeDragStop?.(event, node);
},
[originalOnNodeDragStop, setScrolling]
);
const onSelectionDragStart = React.useCallback(
(event: ReactMouseEvent, nodes: Node[]) => {
setScrolling(true);
originalOnSelectionDragStart?.(event, nodes);
},
[originalOnSelectionDragStart, setScrolling]
);
const onSelectionDragStop = React.useCallback(
(event: ReactMouseEvent, nodes: Node[]) => {
setScrolling(false);
originalOnSelectionDragStop?.(event, nodes);
},
[originalOnSelectionDragStop, setScrolling]
);
const onEdgeUpdateStart = React.useCallback(
(event: ReactMouseEvent, edge: Edge) => {
setScrolling(true);
originalOnEdgeUpdateStart?.(event, edge);
},
[originalOnEdgeUpdateStart, setScrolling]
);
const onEdgeUpdateEnd = React.useCallback(
(event: MouseEvent, edge: Edge) => {
setScrolling(false);
originalOnEdgeUpdateEnd?.(event, edge);
},
[originalOnEdgeUpdateEnd, setScrolling]
);
if (!reactFlowProps.id || !scrollOnDrag) {
// No instance ID or config available, return empty object that will not overwrite any react-flow config parameters
return {};
} else {
return {
onLoad,
onNodeDragStart,
onNodeDragStop,
onConnectStart,
onConnectStop,
onSelectionDragStart,
onSelectionDragStop,
onEdgeUpdateStart,
onEdgeUpdateEnd,
};
}
};
/**
* @deprecated (v26) Currently it ony supports ReactFlow v9. Better to `useReactFlowScrollOnDragV9` for now.
*/
export const useReactFlowScrollOnDrag = useReactFlowScrollOnDragV9;