3
3
- id : news
4
4
contents :
5
5
- " news/posts/*/index.qmd"
6
+ max-items : 10
6
7
sort : date desc
7
8
type : grid
8
9
grid-columns : 3
@@ -29,39 +30,43 @@ listing:
29
30
display: none !important;
30
31
}
31
32
32
- /* carousel wrapper */
33
33
#carousel-container {
34
34
width: 100%;
35
35
overflow: hidden;
36
36
position: relative;
37
37
}
38
- /* focus outline for accessibility */
39
38
#carousel-container:focus {
40
39
outline: 2px solid #007acc;
41
40
outline-offset: 4px;
42
41
}
43
42
44
- /* sliding track */
45
43
#carousel-track {
46
44
display: flex;
47
45
align-items: flex-start;
48
- transition: transform 0.5s ease ;
46
+ transition: transform 0.7s cubic-bezier(0.25, 1, 0.5, 1) ;
49
47
will-change: transform;
50
48
}
51
49
52
50
/* each slide sizing & height animation */
53
51
#carousel-track > .g-col-1 {
54
- flex: 0 0 33.3333%;
52
+ flex: 0 0 33.3333%; /* Default for desktop (3 columns) */
55
53
padding: 1rem;
56
54
box-sizing: border-box;
57
55
display: block !important;
58
56
transition: height 0.3s ease;
59
57
}
60
58
61
- /* single‑column on mobile */
59
+ /* Tablet/iPad size: 2 columns */
60
+ @media (max-width: 1024px) and (min-width: 769px) {
61
+ #carousel-track > .g-col-1 {
62
+ flex: 0 0 50%; /* 2 columns */
63
+ }
64
+ }
65
+
66
+ /* Single-column on smaller mobile */
62
67
@media (max-width: 768px) {
63
68
#carousel-track > .g-col-1 {
64
- flex: 0 0 100%;
69
+ flex: 0 0 100%; /* 1 column */
65
70
}
66
71
}
67
72
@@ -79,73 +84,124 @@ listing:
79
84
</style>
80
85
81
86
<script>
82
- // initialize carousel after DOM is ready
83
87
document.addEventListener('DOMContentLoaded', function () {
84
88
const listing = document.getElementById('listing-news');
85
89
if (!listing) return;
86
- listing.classList.add('enhanced-carousel'); // flag JS enhancement
87
90
88
- const items = Array.from(
91
+ const originalItems = Array.from(
89
92
listing.querySelectorAll('.list.grid.quarto-listing-cols-3 > .g-col-1')
90
- ); // collect slides
91
- const N = items.length; // total number of slides
92
- if (!N) return;
93
-
94
- // create carousel wrapper with accessibility roles
95
- const carouselContainer = document.createElement('div');
96
- carouselContainer.id = 'carousel-container';
97
- carouselContainer.setAttribute('role', 'region');
98
- carouselContainer.setAttribute('aria-live', 'polite');
99
- carouselContainer.setAttribute('tabindex', '0');
100
-
101
- // create track element
102
- const carouselTrack = document.createElement('div');
103
- carouselTrack.id = 'carousel-track';
104
-
105
- items.forEach(i => carouselTrack.appendChild(i)); // move slides into track
106
- carouselContainer.appendChild(carouselTrack);
107
- listing.parentNode.insertBefore(carouselContainer, listing.nextSibling); // insert carousel
108
-
109
- // determine items per view (responsive)
110
- function getItemsPerView() { return window.innerWidth < 768 ? 1 : 3; }
111
- let itemsPerView = getItemsPerView();
112
- if (N <= itemsPerView) { // handle few slides
113
- const h = Math.max(...items.map(i => i.offsetHeight));
114
- carouselContainer.style.height = h + 'px';
93
+ );
94
+ const N_original = originalItems.length;
95
+
96
+ // Helper to get items per view (cached on first call, recalculated on resize)
97
+ function getItemsPerView() {
98
+ const width = window.innerWidth;
99
+ if (width <= 768) { // Mobile
100
+ return 1;
101
+ } else if (width > 768 && width <= 1024) { // Tablet/iPad
102
+ return 2;
103
+ } else { // Desktop
104
+ return 3;
105
+ }
106
+ }
107
+
108
+ // If there are too few items to scroll, just display them statically.
109
+ // This check now uses the initial itemsPerView.
110
+ if (N_original <= getItemsPerView()) {
111
+ listing.classList.remove('enhanced-carousel');
115
112
return;
116
113
}
117
114
115
+ // Add enhanced-carousel class only if the carousel is actually being initialized
116
+ listing.classList.add('enhanced-carousel');
117
+
118
+ let carouselContainer = document.getElementById('carousel-container');
119
+ let carouselTrack = document.getElementById('carousel-track');
120
+
121
+ // Initialize carousel elements if they don't exist (first load or after a full re-init on resize)
122
+ if (!carouselContainer) {
123
+ carouselContainer = document.createElement('div');
124
+ carouselContainer.id = 'carousel-container';
125
+ carouselContainer.setAttribute('role', 'region');
126
+ carouselContainer.setAttribute('aria-live', 'polite');
127
+ carouselContainer.setAttribute('tabindex', '0');
128
+
129
+ carouselTrack = document.createElement('div');
130
+ carouselTrack.id = 'carousel-track';
131
+ carouselContainer.appendChild(carouselTrack);
132
+ listing.parentNode.insertBefore(carouselContainer, listing.nextSibling);
133
+ } else {
134
+ // Clear existing children from track if re-initializing on resize
135
+ while(carouselTrack.firstChild) {
136
+ carouselTrack.removeChild(carouselTrack.firstChild);
137
+ }
138
+ }
139
+
140
+ let itemsPerView = getItemsPerView(); // Initial calculation
141
+ const numClones = Math.max(itemsPerView, 1);
142
+
143
+ const clonedItems = [];
144
+ for (let i = 0; i < numClones; i++) {
145
+ const clone = originalItems[i % N_original].cloneNode(true);
146
+ clone.setAttribute('aria-hidden', 'true');
147
+ clonedItems.push(clone);
148
+ }
149
+
150
+ originalItems.forEach(i => {
151
+ carouselTrack.appendChild(i);
152
+ i.setAttribute('aria-hidden', 'false');
153
+ });
154
+ clonedItems.forEach(i => carouselTrack.appendChild(i));
155
+
156
+ const allItems = [...originalItems, ...clonedItems];
157
+
118
158
let currentIndex = 0;
119
- let maxIndex = N - itemsPerView;
120
159
let shiftPercent = 100 / itemsPerView;
121
- const displayDuration = 2000; // slide interval
160
+ const displayDuration = 2000;
161
+ const transitionDuration = 700;
122
162
123
- // normalize visible slide heights
124
163
function recalcHeight() {
125
- items.forEach(i => i.style.height = 'auto');
126
- const vis = items.slice(currentIndex, currentIndex + itemsPerView);
127
- const h = Math.max(...vis.map(i => i.offsetHeight));
164
+ for (let i = currentIndex; i < Math.min(currentIndex + itemsPerView, allItems.length); i++) {
165
+ allItems[i].style.height = 'auto';
166
+ }
167
+
168
+ const vis = allItems.slice(currentIndex, currentIndex + itemsPerView);
169
+ const h = vis.length > 0 ? Math.max(...vis.map(i => i.offsetHeight)) : 0;
128
170
vis.forEach(i => i.style.height = h + 'px');
129
171
carouselContainer.style.height = h + 'px';
130
172
}
131
173
132
- // move track and adjust heights
133
- function updateSlide(idx) {
174
+ function updateSlide(idx, instant = false) {
175
+ if (instant) {
176
+ carouselTrack.style.transition = 'none';
177
+ } else {
178
+ carouselTrack.style.transition = `transform ${transitionDuration / 1000}s cubic-bezier(0.25, 1, 0.5, 1)`;
179
+ }
180
+
134
181
carouselTrack.style.transform = `translateX(-${idx * shiftPercent}%)`;
135
182
recalcHeight();
183
+
184
+ allItems.forEach((item, i) => {
185
+ if (i >= currentIndex && i < currentIndex + itemsPerView) {
186
+ item.setAttribute('aria-hidden', 'false');
187
+ } else {
188
+ item.setAttribute('aria-hidden', 'true');
189
+ }
190
+ });
191
+
192
+ if (!instant && idx >= N_original) {
193
+ setTimeout(() => {
194
+ currentIndex = 0;
195
+ updateSlide(currentIndex, true);
196
+ }, transitionDuration);
197
+ }
136
198
}
137
199
138
- // slide controls
139
200
function nextSlide() {
140
- currentIndex = currentIndex < maxIndex ? currentIndex + 1 : 0;
141
- updateSlide(currentIndex);
142
- }
143
- function prevSlide() {
144
- currentIndex = currentIndex > 0 ? currentIndex - 1 : maxIndex;
201
+ currentIndex++;
145
202
updateSlide(currentIndex);
146
203
}
147
204
148
- // initial render
149
205
recalcHeight();
150
206
updateSlide(0);
151
207
@@ -168,26 +224,24 @@ listing:
168
224
}
169
225
});
170
226
171
- // keyboard navigation
172
- carouselContainer.addEventListener('keydown', e => {
173
- if (e.key === 'ArrowRight') { nextSlide(); e.preventDefault(); }
174
- if (e.key === 'ArrowLeft') { prevSlide(); e.preventDefault(); }
175
- });
176
-
177
- // debounce on window resize
178
227
let resizeTimeout = null;
179
228
window.addEventListener('resize', () => {
180
229
clearTimeout(resizeTimeout);
181
230
resizeTimeout = setTimeout(() => {
182
- const v = getItemsPerView();
183
- if (v !== itemsPerView) {
184
- itemsPerView = v;
185
- maxIndex = N - itemsPerView;
186
- shiftPercent = 100 / itemsPerView;
187
- currentIndex = Math.min(currentIndex, maxIndex);
231
+ const newItemsPerView = getItemsPerView();
232
+
233
+ if (newItemsPerView !== itemsPerView || N_original <= newItemsPerView) {
234
+ clearInterval(intervalId);
235
+ carouselContainer.remove();
236
+ document.dispatchEvent(new Event('DOMContentLoaded'));
237
+ return;
188
238
}
239
+
240
+ itemsPerView = newItemsPerView;
241
+ shiftPercent = 100 / itemsPerView;
242
+ currentIndex = Math.min(currentIndex, N_original - 1);
189
243
recalcHeight();
190
- updateSlide(currentIndex);
244
+ updateSlide(currentIndex, true );
191
245
}, 150);
192
246
});
193
247
});
0 commit comments