Skip to content

Commit 4933d64

Browse files
Improve news animation
1 parent c7eacc8 commit 4933d64

File tree

1 file changed

+118
-64
lines changed

1 file changed

+118
-64
lines changed

_includes/news.qmd

Lines changed: 118 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ listing:
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

Comments
 (0)