mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-11-04 04:57:51 +00:00 
			
		
		
		
	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,
 | 
						|
  };
 | 
						|
};
 |