@@ -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 = '❮';
202-
203- const nextButton = document.createElement('button');
204- nextButton.className = 'carousel-button next';
205- nextButton.setAttribute('aria-label', 'Next slide');
206- nextButton.innerHTML = '❯';
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