212 lines
5.6 KiB
TypeScript
212 lines
5.6 KiB
TypeScript
/*
|
|
@source https://github.com/ajainarayanan/react-pan-zoom
|
|
|
|
Heavily inspired/lifted from this idea: https://stackoverflow.com/a/39311435/661768
|
|
without jqueryUI or jquery dependency.
|
|
*/
|
|
import React, {
|
|
FC, memo, useEffect, useRef, useState,
|
|
} from '../../lib/teact/teact';
|
|
|
|
import { areSortedArraysEqual } from '../../util/iteratees';
|
|
|
|
import './PanZoom.scss';
|
|
|
|
export interface IDragData {
|
|
x: number;
|
|
y: number;
|
|
dx: number;
|
|
dy: number;
|
|
}
|
|
|
|
export interface OwnProps {
|
|
children: any;
|
|
className?: string;
|
|
noWrap: boolean;
|
|
canPan: boolean;
|
|
zoomLevel: number;
|
|
panDeltaX: number;
|
|
panDeltaY: number;
|
|
onPan?: (x: number, y: number) => void;
|
|
}
|
|
|
|
const INITIAL_MATRIX = [
|
|
1, 0, 0, 1, 0, 0,
|
|
];
|
|
|
|
const SCALE_VALUES = {
|
|
1: 1,
|
|
1.5: 1.5,
|
|
2: 2.2,
|
|
2.5: 3.3,
|
|
3: 5.5,
|
|
};
|
|
|
|
const ZOOM_SAFE_AREA = 150;
|
|
|
|
function calculateSafeZoneOnZoom(oldScale: number, matrixData: number[], wrapper: HTMLDivElement | null) {
|
|
const image = wrapper && wrapper.querySelector('.active img');
|
|
if (!wrapper || !image) {
|
|
return matrixData;
|
|
}
|
|
const wrapperRect = wrapper.getBoundingClientRect();
|
|
const imageRect = image.getBoundingClientRect();
|
|
|
|
const newImgWidth = (imageRect.width / oldScale) * matrixData[0];
|
|
const newImgHeight = (imageRect.height / oldScale) * matrixData[3];
|
|
const newImgX = (wrapperRect.width - newImgWidth) / 2 + matrixData[4];
|
|
const newImgY = (wrapperRect.height - newImgHeight) / 2 + matrixData[5];
|
|
if (wrapperRect.width && wrapperRect.width - ZOOM_SAFE_AREA < newImgX) {
|
|
matrixData[4] -= newImgX + wrapperRect.width - ZOOM_SAFE_AREA;
|
|
} else if (newImgWidth && newImgWidth + newImgX < ZOOM_SAFE_AREA) {
|
|
matrixData[4] -= newImgWidth + newImgX - ZOOM_SAFE_AREA;
|
|
}
|
|
if (wrapperRect.height && wrapperRect.height - ZOOM_SAFE_AREA < newImgY) {
|
|
matrixData[5] -= newImgY + wrapperRect.height - ZOOM_SAFE_AREA;
|
|
} else if (newImgHeight && newImgHeight + newImgY < ZOOM_SAFE_AREA) {
|
|
matrixData[5] -= newImgHeight + newImgY - ZOOM_SAFE_AREA;
|
|
}
|
|
|
|
return matrixData;
|
|
}
|
|
|
|
const PanZoom: FC<OwnProps> = ({
|
|
children,
|
|
className,
|
|
noWrap,
|
|
canPan,
|
|
zoomLevel,
|
|
panDeltaX,
|
|
panDeltaY,
|
|
onPan,
|
|
}) => {
|
|
const tunedZoomLevel = SCALE_VALUES[zoomLevel as keyof typeof SCALE_VALUES] || zoomLevel;
|
|
const [isDragging, setIsDragging] = useState<boolean>(false);
|
|
const [dragData, setDragData] = useState<IDragData>({
|
|
dx: panDeltaX, dy: panDeltaY, x: 0, y: 0,
|
|
});
|
|
// [zoom, skew, skew, zoom, dx, dy] - see https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/matrix()
|
|
const [matrixData, setMatrixData] = useState<number[]>(INITIAL_MATRIX);
|
|
// Used to set cursor while moving.
|
|
// eslint-disable-next-line no-null/no-null
|
|
const panWrapperRef = useRef<HTMLDivElement>(null);
|
|
// Used to set transform for pan.
|
|
// eslint-disable-next-line no-null/no-null
|
|
const panContainerRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
const newZoomLevel = tunedZoomLevel || matrixData[0];
|
|
const newPandx = panDeltaX || matrixData[4];
|
|
const newPandy = panDeltaY || matrixData[5];
|
|
|
|
const newMatrixData = [...matrixData];
|
|
if (matrixData[0] !== newZoomLevel) {
|
|
newMatrixData[0] = newZoomLevel || newMatrixData[0];
|
|
newMatrixData[3] = newZoomLevel || newMatrixData[3];
|
|
}
|
|
if (matrixData[4] !== newPandx) {
|
|
newMatrixData[4] = newPandx;
|
|
}
|
|
if (matrixData[5] !== newPandy) {
|
|
newMatrixData[5] = newPandy;
|
|
}
|
|
|
|
if (!areSortedArraysEqual(matrixData, newMatrixData)) {
|
|
setMatrixData(calculateSafeZoneOnZoom(matrixData[0], newMatrixData, panWrapperRef.current));
|
|
}
|
|
// eslint-disable-next-line
|
|
}, [panDeltaX, panDeltaY, tunedZoomLevel]);
|
|
|
|
useEffect(() => {
|
|
if (!canPan) {
|
|
setMatrixData(INITIAL_MATRIX);
|
|
}
|
|
}, [canPan]);
|
|
|
|
useEffect(() => {
|
|
if (panContainerRef.current) {
|
|
panContainerRef.current.style.transform = `matrix(${matrixData.toString()})`;
|
|
}
|
|
}, [noWrap, matrixData]);
|
|
|
|
const handleMouseDown = (e: React.MouseEvent<EventTarget>) => {
|
|
if (!canPan) {
|
|
return;
|
|
}
|
|
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
|
|
const offsetX = matrixData[4];
|
|
const offsetY = matrixData[5];
|
|
const newDragData: IDragData = {
|
|
dx: offsetX,
|
|
dy: offsetY,
|
|
x: e.pageX,
|
|
y: e.pageY,
|
|
};
|
|
setDragData(newDragData);
|
|
setIsDragging(true);
|
|
|
|
if (panWrapperRef.current) {
|
|
panWrapperRef.current.classList.add('move');
|
|
}
|
|
};
|
|
|
|
const handleMouseUp = () => {
|
|
setIsDragging(false);
|
|
|
|
if (panWrapperRef.current) {
|
|
panWrapperRef.current.classList.remove('move');
|
|
}
|
|
|
|
if (onPan) {
|
|
onPan(matrixData[4], matrixData[5]);
|
|
}
|
|
};
|
|
|
|
function getNewMatrixData(x: number, y: number): number[] {
|
|
const newMatrixData = [...matrixData];
|
|
const deltaX = dragData.x - x;
|
|
const deltaY = dragData.y - y;
|
|
newMatrixData[4] = dragData.dx - deltaX;
|
|
newMatrixData[5] = dragData.dy - deltaY;
|
|
|
|
return newMatrixData;
|
|
}
|
|
|
|
const handleMouseMove = (e: React.MouseEvent<EventTarget>) => {
|
|
if (isDragging) {
|
|
const newMatrixData = getNewMatrixData(e.pageX, e.pageY);
|
|
setMatrixData(newMatrixData);
|
|
|
|
if (panContainerRef.current) {
|
|
panContainerRef.current.style.transform = `matrix(${matrixData.toString()})`;
|
|
}
|
|
}
|
|
};
|
|
|
|
if (noWrap) {
|
|
return children;
|
|
}
|
|
|
|
return (
|
|
<div
|
|
ref={panWrapperRef}
|
|
className={`pan-wrapper ${className || ''}`}
|
|
onMouseDown={handleMouseDown}
|
|
onMouseUp={handleMouseUp}
|
|
onMouseMove={handleMouseMove}
|
|
>
|
|
<div
|
|
ref={panContainerRef}
|
|
className="pan-container"
|
|
>
|
|
{children}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default memo(PanZoom);
|