Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions assets/scss/_image-modal_project.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,33 @@
/* The markdown image container */
.md__image {
position: relative;
display: block;
width: fit-content;
max-width: 100%;
margin: 0 auto;
cursor: pointer;
}

/* Native button used as the keyboard-accessible image trigger */
.md__image-trigger {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 0;
border: 0;
background: transparent;
color: inherit;
font: inherit;
text-align: inherit;
cursor: pointer;
border-radius: 4px;

&:focus-visible {
outline: 2px solid #00b39f;
outline-offset: 4px;
}
}

/* The Modal (background) */
Expand Down Expand Up @@ -48,6 +75,11 @@
overflow: hidden;
border: 0;
z-index: 99;

&:focus-visible {
outline: 2px solid #00b39f;
outline-offset: 2px;
}
}

.modal-close::after {
Expand All @@ -56,12 +88,12 @@
top: 0.2em;
left: 0.1em;
text-indent: 0;
content: "\00D7"
content: "\00D7";
}

.modal-close:hover,
.modal-close:focus {
color: #999;
text-decoration: none;
cursor: pointer;
}
}
19 changes: 13 additions & 6 deletions layouts/_default/_markup/render-image.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,22 @@

<div class="md__image">
{{- with $img }}
{{- $webp := .Process "webp q85" }}
<picture>
<source srcset="{{ $webp.RelPermalink }}" type="image/webp" width="{{ .Width }}" height="{{ .Height }}">
<img src="{{ $dest | safeURL }}" onclick="openModal(this)" alt="{{ $alt }}"
{{- if and (eq .ResourceType "image") (ne .MediaType.SubType "svg") }}
{{- $webp := .Process "webp q85" }}
<picture>
<source srcset="{{ $webp.RelPermalink }}" type="image/webp" width="{{ .Width }}" height="{{ .Height }}">
<img src="{{ $dest | safeURL }}" alt="{{ $alt }}"
width="{{ .Width }}" height="{{ .Height }}"
class="{{ $class }}" />
</picture>
{{- else }}
<img src="{{ $dest | safeURL }}" alt="{{ $alt }}"
width="{{ .Width }}" height="{{ .Height }}"
class="{{ $class }}" />
</picture>
{{- end }}
{{- else }}
<img src="{{ $dest | safeURL }}" onclick="openModal(this)" alt="{{ $alt }}"
<img src="{{ $dest | safeURL }}" alt="{{ $alt }}"
class="{{ $class }}" />
{{- end }}
<button type="button" class="md__image-trigger" aria-label="Expand image{{ with $alt }}: {{ . }}{{ end }}"></button>
</div>
213 changes: 183 additions & 30 deletions layouts/partials/image-modal.html
Original file line number Diff line number Diff line change
@@ -1,61 +1,214 @@
<div id="myModal" class="modal">
<button class="modal-close" onclick="closeModal()">close</button>
<div
id="myModal"
class="modal"
role="dialog"
aria-modal="true"
aria-label="Image preview"
>
<button
type="button"
class="modal-close"
onclick="closeModal()"
aria-label="Close image preview"
>
close
</button>
<div class="modal-cont">
<img class="modal-pic" id="modalPic" onclick="closeModal()" style="max-width: 100%; max-height: 80vh; margin: auto;" alt="Modal-pic">
<img
class="modal-pic"
id="modalPic"
onclick="closeModal()"
style="max-width: 100%; max-height: 80vh; margin: auto;"
alt=""
>
</div>
</div>

<script>
var modalTrigger = null;

