diff --git a/website/static/website/css/carousel_fade.css b/website/static/website/css/carousel_fade.css index 47cbb9c2..d02708fb 100644 --- a/website/static/website/css/carousel_fade.css +++ b/website/static/website/css/carousel_fade.css @@ -1,53 +1,41 @@ /* -Hack to make the carousel transition using fade instead of slide -inspired from http://codepen.io/Rowno/pen/Afykb -*/ - -.carousel-fade .carousel-inner .item { - opacity: 0; - transition-property: opacity; - /* transition-duration: 2s; */ -} - -.carousel-fade .carousel-inner .active { - opacity: 1; -} - -.carousel-fade .carousel-inner .active.left,.carousel-fade .carousel-inner .active.right { + * Crossfade carousel transition. + * + * Slides are stacked and crossfaded purely with opacity, driven by the + * `.active` class that carousel.js toggles. The carousel has a fixed height + * (see `.carousel` / `.shortCarousel` in base.css), so absolutely positioning + * the slides does not collapse the container. + * + * Previously this file relied on Bootstrap 3's slide-transition classes + * (.next/.prev/.left/.right); that machinery went away with the Bootstrap + * carousel JS (Track A — issues #1288 / #1253), so the fade is now self- + * contained CSS rather than a hack layered on Bootstrap's animation. + */ + +.carousel-fade .carousel-inner > .item { + position: absolute; + top: 0; left: 0; + width: 100%; + /* Override Bootstrap's display:none on inactive items so opacity can animate. */ + display: block; opacity: 0; - z-index: 1; + z-index: 0; + /* Only the visible slide should capture clicks. */ + pointer-events: none; + transition: opacity 0.6s ease-in-out; } -.carousel-fade .carousel-inner .next.left,.carousel-fade .carousel-inner .prev.right { +.carousel-fade .carousel-inner > .item.active { opacity: 1; + z-index: 1; + pointer-events: auto; } -.carousel-fade .carousel-control { - z-index: 2; -} - -/* -WHAT IS NEW IN 3.3: "Added transforms to improve carousel performance in modern browsers." -now override the 3.3 new styles for modern browsers & apply opacity -*/ - -@media all and (transform-3d), (-webkit-transform-3d) { - .carousel-fade .carousel-inner > .item.next, .carousel-fade .carousel-inner > .item.active.right { - opacity: 0; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); +/* Respect users who prefer reduced motion: switch instantly, no crossfade. + (carousel.js also disables autoplay under this preference.) */ +@media (prefers-reduced-motion: reduce) { + .carousel-fade .carousel-inner > .item { + transition: none; } - - .carousel-fade .carousel-inner > .item.prev, .carousel-fade .carousel-inner > .item.active.left { - opacity: 0; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - - .carousel-fade .carousel-inner > .item.next.left, .carousel-fade .carousel-inner > .item.prev.right, .carousel-fade .carousel-inner > .item.active { - opacity: 1; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} \ No newline at end of file +} diff --git a/website/static/website/js/carousel.js b/website/static/website/js/carousel.js new file mode 100644 index 00000000..c3ab29fb --- /dev/null +++ b/website/static/website/js/carousel.js @@ -0,0 +1,182 @@ +/*! + * Carousel — vanilla JS, no jQuery / Bootstrap JS. + * + * Auto-rotating crossfade banner with clickable indicator dots, replacing + * Bootstrap 3's carousel plugin (Track A, see issues #1288 / #1253). It works + * with the existing markup: a `.carousel` containing `.carousel-inner > .item` + * slides (one marked `.active`) and an optional `.carousel-indicators > li` + * dot list. The crossfade itself is pure CSS (carousel_fade.css); this script + * only moves the `.active` class and runs the autoplay timer. + * + * Per-carousel options (data attributes on the `.carousel` element): + * - data-interval: autoplay delay in ms (default 5000; <= 0 disables autoplay). + * - data-pause="false": do NOT pause on mouse hover (default is to pause). + * + * Accessibility: + * - Honors prefers-reduced-motion: no autoplay and (via CSS) no crossfade. + * - Pauses while focus is inside the carousel (keyboard users reading links) + * and while the browser tab is hidden. + * - Marks the active indicator dot with aria-current. + * + * Note: there are no prev/next arrows or swipe gestures — this matches the + * previous Bootstrap setup, which had neither. + */ +(function () { + 'use strict'; + + var prefersReducedMotion = window.matchMedia + ? window.matchMedia('(prefers-reduced-motion: reduce)').matches + : false; + + function setupCarousel(root) { + var inner = root.querySelector('.carousel-inner'); + if (!inner) { + return; + } + + var slides = Array.prototype.slice.call(inner.querySelectorAll(':scope > .item')); + if (slides.length === 0) { + return; + } + + var indicators = Array.prototype.slice.call( + root.querySelectorAll('.carousel-indicators > li') + ); + + // Start from whichever slide the template marked active (default first). + var current = slides.findIndex(function (slide) { + return slide.classList.contains('active'); + }); + if (current < 0) { + current = 0; + } + + /** + * Shows the slide at `index`, updates the indicator dots, and plays only + * the active slide's video (if any) to avoid decoding hidden videos. + */ + function render(index) { + slides.forEach(function (slide, i) { + var isActive = i === index; + slide.classList.toggle('active', isActive); + + var video = slide.querySelector('video'); + if (video) { + if (isActive) { + var playback = video.play(); + if (playback && playback.catch) { + playback.catch(function () { /* autoplay blocked — ignore */ }); + } + } else { + video.pause(); + } + } + }); + + indicators.forEach(function (dot, i) { + var isActive = i === index; + dot.classList.toggle('active', isActive); + if (isActive) { + dot.setAttribute('aria-current', 'true'); + } else { + dot.removeAttribute('aria-current'); + } + }); + + current = index; + } + + render(current); + + // A single slide has nothing to rotate or navigate. + if (slides.length < 2) { + return; + } + + /* ----------------------------- Autoplay ----------------------------- */ + + var intervalAttr = root.getAttribute('data-interval'); + var interval = intervalAttr == null ? 5000 : parseInt(intervalAttr, 10); + var pauseOnHover = root.getAttribute('data-pause') !== 'false'; + + var timer = null; + var paused = false; + + function showNext() { + render((current + 1) % slides.length); + } + + function canPlay() { + return !prefersReducedMotion && interval > 0 && !paused && !document.hidden; + } + + function start() { + stop(); + if (canPlay()) { + timer = window.setInterval(showNext, interval); + } + } + + function stop() { + if (timer !== null) { + window.clearInterval(timer); + timer = null; + } + } + + function restart() { + // After a manual jump, give the next auto-advance a full interval. + stop(); + start(); + } + + /* ---------------------------- Indicators ---------------------------- */ + + indicators.forEach(function (dot, i) { + dot.addEventListener('click', function () { + render(i); + restart(); + }); + }); + + /* -------------------------- Pause conditions ------------------------ */ + + if (pauseOnHover) { + root.addEventListener('mouseenter', function () { + paused = true; + stop(); + }); + root.addEventListener('mouseleave', function () { + paused = false; + start(); + }); + } + + // Pause while focus is inside the carousel (keyboard users on slide links). + root.addEventListener('focusin', function () { + paused = true; + stop(); + }); + root.addEventListener('focusout', function (event) { + if (!root.contains(event.relatedTarget)) { + paused = false; + start(); + } + }); + + // Pause when the tab is hidden; resume when it becomes visible again. + document.addEventListener('visibilitychange', function () { + if (document.hidden) { + stop(); + } else { + start(); + } + }); + + start(); + } + + document.addEventListener('DOMContentLoaded', function () { + document.querySelectorAll('.carousel').forEach(setupCarousel); + }); +})(); diff --git a/website/templates/snippets/display_short_carousel_snippet.html b/website/templates/snippets/display_short_carousel_snippet.html index c30bc6d3..42ebb1bf 100644 --- a/website/templates/snippets/display_short_carousel_snippet.html +++ b/website/templates/snippets/display_short_carousel_snippet.html @@ -7,7 +7,7 @@ {% load cropping %} {% load ml_tags %} -