@@ -30,21 +30,17 @@ listing:
30
30
}
31
31
32
32
#carousel-container {
33
- width: 100%;
34
33
overflow: hidden;
35
- position: relative ;
34
+ cursor: grab ;
36
35
}
37
36
38
- #carousel-container:focus {
39
- outline: 2px solid #007acc;
40
- outline-offset: 4px;
37
+ #carousel-container.grabbing {
38
+ cursor: grabbing;
41
39
}
42
40
43
41
#carousel-track {
44
42
display: flex;
45
43
align-items: stretch;
46
- transition: transform 0.7s cubic-bezier(0.25, 1, 0.5, 1);
47
- will-change: transform;
48
44
}
49
45
50
46
#carousel-track>.g-col-1 {
@@ -55,6 +51,10 @@ listing:
55
51
min-width: 0;
56
52
}
57
53
54
+ #carousel-container.grabbing a {
55
+ pointer-events: none;
56
+ }
57
+
58
58
@media (max-width: 1024px) and (min-width: 769px) {
59
59
#carousel-track>.g-col-1 {
60
60
flex: 0 0 50%;
@@ -75,6 +75,7 @@ listing:
75
75
overflow: hidden;
76
76
display: flex;
77
77
flex-direction: column;
78
+ height: 100%;
78
79
}
79
80
80
81
#carousel-track>.g-col-1 .card-body {
@@ -123,175 +124,131 @@ listing:
123
124
white-space: nowrap;
124
125
flex-shrink: 0;
125
126
}
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
- }
169
127
</style>
170
128
171
129
<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;
276
165
}
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();
277
252
});
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
- });
296
253
</script>
297
254
```
0 commit comments