Carousels: drag + position dots, and real Become-a-member photos
Convert the Hub / Become-a-member carousels from a pure-CSS marquee to a JS-driven scroller with click-and-drag, touch swipe, and progress dots (setupHubCarousel). Turn the "ways to be part of MSOS" cards into hub-style photo cards and add the final Become-a-member photos (WebP). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
a7782eb28f
commit
4c27ebb105
14 files changed
+238
-54
No files matched your search
@@ -931,22 +931,174 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
/**
|
||||
* ===================================================================
|
||||
* HUB USE-CASE CAROUSEL (auto-scrolling)
|
||||
* HUB USE-CASE CAROUSEL (auto-scrolling + drag + position dots)
|
||||
* ===================================================================
|
||||
* Drives every .hub-carousel on the page (Hub "What you can do here"
|
||||
* and the "ways to be part of MSOS" slider). The track is built from
|
||||
* three identical .hub-group copies so the scroll loops seamlessly in
|
||||
* both directions; auto-scroll is JS-driven (not a CSS animation) so it
|
||||
* can co-exist with click-and-drag and touch swiping.
|
||||
*/
|
||||
function setupHubCarousel() {
|
||||
const trackEl = document.querySelector('.hub-track');
|
||||
if (!trackEl) return;
|
||||
const group = trackEl.querySelector('.hub-group');
|
||||
if (!group) return;
|
||||
const carousels = document.querySelectorAll('.hub-carousel');
|
||||
if (!carousels.length) return;
|
||||
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
const LOOP_MS = 55000; // time to travel one group width (matches old 55s marquee)
|
||||
|
||||
// Duplicate the group so translateX(-50%) loops back seamlessly (waterslide flow).
|
||||
// Guard against double-cloning if the script somehow runs twice.
|
||||
if (trackEl.querySelectorAll('.hub-group').length < 2) {
|
||||
const clone = group.cloneNode(true);
|
||||
clone.setAttribute('aria-hidden', 'true');
|
||||
trackEl.appendChild(clone);
|
||||
}
|
||||
carousels.forEach(function (carousel) {
|
||||
if (carousel.dataset.carouselInit) return;
|
||||
carousel.dataset.carouselInit = '1';
|
||||
|
||||
const scroller = carousel.querySelector('.hub-uses');
|
||||
const track = carousel.querySelector('.hub-track');
|
||||
if (!scroller || !track) return;
|
||||
const firstGroup = track.querySelector('.hub-group');
|
||||
if (!firstGroup) return;
|
||||
|
||||
// Need 3 identical groups so a full neighbour is always visible at each wrap point.
|
||||
while (track.querySelectorAll('.hub-group').length < 3) {
|
||||
const clone = firstGroup.cloneNode(true);
|
||||
clone.setAttribute('aria-hidden', 'true');
|
||||
track.appendChild(clone);
|
||||
}
|
||||
|
||||
const cards = firstGroup.children;
|
||||
const cardCount = cards.length;
|
||||
if (!cardCount) return;
|
||||
|
||||
// --- Position dots ---
|
||||
const dotsWrap = document.createElement('div');
|
||||
dotsWrap.className = 'hub-dots';
|
||||
dotsWrap.setAttribute('role', 'tablist');
|
||||
dotsWrap.setAttribute('aria-label', 'Carousel position');
|
||||
const dotEls = [];
|
||||
for (let i = 0; i < cardCount; i++) {
|
||||
const b = document.createElement('button');
|
||||
b.type = 'button';
|
||||
b.className = 'hub-dot';
|
||||
b.setAttribute('aria-label', 'Go to item ' + (i + 1));
|
||||
b.addEventListener('click', (function (idx) {
|
||||
return function () { goTo(idx); };
|
||||
})(i));
|
||||
dotsWrap.appendChild(b);
|
||||
dotEls.push(b);
|
||||
}
|
||||
carousel.appendChild(dotsWrap);
|
||||
|
||||
// --- Measurements (recomputed on resize / image load) ---
|
||||
let groupWidth = 0, step = 0;
|
||||
function measure() {
|
||||
groupWidth = firstGroup.offsetWidth;
|
||||
step = cardCount > 1 ? (cards[1].offsetLeft - cards[0].offsetLeft) : groupWidth;
|
||||
if (!step) step = groupWidth / cardCount;
|
||||
}
|
||||
|
||||
// Keep scroll within the middle copy [groupWidth, 2*groupWidth) so it loops both ways.
|
||||
function wrap() {
|
||||
if (!groupWidth) return;
|
||||
if (scroller.scrollLeft >= 2 * groupWidth) scroller.scrollLeft -= groupWidth;
|
||||
else if (scroller.scrollLeft < groupWidth) scroller.scrollLeft += groupWidth;
|
||||
}
|
||||
|
||||
function updateDots() {
|
||||
if (!step || !groupWidth) return;
|
||||
const within = ((scroller.scrollLeft % groupWidth) + groupWidth) % groupWidth;
|
||||
const active = Math.round(within / step) % cardCount;
|
||||
for (let i = 0; i < cardCount; i++) {
|
||||
const dist = Math.min(Math.abs(i - active), cardCount - Math.abs(i - active));
|
||||
dotEls[i].classList.toggle('is-active', i === active);
|
||||
dotEls[i].classList.toggle('is-near', dist === 1);
|
||||
dotEls[i].classList.toggle('is-far', dist >= 2);
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
measure();
|
||||
if (groupWidth) scroller.scrollLeft = groupWidth;
|
||||
updateDots();
|
||||
}
|
||||
reset();
|
||||
window.addEventListener('load', reset);
|
||||
window.addEventListener('resize', function () { measure(); wrap(); updateDots(); });
|
||||
|
||||
// --- Jump to a specific card when a dot is clicked ---
|
||||
// Custom eased scroll (not native smooth) so it never fights the
|
||||
// rAF auto-scroll loop. Target always sits in the middle copy
|
||||
// [groupWidth, 2*groupWidth), so wrap() stays a no-op during it.
|
||||
let programmatic = 0; // ms remaining where auto-scroll yields
|
||||
let animId = 0;
|
||||
function goTo(idx) {
|
||||
if (!step) measure();
|
||||
const start = scroller.scrollLeft;
|
||||
const target = groupWidth + idx * step;
|
||||
const dur = 450, t0 = performance.now();
|
||||
const ease = function (t) { return 1 - Math.pow(1 - t, 3); };
|
||||
cancelAnimationFrame(animId);
|
||||
programmatic = dur + 80;
|
||||
(function anim(now) {
|
||||
const t = Math.min(1, (now - t0) / dur);
|
||||
scroller.scrollLeft = start + (target - start) * ease(t);
|
||||
updateDots();
|
||||
if (t < 1) animId = requestAnimationFrame(anim);
|
||||
})(t0);
|
||||
}
|
||||
|
||||
// --- Auto-scroll loop ---
|
||||
let paused = false;
|
||||
let dragging = false;
|
||||
let last = null;
|
||||
function tick(ts) {
|
||||
if (last == null) last = ts;
|
||||
const dt = ts - last;
|
||||
last = ts;
|
||||
if (!reduceMotion && !paused && programmatic <= 0 && groupWidth) {
|
||||
scroller.scrollLeft += (groupWidth / LOOP_MS) * dt;
|
||||
}
|
||||
if (programmatic > 0) programmatic -= dt;
|
||||
wrap();
|
||||
updateDots();
|
||||
requestAnimationFrame(tick);
|
||||
}
|
||||
requestAnimationFrame(tick);
|
||||
|
||||
// Pause on hover (desktop).
|
||||
carousel.addEventListener('mouseenter', function () { paused = true; });
|
||||
carousel.addEventListener('mouseleave', function () { if (!dragging) paused = false; });
|
||||
|
||||
// --- Click-and-drag (mouse) + pause during touch (native scroll handles the swipe) ---
|
||||
let lastX = 0;
|
||||
scroller.addEventListener('pointerdown', function (e) {
|
||||
paused = true;
|
||||
if (e.pointerType !== 'mouse') return; // touch/pen: let the browser scroll natively
|
||||
dragging = true;
|
||||
lastX = e.clientX;
|
||||
scroller.classList.add('is-dragging');
|
||||
try { scroller.setPointerCapture(e.pointerId); } catch (err) {}
|
||||
});
|
||||
scroller.addEventListener('pointermove', function (e) {
|
||||
if (!dragging) return;
|
||||
const dx = e.clientX - lastX;
|
||||
lastX = e.clientX;
|
||||
scroller.scrollLeft -= dx;
|
||||
wrap();
|
||||
updateDots();
|
||||
e.preventDefault();
|
||||
});
|
||||
function endDrag(e) {
|
||||
if (dragging) {
|
||||
dragging = false;
|
||||
scroller.classList.remove('is-dragging');
|
||||
try { scroller.releasePointerCapture(e.pointerId); } catch (err) {}
|
||||
}
|
||||
// Resume auto-scroll unless the cursor is still hovering the carousel.
|
||||
if (!carousel.matches(':hover')) paused = false;
|
||||
}
|
||||
scroller.addEventListener('pointerup', endDrag);
|
||||
scroller.addEventListener('pointercancel', endDrag);
|
||||
|
||||
// Prevent the browser's native image drag-ghost while dragging.
|
||||
track.addEventListener('dragstart', function (e) { e.preventDefault(); });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user