react-inner-image-zoom
Version:
A React component for magnifying an image within its parent container 🔎
286 lines (248 loc) • 8.24 kB
JavaScript
import React, { forwardRef, Fragment, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
import Image from './components/Image';
import ZoomImage from './components/ZoomImage';
import FullscreenPortal from './components/FullscreenPortal';
import {
getBounds,
getFullscreenStatus,
getImgPropsDefaults,
getOffsets,
getRatios,
getScaledDimensions
} from 'inner-image-zoom/src/utils/images';
import {
getDragMovePositions,
getEventCoords,
getInitialDragCoords,
getIsValidDrag,
getMouseMovePositions
} from 'inner-image-zoom/src/utils/events';
const InnerImageZoom = forwardRef(
(
{
moveType = 'pan',
zoomType = 'click',
src,
sources,
width,
height,
hasSpacer,
imgAttributes = {},
zoomSrc,
zoomScale = 1,
zoomPreload,
fadeDuration = 150,
fullscreenOnMobile,
mobileBreakpoint = 640,
hideCloseButton,
hideHint,
className,
afterZoomIn,
afterZoomOut
},
ref
) => {
const container = useRef(null);
const zoomImg = useRef(null);
const portal = useRef(null);
const imgProps = useRef({});
const [isActive, setIsActive] = useState(zoomPreload);
const [isTouch, setIsTouch] = useState(false);
const [isZoomed, setIsZoomed] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const [isValidDrag, setIsValidDrag] = useState(false);
const [isFading, setIsFading] = useState(false);
const [currentMoveType, setCurrentMoveType] = useState(moveType);
const [left, setLeft] = useState(0);
const [top, setTop] = useState(0);
const handleMouseEnter = (e) => {
setIsActive(true);
setIsFading(false);
zoomType === 'hover' && !isZoomed && handleClick(e);
};
const handleTouchStart = () => {
setIsTouch(true);
setIsFullscreen(getFullscreenStatus(fullscreenOnMobile, mobileBreakpoint));
setCurrentMoveType('drag');
};
const handleClick = (e) => {
if (isZoomed) {
if (isTouch) {
hideCloseButton && handleClose(e);
} else {
!isValidDrag && zoomOut();
}
return;
}
isTouch && setIsActive(true);
if (zoomImg.current) {
handleLoad({ target: zoomImg.current });
zoomIn(e);
} else {
imgProps.current.onLoadCallback = zoomIn.bind(this, e);
}
};
const handleLoad = (e) => {
const scaledDimensions = getScaledDimensions(e.target, zoomScale);
zoomImg.current = e.target;
zoomImg.current.setAttribute('width', scaledDimensions.width);
zoomImg.current.setAttribute('height', scaledDimensions.height);
imgProps.current.scaledDimensions = scaledDimensions;
imgProps.current.bounds = getBounds(container.current, false);
imgProps.current.ratios = getRatios(imgProps.current.bounds, scaledDimensions);
if (imgProps.current.onLoadCallback) {
imgProps.current.onLoadCallback();
imgProps.current.onLoadCallback = null;
}
};
const handleMouseMove = (e) => {
const positions = getMouseMovePositions(e, imgProps.current);
setLeft(positions.left);
setTop(positions.top);
};
const handleDragStart = (e) => {
const coords = getEventCoords(e);
imgProps.current.offsets = getOffsets(coords.x, coords.y, zoomImg.current.offsetLeft, zoomImg.current.offsetTop);
setIsDragging(true);
if (!isTouch) {
imgProps.current.eventPosition = coords;
}
};
const handleDragMove = useCallback((e) => {
e.stopPropagation();
const positions = getDragMovePositions(e, imgProps.current);
setLeft(positions.left);
setTop(positions.top);
}, []);
const handleDragEnd = (e) => {
setIsDragging(false);
if (!isTouch) {
setIsValidDrag(getIsValidDrag(e, imgProps.current));
}
};
const handleMouseLeave = (e) => {
currentMoveType === 'drag' && isZoomed ? handleDragEnd(e) : handleClose(e);
};
const handleClose = (e) => {
if (!(!isTouch && e.target.classList.contains('iiz__close'))) {
if (!isZoomed || isFullscreen || !fadeDuration) {
handleFadeOut({}, true);
} else {
setIsFading(true);
}
}
zoomOut();
};
const handleFadeOut = (e, noTransition) => {
if (noTransition || (e.propertyName === 'opacity' && container.current.contains(e.target))) {
if ((zoomPreload && isTouch) || !zoomPreload) {
zoomImg.current = null;
imgProps.current = getImgPropsDefaults();
setIsActive(false);
}
setIsTouch(false);
setIsFullscreen(false);
setCurrentMoveType(moveType);
setIsFading(false);
}
};
const initialMove = (e) => {
imgProps.current.offsets = getOffsets(
window.pageXOffset,
window.pageYOffset,
-imgProps.current.bounds.left,
-imgProps.current.bounds.top
);
handleMouseMove(e);
};
const initialDrag = (e) => {
const initialDragCoords = getInitialDragCoords(e, imgProps.current, isFullscreen);
imgProps.current.bounds = getBounds(container.current, isFullscreen);
imgProps.current.offsets = getOffsets(0, 0, 0, 0);
handleDragMove({
...initialDragCoords,
stopPropagation: () => {}
});
};
const zoomIn = (e) => {
setIsZoomed(true);
currentMoveType === 'drag' ? initialDrag(e) : initialMove(e);
afterZoomIn && afterZoomIn();
};
const zoomOut = () => {
setIsZoomed(false);
afterZoomOut && afterZoomOut();
};
const zoomImageProps = {
src: zoomSrc || src,
fadeDuration: isFullscreen ? 0 : fadeDuration,
top,
left,
isZoomed,
onLoad: handleLoad,
onDragStart: currentMoveType === 'drag' ? handleDragStart : null,
onDragEnd: currentMoveType === 'drag' ? handleDragEnd : null,
onClose: !hideCloseButton && currentMoveType === 'drag' ? handleClose : null,
onFadeOut: isFading ? handleFadeOut : null
};
useEffect(() => {
imgProps.current = getImgPropsDefaults();
}, []);
useEffect(() => {
getFullscreenStatus(fullscreenOnMobile, mobileBreakpoint) && setIsActive(false);
}, [fullscreenOnMobile, mobileBreakpoint]);
useEffect(() => {
if (!zoomImg.current) {
return;
}
const eventType = isTouch ? 'touchmove' : 'mousemove';
if (isDragging) {
zoomImg.current.addEventListener(eventType, handleDragMove, { passive: true });
} else {
zoomImg.current.removeEventListener(eventType, handleDragMove);
}
}, [isDragging, isTouch, handleDragMove]);
useImperativeHandle(ref, () => ({
container: container.current,
portal: portal.current
}));
return (
<figure
className={`iiz ${currentMoveType === 'drag' ? 'iiz--drag' : ''} ${className ? className : ''}`}
style={{ width: width }}
ref={container}
onTouchStart={isZoomed ? null : handleTouchStart}
onClick={handleClick}
onMouseEnter={isTouch ? null : handleMouseEnter}
onMouseMove={currentMoveType === 'drag' || !isZoomed ? null : handleMouseMove}
onMouseLeave={isTouch ? null : handleMouseLeave}
>
<Image
src={src}
sources={sources}
width={width}
height={height}
hasSpacer={hasSpacer}
imgAttributes={imgAttributes}
fadeDuration={fadeDuration}
isZoomed={isZoomed}
/>
{isActive && (
<Fragment>
{isFullscreen ? (
<FullscreenPortal ref={portal}>
<ZoomImage {...zoomImageProps} />
</FullscreenPortal>
) : (
<ZoomImage {...zoomImageProps} />
)}
</Fragment>
)}
{!hideHint && !isZoomed && <span className="iiz__btn iiz__hint"></span>}
</figure>
);
}
);
InnerImageZoom.displayName = 'InnerImageZoom';
export default InnerImageZoom;