Add smooth drag-to-slide gallery with 60% threshold and real-time tracking
This commit is contained in:
@@ -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 %}
|
||||||
|
|||||||
Reference in New Issue
Block a user