improved Modal with zoom and pagination. also implementing thumbnail generation

This commit is contained in:
Bryan Van Deusen
2025-08-01 23:19:17 -04:00
parent e3a4c348f1
commit 63918363c1
15 changed files with 536 additions and 176 deletions
+209
View File
@@ -0,0 +1,209 @@
document.addEventListener('DOMContentLoaded', () => {
const modal = document.getElementById('imageModal');
const modalImage = document.getElementById('modalImage');
const modalPrev = document.getElementById('modalPrev');
const modalNext = document.getElementById('modalNext');
const modalClose = document.getElementById('modalClose');
const modalImageWrapper = document.querySelector('.modal-image-wrapper');
modalImage.setAttribute('draggable', 'false');
let images = Array.from(document.querySelectorAll('.img-clickable'));
let currentIndex = -1;
let isZoomed = false;
let isDragging = false;
let startX, startY, scrollLeft, scrollTop;
let dragStartX = 0;
let dragStartY = 0;
let dragMoved = false;
const hasNextPageInput = document.getElementById('hasNextPage');
const nextPageUrlInput = document.getElementById('nextPageUrl');
const hasPrevPageInput = document.getElementById('hasPrevPage');
const prevPageUrlInput = document.getElementById('prevPageUrl');
function updateImage(index) {
const img = images[index];
modalImage.src = img.dataset.full;
currentIndex = index;
resetZoom();
}
function openModal(index) {
modal.classList.add('active');
updateImage(index);
history.replaceState({ modalIndex: index }, '', window.location.href);
}
function closeModal() {
modal.classList.remove('active');
modalImage.src = '';
currentIndex = -1;
resetZoom();
history.replaceState({}, '', window.location.pathname + window.location.search);
}
function showNext() {
if (currentIndex < images.length - 1) {
updateImage(currentIndex + 1);
} else if (hasNextPageInput?.value === "true") {
fetch(nextPageUrlInput.value)
.then(res => res.text())
.then(html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const newGallery = doc.querySelector('.gallery-grid');
const newImages = doc.querySelectorAll('.img-clickable');
if (newGallery && newImages.length > 0) {
const gallery = document.querySelector('.gallery-grid');
gallery.innerHTML = newGallery.innerHTML;
images = Array.from(document.querySelectorAll('.img-clickable'));
images.forEach((img, i) => {
img.addEventListener('click', () => openModal(i));
});
const newHasNext = doc.getElementById('hasNextPage')?.value || "false";
const newNextUrl = doc.getElementById('nextPageUrl')?.value || "";
const newHasPrev = doc.getElementById('hasPrevPage')?.value || "false";
const newPrevUrl = doc.getElementById('prevPageUrl')?.value || "";
hasNextPageInput.value = newHasNext;
nextPageUrlInput.value = newNextUrl;
hasPrevPageInput.value = newHasPrev;
prevPageUrlInput.value = newPrevUrl;
history.pushState({}, '', nextPageUrlInput.value);
openModal(0);
}
});
}
}
function showPrev() {
if (currentIndex > 0) {
updateImage(currentIndex - 1);
} else if (hasPrevPageInput?.value === "true") {
fetch(prevPageUrlInput.value)
.then(res => res.text())
.then(html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const newGallery = doc.querySelector('.gallery-grid');
const newImages = doc.querySelectorAll('.img-clickable');
if (newGallery && newImages.length > 0) {
const gallery = document.querySelector('.gallery-grid');
gallery.innerHTML = newGallery.innerHTML;
images = Array.from(document.querySelectorAll('.img-clickable'));
images.forEach((img, i) => {
img.addEventListener('click', () => openModal(i));
});
const newHasNext = doc.getElementById('hasNextPage')?.value || "false";
const newNextUrl = doc.getElementById('nextPageUrl')?.value || "";
const newHasPrev = doc.getElementById('hasPrevPage')?.value || "false";
const newPrevUrl = doc.getElementById('prevPageUrl')?.value || "";
hasNextPageInput.value = newHasNext;
nextPageUrlInput.value = newNextUrl;
hasPrevPageInput.value = newHasPrev;
prevPageUrlInput.value = newPrevUrl;
history.pushState({}, '', prevPageUrlInput.value);
openModal(images.length - 1);
}
});
}
}
function toggleZoom() {
isZoomed = !isZoomed;
modalImageWrapper.classList.toggle('zoomed', isZoomed);
modalImageWrapper.style.cursor = isZoomed ? 'grab' : '';
if (!isZoomed) {
modalImageWrapper.scrollLeft = 0;
modalImageWrapper.scrollTop = 0;
}
}
function resetZoom() {
isZoomed = false;
modalImageWrapper.classList.remove('zoomed');
modalImageWrapper.scrollLeft = 0;
modalImageWrapper.scrollTop = 0;
modalImageWrapper.style.cursor = '';
}
modalImageWrapper.addEventListener('mousedown', (e) => {
if (!isZoomed) return;
isDragging = true;
dragMoved = false;
modalImageWrapper.style.cursor = 'grabbing';
startX = e.pageX - modalImageWrapper.offsetLeft;
startY = e.pageY - modalImageWrapper.offsetTop;
scrollLeft = modalImageWrapper.scrollLeft;
scrollTop = modalImageWrapper.scrollTop;
dragStartX = e.pageX;
dragStartY = e.pageY;
});
modalImageWrapper.addEventListener('mouseleave', () => {
isDragging = false;
if (isZoomed) modalImageWrapper.style.cursor = 'grab';
});
modalImageWrapper.addEventListener('mouseup', () => {
isDragging = false;
if (isZoomed) modalImageWrapper.style.cursor = 'grab';
});
modalImageWrapper.addEventListener('mousemove', (e) => {
if (!isDragging || !isZoomed) return;
e.preventDefault();
dragMoved = true;
const x = e.pageX - modalImageWrapper.offsetLeft;
const y = e.pageY - modalImageWrapper.offsetTop;
const walkX = x - startX;
const walkY = y - startY;
modalImageWrapper.scrollLeft = scrollLeft - walkX;
modalImageWrapper.scrollTop = scrollTop - walkY;
});
modalImageWrapper.addEventListener('click', (e) => {
if (dragMoved) {
dragMoved = false;
return;
}
toggleZoom();
});
modalClose?.addEventListener('click', closeModal);
modalPrev?.addEventListener('click', showPrev);
modalNext?.addEventListener('click', showNext);
modal.addEventListener('click', (e) => {
if (e.target === modal) closeModal();
});
document.addEventListener('keydown', (e) => {
if (!modal.classList.contains('active')) return;
if (e.key === 'ArrowRight') showNext();
if (e.key === 'ArrowLeft') showPrev();
if (e.key === 'Escape') closeModal();
});
images.forEach((img, index) => {
img.addEventListener('click', () => openModal(index));
});
window.addEventListener('popstate', () => {
if (modal.classList.contains('active')) {
closeModal();
} else {
location.reload();
}
});
});
+82 -38
View File
@@ -43,22 +43,39 @@ body {
transform: translateY(1px);
}
.flash-container {
max-width: 800px;
margin: 1rem auto;
padding: 0 1rem;
}
.flash {
padding: 0.5rem 1rem;
margin: 1rem 0;
border-radius: 4px;
padding: 1rem;
margin-bottom: 1rem;
border-radius: 6px;
font-weight: 500;
color: #fff;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
.flash.success {
background-color: #d4edda;
color: #155724;
.flash-info {
background-color: #3182ce; /* blue */
}
.flash.danger {
background-color: #f8d7da;
color: #721c24;
.flash-message, .flash-success {
background-color: #38a169; /* green */
}
.flash-warning {
background-color: #dd6b20; /* orange */
}
.flash-danger,
.flash-error {
background-color: #e53e3e; /* red */
}
.content {
padding: 8px;
}
@@ -84,7 +101,7 @@ body {
flex-direction: column;
align-items: center;
height: 450px;
background-color: rgba(1,1,1,.1);
background-color: #1c1c1c;
box-shadow: 0 3px 4px rgba(0,0,0,0.7);
padding: 8px;
border-radius: 8px;
@@ -140,6 +157,11 @@ body {
border-color: #007BFF;
}
.pagination-ellipsis {
padding: 0 0.5rem;
color: #999;
}
/* MODAL STYLES */
.modal {
display: none;
@@ -157,72 +179,94 @@ body {
.modal-content {
position: relative;
max-width: 95vw;
width: 100%;
max-width: 1600px;
max-height: 95vh;
display: flex;
flex-direction: column;
align-items: center;
}
/* Modal body */
.modal-body {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
width: 100%;
}
.modal-body img {
max-width: 90vw;
/* Image wrapper for tall image logic */
.modal-image-wrapper {
max-width: 100%;
max-height: 80vh;
overflow: hidden;
cursor: default;
}
.modal-image-wrapper.zoomed {
overflow: auto;
cursor: grab;
}
.modal-image-wrapper.zoomed img {
width: auto;
height: auto;
max-width: none;
max-height: none;
}
.modal-image-wrapper img {
max-width: 100%;
max-height: 80vh;
object-fit: contain;
border-radius: 6px;
box-shadow: 0 0 10px rgba(0,0,0,0.6);
}
.modal-filename {
color: #fff;
background: rgba(0, 0, 0, 0.6);
padding: 0.5rem 1rem;
border-radius: 4px;
font-size: 0.9rem;
text-align: center;
word-break: break-word;
/* Adjust for very tall images */
.modal-image-wrapper.too-tall {
overflow-y: auto;
}
/* Modal arrows and close */
.modal-image-wrapper.too-tall img {
width: 90vw;
height: auto;
max-height: none;
}
/* Modal buttons */
.modal-button {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: rgba(0, 0, 0, 0.4);
top: 10%;
bottom: 10%;
width: 60px;
background: rgba(0, 0, 0, 0.3);
border: none;
color: white;
font-size: 2rem;
padding: 0.5rem 1rem;
font-size: 2.5rem;
cursor: pointer;
z-index: 1001;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s;
border-radius: 4px;
}
.modal:hover .modal-button {
.modal-content:hover .modal-button {
opacity: 1;
}
.modal-prev {
left: -60px;
left: 0;
}
.modal-next {
right: -60px;
}
.modal-close {
top: 10px;
right: 10px;
transform: none;
font-size: 1.5rem;
opacity: 1;
right: 0;
}
/* Artist Styling */