Add smooth drag-to-slide gallery with 60% threshold and real-time tracking
This commit is contained in:
@@ -68,14 +68,48 @@
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
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 {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
cursor: zoom-in;
|
||||
background: #000;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Navigation Arrows */
|
||||
@@ -159,7 +193,7 @@
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.gallery-main-image {
|
||||
.gallery-slides-wrapper {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
@@ -186,6 +220,24 @@
|
||||
flex-wrap: wrap;
|
||||
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>
|
||||
{% endblock %}
|
||||
|
||||
@@ -258,12 +310,31 @@
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
|
||||
<!-- Main Image -->
|
||||
<img src="{% if item.image %}{{ item.image.url }}{% else %}{{ item.gallery_images.first.image.url }}{% endif %}"
|
||||
<!-- Slides Wrapper -->
|
||||
<div class="gallery-slides-wrapper" id="galleryWrapper">
|
||||
<div class="gallery-slides-container" id="gallerySlides">
|
||||
<!-- Slides will be generated by JavaScript -->
|
||||
{% if item.image %}
|
||||
<div class="gallery-slide">
|
||||
<img src="{{ item.image.url }}"
|
||||
class="gallery-main-image"
|
||||
id="galleryMainImage"
|
||||
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 -->
|
||||
<div class="gallery-counter" id="galleryCounter">
|
||||
@@ -309,11 +380,11 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Advanced Gallery Navigation Script -->
|
||||
<!-- Advanced Gallery Navigation Script with Smooth Drag -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const galleryMain = document.getElementById('galleryMain');
|
||||
const mainImage = document.getElementById('galleryMainImage');
|
||||
const galleryWrapper = document.getElementById('galleryWrapper');
|
||||
const gallerySlides = document.getElementById('gallerySlides');
|
||||
const thumbnails = document.querySelectorAll('.thumbnail-item');
|
||||
const prevBtn = document.getElementById('galleryPrev');
|
||||
const nextBtn = document.getElementById('galleryNext');
|
||||
@@ -323,22 +394,49 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
let currentIndex = 0;
|
||||
const totalImages = thumbnails.length;
|
||||
|
||||
// Update gallery display
|
||||
function updateGallery(index) {
|
||||
// Drag/Swipe variables
|
||||
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 >= totalImages) index = 0;
|
||||
|
||||
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 fullUrl = currentThumbnail.dataset.full;
|
||||
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'));
|
||||
currentThumbnail.classList.add('active');
|
||||
|
||||
@@ -357,12 +455,84 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
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) => {
|
||||
thumbnail.addEventListener('click', () => updateGallery(index));
|
||||
});
|
||||
|
||||
// Navigation button handlers
|
||||
// Navigation buttons
|
||||
prevBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
updateGallery(currentIndex - 1);
|
||||
@@ -382,45 +552,22 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
|
||||
// Touch/Swipe support for mobile devices
|
||||
let touchStartX = 0;
|
||||
let touchEndX = 0;
|
||||
const swipeThreshold = 50;
|
||||
// Prevent context menu on long press
|
||||
galleryWrapper.addEventListener('contextmenu', (e) => {
|
||||
if (isDragging) e.preventDefault();
|
||||
});
|
||||
|
||||
galleryMain.addEventListener('touchstart', (e) => {
|
||||
touchStartX = e.changedTouches[0].screenX;
|
||||
}, { passive: true });
|
||||
// Initialize gallery
|
||||
updateGallery(0, false);
|
||||
|
||||
galleryMain.addEventListener('touchend', (e) => {
|
||||
touchEndX = e.changedTouches[0].screenX;
|
||||
handleSwipe();
|
||||
}, { passive: true });
|
||||
|
||||
function handleSwipe() {
|
||||
const swipeDistance = touchStartX - touchEndX;
|
||||
|
||||
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 });
|
||||
// Handle window resize
|
||||
let resizeTimer;
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(() => {
|
||||
updateGallery(currentIndex, false);
|
||||
}, 250);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user