mirror of
https://github.com/imfing/hextra.git
synced 2025-09-16 03:56:48 -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