Skip to content

Commit f7a7ee8

Browse files
jonfroehlichclaude
andcommitted
Replace Bootstrap carousel JS with a vanilla crossfade carousel (#1288)
Track A (#1253). Drops the Bootstrap 3 carousel plugin (and the inline jQuery `$('.carousel').carousel()` init) in favor of a small self-contained vanilla module. - website/static/website/js/carousel.js: auto-rotating crossfade with clickable indicator dots. Reads data-interval / data-pause from the markup. Honors prefers-reduced-motion (no autoplay), pauses on hover (unless data-pause="false"), on focus-within (keyboard users), and when the tab is hidden; marks the active dot with aria-current; pauses hidden slides' videos. - carousel_fade.css: the crossfade is now self-contained (stacked, opacity-only) instead of layered on Bootstrap's .next/.prev/.left/.right slide classes. Safe because the carousel has a fixed height (.carousel/.shortCarousel). - base.html / display_short_carousel_snippet.html: removed `data-ride` and the indicators' `data-slide-to`/`data-target` so Bootstrap's still-loaded carousel data-api can't hijack these carousels; load carousel.js; drop the inline init. Notes uncovered while doing this: `data-pause="true"/"false"` never did anything under Bootstrap (it only pauses on the literal value "hover"), and the short carousel's indicators pointed at `#mainCarousel` (the id is `main-carousel`) so those dots were dead. The position-based vanilla indicators fix both. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b17cd17 commit f7a7ee8

4 files changed

Lines changed: 220 additions & 57 deletions

File tree

Lines changed: 32 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,41 @@
11
/*
2-
Hack to make the carousel transition using fade instead of slide
3-
inspired from http://codepen.io/Rowno/pen/Afykb
4-
*/
5-
6-
.carousel-fade .carousel-inner .item {
7-
opacity: 0;
8-
transition-property: opacity;
9-
/* transition-duration: 2s; */
10-
}
11-
12-
.carousel-fade .carousel-inner .active {
13-
opacity: 1;
14-
}
15-
16-
.carousel-fade .carousel-inner .active.left,.carousel-fade .carousel-inner .active.right {
2+
* Crossfade carousel transition.
3+
*
4+
* Slides are stacked and crossfaded purely with opacity, driven by the
5+
* `.active` class that carousel.js toggles. The carousel has a fixed height
6+
* (see `.carousel` / `.shortCarousel` in base.css), so absolutely positioning
7+
* the slides does not collapse the container.
8+
*
9+
* Previously this file relied on Bootstrap 3's slide-transition classes
10+
* (.next/.prev/.left/.right); that machinery went away with the Bootstrap
11+
* carousel JS (Track A — issues #1288 / #1253), so the fade is now self-
12+
* contained CSS rather than a hack layered on Bootstrap's animation.
13+
*/
14+
15+
.carousel-fade .carousel-inner > .item {
16+
position: absolute;
17+
top: 0;
1718
left: 0;
19+
width: 100%;
20+
/* Override Bootstrap's display:none on inactive items so opacity can animate. */
21+
display: block;
1822
opacity: 0;
19-
z-index: 1;
23+
z-index: 0;
24+
/* Only the visible slide should capture clicks. */
25+
pointer-events: none;
26+
transition: opacity 0.6s ease-in-out;
2027
}
2128

22-
.carousel-fade .carousel-inner .next.left,.carousel-fade .carousel-inner .prev.right {
29+
.carousel-fade .carousel-inner > .item.active {
2330
opacity: 1;
31+
z-index: 1;
32+
pointer-events: auto;
2433
}
2534

26-
.carousel-fade .carousel-control {
27-
z-index: 2;
28-
}
29-
30-
/*
31-
WHAT IS NEW IN 3.3: "Added transforms to improve carousel performance in modern browsers."
32-
now override the 3.3 new styles for modern browsers & apply opacity
33-
*/
34-
35-
@media all and (transform-3d), (-webkit-transform-3d) {
36-
.carousel-fade .carousel-inner > .item.next, .carousel-fade .carousel-inner > .item.active.right {
37-
opacity: 0;
38-
-webkit-transform: translate3d(0, 0, 0);
39-
transform: translate3d(0, 0, 0);
35+
/* Respect users who prefer reduced motion: switch instantly, no crossfade.
36+
(carousel.js also disables autoplay under this preference.) */
37+
@media (prefers-reduced-motion: reduce) {
38+
.carousel-fade .carousel-inner > .item {
39+
transition: none;
4040
}
41-
42-
.carousel-fade .carousel-inner > .item.prev, .carousel-fade .carousel-inner > .item.active.left {
43-
opacity: 0;
44-
-webkit-transform: translate3d(0, 0, 0);
45-
transform: translate3d(0, 0, 0);
46-
}
47-
48-
.carousel-fade .carousel-inner > .item.next.left, .carousel-fade .carousel-inner > .item.prev.right, .carousel-fade .carousel-inner > .item.active {
49-
opacity: 1;
50-
-webkit-transform: translate3d(0, 0, 0);
51-
transform: translate3d(0, 0, 0);
52-
}
53-
}
41+
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/*!
2+
* Carousel — vanilla JS, no jQuery / Bootstrap JS.
3+
*
4+
* Auto-rotating crossfade banner with clickable indicator dots, replacing
5+
* Bootstrap 3's carousel plugin (Track A, see issues #1288 / #1253). It works
6+
* with the existing markup: a `.carousel` containing `.carousel-inner > .item`
7+
* slides (one marked `.active`) and an optional `.carousel-indicators > li`
8+
* dot list. The crossfade itself is pure CSS (carousel_fade.css); this script
9+
* only moves the `.active` class and runs the autoplay timer.
10+
*
11+
* Per-carousel options (data attributes on the `.carousel` element):
12+
* - data-interval: autoplay delay in ms (default 5000; <= 0 disables autoplay).
13+
* - data-pause="false": do NOT pause on mouse hover (default is to pause).
14+
*
15+
* Accessibility:
16+
* - Honors prefers-reduced-motion: no autoplay and (via CSS) no crossfade.
17+
* - Pauses while focus is inside the carousel (keyboard users reading links)
18+
* and while the browser tab is hidden.
19+
* - Marks the active indicator dot with aria-current.
20+
*
21+
* Note: there are no prev/next arrows or swipe gestures — this matches the
22+
* previous Bootstrap setup, which had neither.
23+
*/
24+
(function () {
25+
'use strict';
26+
27+
var prefersReducedMotion = window.matchMedia
28+
? window.matchMedia('(prefers-reduced-motion: reduce)').matches
29+
: false;
30+
31+
function setupCarousel(root) {
32+
var inner = root.querySelector('.carousel-inner');
33+
if (!inner) {
34+
return;
35+
}
36+
37+
var slides = Array.prototype.slice.call(inner.querySelectorAll(':scope > .item'));
38+
if (slides.length === 0) {
39+
return;
40+
}
41+
42+
var indicators = Array.prototype.slice.call(
43+
root.querySelectorAll('.carousel-indicators > li')
44+
);
45+
46+
// Start from whichever slide the template marked active (default first).
47+
var current = slides.findIndex(function (slide) {
48+
return slide.classList.contains('active');
49+
});
50+
if (current < 0) {
51+
current = 0;
52+
}
53+
54+
/**
55+
* Shows the slide at `index`, updates the indicator dots, and plays only
56+
* the active slide's video (if any) to avoid decoding hidden videos.
57+
*/
58+
function render(index) {
59+
slides.forEach(function (slide, i) {
60+
var isActive = i === index;
61+
slide.classList.toggle('active', isActive);
62+
63+
var video = slide.querySelector('video');
64+
if (video) {
65+
if (isActive) {
66+
var playback = video.play();
67+
if (playback && playback.catch) {
68+
playback.catch(function () { /* autoplay blocked — ignore */ });
69+
}
70+
} else {
71+
video.pause();
72+
}
73+
}
74+
});
75+
76+
indicators.forEach(function (dot, i) {
77+
var isActive = i === index;
78+
dot.classList.toggle('active', isActive);
79+
if (isActive) {
80+
dot.setAttribute('aria-current', 'true');
81+
} else {
82+
dot.removeAttribute('aria-current');
83+
}
84+
});
85+
86+
current = index;
87+
}
88+
89+
render(current);
90+
91+
// A single slide has nothing to rotate or navigate.
92+
if (slides.length < 2) {
93+
return;
94+
}
95+
96+
/* ----------------------------- Autoplay ----------------------------- */
97+
98+
var intervalAttr = root.getAttribute('data-interval');
99+
var interval = intervalAttr == null ? 5000 : parseInt(intervalAttr, 10);
100+
var pauseOnHover = root.getAttribute('data-pause') !== 'false';
101+
102+
var timer = null;
103+
var paused = false;
104+
105+
function showNext() {
106+
render((current + 1) % slides.length);
107+
}
108+
109+
function canPlay() {
110+
return !prefersReducedMotion && interval > 0 && !paused && !document.hidden;
111+
}
112+
113+
function start() {
114+
stop();
115+
if (canPlay()) {
116+
timer = window.setInterval(showNext, interval);
117+
}
118+
}
119+
120+
function stop() {
121+
if (timer !== null) {
122+
window.clearInterval(timer);
123+
timer = null;
124+
}
125+
}
126+
127+
function restart() {
128+
// After a manual jump, give the next auto-advance a full interval.
129+
stop();
130+
start();
131+
}
132+
133+
/* ---------------------------- Indicators ---------------------------- */
134+
135+
indicators.forEach(function (dot, i) {
136+
dot.addEventListener('click', function () {
137+
render(i);
138+
restart();
139+
});
140+
});
141+
142+
/* -------------------------- Pause conditions ------------------------ */
143+
144+
if (pauseOnHover) {
145+
root.addEventListener('mouseenter', function () {
146+
paused = true;
147+
stop();
148+
});
149+
root.addEventListener('mouseleave', function () {
150+
paused = false;
151+
start();
152+
});
153+
}
154+
155+
// Pause while focus is inside the carousel (keyboard users on slide links).
156+
root.addEventListener('focusin', function () {
157+
paused = true;
158+
stop();
159+
});
160+
root.addEventListener('focusout', function (event) {
161+
if (!root.contains(event.relatedTarget)) {
162+
paused = false;
163+
start();
164+
}
165+
});
166+
167+
// Pause when the tab is hidden; resume when it becomes visible again.
168+
document.addEventListener('visibilitychange', function () {
169+
if (document.hidden) {
170+
stop();
171+
} else {
172+
start();
173+
}
174+
});
175+
176+
start();
177+
}
178+
179+
document.addEventListener('DOMContentLoaded', function () {
180+
document.querySelectorAll('.carousel').forEach(setupCarousel);
181+
});
182+
})();

website/templates/snippets/display_short_carousel_snippet.html

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
{% load cropping %}
88
{% load ml_tags %}
99

10-
<div id="main-carousel" class="carousel slide carousel-fade shortCarousel" data-ride="carousel" data-interval="10000"
10+
<div id="main-carousel" class="carousel slide carousel-fade shortCarousel" data-interval="10000"
1111
data-pause="false">
1212
<div class="page-title shadow-left">
1313
<div class="container carousel-container">
@@ -45,8 +45,7 @@
4545
</div>
4646
<ol class="carousel-indicators" {% if banners|length < 2 %} style="display: none" {% endif %}>
4747
{% for banner in banners %}
48-
<li data-target="#mainCarousel" data-slide-to="{{ forloop.counter0 }}"
49-
{% if forloop.first %}class="active"{% endif %}></li>
48+
<li {% if forloop.first %}class="active"{% endif %}></li>
5049
{% endfor %}
5150
</ol>
5251
</div>

website/templates/website/base.html

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -106,17 +106,14 @@
106106
crossorigin="anonymous"></script>
107107
<script src="{% static 'website/js/jquery.easing.min.js' %}"></script>
108108
<script src="{% static 'website/js/top-navbar.js' %}"></script>
109+
<script src="{% static 'website/js/carousel.js' %}"></script>
109110

110111
{% block external_scripts %}{% endblock %}
111112

112113
<title>{% block pagetitle %}{{ request.resolver_match.url_name }}{% endblock %} | Makeability Lab</title>
113114

114115
<script>
115116
{% block scripts %}{% endblock %}
116-
117-
$(document).ready(function() {
118-
$('.carousel').carousel();
119-
});
120117
</script>
121118

122119
{% if debug %}
@@ -261,9 +258,8 @@
261258
- Nested links are allowed within region (no nested-interactive violation)
262259
{% endcomment %}
263260
{% block maincarousel %}
264-
<div id="main-carousel"
265-
class="carousel slide carousel-fade"
266-
data-ride="carousel"
261+
<div id="main-carousel"
262+
class="carousel slide carousel-fade"
267263
data-interval="10000"
268264
data-pause="true"
269265
role="region"
@@ -321,9 +317,7 @@
321317
{% if banners|length < 2 %}style="display: none"{% endif %}
322318
aria-label="Carousel navigation">
323319
{% for banner in banners %}
324-
<li data-target="#main-carousel"
325-
data-slide-to="{{ forloop.counter0 }}"
326-
{% if forloop.first %}class="active"{% endif %}
320+
<li {% if forloop.first %}class="active"{% endif %}
327321
aria-label="Slide {{ forloop.counter }}">
328322
</li>
329323
{% endfor %}

0 commit comments

Comments
 (0)