PageSourceSearch

https://www.energymonitor.ai/wp-content/themes/goodlife-wp-B2B/assets/js/modules/productCarousel.js

js energymonitor.ai collected 2026-09-24 08:45:02 UTC 7,511 bytes, 240 lines download raw bytes

1/**
2 * Product Carousel Module
3 * Creates navigation and handles slide transitions for product carousels
4 */
5
6export default function initProductCarousel() {
7  const carousels = document.querySelectorAll('.product-carousel');
8
9  if (!carousels.length) return;
10
11  carousels.forEach((carousel, carouselIndex) => {
12    const slides = Array.from(carousel.children);
13
14    if (slides.length <= 1) return; // No need for navigation if only one slide
15
16    // Create navigation
17    const nav = createNavigation(slides.length, carouselIndex);
18
19    // Insert navigation after carousel
20    carousel.insertAdjacentElement('afterend', nav);
21
22    // Initialize carousel
23    initializeCarousel(carousel, slides, nav, carouselIndex);
24
25    // Bind navigation events
26    bindNavigationEvents(carousel, slides, nav, carouselIndex);
27  });
28}
29
30/**
31 * Create navigation element with full accessibility support
32 */
33function createNavigation(slideCount, carouselIndex) {
34  const nav = document.createElement('nav');
35  nav.className = 'product-carousel-nav';
36  nav.setAttribute('aria-label', `Product carousel ${carouselIndex + 1} navigation`);
37  nav.setAttribute('role', 'tablist');
38
39  const ul = document.createElement('ul');
40  ul.setAttribute('role', 'presentation');
41
42  for (let i = 0; i < slideCount; i++) {
43    const li = document.createElement('li');
44    li.setAttribute('role', 'presentation');
45
46    const button = document.createElement('button');
47    button.setAttribute('type', 'button');
48    button.setAttribute('role', 'tab');
49    button.setAttribute('id', `carousel-${carouselIndex}-tab-${i}`);
50    button.setAttribute('aria-controls', `carousel-${carouselIndex}-slide-${i}`);
51    button.setAttribute('aria-label', `Go to slide ${i + 1} of ${slideCount}`);
52    button.setAttribute('aria-selected', i === 0 ? 'true' : 'false');
53    button.setAttribute('tabindex', i === 0 ? '0' : '-1');
54    button.setAttribute('data-slide-index', i);
55
56    li.appendChild(button);
57    ul.appendChild(li);
58  }
59
60  nav.appendChild(ul);
61  return nav;
62}
63
64/**
65 * Initialize carousel state with full accessibility
66 */
67function initializeCarousel(carousel, slides, nav, carouselIndex) {
68  // Set up carousel container
69  carousel.style.position = 'relative';
70  carousel.style.overflow = 'hidden';
71  carousel.style.width = '100%';
72  carousel.setAttribute('role', 'region');
73  carousel.setAttribute('aria-label', `Product carousel ${carouselIndex + 1}`);
74  carousel.setAttribute('aria-live', 'polite');
75
76  // Create slide container
77  const slideContainer = document.createElement('div');
78  slideContainer.style.display = 'flex';
79  slideContainer.style.width = `${slides.length * 100}%`;
80  slideContainer.style.transition = 'transform 0.3s ease-in-out';
81  slideContainer.setAttribute('data-carousel-container', 'true');
82
83  // Move all slides into the container
84  slides.forEach((slide) => {
85    slideContainer.appendChild(slide);
86  });
87
88  // Add container to carousel
89  carousel.appendChild(slideContainer);
90
91  // Create live region for slide change announcements
92  const liveRegion = document.createElement('div');
93  liveRegion.setAttribute('aria-live', 'polite');
94  liveRegion.setAttribute('aria-atomic', 'true');
95  liveRegion.setAttribute('class', 'sr-only');
96  liveRegion.setAttribute('id', `carousel-${carouselIndex}-live-region`);
97  carousel.appendChild(liveRegion);
98
99  // Set up slides
100  slides.forEach((slide, index) => {
101    slide.style.flex = `0 0 ${100 / slides.length}%`;
102    slide.style.minWidth = '0';
103    slide.setAttribute('role', 'tabpanel');
104    slide.setAttribute('id', `carousel-${carouselIndex}-slide-${index}`);
105    slide.setAttribute('aria-labelledby', `carousel-${carouselIndex}-tab-${index}`);
106    slide.setAttribute('aria-hidden', index === 0 ? 'false' : 'true');
107
108    if (index === 0) {
109      slide.setAttribute('tabindex', '0');
110    } else {
111      slide.setAttribute('tabindex', '-1');
112    }
113  });
114
115  // Set initial active nav item
116  const firstNavButton = nav.querySelector('button');
117  if (firstNavButton) {
118    firstNavButton.classList.add('active');
119    firstNavButton.setAttribute('aria-selected', 'true');
120    firstNavButton.setAttribute('tabindex', '0');
121  }
122
123  // Announce initial slide
124  announceSlideChange(liveRegion, 1, slides.length);
125}
126
127/**
128 * Bind navigation events with full keyboard support
129 */
130function bindNavigationEvents(carousel, slides, nav, carouselIndex) {
131  const navButtons = nav.querySelectorAll('button');
132
133  navButtons.forEach((button, index) => {
134    // Click events
135    button.addEventListener('click', (e) => {
136      e.preventDefault();
137      goToSlide(carousel, slides, nav, index, carouselIndex);
138    });
139
140    // Keyboard events
141    button.addEventListener('keydown', (e) => {
142      let targetIndex = index;
143
144      switch (e.key) {
145        case 'ArrowRight':
146        case 'ArrowDown':
147          e.preventDefault();
148          targetIndex = (index + 1) % navButtons.length;
149          break;
150
151        case 'ArrowLeft':
152        case 'ArrowUp':
153          e.preventDefault();
154          targetIndex = (index - 1 + navButtons.length) % navButtons.length;
155          break;
156
157        case 'Home':
158          e.preventDefault();
159          targetIndex = 0;
160          break;
161
162        case 'End':
163          e.preventDefault();
164          targetIndex = navButtons.length - 1;
165          break;
166
167        case 'Enter':
168        case ' ':
169          e.preventDefault();
170          goToSlide(carousel, slides, nav, index, carouselIndex);
171          return;
172
173        default:
174          return;
175      }
176
177      // Focus the target button
178      navButtons[targetIndex].focus();
179      goToSlide(carousel, slides, nav, targetIndex, carouselIndex);
180    });
181
182    // Focus management
183    button.addEventListener('focus', () => {
184      // Optional: scroll into view if needed
185      button.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
186    });
187  });
188}
189
190/**
191 * Navigate to specific slide with full accessibility updates
192 */
193function goToSlide(carousel, slides, nav, targetIndex, carouselIndex) {
194  const navButtons = nav.querySelectorAll('button');
195  const liveRegion = carousel.querySelector(`#carousel-${carouselIndex}-live-region`);
196  const slideContainer = carousel.querySelector('[data-carousel-container]');
197
198  // Move the slide container to show the target slide
199  if (slideContainer) {
200    const translateX = -(targetIndex * (100 / slides.length));
201    slideContainer.style.transform = `translateX(${translateX}%)`;
202  }
203
204  // Update slide accessibility
205  slides.forEach((slide, index) => {
206    // Update ARIA attributes
207    if (index === targetIndex) {
208      slide.setAttribute('aria-hidden', 'false');
209      slide.setAttribute('tabindex', '0');
210    } else {
211      slide.setAttribute('aria-hidden', 'true');
212      slide.setAttribute('tabindex', '-1');
213    }
214  });
215
216  // Update navigation active states
217  navButtons.forEach((button, index) => {
218    if (index === targetIndex) {
219      button.classList.add('active');
220      button.setAttribute('aria-selected', 'true');
221      button.setAttribute('tabindex', '0');
222    } else {
223      button.classList.remove('active');
224      button.setAttribute('aria-selected', 'false');
225      button.setAttribute('tabindex', '-1');
226    }
227  });
228
229  // Announce slide change to screen readers
230  announceSlideChange(liveRegion, targetIndex + 1, slides.length);
231}
232
233/**
234 * Announce slide changes to screen readers
235 */
236function announceSlideChange(liveRegion, currentSlide, totalSlides) {
237  if (liveRegion) {
238    liveRegion.textContent = `Slide ${currentSlide} of ${totalSlides}`;
239  }
240}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.