33- id : news
44 contents :
55 - " news/posts/*/index.qmd"
6+ max-items : 10
67 sort : date desc
78 type : grid
89 grid-columns : 3
@@ -29,39 +30,43 @@ listing:
2930 display: none !important;
3031 }
3132
32- /* carousel wrapper */
3333 #carousel-container {
3434 width: 100%;
3535 overflow: hidden;
3636 position: relative;
3737 }
38- /* focus outline for accessibility */
3938 #carousel-container:focus {
4039 outline: 2px solid #007acc;
4140 outline-offset: 4px;
4241 }
4342
44- /* sliding track */
4543 #carousel-track {
4644 display: flex;
4745 align-items: flex-start;
48- transition: transform 0.5s ease ;
46+ transition: transform 0.7s cubic-bezier(0.25, 1, 0.5, 1) ;
4947 will-change: transform;
5048 }
5149
5250 /* each slide sizing & height animation */
5351 #carousel-track > .g-col-1 {
54- flex: 0 0 33.3333%;
52+ flex: 0 0 33.3333%; /* Default for desktop (3 columns) */
5553 padding: 1rem;
5654 box-sizing: border-box;
5755 display: block !important;
5856 transition: height 0.3s ease;
5957 }
6058
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 */
6267 @media (max-width: 768px) {
6368 #carousel-track > .g-col-1 {
64- flex: 0 0 100%;
69+ flex: 0 0 100%; /* 1 column */
6570 }
6671 }
6772
@@ -79,73 +84,124 @@ listing:
7984</style>
8085
8186<script>
82- // initialize carousel after DOM is ready
8387 document.addEventListener('DOMContentLoaded', function () {
8488 const listing = document.getElementById('listing-news');
8589 if (!listing) return;
86- listing.classList.add('enhanced-carousel'); // flag JS enhancement
8790
88- const items = Array.from(
91+ const originalItems = Array.from(
8992 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');
115112 return;
116113 }
117114
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+
118158 let currentIndex = 0;
119- let maxIndex = N - itemsPerView;
120159 let shiftPercent = 100 / itemsPerView;
121- const displayDuration = 2000; // slide interval
160+ const displayDuration = 2000;
161+ const transitionDuration = 700;
122162
123- // normalize visible slide heights
124163 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;
128170 vis.forEach(i => i.style.height = h + 'px');
129171 carouselContainer.style.height = h + 'px';
130172 }
131173
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+
134181 carouselTrack.style.transform = `translateX(-${idx * shiftPercent}%)`;
135182 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+ }
136198 }
137199
138- // slide controls
139200 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++;
145202 updateSlide(currentIndex);
146203 }
147204
148- // initial render
149205 recalcHeight();
150206 updateSlide(0);
151207
@@ -168,26 +224,24 @@ listing:
168224 }
169225 });
170226
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
178227 let resizeTimeout = null;
179228 window.addEventListener('resize', () => {
180229 clearTimeout(resizeTimeout);
181230 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;
188238 }
239+
240+ itemsPerView = newItemsPerView;
241+ shiftPercent = 100 / itemsPerView;
242+ currentIndex = Math.min(currentIndex, N_original - 1);
189243 recalcHeight();
190- updateSlide(currentIndex);
244+ updateSlide(currentIndex, true );
191245 }, 150);
192246 });
193247 });
0 commit comments