mirror of
				https://github.com/imfing/hextra.git
				synced 2025-10-31 17:04:55 -04:00 
			
		
		
		
	feat(image-zoom): add minimal image zoom functionality
- Introduced CSS for image zoom overlay and image styling. - Implemented JavaScript for handling image zoom interactions, including overlay creation and close functionality. - Updated configuration to enable image zoom feature in site parameters. - Added partial for including image zoom assets in the layout.
This commit is contained in:
		
							
								
								
									
										42
									
								
								assets/css/components/image-zoom.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								assets/css/components/image-zoom.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | /* Minimal styles for Hextra image zoom overlay */ | ||||||
|  | .hextra-zoom-image-overlay { | ||||||
|  |   position: fixed; | ||||||
|  |   inset: 0; | ||||||
|  |   background: var(--hextra-image-zoom-backdrop, rgba(0, 0, 0, 0.9)); | ||||||
|  |   display: flex; | ||||||
|  |   align-items: center; | ||||||
|  |   justify-content: center; | ||||||
|  |   z-index: 9999; | ||||||
|  |   opacity: 0; | ||||||
|  |   transition: opacity 0.25s ease-out; | ||||||
|  |   cursor: zoom-out; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .hextra-zoom-image-overlay.show { | ||||||
|  |   opacity: 1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .hextra-zoom-image { | ||||||
|  |   max-width: min(95vw, 1200px); | ||||||
|  |   max-height: 95vh; | ||||||
|  |   border-radius: 8px; | ||||||
|  |   box-shadow: 0 8px 40px rgba(0, 0, 0, 0.35); | ||||||
|  |   transition: transform 0.3s ease-out; | ||||||
|  |   transform: scale(0.98); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .hextra-zoom-image-overlay.show .hextra-zoom-image { | ||||||
|  |   transform: scale(1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @media (prefers-reduced-motion: reduce) { | ||||||
|  |   .hextra-zoom-image-overlay, | ||||||
|  |   .hextra-zoom-image { | ||||||
|  |     transition: none !important; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Show magnifier cursor over zoomable images in content */ | ||||||
|  | .content img:not([data-no-zoom]) { | ||||||
|  |   cursor: zoom-in; | ||||||
|  | } | ||||||
							
								
								
									
										76
									
								
								assets/js/image-zoom.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								assets/js/image-zoom.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | |||||||
|  | // Minimal, dependency-free image zoom for Hextra | ||||||
|  | // - Activates on images inside `.content` | ||||||
|  | // - Close on overlay click or Escape | ||||||
|  | // - Opt-out with `data-no-zoom` on <img> | ||||||
|  |  | ||||||
|  | (function () { | ||||||
|  |   function ready(fn) { | ||||||
|  |     if (document.readyState === "loading") { | ||||||
|  |       document.addEventListener("DOMContentLoaded", fn, { once: true }); | ||||||
|  |     } else { | ||||||
|  |       fn(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function createOverlay(src, alt) { | ||||||
|  |     const overlay = document.createElement("div"); | ||||||
|  |     overlay.className = "hextra-zoom-image-overlay"; | ||||||
|  |     overlay.setAttribute("role", "dialog"); | ||||||
|  |     overlay.setAttribute("aria-modal", "true"); | ||||||
|  |  | ||||||
|  |     const img = document.createElement("img"); | ||||||
|  |     img.className = "hextra-zoom-image"; | ||||||
|  |     img.src = src; | ||||||
|  |     if (alt) img.alt = alt; | ||||||
|  |  | ||||||
|  |     overlay.appendChild(img); | ||||||
|  |  | ||||||
|  |     function close() { | ||||||
|  |       overlay.classList.remove("show"); | ||||||
|  |       document.documentElement.style.removeProperty("overflow"); | ||||||
|  |       window.removeEventListener("keydown", onKeyDown, true); | ||||||
|  |       overlay.addEventListener( | ||||||
|  |         "transitionend", | ||||||
|  |         () => overlay.remove(), | ||||||
|  |         { once: true } | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function onKeyDown(e) { | ||||||
|  |       if (e.key === "Escape") close(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     overlay.addEventListener("click", close, { once: true }); | ||||||
|  |     window.addEventListener("keydown", onKeyDown, true); | ||||||
|  |  | ||||||
|  |     document.body.appendChild(overlay); | ||||||
|  |     // lock scroll | ||||||
|  |     document.documentElement.style.overflow = "hidden"; | ||||||
|  |  | ||||||
|  |     // trigger fade-in | ||||||
|  |     requestAnimationFrame(() => overlay.classList.add("show")); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ready(function () { | ||||||
|  |     const container = document.querySelector(".content"); | ||||||
|  |     if (!container) return; | ||||||
|  |  | ||||||
|  |     container.addEventListener( | ||||||
|  |       "click", | ||||||
|  |       function (e) { | ||||||
|  |         const target = e.target; | ||||||
|  |         if (!(target instanceof HTMLImageElement)) return; | ||||||
|  |         if (target.dataset.noZoom === "" || target.dataset.noZoom === "true") return; | ||||||
|  |  | ||||||
|  |         // avoid following parent links when zooming | ||||||
|  |         e.preventDefault(); | ||||||
|  |         e.stopPropagation(); | ||||||
|  |  | ||||||
|  |         const src = target.currentSrc || target.src; | ||||||
|  |         if (!src) return; | ||||||
|  |         createOverlay(src, target.alt || ""); | ||||||
|  |       }, | ||||||
|  |       true | ||||||
|  |     ); | ||||||
|  |   }); | ||||||
|  | })(); | ||||||
| @@ -238,3 +238,6 @@ params: | |||||||
|       # inputPosition: top |       # inputPosition: top | ||||||
|       # lang: en |       # lang: en | ||||||
|       # theme: noborder_dark |       # theme: noborder_dark | ||||||
|  |  | ||||||
|  |   imageZoom: | ||||||
|  |     enable: true | ||||||
|   | |||||||
| @@ -13,3 +13,10 @@ | |||||||
| {{- if (.Store.Get "hasAsciinema") -}} | {{- if (.Store.Get "hasAsciinema") -}} | ||||||
|   {{- partial "scripts/asciinema.html" . -}} |   {{- partial "scripts/asciinema.html" . -}} | ||||||
| {{- end -}} | {{- end -}} | ||||||
|  |  | ||||||
|  | {{/* Image zoom */}} | ||||||
|  | {{- with site.Params.imageZoom }} | ||||||
|  |   {{- if .enable }} | ||||||
|  |     {{- partial "scripts/image-zoom.html" $ -}} | ||||||
|  |   {{- end -}} | ||||||
|  | {{- end -}} | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								layouts/_partials/scripts/image-zoom.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								layouts/_partials/scripts/image-zoom.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | {{/* Optional minimal image zoom assets */}} | ||||||
|  | {{- $js := resources.Get "js/image-zoom.js" -}} | ||||||
|  | {{- $css := resources.Get "css/components/image-zoom.css" -}} | ||||||
|  |  | ||||||
|  | {{- if hugo.IsProduction -}} | ||||||
|  |   {{- $js = $js | minify | fingerprint -}} | ||||||
|  |   {{- $css = $css | minify | fingerprint -}} | ||||||
|  | {{- end -}} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | <link rel="preload" href="{{ $css.RelPermalink }}" as="style" integrity="{{ $css.Data.Integrity }}" /> | ||||||
|  | <link href="{{ $css.RelPermalink }}" rel="stylesheet" integrity="{{ $css.Data.Integrity }}" /> | ||||||
|  | <script defer src="{{ $js.RelPermalink }}" integrity="{{ $js.Data.Integrity }}"></script> | ||||||
		Reference in New Issue
	
	Block a user
	 Xin
					Xin