Add smooth drag-to-slide gallery with 60% threshold and real-time tracking

This commit is contained in:
2025-11-24 14:39:33 +09:00
parent 3293a06ed7
commit c46170fc2b

View File

@@ -68,14 +68,48 @@
background: #f8f9fa; background: #f8f9fa;
border-radius: 12px; border-radius: 12px;
overflow: hidden; overflow: hidden;
touch-action: pan-y pinch-zoom;
}
.gallery-slides-wrapper {
position: relative;
width: 100%;
height: 600px;
overflow: hidden;
cursor: grab;
}
.gallery-slides-wrapper.dragging {
cursor: grabbing;
}
.gallery-slides-container {
display: flex;
height: 100%;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform;
}
.gallery-slides-container.no-transition {
transition: none;
}
.gallery-slide {
flex: 0 0 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #000;
} }
.gallery-main-image { .gallery-main-image {
width: 100%; width: 100%;
height: 600px; height: 100%;
object-fit: contain; object-fit: contain;
cursor: zoom-in; cursor: zoom-in;
background: #000; user-select: none;
pointer-events: none;
} }
/* Navigation Arrows */ /* Navigation Arrows */
@@ -159,7 +193,7 @@
height: 80px; height: 80px;
} }
.gallery-main-image { .gallery-slides-wrapper {
height: 400px; height: 400px;
} }
@@ -186,6 +220,24 @@
flex-wrap: wrap; flex-wrap: wrap;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
/* Drag indicator */
.gallery-slides-wrapper::after {
content: '';
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
width: 40px;
height: 4px;
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
pointer-events: none;
}
.gallery-slides-wrapper.dragging::after {
background: rgba(255, 255, 255, 0.6);
}
</style> </style>
{% endblock %} {% endblock %}
@@ -258,12 +310,31 @@
<i class="fas fa-chevron-right"></i> <i class="fas fa-chevron-right"></i>
</button> </button>
<!-- Main Image --> <!-- Slides Wrapper -->
<img src="{% if item.image %}{{ item.image.url }}{% else %}{{ item.gallery_images.first.image.url }}{% endif %}" <div class="gallery-slides-wrapper" id="galleryWrapper">
class="gallery-main-image" <div class="gallery-slides-container" id="gallerySlides">
id="galleryMainImage" <!-- Slides will be generated by JavaScript -->
data-lightbox="portfolio-gallery" {% if item.image %}
alt="{{ item.title }}"> <div class="gallery-slide">
<img src="{{ item.image.url }}"
class="gallery-main-image"
data-lightbox="portfolio-gallery"
data-title="{{ item.title }}"
alt="{{ item.title }}">
</div>
{% endif %}
{% for gallery_image in item.gallery_images.all %}
<div class="gallery-slide">
<img src="{{ gallery_image.image.url }}"
class="gallery-main-image"
data-lightbox="portfolio-gallery"
data-title="{{ gallery_image.caption|default:item.title }}"
alt="{{ gallery_image.caption }}">
</div>
{% endfor %}
</div>
</div>
<!-- Gallery Counter --> <!-- Gallery Counter -->
<div class="gallery-counter" id="galleryCounter"> <div class="gallery-counter" id="galleryCounter">
@@ -309,11 +380,11 @@
}); });
</script> </script>
<!-- Advanced Gallery Navigation Script --> <!-- Advanced Gallery Navigation Script with Smooth Drag -->
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const galleryMain = document.getElementById('galleryMain'); const galleryWrapper = document.getElementById('galleryWrapper');
const mainImage = document.getElementById('galleryMainImage'); const gallerySlides = document.getElementById('gallerySlides');
const thumbnails = document.querySelectorAll('.thumbnail-item'); const thumbnails = document.querySelectorAll('.thumbnail-item');
const prevBtn = document.getElementById('galleryPrev'); const prevBtn = document.getElementById('galleryPrev');
const nextBtn = document.getElementById('galleryNext'); const nextBtn = document.getElementById('galleryNext');
@@ -323,22 +394,49 @@ document.addEventListener('DOMContentLoaded', function() {
let currentIndex = 0; let currentIndex = 0;
const totalImages = thumbnails.length; const totalImages = thumbnails.length;
// Update gallery display // Drag/Swipe variables
function updateGallery(index) { let isDragging = false;
let startX = 0;
let currentTranslate = 0;
let prevTranslate = 0;
let animationID = 0;
// Threshold for slide change (60% of wrapper width)
const slideThreshold = 0.6;
// Update gallery position
function setSliderPosition() {
gallerySlides.style.transform = `translateX(${currentTranslate}px)`;
}
// Smooth animation
function animation() {
setSliderPosition();
if (isDragging) requestAnimationFrame(animation);
}
// Update gallery to specific index
function updateGallery(index, smooth = true) {
if (index < 0) index = totalImages - 1; if (index < 0) index = totalImages - 1;
if (index >= totalImages) index = 0; if (index >= totalImages) index = 0;
currentIndex = index; currentIndex = index;
// Calculate position
const wrapperWidth = galleryWrapper.offsetWidth;
currentTranslate = -currentIndex * wrapperWidth;
prevTranslate = currentTranslate;
// Update with or without transition
if (smooth) {
gallerySlides.classList.remove('no-transition');
}
setSliderPosition();
// Update thumbnail
const currentThumbnail = thumbnails[index]; const currentThumbnail = thumbnails[index];
const fullUrl = currentThumbnail.dataset.full;
const caption = currentThumbnail.dataset.caption; const caption = currentThumbnail.dataset.caption;
// Update main image
mainImage.src = fullUrl;
mainImage.setAttribute('href', fullUrl);
// Update active thumbnail
thumbnails.forEach(thumb => thumb.classList.remove('active')); thumbnails.forEach(thumb => thumb.classList.remove('active'));
currentThumbnail.classList.add('active'); currentThumbnail.classList.add('active');
@@ -357,12 +455,84 @@ document.addEventListener('DOMContentLoaded', function() {
currentThumbnail.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); currentThumbnail.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} }
// Thumbnail click handlers // Mouse/Touch start
function handleDragStart(e) {
isDragging = true;
galleryWrapper.classList.add('dragging');
gallerySlides.classList.add('no-transition');
startX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
animationID = requestAnimationFrame(animation);
}
// Mouse/Touch move
function handleDragMove(e) {
if (!isDragging) return;
const currentX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
const deltaX = currentX - startX;
currentTranslate = prevTranslate + deltaX;
// Add resistance at edges
const wrapperWidth = galleryWrapper.offsetWidth;
const maxTranslate = 0;
const minTranslate = -(totalImages - 1) * wrapperWidth;
if (currentTranslate > maxTranslate) {
// Resistance at start
currentTranslate = maxTranslate + (currentTranslate - maxTranslate) * 0.3;
} else if (currentTranslate < minTranslate) {
// Resistance at end
currentTranslate = minTranslate + (currentTranslate - minTranslate) * 0.3;
}
}
// Mouse/Touch end
function handleDragEnd(e) {
if (!isDragging) return;
isDragging = false;
galleryWrapper.classList.remove('dragging');
cancelAnimationFrame(animationID);
const movedBy = currentTranslate - prevTranslate;
const wrapperWidth = galleryWrapper.offsetWidth;
const threshold = wrapperWidth * slideThreshold;
// Check if moved enough to change slide
if (Math.abs(movedBy) > threshold) {
if (movedBy < 0) {
// Moved left - next slide
updateGallery(currentIndex + 1);
} else {
// Moved right - previous slide
updateGallery(currentIndex - 1);
}
} else {
// Return to current slide
updateGallery(currentIndex);
}
}
// Mouse events
galleryWrapper.addEventListener('mousedown', handleDragStart);
galleryWrapper.addEventListener('mousemove', handleDragMove);
galleryWrapper.addEventListener('mouseup', handleDragEnd);
galleryWrapper.addEventListener('mouseleave', handleDragEnd);
// Touch events
galleryWrapper.addEventListener('touchstart', handleDragStart, { passive: true });
galleryWrapper.addEventListener('touchmove', handleDragMove, { passive: true });
galleryWrapper.addEventListener('touchend', handleDragEnd, { passive: true });
// Thumbnail clicks
thumbnails.forEach((thumbnail, index) => { thumbnails.forEach((thumbnail, index) => {
thumbnail.addEventListener('click', () => updateGallery(index)); thumbnail.addEventListener('click', () => updateGallery(index));
}); });
// Navigation button handlers // Navigation buttons
prevBtn.addEventListener('click', (e) => { prevBtn.addEventListener('click', (e) => {
e.preventDefault(); e.preventDefault();
updateGallery(currentIndex - 1); updateGallery(currentIndex - 1);
@@ -382,45 +552,22 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
// Touch/Swipe support for mobile devices // Prevent context menu on long press
let touchStartX = 0; galleryWrapper.addEventListener('contextmenu', (e) => {
let touchEndX = 0; if (isDragging) e.preventDefault();
const swipeThreshold = 50; });
galleryMain.addEventListener('touchstart', (e) => { // Initialize gallery
touchStartX = e.changedTouches[0].screenX; updateGallery(0, false);
}, { passive: true });
galleryMain.addEventListener('touchend', (e) => { // Handle window resize
touchEndX = e.changedTouches[0].screenX; let resizeTimer;
handleSwipe(); window.addEventListener('resize', () => {
}, { passive: true }); clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
function handleSwipe() { updateGallery(currentIndex, false);
const swipeDistance = touchStartX - touchEndX; }, 250);
});
if (Math.abs(swipeDistance) > swipeThreshold) {
if (swipeDistance > 0) {
// Swipe left - next image
updateGallery(currentIndex + 1);
} else {
// Swipe right - previous image
updateGallery(currentIndex - 1);
}
}
}
// Mouse wheel support (optional)
galleryMain.addEventListener('wheel', (e) => {
if (Math.abs(e.deltaY) > 10) {
e.preventDefault();
if (e.deltaY > 0) {
updateGallery(currentIndex + 1);
} else {
updateGallery(currentIndex - 1);
}
}
}, { passive: false });
}); });
</script> </script>
{% endblock %} {% endblock %}