1/** 2 * Custom Slider Library - JavaScript with Lazy Loading 3 * 4 * Available CSS Classes: 5 * - no-arrows : Hide arrow navigation buttons 6 * - no-slider-dots : Hide dot navigation 7 * - slider-dots-bottom : Position dots at bottom center (default) 8 * - slider-dots-top : Position dots at top center 9 * - slider-dots-left : Position dots vertically on the left side 10 * - slider-dots-right : Position dots vertically on the right side 11 * - infinite-loop : Enable seamless infinite loop scrolling 12 * - lazy-load : Enable lazy loading for images (enabled by default) 13 * - no-lazy-load : Disable lazy loading and load all images immediately 14 * - preload-adjacent : Preload previous/next slides for smoother transitions 15 * - vertical-slider : Enable vertical sliding (bottom to top movement) 16 * 17 * Required HTML Structure (with Lazy Loading): 18 * <div class="custom-slider" [optional-classes] [data-auto-slide-time="5000"] [data-lazy-threshold="0.1"]> 19 * <div class="slider-container"> 20 * <div class="slider-wrapper"> 21 * <div class="slide"> 22 * <img data-lazy-src="{{image}}" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='800' height='400'%3E%3Crect width='100%25' height='100%25' fill='%23f0f0f0'/%3E%3C/svg%3E" alt="{{title}}" class="slide-bg-image lazy-image" loading="lazy" /> 23 * <div class="slide-overlay"> 24 * <h1>Title</h1> 25 * <p>Description</p> 26 * </div> 27 * </div> 28 * <div class="slide"> 29 * <img data-lazy-src="{{image2}}" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='800' height='400'%3E%3Crect width='100%25' height='100%25' fill='%23f0f0f0'/%3E%3C/svg%3E" alt="{{title2}}" class="slide-bg-image lazy-image" loading="lazy" /> 30 * <!-- More slides... --> 31 * </div> 32 * </div> 33 * </div> 34 * </div> 35 * 36 * Lazy Loading Configuration: 37 * - data-lazy-threshold: Intersection observer threshold (0.0 to 1.0, default: 0.1) 38 * - data-preload-distance: Number of slides to preload ahead/behind (default: 1) 39 * - data-placeholder-color: Placeholder background color (default: #f0f0f0) 40 * 41 * Custom Timing: 42 * Use data-auto-slide-time attribute to set custom timing in milliseconds 43 * Default: 4000ms (4 seconds) 44 * Example: data-auto-slide-time="6000" for 6-second intervals 45 * 46 * Usage: 47 * 1. Include slider.css in your HTML head 48 * 2. Include slider.js before closing body tag 49 * 3. Auto-initialization will handle all .custom-slider elements 50 * 4. Manual initialization: window.initCustomSlider(element) 51 * 52 * Performance Features: 53 * - Lazy loading with Intersection Observer API 54 * - Progressive image loading 55 * - Placeholder images to prevent layout shift 56 * - Intelligent preloading of adjacent slides 57 * - Viewport-based slider initialization 58 * - Memory-efficient slide management 59 * 60 * Features: 61 * - Touch/swipe support 62 * - Keyboard navigation (Arrow keys, Space to pause/resume) 63 * - Auto-play with custom timing 64 * - Responsive design 65 * - Accessibility features (ARIA attributes) 66 * - Hover to pause functionality 67 * - Multiple positioning options for navigation dots 68 * - Performance-optimized lazy loading 69 */ 70 71// Custom Slider JavaScript Implementation with Lazy Loading 72class CustomSlider { 73 constructor(element) { 74 // Initialize error tracking 75 this.errorCount = 0; 76 this.maxErrors = 10; 77 this.isDestroyed = false; 78 this.isInitializing = true; 79 80 try { 81 // Validate input element 82 if (!element) { 83 throw new Error('Slider element is required'); 84 } 85 86 // Comprehensive element validation (try instanceof first, then cross-context validation) 87 const isNativeHTMLElement = element instanceof HTMLElement; 88 89 if (!isNativeHTMLElement) { 90 // Alternative validation for cross-context elements 91 const isValidCrossContextElement = ( 92 element && 93 typeof element === 'object' && 94 element.nodeType === 1 && // ELEMENT_NODE 95 (typeof element.tagName === 'string' || typeof element.nodeName === 'string') && 96 typeof element.classList === 'object' && 97 element.classList !== null && 98 typeof element.querySelector === 'function' 99 ); 100 101 if (!isValidCrossContextElement) { 102 // Only log error if element fails ALL validation methods 103 const errorDetails = { 104 received: element, 105 type: typeof element, 106 nodeType: element?.nodeType, 107 nodeName: element?.nodeName, 108 constructor: element?.constructor?.name, 109 isNode: element instanceof Node, 110 isElement: element instanceof Element, 111 isHTMLElement: element instanceof HTMLElement, 112 hasTagName: !!element?.tagName, 113 hasClassList: !!element?.classList, 114 hasQuerySelector: typeof element?.querySelector === 'function' 115 }; 116 117 console.error('ð¨ Invalid element passed to CustomSlider:', errorDetails); 118 throw new Error(`Slider element must be a valid HTML element. Received: ${typeof element} (${element?.constructor?.name || 'unknown'})`); 119 } else { 120 console.info('â¹ï¸ Cross-context element detected - element is valid but from different execution context'); 121 } 122 } 123 124 // Core element validation with detailed errors 125 this.slider = element; 126 this.sliderWrapper = this.validateAndGetElement('.slider-wrapper', element); 127 128 if (!this.sliderWrapper) { 129 throw new Error('Required .slider-wrapper element not found'); 130 } 131 132 this.slides = this.slider.querySelectorAll('.slide'); 133 this.slideCount = this.slides.length; 134 135 // Validate minimum requirements 136 if (this.slideCount === 0) { 137 console.warn('â ï¸ Slider initialized with no slides'); 138 this.slideCount = 0; 139 } 140 141 // Initialize core properties with validation 142 this.currentSlide = 0; 143 this.autoSlideInterval = null; 144 this.isTransitioning = false;
145 this.startX = 0; 146 this.endX = 0; 147 148 // Validate classList access 149 try { 150 this.isInfiniteLoop = this.slider.classList.contains('infinite-loop'); 151 this.isCentered = this.slider.classList.contains('centered'); 152 this.lazyLoadEnabled = !this.slider.classList.contains('no-lazy-load'); 153 } catch (classError) { 154 console.error('â ï¸ Error accessing classList:', classError); 155 this.isInfiniteLoop = false; 156 this.isCentered = false; 157 this.lazyLoadEnabled = true; 158 } 159 160 this.originalSlideCount = this.slideCount; 161 this.cloneCount = 0; 162 this.isDragging = false; 163 this.startTransform = 0; 164 this.currentTransform = 0; 165 this.slidesPerView = 1; 166 this.centerOffset = 0; 167 168 // Enhanced timeout tracking for race condition prevention 169 this.activeTimeouts = new Map(); // Changed to Map for better tracking 170 this.transitionTimeoutId = null; 171 this.infiniteLoopTimeoutId = null; 172 this.autoSlideTimeoutId = null; 173 this.initTimeoutId = null; 174 175 // Lazy loading configuration with validation 176 this.validateLazyLoadingConfig(); 177 // Initialize state management with better validation 178 this.imageObserver = null; 179 this.loadedImages = new Set(); 180 this.loadingImages = new Set(); 181 this.priorityImages = new Set(); 182 this.failedImages = new Set(); // Track failed images 183 184 // Performance optimization flags 185 this.initialized = false; 186 this.isInViewport = false; 187 this.isVisible = true; // Track visibility 188 this.performanceMetrics = { 189 initTime: Date.now(), 190 imageLoadCount: 0, 191 errorCount: 0 192 }; 193 194 // Initialize async to prevent blocking 195 this.initTimeoutId = this.safeSetTimeout(() => { 196 try { 197 this.isInitializing = false; 198 this.init(); 199 } catch (initError) { 200 console.error('ð¨ SLIDER: Delayed init failed:', initError); 201 this.handleCriticalError(initError); 202 } 203 }, 0); 204 205 } catch (error) { 206 this.handleConstructorError(error, element); 207 } 208 } 209 210 /** 211 * Enhanced element validation with detailed error reporting 212 */ 213 validateAndGetElement(selector, parent = this.slider) { 214 try { 215 if (!parent || typeof parent.querySelector !== 'function') { 216 return null; 217 } 218 return parent.querySelector(selector); 219 } catch (error) { 220 console.error('ð¨ Element validation failed:', { selector, error }); 221 return null; 222 } 223 } 224 225 /** 226 * Validate lazy loading configuration with safe defaults 227 */ 228 validateLazyLoadingConfig() { 229 try { 230 this.preloadAdjacent = this.slider.classList.contains('preload-adjacent'); 231 232 // Safe attribute parsing with validation 233 const thresholdAttr = this.slider.getAttribute('data-lazy-threshold'); 234 this.lazyThreshold = this.parseFloatSafe(thresholdAttr, 0.1, 0, 1); 235 236 const distanceAttr = this.slider.getAttribute('data-preload-distance'); 237 this.preloadDistance = this.parseIntSafe(distanceAttr, 1, 0, 10); 238 239 this.placeholderColor = this.slider.getAttribute('data-placeholder-color') || '#f0f0f0'; 240 241 // Validate hex color 242 if (!/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(this.placeholderColor)) { 243 this.placeholderColor = '#f0f0f0'; 244 } 245 246 // Auto-slide timing with validation 247 const autoSlideAttr = this.slider.getAttribute('data-auto-slide-time'); 248 this.autoSlideTime = this.parseIntSafe(autoSlideAttr, 4000, 0, 60000); 249 250 } catch (error) { 251 console.error('ð¨ Lazy loading config validation failed:', error); 252 this.setDefaultLazyConfig(); 253 } 254 } 255 256 /** 257 * Safe float parsing with bounds checking 258 */ 259 parseFloatSafe(value, defaultValue, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) { 260 try { 261 const parsed = parseFloat(value); 262 if (isNaN(parsed)) return defaultValue; 263 return Math.max(min, Math.min(max, parsed)); 264 } catch (error) { 265 return defaultValue; 266 } 267 } 268 269 /** 270 * Safe integer parsing with bounds checking 271 */ 272 parseIntSafe(value, defaultValue, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) { 273 try { 274 const parsed = parseInt(value); 275 if (isNaN(parsed)) return defaultValue; 276 return Math.max(min, Math.min(max, parsed)); 277 } catch (error) { 278 return defaultValue; 279 } 280 } 281 282 /** 283 * Set default lazy loading configuration 284 */ 285 setDefaultLazyConfig() { 286 this.preloadAdjacent = false;
287 this.lazyThreshold = 0.1; 288 this.preloadDistance = 1; 289 this.placeholderColor = '#f0f0f0'; 290 this.autoSlideTime = 4000; 291 } 292 293 /** 294 * Enhanced constructor error handling 295 */ 296 handleConstructorError(error, element) { 297 console.error('ð¨ SLIDER CRASH: Constructor failed:', error); 298 console.error('ð¨ SLIDER CRASH: Element details:', { 299 element: element, 300 className: element?.className, 301 tagName: element?.tagName, 302 hasSlides: element?.querySelectorAll('.slide').length || 0, 303 hasWrapper: !!element?.querySelector('.slider-wrapper') 304 }); 305 306 // Attempt minimal fallback initialization 307 try { 308 this.slider = element; 309 this.slideCount = 0; 310 this.initialized = false; 311 this.isDestroyed = true; 312 this.errorCount = this.maxErrors; // Mark as error-prone 313 314 // Set safe defaults 315 this.setDefaultLazyConfig(); 316 this.activeTimeouts = new Map(); 317 this.loadedImages = new Set(); 318 this.loadingImages = new Set(); 319 this.priorityImages = new Set(); 320 this.failedImages = new Set(); 321 322 } catch (fallbackError) { 323 console.error('ð¨ SLIDER: Even fallback initialization failed:', fallbackError); 324 } 325 } 326 327 /** 328 * Handle critical errors during operation 329 */ 330 handleCriticalError(error) 331 { 332 this.errorCount++; 333 console.error(`ð¨ SLIDER CRITICAL ERROR #${this.errorCount}:`, error); 334 335 if (this.errorCount >= this.maxErrors) { 336 console.error('ð¨ SLIDER: Max errors reached, disabling slider'); 337 this.gracefulShutdown(); 338 } 339 } 340 341 /** 342 * Graceful shutdown when too many errors occur 343 */ 344 gracefulShutdown() { 345 try { 346 this.isDestroyed = true; 347 this.stopAutoSlide(); 348 this.clearAllTimeouts(); 349 350 if (this.imageObserver) { 351 this.imageObserver.disconnect(); 352 this.imageObserver = null; 353 } 354 355 // Add visual indicator of broken slider 356 if (this.slider && this.slider.style) { 357 this.slider.style.border = '2px dashed #ff6b6b'; 358 this.slider.style.opacity = '0.5'; 359 this.slider.setAttribute('data-slider-status', 'error'); 360 } 361 362 } catch (shutdownError) { 363 console.error('ð¨ SLIDER: Error during shutdown:', shutdownError); 364 } 365 } 366 367 /** 368 * Safe timeout management to prevent race conditions 369 */ 370 safeSetTimeout(callback, delay, description = 'unknown') { 371 try { 372 const timeoutId = setTimeout(() => { 373 try { 374 if (!this.isDestroyed) { 375 callback(); 376 } 377 } catch (error) { 378 console.error('ð¨ SLIDER: Timeout callback error:', error); 379 this.handleCriticalError(error); 380 } finally { 381 this.activeTimeouts.delete(timeoutId); 382 } 383 }, delay); 384 385 this.activeTimeouts.set(timeoutId, { 386 description, 387 timestamp: Date.now(), 388 delay 389 }); 390 391 return timeoutId; 392 } catch (error) { 393 console.error('ð¨ SLIDER: Error creating timeout:', error); 394 return null; 395 } 396 } 397 398 /** 399 * Clear all tracked timeouts 400 */ 401 clearAllTimeouts() { 402 try { 403 for (const [timeoutId, info] of this.activeTimeouts.entries()) { 404 clearTimeout(timeoutId); 405 } 406 this.activeTimeouts.clear(); 407 408 // Clear specific timeout IDs 409 if (this.transitionTimeoutId) { 410 clearTimeout(this.transitionTimeoutId); 411 this.transitionTimeoutId = null; 412 } 413 414 if (this.infiniteLoopTimeoutId) { 415 clearTimeout(this.infiniteLoopTimeoutId); 416 this.infiniteLoopTimeoutId = null; 417 } 418 419 if (this.autoSlideTimeoutId) { 420 clearTimeout(this.autoSlideTimeoutId); 421 this.autoSlideTimeoutId = null; 422 } 423 424 if (this.initTimeoutId) { 425 clearTimeout(this.initTimeoutId); 426 this.initTimeoutId = null; 427 } 428 429 } catch (error) { 430 console.error('ð¨ SLIDER: Error clearing timeouts:', error); 431 } 432 } 433 434 init() { 435 try { 436 // Prevent multiple initializations 437 if (this.initialized || this.isDestroyed) { 438 console.warn('â ï¸ Slider already initialized or destroyed'); 439 return; 440 } 441 442 // Check if we have minimum requirements 443 if (this.slideCount === 0) { 444 console.warn('â ï¸ Slider initialized with no slides, skipping setup'); 445 this.initialized = true; 446 return; 447 } 448 449 // Browser compatibility checks 450 if (!this.checkBrowserCompatibility()) { 451 console.warn('â ï¸ Browser compatibility issues detected, using fallback mode'); 452 this.lazyLoadEnabled = false;
453 } 454 455 // Set initial viewport state with error handling 456 try { 457 this.isInViewport = this.checkIfInViewport(); 458 } catch (viewportError) { 459 console.error('â ï¸ Viewport check failed:', viewportError); 460 this.isInViewport = true; // Assume visible on error 461 } 462 463 // Initialize lazy loading system first with validation 464 if (this.lazyLoadEnabled) { 465 try { 466 this.initializeLazyLoading(); 467 } catch (lazyError) { 468 console.error('â ï¸ Lazy loading initialization failed:', lazyError); 469 this.lazyLoadEnabled = false; 470 } 471 } 472 473 // Setup infinite loop clones if enabled 474 if (this.isInfiniteLoop) { 475 try { 476 this.setupInfiniteLoop(); 477 478 // Re-run lazy loading setup for cloned images if lazy loading is enabled 479 if (this.lazyLoadEnabled) { 480 this.setupLazyImages(); 481 } 482 } catch (infiniteError) { 483 console.error('â ï¸ Infinite loop setup failed:', infiniteError); 484 this.isInfiniteLoop = false; 485 this.slider.classList.remove('infinite-loop'); 486 } 487 } 488 489 // Create navigation with validation 490 try { 491 this.createNavigation(); 492 } catch (navError) { 493 console.error('â ï¸ Navigation creation failed:', navError); 494 } 495 496 // Enhanced dot creation with validation 497 try { 498 this.validateAndFixDots(); 499 if (!this.dotsCreated) { 500 this.createDots(); 501 } 502 } catch (dotError) { 503 console.error('â ï¸ Dots creation failed:', dotError); 504 } 505 506 // Bind events with error handling 507 try { 508 this.bindEvents(); 509 } catch (eventError) { 510 console.error('â ï¸ Event binding failed:', eventError); 511 } 512 513 // Load critical images immediately (current slide and adjacent slides) 514 if (this.lazyLoadEnabled) { 515 try { 516 this.loadCriticalImages(); 517 } catch (imageError) { 518 console.error('â ï¸ Critical image loading failed:', imageError); 519 } 520 } 521 522 // Only start auto slide if time > 0 and no errors occurred 523 if (this.autoSlideTime > 0 && !this.isDestroyed) { 524 try { 525 this.startAutoSlide(); 526 } catch (autoError) { 527 console.error('â ï¸ Auto-slide start failed:', autoError); 528 } 529 } 530 531 // Initial setup with validation 532 try { 533 this.updateSlider(); 534 } catch (updateError) { 535 console.error('â ï¸ Initial slider update failed:', updateError); 536 } 537 538 this.initialized = true; 539 this.performanceMetrics.initTime = Date.now() - this.performanceMetrics.initTime; 540 541 // Add success indicator 542 if (this.slider) { 543 this.slider.setAttribute('data-slider-status', 'initialized'); 544 } 545 546 } catch (error) { 547 console.error('ð¨ SLIDER CRASH: Initialization failed:', error); 548 console.error('ð¨ SLIDER CRASH: Slider state:', { 549 slideCount: this.slideCount, 550 currentSlide: this.currentSlide, 551 sliderWrapper: !!this.sliderWrapper, 552 lazyLoadEnabled: this.lazyLoadEnabled, 553 browserSupport: this.checkBrowserCompatibility() 554 }); 555 this.handleCriticalError(error); 556 } 557 } 558 559 /** 560 * Check browser compatibility for advanced features 561 */ 562 checkBrowserCompatibility() { 563 const support = { 564 intersectionObserver: 'IntersectionObserver' in window, 565 classList: 'classList' in document.createElement('div'), 566 addEventListener: 'addEventListener' in window, 567 querySelector: 'querySelector' in document, 568 getBoundingClientRect: 'getBoundingClientRect' in document.createElement('div'), 569 setTimeout: 'setTimeout' in window, 570 JSON: 'JSON' in window 571 }; 572 573 const unsupportedFeatures = Object.keys(support).filter(key => !support[key]); 574 575 if (unsupportedFeatures.length > 0) { 576 console.warn('â ï¸ Unsupported browser features:', unsupportedFeatures); 577 return false;
578 } 579 580 return true; 581 } 582 583 /** 584 * Check if slider is currently in viewport with error handling 585 */ 586 checkIfInViewport() { 587 try { 588 if (!this.slider || !this.slider.getBoundingClientRect) { 589 return false; 590 } 591 592 const rect = this.slider.getBoundingClientRect(); 593 const windowHeight = window.innerHeight || document.documentElement.clientHeight; 594 const windowWidth = window.innerWidth || document.documentElement.clientWidth; 595 596 return ( 597 rect.top >= -100 && // Allow some margin above 598 rect.left >= -100 && // Allow some margin left 599 rect.bottom <= windowHeight + 100 && // Allow some margin below 600 rect.right <= windowWidth + 100 // Allow some margin right 601 ); 602 } catch (error) { 603 console.error('â ï¸ Viewport check error:', error); 604 return true; // Assume visible on error 605 } 606 } 607 608 /** 609 * Initialize lazy loading system with enhanced error handling and browser compatibility 610 */ 611 initializeLazyLoading() { 612 try { 613 // Check for Intersection Observer support 614 if (!('IntersectionObserver' in window)) { 615 console.warn('â ï¸ IntersectionObserver not supported, disabling lazy loading'); 616 this.lazyLoadEnabled = false; 617 return; 618 } 619 620 // Create intersection observer for images with enhanced options 621 try { 622 this.imageObserver = new IntersectionObserver((entries) => { 623 entries.forEach(entry => { 624 try { 625 if (entry.isIntersecting && entry.target) { 626 this.loadImage(entry.target, false); 627 } 628 } catch (entryError) { 629 console.error('â ï¸ Intersection observer entry error:', entryError); 630 } 631 }); 632 }, { 633 root: null, // Use viewport instead of slider container 634 rootMargin: '100px', // Increased margin for better preloading 635 threshold: [0, this.lazyThreshold] // Multiple thresholds for better detection 636 }); 637 } catch (observerError) { 638 console.error('â ï¸ Failed to create intersection observer:', observerError); 639 this.lazyLoadEnabled = false; 640 return; 641 } 642 643 // Setup lazy loading for all images 644 this.setupLazyImages(); 645 646 // Create observer for slider visibility 647 this.createSliderObserver(); 648 649 } catch (error) { 650 console.error('â ï¸ Lazy loading initialization error:', error); 651 this.lazyLoadEnabled = false; 652 } 653 } 654 655 /** 656 * Setup lazy loading attributes for all images with enhanced error handling 657 */ 658 setupLazyImages() { 659 try { 660 if (!this.slider) { 661 console.error('â ï¸ Cannot setup lazy images: slider element missing'); 662 return; 663 } 664 665 const images = this.slider.querySelectorAll('img'); 666 667 if (images.length === 0) { 668 console.info('â¹ï¸ No images found for lazy loading'); 669 return; 670 } 671 672 let processedCount = 0; 673 let errorCount = 0; 674 675 images.forEach((img, index) => { 676 try { 677 // Skip if image already processed 678 if (img.dataset.lazyProcessed === 'true') { 679 return; 680 } 681 682 // Validate image element 683 if (!img || typeof img.setAttribute !== 'function') { 684 errorCount++; 685 return; 686 } 687 688 const lazySrc = img.dataset.lazySrc; 689 const originalSrc = img.getAttribute('src'); 690 691 // If no data-lazy-src, convert existing src to lazy 692 if (!lazySrc && originalSrc && !originalSrc.startsWith('data:')) { 693 img.dataset.lazySrc = originalSrc; 694 img.src = this.createPlaceholderImage(img); 695 } 696 697 // Add lazy loading classes safely 698 if (img.classList) { 699 img.classList.add('lazy-image'); 700 } 701 702 // Add loading state handlers with error protection 703 const loadHandler = () => { 704 try { 705 if (img.classList) { 706 img.classList.add('loaded'); 707 img.classList.remove('loading'); 708 } 709 this.performanceMetrics.imageLoadCount++; 710 } catch (loadError) { 711 console.error('â ï¸ Image load handler error:', loadError); 712 } 713 }; 714 715 const errorHandler = (event) => { 716 try { 717 if (img.classList) { 718 img.classList.add('error'); 719 img.classList.remove('loading'); 720 } 721 this.failedImages.add(img); 722 this.performanceMetrics.errorCount++; 723 console.warn('â ï¸ Image failed to load:', img.src); 724 } catch (errorHandlerError) { 725 console.error('â ï¸ Image error handler error:', errorHandlerError); 726 } 727 }; 728 729 // Remove existing listeners to prevent duplicates 730 img.removeEventListener('load', loadHandler); 731 img.removeEventListener('error', errorHandler); 732 733 // Add new listeners 734 img.addEventListener('load', loadHandler); 735 img.addEventListener('error', errorHandler); 736 737 img.dataset.lazyProcessed = 'true'; 738 739 // Observe the image for lazy loading if observer exists 740 if (this.imageObserver) { 741 this.imageObserver.observe(img); 742 } 743 744 processedCount++; 745 746 } catch (imgError) { 747 console.error(`â ï¸ Error processing image ${index}:`, imgError); 748 errorCount++; 749 } 750 }); 751 752 console.info(`â¹ï¸ Lazy loading setup: ${processedCount} images processed, ${errorCount} errors`); 753 754 } catch (error) { 755 console.error('ð¨ Setup lazy images failed:', error); 756 } 757 } 758 759 /** 760 * Create placeholder image data URL with validation 761 */ 762 createPlaceholderImage(img) { 763 try { 764 if (!img) { 765 return 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="800" height="400"%3E%3Crect width="100%25" height="100%25" fill="%23f0f0f0"/%3E%3C/svg%3E'; 766 } 767 768 // Get dimensions with fallbacks 769 let width = 800; 770 let height = 400; 771 772 try { 773 const widthAttr = img.getAttribute('width'); 774 const heightAttr = img.getAttribute('height'); 775 776 if (widthAttr) width = parseInt(widthAttr) || width; 777 if (heightAttr) height = parseInt(heightAttr) || height; 778 779 // Fallback to computed/offset dimensions 780 if (img.offsetWidth && img.offsetWidth > 0) width = img.offsetWidth; 781 if (img.offsetHeight && img.offsetHeight > 0) height = img.offsetHeight; 782 783 // Ensure reasonable bounds 784 width = Math.max(50, Math.min(2000, width)); 785 height = Math.max(50, Math.min(2000, height)); 786 787 } catch (dimError) { 788 console.warn('â ï¸ Could not determine image dimensions:', dimError); 789 } 790 791 // Validate placeholder color 792 const color = this.placeholderColor || '#f0f0f0'; 793 const encodedColor = encodeURIComponent(color); 794 795 return `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='${width}' height='${height}'%3E%3Crect width='100%25' height='100%25' fill='${encodedColor}'/%3E%3Ctext x='50%25' y='50%25' text-anchor='middle' dy='0.3em' fill='%23999' font-family='Arial' font-size='16'%3ELoading...%3C/text%3E%3C/svg%3E`; 796 797 } catch (error) { 798 console.error('â ï¸ Error creating placeholder:', error); 799 // Return minimal fallback 800 return 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="800" height="400"%3E%3Crect width="100%25" height="100%25" fill="%23f0f0f0"/%3E%3C/svg%3E'; 801 } 802 } 803 804 /** 805 * Load critical images with enhanced error handling and performance optimization 806 */ 807 loadCriticalImages() { 808 try { 809 if (!this.slides || this.slides.length === 0) { 810 console.warn('â ï¸ No slides available for critical image loading'); 811 return; 812 } 813
814 const criticalSlides = new Set(); 815 const startTime = performance.now ? performance.now() : Date.now(); 816 817 // Current slide 818 if (this.currentSlide >= 0 && this.currentSlide < this.slides.length) { 819 criticalSlides.add(this.currentSlide); 820 } 821 822 // Adjacent slides for preloading with bounds checking 823 for (let i = 1; i <= this.preloadDistance; i++) { 824 const prevIndex = (this.currentSlide - i + this.slides.length) % this.slides.length; 825 const nextIndex = (this.currentSlide + i) % this.slides.length; 826 827 if (prevIndex >= 0 && prevIndex < this.slides.length) { 828 criticalSlides.add(prevIndex); 829 } 830 if (nextIndex >= 0 && nextIndex < this.slides.length) { 831 criticalSlides.add(nextIndex); 832 } 833 } 834 835 // Enhanced packages slider handling 836 const isPackagesSlider = this.slider.classList.contains('packages-slider') || 837 this.slider.classList.contains('custom-cards-slider'); 838 839 if (isPackagesSlider) { 840 try { 841 const slideWidth = this.getSlideWidth(); 842 const containerWidth = this.slider.offsetWidth || 800; 843 const visibleSlides = Math.ceil(containerWidth / slideWidth) + 2; // +2 for safety 844 845 // Load all currently visible slides 846 for (let i = 0; i < Math.min(visibleSlides, this.slides.length); i++) { 847 const visibleSlideIndex = (this.currentSlide + i) % this.slides.length; 848 if (visibleSlideIndex >= 0 && visibleSlideIndex < this.slides.length) { 849 criticalSlides.add(visibleSlideIndex); 850 } 851 } 852 } catch (packagesError) { 853 console.error('â ï¸ Packages slider critical loading error:', packagesError); 854 } 855 } 856 857 let loadedCount = 0; 858 let errorCount = 0; 859 860 // Load images for critical slides
861 criticalSlides.forEach(slideIndex => { 862 try { 863 if (this.slides[slideIndex]) { 864 const slideImages = this.slides[slideIndex].querySelectorAll('img'); 865 slideImages.forEach(img => { 866 try { 867 if (img && !this.failedImages.has(img)) { 868 this.priorityImages.add(img); 869 this.loadImage(img, true); 870 loadedCount++; 871 } 872 } catch (imgError) { 873 console.error('â ï¸ Error loading critical image:', imgError); 874 errorCount++; 875 } 876 }); 877 } 878 } catch (slideError) { 879 console.error(`â ï¸ Error processing slide ${slideIndex}:`, slideError); 880 errorCount++; 881 } 882 }); 883 884 const endTime = performance.now ? performance.now() : Date.now(); 885 //console.info(`â¹ï¸ Critical images loaded: ${loadedCount} images, ${errorCount} errors, ${Math.round(endTime - startTime)}ms`); 886 887 } catch (error) { 888 console.error('ð¨ Load critical images failed:', error); 889 } 890 } 891 892 /** 893 * Load individual image with enhanced error handling and race condition prevention 894 */ 895 loadImage(img, isPriority = false) { 896 try { 897 // Validate image element 898 if (!img || !img.tagName || img.tagName.toLowerCase() !== 'img') { 899 console.warn('â ï¸ Invalid image element provided to loadImage'); 900 return; 901 } 902 903 // Check if slider is destroyed 904 if (this.isDestroyed) { 905 return; 906 } 907 908 // Check if image already has a real src (not placeholder) 909 const currentSrc = img.getAttribute('src'); 910 if (currentSrc && !currentSrc.startsWith('data:') && this.loadedImages.has(img)) { 911 return; 912 } 913 914 const lazySrc = img.dataset.lazySrc || currentSrc; 915 916 // If no lazy src and current src is not a placeholder, mark as loaded 917 if (!lazySrc) { 918 if (currentSrc && !currentSrc.startsWith('data:')) { 919 this.loadedImages.add(img); 920 if (img.classList) { 921 img.classList.add('loaded'); 922 } 923 } 924 return; 925 } 926 927 // Skip if already processed or currently loading (prevent duplicate loading) 928 if (this.loadingImages.has(img) || this.loadedImages.has(img)) { 929 return; 930 } 931 932 // Check if image was previously failed and should be retried 933 if (this.failedImages.has(img)) { 934 console.info('â¹ï¸ Retrying failed image:', lazySrc); 935 this.failedImages.delete(img); 936 } 937 938 this.loadingImages.add(img); 939 if (img.classList) { 940 img.classList.add('loading'); 941 } 942 943 // For priority images or if not using lazy loading, load immediately 944 if (isPriority || !this.lazyLoadEnabled) { 945 try { 946 img.src = lazySrc; 947 this.loadedImages.add(img); 948 this.loadingImages.delete(img); 949 this.priorityImages.delete(img); 950 951 if (img.classList) { 952 img.classList.remove('loading'); 953 img.classList.add('loaded'); 954 } 955 956 // Unobserve once loaded 957 if (this.imageObserver) { 958 this.imageObserver.unobserve(img); 959 } 960 return; 961 } catch (immediateLoadError) { 962 console.error('â ï¸ Immediate image load failed:', immediateLoadError); 963 this.handleImageLoadError(img, lazySrc); 964 return; 965 } 966 } 967 968 // Create new image to preload for non-priority images 969 const preloadImg = new Image(); 970 971 // Set up timeout for image loading 972 const loadTimeout = this.safeSetTimeout(() => { 973 console.warn('â ï¸ Image load timeout:', lazySrc); 974 this.handleImageLoadError(img, lazySrc, 'timeout'); 975 }, 10000, `image-load-timeout-${lazySrc.substring(0, 50)}`); 976 977 preloadImg.onload = () => { 978 try { 979 // Clear timeout 980 if (loadTimeout) { 981 clearTimeout(loadTimeout); 982 this.activeTimeouts.delete(loadTimeout); 983 } 984 985 // Check if slider is still active 986 if (this.isDestroyed) { 987 return; 988 } 989 990 // Update the actual image source 991 img.src = lazySrc; 992 this.loadedImages.add(img); 993 this.loadingImages.delete(img); 994 this.priorityImages.delete(img); 995 996 if (img.classList) { 997 img.classList.remove('loading'); 998 img.classList.add('loaded'); 999 } 1000 1001 // Unobserve once loaded 1002 if (this.imageObserver) { 1003 this.imageObserver.unobserve(img); 1004 } 1005 1006 } catch (onloadError) { 1007 console.error('â ï¸ Error in image onload handler:', onloadError); 1008 this.handleImageLoadError(img, lazySrc); 1009 } 1010 }; 1011 1012 preloadImg.onerror = (errorEvent) => { 1013 try { 1014 // Clear timeout 1015 if (loadTimeout) { 1016 clearTimeout(loadTimeout); 1017 this.activeTimeouts.delete(loadTimeout); 1018 } 1019 1020 console.error('â ï¸ Image load error:', lazySrc, errorEvent); 1021 this.handleImageLoadError(img, lazySrc, 'load-error'); 1022 1023 } catch (onerrorError) { 1024 console.error('â ï¸ Error in image onerror handler:', onerrorError); 1025 } 1026 }; 1027 1028 // Start loading 1029 preloadImg.src = lazySrc; 1030 1031 } catch (error) { 1032 console.error('ð¨ SLIDER CRASH: loadImage failed:', error); 1033 console.error('ð¨ SLIDER CRASH: Image details during crash:', { 1034 imageSrc: img?.src, 1035 lazySrc: img?.dataset?.lazySrc, 1036 isPriority: isPriority, 1037 loadedImagesCount: this.loadedImages?.size, 1038 loadingImagesCount: this.loadingImages?.size 1039 }); 1040 // Cleanup on error 1041 this.handleImageLoadError(img, img?.dataset?.lazySrc || img?.src); 1042 } 1043 } 1044 1045 /** 1046 * Handle image loading errors consistently 1047 */ 1048 handleImageLoadError(img, src, reason = 'unknown') { 1049 try { 1050 if (img && this.loadingImages) { 1051 this.loadingImages.delete(img); 1052 this.priorityImages.delete(img); 1053 this.failedImages.add(img); 1054 1055 if (img.classList) { 1056 img.classList.remove('loading'); 1057 img.classList.add('error'); 1058 } 1059 1060 // Set alt text if not present 1061 if (!img.alt) { 1062 img.alt = 'Failed to load image'; 1063 } 1064 } 1065 1066 this.performanceMetrics.errorCount++; 1067 console.warn(`â ï¸ Image load failed (${reason}):`, src); 1068 1069 } catch (error) { 1070 console.error('ð¨ Error handling image load error:', error); 1071 } 1072 } 1073 1074 /** 1075 * Create intersection observer for slider viewport detection 1076 */ 1077 createSliderObserver() { 1078 const sliderObserver = new IntersectionObserver((entries) => {
1079 entries.forEach(entry => { 1080 this.isInViewport = entry.isIntersecting; 1081 1082 if (entry.isIntersecting) { 1083 // Slider is in viewport, ensure critical images are loaded 1084 this.loadCriticalImages(); 1085 // Restart auto-slide if it was stopped 1086 if (!this.autoSlideInterval && this.autoSlideTime > 0) { 1087 this.startAutoSlide(); 1088 } 1089 } 1090 }); 1091 }, { 1092 root: null, 1093 rootMargin: '100px', 1094 threshold: 0.1 1095 }); 1096 1097 sliderObserver.observe(this.slider); 1098 } 1099 1100 /** 1101 * Preload images for upcoming slides 1102 */ 1103 preloadUpcomingSlides() { 1104 if (!this.lazyLoadEnabled || !this.preloadAdjacent) return; 1105 1106 const upcomingSlides = new Set(); 1107 1108 // Calculate upcoming slides 1109 for (let i = 1; i <= this.preloadDistance; i++) { 1110 const nextIndex = (this.currentSlide + i) % this.slides.length; 1111 upcomingSlides.add(nextIndex); 1112 } 1113 1114 // For packages slider, also preload slides that might become visible 1115 const isPackagesSlider = this.slider.classList.contains('packages-slider') || 1116 this.slider.classList.contains('custom-cards-slider'); 1117 1118 if (isPackagesSlider) { 1119 const slideWidth = this.getSlideWidth(); 1120 const containerWidth = this.slider.offsetWidth; 1121 const visibleSlides = Math.ceil(containerWidth / slideWidth); 1122 1123 // Preload next batch of potentially visible slides 1124 for (let i = visibleSlides; i <= visibleSlides + this.preloadDistance; i++) { 1125 const preloadIndex = (this.currentSlide + i) % this.slides.length; 1126 upcomingSlides.add(preloadIndex); 1127 } 1128 } 1129 1130 // Load images for upcoming slides
1131 upcomingSlides.forEach(slideIndex => { 1132 if (this.slides[slideIndex]) { 1133 const slideImages = this.slides[slideIndex].querySelectorAll('img[data-lazy-src]:not(.loaded):not(.loading)'); 1134 slideImages.forEach(img => { 1135 if (!this.loadedImages.has(img) && !this.loadingImages.has(img)) { 1136 // Load with slight delay to not interfere with current transition 1137 setTimeout(() => { 1138 this.loadImage(img); 1139 }, 100); 1140 } 1141 }); 1142 } 1143 }); 1144 } 1145 1146 setupInfiniteLoop() { 1147 if (this.slideCount < 2) return; // Need at least 2 slides for infinite loop 1148 1149 // Check if infinite loop is already set up 1150 if (this.infiniteLoopSetup) { 1151 1152 return; 1153 } 1154 1155 1156 1157 const isPackagesSlider = this.slider.classList.contains('packages-slider') || 1158 this.slider.classList.contains('custom-cards-slider'); 1159 1160 if (isPackagesSlider) { 1161 // For packages slider, clone 2-3 slides on each side for smooth infinite scrolling 1162 this.cloneCount = Math.min(3, this.slideCount); 1163 } else { 1164 // For banner sliders, clone 1 slide on each side 1165 this.cloneCount = 1; 1166 } 1167 1168 1169 1170 // Store original slides before cloning 1171 const originalSlides = Array.from(this.slides); 1172 1173 // Clone first slides and append to end 1174 for (let i = 0; i < this.cloneCount; i++) { 1175 const clone = originalSlides[i].cloneNode(true); 1176 clone.classList.add('slide-clone', 'slide-clone-end'); 1177 clone.setAttribute('data-clone-index', i); 1178 clone.setAttribute('data-original-index', i); 1179 1180 // Fix lazy loading for cloned images 1181 this.setupClonedImages(clone, i); 1182 1183 this.sliderWrapper.appendChild(clone); 1184 } 1185 1186 // Clone last slides and prepend to beginning 1187 for (let i = this.cloneCount - 1; i >= 0; i--) { 1188 const originalIndex = this.slideCount - 1 - i; 1189 const clone = originalSlides[originalIndex].cloneNode(true); 1190 clone.classList.add('slide-clone', 'slide-clone-start'); 1191 clone.setAttribute('data-clone-index', originalIndex); 1192 clone.setAttribute('data-original-index', originalIndex); 1193 1194 // Fix lazy loading for cloned images 1195 this.setupClonedImages(clone, originalIndex); 1196 1197 this.sliderWrapper.insertBefore(clone, this.sliderWrapper.firstChild); 1198 } 1199 1200 // Update slides collection to include clones 1201 this.slides = this.sliderWrapper.querySelectorAll('.slide'); 1202 1203 // Adjust current slide to account for prepended clones 1204 this.currentSlide = this.cloneCount; 1205 1206 // Mark as set up to prevent re-execution 1207 this.infiniteLoopSetup = true; 1208 1209 1210 1211 } 1212 1213 /** 1214 * Setup lazy loading for cloned slide images 1215 */ 1216 setupClonedImages(clonedSlide, originalIndex) { 1217 if (!this.lazyLoadEnabled) return; 1218 1219 const clonedImages = clonedSlide.querySelectorAll('img'); 1220 const originalSlide = this.slides[originalIndex]; 1221 const originalImages = originalSlide.querySelectorAll('img'); 1222 1223 clonedImages.forEach((clonedImg, imgIndex) => { 1224 const originalImg = originalImages[imgIndex]; 1225 if (originalImg) { 1226 // Copy lazy loading attributes from original 1227 const lazySrc = originalImg.dataset.lazySrc; 1228 const originalSrc = originalImg.getAttribute('src'); 1229 1230 if (lazySrc) { 1231 clonedImg.dataset.lazySrc = lazySrc; 1232 clonedImg.dataset.lazyProcessed = 'true'; 1233 clonedImg.classList.add('lazy-image'); 1234 1235 // Set placeholder if original has one 1236 if (originalSrc && originalSrc.startsWith('data:')) { 1237 clonedImg.src = originalSrc; 1238 } else { 1239 clonedImg.src = this.createPlaceholderImage(clonedImg); 1240 } 1241 1242 // Add event listeners 1243 clonedImg.addEventListener('load', () => { 1244 clonedImg.classList.add('loaded'); 1245 clonedImg.classList.remove('loading'); 1246 }); 1247 1248 clonedImg.addEventListener('error', () => { 1249 clonedImg.classList.add('error'); 1250 clonedImg.classList.remove('loading'); 1251 1252 }); 1253 1254 // Observe for lazy loading if observer exists 1255 if (this.imageObserver) { 1256 this.imageObserver.observe(clonedImg); 1257 } 1258 } else if (originalSrc && !originalSrc.startsWith('data:')) { 1259 // Original doesn't use lazy loading, copy src directly 1260 clonedImg.src = originalSrc; 1261 clonedImg.classList.add('loaded'); 1262 } 1263 } 1264 }); 1265 } 1266 1267 createNavigation() { 1268 if (this.slider.classList.contains('no-arrows')) return; 1269 1270 // AGGRESSIVE CHECK: Remove any existing navigation first 1271 const existingNav = this.slider.querySelectorAll('.slider-navigation'); 1272 if (existingNav.length > 0) { 1273 1274 existingNav.forEach(nav => nav.remove()); 1275 } 1276 1277 // Double-check the flag 1278 if (this.navigationCreated) { 1279 1280 return; 1281 } 1282 1283 const navigation = document.createElement('div'); 1284 navigation.className = 'slider-navigation'; 1285 1286 const prevBtn = document.createElement('button'); 1287 prevBtn.className = 'slider-btn prev-btn'; 1288 prevBtn.innerHTML = '‹'; 1289 prevBtn.setAttribute('aria-label', 'Previous slide'); 1290 1291 const nextBtn = document.createElement('button'); 1292 nextBtn.className = 'slider-btn next-btn'; 1293 nextBtn.innerHTML = '›'; 1294 nextBtn.setAttribute('aria-label', 'Next slide'); 1295 1296 navigation.appendChild(prevBtn); 1297 navigation.appendChild(nextBtn); 1298 this.slider.appendChild(navigation); 1299 1300 // Store references 1301 this.prevBtn = prevBtn; 1302 this.nextBtn = nextBtn; 1303 this.navigationCreated = true; 1304 1305 1306 } 1307 1308 /** 1309 * Clean up duplicate dots without affecting infinite loop functionality 1310 */
1311 cleanupDuplicateDots() { 1312 try { 1313 // Find all dot containers in this slider 1314 const allDotContainers = this.slider.querySelectorAll('.slider-dots'); 1315 1316 if (allDotContainers.length <= 1) { 1317 return; // No duplicates found 1318 } 1319 1320 1321 1322 // Keep the first properly constructed container, remove others 1323 let keptContainer = null; 1324 const containersToRemove = []; 1325 1326 allDotContainers.forEach((container, index) => { 1327 const dots = container.querySelectorAll('.dot'); 1328 const expectedDotCount = this.isInfiniteLoop ? this.originalSlideCount : this.slideCount; 1329 1330 if (!keptContainer && dots.length === expectedDotCount) { 1331 // Keep the first container with correct dot count 1332 keptContainer = container; 1333 1334 } else { 1335 containersToRemove.push(container); 1336 } 1337 }); 1338 1339 // Remove duplicate containers 1340 containersToRemove.forEach((container, index) => { 1341 1342 container.remove(); 1343 }); 1344 1345 // Update dots reference if we kept a container 1346 if (keptContainer) { 1347 this.dots = keptContainer.querySelectorAll('.dot'); 1348 this.dotsCreated = true; 1349 } else { 1350 // No good container found, reset for recreation 1351 this.dots = null; 1352 this.dotsCreated = false; 1353 } 1354 1355 1356 1357 } catch (error) { 1358 console.error('ð¨ Error cleaning up duplicate dots:', error); 1359 } 1360 } 1361 1362 /** 1363 * Check for and fix any dot-related issues without touching infinite loop 1364 */ 1365 validateAndFixDots() { 1366 try { 1367 const expectedDotCount = this.isInfiniteLoop ? this.originalSlideCount : this.slideCount; 1368 const dotContainers = this.slider.querySelectorAll('.slider-dots'); 1369 1370 // Check for multiple containers 1371 if (dotContainers.length > 1) { 1372 1373 this.cleanupDuplicateDots(); 1374 return; 1375 } 1376 1377 // Check for incorrect dot count in single container 1378 if (dotContainers.length === 1) { 1379 const container = dotContainers[0]; 1380 const dots = container.querySelectorAll('.dot'); 1381 1382 if (dots.length !== expectedDotCount) { 1383 1384 container.remove(); 1385 this.dotsCreated = false; 1386 this.createDots(); 1387 return; 1388 } 1389 1390 // Dots look good, just update reference 1391 this.dots = dots; 1392 this.dotsCreated = true; 1393 } 1394 1395 // No container exists and dots should exist 1396 if (dotContainers.length === 0 && !this.slider.classList.contains('no-slider-dots')) { 1397 1398 this.dotsCreated = false; 1399 this.createDots(); 1400 } 1401 1402 } catch (error) { 1403 console.error('ð¨ Error validating dots:', error); 1404 } 1405 } 1406 1407 createDots() { 1408 if (this.slider.classList.contains('no-slider-dots')) return; 1409 1410 // Enhanced cleanup - Remove ALL existing dots first with better detection 1411 const existingDots = this.slider.querySelectorAll('.slider-dots'); 1412 if (existingDots.length > 0) { 1413 1414 existingDots.forEach(dots => { 1415 // Remove event listeners before removing element 1416 const dotButtons = dots.querySelectorAll('.dot'); 1417 dotButtons.forEach(dot => { 1418 dot.replaceWith(dot.cloneNode(true)); // Remove all event listeners 1419 }); 1420 dots.remove(); 1421 }); 1422 } 1423 1424 // Triple-check: Look for any orphaned dot elements 1425 const orphanedDots = this.slider.querySelectorAll('.dot:not(.slider-
1425dots .dot)'); 1426 if (orphanedDots.length > 0) { 1427 1428 orphanedDots.forEach(dot => dot.remove()); 1429 } 1430 1431 // Check flag after cleanup 1432 if (this.dotsCreated) { 1433 1434 this.dotsCreated = false; 1435 } 1436 1437 const dotsContainer = document.createElement('div'); 1438 dotsContainer.className = 'slider-dots'; 1439 1440 // Determine position class 1441 if (this.slider.classList.contains('slider-dots-top')) { 1442 dotsContainer.classList.add('slider-dots-top'); 1443 } else if (this.slider.classList.contains('slider-dots-left')) { 1444 dotsContainer.classList.add('slider-dots-left'); 1445 } else if (this.slider.classList.contains('slider-dots-right')) { 1446 dotsContainer.classList.add('slider-dots-right'); 1447 } else if (this.slider.classList.contains('slider-dots-right-center')) { 1448 dotsContainer.classList.add('slider-dots-right-center'); 1449 } else if (this.slider.classList.contains('slider-dots-left-center')) { 1450 dotsContainer.classList.add('slider-dots-left-center'); 1451 } else { 1452 dotsContainer.classList.add('slider-dots-bottom'); 1453 } 1454 1455 // Create dots - ONLY for original slides, not clones 1456 const dotCount = this.isInfiniteLoop ? this.originalSlideCount : this.slideCount; 1457 1458 1459 for (let i = 0; i < dotCount; i++) { 1460 const dot = document.createElement('button'); 1461 dot.className = 'dot'; 1462 if (i === 0) dot.classList.add('active'); 1463 dot.setAttribute('data-slide', i); 1464 dot.setAttribute('aria-label', `Go to slide ${i + 1}`); 1465 dotsContainer.appendChild(dot); 1466 } 1467 1468 // Add unique identifier to prevent conflicts 1469 dotsContainer.setAttribute('data-slider-dots-id', `dots-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`); 1470 1471 this.slider.appendChild(dotsContainer); 1472 this.dots = dotsContainer.querySelectorAll('.dot'); 1473 this.dotsCreated = true; 1474 1475 1476 } 1477 1478 /** 1479 * Enhanced event binding with proper error handling and memory management 1480 */ 1481 bindEvents() { 1482 try { 1483 // Create bound handler methods to maintain context and enable proper cleanup 1484 this.prevSlideHandler = () => this.prevSlide(); 1485 this.nextSlideHandler = () => this.nextSlide(); 1486 this.mouseEnterHandler = () => this.stopAutoSlide(); 1487 this.mouseLeaveHandler = () => this.startAutoSlide(); 1488 this.touchStartHandler = (e) => this.handleTouchStart(e); 1489 this.touchMoveHandler = (e) => this.handleTouchMove(e); 1490 this.touchEndHandler = (e) => this.handleTouchEnd(e); 1491 this.mouseDownHandler = (e) => this.handleMouseStart(e); 1492 this.mouseMoveHandler = (e) => this.handleMouseMove(e); 1493 this.mouseUpHandler = (e) => this.handleMouseEnd(e); 1494 this.keydownHandler = (e) => this.handleKeyboard(e); 1495 1496 // Arrow buttons with validation 1497 if (this.prevBtn && this.prevBtn.addEventListener) { 1498 this.prevBtn.addEventListener('click', this.prevSlideHandler); 1499 } 1500 if (this.nextBtn && this.nextBtn.addEventListener) { 1501 this.nextBtn.addEventListener('click', this.nextSlideHandler); 1502 } 1503 1504 // Dots navigation with comprehensive error handling 1505 if (this.dots && this.dots.length > 0) { 1506 this.dotClickHandlers = []; 1507 this.dotMousedownHandlers = []; 1508 1509 this.dots.forEach((dot, index) => { 1510 try { 1511 if (dot && dot.addEventListener) { 1512 const clickHandler = (e) => { 1513 try { 1514 e.stopPropagation(); 1515 if (!this.isDestroyed) { 1516 this.goToSlide(index); 1517 } 1518 } catch (clickError) { 1519 console.error('â ï¸ Dot click error:', clickError); 1520 } 1521 }; 1522 1523 const mousedownHandler = (e) => { 1524 try { 1525 e.stopPropagation(); 1526 } catch (mousedownError) { 1527 console.error('â ï¸ Dot mousedown error:', mousedownError); 1528 } 1529 }; 1530 1531 dot.addEventListener('click', clickHandler); 1532 dot.addEventListener('mousedown', mousedownHandler); 1533 1534 // Store handlers for cleanup 1535 this.dotClickHandlers[index] = clickHandler; 1536 this.dotMousedownHandlers[index] = mousedownHandler; 1537 } 1538 } catch (dotError) { 1539 console.error(`â ï¸ Error binding events for dot ${index}:`, dotError); 1540 } 1541 }); 1542 } 1543 1544 // Slider events with validation 1545 if (this.slider && this.slider.addEventListener) { 1546 // Hover events 1547 this.slider.addEventListener('mouseenter', this.mouseEnterHandler); 1548 this.slider.addEventListener('mouseleave', this.mouseLeaveHandler); 1549 1550 // Touch events 1551 this.slider.addEventListener('touchstart', this.touchStartHandler, { passive: false }); 1552 this.slider.addEventListener('touchmove', this.touchMoveHandler, { passive: false }); 1553 this.slider.addEventListener('touchend', this.touchEndHandler); 1554 1555 // Mouse events for desktop dragging 1556 this.slider.addEventListener('mousedown', this.mouseDownHandler); 1557 this.slider.addEventListener('mousemove', this.mouseMoveHandler); 1558 this.slider.addEventListener('mouseup', this.mouseUpHandler); 1559 this.slider.addEventListener('mouseleave', this.mouseUpHandler); // Also handle mouse leave as mouse up 1560 1561 // Keyboard events 1562 this.slider.addEventListener('keydown', this.keydownHandler); 1563 1564 // Make slider focusable for keyboard navigation 1565 this.slider.setAttribute('tabindex', '0'); 1566 } 1567 1568 } catch (error) { 1569 console.error('â ï¸ Event binding failed:', error); 1570 } 1571 } 1572 1573 updateSlider() { 1574 try { 1575 // Early validation checks 1576 if (!this.sliderWrapper || this.isTransitioning || this.isDestroyed) { 1577 if (this.isDestroyed) { 1578 console.warn('â ï¸ updateSlider called on destroyed slider'); 1579 } 1580 return; 1581 } 1582 1583 // Clear any existing transition timeout to prevent conflicts 1584 if (this.transitionTimeoutId) { 1585 clearTimeout(this.transitionTimeoutId); 1586 this.activeTimeouts.delete(this.transitionTimeoutId); 1587 this.transitionTimeoutId = null; 1588 } 1589 1590 this.isTransitioning = true; 1591 const startTime = performance.now ? performance.now() : Date.now(); 1592 1593 // Validate current slide bounds
1594 if (this.currentSlide < 0 || this.currentSlide >= this.slides.length) { 1595 console.warn(`â ï¸ Current slide index out of bounds: ${this.currentSlide}/${this.slides.length}`); 1596 this.currentSlide = Math.max(0, Math.min(this.slides.length - 1, this.currentSlide)); 1597 } 1598 1599 // Calculate transform based on slider type 1600 const transformResult = this.calculateTransform(); 1601 if (!transformResult.success) { 1602 console.error('â ï¸ Transform calculation failed'); 1603 this.isTransitioning = false; 1604 return; 1605 } 1606 1607 // Apply transform with error handling 1608 try { 1609 this.sliderWrapper.style.transform = transformResult.transform; 1610 } catch (transformError) { 1611 console.error('â ï¸ Failed to apply transform:', transformError); 1612 this.isTransitioning = false; 1613 return; 1614 } 1615 1616 // Update dots with validation 1617 this.updateDots(); 1618 1619 // Update active slide class for centered mode 1620 if (this.isCentered && this.slides.length > 0) { 1621 this.updateActiveSlideClasses(); 1622 } 1623 1624 // Update ARIA attributes for accessibility 1625 this.updateAriaAttributes(); 1626 1627 // Enhanced lazy loading with error protection 1628 if (this.lazyLoadEnabled && !this.isDestroyed) { 1629 this.safeSetTimeout(() => { 1630 if (!this.isDestroyed) { 1631 try { 1632 this.loadCriticalImages(); 1633 // Preload upcoming slides with delay 1634 this.safeSetTimeout(() => { 1635 if (!this.isDestroyed) { 1636 this.preloadUpcomingSlides(); 1637 } 1638 }, 100, 'preload-upcoming'); 1639 } catch (lazyError) { 1640 console.error('â ï¸ Lazy loading during update failed:', lazyError); 1641 } 1642 } 1643 }, 0, 'update-lazy-loading'); 1644 } 1645 1646 // Set transition end timeout with race condition protection 1647 this.transitionTimeoutId = this.safeSetTimeout(() => { 1648 try { 1649 if (!this.isDestroyed) { 1650 this.isTransitioning = false; 1651 this.transitionTimeoutId = null; 1652 } 1653 } catch (timeoutError) { 1654 console.error('â ï¸ Transition timeout error:', timeoutError); 1655 // Force reset the flag 1656 this.isTransitioning = false; 1657 this.transitionTimeoutId = null; 1658 } 1659 }, 500, 'transition-end'); 1660 1661 const endTime = performance.now ? performance.now() : Date.now(); 1662 const duration = Math.round(endTime - startTime); 1663 1664 if (duration > 16) { // Log if update takes longer than 1 frame 1665 console.warn(`â ï¸ Slow updateSlider: ${duration}ms`); 1666 } 1667 1668 } catch (error) { 1669 console.error('ð¨ SLIDER CRASH: updateSlider failed:', error); 1670 console.error('ð¨ SLIDER CRASH: Slider state during crash:', { 1671 currentSlide: this.currentSlide, 1672 slideCount: this.slideCount, 1673 isTransitioning: this.isTransitioning, 1674 sliderWrapper: !!this.sliderWrapper, 1675 sliderClasses: this.slider?.className 1676 }); 1677 // Force reset transition state 1678 this.isTransitioning = false; 1679 if (this.transitionTimeoutId) { 1680 clearTimeout(this.transitionTimeoutId); 1681 this.activeTimeouts.delete(this.transitionTimeoutId); 1682 this.transitionTimeoutId = null; 1683 } 1684 this.handleCriticalError(error); 1685 } 1686 } 1687 1688 /**
1689 * Calculate transform value based on slider type 1690 */ 1691 calculateTransform() { 1692 try { 1693 const isVerticalSlider = this.slider.classList.contains('vertical-slider'); 1694 const isPackagesSlider = this.slider.classList.contains('packages-slider') || 1695 this.slider.classList.contains('custom-cards-slider'); 1696 1697 if (isVerticalSlider) { 1698 // Vertical slider: move from bottom to top with centering support 1699 const slideHeight = this.getSlideHeight(); 1700 const containerHeight = this.slider.offsetHeight || 600; 1701 let translateY = -this.currentSlide * slideHeight; 1702 1703 // If centered, add offset to keep active slide in center 1704 if (this.isCentered) { 1705 const centerOffset = (containerHeight - slideHeight) / 2; 1706 translateY += centerOffset; 1707 } 1708 1709 return { 1710 success: true, 1711 transform: `translateY(${translateY}px)` 1712 }; 1713 } else if (isPackagesSlider) { 1714 const slideWidth = this.getSlideWidth(); 1715 const containerWidth = this.slider.offsetWidth || 800; 1716 this.slidesPerView = Math.floor(containerWidth / slideWidth); 1717 1718 let translateX; 1719 1720 if (this.isCentered) { 1721 // Centered mode: position active slide in center 1722 this.centerOffset = (containerWidth - slideWidth) / 2; 1723 translateX = -this.currentSlide * slideWidth + this.centerOffset; 1724 } else { 1725 // Standard mode: align to left edge 1726 const maxSlide = Math.max(0, this.slideCount - this.slidesPerView); 1727 1728 // Ensure currentSlide doesn't exceed maxSlide 1729 if (this.currentSlide > maxSlide) { 1730 this.currentSlide = maxSlide; 1731 } 1732 1733 translateX = -this.currentSlide * slideWidth; 1734 } 1735 1736 return { 1737 success: true, 1738 transform: `translateX(${translateX}px)` 1739 }; 1740 } else { 1741 // Default behavior for banner-style sliders 1742 const translateX = -this.currentSlide * 100; 1743 return { 1744 success: true, 1745 transform: `translateX(${translateX}%)` 1746 }; 1747 } 1748 1749 } catch (error) { 1750 console.error('â ï¸ Transform calculation error:', error); 1751 return { success: false, transform: null }; 1752 } 1753 } 1754 1755 /** 1756 * Update dots with enhanced error handling 1757 */ 1758 updateDots() { 1759 try { 1760 if (!this.dots || this.dots.length === 0) { 1761 return; 1762 } 1763 1764 let activeIndex = this.currentSlide; 1765 if (this.isInfiniteLoop) { 1766 // Calculate the actual slide index (excluding clones) 1767 activeIndex = (this.currentSlide - this.cloneCount + this.originalSlideCount) % this.originalSlideCount; 1768 } 1769 1770 // Validate activeIndex 1771 if (activeIndex < 0 || activeIndex >= this.dots.length) { 1772 activeIndex = 0; 1773 } 1774 1775 this.dots.forEach((dot, index) => { 1776 try { 1777 if (dot && dot.classList) { 1778 dot.classList.toggle('active', index === activeIndex); 1779 } 1780 } catch (dotError) { 1781 console.error(`â ï¸ Error updating dot ${index}:`, dotError); 1782 } 1783 }); 1784 1785 } catch (error) { 1786 console.error('â ï¸ Dots update error:', error); 1787 } 1788 } 1789 1790 /** 1791 * Update active slide classes 1792 */ 1793 updateActiveSlideClasses() { 1794 try {
1795 this.slides.forEach((slide, index) => { 1796 try { 1797 if (slide && slide.classList) { 1798 slide.classList.toggle('active', index === this.currentSlide); 1799 } 1800 } catch (slideError) { 1801 console.error(`â ï¸ Error updating slide class ${index}:`, slideError); 1802 } 1803 }); 1804 } catch (error) { 1805 console.error('â ï¸ Active slide classes update error:', error); 1806 } 1807 } 1808 1809 /** 1810 * Update ARIA attributes for accessibility 1811 */ 1812 updateAriaAttributes() { 1813 try { 1814 this.slides.forEach((slide, index) => { 1815 try { 1816 if (slide && slide.setAttribute) { 1817 slide.setAttribute('aria-hidden', index !== this.currentSlide); 1818 } 1819 } catch (ariaError) { 1820 console.error(`â ï¸ Error updating ARIA for slide ${index}:`, ariaError); 1821 } 1822 }); 1823 } catch (error) { 1824 console.error('â ï¸ ARIA attributes update error:', error); 1825 } 1826 } 1827 1828 /** 1829 * Get slide width with caching and comprehensive error handling 1830 */ 1831 getSlideWidth() { 1832 try { 1833 // Use cached value if available and valid 1834 if (this.cachedSlideWidth && this.lastWidthCheck && 1835 (Date.now() - this.lastWidthCheck) < 1000) { // Cache for 1 second 1836 return this.cachedSlideWidth; 1837 } 1838 1839 if (!this.slides || this.slides.length === 0) { 1840 console.warn('â ï¸ No slides available for width calculation'); 1841 return 380; // Default fallback 1842 } 1843 1844 const firstSlide = this.slides[0]; 1845 if (!firstSlide) { 1846 console.warn('â ï¸ First slide not found'); 1847 return 380; 1848 } 1849 1850 let slideWidth = 380; // Safe default 1851 1852 try { 1853 // Get computed styles with fallback 1854 const styles = window.getComputedStyle ? window.getComputedStyle(firstSlide) : null; 1855 1856 if (styles) { 1857 const marginLeft = parseFloat(styles.marginLeft) || 0; 1858 const marginRight = parseFloat(styles.marginRight) || 0; 1859 const offsetWidth = firstSlide.offsetWidth || 380; 1860 1861 slideWidth = offsetWidth + marginLeft + marginRight; 1862 1863 // Ensure reasonable bounds 1864 slideWidth = Math.max(100, Math.min(2000, slideWidth)); 1865 } else { 1866 // Fallback if getComputedStyle not available 1867 slideWidth = firstSlide.offsetWidth || 380; 1868 } 1869 1870 } catch (styleError) { 1871 console.error('â ï¸ Error calculating slide width:', styleError); 1872 slideWidth = firstSlide.offsetWidth || 380; 1873 } 1874 1875 // Cache the result 1876 this.cachedSlideWidth = slideWidth; 1877 this.lastWidthCheck = Date.now(); 1878 1879 return slideWidth; 1880 1881 } catch (error) { 1882 console.error('â ï¸ getSlideWidth failed:', error); 1883 return 380; // Safe fallback 1884 } 1885 } 1886 1887 /** 1888 * Get slide height with caching and comprehensive error handling (for vertical sliders) 1889 */ 1890 getSlideHeight() { 1891 try { 1892 // Use cached value if available and valid 1893 if (this.cachedSlideHeight && this.lastHeightCheck && 1894 (Date.now() - this.lastHeightCheck) < 1000) { // Cache for 1 second 1895 return this.cachedSlideHeight; 1896 } 1897 1898 if (!this.slides || this.slides.length === 0) { 1899 console.warn('â ï¸ No slides available for height calculation'); 1900 return 400; // Default fallback 1901 } 1902 1903 const firstSlide = this.slides[0]; 1904 if (!firstSlide) { 1905 console.warn('â ï¸ First slide not found'); 1906 return 400; 1907 } 1908 1909 let slideHeight = 400; // Safe default 1910 1911 try { 1912 // Get computed styles with fallback 1913 const styles = window.getComputedStyle ? window.getComputedStyle(firstSlide) : null; 1914 1915 if (styles) { 1916 const marginTop = parseFloat(styles.marginTop) || 0; 1917 const marginBottom = parseFloat(styles.marginBottom) || 0; 1918 const offsetHeight = firstSlide.offsetHeight || 400; 1919 1920 slideHeight = offsetHeight + marginTop + marginBottom; 1921 1922 // Ensure reasonable bounds 1923 slideHeight = Math.max(100, Math.min(2000, slideHeight)); 1924 } else { 1925 // Fallback if getComputedStyle not available 1926 slideHeight = firstSlide.offsetHeight || 400; 1927 } 1928 1929 } catch (styleError) { 1930 console.error('â ï¸ Error calculating slide height:', styleError); 1931 slideHeight = firstSlide.offsetHeight || 400; 1932 } 1933 1934 // Cache the result 1935 this.cachedSlideHeight = slideHeight; 1936 this.lastHeightCheck = Date.now(); 1937 1938 return slideHeight; 1939 1940 } catch (error) { 1941 console.error('â ï¸ getSlideHeight failed:', error); 1942 return 400; // Safe fallback 1943 } 1944 } 1945 1946 nextSlide() { 1947 try { 1948 if (this.isTransitioning || this.isDestroyed) { 1949 return; 1950 } 1951 1952 if (!this.slides || this.slides.length === 0) { 1953 console.warn('â ï¸ nextSlide called with no slides'); 1954 return; 1955 } 1956 1957 const isPackagesSlider = this.slider.classList.contains('packages-slider') || 1958 this.slider.classList.contains('custom-cards-slider'); 1959 1960 if (this.isInfiniteLoop) { 1961 this.handleInfiniteNextSlide(isPackagesSlider); 1962 } else { 1963 this.handleNormalNextSlide(isPackagesSlider); 1964 } 1965 1966 } catch (error) { 1967 console.error('ð¨ SLIDER CRASH: nextSlide failed:', error); 1968 this.handleNavigationError(error, 'nextSlide'); 1969 } 1970 } 1971 1972 /** 1973 * Handle infinite loop next slide logic 1974 */ 1975 handleInfiniteNextSlide(isPackagesSlider) { 1976 try { 1977 this.currentSlide++; 1978 this.updateSlider(); 1979 1980 // Check if we need to jump back to the beginning 1981 if (isPackagesSlider) { 1982 const maxSlideIndex = this.originalSlideCount + this.cloneCount; 1983 if (this.currentSlide >= maxSlideIndex) { 1984 this.handleInfiniteLoopReset('next'); 1985 } 1986 } else { 1987 if (this.currentSlide >= this.originalSlideCount + this.cloneCount) { 1988 this.handleInfiniteLoopReset('next'); 1989 } 1990 } 1991 } catch (error) { 1992 console.error('â ï¸ Infinite next slide error:', error); 1993 throw error; 1994 } 1995 } 1996 1997 /** 1998 * Handle normal next slide logic 1999 */ 2000 handleNormalNextSlide(isPackagesSlider) { 2001 try { 2002 if (isPackagesSlider) {
2003 const slideWidth = this.getSlideWidth(); 2004 const containerWidth = this.slider.offsetWidth || 800; 2005 const slidesPerView = Math.floor(containerWidth / slideWidth); 2006 const maxSlide = Math.max(0, this.slideCount - slidesPerView); 2007 2008 if (this.currentSlide < maxSlide) { 2009 this.currentSlide++; 2010 } else { 2011 this.currentSlide = 0; // Loop back to start 2012 } 2013 } else { 2014 this.currentSlide = (this.currentSlide + 1) % this.slideCount; 2015 } 2016 this.updateSlider(); 2017 } catch (error) { 2018 console.error('â ï¸ Normal next slide error:', error); 2019 throw error; 2020 } 2021 } 2022 2023 /** 2024 * Handle infinite loop reset with timeout management 2025 */ 2026 handleInfiniteLoopReset(direction) { 2027 try { 2028 // Clear any existing infinite loop timeout to prevent conflicts 2029 if (this.infiniteLoopTimeoutId) { 2030 clearTimeout(this.infiniteLoopTimeoutId); 2031 this.activeTimeouts.delete(this.infiniteLoopTimeoutId); 2032 } 2033 2034 // Determine reset delay based on slider type - much faster for vertical sliders 2035 const resetDelay = this.isVertical ? 5 : 500; 2036 2037 this.infiniteLoopTimeoutId = this.safeSetTimeout(() => { 2038 try { 2039 if (this.isDestroyed) return; 2040 2041 this.sliderWrapper.style.transition = 'none'; 2042 2043 if (direction === 'next') { 2044 this.currentSlide = this.cloneCount; 2045 } else { 2046 this.currentSlide = this.originalSlideCount + this.cloneCount - 1; 2047 } 2048 2049 this.updateSliderInstantly(); 2050 2051 // Re-enable transition for next slide - faster for vertical sliders 2052 const transitionDelay = this.isVertical ? 5 : 50; 2053 this.safeSetTimeout(() => { 2054 if (!this.isDestroyed && this.sliderWrapper) { 2055 this.sliderWrapper.style.transition = this.isVertical ? 2056 'transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)' : 2057 ''; 2058 } 2059 }, transitionDelay, 'enable-transition'); 2060 2061 this.infiniteLoopTimeoutId = null; 2062 2063 } catch (resetError) { 2064 console.error('â ï¸ Infinite loop reset error:', resetError); 2065 // Recovery: reset styles and position 2066 if (this.sliderWrapper) { 2067 this.sliderWrapper.style.transition = ''; 2068 } 2069 this.currentSlide = this.cloneCount; 2070 this.updateSlider(); 2071 this.infiniteLoopTimeoutId = null; 2072 } 2073 }, resetDelay, 'infinite-loop-reset'); 2074 2075 } catch (error) { 2076 console.error('â ï¸ Infinite loop reset setup error:', error); 2077 } 2078 } 2079 2080 prevSlide() { 2081 try { 2082 if (this.isTransitioning || this.isDestroyed) { 2083 return; 2084 } 2085 2086 if (!this.slides || this.slides.length === 0) { 2087 console.warn('â ï¸ prevSlide called with no slides'); 2088 return; 2089 } 2090 2091 const isPackagesSlider = this.slider.classList.contains('packages-slider') || 2092 this.slider.classList.contains('custom-cards-slider'); 2093 2094 if (this.isInfiniteLoop) { 2095 this.handleInfinitePrevSlide(isPackagesSlider); 2096 } else { 2097 this.handleNormalPrevSlide(isPackagesSlider); 2098 } 2099 2100 } catch (error) { 2101 console.error('ð¨ SLIDER CRASH: prevSlide failed:', error); 2102 this.handleNavigationError(error, 'prevSlide'); 2103 } 2104 } 2105 2106 /** 2107 * Handle infinite loop prev slide logic 2108 */ 2109 handleInfinitePrevSlide(isPackagesSlider) { 2110 try { 2111 this.currentSlide--; 2112 this.updateSlider(); 2113 2114 // Check if we've reached the cloned slides at the beginning
2115 if (this.currentSlide < this.cloneCount) { 2116 this.handleInfiniteLoopReset('prev'); 2117 } 2118 } catch (error) { 2119 console.error('â ï¸ Infinite prev slide error:', error); 2120 throw error; 2121 } 2122 } 2123 2124 /** 2125 * Handle normal prev slide logic 2126 */ 2127 handleNormalPrevSlide(isPackagesSlider) { 2128 try { 2129 if (isPackagesSlider) { 2130 const slideWidth = this.getSlideWidth(); 2131 const containerWidth = this.slider.offsetWidth || 800; 2132 const slidesPerView = Math.floor(containerWidth / slideWidth); 2133 const maxSlide = Math.max(0, this.slideCount - slidesPerView); 2134 2135 if (this.currentSlide > 0) { 2136 this.currentSlide--; 2137 } else { 2138 this.currentSlide = maxSlide; // Loop to end 2139 } 2140 } else { 2141 this.currentSlide = (this.currentSlide - 1 + this.slideCount) % this.slideCount; 2142 } 2143 this.updateSlider(); 2144 } catch (error) { 2145 console.error('â ï¸ Normal prev slide error:', error); 2146 throw error; 2147 } 2148 } 2149 2150 /** 2151 * Handle navigation errors consistently 2152 */ 2153 handleNavigationError(error, method) { 2154 console.error(`ð¨ SLIDER CRASH: State during ${method} crash:`, { 2155 currentSlide: this.currentSlide, 2156 slideCount: this.slideCount, 2157 isInfiniteLoop: this.isInfiniteLoop, 2158 isTransitioning: this.isTransitioning, 2159 isDestroyed: this.isDestroyed 2160 }); 2161 2162 // Force reset transition state and attempt recovery 2163 this.isTransitioning = false; 2164 try { 2165 this.updateSlider(); 2166 } catch (recoveryError) { 2167 console.error(`ð¨ SLIDER CRASH: ${method} recovery attempt failed:`, recoveryError); 2168 this.handleCriticalError(recoveryError); 2169 } 2170 } 2171 2172 goToSlide(index) { 2173 try { 2174 if (this.isTransitioning) return; 2175 2176 // Stop auto-slide temporarily when manually navigating 2177 this.stopAutoSlide(); 2178 2179 if (this.isInfiniteLoop) { 2180 // Convert logical index to actual slide position (accounting for clones) 2181 this.currentSlide = index + this.cloneCount; 2182 } else { 2183 if (index === this.currentSlide) return; 2184 this.currentSlide = index; 2185 } 2186 2187 this.updateSlider(); 2188 2189 // Restart auto-slide after navigation 2190 if (this.autoSlideTimeoutId) { 2191 clearTimeout(this.autoSlideTimeoutId); 2192 } 2193 2194 this.autoSlideTimeoutId = setTimeout(() => { 2195 this.autoSlideTimeoutId = null; 2196 this.startAutoSlide(); 2197 }, 1000); // Wait 1 second before resuming auto-slide 2198 } catch (error) { 2199 console.error('ð¨ SLIDER CRASH: goToSlide failed:', error); 2200 console.error('ð¨ SLIDER CRASH: State during goToSlide crash:', { 2201 targetIndex: index, 2202 currentSlide: this.currentSlide, 2203 slideCount: this.slideCount, 2204 isInfiniteLoop: this.isInfiniteLoop 2205 }); 2206 // Force reset transition state 2207 this.isTransitioning = false; 2208 } 2209 } 2210 2211 updateSliderInstantly() { 2212 // Same as updateSlider but without transition effects 2213 if (!this.sliderWrapper) return; 2214 2215 const isVerticalSlider = this.slider.classList.contains('vertical-slider'); 2216 const isPackagesSlider = this.slider.classList.contains('packages-slider') || 2217 this.slider.classList.contains('custom-cards-slider'); 2218 2219 if (isVerticalSlider) { 2220 // Handle vertical slider transforms 2221 const slideHeight = this.getSlideHeight(); 2222 const containerHeight = this.slider.offsetHeight || 600; 2223 2224 // For centered vertical sliders, adjust the transform to account for centering 2225 let translateY = -this.currentSlide * slideHeight; 2226 2227 // If centered, add offset to keep active slide in center 2228 if (this.isCentered) { 2229 const centerOffset = (containerHeight - slideHeight) / 2; 2230 translateY += centerOffset; 2231 } 2232 2233 this.sliderWrapper.style.transform = `translateY(${translateY}px)`; 2234 } else if (isPackagesSlider) {
2235 const slideWidth = this.getSlideWidth(); 2236 let translateX; 2237 2238 if (this.isCentered) { 2239 const containerWidth = this.slider.offsetWidth; 2240 this.centerOffset = (containerWidth - slideWidth) / 2; 2241 translateX = -this.currentSlide * slideWidth + this.centerOffset; 2242 } else { 2243 translateX = -this.currentSlide * slideWidth; 2244 } 2245 2246 this.sliderWrapper.style.transform = `translateX(${translateX}px)`; 2247 } else { 2248 const translateX = -this.currentSlide * 100; 2249 this.sliderWrapper.style.transform = `translateX(${translateX}%)`; 2250 } 2251 2252 // Update dots (only for actual slides, not clones) 2253 if (this.dots) { 2254 this.dots.forEach((dot, index) => { 2255 let activeIndex = this.currentSlide; 2256 if (this.isInfiniteLoop) { 2257 activeIndex = (this.currentSlide - this.cloneCount + this.originalSlideCount) % this.originalSlideCount; 2258 } 2259 dot.classList.toggle('active', index === activeIndex); 2260 }); 2261 } 2262 2263 // Update active slide class for centered mode 2264 if (this.isCentered) { 2265 this.slides.forEach((slide, index) => { 2266 slide.classList.toggle('active', index === this.currentSlide); 2267 }); 2268 } 2269 } 2270 2271 startAutoSlide() { 2272 try { 2273 if (this.slideCount <= 1) return; 2274 this.stopAutoSlide(); 2275 2276 this.autoSlideInterval = setInterval(() => { 2277 try { 2278 this.nextSlide(); 2279 } catch (error) { 2280 console.error('ð¨ SLIDER CRASH: Auto-slide nextSlide failed:', error); 2281 // Stop auto-slide on repeated failures 2282 this.stopAutoSlide(); 2283 } 2284 }, this.autoSlideTime); 2285 } catch (error) { 2286 console.error('ð¨ SLIDER CRASH: startAutoSlide failed:', error); 2287 console.error('ð¨ SLIDER CRASH: Auto-slide state:', { 2288 slideCount: this.slideCount, 2289 autoSlideTime: this.autoSlideTime, 2290 currentSlide: this.currentSlide 2291 }); 2292 } 2293 } 2294 2295 stopAutoSlide() { 2296 if (this.autoSlideInterval) { 2297 clearInterval(this.autoSlideInterval); 2298 this.autoSlideInterval = null; 2299 } 2300 } 2301 2302 handleTouchStart(e) { 2303 try { 2304 // Don't start drag if touching links or buttons 2305 if (e.target.closest('a') || 2306 e.target.tagName === 'A' || 2307 e.target.closest('button') || 2308 e.target.tagName === 'BUTTON' || 2309 e.target.classList.contains('dot') || 2310 e.target.classList.contains('slider-btn')) { 2311 return; 2312 } 2313 const isVerticalSlider = this.slider.classList.contains('vertical-slider'); 2314 if (isVerticalSlider) { 2315 this.startDrag(e.touches[0].clientY); 2316 } else { 2317 this.startDrag(e.touches[0].clientX); 2318 } 2319 } catch (error) { 2320 console.error('ð¨ SLIDER CRASH: handleTouchStart failed:', error); 2321 console.error('ð¨ SLIDER CRASH: Touch event details:', { 2322 touchesLength: e?.touches?.length, 2323 targetElement: e?.target?.tagName, 2324 targetClass: e?.target?.className 2325 }); 2326 } 2327 } 2328 2329 handleTouchMove(e) { 2330 try { 2331 if (!this.isDragging) return; 2332 e.preventDefault(); 2333 const isVerticalSlider = this.slider.classList.contains('vertical-slider'); 2334 if (isVerticalSlider) { 2335 this.updateDrag(e.touches[0].clientY); 2336 } else { 2337 this.updateDrag(e.touches[0].clientX); 2338 } 2339 } catch (error) { 2340 console.error('ð¨ SLIDER CRASH: handleTouchMove failed:', error); 2341 this.isDragging = false; 2342 } 2343 } 2344 2345 handleTouchEnd(e) { 2346 try { 2347 if (!this.isDragging) return; 2348 const isVerticalSlider = this.slider.classList.contains('vertical-slider'); 2349 if (isVerticalSlider) { 2350 this.endDrag(e.changedTouches[0].clientY); 2351 } else { 2352 this.endDrag(e.changedTouches[0].clientX); 2353 } 2354 } catch (error) { 2355 console.error('ð¨ SLIDER CRASH: handleTouchEnd failed:', error); 2356 this.isDragging = false;
2357 } 2358 } 2359 2360 handleMouseStart(e) { 2361 try { 2362 // Don't start drag if clicking on dots, buttons, or links 2363 if (e.target.classList.contains('dot') || 2364 e.target.classList.contains('slider-btn') || 2365 e.target.closest('.slider-dots') || 2366 e.target.closest('.slider-navigation') || 2367 e.target.closest('a') || // Allow clicks on links 2368 e.target.tagName === 'A' || // Direct anchor tags 2369 e.target.closest('button') || // Allow clicks on buttons 2370 e.target.tagName === 'BUTTON') { // Direct button tags 2371 return; 2372 } 2373 2374 e.preventDefault(); 2375 const isVerticalSlider = this.slider.classList.contains('vertical-slider'); 2376 if (isVerticalSlider) { 2377 this.startDrag(e.clientY); 2378 } else { 2379 this.startDrag(e.clientX); 2380 } 2381 } catch (error) { 2382 console.error('ð¨ SLIDER CRASH: handleMouseStart failed:', error); 2383 console.error('ð¨ SLIDER CRASH: Mouse event details:', { 2384 clientX: e?.clientX, 2385 targetElement: e?.target?.tagName, 2386 targetClass: e?.target?.className 2387 }); 2388 } 2389 } 2390 2391 handleMouseMove(e) { 2392 try { 2393 if (!this.isDragging) return; 2394 e.preventDefault(); 2395 const isVerticalSlider = this.slider.classList.contains('vertical-slider'); 2396 if (isVerticalSlider) { 2397 this.updateDrag(e.clientY); 2398 } else { 2399 this.updateDrag(e.clientX); 2400 } 2401 } catch (error) { 2402 console.error('ð¨ SLIDER CRASH: handleMouseMove failed:', error); 2403 this.isDragging = false; 2404 } 2405 } 2406 2407 handleMouseEnd(e) { 2408 try { 2409 if (!this.isDragging) return; 2410 const isVerticalSlider = this.slider.classList.contains('vertical-slider'); 2411 if (isVerticalSlider) { 2412 this.endDrag(e.clientY); 2413 } else { 2414 this.endDrag(e.clientX); 2415 } 2416 } catch (error) { 2417 console.error('ð¨ SLIDER CRASH: handleMouseEnd failed:', error); 2418 this.isDragging = false; 2419 } 2420 } 2421 2422 startDrag(clientPos) { 2423 try { 2424 this.isDragging = true; 2425 const isVerticalSlider = this.slider.classList.contains('vertical-slider'); 2426 2427 if (isVerticalSlider) { 2428 this.startY = clientPos; 2429 // Get current transform value for vertical slider 2430 const slideHeight = this.getSlideHeight(); 2431 this.startTransform = -this.currentSlide * slideHeight; 2432 } else { 2433 this.startX = clientPos; 2434 // Get current transform value for horizontal slider 2435 const isPackagesSlider = this.slider.classList.contains('packages-slider') || 2436 this.slider.classList.contains('custom-cards-slider'); 2437 2438 if (isPackagesSlider) { 2439 const slideWidth = this.getSlideWidth(); 2440 this.startTransform = -this.currentSlide * slideWidth; 2441 } else { 2442 this.startTransform = -this.currentSlide * 100; 2443 } 2444 } 2445 2446 this.stopAutoSlide(); 2447 this.currentTransform = this.startTransform; 2448 2449 // Disable CSS transitions during drag 2450 this.sliderWrapper.style.transition = 'none'; 2451 2452 // Add dragging class for cursor changes 2453 this.slider.classList.add('dragging'); 2454 } catch (error) { 2455 console.error('ð¨ SLIDER CRASH: startDrag failed:', error); 2456 this.isDragging = false; 2457 } 2458 } 2459 2460 updateDrag(clientPos) { 2461 try { 2462 if (!this.isDragging) return; 2463 2464 const isVerticalSlider = this.slider.classList.contains('vertical-slider'); 2465 2466 if (isVerticalSlider) { 2467 // Handle vertical dragging 2468 const deltaY = clientPos - this.startY; 2469 this.currentTransform = this.startTransform + deltaY; 2470 this.sliderWrapper.style.transform = `translateY(${this.currentTransform}px)`; 2471 } else { 2472 // Handle horizontal dragging 2473 const deltaX = clientPos - this.startX; 2474 const isPackagesSlider = this.slider.classList.contains('packages-slider') || 2475 this.slider.classList.contains('custom-cards-slider'); 2476 2477 if (isPackagesSlider) { 2478 // For packages slider, translate by pixel amount 2479 this.currentTransform = this.startTransform + deltaX; 2480 this.sliderWrapper.style.transform = `translateX(${this.currentTransform}px)`; 2481 } else { 2482 // For banner slider, convert pixels to percentage 2483 const containerWidth = this.slider.offsetWidth; 2484 const deltaPercentage = (deltaX / containerWidth) * 100; 2485 this.currentTransform = this.startTransform + deltaPercentage; 2486 this.sliderWrapper.style.transform = `translateX(${this.currentTransform}%)`; 2487 } 2488 } 2489 } catch (error) { 2490 console.error('ð¨ SLIDER CRASH: updateDrag failed:', error); 2491 this.isDragging = false;
2492 } 2493 } 2494 2495 endDrag(clientPos) { 2496 try { 2497 if (!this.isDragging) return; 2498 2499 this.isDragging = false; 2500 const isVerticalSlider = this.slider.classList.contains('vertical-slider'); 2501 2502 // Re-enable CSS transitions 2503 this.sliderWrapper.style.transition = ''; 2504 this.slider.classList.remove('dragging'); 2505 2506 if (isVerticalSlider) { 2507 // Handle vertical slider direction 2508 this.endY = clientPos; 2509 const deltaY = this.startY - this.endY; 2510 const threshold = 50; // Minimum drag distance to trigger slide change 2511 2512 if (Math.abs(deltaY) > threshold) { 2513 if (deltaY > 0) { 2514 // Swiped up = next slide (move from bottom to top) 2515 this.nextSlide(); 2516 } else { 2517 // Swiped down = previous slide 2518 this.prevSlide(); 2519 } 2520 } else { 2521 // Snap back to current slide 2522 this.updateSlider(); 2523 } 2524 2525 // Reset values for vertical slider 2526 this.startY = 0; 2527 this.endY = 0; 2528 } else { 2529 // Handle horizontal slider (existing logic) 2530 this.endX = clientPos; 2531 const deltaX = this.startX - this.endX; 2532 2533 const isPackagesSlider = this.slider.classList.contains('packages-slider') || 2534 this.slider.classList.contains('custom-cards-slider'); 2535 2536 if (this.isCentered && isPackagesSlider) { 2537 // For centered mode, snap to the nearest slide 2538 const slideWidth = this.getSlideWidth(); 2539 const dragDistance = Math.abs(deltaX); 2540 const threshold = slideWidth * 0.2; // 20% of slide width to trigger change 2541 2542 if (dragDistance > threshold) { 2543 if (deltaX > 0) { 2544 this.nextSlide(); 2545 } else { 2546 this.prevSlide(); 2547 } 2548 } else { 2549 // Snap back to current slide 2550 this.updateSlider(); 2551 } 2552 } else { 2553 // Standard behavior 2554 const threshold = 50; // Minimum drag distance to trigger slide change 2555 2556 if (Math.abs(deltaX) > threshold) { 2557 if (deltaX > 0) { 2558 this.nextSlide(); 2559 } else { 2560 this.prevSlide(); 2561 } 2562 } else { 2563 // Snap back to current slide 2564 this.updateSlider(); 2565 } 2566 } 2567 2568 // Reset values for horizontal slider 2569 this.startX = 0; 2570 this.endX = 0; 2571 } 2572 2573 this.startTransform = 0; 2574 this.currentTransform = 0; 2575 2576 // Restart auto-slide 2577 this.startAutoSlide(); 2578 } catch (error) { 2579 console.error('ð¨ SLIDER CRASH: endDrag failed:', error); 2580 // Force cleanup on error 2581 this.isDragging = false; 2582 this.slider.classList.remove('dragging'); 2583 this.sliderWrapper.style.transition = ''; 2584 this.startX = 0; 2585 this.endX = 0; 2586 this.startY = 0; 2587 this.endY = 0; 2588 this.startTransform = 0; 2589 this.currentTransform = 0; 2590 this.startAutoSlide(); 2591 } 2592 } 2593 2594 handleKeyboard(e) { 2595 try { 2596 switch (e.key) { 2597 case 'ArrowLeft': 2598 e.preventDefault(); 2599 this.prevSlide(); 2600 break; 2601 case 'ArrowRight': 2602 e.preventDefault(); 2603 this.nextSlide(); 2604 break; 2605 case ' ': 2606 e.preventDefault(); 2607 if (this.autoSlideInterval) { 2608 this.stopAutoSlide(); 2609 } else { 2610 this.startAutoSlide(); 2611 } 2612 break; 2613 } 2614 } catch (error) { 2615 console.error('ð¨ SLIDER CRASH: handleKeyboard failed:', error); 2616 console.error('ð¨ SLIDER CRASH: Keyboard event details:', { 2617 key: e?.key, 2618 keyCode: e?.keyCode, 2619 autoSlideActive: !!this.autoSlideInterval 2620 }); 2621 } 2622 } 2623 2624 /** 2625 * Enhanced destroy method with comprehensive cleanup and memory leak prevention 2626 */ 2627 destroy() { 2628 try { 2629 console.info('â¹ï¸ Destroying slider instance'); 2630 2631 // Prevent multiple destroy calls 2632 if (this.isDestroyed) { 2633 console.warn('â ï¸ Slider already destroyed'); 2634 return; 2635 } 2636 2637 this.isDestroyed = true; 2638 2639 // Stop all auto-slide activities 2640 this.stopAutoSlide(); 2641 2642 // Clear ALL timeouts comprehensively 2643 this.clearAllTimeouts(); 2644 2645 // Reset transition flag to prevent getting stuck 2646 this.isTransitioning = false;
2647 2648 // Enhanced intersection observer cleanup 2649 if (this.imageObserver) { 2650 try { 2651 this.imageObserver.disconnect(); 2652 this.imageObserver = null; 2653 } catch (observerError) { 2654 console.error('â ï¸ Error disconnecting image observer:', observerError); 2655 } 2656 } 2657 2658 // Clear image caches and sets 2659 if (this.loadedImages) { 2660 this.loadedImages.clear(); 2661 this.loadedImages = null; 2662 } 2663 if (this.loadingImages) { 2664 this.loadingImages.clear(); 2665 this.loadingImages = null; 2666 } 2667 if (this.priorityImages) { 2668 this.priorityImages.clear(); 2669 this.priorityImages = null; 2670 } 2671 if (this.failedImages) { 2672 this.failedImages.clear(); 2673 this.failedImages = null; 2674 } 2675 2676 // Reset all initialization flags 2677 this.infiniteLoopSetup = false; 2678 this.navigationCreated = false; 2679 this.dotsCreated = false; 2680 2681 // Enhanced DOM cleanup with error handling 2682 this.cleanupDOMElements(); 2683 2684 // Enhanced event listener cleanup 2685 this.removeAllEventListeners(); 2686 2687 // Reset slider wrapper styles safely 2688 if (this.sliderWrapper && this.sliderWrapper.style) { 2689 try { 2690 this.sliderWrapper.style.transform = ''; 2691 this.sliderWrapper.style.transition = ''; 2692 } catch (styleError) { 2693 console.error('â ï¸ Error resetting wrapper styles:', styleError); 2694 } 2695 } 2696 2697 // Reset slider element safely 2698 if (this.slider) { 2699 try { 2700 this.slider.classList.remove('initialized', 'dragging'); 2701 this.slider.removeAttribute('tabindex'); 2702 this.slider.setAttribute('data-slider-status', 'destroyed'); 2703 } catch (sliderError) { 2704 console.error('â ï¸ Error resetting slider element:', sliderError); 2705 } 2706 } 2707 2708 // Remove from global instance tracking 2709 if (typeof window !== 'undefined' && window.sliderInstances && window.sliderInstances.has && window.sliderInstances.has(this.slider)) { 2710 try { 2711 window.sliderInstances.delete(this.slider); 2712 } catch (globalError) { 2713 console.error('â ï¸ Error removing from global instances:', globalError); 2714 } 2715 } 2716 2717 // Clear references to DOM elements 2718 this.slider = null; 2719 this.sliderWrapper = null; 2720 this.slides = null; 2721 this.dots = null; 2722 this.prevBtn = null; 2723 this.nextBtn = null; 2724 2725 console.info('â Slider destroyed successfully'); 2726 2727 } catch (error) { 2728 console.error('ð¨ Error during slider destruction:', error); 2729 // Force cleanup on error 2730 this.isDestroyed = true; 2731 this.isTransitioning = false; 2732 } 2733 } 2734 2735 /** 2736 * Clean up DOM elements created by the slider 2737 */ 2738 cleanupDOMElements() { 2739 try { 2740 if (!this.slider) return; 2741 2742 // Remove navigation elements 2743 const navigations = this.slider.querySelectorAll('.slider-navigation'); 2744 navigations.forEach(nav => { 2745 try { 2746 nav.remove(); 2747 } catch (navError) { 2748 console.error('â ï¸ Error removing navigation:', navError); 2749 } 2750 }); 2751 2752 // Remove dot containers 2753 const dotContainers = this.slider.querySelectorAll('.slider-dots'); 2754 dotContainers.forEach(dots => { 2755 try { 2756 dots.remove(); 2757 } catch (dotError) { 2758 console.error('â ï¸ Error removing dots:', dotError); 2759 } 2760 }); 2761 2762 // Remove cloned slides 2763 const clonedSlides = this.slider.querySelectorAll('.slide-clone');
2764 clonedSlides.forEach(clone => { 2765 try { 2766 clone.remove(); 2767 } catch (cloneError) { 2768 console.error('â ï¸ Error removing clone:', cloneError); 2769 } 2770 }); 2771 2772 } catch (error) { 2773 console.error('â ï¸ DOM cleanup error:', error); 2774 } 2775 } 2776 2777 /** 2778 * Remove all event listeners comprehensively 2779 */ 2780 removeAllEventListeners() { 2781 try { 2782 // Remove button event listeners 2783 if (this.prevBtn) { 2784 this.prevBtn.removeEventListener('click', this.prevSlideHandler); 2785 } 2786 if (this.nextBtn) { 2787 this.nextBtn.removeEventListener('click', this.nextSlideHandler); 2788 } 2789 2790 // Remove dot event listeners 2791 if (this.dots) { 2792 this.dots.forEach((dot, index) => { 2793 if (dot && dot.removeEventListener) { 2794 dot.removeEventListener('click', this.dotClickHandlers?.[index]); 2795 dot.removeEventListener('mousedown', this.dotMousedownHandlers?.[index]); 2796 } 2797 }); 2798 } 2799 2800 // Remove slider event listeners 2801 if (this.slider) { 2802 this.slider.removeEventListener('mouseenter', this.mouseEnterHandler); 2803 this.slider.removeEventListener('mouseleave', this.mouseLeaveHandler); 2804 this.slider.removeEventListener('touchstart', this.touchStartHandler); 2805 this.slider.removeEventListener('touchmove', this.touchMoveHandler); 2806 this.slider.removeEventListener('touchend', this.touchEndHandler); 2807 this.slider.removeEventListener('mousedown', this.mouseDownHandler); 2808 this.slider.removeEventListener('mousemove', this.mouseMoveHandler); 2809 this.slider.removeEventListener('mouseup', this.mouseUpHandler); 2810 this.slider.removeEventListener('mouseleave', this.mouseLeaveHandler); 2811 this.slider.removeEventListener('keydown', this.keydownHandler); 2812 } 2813 2814 // Clear handler references 2815 this.prevSlideHandler = null; 2816 this.nextSlideHandler = null; 2817 this.dotClickHandlers = null; 2818 this.dotMousedownHandlers = null; 2819 this.mouseEnterHandler = null; 2820 this.mouseLeaveHandler = null; 2821 this.touchStartHandler = null; 2822 this.touchMoveHandler = null; 2823 this.touchEndHandler = null; 2824 this.mouseDownHandler = null; 2825 this.mouseMoveHandler = null; 2826 this.mouseUpHandler = null; 2827 this.keydownHandler = null; 2828 2829 } catch (error) { 2830 console.error('â ï¸ Event listener cleanup error:', error); 2831 } 2832 } 2833 2834 /** 2835 * Get performance metrics for debugging 2836 */ 2837 getPerformanceMetrics() { 2838 return { 2839 totalSlides: this.slideCount, 2840 loadedImages: this.loadedImages.size, 2841 loadingImages: this.loadingImages.size, 2842 priorityImages: this.priorityImages.size, 2843 lazyLoadEnabled: this.lazyLoadEnabled, 2844 isInViewport: this.isInViewport, 2845 initialized: this.initialized, 2846 currentSlide: this.currentSlide 2847 }; 2848 } 2849} 2850 2851// Auto-initialize all custom sliders with lazy loading optimization 2852function initCustomSliders() { 2853 try { 2854 // Find all sliders (check both initialized and uninitialized) 2855 const allSliders = document.querySelectorAll('.custom-slider'); 2856 const sliderInstances = []; 2857 2858 2859 2860 // COMPREHENSIVE CLEANUP - Force destroy existing instances and clean DOM 2861 allSliders.forEach((slider, index) => { 2862 2863 2864 // Force destroy existing instance if it exists 2865 if (window.sliderInstances && window.sliderInstances.has(slider)) { 2866 2867 const existingInstance = window.sliderInstances.get(slider); 2868 existingInstance.destroy(); 2869 window.sliderInstances.delete(slider); 2870 } 2871 2872 // Force remove initialized class 2873 slider.classList.remove('initialized'); 2874 2875 // AGGRESSIVE DOM CLEANUP - Remove ALL generated elements 2876 const navigations = slider.querySelectorAll('.slider-navigation'); 2877 const dotContainers = slider.querySelectorAll('.slider-dots'); 2878 const clonedSlides = slider.querySelectorAll('.slide-clone'); 2879 2880 navigations.forEach(nav => { 2881 2882 nav.remove(); 2883 }); 2884
2885 dotContainers.forEach(dots => { 2886 2887 dots.remove(); 2888 }); 2889 2890 clonedSlides.forEach(clone => { 2891 2892 clone.remove(); 2893 }); 2894 2895 // Reset wrapper styles 2896 const wrapper = slider.querySelector('.slider-wrapper'); 2897 if (wrapper) { 2898 wrapper.style.transform = ''; 2899 wrapper.style.transition = ''; 2900 } 2901 2902 // Remove slider-specific attributes and classes 2903 slider.removeAttribute('tabindex'); 2904 slider.classList.remove('dragging'); 2905 2906 2907 }); 2908 2909 // Wait a moment for DOM cleanup to complete 2910 setTimeout(() => { 2911 // Now find sliders that need initialization with enhanced validation 2912 const potentialSliders = document.querySelectorAll('.custom-slider:not(.initialized)'); 2913 const validSliders = []; 2914 2915 // Filter and validate slider elements 2916 potentialSliders.forEach((element, index) => { 2917 try { 2918 if (!element) { 2919 console.warn(`â ï¸ Found null/undefined slider at index ${index}`); 2920 return; 2921 } 2922 2923 // Check if it's a valid HTMLElement (with cross-context support) 2924 const isValidHTMLElement = ( 2925 element instanceof HTMLElement || 2926 ( 2927 element && 2928 typeof element === 'object' && 2929 element.nodeType === 1 && // ELEMENT_NODE 2930 (typeof element.tagName === 'string' || typeof element.nodeName === 'string') && 2931 typeof element.classList === 'object' && 2932 element.classList !== null && 2933 typeof element.querySelector === 'function' 2934 ) 2935 ); 2936 2937 if (!isValidHTMLElement) { 2938 console.error(`ð¨ Invalid slider element at index ${index}:`, { 2939 type: typeof element, 2940 nodeType: element?.nodeType, 2941 nodeName: element?.nodeName, 2942 constructor: element?.constructor?.name, 2943 element: element 2944 }); 2945 return; 2946 } 2947 2948 // Additional validation checks 2949 if (!element.classList || typeof element.classList.contains !== 'function') { 2950 console.error(`ð¨ Slider element missing classList at index ${index}:`, element); 2951 return; 2952 } 2953 2954 // Check for required wrapper 2955 const wrapper = element.querySelector('.slider-wrapper'); 2956 if (!wrapper) { 2957 console.warn(`â ï¸ Slider at index ${index} missing .slider-wrapper, skipping initialization`); 2958 return; 2959 } 2960 2961 // Check for slides 2962 const slides = element.querySelectorAll('.slide'); 2963 if (slides.length === 0) { 2964 console.info(`â¹ï¸ Slider at index ${index} has no slides, but will still initialize`); 2965 } 2966 2967 validSliders.push(element); 2968 2969 } catch (validationError) { 2970 console.error(`ð¨ Error validating slider element at index ${index}:`, validationError); 2971 } 2972 }); 2973 2974 console.info(`â¹ï¸ Found ${validSliders.length} valid sliders out of ${potentialSliders.length} potential sliders`); 2975 2976 // Lazy initialization using Intersection Observer for better performance 2977 if ('IntersectionObserver' in window && validSliders.length > 0) {
2978 const initObserver = new IntersectionObserver((entries) => { 2979 entries.forEach(entry => { 2980 if (entry.isIntersecting) { 2981 const slider = entry.target; 2982 2983 // Double-check element validity before initialization (cross-context support) 2984 const isValidElement = ( 2985 slider instanceof HTMLElement || 2986 ( 2987 slider && 2988 typeof slider === 'object' && 2989 slider.nodeType === 1 && 2990 (typeof slider.tagName === 'string' || typeof slider.nodeName === 'string') && 2991 typeof slider.classList === 'object' && 2992 slider.classList !== null && 2993 typeof slider.querySelector === 'function' 2994 ) 2995 ); 2996 2997 if (!isValidElement) { 2998 console.error('ð¨ Non-HTMLElement detected during intersection:', slider); 2999 return; 3000 } 3001 3002 try { 3003 const instance = new CustomSlider(slider); 3004 sliderInstances.push(instance); 3005 window.sliderInstances.set(slider, instance); 3006 slider.classList.add('initialized'); 3007 initObserver.unobserve(slider); // Stop observing once initialized 3008 3009 console.info('â Slider initialized successfully via intersection observer'); 3010 } catch (error) { 3011 console.error('ð¨ Failed to initialize slider via intersection observer:', error); 3012 console.error('ð¨ Problematic slider element:', { 3013 element: slider, 3014 className: slider?.className, 3015 tagName: slider?.tagName, 3016 isHTMLElement: slider instanceof HTMLElement 3017 }); 3018 } 3019 } 3020 }); 3021 }, { 3022 root: null, 3023 rootMargin: '100px', // Start loading 100px before entering viewport 3024 threshold: 0.1 3025 }); 3026 3027 // Observe all valid sliders for lazy initialization 3028 validSliders.forEach((slider, index) => { 3029 try { 3030 // Final validation before observing (cross-context support) 3031 const isValidElement = ( 3032 slider instanceof HTMLElement || 3033 ( 3034 slider && 3035 typeof slider === 'object' && 3036 slider.nodeType === 1 && 3037 (typeof slider.tagName === 'string' || typeof slider.nodeName === 'string') && 3038 typeof slider.classList === 'object' && 3039 slider.classList !== null && 3040 typeof slider.querySelector === 'function' 3041 ) 3042 ); 3043 3044 if (!isValidElement) { 3045 console.error(`ð¨ Invalid element detected before observing at index ${index}:`, slider); 3046 return; 3047 } 3048 3049 // Check if slider is already in viewport 3050 const rect = slider.getBoundingClientRect(); 3051 const isInViewport = ( 3052 rect.top >= -100 && 3053 rect.left >= -100 && 3054 rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) + 100 && 3055 rect.right <= (window.innerWidth || document.documentElement.clientWidth) + 100 3056 ); 3057 3058 if (isInViewport) { 3059 // Initialize immediately if already visible 3060 console.info(`â¹ï¸ Initializing visible slider ${index + 1}/${validSliders.length}`); 3061 try { 3062 const instance = new CustomSlider(slider); 3063 sliderInstances.push(instance); 3064 window.sliderInstances.set(slider, instance); 3065 slider.classList.add('initialized'); 3066 console.info('â Visible slider initialized successfully'); 3067 } catch (error) { 3068 console.error('ð¨ Failed to initialize visible slider:', error); 3069 console.error('ð¨ Problematic slider element:', { 3070 element: slider, 3071 className: slider?.className, 3072 tagName: slider?.tagName, 3073 isHTMLElement: slider instanceof HTMLElement 3074 }); 3075 } 3076 } else { 3077 // Use intersection observer for off-screen sliders
3078 initObserver.observe(slider); 3079 } 3080 } catch (observeError) { 3081 console.error(`ð¨ Error setting up observation for slider ${index}:`, observeError); 3082 } 3083 }); 3084 } else { 3085 // Fallback: Initialize all sliders immediately if no Intersection Observer 3086 console.info('â¹ï¸ IntersectionObserver not available, initializing all sliders immediately'); 3087 validSliders.forEach((slider, index) => { 3088 try { 3089 // Final validation before initialization (cross-context support) 3090 const isValidElement = ( 3091 slider instanceof HTMLElement || 3092 ( 3093 slider && 3094 typeof slider === 'object' && 3095 slider.nodeType === 1 && 3096 (typeof slider.tagName === 'string' || typeof slider.nodeName === 'string') && 3097 typeof slider.classList === 'object' && 3098 slider.classList !== null && 3099 typeof slider.querySelector === 'function' 3100 ) 3101 ); 3102 3103 if (!isValidElement) { 3104 console.error(`ð¨ Invalid element detected during fallback initialization at index ${index}:`, slider); 3105 return; 3106 } 3107 3108 console.info(`â¹ï¸ Initializing slider ${index + 1}/${validSliders.length}`); 3109 const instance = new CustomSlider(slider); 3110 sliderInstances.push(instance); 3111 window.sliderInstances.set(slider, instance); 3112 slider.classList.add('initialized'); 3113 console.info('â Slider initialized successfully via fallback'); 3114 } catch (error) { 3115 console.error(`ð¨ Failed to initialize slider ${index + 1}:`, error); 3116 console.error('ð¨ Problematic slider element:', { 3117 element: slider, 3118 className: slider?.className, 3119 tagName: slider?.tagName, 3120 isHTMLElement: slider instanceof HTMLElement 3121 }); 3122 } 3123 }); 3124 } 3125 }, 50); // Small delay to ensure DOM cleanup 3126 3127 } catch (error) { 3128 console.error('ð¨ SLIDER CRASH: initCustomSliders failed:', error); 3129 console.error('ð¨ SLIDER CRASH: Global initialization state:', { 3130 totalSliders: document.querySelectorAll('.custom-slider').length, 3131 uninitializedSliders: document.querySelectorAll('.custom-slider:not(.initialized)').length, 3132 hasIntersectionObserver: 'IntersectionObserver' in window 3133 }); 3134 return []; 3135 } 3136} 3137 3138// Global slider instance tracking 3139if (typeof window !== 'undefined') { 3140 window.sliderInstances = window.sliderInstances || new Map(); 3141} 3142 3143// Global function for manual initialization 3144window.initCustomSlider = function (element) { 3145 try { 3146 // Enhanced element validation (cross-context support) 3147 if (!element) { 3148 console.error('ð¨ initCustomSlider: No element provided'); 3149 return null; 3150 } 3151 3152 const isValidHTMLElement = ( 3153 element instanceof HTMLElement || 3154 ( 3155 element && 3156 typeof element === 'object' && 3157 element.nodeType === 1 && // ELEMENT_NODE 3158 (typeof element.tagName === 'string' || typeof element.nodeName === 'string') && 3159 typeof element.classList === 'object' && 3160 element.classList !== null && 3161 typeof element.querySelector === 'function' 3162 ) 3163 ); 3164 3165 if (!isValidHTMLElement) { 3166 console.error('ð¨ initCustomSlider: Invalid element provided:', { 3167 received: element, 3168 type: typeof element, 3169 nodeType: element?.nodeType, 3170 nodeName: element?.nodeName, 3171 constructor: element?.constructor?.name, 3172 isNode: element instanceof Node, 3173 isElement: element instanceof Element, 3174 isHTMLElement: element instanceof HTMLElement 3175 }); 3176 return null; 3177 } 3178 3179 // Check if slider already has an instance 3180 if (window.sliderInstances && window.sliderInstances.has(element)) { 3181 console.info('â¹ï¸ Destroying existing slider instance'); 3182 try { 3183 const existingInstance = window.sliderInstances.get(element); 3184 existingInstance.destroy(); 3185 window.sliderInstances.delete(element); 3186 } catch (destroyError) { 3187 console.error('â ï¸ Error destroying existing instance:', destroyError); 3188 } 3189 } 3190 3191 console.info('â¹ï¸ Creating new slider instance'); 3192 const instance = new CustomSlider(element); 3193 3194 if (window.sliderInstances) { 3195 window.sliderInstances.set(element, instance); 3196 } 3197 3198 console.info('â Manual slider initialization successful'); 3199 return instance; 3200 } catch (error) { 3201 console.error('ð¨ SLIDER CRASH: Manual initialization failed:', error); 3202 console.error('ð¨ SLIDER CRASH: Element details:', { 3203 element: element, 3204 className: element?.className, 3205 tagName: element?.tagName, 3206 isHTMLElement: element instanceof HTMLElement 3207 }); 3208 return null; 3209 } 3210}; 3211 3212// Global function to manually initialize all sliders (for debugging) 3213window.initAllSliders = function() { 3214 try { 3215 console.info('â¹ï¸ Manual initialization of all sliders requested'); 3216 3217 // Pre-validate environment 3218 if (typeof document === 'undefined') { 3219 console.error('ð¨ Document not available'); 3220 return []; 3221 } 3222 3223 if (!document.querySelectorAll) { 3224 console.error('ð¨ querySelectorAll not supported'); 3225 return []; 3226 } 3227 3228 return initCustomSliders(); 3229 } catch (error) { 3230 console.error('ð¨ SLIDER CRASH: Manual initialization of all sliders failed:', error); 3231 return []; 3232 } 3233}; 3234 3235// Global function to force reinitialize all sliders (destroys existing first) 3236window.reinitializeAllSliders = function() { 3237 try { 3238 console.info('â¹ï¸ Force reinitializing all sliders'); 3239 3240 // Pre-validate environment 3241 if (typeof document === 'undefined' || !document.querySelectorAll) { 3242 console.error('ð¨ DOM not available for reinitialization'); 3243 return []; 3244 } 3245 3246 // Force remove initialized class from all sliders to ensure reinit 3247 const allSliders = document.querySelectorAll('.custom-slider'); 3248 console.info(`â¹ï¸ Found ${allSliders.length} slider elements for reinitialization`); 3249 3250 let validatedCount = 0;
3251 allSliders.forEach((slider, index) => { 3252 try { 3253 // Validate element before processing (cross-context support) 3254 const isValidElement = ( 3255 slider instanceof HTMLElement || 3256 ( 3257 slider && 3258 typeof slider === 'object' && 3259 slider.nodeType === 1 && 3260 (typeof slider.tagName === 'string' || typeof slider.nodeName === 'string') && 3261 typeof slider.classList === 'object' && 3262 slider.classList !== null && 3263 typeof slider.querySelector === 'function' 3264 ) 3265 ); 3266 3267 if (!isValidElement) { 3268 console.warn(`â ï¸ Skipping invalid element at index ${index}:`, slider); 3269 return; 3270 } 3271 3272 validatedCount++; 3273 3274 if (window.sliderInstances && window.sliderInstances.has(slider)) { 3275 try { 3276 const existingInstance = window.sliderInstances.get(slider); 3277 existingInstance.destroy(); 3278 } catch (destroyError) { 3279 console.error(`â ï¸ Error destroying slider ${index}:`, destroyError); 3280 } 3281 } 3282 slider.classList.remove('initialized'); 3283 } catch (sliderError) { 3284 console.error(`ð¨ Error processing slider ${index} for reinitialization:`, sliderError); 3285 } 3286 }); 3287 3288 console.info(`â¹ï¸ Processed ${validatedCount} valid sliders for reinitialization`); 3289 3290 // Clear the entire instances map 3291 if (window.sliderInstances) { 3292 window.sliderInstances.clear(); 3293 } 3294 3295 return initCustomSliders(); 3296 } catch (error) { 3297 console.error('ð¨ SLIDER CRASH: Force reinitializing all sliders failed:', error); 3298 return []; 3299 } 3300}; 3301 3302// Global function to check for slider health and potential duplicates 3303window.checkSliderDuplicates = function() { 3304 const allSliders = document.querySelectorAll('.custom-slider'); 3305 const duplicateReport = { 3306 totalSliders: allSliders.length, 3307 duplicateNavigations: 0, 3308 duplicateDots: 0, 3309 multipleInstances: 0, 3310 healthySliders: 0, 3311 issues: [] 3312 }; 3313 3314 allSliders.forEach((slider, index) => { 3315 const navigations = slider.querySelectorAll('.slider-navigation'); 3316 const dotContainers = slider.querySelectorAll('.slider-dots'); 3317 const hasMultipleInstances = window.sliderInstances && window.sliderInstances.has(slider); 3318 3319 // Check for duplicate navigation 3320 if (navigations.length > 1) { 3321 duplicateReport.duplicateNavigations++; 3322 duplicateReport.issues.push(`Slider ${index}: Found ${navigations.length} navigation elements`); 3323 } 3324 3325 // Check for duplicate dots 3326 if (dotContainers.length > 1) { 3327 duplicateReport.duplicateDots++; 3328 duplicateReport.issues.push(`Slider ${index}: Found ${dotContainers.length} dot containers`); 3329 } 3330 3331 // Check instance tracking 3332 if (hasMultipleInstances) { 3333 duplicateReport.multipleInstances++; 3334 } 3335 3336 // Mark as healthy if no issues 3337 if (navigations.length <= 1 && dotContainers.length <= 1) { 3338 duplicateReport.healthySliders++; 3339 } 3340 3341 3342 }); 3343 3344 3345 3346 3347 3348 3349 3350 3351 if (duplicateReport.issues.length > 0) { 3352 3353 3354 } else { 3355 3356 } 3357 3358 return duplicateReport; 3359}; 3360 3361// Global function to instantly fix duplicate dots issue 3362window.fixSliderDuplicatesNow = function() { 3363 3364 3365 const allSliders = document.querySelectorAll('.custom-slider'); 3366 let fixedCount = 0; 3367 3368 allSliders.forEach((slider, index) => { 3369 const dotContainers = slider.querySelectorAll('.slider-dots'); 3370 const navigations = slider.querySelectorAll('.slider-navigation'); 3371 3372 // Fix duplicate dots 3373 if (dotContainers.length > 1) { 3374 3375 // Keep only the first dots container, remove the rest 3376 for (let i = 1; i < dotContainers.length; i++) { 3377 dotContainers[i].remove(); 3378 } 3379 fixedCount++; 3380 } 3381 3382 // Fix duplicate navigation 3383 if (navigations.length > 1) { 3384 3385 // Keep only the first navigation, remove the rest 3386 for (let i = 1; i < navigations.length; i++) { 3387 navigations[i].remove(); 3388 } 3389 fixedCount++; 3390 } 3391 }); 3392 3393 3394 3395 // Run a verification check 3396 setTimeout(() => { 3397 3398 checkSliderDuplicates(); 3399 }, 100); 3400 3401 return fixedCount; 3402}; 3403 3404// Global function to emergency clean broken sliders 3405window.emergencySliderCleanup = function() { 3406 3407 3408 const allSliders = document.querySelectorAll('.custom-slider');
3409 let cleanedCount = 0; 3410 let elementsRemoved = 0; 3411 3412 allSliders.forEach((slider, index) => { 3413 3414 3415 // Force destroy any tracked instances 3416 if (window.sliderInstances && window.sliderInstances.has(slider)) { 3417 try { 3418 const instance = window.sliderInstances.get(slider); 3419 instance.destroy(); 3420 window.sliderInstances.delete(slider); 3421 3422 } catch (error) { 3423 3424 window.sliderInstances.delete(slider); 3425 } 3426 } 3427 3428 // Remove ALL navigation elements 3429 const navigations = slider.querySelectorAll('.slider-navigation'); 3430 navigations.forEach(nav => nav.remove()); 3431 elementsRemoved += navigations.length; 3432 3433 // Remove ALL dot containers 3434 const dotContainers = slider.querySelectorAll('.slider-dots'); 3435 dotContainers.forEach(dots => dots.remove()); 3436 elementsRemoved += dotContainers.length; 3437 3438 // Remove ALL cloned slides 3439 const clonedSlides = slider.querySelectorAll('.slide-clone'); 3440 clonedSlides.forEach(clone => clone.remove()); 3441 elementsRemoved += clonedSlides.length; 3442 3443 // Reset all styles and classes 3444 const wrapper = slider.querySelector('.slider-wrapper'); 3445 if (wrapper) { 3446 wrapper.style.transform = ''; 3447 wrapper.style.transition = ''; 3448 } 3449 3450 slider.classList.remove('initialized', 'dragging'); 3451 slider.removeAttribute('tabindex'); 3452 3453 if (navigations.length > 0 || dotContainers.length > 0 || clonedSlides.length > 0) { 3454 cleanedCount++; 3455 3456 } 3457 }); 3458 3459 // Clear global instances map 3460 if (window.sliderInstances) { 3461 window.sliderInstances.clear(); 3462 } 3463 3464 3465 3466 3467 3468 3469 return { 3470 slidersProcessed: allSliders.length, 3471 slidersWithDuplicates: cleanedCount, 3472 elementsRemoved: elementsRemoved 3473 }; 3474}; 3475 3476// Global function to safely and completely reinitialize sliders 3477window.safeReinitializeAllSliders = function() { 3478 3479 3480 // First, emergency cleanup 3481 const cleanupResult = window.emergencySliderCleanup(); 3482 3483 3484 // Wait a moment for DOM to settle 3485 setTimeout(() => { 3486 3487 const result = initCustomSliders(); 3488 3489 return result; 3490 }, 100); 3491 3492 return cleanupResult; 3493}; 3494 3495// Global function to clean up duplicate elements without full reinit 3496window.cleanupSliderDuplicates = function() { 3497 const allSliders = document.querySelectorAll('.custom-slider'); 3498 let cleanedCount = 0; 3499 3500 allSliders.forEach((slider, index) => { 3501 let cleaned = false; 3502 3503 // Remove duplicate navigation elements (keep only the first one) 3504 const navigations = slider.querySelectorAll('.slider-navigation'); 3505 if (navigations.length > 1) { 3506 3507 for (let i = 1; i < navigations.length; i++) { 3508 navigations[i].remove(); 3509 cleaned = true; 3510 } 3511 } 3512 3513 // Remove duplicate dot containers (keep only the first one) 3514 const dotContainers = slider.querySelectorAll('.slider-dots'); 3515 if (dotContainers.length > 1) { 3516 3517 for (let i = 1; i < dotContainers.length; i++) { 3518 dotContainers[i].remove(); 3519 cleaned = true; 3520 } 3521 } 3522 3523 if (cleaned) { 3524 cleanedCount++; 3525 } 3526 }); 3527 3528 3529 return cleanedCount; 3530}; 3531 3532window.debugSliders = function() { 3533 const sliders = document.querySelectorAll('.custom-slider'); 3534 const packages = document.querySelectorAll('.packages_1'); 3535 const packagesSliders = document.querySelectorAll('.custom-cards-slider'); 3536 3537 3538 3539 3540 3541 3542 // Check if packages widget exists at all 3543 packages.forEach((pkg, index) => { 3544 3545 3546 3547 }); 3548 3549 sliders.forEach((slider, index) => { 3550 const slides = slider.querySelectorAll('.slide'); 3551 const clones = slider.querySelectorAll('.slide-clone'); 3552 const wrapper = slider.querySelector('.slider-wrapper'); 3553 const navigation = slider.querySelector('.slider-navigation'); 3554 const lazyImages = slider.querySelectorAll('.lazy-image'); 3555 const loadedImages = slider.querySelectorAll('.lazy-image.loaded'); 3556 const loadingImages = slider.querySelectorAll('.lazy-image.loading'); 3557 3558 3559 3560 3561 3562 3563 3564 3565 3566 3567 3568 3569 3570 // Performance metrics 3571 3572 3573 3574 3575 3576 3577 // Log clone information
3578 clones.forEach((clone, cloneIndex) => { 3579 3580 }); 3581 3582 // Log image loading status 3583 lazyImages.forEach((img, imgIndex) => { 3584 3585 }); 3586 }); 3587 3588 // Also check for any template placeholders that weren't processed 3589 const unprocessedTemplates = document.querySelectorAll('*'); 3590 let foundTemplates = 0; 3591 unprocessedTemplates.forEach(el => { 3592 if (el.innerHTML && el.innerHTML.includes('{{#each packagesList}}')) { 3593 foundTemplates++; 3594 3595 } 3596 }); 3597 3598 3599 // Performance summary 3600 const totalImages = document.querySelectorAll('.lazy-image').length; 3601 const totalLoaded = document.querySelectorAll('.lazy-image.loaded').length; 3602 3603 3604 3605 3606}; 3607 3608// Global function to get performance metrics for all sliders 3609window.getSliderPerformanceMetrics = function() { 3610 const sliders = document.querySelectorAll('.custom-slider.initialized'); 3611 const metrics = { 3612 totalSliders: sliders.length, 3613 totalImages: 0, 3614 loadedImages: 0, 3615 loadingImages: 0, 3616 loadCompletion: 0, 3617 sliders: [] 3618 }; 3619 3620 sliders.forEach((slider, index) => { 3621 const lazyImages = slider.querySelectorAll('.lazy-image'); 3622 const loadedImages = slider.querySelectorAll('.lazy-image.loaded'); 3623 const loadingImages = slider.querySelectorAll('.lazy-image.loading'); 3624 3625 const sliderMetrics = { 3626 index: index, 3627 classes: slider.className, 3628 totalImages: lazyImages.length, 3629 loadedImages: loadedImages.length, 3630 loadingImages: loadingImages.length, 3631 loadCompletion: lazyImages.length > 0 ? Math.round((loadedImages.length / lazyImages.length) * 100) : 0 3632 }; 3633 3634 metrics.sliders.push(sliderMetrics); 3635 metrics.totalImages += sliderMetrics.totalImages; 3636 metrics.loadedImages += sliderMetrics.loadedImages; 3637 metrics.loadingImages += sliderMetrics.loadingImages; 3638 }); 3639 3640 metrics.loadCompletion = metrics.totalImages > 0 ? Math.round((metrics.loadedImages / metrics.totalImages) * 100) : 0; 3641 3642 return metrics; 3643}; 3644 3645// Global function to check for crashed sliders and get health status 3646window.checkSliderHealth = function() { 3647 const allSliders = document.querySelectorAll('.custom-slider'); 3648 const initializedSliders = document.querySelectorAll('.custom-slider.initialized'); 3649 const healthReport = { 3650 totalSliders: allSliders.length, 3651 initializedSliders: initializedSliders.length, 3652 crashedSliders: [], 3653 healthySliders: [], 3654 issues: [] 3655 }; 3656 3657 allSliders.forEach((slider, index) => { 3658 const isInitialized = slider.classList.contains('initialized'); 3659 const hasWrapper = !!slider.querySelector('.slider-wrapper'); 3660 const slides = slider.querySelectorAll('.slide'); 3661 const hasSlides = slides.length > 0; 3662 3663 const sliderInfo = { 3664 index: index, 3665 element: slider, 3666 className: slider.className, 3667 isInitialized: isInitialized, 3668 hasWrapper: hasWrapper, 3669 slideCount: slides.length, 3670 hasSlides: hasSlides, 3671 position: slider.getBoundingClientRect() 3672 }; 3673 3674 // Check for potential issues 3675 if (!isInitialized && hasSlides) { 3676 healthReport.issues.push(`Slider ${index}: Has slides but not initialized`); 3677 healthReport.crashedSliders.push(sliderInfo); 3678 } else if (isInitialized && !hasWrapper) { 3679 healthReport.issues.push(`Slider ${index}: Initialized but missing wrapper`); 3680 healthReport.crashedSliders.push(sliderInfo); 3681 } else if (isInitialized && !hasSlides) { 3682 healthReport.issues.push(`Slider ${index}: Initialized but no slides found`); 3683 healthReport.crashedSliders.push(sliderInfo); 3684 } else if (isInitialized && hasWrapper && hasSlides) { 3685 healthReport.healthySliders.push(sliderInfo); 3686 } 3687 }); 3688 3689 3690 3691 3692 3693 3694 3695 if (healthReport.issues.length > 0) { 3696 3697 } else { 3698 3699 } 3700 3701 return healthReport;
3702}; 3703 3704// Export for module systems 3705if (typeof module !== 'undefined' && module.exports) { 3706 module.exports = { CustomSlider, initCustomSliders }; 3707} 3708 3709// Global error tracking for slider-related crashes 3710if (typeof window !== 'undefined') { 3711 // Track slider-related errors globally 3712 let sliderErrorCount = 0; 3713 const originalConsoleError = console.error; 3714 3715 console.error = function(...args) { 3716 // Check if this is a slider-related error 3717 const errorMessage = args.join(' ').toLowerCase(); 3718 if (errorMessage.includes('slider') || errorMessage.includes('ð¨ slider crash')) { 3719 sliderErrorCount++; 3720 3721 // Add timestamp and error count to slider errors 3722 originalConsoleError.apply(console, [ 3723 `ð¨ SLIDER ERROR #${sliderErrorCount} [${new Date().toLocaleTimeString()}]:`, 3724 ...args 3725 ]); 3726 3727 // Log error summary every 5 errors 3728 if (sliderErrorCount % 5 === 0) { 3729 originalConsoleError.apply(console, [ 3730 `â ï¸ SLIDER ERROR SUMMARY: ${sliderErrorCount} slider-related errors detected. Run checkSliderHealth() for diagnosis.` 3731 ]); 3732 } 3733 } else { 3734 originalConsoleError.apply(console, args); 3735 } 3736 }; 3737 3738 // Global function to get error statistics 3739 window.getSliderErrorCount = function() { 3740 return sliderErrorCount; 3741 }; 3742 3743 // Global function to reset error count 3744 window.resetSliderErrorCount = function() { 3745 const previousCount = sliderErrorCount; 3746 sliderErrorCount = 0; 3747 3748 return previousCount; 3749 }; 3750 3751 /** 3752 * Global function to disable infinite loop on any slider and convert it to normal slider 3753 * @param {Element|string} sliderElement - The slider element or CSS selector 3754 * @returns {boolean} - Success status 3755 */ 3756 window.disableInfiniteLoop = function(sliderElement) { 3757 try { 3758 let slider; 3759 3760 // Handle different input types 3761 if (typeof sliderElement === 'string') { 3762 slider = document.querySelector(sliderElement); 3763 } else if (sliderElement instanceof Element) { 3764 slider = sliderElement; 3765 } else { 3766 console.error('ð¨ disableInfiniteLoop: Invalid slider element provided'); 3767 return false; 3768 } 3769 3770 if (!slider) { 3771 console.error('ð¨ disableInfiniteLoop: Slider element not found'); 3772 return false; 3773 } 3774 3775 // Check if slider has infinite loop enabled 3776 if (!slider.classList.contains('infinite-loop')) { 3777 3778 return true; 3779 } 3780 3781 3782 3783 // Get the slider instance if it exists 3784 const sliderInstance = slider.sliderInstance; 3785 if (!sliderInstance) { 3786 console.error('ð¨ disableInfiniteLoop: Slider instance not found. Slider may not be initialized.'); 3787 return false; 3788 } 3789 3790 // 1. Stop any auto-slide and clear timeouts 3791 if (sliderInstance.autoSlideInterval) { 3792 clearInterval(sliderInstance.autoSlideInterval); 3793 sliderInstance.autoSlideInterval = null; 3794 } 3795 3796 if (sliderInstance.autoSlideTimeoutId) { 3797 clearTimeout(sliderInstance.autoSlideTimeoutId); 3798 sliderInstance.autoSlideTimeoutId = null; 3799 } 3800 3801 if (sliderInstance.infiniteLoopTimeoutId) { 3802 clearTimeout(sliderInstance.infiniteLoopTimeoutId); 3803 sliderInstance.infiniteLoopTimeoutId = null; 3804 } 3805 3806 // Clear all active timeouts 3807 if (sliderInstance.activeTimeouts) { 3808 sliderInstance.activeTimeouts.forEach(timeoutId => clearTimeout(timeoutId)); 3809 sliderInstance.activeTimeouts.clear(); 3810 } 3811 3812 // 2. Remove cloned slides 3813 const clonedSlides = slider.querySelectorAll('.slide-clone'); 3814 3815
3816 clonedSlides.forEach(clone => { 3817 try { 3818 clone.remove(); 3819 } catch (error) { 3820 3821 } 3822 }); 3823 3824 // 3. Update slider instance properties 3825 sliderInstance.isInfiniteLoop = false; 3826 sliderInstance.infiniteLoopSetup = false; 3827 sliderInstance.cloneCount = 0; 3828 3829 // 4. Reset slides collection to only include original slides 3830 sliderInstance.slides = slider.querySelectorAll('.slide:not(.slide-clone)'); 3831 sliderInstance.slideCount = sliderInstance.slides.length; 3832 sliderInstance.originalSlideCount = sliderInstance.slideCount; 3833 3834 // 5. Reset current slide position to within normal bounds 3835 if (sliderInstance.currentSlide >= sliderInstance.slideCount) { 3836 sliderInstance.currentSlide = 0; 3837 } else if (sliderInstance.currentSlide < 0) { 3838 sliderInstance.currentSlide = 0; 3839 } 3840 3841 // 6. Remove infinite-loop class from slider element 3842 slider.classList.remove('infinite-loop'); 3843 3844 // 7. Remove transition temporarily for instant positioning 3845 const sliderWrapper = slider.querySelector('.slider-wrapper'); 3846 if (sliderWrapper) { 3847 sliderWrapper.style.transition = 'none'; 3848 3849 // Update slider position instantly 3850 sliderInstance.updateSliderInstantly(); 3851 3852 // Re-enable transitions after a short delay 3853 setTimeout(() => { 3854 sliderWrapper.style.transition = ''; 3855 }, 100); 3856 } 3857 3858 // 8. Recreate navigation dots for normal slider behavior 3859 if (sliderInstance.dots && sliderInstance.dots.length > 0) { 3860 3861 3862 // Remove existing dots 3863 const dotsContainer = slider.querySelector('.slider-dots'); 3864 if (dotsContainer) { 3865 dotsContainer.innerHTML = ''; 3866 3867 // Create new dots for actual slides only 3868 for (let i = 0; i < sliderInstance.slideCount; i++) { 3869 const dot = document.createElement('button'); 3870 dot.classList.add('slider-dot'); 3871 dot.setAttribute('data-slide', i); 3872 dot.setAttribute('aria-label', `Go to slide ${i + 1}`); 3873 3874 if (i === sliderInstance.currentSlide) { 3875 dot.classList.add('active'); 3876 } 3877 3878 // Add click event listener for normal slider behavior 3879 dot.addEventListener('click', () => { 3880 if (sliderInstance.isTransitioning) return; 3881 sliderInstance.currentSlide = i; 3882 sliderInstance.updateSlider(); 3883 }); 3884 3885 dotsContainer.appendChild(dot); 3886 } 3887 3888 // Update dots collection 3889 sliderInstance.dots = dotsContainer.querySelectorAll('.slider-dot'); 3890 } 3891 } 3892 3893 // 9. Update navigation behavior for normal slider 3894 sliderInstance.nextSlide = function() { 3895 try { 3896 if (this.isTransitioning) return; 3897 3898 const isPackagesSlider = this.slider.classList.contains('packages-slider') || 3899 this.slider.classList.contains('custom-cards-slider'); 3900 3901 if (isPackagesSlider) { 3902 // For packages slider, calculate max slides considering visible slides 3903 const slideWidth = this.getSlideWidth(); 3904 const containerWidth = this.slider.offsetWidth; 3905 const slidesPerView = Math.floor(containerWidth / slideWidth); 3906 const maxSlide = Math.max(0, this.slideCount - slidesPerView); 3907
3908 if (this.currentSlide < maxSlide) { 3909 this.currentSlide++; 3910 } else { 3911 this.currentSlide = 0; // Loop back to start 3912 } 3913 } else { 3914 // Default behavior - loop back to beginning 3915 this.currentSlide = (this.currentSlide + 1) % this.slideCount; 3916 } 3917 3918 this.updateSlider(); 3919 } catch (error) { 3920 console.error('Error in normal nextSlide:', error); 3921 } 3922 }; 3923 3924 sliderInstance.prevSlide = function() { 3925 try { 3926 if (this.isTransitioning) return; 3927 3928 const isPackagesSlider = this.slider.classList.contains('packages-slider') || 3929 this.slider.classList.contains('custom-cards-slider'); 3930 3931 if (isPackagesSlider) { 3932 // For packages slider 3933 if (this.currentSlide > 0) { 3934 this.currentSlide--; 3935 } else { 3936 // Jump to last possible position 3937 const slideWidth = this.getSlideWidth(); 3938 const containerWidth = this.slider.offsetWidth; 3939 const slidesPerView = Math.floor(containerWidth / slideWidth); 3940 this.currentSlide = Math.max(0, this.slideCount - slidesPerView); 3941 } 3942 } else { 3943 // Default behavior - loop to end 3944 this.currentSlide = (this.currentSlide - 1 + this.slideCount) % this.slideCount; 3945 } 3946 3947 this.updateSlider(); 3948 } catch (error) { 3949 console.error('Error in normal prevSlide:', error); 3950 } 3951 }; 3952 3953 sliderInstance.goToSlide = function(index) { 3954 try { 3955 if (this.isTransitioning) return; 3956 if (index === this.currentSlide) return; 3957 3958 // Ensure index is within bounds for normal slider 3959 if (index < 0) index = 0; 3960 if (index >= this.slideCount) index = this.slideCount - 1; 3961 3962 this.currentSlide = index; 3963 this.updateSlider(); 3964 } catch (error) { 3965 console.error('Error in normal goToSlide:', error); 3966 } 3967 }; 3968 3969 // 10. Restart auto-slide if it was enabled 3970 const autoSlideTime = parseInt(slider.getAttribute('data-auto-slide-time')) || 4000; 3971 if (autoSlideTime > 0) { 3972 setTimeout(() => { 3973 sliderInstance.startAutoSlide(); 3974 }, 1000); 3975 } 3976 3977 3978 3979 3980 return true; 3981 3982 } catch (error) { 3983 console.error('ð¨ Error disabling infinite loop:', error); 3984 return false; 3985 } 3986 }; 3987 3988 /** 3989 * Global function to disable infinite loop on all sliders on the page 3990 * @returns {Object} - Results summary 3991 */ 3992 window.disableAllInfiniteLoops = function() { 3993 const results = { 3994 total: 0, 3995 success: 0, 3996 failed: 0, 3997 alreadyDisabled: 0, 3998 details: [] 3999 }; 4000 4001 try { 4002 const allSliders = document.querySelectorAll('.custom-slider, .packages-slider, .testimonials_2_custom-card-slider'); 4003 results.total = allSliders.length; 4004 4005 4006 4007 allSliders.forEach((slider, index) => { 4008 try { 4009 const sliderClass = slider.className; 4010 const hasInfiniteLoop = slider.classList.contains('infinite-loop'); 4011 4012 if (!hasInfiniteLoop) { 4013 results.alreadyDisabled++; 4014 results.details.push({ 4015 index: index + 1, 4016 className: sliderClass, 4017 status: 'already-disabled' 4018 }); 4019 return; 4020 } 4021 4022 const success = window.disableInfiniteLoop(slider); 4023 if (success) { 4024 results.success++; 4025 results.details.push({ 4026 index: index + 1, 4027 className: sliderClass, 4028 status: 'success' 4029 }); 4030 } else { 4031 results.failed++; 4032 results.details.push({ 4033 index: index + 1, 4034 className: sliderClass, 4035 status: 'failed' 4036 }); 4037 } 4038 } catch (error) { 4039 results.failed++; 4040 results.details.push({ 4041 index: index + 1, 4042 className: slider.className, 4043 status: 'error', 4044 error: error.message 4045 }); 4046 } 4047 }); 4048 4049 4050 4051 return results; 4052 4053 } catch (error) { 4054 console.error('ð¨ Error disabling infinite loops:', error); 4055 results.failed = results.total; 4056 return results; 4057 } 4058 }; 4059 4060 /** 4061 * Global function to check which sliders have infinite loop enabled 4062 * @returns {Array} - List of sliders with infinite loop status 4063 */ 4064 window.checkInfiniteLoopStatus = function() { 4065 const sliders = document.querySelectorAll('.custom-slider, .packages-slider, .testimonials_2_custom-card-slider'); 4066 const status = []; 4067
4068 sliders.forEach((slider, index) => { 4069 const hasInfiniteLoop = slider.classList.contains('infinite-loop'); 4070 const sliderInstance = slider.sliderInstance; 4071 const isInitialized = !!sliderInstance; 4072 4073 status.push({ 4074 index: index + 1, 4075 element: slider, 4076 className: slider.className, 4077 hasInfiniteLoopClass: hasInfiniteLoop, 4078 isInitialized: isInitialized, 4079 instanceInfiniteLoop: isInitialized ? sliderInstance.isInfiniteLoop : null, 4080 slideCount: isInitialized ? sliderInstance.slideCount : null, 4081 cloneCount: isInitialized ? sliderInstance.cloneCount : null, 4082 currentSlide: isInitialized ? sliderInstance.currentSlide : null 4083 }); 4084 }); 4085 4086 console.table(status); 4087 return status; 4088 }; 4089 4090 /** 4091 * Global function to toggle infinite loop on a specific slider 4092 * @param {Element|string} sliderElement - The slider element or CSS selector 4093 * @param {boolean} enable - Whether to enable (true) or disable (false) infinite loop 4094 * @returns {boolean} - Success status 4095 */ 4096 window.toggleInfiniteLoop = function(sliderElement, enable) { 4097 try { 4098 let slider; 4099 4100 if (typeof sliderElement === 'string') { 4101 slider = document.querySelector(sliderElement); 4102 } else if (sliderElement instanceof Element) { 4103 slider = sliderElement; 4104 } else { 4105 console.error('ð¨ toggleInfiniteLoop: Invalid slider element provided'); 4106 return false; 4107 } 4108 4109 if (!slider) { 4110 console.error('ð¨ toggleInfiniteLoop: Slider element not found'); 4111 return false; 4112 } 4113 4114 const currentlyEnabled = slider.classList.contains('infinite-loop'); 4115 4116 if (enable === true && !currentlyEnabled) { 4117 // Enable infinite loop - add class and reinitialize 4118 slider.classList.add('infinite-loop'); 4119 4120 4121 // Destroy and reinitialize the slider to set up infinite loop 4122 if (slider.sliderInstance) { 4123 slider.sliderInstance.destroy(); 4124 } 4125 4126 // Small delay to allow cleanup 4127 setTimeout(() => { 4128 window.initCustomSlider(slider); 4129 }, 100); 4130 4131 return true; 4132 4133 } else if (enable === false && currentlyEnabled) { 4134 // Disable infinite loop 4135 return window.disableInfiniteLoop(slider); 4136 4137 } else { 4138 4139 return true; 4140 } 4141 4142 } catch (error) { 4143 console.error('ð¨ Error toggling infinite loop:', error); 4144 return false; 4145 } 4146 }; 4147 4148 // Add infinite loop control to global slider helpers 4149 if (typeof window.sliderHelpers === 'object') { 4150 window.sliderHelpers.disableInfiniteLoop = window.disableInfiniteLoop; 4151 window.sliderHelpers.disableAllInfiniteLoops = window.disableAllInfiniteLoops; 4152 window.sliderHelpers.checkInfiniteLoopStatus = window.checkInfiniteLoopStatus; 4153 window.sliderHelpers.toggleInfiniteLoop = window.toggleInfiniteLoop; 4154 window.sliderHelpers.cleanupDuplicateDots = window.cleanupDuplicateDots; 4155 window.sliderHelpers.fixDotDuplicatesNow = window.fixDotDuplicatesNow; 4156 window.sliderHelpers.checkDotsHealth = window.checkDotsHealth; 4157 } 4158 4159 /** 4160 * Global function to clean up duplicate dots across all sliders without affecting infinite loop 4161 * @returns {Object} - Cleanup results 4162 */ 4163 window.cleanupDuplicateDots = function() { 4164 const results = { 4165 total: 0, 4166 fixed: 0, 4167 failed: 0, 4168 details: [] 4169 }; 4170 4171 try { 4172 const allSliders = document.querySelectorAll('.custom-slider, .packages-slider, .testimonials_2_custom-card-slider'); 4173 results.total = allSliders.length; 4174 4175 4176
4177 allSliders.forEach((slider, index) => { 4178 try { 4179 const sliderInstance = slider.sliderInstance; 4180 const sliderClass = slider.className; 4181 4182 if (!sliderInstance) { 4183 results.details.push({ 4184 index: index + 1, 4185 className: sliderClass, 4186 status: 'no-instance', 4187 message: 'Slider instance not found' 4188 }); 4189 return; 4190 } 4191 4192 // Use the instance's cleanup method 4193 sliderInstance.cleanupDuplicateDots(); 4194 sliderInstance.validateAndFixDots(); 4195 4196 results.fixed++; 4197 results.details.push({ 4198 index: index + 1, 4199 className: sliderClass, 4200 status: 'fixed', 4201 dotsCount: sliderInstance.dots ? sliderInstance.dots.length : 0 4202 }); 4203 4204 } catch (error) { 4205 results.failed++; 4206 results.details.push({ 4207 index: index + 1, 4208 className: slider.className, 4209 status: 'error', 4210 error: error.message 4211 }); 4212 } 4213 }); 4214 4215 4216 4217 return results; 4218 4219 } catch (error) { 4220 console.error('ð¨ Error cleaning up duplicate dots:', error); 4221 results.failed = results.total; 4222 return results; 4223 } 4224 }; 4225 4226 /** 4227 * Global function to immediately fix dot duplicates (emergency function) 4228 * @returns {boolean} - Success status 4229 */ 4230 window.fixDotDuplicatesNow = function() { 4231 try { 4232 4233 4234 // Find and remove all extra dot containers 4235 const allDotContainers = document.querySelectorAll('.slider-dots'); 4236 const processedSliders = new Set(); 4237 let removedContainers = 0; 4238 4239 allDotContainers.forEach(container => { 4240 const parentSlider = container.closest('.custom-slider, .packages-slider, .testimonials_2_custom-card-slider'); 4241 if (!parentSlider) return; 4242 4243 const sliderId = parentSlider.className; 4244 if (processedSliders.has(parentSlider)) { 4245 // This slider already has a dot container, remove this one 4246 4247 container.remove(); 4248 removedContainers++; 4249 } else { 4250 processedSliders.add(parentSlider); 4251 4252 // Validate this container 4253 const sliderInstance = parentSlider.sliderInstance; 4254 if (sliderInstance) { 4255 const expectedDots = sliderInstance.isInfiniteLoop ? sliderInstance.originalSlideCount : sliderInstance.slideCount; 4256 const actualDots = container.querySelectorAll('.dot').length; 4257 4258 if (actualDots !== expectedDots) { 4259 4260 container.remove(); 4261 sliderInstance.dotsCreated = false; 4262 sliderInstance.createDots(); 4263 } 4264 } 4265 } 4266 }); 4267 4268 4269 return true; 4270 4271 } catch (error) { 4272 console.error('ð¨ Emergency dot fix failed:', error); 4273 return false; 4274 } 4275 }; 4276 4277 /** 4278 * Global function to check dots health across all sliders 4279 * @returns {Array} - Detailed health report 4280 */ 4281 window.checkDotsHealth = function() { 4282 const healthReport = []; 4283 4284 try { 4285 const allSliders = document.querySelectorAll('.custom-slider, .packages-slider, .testimonials_2_custom-card-slider'); 4286
4287 allSliders.forEach((slider, index) => { 4288 const sliderInstance = slider.sliderInstance; 4289 const dotContainers = slider.querySelectorAll('.slider-dots'); 4290 const totalDots = slider.querySelectorAll('.dot').length; 4291 4292 const health = { 4293 index: index + 1, 4294 element: slider, 4295 className: slider.className, 4296 isInitialized: !!sliderInstance, 4297 dotContainerCount: dotContainers.length, 4298 totalDotCount: totalDots, 4299 expectedDots: null, 4300 isHealthy: false, 4301 issues: [], 4302 infiniteLoop: false 4303 }; 4304 4305 if (sliderInstance) { 4306 health.infiniteLoop = sliderInstance.isInfiniteLoop; 4307 health.expectedDots = health.infiniteLoop ? sliderInstance.originalSlideCount : sliderInstance.slideCount; 4308 4309 // Check for issues 4310 if (dotContainers.length > 1) {
4311 health.issues.push(`Multiple dot containers (${dotContainers.length})`); 4312 } 4313 4314 if (dotContainers.length === 0 && !slider.classList.contains('no-slider-dots')) { 4315 health.issues.push('Missing dot container'); 4316 } 4317 4318 if (totalDots !== health.expectedDots && !slider.classList.contains('no-slider-dots')) { 4319 health.issues.push(`Incorrect dot count (${totalDots}, expected ${health.expectedDots})`); 4320 } 4321 4322 health.isHealthy = health.issues.length === 0; 4323 } else { 4324 health.issues.push('Slider not initialized'); 4325 } 4326 4327 healthReport.push(health); 4328 }); 4329 4330 const healthySlidersCount = healthReport.filter(h => h.isHealthy).length; 4331 const unhealthySlidersCount = healthReport.length - healthySlidersCount; 4332 4333 4334 if (unhealthySlidersCount > 0) { 4335 4336 healthReport.filter(h => !h.isHealthy).forEach(report => { 4337 4338 }); 4339 } 4340 4341 console.table(healthReport.map(h => ({ 4342 Index: h.index, 4343 ClassName: h.className.split(' ').slice(0, 2).join(' '), 4344 Containers: h.dotContainerCount, 4345 Dots: h.totalDotCount, 4346 Expected: h.expectedDots, 4347 Healthy: h.isHealthy ? 'â ' : 'â', 4348 Issues: h.issues.join('; ') 4349 }))); 4350 4351 return healthReport; 4352 4353 } catch (error) { 4354 console.error('ð¨ Health check failed:', error); 4355 return []; 4356 } 4357 }; 4358}
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.