// Open the Modal
function openModal(imageIdOrElement) {
var src;
var image = null;
var trigger = null;

if (typeof imageIdOrElement === 'string') {
// If it's a string (image ID), get the source using the ID
src = document.getElementById(imageIdOrElement).src;
if (typeof imageIdOrElement === "string") {
image = document.getElementById(imageIdOrElement);
if (image) {
var container = image.closest(".md__image");
trigger = container ? container.querySelector(".md__image-trigger") : null;
}
} else if (imageIdOrElement instanceof HTMLImageElement) {
// If <img>, get the source directly. currentSrc is what the browser actually
// chose, so inside a <picture> the modal reuses the already-fetched WebP
// rather than downloading the larger original.
src = imageIdOrElement.currentSrc || imageIdOrElement.src;
image = imageIdOrElement;
var container = image.closest(".md__image");
trigger = container ? container.querySelector(".md__image-trigger") : image.closest(".md__image-trigger");
} else if (imageIdOrElement instanceof HTMLElement) {
if (imageIdOrElement.classList.contains("md__image-trigger")) {
trigger = imageIdOrElement;
var parent = trigger.closest(".md__image");
image = parent ? parent.querySelector("img") : null;
} else {
trigger = imageIdOrElement.closest(".md__image-trigger") || imageIdOrElement;
image = imageIdOrElement.querySelector("img") || imageIdOrElement;
}
}

if (!image || !(image instanceof HTMLImageElement)) {
return;
}

// Use currentSrc if available (e.g. inside a <picture>) so the modal reuses
// the already-fetched WebP image rather than downloading the larger fallback.
var src = image.currentSrc || image.src;
if (src && src.includes("#")) {
src = src.substring(0, src.indexOf("#"));
}

document.getElementById("modalPic").src = src;
document.getElementById("myModal").style.display = "block";
var modal = document.getElementById("myModal");
var modalPic = document.getElementById("modalPic");
var closeButton = modal ? modal.querySelector(".modal-close") : null;

// Track the trigger element to restore focus on close:
// Prefer the button trigger; fall back to the image element itself.
modalTrigger = trigger || image;

if (modalTrigger === image && !image.hasAttribute("tabindex")) {
image.setAttribute("tabindex", "-1");
}

if (modalPic) {
modalPic.src = src;
modalPic.alt = image.alt || "";
}

if (modal) {
modal.style.display = "block";
}

if (closeButton) {
closeButton.focus();
}
}

// Close the Modal
function closeModal() {
document.getElementById("modalPic").src = "";
document.getElementById("myModal").style.display = "none";
function closeModal() {
var modal = document.getElementById("myModal");
var modalPic = document.getElementById("modalPic");
var trigger = modalTrigger;

if (modalPic) {
modalPic.src = "";
modalPic.alt = "";
}

if (modal) {
modal.style.display = "none";
}

modalTrigger = null;

if (trigger && typeof trigger.focus === "function" && document.contains(trigger)) {
trigger.focus();
}
}

function isModalOpen() {
var modal = document.getElementById("myModal");
return modal && modal.style.display === "block";
}

// Listen for the Escape key to close the modal
// Trap keyboard focus within the modal dialog
function trapModalFocus(event) {
if (event.key !== "Tab" || !isModalOpen()) {
return;
}

var modal = document.getElementById("myModal");
var focusableElements = modal.querySelectorAll(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
);

if (!focusableElements.length) {
event.preventDefault();
return;
}

var firstElement = focusableElements[0];
var lastElement = focusableElements[focusableElements.length - 1];

if (!modal.contains(document.activeElement)) {
event.preventDefault();
firstElement.focus();
return;
}

if (event.shiftKey && document.activeElement === firstElement) {
event.preventDefault();
lastElement.focus();
} else if (!event.shiftKey && document.activeElement === lastElement) {
event.preventDefault();
firstElement.focus();
}
}

// Listen for Escape key and Tab key
document.addEventListener("keydown", function (event) {
if (event.key === "Escape") {
if (event.key === "Escape" && isModalOpen()) {
event.preventDefault();
closeModal();
return;
}

trapModalFocus(event);
});

// Attach onclick attribute to all <img> after DOM is loaded
document.addEventListener("DOMContentLoaded", function () {
var imgTags = document.querySelectorAll("img");
imgTags.forEach(function (img) {
img.onclick = function () {
if (img.dataset.modal !== "false") {
openModal(img);
var modal = document.getElementById("myModal");

// Handle Markdown image triggers
var imageTriggers = document.querySelectorAll(".md__image-trigger");
imageTriggers.forEach(function (trigger) {
// If the image is inside an anchor link, avoid nesting interactive elements.
var container = trigger.closest(".md__image");
var link = trigger.closest("a") || (container ? container.closest("a") || container.querySelector("a") : null);
if (link) {
var linkedImg = container ? container.querySelector("img") : null;
if (linkedImg) {
linkedImg.dataset.modal = "false";
}
};
trigger.remove();
return;
}

trigger.addEventListener("click", function (event) {
event.preventDefault();
openModal(trigger);
});
});

// Close modal when clicking anywhere inside #myModal
var modal = document.getElementById("myModal");
modal.addEventListener("click", function () {
closeModal();
// Attach click handler to any raw content <img> not inside .md__image-trigger or <a>
document.querySelectorAll("img").forEach(function (img) {
if (
img.dataset.modal === "false" ||
img.closest("#myModal") ||
img.closest("a") ||
img.closest(".md__image-trigger")
) {
return;
}

img.addEventListener("click", function () {
openModal(img);
});
});

// Close modal when clicking directly on the overlay background
if (modal) {
modal.addEventListener("click", function (event) {
if (event.target === modal) {
closeModal();
}
});
}
});
</script>
</script>
Loading