PageSourceSearch

https://www.visionsource-joplin.com/scripts/utils-canvas-carousel-detector.js

js visionsource-joplin.com collected 2026-10-08 23:18:24 UTC 12,941 bytes, 432 lines download raw bytes

1// src/utils/canvas-carousel-detector.js
2class CANVASCarouselDetector {
3  constructor() {
4    this.detectedCarousels = new Map();
5    this.observer = null;
6    this.Swiper = null;
7  }
8
9
10  async init() {
11    try {
12
13      await this.loadSwiper();
14      
15
16      if (document.readyState === 'loading') {
17        document.addEventListener('DOMContentLoaded', () => this.startDetection());
18      } else {
19        this.startDetection();
20      }
21    } catch (error) {
22      console.error('❌ Failed to initialize CANVAS Carousel Detector:', error);
23    }
24  }
25
26
27  async loadSwiper() {
28    return new Promise((resolve, reject) => {
29
30      if (window.Swiper) {
31        this.Swiper = window.Swiper;
32        //console.log('Swiper already available globally');
33        resolve();
34        return;
35      }
36
37
38      const script = document.createElement('script');
39      script.type = 'module';
40      script.innerHTML = `
41        import Swiper from 'https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.mjs';
42        window.Swiper = Swiper;
43        window.SwiperLoaded = true;
44      `;
45
46      script.onload = () => {
47
48        const checkSwiper = () => {
49          if (window.Swiper && window.SwiperLoaded) {
50            this.Swiper = window.Swiper;
51            //console.log('Swiper loaded from CDN');
52            resolve();
53          } else {
54            setTimeout(checkSwiper, 100);
55          }
56        };
57        checkSwiper();
58      };
59
60      script.onerror = () => {
61        console.error('❌ Failed to load Swiper from CDN');
62        reject(new Error('Failed to load Swiper'));
63      };
64
65      document.head.appendChild(script);
66
67      if (!document.querySelector('link[href*="swiper"]')) {
68        const link = document.createElement('link');
69        link.rel = 'stylesheet';
70        link.href = 'https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css';
71        document.head.appendChild(link);
72      }
73    });
74  }
75
76  startDetection() {
77    this.scanForCarousels();
78
79    this.setupMutationObserver();
80
81    //console.log('CANVAS Carousel Detector initialized');
82  }
83
84  scanForCarousels() {
85    const carousels = document.querySelectorAll('.carousel.slide');
86
87    carousels.forEach(carousel => {
88      if (!this.detectedCarousels.has(carousel.id)) {
89        this.convertCarousel(carousel);
90      }
91    });
92  }
93
94  setupMutationObserver() {
95    this.observer = new MutationObserver((mutations) => {
96      mutations.forEach((mutation) => {
97        mutation.addedNodes.forEach((node) => {
98          if (node.nodeType === Node.ELEMENT_NODE) {
99            if (node.classList && node.classList.contains('carousel') && node.classList.contains('slide')) {
100              this.convertCarousel(node);
101            }
102
103            const childCarousels = node.querySelectorAll && node.querySelectorAll('.carousel.slide');
104            if (childCarousels && childCarousels.length > 0) {
105              childCarousels.forEach(carousel => this.convertCarousel(carousel));
106            }
107          }
108        });
109      });
110    });
111
112    this.observer.observe(document.body, {
113      childList: true,
114      subtree: true
115    });
116  }
117
118  convertCarousel(carousel) {
119    if (!carousel.id || this.detectedCarousels.has(carousel.id)) {
120      return;
121    }
122
123    //console.log('Converting CANVAS carousel:', carousel.id);
124
125    try {
126      const settings = this.extractCarouselSettings(carousel);
127
128      const swiperStructure = this.createSwiperStructure(carousel, settings);
129
130      carousel.parentNode.replaceChild(swiperStructure, carousel);
131
132      setTimeout(() => {
133        const swiper = this.initializeSwiper(swiperStructure, settings);
134        
135        if (swiper) {
136          this.detectedCarousels.set(carousel.id, {
137            originalElement: carousel,
138            swiperElement: swiperStructure,
139            swiperInstance: swiper,
140            settings: settings
141          });
142
143          //console.log('Successfully converted carousel:', carousel.id);
144        }
145      }, 200);
146
147    } catch (error) {
148      console.error('❌ Failed to convert carousel:', carousel.id, error);
149    }
150  }
151
152  extractCarouselSettings(carousel) {
153    const settings = {
154      id: carousel.id,
155      autoplay: carousel.dataset.interval && carousel.dataset.interval !== 'false',
156      interval: parseInt(carousel.dataset.interval) || 3000,
157      pause: carousel.dataset.pause === 'true',
158      wrap: carousel.dataset.wrap === 'true',
159      effect: carousel.classList.contains('carousel-fade') ? 'fade' : 'slide',
160      width: carousel.dataset.width || '100%',
161      height: carousel.dataset.height || 'auto',
162      showIndicators: !!carousel.querySelector('.carousel-indicators'),
163      showControls: !!carousel.querySelector('.carousel-control')
164    };
165
166    return settings;
167  }
168
169  createSwiperStructure(carousel, settings) {
170
171    const swiperContainer = document.createElement('div');
172    swiperContainer.className = 'swiper canvas-converted-carousel';
173    swiperContainer.id = settings.id;
174    swiperContainer.style.width = settings.width;
175    swiperContainer.style.height = settings.height;
176
177    const swiperWrapper = document.createElement('div');
178    swiperWrapper.className = 'swiper-wrapper';
179
180    const carouselItems = carousel.querySelectorAll('.carousel-inner .item');
181    carouselItems.forEach((item, index) => {
182      const slide = document.createElement('div');
183      slide.className = 'swiper-slide';
184
185      slide.innerHTML = item.innerHTML;
186
187      if (item.style.cssText) {
188        slide.style.cssText = item.style.cssText;
189      }
190
191      swiperWrapper.appendChild(slide);
192    });
193
194    swiperContainer.appendChild(swiperWrapper);
195
196    if (settings.showIndicators) {
197      const pagination = document.createElement('div');
198      pagination.className = 'swiper-pagination';
199      pagination.id = `${settings.id}-pagination`;
200      swiperContainer.appendChild(pagination);
201    }
202
203    if (settings.showControls) {
204      const prevButton = document.createElement('div');
205      prevButton.className = 'swiper-button-prev';
206      prevButton.id = `${settings.id}-prev`;
207
208      const nextButton = document.createElement('div');
209      nextButton.className = 'swiper-button-next';
210      nextButton.id = `${settings.id}-next`;
211
212      swiperContainer.appendChild(prevButton);
213      swiperContainer.appendChild(nextButton);
214    }
215
216    return swiperContainer;
217  }
218
219  initializeSwiper(swiperElement, settings) {
220    if (!this.Swiper) {
221      console.error('❌ Swiper not available');
222      return null;
223    }
224
225    const slideCount = swiperElement.querySelectorAll('.swiper-slide').length;
226    
227    const enableLoop = settings.wrap && 
228                      slideCount > 2 && 
229                      settings.effect !== 'fade';
230
231    const swiperConfig = {
232      slidesPerView: 1,
233      spaceBetween: 0,
234      loop: enableLoop,
235      speed: 600,
236      grabCursor: true,
237      watchOverflow: true,
238      centeredSlides: false,
239      initialSlide: 0,
240      effect: settings.effect === 'fade' ? 'fade' : 'slide',
241      fadeEffect: settings.effect === 'fade' ? {
242        crossFade: true
243      } : undefined,
244
245      slideToClickedSlide: false,
246      touchRatio: 1,
247      touchAngle: 45,
248      shortSwipes: true,
249      longSwipes: true,
250
251      navigation: settings.showControls ? {
252        nextEl: `#${settings.id}-next`,
253        prevEl: `#${settings.id}-prev`,
254        hideOnClick: false,
255      } : false,
256
257      pagination: settings.showIndicators ? {
258        el: `#${settings.id}-pagination`,
259        clickable: true,
260        type: 'bullets',
261        dynamicBullets: false,
262        hideOnClick: false,
263      } : false,
264
265      autoplay: settings.autoplay ? {
266        delay: settings.interval,
267        pauseOnMouseEnter: settings.pause,
268        disableOnInteraction: false,
269        stopOnLastSlide: !enableLoop,
270        reverseDirection: false,
271      } : false,
272
273      // Events
274      on: {
275        init: (swiper) => {
276          setTimeout(() => {
277            swiper.update();
278            if (swiper.pagination) {
279              swiper.pagination.render();
280              swiper.pagination.update();
281            }
282          }, 100);
283        },
284        slideChange: (swiper) => {
285
286          const actualIndex = enableLoop ? swiper.realIndex : swiper.activeIndex;
287          //console.log(`Slide changed in ${settings.id}: activeIndex=${swiper.activeIndex}, realIndex=${actualIndex}`);
288          
289          const event = new CustomEvent('canvas-carousel-slide-change', {
290            detail: {
291              id: settings.id,
292              activeIndex: swiper.activeIndex,
293              realIndex: actualIndex,
294              swiper: swiper
295            }
296          });
297          document.dispatchEvent(event);
298        },
299        navigationPrev: (swiper) => {
300          //console.log(`Previous navigation clicked for ${settings.id}, going to: ${enableLoop ? swiper.realIndex : swiper.activeIndex}`);
301        },
302        navigationNext: (swiper) => {
303          //console.log(`Next navigation clicked for ${settings.id}, going to: ${enableLoop ? swiper.realIndex : swiper.activeIndex}`);
304        },
305        paginationClick: (swiper, event) => {
306         // console.log(`Pagination clicked for ${settings.id}`);
307        }
308      }
309    };
310
311    // Initialize Swiper
312    let swiper;
313    try {
314      swiper = new this.Swiper(swiperElement, swiperConfig);
315      
316      if (settings.showControls) {
317        const prevBtn = document.getElementById(`${settings.id}-prev`);
318        const nextBtn = document.getElementById(`${settings.id}-next`);
319        
320        if (prevBtn && nextBtn) {
321          
322          prevBtn.replaceWith(prevBtn.cloneNode(true));
323          nextBtn.replaceWith(nextBtn.cloneNode(true));
324          
325          const freshPrevBtn = document.getElementById(`${settings.id}-prev`);
326          const freshNextBtn = document.getElementById(`${settings.id}-next`);
327          
328          freshPrevBtn.addEventListener('click', (e) => {
329            e.preventDefault();
330            e.stopPropagation();
331            //console.log('Manual prev click');
332            
333            if (enableLoop) {
334              swiper.slidePrev();
335            } else {
336              const currentIndex = swiper.activeIndex;
337              const targetIndex = currentIndex === 0 ? slideCount - 1 : currentIndex - 1;
338              swiper.slideTo(targetIndex);
339            }
340          });
341          
342          freshNextBtn.addEventListener('click', (e) => {
343            e.preventDefault();
344            e.stopPropagation();
345            //console.log('Manual next click');
346            
347            if (enableLoop) {
348              swiper.slideNext();
349            } else {
350              const currentIndex = swiper.activeIndex;
351              const targetIndex = currentIndex === slideCount - 1 ? 0 : currentIndex + 1;
352              swiper.slideTo(targetIndex);
353            }
354          });
355        } else {
356          console.warn(`Navigation buttons not found for ${settings.id}`);
357        }
358      }
359      
360
361      if (settings.showIndicators) {
362        const pagination = document.getElementById(`${settings.id}-pagination`);
363        if (pagination) {
364          //console.log(`Pagination found for ${settings.id}`);
365          
366          setTimeout(() => {
367            const bullets = pagination.querySelectorAll('.swiper-pagination-bullet');
368            bullets.forEach((bullet, index) => {
369              bullet.addEventListener('click', (e) => {
370                e.preventDefault();
371                e.stopPropagation();
372               // console.log(`Manual pagination click to slide ${index}`);
373                swiper.slideTo(index);
374              });
375            });
376          }, 200);
377        } else {
378          console.warn(`Pagination not found for ${settings.id}`);
379        }
380      }
381      
382    } catch (error) {
383      console.error('❌ Failed to initialize Swiper:', error);
384      return null;
385    }
386
387    return swiper;
388  }
389
390  getCarousel(carouselId) {
391    return this.detectedCarousels.get(carouselId) || null;
392  }
393
394  getAllCarousels() {
395    return this.detectedCarousels;
396  }
397
398  destroyCarousel(carouselId) {
399    const carouselData = this.detectedCarousels.get(carouselId);
400    if (carouselData && carouselData.swiperInstance) {
401      carouselData.swiperInstance.destroy(true, true);
402      this.detectedCarousels.delete(carouselId);
403      //console.log('Destroyed carousel:', carouselId);
404    }
405  }
406
407  destroy() {
408    // Destroy all Swiper instances
409    this.detectedCarousels.forEach((carouselData, id) => {
410      if (carouselData.swiperInstance) {
411        carouselData.swiperInstance.destroy(true, true);
412      }
413    });
414
415    // Clear the map
416    this.detectedCarousels.clear();
417
418    // Disconnect observer
419    if (this.observer) {
420      this.observer.disconnect();
421      this.observer = null;
422    }
423
424    //console.log('CANVAS Carousel Detector destroyed');
425  }
426}
427
428const canvasCarouselDetector = new CANVASCarouselDetector();
429
430window.canvasCarouselDetector = canvasCarouselDetector;
431
432export default canvasCarouselDetector;

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.