PageSourceSearch

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

js energymonitor.ai collected 2026-09-24 08:45:07 UTC 9,600 bytes, 322 lines download raw bytes

1/**
2 * Article Card Carousel Module
3 * Creates a horizontal scrolling carousel for article cards with navigation controls
4 */
5
6const articleCardCarousel = {
7  /**
8   * Initialize all carousels on the page
9   * @param {string} selector - CSS selector for carousel containers
10   * @param {object} options - Configuration options
11   */
12  init(selector = '.article-card-carousel', options = {}) {
13    const containers = document.querySelectorAll(selector);
14
15    if (!containers.length) return;
16
17    const defaultOptions = {
18      visibleCards: {
19        mobile: 1.2,    // screens < 400px
20        tablet: 2.5,    // screens 400-768px
21        desktop: 3.7    // screens > 768px
22      },
23      ...options
24    };
25
26    containers.forEach(container => {
27      this.setupCarousel(container, defaultOptions);
28    });
29  },
30
31  /**
32   * Set up individual carousel
33   */
34  setupCarousel(container, options) {
35    const articles = container.querySelectorAll('article');
36
37    if (!articles.length) return;
38
39    // Add carousel wrapper and navigation
40    this.wrapCarousel(container);
41
42    // Calculate and set dimensions
43    this.calculateDimensions(container, options);
44
45    // Add navigation if needed
46    const shouldShowNav = this.shouldShowNavigation(container, options);
47    if (shouldShowNav) {
48      this.addNavigation(container);
49    }
50
51    // Add touch/swipe support
52    this.addTouchSupport(container);
53
54    // Handle window resize
55    this.handleResize(container, options);
56  },
57
58  /**
59   * Wrap carousel content for proper styling
60   */
61  wrapCarousel(container) {
62    // Create wrapper for the entire carousel
63    const wrapper = document.createElement('div');
64    wrapper.className = 'carousel-wrapper';
65
66    // Insert wrapper before container
67    container.parentNode.insertBefore(wrapper, container);
68
69    // Move container into wrapper
70    wrapper.appendChild(container);
71
72    // Add scrollable class to container
73    container.classList.add('carousel-scrollable');
74  },
75
76  /**
77   * Calculate and set carousel dimensions
78   */
79  calculateDimensions(container, options) {
80    const articles = container.querySelectorAll('article');
81    if (!articles.length) return;
82
83    const containerWidth = container.offsetWidth;
84    const visibleCards = this.getVisibleCards(options);
85
86    // Get the actual gap from computed styles
87    const computedStyle = getComputedStyle(container);
88    let gap = parseFloat(computedStyle.gap) || 0;
89
90    // If gap is 0, try to get it from CSS custom property
91    if (gap === 0) {
92      const rootStyles = getComputedStyle(document.documentElement);
93      const cssGap = rootStyles.getPropertyValue('--gap').trim();
94      // Convert rem to px (assuming 1rem = 10px base font size)
95      if (cssGap.includes('rem')) {
96        gap = parseFloat(cssGap) * 10;
97      } else {
98        gap = parseFloat(cssGap) || 24; // fallback
99      }
100    }
101
102    // Calculate article width
103    const articleWidth = (containerWidth - (gap * (visibleCards - 1))) / visibleCards;
104
105    // Set article widths
106    articles.forEach(article => {
107      article.style.minWidth = `${articleWidth}px`;
108      article.style.maxWidth = `${articleWidth}px`;
109    });
110
111    // Store dimensions for navigation - use measured distance for accuracy
112    setTimeout(() => {
113      const scrollDistance = this.calculateScrollDistance(articles);
114      container.dataset.articleWidth = articleWidth;
115      container.dataset.gap = gap;
116      container.dataset.scrollDistance = scrollDistance;
117      container.dataset.visibleCards = visibleCards;
118    }, 50); // Small delay to ensure DOM is updated
119  },
120
121  /**
122   * Calculate the actual scroll distance between articles
123   */
124  calculateScrollDistance(articles) {
125    if (articles.length < 2) return 0;
126
127    const firstArticle = articles[0];
128    const secondArticle = articles[1];
129
130    // Get the actual distance between the left edges of consecutive articles
131    const firstRect = firstArticle.getBoundingClientRect();
132    const secondRect = secondArticle.getBoundingClientRect();
133
134    return Math.abs(secondRect.left - firstRect.left);
135  },
136
137  /**
138   * Get number of visible cards based on screen width
139   */
140  getVisibleCards(options) {
141    const width = window.innerWidth;
142
143    if (width < 400) {
144      return options.visibleCards.mobile;
145    } else if (width <= 768) {
146      return options.visibleCards.tablet;
147    } else {
148      return options.visibleCards.desktop;
149    }
150  },
151
152  /**
153   * Check if navigation should be shown
154   */
155  shouldShowNavigation(container, options) {
156    const articles = container.querySelectorAll('article');
157    const visibleCards = this.getVisibleCards(options);
158
159    return articles.length > Math.floor(visibleCards);
160  },
161
162  /**
163   * Add navigation controls
164   */
165  addNavigation(container) {
166    const wrapper = container.parentNode;
167
168    // Create navigation container
169    const nav = document.createElement('div');
170    nav.className = 'carousel-nav';
171    nav.innerHTML = `
172      <button class="carousel-btn carousel-btn--prev" aria-label="Previous articles">
173        <svg width="24" height="24" viewBox="0 0 24 24" fill="none">
174          <path d="M15 18L9 12L15 6" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
175        </svg>
176      </button>
177      <button class="carousel-btn carousel-btn--next" aria-label="Next articles">
178        <svg width="24" height="24" viewBox="0 0 24 24" fill="none">
179          <path d="M9 18L15 12L9 6" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
180        </svg>
181      </button>
182    `;
183
184    // Insert navigation before carousel
185    wrapper.insertBefore(nav, container);
186
187    // Add event listeners
188    const prevBtn = nav.querySelector('.carousel-btn--prev');
189    const nextBtn = nav.querySelector('.carousel-btn--next');
190
191    prevBtn.addEventListener('click', () => this.scrollCarousel(container, 'prev'));
192    nextBtn.addEventListener('click', () => this.scrollCarousel(container, 'next'));
193
194    // Update button states
195    this.updateNavigationState(container);
196  },
197
198  /**
199   * Scroll carousel in specified direction
200   */
201  scrollCarousel(container, direction) {
202    // Use the calculated scroll distance (article width + gap)
203    const scrollDistance = parseFloat(container.dataset.scrollDistance) ||
204      (parseFloat(container.dataset.articleWidth) + parseFloat(container.dataset.gap));
205
206    const currentScroll = container.scrollLeft;
207    const newScroll = direction === 'next'
208      ? currentScroll + scrollDistance
209      : currentScroll - scrollDistance;
210
211    container.scrollTo({
212      left: newScroll,
213      behavior: 'smooth'
214    });
215
216    // Update navigation state after scroll completes
217    setTimeout(() => this.updateNavigationState(container), 300);
218  },
219
220  /**
221   * Update navigation button states
222   */
223  updateNavigationState(container) {
224    const wrapper = container.parentNode;
225    const nav = wrapper.querySelector('.carousel-nav');
226
227    if (!nav) return;
228
229    const prevBtn = nav.querySelector('.carousel-btn--prev');
230    const nextBtn = nav.querySelector('.carousel-btn--next');
231
232    const scrollLeft = container.scrollLeft;
233    const maxScroll = container.scrollWidth - container.offsetWidth;
234
235    // Update prev button
236    prevBtn.disabled = scrollLeft <= 0;
237    prevBtn.classList.toggle('disabled', scrollLeft <= 0);
238
239    // Update next button
240    nextBtn.disabled = scrollLeft >= maxScroll - 1; // -1 for rounding errors
241    nextBtn.classList.toggle('disabled', scrollLeft >= maxScroll - 1);
242  },
243
244  /**
245   * Add touch/swipe support for mobile devices
246   */
247  addTouchSupport(container) {
248    let startX = 0;
249    let startY = 0;
250    let isDragging = false;
251
252    container.addEventListener('touchstart', (e) => {
253      startX = e.touches[0].clientX;
254      startY = e.touches[0].clientY;
255      isDragging = true;
256    });
257
258    container.addEventListener('touchmove', (e) => {
259      if (!isDragging) return;
260
261      const currentX = e.touches[0].clientX;
262      const currentY = e.touches[0].clientY;
263
264      const diffX = Math.abs(currentX - startX);
265      const diffY = Math.abs(currentY - startY);
266
267      // If horizontal swipe is more prominent, prevent vertical scroll
268      if (diffX > diffY) {
269        e.preventDefault();
270      }
271    });
272
273    container.addEventListener('touchend', (e) => {
274      if (!isDragging) return;
275
276      const endX = e.changedTouches[0].clientX;
277      const diffX = startX - endX;
278
279      // Minimum swipe distance
280      if (Math.abs(diffX) > 50) {
281        if (diffX > 0) {
282          // Swiped left - go next
283          this.scrollCarousel(container, 'next');
284        } else {
285          // Swiped right - go prev
286          this.scrollCarousel(container, 'prev');
287        }
288      }
289
290      isDragging = false;
291    });
292
293    // Update navigation on scroll (for manual scrolling)
294    container.addEventListener('scroll', () => {
295      this.updateNavigationState(container);
296    });
297  },
298
299  /**
300   * Handle window resize
301   */
302  handleResize(container, options) {
303    let resizeTimeout;
304
305    window.addEventListener('resize', () => {
306      clearTimeout(resizeTimeout);
307      resizeTimeout = setTimeout(() => {
308        this.calculateDimensions(container, options);
309        this.updateNavigationState(container);
310
311        // Recalculate scroll distance after dimensions change
312        const articles = container.querySelectorAll('article');
313        if (articles.length >= 2) {
314          const scrollDistance = this.calculateScrollDistance(articles);
315          container.dataset.scrollDistance = scrollDistance;
316        }
317      }, 250);
318    });
319  }
320};
321
322export default articleCardCarousel;

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.