Skip to content

Commit 7f94990

Browse files
Experiment another approach on news carousel
1 parent b4a3dd6 commit 7f94990

File tree

1 file changed

+129
-172
lines changed

1 file changed

+129
-172
lines changed

_includes/news.qmd

Lines changed: 129 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,17 @@ listing:
3030
}
3131
3232
#carousel-container {
33-
width: 100%;
3433
overflow: hidden;
35-
position: relative;
34+
cursor: grab;
3635
}
3736
38-
#carousel-container:focus {
39-
outline: 2px solid #007acc;
40-
outline-offset: 4px;
37+
#carousel-container.grabbing {
38+
cursor: grabbing;
4139
}
4240
4341
#carousel-track {
4442
display: flex;
4543
align-items: stretch;
46-
transition: transform 0.7s cubic-bezier(0.25, 1, 0.5, 1);
47-
will-change: transform;
4844
}
4945
5046
#carousel-track>.g-col-1 {
@@ -55,6 +51,10 @@ listing:
5551
min-width: 0;
5652
}
5753
54+
#carousel-container.grabbing a {
55+
pointer-events: none;
56+
}
57+
5858
@media (max-width: 1024px) and (min-width: 769px) {
5959
#carousel-track>.g-col-1 {
6060
flex: 0 0 50%;
@@ -75,6 +75,7 @@ listing:
7575
overflow: hidden;
7676
display: flex;
7777
flex-direction: column;
78+
height: 100%;
7879
}
7980
8081
#carousel-track>.g-col-1 .card-body {
@@ -123,175 +124,131 @@ listing:
123124
white-space: nowrap;
124125
flex-shrink: 0;
125126
}
126-
127-
/* Carousel Buttons */
128-
.carousel-button {
129-
position: absolute;
130-
top: 50%;
131-
transform: translateY(-50%);
132-
background: transparent;
133-
color: #999;
134-
border: none;
135-
border-radius: 50%;
136-
width: 3rem;
137-
height: 3rem;
138-
margin: 0 0.75rem;
139-
font-size: 1.5rem;
140-
font-weight: bold;
141-
cursor: pointer;
142-
z-index: 10;
143-
opacity: 0;
144-
transition: all 0.2s ease-in-out;
145-
display: flex;
146-
align-items: center;
147-
justify-content: center;
148-
}
149-
150-
#carousel-container:hover .carousel-button,
151-
#carousel-container:focus-within .carousel-button {
152-
opacity: 1;
153-
}
154-
155-
.carousel-button.prev {
156-
left: 0;
157-
}
158-
159-
.carousel-button.next {
160-
right: 0;
161-
}
162-
163-
.carousel-button:hover,
164-
.carousel-button:focus {
165-
background: rgba(0, 0, 0, 0.05);
166-
color: #000;
167-
outline: none;
168-
}
169127
</style>
170128
171129
<script>
172-
// The script block remains the same as the previous version.
173-
document.addEventListener('DOMContentLoaded', function () {
174-
const listing = document.getElementById('listing-news');
175-
if (!listing) return;
176-
177-
const originalItems = Array.from(
178-
listing.querySelectorAll('.list.grid.quarto-listing-cols-3 > .g-col-1')
179-
);
180-
const N_original = originalItems.length;
181-
182-
let itemsPerView = getItemsPerView();
183-
184-
if (N_original <= itemsPerView) {
185-
listing.classList.remove('enhanced-carousel');
186-
return;
187-
}
188-
189-
listing.classList.add('enhanced-carousel');
190-
191-
const carouselContainer = document.createElement('div');
192-
carouselContainer.id = 'carousel-container';
193-
carouselContainer.setAttribute('tabindex', '0');
194-
195-
const carouselTrack = document.createElement('div');
196-
carouselTrack.id = 'carousel-track';
197-
198-
const prevButton = document.createElement('button');
199-
prevButton.className = 'carousel-button prev';
200-
prevButton.setAttribute('aria-label', 'Previous slide');
201-
prevButton.innerHTML = '&#10094;';
202-
203-
const nextButton = document.createElement('button');
204-
nextButton.className = 'carousel-button next';
205-
nextButton.setAttribute('aria-label', 'Next slide');
206-
nextButton.innerHTML = '&#10095;';
207-
208-
carouselContainer.append(carouselTrack, prevButton, nextButton);
209-
listing.parentNode.insertBefore(carouselContainer, listing.nextSibling);
210-
211-
const postClones = [];
212-
for (let i = 0; i < itemsPerView; i++) {
213-
const clone = originalItems[i % N_original].cloneNode(true);
214-
clone.classList.add('clone');
215-
postClones.push(clone);
216-
}
217-
const preClones = [];
218-
for (let i = N_original - itemsPerView; i < N_original; i++) {
219-
const clone = originalItems[i].cloneNode(true);
220-
clone.classList.add('clone');
221-
preClones.push(clone);
222-
}
223-
224-
carouselTrack.append(...preClones, ...originalItems, ...postClones);
225-
226-
const allItems = carouselTrack.querySelectorAll('.g-col-1');
227-
allItems.forEach(item => {
228-
const titleElement = item.querySelector('.listing-title');
229-
if (titleElement) {
230-
titleElement.setAttribute('title', titleElement.textContent.trim());
231-
}
232-
});
233-
234-
let currentIndex = itemsPerView;
235-
let shiftPercent = 100 / itemsPerView;
236-
const transitionDuration = 700;
237-
const displayDuration = 3000;
238-
let intervalId;
239-
240-
function moveTo(index, withAnimation = true) {
241-
carouselTrack.style.transition = withAnimation
242-
? `transform ${transitionDuration / 1000}s cubic-bezier(0.25, 1, 0.5, 1)`
243-
: 'none';
244-
currentIndex = index;
245-
carouselTrack.style.transform = `translateX(-${currentIndex * shiftPercent}%)`;
246-
}
247-
248-
moveTo(currentIndex, false);
249-
250-
function nextSlide() {
251-
moveTo(currentIndex + 1);
252-
}
253-
254-
function prevSlide() {
255-
moveTo(currentIndex - 1);
256-
}
257-
258-
function startAutoplay() {
259-
clearInterval(intervalId);
260-
intervalId = setInterval(nextSlide, displayDuration);
261-
}
262-
263-
function stopAutoplay() {
264-
clearInterval(intervalId);
265-
}
266-
267-
nextButton.addEventListener('click', () => { stopAutoplay(); nextSlide(); });
268-
prevButton.addEventListener('click', () => { stopAutoplay(); prevSlide(); });
269-
270-
carouselTrack.addEventListener('transitionend', () => {
271-
if (currentIndex >= N_original + itemsPerView) {
272-
moveTo(itemsPerView + (currentIndex - N_original - itemsPerView), false);
273-
}
274-
if (currentIndex < itemsPerView) {
275-
moveTo(N_original + (currentIndex), false);
130+
document.addEventListener('DOMContentLoaded', function () {
131+
const listing = document.getElementById('listing-news');
132+
if (!listing) return;
133+
134+
const originalItems = Array.from(listing.querySelectorAll('.list.grid.quarto-listing-cols-3 > .g-col-1'));
135+
if (originalItems.length === 0) return;
136+
137+
listing.classList.add('enhanced-carousel');
138+
139+
const carouselContainer = document.createElement('div');
140+
carouselContainer.id = 'carousel-container';
141+
142+
const carouselTrack = document.createElement('div');
143+
carouselTrack.id = 'carousel-track';
144+
145+
carouselTrack.append(...originalItems);
146+
carouselContainer.append(carouselTrack);
147+
listing.parentNode.insertBefore(carouselContainer, listing.nextSibling);
148+
149+
const slides = Array.from(carouselTrack.children);
150+
const displayDuration = 3500;
151+
let isDragging = false,
152+
startPos = 0,
153+
currentTranslate = 0,
154+
prevTranslate = 0,
155+
currentIndex = 0,
156+
hasDragged = false,
157+
intervalId;
158+
159+
const getPositionX = (event) => event.type.includes('mouse') ? event.pageX : event.touches[0].clientX;
160+
const getItemsPerView = () => {
161+
const width = window.innerWidth;
162+
if (width <= 768) return 1;
163+
if (width > 768 && width <= 1024) return 2;
164+
return 3;
276165
}
166+
const stopAutoplay = () => clearInterval(intervalId);
167+
const startAutoplay = () => {
168+
stopAutoplay();
169+
intervalId = setInterval(autoplayNext, displayDuration);
170+
};
171+
172+
const dragStart = (event) => {
173+
isDragging = true;
174+
hasDragged = false;
175+
startPos = getPositionX(event);
176+
const style = window.getComputedStyle(carouselTrack);
177+
const matrix = new DOMMatrix(style.transform);
178+
prevTranslate = matrix.m41;
179+
carouselContainer.classList.add('grabbing');
180+
carouselTrack.style.transition = 'none';
181+
stopAutoplay();
182+
};
183+
184+
const dragMove = (event) => {
185+
if (!isDragging) return;
186+
hasDragged = true;
187+
const currentPosition = getPositionX(event);
188+
currentTranslate = prevTranslate + currentPosition - startPos;
189+
setSliderPosition();
190+
};
191+
192+
const dragEnd = () => {
193+
if (!isDragging) return;
194+
isDragging = false;
195+
carouselContainer.classList.remove('grabbing');
196+
197+
const slideWidth = slides[0].getBoundingClientRect().width;
198+
const movedBy = currentTranslate - prevTranslate;
199+
200+
if (movedBy < -50 && currentIndex < slides.length - getItemsPerView()) {
201+
currentIndex++;
202+
}
203+
if (movedBy > 50 && currentIndex > 0) {
204+
currentIndex--;
205+
}
206+
207+
setPositionByIndex();
208+
};
209+
210+
const setSliderPosition = () => {
211+
const maxScroll = -(carouselTrack.scrollWidth - carouselContainer.clientWidth);
212+
currentTranslate = Math.max(maxScroll, Math.min(0, currentTranslate));
213+
carouselTrack.style.transform = `translateX(${currentTranslate}px)`;
214+
};
215+
216+
const setPositionByIndex = () => {
217+
const slideWidth = slides[0].getBoundingClientRect().width;
218+
currentTranslate = currentIndex * -slideWidth;
219+
carouselTrack.style.transition = 'transform 0.4s ease-out';
220+
setSliderPosition();
221+
};
222+
223+
const autoplayNext = () => {
224+
const itemsPerView = getItemsPerView();
225+
const maxIndex = slides.length - itemsPerView;
226+
currentIndex = (currentIndex >= maxIndex) ? 0 : currentIndex + 1;
227+
setPositionByIndex();
228+
};
229+
230+
carouselTrack.addEventListener('click', (e) => {
231+
if (hasDragged) {
232+
e.preventDefault();
233+
}
234+
}, true);
235+
236+
carouselContainer.addEventListener('mousedown', dragStart);
237+
carouselContainer.addEventListener('touchstart', dragStart, { passive: true });
238+
239+
window.addEventListener('mousemove', dragMove);
240+
window.addEventListener('touchmove', dragMove, { passive: true });
241+
242+
window.addEventListener('mouseup', dragEnd);
243+
window.addEventListener('touchend', dragEnd);
244+
245+
carouselContainer.addEventListener('mouseenter', stopAutoplay);
246+
carouselContainer.addEventListener('mouseleave', startAutoplay);
247+
248+
document.addEventListener('visibilitychange', () => document.hidden ? stopAutoplay() : startAutoplay());
249+
window.addEventListener('resize', () => window.location.reload());
250+
251+
startAutoplay();
277252
});
278-
279-
['mouseenter', 'focusin'].forEach(e => carouselContainer.addEventListener(e, stopAutoplay));
280-
['mouseleave', 'focusout'].forEach(e => carouselContainer.addEventListener(e, startAutoplay));
281-
document.addEventListener('visibilitychange', () => document.hidden ? stopAutoplay() : startAutoplay());
282-
283-
function getItemsPerView() {
284-
const width = window.innerWidth;
285-
if (width <= 768) return 1;
286-
if (width > 768 && width <= 1024) return 2;
287-
return 3;
288-
}
289-
290-
window.addEventListener('resize', () => {
291-
window.location.reload();
292-
});
293-
294-
startAutoplay();
295-
});
296253
</script>
297254
```

0 commit comments

Comments
 (0)