-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathcarousel.js
More file actions
310 lines (273 loc) · 9.2 KB
/
carousel.js
File metadata and controls
310 lines (273 loc) · 9.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
/**
* JavaScript Carousel
*
* A simple customizable carousel built with plain JavaScript which can
* be the starting point to build your own carousel solution.
*
* @constructor
* @param {Object} Configuration options for the carousel.
* @param {string} config.carouselSelector - CSS selector for the carousel container element.
* @param {string} config.slideSelector - CSS selector for the individual slide elements within the carousel.
* @param {boolean} [config.enablePagination=true] - Whether to enable pagination for the carousel.
* @param {boolean} [config.enableAutoplay=true] - Whether to enable autoplay for the carousel.
* @param {number} [config.autoplayInterval=2000] - Autoplay interval in milliseconds.
* @returns {Object} An object containing methods to control the carousel.
* @returns {Function} create - A function to create and initialize the carousel.
* @returns {Function} destroy - A function to destroy and clean up the carousel.
* @author Rahul C.
* @see https://github.com/rahuldotdev/js-carousel
*/
const JSCarousel = ({
carouselSelector,
slideSelector,
enablePagination = true,
enableAutoplay = true,
autoplayInterval = 2000,
}) => {
/*
* Initialize variables to keep track of carousel state and
* references to different elements.
*/
let currentSlideIndex = 0;
let prevBtn, nextBtn;
let autoplayTimer;
let paginationContainer;
// Find the carousel element in the DOM.
const carousel = document.querySelector(carouselSelector);
// If carousel element is not found, log an error and exit.
if (!carousel) {
console.error("Specify a valid selector for the carousel.");
return null;
}
// Find all slides within the carousel
const slides = carousel.querySelectorAll(slideSelector);
// If no slides are found, log an error and exit.
if (!slides.length) {
console.error("Specify a valid selector for slides.");
return null;
}
/*
* Utility function to create and append HTML elements with
* attributes and children.
*/
const addElement = (tag, attributes, children) => {
const element = document.createElement(tag);
if (attributes) {
// Set attributes to the element.
Object.entries(attributes).forEach(([key, value]) => {
element.setAttribute(key, value);
});
}
if (children) {
// Set content to the element.
if (typeof children === "string") {
element.textContent = children;
} else {
children.forEach((child) => {
if (typeof child === "string") {
element.appendChild(document.createTextNode(child));
} else {
element.appendChild(child);
}
});
}
}
return element;
};
/*
* Modify the DOM structure and add required containers and controls
* to the carousel element.
*/
const tweakStructure = () => {
carousel.setAttribute("tabindex", "0");
// Create a div for carousel inner content.
const carouselInner = addElement("div", {
class: "carousel-inner",
});
carousel.insertBefore(carouselInner, slides[0]);
// If pagination is enabled, create and append pagination buttons.
if (enablePagination) {
paginationContainer = addElement("nav", {
class: "carousel-pagination",
role: "tablist",
});
carousel.appendChild(paginationContainer);
}
/*
* Move slides from the carousel element to the carousel inner
* container to facilitate alignment.
*/
slides.forEach((slide, index) => {
carouselInner.appendChild(slide);
slide.style.transform = `translateX(${index * 100}%)`;
if (enablePagination) {
const paginationBtn = addElement(
"btn",
{
class: `carousel-btn caroursel-btn--${index + 1}`,
role: "tab",
},
`${index + 1}`
);
paginationContainer.appendChild(paginationBtn);
if (index === 0) {
paginationBtn.classList.add("carousel-btn--active");
paginationBtn.setAttribute("aria-selected", true);
}
paginationBtn.addEventListener("click", () => {
handlePaginationBtnClick(index);
});
}
});
// Create and append previous button.
prevBtn = addElement(
"btn",
{
class: "carousel-btn carousel-btn--prev-next carousel-btn--prev",
"aria-label": "Previous Slide",
},
"<"
);
carouselInner.appendChild(prevBtn);
// Create and append next button.
nextBtn = addElement(
"btn",
{
class: "carousel-btn carousel-btn--prev-next carousel-btn--next",
"aria-label": "Next Slide",
},
">"
);
carouselInner.appendChild(nextBtn);
};
// Adjust slide positions according to the currently selected slide.
const adjustSlidePosition = () => {
slides.forEach((slide, i) => {
slide.style.transform = `translateX(${100 * (i - currentSlideIndex)}%)`;
});
};
/*
* Update the state of pagination buttons according to the currently
* selected slide.
*/
const updatePaginationBtns = () => {
const paginationBtns = paginationContainer.children;
const prevActiveBtns = Array.from(paginationBtns).filter((btn) =>
btn.classList.contains("carousel-btn--active")
);
prevActiveBtns.forEach((btn) => {
btn.classList.remove("carousel-btn--active");
btn.removeAttribute("aria-selected");
});
const currActiveBtns = paginationBtns[currentSlideIndex];
if (currActiveBtns) {
currActiveBtns.classList.add("carousel-btn--active");
currActiveBtns.setAttribute("aria-selected", true);
}
};
// Update the overall carousel state.
const updateCarouselState = () => {
if (enablePagination) {
updatePaginationBtns();
}
adjustSlidePosition();
};
// Move slide left and right based on direction provided.
const moveSlide = (direction) => {
const newSlideIndex =
direction === "next"
? (currentSlideIndex + 1) % slides.length
: (currentSlideIndex - 1 + slides.length) % slides.length;
currentSlideIndex = newSlideIndex;
updateCarouselState();
};
// Event handler for pagination button click event.
const handlePaginationBtnClick = (index) => {
currentSlideIndex = index;
updateCarouselState();
};
// Event handlers for previous and next button clicks.
const handlePrevBtnClick = () => moveSlide("prev");
const handleNextBtnClick = () => moveSlide("next");
// Start autoplaying of slides.
const startAutoplay = () => {
autoplayTimer = setInterval(() => {
moveSlide("next");
}, autoplayInterval);
};
// Stop autoplaying of slides.
const stopAutoplay = () => clearInterval(autoplayTimer);
/* Event handlers to manage autoplaying intelligentally on mouse
* enter and leave events.
*/
const handleMouseEnter = () => stopAutoplay();
const handleMouseLeave = () => startAutoplay();
// Event handler for keyboard navigation.
const handleKeyboardNav = (event) => {
// Exit, if carousel is not the event target.
if (!carousel.contains(event.target)) return;
// Exit, if the default action on the event is already prevented.
if (event.defaultPrevented) return;
/*
* Move slides in the respective directions when left and right
* keys are pressed.
*/
switch (event.key) {
case "ArrowLeft":
moveSlide("prev");
break;
case "ArrowRight":
moveSlide("next");
break;
default:
return;
}
/*
* Stop the default actions of the event in question when the
* carousel is the event target.
*/
event.preventDefault();
};
// Attach event listeners to relevant elements.
const attachEventListeners = () => {
prevBtn.addEventListener("click", handlePrevBtnClick);
nextBtn.addEventListener("click", handleNextBtnClick);
carousel.addEventListener("keydown", handleKeyboardNav);
if (enableAutoplay && autoplayInterval !== null) {
carousel.addEventListener("mouseenter", handleMouseEnter);
carousel.addEventListener("mouseleave", handleMouseLeave);
}
};
// Initialize/create the carousel.
const create = () => {
tweakStructure();
attachEventListeners();
if (enableAutoplay && autoplayInterval !== null) {
startAutoplay();
}
};
// Destroy the carousel/clean-up.
const destroy = () => {
// Remove event listeners.
prevBtn.removeEventListener("click", handlePrevBtnClick);
nextBtn.removeEventListener("click", handleNextBtnClick);
carousel.removeEventListener("keydown", handleKeyboardNav);
if (enablePagination) {
const paginationBtns =
paginationContainer.querySelectorAll(".carousel-btn");
if (paginationBtns.length) {
paginationBtns.forEach((btn) => {
btn.removeEventListener("click", handlePaginationBtnClick);
});
}
}
// Clear autoplay intervals if autoplay is enabled.
if (enableAutoplay && autoplayInterval !== null) {
carousel.removeEventListener("mouseenter", handleMouseEnter);
carousel.removeEventListener("mouseleave", handleMouseLeave);
stopAutoplay();
}
};
// Return an object with methods to create and destroy the carousel.
return { create, destroy };
};