UNPKG

easy-magnify

Version:

Everything you need for magnifying images

696 lines (685 loc) 24.7 kB
import React2, { useRef, useState, useCallback, useEffect } from 'react'; function useMouse(options = { resetOnExit: false }) { const [position, setPosition] = useState({ x: 0, y: 0 }); const ref = useRef(null); const requestRef = useRef(null); const positionRef = useRef(position); const setMousePosition = useCallback((event) => { if (ref.current) { const rect = event.currentTarget.getBoundingClientRect(); const x = Math.max( 0, Math.round(event.pageX - rect.left - (window.pageXOffset || window.scrollX)) ); const y = Math.max( 0, Math.round(event.pageY - rect.top - (window.pageYOffset || window.scrollY)) ); positionRef.current = { x, y }; } else { positionRef.current = { x: event.clientX, y: event.clientY }; } if (requestRef.current === null) { requestRef.current = requestAnimationFrame(() => { setPosition(positionRef.current); requestRef.current = null; }); } }, []); const resetMousePosition = useCallback(() => { positionRef.current = { x: 0, y: 0 }; setPosition({ x: 0, y: 0 }); }, []); useEffect(() => { const element = ref.current || document; element.addEventListener("mousemove", setMousePosition); if (options.resetOnExit) element.addEventListener("mouseleave", resetMousePosition); return () => { element.removeEventListener("mousemove", setMousePosition); if (options.resetOnExit) element.removeEventListener("mouseleave", resetMousePosition); if (requestRef.current !== null) { cancelAnimationFrame(requestRef.current); } }; }, [setMousePosition, resetMousePosition, options.resetOnExit]); return { ref, ...position }; } var use_mouse_default = useMouse; var EasySkeleton = (props) => { const { height, width, backgroundColor, animation, ...other } = props; return /* @__PURE__ */ React2.createElement( "div", { className: "easyPulseSkeleton", style: { backgroundColor: backgroundColor ?? "#0000001c", height: `${height}px`, width: `${width}px`, animation: animation ?? "pulse 2s ease-in-out 0.5s infinite" }, ...other } ); }; var Skeleton_default = EasySkeleton; // src/hooks/useStore/use-store.ts function createStore(initialState) { const listeners = /* @__PURE__ */ new Set(); let batching = false; let state = initialState; let updatedProperties; const setState = (extraState = {}) => { updatedProperties = { ...updatedProperties, ...extraState }; flush(); }; const flush = () => { if (batching) return; let hasChanged = false; if (updatedProperties) { for (const key in updatedProperties) { if (state[key] !== updatedProperties[key]) { hasChanged = true; break; } } } if (!hasChanged) { return; } state = { ...state, ...updatedProperties }; listeners.forEach((listener) => listener({ state, updatedProperties })); updatedProperties = void 0; }; const batch = (cb) => { batching = true; cb(); batching = false; flush(); }; const subscribe = (listener) => { listeners.add(listener); return () => { listeners.delete(listener); }; }; const cleanup = () => listeners.clear(); const getState = () => state; return { subscribe, cleanup, getState, setState, batch }; } // src/core/imageLoader.tsx var THRESHOLD = 50; var makeImageLoader = () => { const createZoomImage = (img, src, store) => { if (img.src === src) return; img.src = src; let complete = false; img.onload = () => { complete = true; store.setState({ zoomedImgStatus: "loaded" }); }; img.onerror = () => { complete = true; store.setState({ zoomedImgStatus: "error" }); }; setTimeout(() => { if (!complete) store.setState({ zoomedImgStatus: "loading" }); }, THRESHOLD); }; return { createZoomImage }; }; var imageLoader = makeImageLoader(); // src/core/clamp.ts function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } function noop() { } function preventDefault(event) { event.preventDefault(); } var keySet = /* @__PURE__ */ new Set(["ArrowUp", "ArrowRight", "ArrowDown", "ArrowLeft"]); function preventDefaultForScrollKeys(event) { if (keySet.has(event.key)) { preventDefault(event); return false; } } var controller; function disableScroll() { controller = new AbortController(); const { signal } = controller; window.addEventListener("DOMMouseScroll", preventDefault, { signal }); window.addEventListener("wheel", preventDefault, { passive: false, signal }); window.addEventListener("touchmove", preventDefault, { passive: false, signal }); window.addEventListener("keydown", preventDefaultForScrollKeys, { signal }); } function enableScroll() { controller?.abort(); } function getSourceImage(container) { if (!container) { throw new Error("Please specify a container for the zoom image"); } const sourceImgElement = container.querySelector("img"); if (!sourceImgElement) { throw new Error("Please place an image inside the container"); } return sourceImgElement; } function getPointersCenter(first, second) { return { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 }; } function computeZoomGesture(prev, curr) { const prevCenter = getPointersCenter(prev[0], prev[1]); const currCenter = getPointersCenter(curr[0], curr[1]); const centerDist = { x: currCenter.x - prevCenter.x, y: currCenter.y - prevCenter.y }; const prevDistance = Math.hypot(prev[0].x - prev[1].x, prev[0].y - prev[1].y); const currDistance = Math.hypot(curr[0].x - curr[1].x, curr[0].y - curr[1].y); let scale = currDistance / prevDistance; const eps = 1e-5; if (Math.abs(scale - 1) < eps) { scale = 1 + eps; } return { scale, center: { // We shift the zoom center away such that the translation part of the gesture // is also captured by the zoom operation. x: prevCenter.x + centerDist.x / (1 - scale), y: prevCenter.y + centerDist.y / (1 - scale) } }; } function makeMaybeCallFunction(predicateFn, fn) { return (arg) => { if (predicateFn()) { fn(arg); } }; } var scaleLinear = ({ domainStart, domainStop, rangeStart, rangeStop }) => (value) => rangeStart + (rangeStop - rangeStart) * ((value - domainStart) / (domainStop - domainStart)); // src/core/createZoomImageHover.tsx function createZoomImageHover(container, options) { const controller2 = new AbortController(); const { signal } = controller2; const sourceImgElement = getSourceImage(container); const zoomedImgWrapper = document.createElement("div"); zoomedImgWrapper.style.overflow = "hidden"; const zoomedImg = zoomedImgWrapper.appendChild(document.createElement("img")); zoomedImg.alt = options.zoomImageProps?.alt || ""; zoomedImg.style.maxWidth = "none"; zoomedImg.style.display = "none"; const zoomLens = container.appendChild(document.createElement("div")); zoomLens.style.display = "none"; zoomedImg.id = "meroZoomedImg"; let sourceImageElementWidth = 0; let sourceImageElementHeight = 0; const finalOptions = { zoomImageSource: options.zoomImageSource || sourceImgElement.src, zoomLensClass: options.zoomLensClass || "", zoomTargetClass: options.zoomTargetClass || "", customZoom: options.customZoom, scale: options.scale || 2, zoomTarget: options.zoomTarget, zoomLensScale: options.zoomLensScale || 1, disableScrollLock: options.disableScrollLock || false }; const { scale, zoomImageSource, customZoom, zoomLensClass, zoomTarget, zoomLensScale, zoomTargetClass, disableScrollLock } = finalOptions; const store = createStore({ zoomedImgStatus: "idle", enabled: true }); let offset = getOffset(sourceImgElement); function getOffset(element) { const elRect = element.getBoundingClientRect(); return { left: elRect.left, top: elRect.top }; } function getLimitX(value) { return sourceImageElementWidth - value; } function getLimitY(value) { return sourceImageElementHeight - value; } function zoomLensLeft(left) { const minX = zoomLens.clientWidth / 2; return clamp(left, minX, getLimitX(minX)) - minX; } function zoomLensTop(top) { const minY = zoomLens.clientHeight / 2; return clamp(top, minY, getLimitY(minY)) - minY; } function processZoom(event) { let offsetX; let offsetY; let backgroundX; let backgroundY; if (offset) { offsetX = zoomLensLeft(event.clientX - offset.left); offsetY = zoomLensTop(event.clientY - offset.top); backgroundX = offsetX * scale / zoomLensScale; backgroundY = offsetY * scale / zoomLensScale; zoomedImg.style.transform = "translate(" + -backgroundX + "px," + -backgroundY + "px)"; zoomLens.style.cssText += "transform:translate(" + offsetX + "px," + offsetY + "px);"; } } async function handlePointerEnter() { imageLoader.createZoomImage(zoomedImg, zoomImageSource, store); zoomedImg.style.display = "block"; zoomLens.style.display = "block"; if (zoomTargetClass) { const classes = zoomTargetClass.split(" "); classes.forEach((className) => zoomTarget.classList.add(className)); } if (!disableScrollLock) disableScroll(); } function handlePointerLeave() { zoomedImg.style.display = "none"; zoomLens.style.display = "none"; if (zoomTargetClass) { const classes = zoomTargetClass.split(" "); classes.forEach((className) => zoomTarget.classList.remove(className)); } if (!disableScrollLock) enableScroll(); } function handleScroll() { offset = getOffset(sourceImgElement); } async function setup() { if (zoomLensClass) { zoomLens.className = zoomLensClass; } else { zoomLens.style.backgroundImage = "url(data:image/gif;base64,R0lGODlhZABkAPABAHOf4fj48yH5BAEAAAEALAAAAABkAGQAAAL+jI+py+0PowOB2oqvznz7Dn5iSI7SiabqWrbj68bwTLL2jUv0Lvf8X8sJhzmg0Yc8mojM5kmZjEKPzqp1MZVqs7Cr98rdisOXr7lJHquz57YwDV8j3XRb/C7v1vcovD8PwicY8VcISDGY2GDIKKf4mNAoKQZZeXg5aQk5yRml+dgZ2vOpKGraQpp4uhqYKsgKi+H6iln7N8sXG4u7p2s7ykvnyxos/DuMWtyGfKq8fAwd5nzGHN067VUtiv2lbV3GDfY9DhQu7p1pXoU+rr5ODk/j7sSePk9Ub33PlN+4jx8v4JJ/RQQa3EDwzcGFiBLi6AfN4UOGCyXegGjIoh0fisQ0rsD4y+NHjgZFqgB5y2Qfks1UPmEZ0OVLlIcKAAA7)"; zoomLens.style.cursor = "pointer"; } container.addEventListener("pointerdown", processZoom, { signal }); container.addEventListener("pointermove", processZoom, { signal }); container.addEventListener("pointerenter", handlePointerEnter, { signal }); container.addEventListener("pointerleave", handlePointerLeave, { signal }); window.addEventListener("scroll", handleScroll, { signal }); zoomTarget.appendChild(zoomedImgWrapper); await new Promise((resolve) => setTimeout(resolve, 1)); const containerRect = container.getBoundingClientRect(); sourceImageElementWidth = containerRect.width; sourceImageElementHeight = containerRect.height; if (customZoom) { zoomedImgWrapper.style.maxWidth = customZoom.width + "px"; zoomedImgWrapper.style.height = customZoom.height + "px"; } else { zoomedImgWrapper.style.width = sourceImageElementWidth + "px"; zoomedImgWrapper.style.height = sourceImageElementHeight + "px"; } zoomedImg.width = sourceImageElementWidth * scale / zoomLensScale; zoomedImg.height = sourceImageElementHeight * scale / zoomLensScale; const sourceImageRect = sourceImgElement.getBoundingClientRect(); const fromLeft = sourceImageRect.left - containerRect.left; const fromTop = sourceImageRect.top - containerRect.top; zoomTarget.style.pointerEvents = "none"; zoomLens.style.position = "absolute"; zoomLens.style.left = fromLeft + "px"; zoomLens.style.top = fromTop + "px"; zoomLens.style.width = customZoom.width / scale * zoomLensScale + "px"; zoomLens.style.height = customZoom.height / scale * zoomLensScale + "px"; } setup(); return { cleanup: () => { controller2.abort(); container.contains(zoomLens) && container.removeChild(zoomLens); if (zoomTarget && zoomTarget.contains(zoomedImgWrapper)) { zoomTarget.removeChild(zoomedImgWrapper); return; } container.contains(zoomedImgWrapper) && container.removeChild(zoomedImgWrapper); }, subscribe: store.subscribe, getState: store.getState, setState: (newState) => { store.setState(newState); } }; } // src/utils/useZoomImageHover.ts function useZoomImageHover() { const result = useRef(); const [zoomImageState, updateZoomImageState] = useState({ enabled: false, zoomedImgStatus: "idle" }); const createZoomImage = useCallback((...arg) => { result.current?.cleanup(); result.current = createZoomImageHover(...arg); updateZoomImageState(result.current.getState()); result.current.subscribe(({ state }) => { updateZoomImageState(state); }); }, []); useEffect(() => { return () => { result.current?.cleanup(); }; }, []); return { createZoomImage, zoomImageState, setZoomImageState: result.current?.setState ?? (() => { }) }; } // src/core/cropImage.tsx var cropImage = async ({ image, positionX, positionY, currentZoom, rotation = 0 }) => { const canvas = document.createElement("canvas"); const scale = image.naturalWidth / (image.clientWidth * currentZoom); const normalizedRotation = rotation % 360; const croppedImageWidth = image.clientWidth * scale; const croppedImageHeight = image.clientHeight * scale; canvas.width = croppedImageWidth; canvas.height = croppedImageHeight; const canvasContext = canvas.getContext("2d"); const sx = Math.max(0, Math.abs(positionX) * scale); const sy = Math.max(0, Math.abs(positionY) * scale); canvasContext.drawImage( image, sx, sy, croppedImageWidth, croppedImageHeight, 0, 0, croppedImageWidth, croppedImageHeight ); const originalImage = new Image(); originalImage.src = canvas.toDataURL(); await new Promise((resolve) => setTimeout(resolve, 0)); const rotatedCanvas = document.createElement("canvas"); const rotatedCanvasContext = rotatedCanvas.getContext("2d"); if (normalizedRotation === 90 || normalizedRotation === 270) { rotatedCanvas.width = originalImage.naturalHeight; rotatedCanvas.height = originalImage.naturalWidth; } else { rotatedCanvas.width = originalImage.naturalWidth; rotatedCanvas.height = originalImage.naturalHeight; } rotatedCanvasContext.clearRect(0, 0, canvas.width, canvas.height); if (normalizedRotation === 90 || normalizedRotation === 270) { rotatedCanvasContext.translate(originalImage.height / 2, originalImage.width / 2); } else { rotatedCanvasContext.translate(originalImage.width / 2, originalImage.height / 2); } rotatedCanvasContext.rotate(normalizedRotation * Math.PI / 180); rotatedCanvasContext.drawImage(originalImage, -originalImage.width / 2, -originalImage.height / 2); return rotatedCanvas.toDataURL(); }; var cropImage_default = cropImage; // src/core/createZoomImageMove.tsx function createZoomImageMove(container, options = {}) { const sourceImgElement = getSourceImage(container); const finalOptions = { zoomFactor: options.zoomFactor ?? 4, zoomImageSource: options.zoomImageSource ?? sourceImgElement.src, disableScrollLock: options.disableScrollLock ?? false }; const { disableScrollLock, zoomFactor, zoomImageSource } = finalOptions; const store = createStore({ zoomedImgStatus: "idle" }); const zoomedImg = container.appendChild(document.createElement("img")); zoomedImg.alt = options.zoomImageProps?.alt || ""; zoomedImg.style.maxWidth = "none"; zoomedImg.style.position = "absolute"; zoomedImg.style.top = "0"; zoomedImg.style.left = "0"; function handlePointerEnter(event) { zoomedImg.style.display = "block"; const zoomedImgWidth = sourceImgElement.clientWidth * zoomFactor; const zoomedImgHeight = sourceImgElement.clientHeight * zoomFactor; zoomedImg.style.width = `${zoomedImgWidth}px`; zoomedImg.style.height = `${zoomedImgHeight}px`; imageLoader.createZoomImage(zoomedImg, zoomImageSource, store); processZoom(event); if (!disableScrollLock) disableScroll(); } function handlePointerMove(event) { processZoom(event); } function handlePointerLeave() { zoomedImg.style.display = "none"; zoomedImg.style.transform = "none"; if (!disableScrollLock) enableScroll(); } const calculatePositionX = (newPositionX) => { const width = container.clientWidth; if (newPositionX > 0) return 0; if (newPositionX + width * zoomFactor < width) return -width * (zoomFactor - 1); return newPositionX; }; const calculatePositionY = (newPositionY) => { const height = container.clientHeight; if (newPositionY > 0) return 0; if (newPositionY + height * zoomFactor < height) return -height * (zoomFactor - 1); return newPositionY; }; function processZoom(event) { zoomedImg.style.display = "block"; const containerRect = container.getBoundingClientRect(); const zoomPointX = event.clientX - containerRect.left; const zoomPointY = event.clientY - containerRect.top; const currentPositionX = calculatePositionX(-zoomPointX * zoomFactor + zoomPointX); const currentPositionY = calculatePositionY(-zoomPointY * zoomFactor + zoomPointY); zoomedImg.style.transform = `translate(${currentPositionX}px, ${currentPositionY}px)`; } const controller2 = new AbortController(); const { signal } = controller2; container.addEventListener("pointerenter", handlePointerEnter, { signal }); container.addEventListener("pointermove", handlePointerMove, { signal }); container.addEventListener("pointerleave", handlePointerLeave, { signal }); return { cleanup: () => { controller2.abort(); container.contains(zoomedImg) && container.removeChild(zoomedImg); container.style.width = "100%"; container.style.height = "100%"; store.cleanup(); }, subscribe: store.subscribe, getState: store.getState }; } // src/utils/useZoomImageMove.ts function useZoomImageMove() { const result = useRef(); const [zoomImageState, updateZoomImageState] = useState({ zoomedImgStatus: "idle" }); const createZoomImage = useCallback((...arg) => { result.current?.cleanup(); result.current = createZoomImageMove(...arg); updateZoomImageState(result.current.getState()); result.current.subscribe(({ state }) => { updateZoomImageState(state); }); }, []); useEffect(() => { return () => { result.current?.cleanup(); }; }, []); return { createZoomImage, zoomImageState }; } // src/EasyZoomOnHover.tsx var EasyZoomOnHover = React2.forwardRef(function EasyZoomOnHover2(props, ref) { const { mainImage, zoomImage, loadingIndicator, delayTimer, distance = 10, zoomContainerWidth } = props; const { createZoomImage: createZoomImageHover2 } = useZoomImageHover(); const imageHoverContainerRef = React2.useRef(null); const zoomTargetRef = React2.useRef(null); const imgRef = React2.useRef(null); const [imageDimension, setImageDimensions] = React2.useState({ height: 0, width: 0 }); const [isImageLoaded, setIsImageLoaded] = React2.useState(false); const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const handleImageLoad = async () => { if (imgRef.current) { setImageDimensions({ width: imgRef.current.naturalWidth, height: imgRef.current.naturalHeight }); await delay(delayTimer ?? 1600); setIsImageLoaded(true); } }; React2.useEffect(() => { if (imageDimension.width > 0 && imageDimension.height > 0) { const imageContainer = imageHoverContainerRef.current; const zoomTarget = zoomTargetRef.current; createZoomImageHover2( imageContainer, { zoomImageSource: zoomImage.src ?? mainImage.src, customZoom: { width: props.zoomContainerWidth ?? imageDimension.width ?? 450, height: props.zoomContainerHeight ?? imageDimension.height ?? 470 }, zoomTarget, scale: props.zoomLensScale ?? 3 } ); } }, [isImageLoaded, imageDimension]); return /* @__PURE__ */ React2.createElement(React2.Fragment, null, !isImageLoaded && (loadingIndicator ?? /* @__PURE__ */ React2.createElement( Skeleton_default, { height: props.mainImage.height ?? 450, width: props.mainImage.width ?? 450 } )), /* @__PURE__ */ React2.createElement( "div", { ref: imageHoverContainerRef, className: "EasyZoomImageHoverMainContainer", style: { position: "relative", width: props.mainImage.width ?? imageDimension.width, height: props.mainImage.height ?? imageDimension.height, display: isImageLoaded ? "flex" : "none", justifyItems: "start" } }, /* @__PURE__ */ React2.createElement( "img", { className: "EasyZoomHoverSmallImage", onLoad: handleImageLoad, ref: imgRef, style: { height: "auto", width: "auto" }, alt: mainImage.alt ?? "Small Pic", src: mainImage.src } ), /* @__PURE__ */ React2.createElement( "div", { ref: zoomTargetRef, className: "EasyZoomImageZoomHoverContainer", id: "zoomTargeted", style: { position: "absolute", maxWidth: zoomContainerWidth ?? "450px", left: `${mainImage.width ? mainImage.width + distance : imageDimension.width + distance}px` } } ) )); }); var EasyZoomOnHover_default = EasyZoomOnHover; var EasyZoomOnMove = (props) => { const { mainImage, zoomImage, loadingIndicator, delayTimer } = props; const [isImageLoaded, setIsImageLoaded] = React2.useState(false); const { createZoomImage: createZoomImageMove2 } = useZoomImageMove(); const imageMoveContainerRef = React2.useRef(null); const imgRef = React2.useRef(null); const [imageDimension, setImageDimensions] = React2.useState({ height: 0, width: 0 }); const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); React2.useEffect(() => { if (imageDimension.width > 0 && imageDimension.height > 0) ; const imageContainer = imageMoveContainerRef.current; if (imageContainer) { createZoomImageMove2(imageContainer, { zoomImageSource: zoomImage.src, zoomImageProps: { alt: zoomImage.alt } }); } }, [zoomImage.src, zoomImage.alt, createZoomImageMove2]); const handleImageLoad = async () => { if (imgRef.current) { setImageDimensions({ width: imgRef.current.naturalWidth, height: imgRef.current.naturalHeight }); await delay(delayTimer ?? 1600); setIsImageLoaded(true); } }; return /* @__PURE__ */ React2.createElement(React2.Fragment, null, !isImageLoaded && (loadingIndicator ?? /* @__PURE__ */ React2.createElement( Skeleton_default, { height: props.mainImage.height ?? 450, width: props.mainImage.width ?? 450 } )), /* @__PURE__ */ React2.createElement( "div", { ref: imageMoveContainerRef, className: "EasyImageZoomOnMoveContainer", style: { position: "relative", maxHeight: mainImage.height ?? imageDimension?.height ?? "auto", maxWidth: mainImage.width ?? imageDimension?.width, overflow: "hidden", cursor: "crosshair", display: isImageLoaded ? "block" : "none" } }, /* @__PURE__ */ React2.createElement( "img", { className: "EasyImageZoomOnMoveImage", onLoad: handleImageLoad, ref: imgRef, style: { width: "full", height: "full" }, alt: mainImage.alt ?? "Large Pic", src: mainImage.src } ) )); }; var EasyZoomOnMove_default = EasyZoomOnMove; export { Skeleton_default as EasySkeleton, EasyZoomOnHover_default as EasyZoomOnHover, EasyZoomOnMove_default as EasyZoomOnMove, clamp, computeZoomGesture, createStore, createZoomImageHover, createZoomImageMove, cropImage_default as cropImage, disableScroll, enableScroll, getPointersCenter, getSourceImage, imageLoader, makeImageLoader, makeMaybeCallFunction, noop, scaleLinear, use_mouse_default as useMouse }; //# sourceMappingURL=out.js.map //# sourceMappingURL=index.mjs.map