mirror of
				https://github.com/imfing/hextra.git
				synced 2025-10-31 19:34:54 -04:00 
			
		
		
		
	feat(image-zoom): enhance zoom functionality with loading states and improved interactions
- Updated CSS to include loading indicators and refined transition effects for zoomed images. - Enhanced JavaScript to manage image loading states, ensuring a smoother user experience during zoom interactions. - Improved gesture handling for touch devices, including better management of pinch and drag events.
This commit is contained in:
		| @@ -9,10 +9,13 @@ | |||||||
|   opacity: 0; |   opacity: 0; | ||||||
|   transition: opacity 260ms cubic-bezier(0.2, 0, 0, 1); |   transition: opacity 260ms cubic-bezier(0.2, 0, 0, 1); | ||||||
|   cursor: zoom-out; |   cursor: zoom-out; | ||||||
|   overscroll-behavior: auto; |   overscroll-behavior: contain; | ||||||
|   touch-action: auto; |   touch-action: none; | ||||||
|   backdrop-filter: blur(var(--hextra-image-zoom-blur, 4px)); |   backdrop-filter: blur(var(--hextra-image-zoom-blur, 4px)); | ||||||
|   -webkit-backdrop-filter: blur(var(--hextra-image-zoom-blur, 4px)); |   -webkit-backdrop-filter: blur(var(--hextra-image-zoom-blur, 4px)); | ||||||
|  |   /* Prevent iOS bounce */ | ||||||
|  |   position: fixed; | ||||||
|  |   overflow: hidden; | ||||||
| } | } | ||||||
|  |  | ||||||
| .hextra-zoom-image-overlay.show { | .hextra-zoom-image-overlay.show { | ||||||
| @@ -25,22 +28,37 @@ | |||||||
| } | } | ||||||
|  |  | ||||||
| .hextra-zoom-image { | .hextra-zoom-image { | ||||||
|   max-width: min(95vw, 1200px); |  | ||||||
|   max-height: 95vh; |  | ||||||
|   border-radius: 8px; |   border-radius: 8px; | ||||||
|   box-shadow: 0 8px 40px rgba(0, 0, 0, 0.35); |   box-shadow: 0 18px 80px rgba(0, 0, 0, 0.5); | ||||||
|  |   image-rendering: -webkit-optimize-contrast; | ||||||
|  |   image-rendering: high-quality; | ||||||
|   will-change: transform; |   will-change: transform; | ||||||
|   transform: scale(0.98); |   /* Prevent image selection on mobile */ | ||||||
|  |   user-select: none; | ||||||
|  |   -webkit-user-select: none; | ||||||
|  |   -webkit-touch-callout: none; | ||||||
|  |   /* Hardware acceleration */ | ||||||
|  |   transform: translateZ(0); | ||||||
|  |   -webkit-transform: translateZ(0); | ||||||
| } | } | ||||||
|  |  | ||||||
| .hextra-zoom-image-overlay.show .hextra-zoom-image { | .hextra-zoom-image-overlay.show .hextra-zoom-image { | ||||||
|   transform: scale(1); |   transition: | ||||||
|   transition: transform 320ms cubic-bezier(0.2, 0.8, 0.2, 1); |     transform 320ms cubic-bezier(0.2, 0.8, 0.2, 1), | ||||||
|  |     left 320ms cubic-bezier(0.2, 0.8, 0.2, 1), | ||||||
|  |     top 320ms cubic-bezier(0.2, 0.8, 0.2, 1); | ||||||
| } | } | ||||||
|  |  | ||||||
| .hextra-zoom-image-overlay.closing .hextra-zoom-image { | .hextra-zoom-image-overlay.closing .hextra-zoom-image { | ||||||
|   transform: scale(0.98); |   transition: | ||||||
|   transition: transform 340ms cubic-bezier(0.3, 0, 0.2, 1); |     transform 340ms cubic-bezier(0.3, 0, 0.2, 1), | ||||||
|  |     left 340ms cubic-bezier(0.3, 0, 0.2, 1), | ||||||
|  |     top 340ms cubic-bezier(0.3, 0, 0.2, 1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Disable transitions during interaction */ | ||||||
|  | .hextra-zoom-image-overlay.interacting .hextra-zoom-image { | ||||||
|  |   transition: none !important; | ||||||
| } | } | ||||||
|  |  | ||||||
| @media (prefers-reduced-motion: reduce) { | @media (prefers-reduced-motion: reduce) { | ||||||
| @@ -50,7 +68,16 @@ | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| /* Show magnifier cursor over zoomable images in content */ |  | ||||||
| .content img:not([data-no-zoom]) { | .content img:not([data-no-zoom]) { | ||||||
|   cursor: zoom-in; |   cursor: zoom-in; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /* Loading indicator */ | ||||||
|  | .hextra-zoom-image.loading { | ||||||
|  |   opacity: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .hextra-zoom-image.loaded { | ||||||
|  |   opacity: 1; | ||||||
|  |   transition: opacity 200ms ease-in-out; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,123 +1,403 @@ | |||||||
| /*! |  | ||||||
|  * Hextra Image Zoom |  | ||||||
|  * - Zooms images inside `.content` into a dark, blurred overlay. |  | ||||||
|  * - Dismiss: overlay click, Esc, wheel/scroll (non-ctrl). |  | ||||||
|  * - Pinch/trackpad pinch (wheel+ctrl) will NOT dismiss. |  | ||||||
|  * - Opt out per image via `data-no-zoom`. |  | ||||||
|  * - Customize via CSS vars: --hextra-image-zoom-backdrop, --hextra-image-zoom-blur. |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| (function () { | (function () { | ||||||
|   'use strict'; |   'use strict'; | ||||||
|  |  | ||||||
|   function createOverlay(src, alt) { |   function createOverlayFromTarget(targetImg) { | ||||||
|  |     const src = targetImg.currentSrc || targetImg.src; | ||||||
|  |     const alt = targetImg.alt || ""; | ||||||
|  |     if (!src) return; | ||||||
|  |  | ||||||
|  |     const rect = targetImg.getBoundingClientRect(); | ||||||
|  |  | ||||||
|     const overlay = document.createElement("div"); |     const overlay = document.createElement("div"); | ||||||
|     overlay.className = "hextra-zoom-image-overlay"; |     overlay.className = "hextra-zoom-image-overlay"; | ||||||
|     overlay.setAttribute("role", "dialog"); |     overlay.setAttribute("role", "dialog"); | ||||||
|     overlay.setAttribute("aria-modal", "true"); |     overlay.setAttribute("aria-modal", "true"); | ||||||
|  |     overlay.setAttribute("aria-label", alt || "Zoomed image"); | ||||||
|  |  | ||||||
|     const img = document.createElement("img"); |     const img = document.createElement("img"); | ||||||
|     img.className = "hextra-zoom-image"; |     img.className = "hextra-zoom-image loading"; | ||||||
|     img.src = src; |     img.src = src; | ||||||
|     if (alt) img.alt = alt; |     if (alt) img.alt = alt; | ||||||
|  |  | ||||||
|  |     // Center-origin positioning for cleaner transforms | ||||||
|  |     const startCX = rect.left + rect.width / 2; | ||||||
|  |     const startCY = rect.top + rect.height / 2; | ||||||
|  |     img.style.position = "fixed"; | ||||||
|  |     img.style.left = startCX + "px"; | ||||||
|  |     img.style.top = startCY + "px"; | ||||||
|  |     img.style.width = rect.width + "px"; | ||||||
|  |     img.style.height = rect.height + "px"; | ||||||
|  |     img.style.transformOrigin = "center center"; | ||||||
|  |     img.style.transform = "translate3d(-50%, -50%, 0) scale(1)"; | ||||||
|  |  | ||||||
|     overlay.appendChild(img); |     overlay.appendChild(img); | ||||||
|  |  | ||||||
|     // Track pinch gesture |     // Image loaded handler | ||||||
|     let pinching = false; |     img.addEventListener('load', function () { | ||||||
|     let pinchTimer = 0; |       img.classList.remove('loading'); | ||||||
|     const activeTouchPointers = new Set(); |       img.classList.add('loaded'); | ||||||
|     function pinchingStart() { |     }); | ||||||
|       pinching = true; |  | ||||||
|       if (pinchTimer) clearTimeout(pinchTimer); |     // Gesture state management | ||||||
|  |     let isPinching = false; | ||||||
|  |     let isDragging = false; | ||||||
|  |     let isInteracting = false; | ||||||
|  |     let pinchEndTimer = null; | ||||||
|  |  | ||||||
|  |     const pointers = new Map(); // pointerId -> {x, y, startX, startY} | ||||||
|  |     let gestureState = { | ||||||
|  |       scale: 1, | ||||||
|  |       panX: 0, | ||||||
|  |       panY: 0, | ||||||
|  |       lastScale: 1, | ||||||
|  |       lastPanX: 0, | ||||||
|  |       lastPanY: 0, | ||||||
|  |       initialDistance: 0, | ||||||
|  |       midpointX: 0, | ||||||
|  |       midpointY: 0 | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // Utility functions | ||||||
|  |     function getDistance(p1, p2) { | ||||||
|  |       const dx = p2.x - p1.x; | ||||||
|  |       const dy = p2.y - p1.y; | ||||||
|  |       return Math.sqrt(dx * dx + dy * dy); | ||||||
|     } |     } | ||||||
|     function pinchingEndSoon() { |  | ||||||
|       if (pinchTimer) clearTimeout(pinchTimer); |     function getMidpoint(p1, p2) { | ||||||
|       pinchTimer = setTimeout(() => (pinching = false), 350); |       return { | ||||||
|  |         x: (p1.x + p2.x) / 2, | ||||||
|  |         y: (p1.y + p2.y) / 2 | ||||||
|  |       }; | ||||||
|     } |     } | ||||||
|     function onPointerDown(e) { |  | ||||||
|       if (e.pointerType === 'touch') { |     function setInteracting(value) { | ||||||
|         activeTouchPointers.add(e.pointerId); |       isInteracting = value; | ||||||
|         if (activeTouchPointers.size > 1) pinchingStart(); |       if (value) { | ||||||
|       } |         overlay.classList.add('interacting'); | ||||||
|     } |       } else { | ||||||
|     function onPointerUp(e) { |         overlay.classList.remove('interacting'); | ||||||
|       if (e.pointerType === 'touch') { |  | ||||||
|         activeTouchPointers.delete(e.pointerId); |  | ||||||
|         if (activeTouchPointers.size < 2) pinchingEndSoon(); |  | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     // Pointer event handlers | ||||||
|  |     function onPointerDown(e) { | ||||||
|  |       e.preventDefault(); | ||||||
|  |  | ||||||
|  |       pointers.set(e.pointerId, { | ||||||
|  |         x: e.clientX, | ||||||
|  |         y: e.clientY, | ||||||
|  |         startX: e.clientX, | ||||||
|  |         startY: e.clientY | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       if (pointers.size === 1) { | ||||||
|  |         // Single touch - start drag | ||||||
|  |         isDragging = true; | ||||||
|  |         setInteracting(true); | ||||||
|  |         gestureState.lastPanX = gestureState.panX; | ||||||
|  |         gestureState.lastPanY = gestureState.panY; | ||||||
|  |       } else if (pointers.size === 2) { | ||||||
|  |         // Two touches - start pinch | ||||||
|  |         isDragging = false; | ||||||
|  |         isPinching = true; | ||||||
|  |         setInteracting(true); | ||||||
|  |  | ||||||
|  |         const pts = Array.from(pointers.values()); | ||||||
|  |         gestureState.initialDistance = getDistance(pts[0], pts[1]); | ||||||
|  |         gestureState.lastScale = gestureState.scale; | ||||||
|  |  | ||||||
|  |         const midpoint = getMidpoint(pts[0], pts[1]); | ||||||
|  |         gestureState.midpointX = midpoint.x; | ||||||
|  |         gestureState.midpointY = midpoint.y; | ||||||
|  |  | ||||||
|  |         if (pinchEndTimer) { | ||||||
|  |           clearTimeout(pinchEndTimer); | ||||||
|  |           pinchEndTimer = null; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function onPointerMove(e) { | ||||||
|  |       if (!pointers.has(e.pointerId)) return; | ||||||
|  |  | ||||||
|  |       e.preventDefault(); | ||||||
|  |  | ||||||
|  |       const pointer = pointers.get(e.pointerId); | ||||||
|  |       pointer.x = e.clientX; | ||||||
|  |       pointer.y = e.clientY; | ||||||
|  |  | ||||||
|  |       if (isPinching && pointers.size === 2) { | ||||||
|  |         // Handle pinch zoom | ||||||
|  |         const pts = Array.from(pointers.values()); | ||||||
|  |         const currentDistance = getDistance(pts[0], pts[1]); | ||||||
|  |         const scaleDelta = currentDistance / gestureState.initialDistance; | ||||||
|  |  | ||||||
|  |         // Calculate new scale with limits - minimum is 1 (original zoom level) | ||||||
|  |         const newScale = Math.max(1, Math.min(5, gestureState.lastScale * scaleDelta)); | ||||||
|  |  | ||||||
|  |         // Only update pan if scale is actually changing | ||||||
|  |         // This prevents drift when pinching at minimum scale | ||||||
|  |         if (Math.abs(newScale - gestureState.scale) > 0.001) { | ||||||
|  |           gestureState.scale = newScale; | ||||||
|  |  | ||||||
|  |           // Calculate pan based on pinch center movement | ||||||
|  |           const currentMidpoint = getMidpoint(pts[0], pts[1]); | ||||||
|  |           const panDeltaX = currentMidpoint.x - gestureState.midpointX; | ||||||
|  |           const panDeltaY = currentMidpoint.y - gestureState.midpointY; | ||||||
|  |  | ||||||
|  |           gestureState.panX = gestureState.lastPanX + panDeltaX; | ||||||
|  |           gestureState.panY = gestureState.lastPanY + panDeltaY; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         applyTransform(); | ||||||
|  |       } else if (isDragging && pointers.size === 1) { | ||||||
|  |         // Handle drag/pan | ||||||
|  |         const deltaX = pointer.x - pointer.startX; | ||||||
|  |         const deltaY = pointer.y - pointer.startY; | ||||||
|  |  | ||||||
|  |         gestureState.panX = gestureState.lastPanX + deltaX; | ||||||
|  |         gestureState.panY = gestureState.lastPanY + deltaY; | ||||||
|  |  | ||||||
|  |         applyTransform(); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function onPointerUp(e) { | ||||||
|  |       pointers.delete(e.pointerId); | ||||||
|  |  | ||||||
|  |       if (pointers.size === 0) { | ||||||
|  |         // All pointers released | ||||||
|  |         isDragging = false; | ||||||
|  |         setInteracting(false); | ||||||
|  |  | ||||||
|  |         // Check if it was a tap (no significant movement) | ||||||
|  |         const pointer = e; | ||||||
|  |         const moveThreshold = 10; | ||||||
|  |         const timeSinceDown = e.timeStamp; | ||||||
|  |  | ||||||
|  |         // If minimal movement and not pinching, treat as tap to close | ||||||
|  |         if (!isPinching && Math.abs(pointer.clientX - e.clientX) < moveThreshold && | ||||||
|  |           Math.abs(pointer.clientY - e.clientY) < moveThreshold) { | ||||||
|  |           // Click on image or overlay should close | ||||||
|  |           close(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (isPinching) { | ||||||
|  |           pinchEndTimer = setTimeout(() => { | ||||||
|  |             isPinching = false; | ||||||
|  |           }, 300); | ||||||
|  |         } | ||||||
|  |       } else if (pointers.size === 1) { | ||||||
|  |         // Going from pinch to single touch | ||||||
|  |         isPinching = false; | ||||||
|  |         isDragging = true; | ||||||
|  |         const remaining = Array.from(pointers.values())[0]; | ||||||
|  |         gestureState.lastPanX = gestureState.panX; | ||||||
|  |         gestureState.lastPanY = gestureState.panY; | ||||||
|  |         remaining.startX = remaining.x; | ||||||
|  |         remaining.startY = remaining.y; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function onPointerCancel(e) { | ||||||
|  |       onPointerUp(e); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Mouse wheel zoom and scroll handling | ||||||
|  |     function onWheel(e) { | ||||||
|  |       e.preventDefault(); | ||||||
|  |  | ||||||
|  |       // If it's a regular scroll (not pinch), dismiss the overlay | ||||||
|  |       if (!e.ctrlKey && !e.metaKey) { | ||||||
|  |         // Regular scroll - dismiss overlay gracefully | ||||||
|  |         close(); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Handle trackpad pinch (ctrl+wheel or cmd+wheel on Mac) | ||||||
|  |       const delta = e.deltaY; | ||||||
|  |       const scaleFactor = 0.01; | ||||||
|  |       const zoomSpeed = Math.exp(-delta * scaleFactor); | ||||||
|  |  | ||||||
|  |       // Minimum scale is 1 (original zoom level), don't allow zooming out smaller | ||||||
|  |       gestureState.scale = Math.max(1, Math.min(5, gestureState.scale * zoomSpeed)); | ||||||
|  |  | ||||||
|  |       // Zoom towards mouse position | ||||||
|  |       const rect = img.getBoundingClientRect(); | ||||||
|  |       const centerX = rect.left + rect.width / 2; | ||||||
|  |       const centerY = rect.top + rect.height / 2; | ||||||
|  |       const offsetX = e.clientX - centerX; | ||||||
|  |       const offsetY = e.clientY - centerY; | ||||||
|  |  | ||||||
|  |       // Adjust pan to zoom towards cursor | ||||||
|  |       const scaleDiff = gestureState.scale - (gestureState.scale / zoomSpeed); | ||||||
|  |       gestureState.panX -= offsetX * scaleDiff * 0.1; | ||||||
|  |       gestureState.panY -= offsetY * scaleDiff * 0.1; | ||||||
|  |  | ||||||
|  |       setInteracting(true); | ||||||
|  |       applyTransform(); | ||||||
|  |       setTimeout(() => setInteracting(false), 150); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Keyboard navigation | ||||||
|  |     function onKeyDown(e) { | ||||||
|  |       if (e.key === "Escape") { | ||||||
|  |         close(); | ||||||
|  |       } else if (e.key === "+" || e.key === "=") { | ||||||
|  |         gestureState.scale = Math.min(5, gestureState.scale * 1.2); | ||||||
|  |         applyTransform(); | ||||||
|  |       } else if (e.key === "-") { | ||||||
|  |         gestureState.scale = Math.max(1, gestureState.scale / 1.2); | ||||||
|  |         applyTransform(); | ||||||
|  |       } else if (e.key === "0") { | ||||||
|  |         gestureState.scale = 1; | ||||||
|  |         gestureState.panX = 0; | ||||||
|  |         gestureState.panY = 0; | ||||||
|  |         applyTransform(); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Apply transforms | ||||||
|  |     let final = computeFinal(); | ||||||
|  |     let baseScale = final.scale; | ||||||
|  |  | ||||||
|  |     function computeFinal() { | ||||||
|  |       const vv = window.visualViewport; | ||||||
|  |       const vw = vv ? vv.width : window.innerWidth; | ||||||
|  |       const vh = vv ? vv.height : window.innerHeight; | ||||||
|  |       const vx = vv ? vv.offsetLeft : 0; | ||||||
|  |       const vy = vv ? vv.offsetTop : 0; | ||||||
|  |  | ||||||
|  |       const margin = 20; | ||||||
|  |       const maxW = Math.min(vw - margin * 2, 1200); | ||||||
|  |       const maxH = vh - margin * 2; | ||||||
|  |  | ||||||
|  |       const scale = Math.min(maxW / rect.width, maxH / rect.height, 2); | ||||||
|  |       const centerX = vx + vw / 2; | ||||||
|  |       const centerY = vy + vh / 2; | ||||||
|  |  | ||||||
|  |       return { cx: centerX, cy: centerY, scale }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function applyTransform() { | ||||||
|  |       img.style.left = final.cx + "px"; | ||||||
|  |       img.style.top = final.cy + "px"; | ||||||
|  |       const totalScale = baseScale * gestureState.scale; | ||||||
|  |       const transform = `translate3d(-50%, -50%, 0) translate3d(${gestureState.panX}px, ${gestureState.panY}px, 0) scale(${totalScale})`; | ||||||
|  |       img.style.transform = transform; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Close function | ||||||
|     function close(immediate = false) { |     function close(immediate = false) { | ||||||
|       // trigger dedicated closing transitions for smoother zoom-out |  | ||||||
|       overlay.classList.add("closing"); |       overlay.classList.add("closing"); | ||||||
|       window.removeEventListener("keydown", onKeyDown, true); |  | ||||||
|       window.removeEventListener("scroll", onScroll, true); |       // Animate back to original position | ||||||
|       overlay.removeEventListener("wheel", onWheel); |       img.style.left = startCX + "px"; | ||||||
|       overlay.removeEventListener("pointerdown", onPointerDown); |       img.style.top = startCY + "px"; | ||||||
|       overlay.removeEventListener("pointerup", onPointerUp); |       img.style.transform = `translate3d(-50%, -50%, 0) scale(1)`; | ||||||
|       overlay.removeEventListener("pointercancel", onPointerUp); |  | ||||||
|       activeTouchPointers.clear(); |       // Cleanup event listeners | ||||||
|       if (pinchTimer) clearTimeout(pinchTimer); |       cleanup(); | ||||||
|  |  | ||||||
|       if (immediate) { |       if (immediate) { | ||||||
|         overlay.remove(); |         overlay.remove(); | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|       overlay.addEventListener("transitionend", () => overlay.remove(), { once: true }); |  | ||||||
|  |       // Remove after animation | ||||||
|  |       const done = () => overlay.remove(); | ||||||
|  |       overlay.addEventListener("transitionend", done, { once: true }); | ||||||
|  |       img.addEventListener("transitionend", done, { once: true }); | ||||||
|  |  | ||||||
|  |       // Fallback removal | ||||||
|  |       setTimeout(() => { | ||||||
|  |         if (overlay.parentNode) { | ||||||
|  |           overlay.remove(); | ||||||
|  |         } | ||||||
|  |       }, 400); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function onKeyDown(e) { |     function cleanup() { | ||||||
|       if (e.key === "Escape") close(); |       window.removeEventListener("keydown", onKeyDown, true); | ||||||
|  |       overlay.removeEventListener("wheel", onWheel); | ||||||
|  |       overlay.removeEventListener("pointermove", onPointerMove); | ||||||
|  |       overlay.removeEventListener("pointerdown", onPointerDown); | ||||||
|  |       overlay.removeEventListener("pointerup", onPointerUp); | ||||||
|  |       overlay.removeEventListener("pointercancel", onPointerCancel); | ||||||
|  |       window.removeEventListener("resize", onResize); | ||||||
|  |       if (window.visualViewport) { | ||||||
|  |         window.visualViewport.removeEventListener("resize", onResize); | ||||||
|  |         window.visualViewport.removeEventListener("scroll", onResize); | ||||||
|  |       } | ||||||
|  |       if (pinchEndTimer) { | ||||||
|  |         clearTimeout(pinchEndTimer); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     overlay.addEventListener("click", () => close(false), { once: true }); |     // Handle viewport changes | ||||||
|     window.addEventListener("keydown", onKeyDown, true); |     function onResize() { | ||||||
|  |       final = computeFinal(); | ||||||
|     function onWheel(e) { |       baseScale = final.scale; | ||||||
|       // Ignore trackpad pinch (wheel + ctrlKey) and active pinch |       applyTransform(); | ||||||
|       if ((e && e.ctrlKey) || pinching) return; |  | ||||||
|       close(true); |  | ||||||
|     } |  | ||||||
|     function onScroll() { |  | ||||||
|       if (pinching) return; |  | ||||||
|       close(true); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     overlay.addEventListener("wheel", onWheel, { passive: true }); |     // Prevent click propagation on image | ||||||
|     // Standard W3C pointer events for multi-touch pinch detection |     img.addEventListener("click", function (e) { | ||||||
|  |       e.stopPropagation(); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // Setup event listeners | ||||||
|  |     overlay.addEventListener("wheel", onWheel, { passive: false }); | ||||||
|  |     overlay.addEventListener("pointermove", onPointerMove); | ||||||
|     overlay.addEventListener("pointerdown", onPointerDown); |     overlay.addEventListener("pointerdown", onPointerDown); | ||||||
|     overlay.addEventListener("pointerup", onPointerUp); |     overlay.addEventListener("pointerup", onPointerUp); | ||||||
|     overlay.addEventListener("pointercancel", onPointerUp); |     overlay.addEventListener("pointercancel", onPointerCancel); | ||||||
|     window.addEventListener("scroll", onScroll, true); |     window.addEventListener("keydown", onKeyDown, true); | ||||||
|  |     window.addEventListener("resize", onResize, { passive: true }); | ||||||
|  |  | ||||||
|  |     if (window.visualViewport) { | ||||||
|  |       window.visualViewport.addEventListener("resize", onResize, { passive: true }); | ||||||
|  |       window.visualViewport.addEventListener("scroll", onResize, { passive: true }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Click outside to close (on overlay background) | ||||||
|  |     overlay.addEventListener("click", function (e) { | ||||||
|  |       if (e.target === overlay) { | ||||||
|  |         close(); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // Add to DOM | ||||||
|     document.body.appendChild(overlay); |     document.body.appendChild(overlay); | ||||||
|  |  | ||||||
|     // trigger fade-in |     // Trigger opening animation | ||||||
|     requestAnimationFrame(() => overlay.classList.add("show")); |     requestAnimationFrame(() => { | ||||||
|  |       overlay.classList.add("show"); | ||||||
|  |       applyTransform(); | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Initialize after DOM is parsed; defer script ensures this usually fires immediately |   // Initialize after DOM ready | ||||||
|   document.addEventListener('DOMContentLoaded', function () { |   function init() { | ||||||
|     const container = document.querySelector(".content"); |     const container = document.querySelector(".content"); | ||||||
|     if (!container) return; |     if (!container) return; | ||||||
|  |  | ||||||
|     container.addEventListener( |     container.addEventListener("click", function (e) { | ||||||
|       "click", |       const target = e.target; | ||||||
|       function (e) { |       if (!(target instanceof HTMLImageElement)) return; | ||||||
|         const target = e.target; |       if (target.dataset.noZoom === "" || target.dataset.noZoom === "true") return; | ||||||
|         if (!(target instanceof HTMLImageElement)) return; |  | ||||||
|         if (target.dataset.noZoom === "" || target.dataset.noZoom === "true") return; |  | ||||||
|  |  | ||||||
|         // avoid following parent links when zooming |       e.preventDefault(); | ||||||
|         e.preventDefault(); |       e.stopPropagation(); | ||||||
|         e.stopPropagation(); |  | ||||||
|  |  | ||||||
|         const src = target.currentSrc || target.src; |       createOverlayFromTarget(target); | ||||||
|         if (!src) return; |     }, true); | ||||||
|         createOverlay(src, target.alt || ""); |   } | ||||||
|       }, |  | ||||||
|       true |   if (document.readyState === 'loading') { | ||||||
|     ); |     document.addEventListener('DOMContentLoaded', init, { once: true }); | ||||||
|   }, { once: true }); |   } else { | ||||||
|  |     init(); | ||||||
|  |   } | ||||||
| })(); | })(); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Xin
					Xin