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:
Xin
2025-09-10 23:59:51 +01:00
parent 3bc454bbf6
commit 09728a4aa9
5 changed files with 141 additions and 0 deletions

View 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
View 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
);
});
})();

View File

@@ -238,3 +238,6 @@ params:
# inputPosition: top
# lang: en
# theme: noborder_dark
imageZoom:
enable: true

View File

@@ -13,3 +13,10 @@
{{- if (.Store.Get "hasAsciinema") -}}
{{- partial "scripts/asciinema.html" . -}}
{{- end -}}
{{/* Image zoom */}}
{{- with site.Params.imageZoom }}
{{- if .enable }}
{{- partial "scripts/image-zoom.html" $ -}}
{{- end -}}
{{- end -}}

View 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>