mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +00:00 
			
		
		
		
	 bbfcdb3d42
			
		
	
	bbfcdb3d42
	
	
	
		
			
			This PR includes improvements in image attachment/gallery viewer: 1. Added double-click zoom functionality (depreciated click to zoom) 2. Implemented scroll zoom based on cursor position 3. Increase the zoom scale 4. Improved layout and styling for better usability Fixes https://linear.app/chatwoot/issue/CW-4127/zoom-images-from-a-specific-location ## How Has This Been Tested? Loom video https://www.loom.com/share/b21e00db3bc74231a90202eb6eb2fb5a?sid=a0651bf1-0952-430b-a5a9-83bf0858e059 --------- Co-authored-by: Pranav <pranav@chatwoot.com> Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
		
			
				
	
	
		
			187 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			187 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import { ref, computed } from 'vue';
 | |
| import {
 | |
|   debounce,
 | |
|   calculateCenterOffset,
 | |
|   applyRotationTransform,
 | |
|   normalizeToPercentage,
 | |
| } from '@chatwoot/utils';
 | |
| 
 | |
| // Composable for images in gallery view
 | |
| export const useImageZoom = imageRef => {
 | |
|   const MAX_ZOOM_LEVEL = 3;
 | |
|   const MIN_ZOOM_LEVEL = 1;
 | |
|   const ZOOM_INCREMENT = 0.2;
 | |
|   const MOUSE_MOVE_DEBOUNCE_MS = 100;
 | |
|   const MOUSE_LEAVE_DEBOUNCE_MS = 110;
 | |
|   const DEFAULT_IMG_TRANSFORM_ORIGIN = 'center center';
 | |
| 
 | |
|   const zoomScale = ref(1);
 | |
|   const imgTransformOriginPoint = ref(DEFAULT_IMG_TRANSFORM_ORIGIN);
 | |
|   const activeImageRotation = ref(0);
 | |
| 
 | |
|   const imageWrapperStyle = computed(() => ({
 | |
|     transform: `rotate(${activeImageRotation.value}deg)`,
 | |
|   }));
 | |
| 
 | |
|   const imageStyle = computed(() => ({
 | |
|     transform: `scale(${zoomScale.value})`,
 | |
|     cursor: zoomScale.value < MAX_ZOOM_LEVEL ? 'zoom-in' : 'zoom-out',
 | |
|     transformOrigin: `${imgTransformOriginPoint.value}`,
 | |
|   }));
 | |
| 
 | |
|   // Resets the transform origin to center
 | |
|   const resetTransformOrigin = () => {
 | |
|     if (imageRef.value) {
 | |
|       imgTransformOriginPoint.value = DEFAULT_IMG_TRANSFORM_ORIGIN;
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   // Rotates the current image
 | |
|   const onRotate = type => {
 | |
|     if (!imageRef.value) return;
 | |
|     resetTransformOrigin();
 | |
| 
 | |
|     const rotation = type === 'clockwise' ? 90 : -90;
 | |
| 
 | |
|     // ensure that the value of the rotation is within the range of -360 to 360 degrees
 | |
|     activeImageRotation.value = (activeImageRotation.value + rotation) % 360;
 | |
| 
 | |
|     // Reset zoom when rotating
 | |
|     zoomScale.value = 1;
 | |
|     resetTransformOrigin();
 | |
|   };
 | |
| 
 | |
|   /**
 | |
|    * Calculates the appropriate transform origin point based on mouse position and image rotation
 | |
|    * Used to create a natural zoom behavior where the image zooms toward/from the cursor position
 | |
|    *
 | |
|    * @param {number} x - The client X coordinate of the mouse pointer
 | |
|    * @param {number} y - The client Y coordinate of the mouse pointer
 | |
|    * @returns {{x: number, y: number}} Object containing the transform origin coordinates as percentages
 | |
|    */
 | |
|   const getZoomOrigin = (x, y) => {
 | |
|     // Default to center
 | |
|     if (!imageRef.value) return { x: 50, y: 50 };
 | |
| 
 | |
|     const rect = imageRef.value.getBoundingClientRect();
 | |
| 
 | |
|     // Step 1: Calculate offset from center
 | |
|     const { relativeX, relativeY } = calculateCenterOffset(x, y, rect);
 | |
| 
 | |
|     // Step 2: Apply rotation transformation
 | |
|     const { rotatedX, rotatedY } = applyRotationTransform(
 | |
|       relativeX,
 | |
|       relativeY,
 | |
|       activeImageRotation.value
 | |
|     );
 | |
| 
 | |
|     // Step 3: Convert to percentage coordinates
 | |
|     return normalizeToPercentage(rotatedX, rotatedY, rect.width, rect.height);
 | |
|   };
 | |
| 
 | |
|   // Handles zooming the image
 | |
|   const onZoom = (scale, x, y) => {
 | |
|     if (!imageRef.value) return;
 | |
| 
 | |
|     // Calculate new scale within bounds
 | |
|     const newScale = Math.max(
 | |
|       MIN_ZOOM_LEVEL,
 | |
|       Math.min(MAX_ZOOM_LEVEL, zoomScale.value + scale)
 | |
|     );
 | |
| 
 | |
|     // Skip if no change
 | |
|     if (newScale === zoomScale.value) return;
 | |
| 
 | |
|     // Update transform origin based on mouse position and zoom scale is minimum
 | |
|     if (x != null && y != null && zoomScale.value === MIN_ZOOM_LEVEL) {
 | |
|       const { x: originX, y: originY } = getZoomOrigin(x, y);
 | |
|       imgTransformOriginPoint.value = `${originX}% ${originY}%`;
 | |
|     }
 | |
| 
 | |
|     // Apply the new scale
 | |
|     zoomScale.value = newScale;
 | |
|   };
 | |
| 
 | |
|   // Handles double-click zoom toggling
 | |
|   const onDoubleClickZoomImage = e => {
 | |
|     if (!imageRef.value) return;
 | |
|     e.preventDefault();
 | |
| 
 | |
|     // Toggle between max zoom and min zoom
 | |
|     const newScale =
 | |
|       zoomScale.value >= MAX_ZOOM_LEVEL ? MIN_ZOOM_LEVEL : MAX_ZOOM_LEVEL;
 | |
| 
 | |
|     // Update transform origin based on mouse position
 | |
|     const { x: originX, y: originY } = getZoomOrigin(e.clientX, e.clientY);
 | |
|     imgTransformOriginPoint.value = `${originX}% ${originY}%`;
 | |
| 
 | |
|     // Apply the new scale
 | |
|     zoomScale.value = newScale;
 | |
|   };
 | |
| 
 | |
|   // Handles mouse wheel zooming for images
 | |
|   const onWheelImageZoom = e => {
 | |
|     if (!imageRef.value) return;
 | |
|     e.preventDefault();
 | |
| 
 | |
|     const scale = e.deltaY > 0 ? -ZOOM_INCREMENT : ZOOM_INCREMENT;
 | |
|     onZoom(scale, e.clientX, e.clientY);
 | |
|   };
 | |
| 
 | |
|   /**
 | |
|    * Sets transform origin to mouse position during hover.
 | |
|    * Enables precise scroll/double-click zoom targeting by updating the
 | |
|    * transform origin to cursor position. Only active at minimum zoom level.
 | |
|    * Debounced (100ms) to improve performance during rapid mouse movement.
 | |
|    */
 | |
|   const onMouseMove = debounce(
 | |
|     e => {
 | |
|       if (!imageRef.value) return;
 | |
|       if (zoomScale.value !== MIN_ZOOM_LEVEL) return;
 | |
| 
 | |
|       const { x: originX, y: originY } = getZoomOrigin(e.clientX, e.clientY);
 | |
|       imgTransformOriginPoint.value = `${originX}% ${originY}%`;
 | |
|     },
 | |
|     MOUSE_MOVE_DEBOUNCE_MS,
 | |
|     false
 | |
|   );
 | |
| 
 | |
|   /**
 | |
|    * Resets transform origin to center when mouse leaves image.
 | |
|    * Ensures button-based zooming works predictably after hover ends.
 | |
|    * Uses slightly longer debounce (110ms) to avoid conflicts with onMouseMove.
 | |
|    */
 | |
|   const onMouseLeave = debounce(
 | |
|     () => {
 | |
|       if (!imageRef.value) return;
 | |
|       if (zoomScale.value !== MIN_ZOOM_LEVEL) return;
 | |
|       imgTransformOriginPoint.value = DEFAULT_IMG_TRANSFORM_ORIGIN;
 | |
|     },
 | |
|     MOUSE_LEAVE_DEBOUNCE_MS,
 | |
|     false
 | |
|   );
 | |
| 
 | |
|   const resetZoomAndRotation = () => {
 | |
|     activeImageRotation.value = 0;
 | |
|     zoomScale.value = 1;
 | |
|     resetTransformOrigin();
 | |
|   };
 | |
| 
 | |
|   return {
 | |
|     zoomScale,
 | |
|     imgTransformOriginPoint,
 | |
|     activeImageRotation,
 | |
|     imageWrapperStyle,
 | |
|     imageStyle,
 | |
|     getZoomOrigin,
 | |
|     resetTransformOrigin,
 | |
|     onRotate,
 | |
|     onZoom,
 | |
|     onDoubleClickZoomImage,
 | |
|     onWheelImageZoom,
 | |
|     onMouseMove,
 | |
|     onMouseLeave,
 | |
|     resetZoomAndRotation,
 | |
|   };
 | |
| };
 |