vvjc-1.0.x-dev/js/vvjc.js

js/vvjc.js
/**
 * @file
 * Views Vanilla JavaScript 3D Carousel
 *
 * Filename:     vvjc.js
 * Website:      https://www.flashwebcenter.com
 * Developer:    Alaa Haddad https://www.alaahaddad.com.
 */
((Drupal, drupalSettings, once) => {
  'use strict';
  const carouselStates = new Map();
  const previousVisibility = new Map();
  let lastKnownWidth = window.innerWidth;
  let resizeTimeout;

  function debounce(func, delay) {
    let timer;
    return function() {
      clearTimeout(timer);
      timer = setTimeout(func, delay);
    };
  }

  /**
   * Detect if element is in RTL context.
   *
   * @param {HTMLElement} element
   *   The element to check.
   *
   * @return {boolean}
   *   True if RTL, false otherwise.
   */
  function isRTL(element) {
    // Check for dir attribute on element or ancestors.
    const dirAttr = element.closest('[dir]');
    if (dirAttr) {
      return dirAttr.getAttribute('dir') === 'rtl';
    }

    // Fallback to computed style.
    const computed = window.getComputedStyle(element);
    return computed.direction === 'rtl';
  }

  const playIconSVG = `
    <svg class="svg-play" xmlns="http://www.w3.org/2000/svg" viewBox="80 -880 800 800" fill="currentcolor">
      <path d="m380-300 280-180-280-180v360ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"></path>
    </svg>`;
  const pauseIconSVG = `
    <svg class="svg-pause" xmlns="http://www.w3.org/2000/svg" viewBox="80 -880 800 800" fill="currentcolor"><path d="M360-320h80v-320h-80v320Zm160 0h80v-320h-80v320ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"></path></svg>`;
  Drupal.behaviors.VVJCarousel = {
    attach(context) {
      once('VVJCarousel', '.vvjc-items', context).forEach(initCarousel);
      if (!document.vvjCarouselVisibilityAttached) {
        document.vvjCarouselVisibilityAttached = true;
        document.addEventListener('visibilitychange', handleVisibilityChange);
      }
    }
  };

  function initCarousel(carousel) {
    const container = carousel.closest('.vvjc');
    const uniqueId = carousel.id.split('-')[2];
    
    // Read configuration from data attributes (security: data attributes are sanitized by Drupal/Twig).
    const config = {
      showProgressBar: carousel.getAttribute('data-show-progress-bar') === 'true',
      showDotsNavigation: carousel.getAttribute('data-show-dots-navigation') === 'true',
      enableKeyboardNav: carousel.getAttribute('data-enable-keyboard-nav') === 'true',
      enableTouchSwipe: carousel.getAttribute('data-enable-touch-swipe') === 'true',
      enablePauseOnHover: carousel.getAttribute('data-enable-pause-on-hover') === 'true',
      enableScreenReader: carousel.getAttribute('data-enable-screen-reader') === 'true',
      pauseOnReducedMotion: carousel.getAttribute('data-pause-on-reduced-motion') === 'true',
      showNavigationArrows: carousel.getAttribute('data-show-navigation-arrows') === 'true',
      showPlayPause: carousel.getAttribute('data-show-play-pause') === 'true',
      showSlideCounter: carousel.getAttribute('data-show-slide-counter') === 'true',
    };
    
    const state = {
      container: carousel,
      cells: carousel.querySelectorAll('.vvjc-item'),
      currentIndex: 0,
      isPaused: false,
      intervalId: null,
      progressIntervalId: null,
      direction: 1,
      rotationDelay: parseInt(container.querySelector('.vvjc-nav')?.getAttribute('data-time') || '0', 10),
      slideNumberElement: container.querySelector(`#index-${uniqueId}`),
      totalSlidesElement: container.querySelector('.total-slides'),
      prevButton: container.querySelector(`#prev-${uniqueId}`),
      nextButton: container.querySelector(`#next-${uniqueId}`),
      playPauseButton: container.querySelector(`#btn-${uniqueId}`),
      progressBar: container.querySelector('.progressbar'),
      announcer: container.querySelector('.announcer'),
      dotsContainer: container.querySelector('.vvjc-dots-nav'),
      config: config,
      uniqueId: uniqueId,
      isRTL: isRTL(container),
    };
    
    const initiallyVisible = isMostlyVisible(state.container);
    previousVisibility.set(carousel.id, initiallyVisible);
    carouselStates.set(carousel.id, state);
    
    if (state.rotationDelay <= 0) {
      state.isPaused = true;
      updatePlayPauseButton(state, true);
    }
    
    // Check for reduced motion preference.
    if (config.pauseOnReducedMotion && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
      state.isPaused = true;
      updatePlayPauseButton(state, true);
      if (config.enableScreenReader && state.announcer) {
        announceMessage(state, 'Carousel paused due to reduced motion preference');
      }
    }
    
    buildCarousel(state);
    setupControls(state);
    
    // Initialize deep linking if enabled
    initializeDeepLinking(state);
    
    if (config.enableTouchSwipe) {
      setupTouchEvents(state);
    }
    
    if (config.showDotsNavigation && state.dotsContainer) {
      setupDotsNavigation(state);
    }
    
    updateSlide(state);
    
    if (initiallyVisible && !state.isPaused) {
      startAutoPlay(state);
    }
  }

  function buildCarousel(state) {
    const {
      cells,
      container,
      config
    } = state;
    const theta = 360 / cells.length;
    const radius = Math.round(container.offsetWidth / 2 / Math.tan(Math.PI / cells.length));
    const reducedMotion = config.pauseOnReducedMotion && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    cells.forEach((cell, i) => {
      cell.style.opacity = '1';
      cell.style.transform = `rotateY(${theta * i}deg) translateZ(${radius}px)`;
      if (reducedMotion) {
        cell.style.transition = 'none';
      }
    });
    if (reducedMotion) {
      container.style.transition = 'none';
      container.classList.add('reduced-motion');
    }
    cells[0].classList.add('active');
    updateNavVisibility(state);
  }

  function setupControls(state) {
    const {config} = state;
    
    // Navigation arrows - conditional based on config.
    if (config.showNavigationArrows) {
      if (state.nextButton) {
        state.nextButton.addEventListener('click', () => manualAdvance(state, 1));
      }
      if (state.prevButton) {
        state.prevButton.addEventListener('click', () => manualAdvance(state, -1));
      }
    }
    
    // Play/pause button - conditional based on config.
    if (config.showPlayPause && state.playPauseButton) {
      state.playPauseButton.style.display = 'flex';
      state.playPauseButton.addEventListener('click', () => togglePlayPause(state));
      state.playPauseButton.addEventListener('keydown', (event) => {
        if (event.key === 'Enter' || event.key === ' ') {
          event.preventDefault();
          togglePlayPause(state);
        }
      });
    }
    
    // Enhanced keyboard navigation - conditional based on config.
    if (config.enableKeyboardNav) {
      state.container.addEventListener('keydown', (event) => {
        // Skip if focus is in an input element.
        if (event.target.closest('input, textarea, [contenteditable="true"]')) {
          return;
        }
        
        switch (event.key) {
          case 'ArrowRight':
            event.preventDefault();
            // RTL: ArrowRight goes backward (previous)
            manualAdvance(state, state.isRTL ? -1 : 1);
            break;
          case 'ArrowLeft':
            event.preventDefault();
            // RTL: ArrowLeft goes forward (next)
            manualAdvance(state, state.isRTL ? 1 : -1);
            break;
          case ' ':
          case 'Spacebar':
            if (config.showPlayPause) {
              event.preventDefault();
              togglePlayPause(state);
            }
            break;
          case 'Home':
            event.preventDefault();
            goToSlide(state, 0);
            restartAutoPlay(state);
            break;
          case 'End':
            event.preventDefault();
            goToSlide(state, state.cells.length - 1);
            restartAutoPlay(state);
            break;
        }
      });
    }
    
    // Pause on hover - conditional based on config.
    if (config.enablePauseOnHover) {
      state.container.addEventListener('mouseenter', () => {
        pauseAutoPlay(state);
        if (config.showProgressBar) {
          pauseProgress(state);
        }
      });
      state.container.addEventListener('mouseleave', () => {
        resumeAutoPlay(state);
      });
    }
  }

  function manualAdvance(state, direction) {
    state.direction = direction;
    advanceSlide(state);
    restartAutoPlay(state);
  }

  function goToSlide(state, targetIndex) {
    // Security: Validate index bounds.
    const cellCount = state.cells.length;
    if (targetIndex < 0 || targetIndex >= cellCount) {
      return;
    }
    
    state.currentIndex = targetIndex;
    
    // Determine direction based on distance (for natural rotation direction).
    const currentIndex = state.currentIndex;
    if (targetIndex > currentIndex) {
      state.direction = 1;
    } else if (targetIndex < currentIndex) {
      state.direction = -1;
    }
    
    updateSlide(state);
  }

  function advanceSlide(state) {
    const {
      cells
    } = state;
    const cellCount = cells.length;
    state.currentIndex += state.direction;
    if (state.currentIndex >= cellCount) {
      // Bounce immediately to previous slide.
      state.currentIndex = cellCount - 2;
      state.direction = -1;
    } else if (state.currentIndex < 0) {
      // Bounce immediately to next slide.
      state.currentIndex = 1;
      state.direction = 1;
    }
    updateSlide(state);
  }

  function updateSlide(state) {
    const {
      cells,
      currentIndex,
      slideNumberElement,
      container,
      config
    } = state;
    const theta = 360 / cells.length;
    const radius = Math.round(container.offsetWidth / 2 / Math.tan(Math.PI / cells.length));
    const angle = theta * currentIndex * -1;
    container.style.transition = 'transform 0.8s ease-in-out';
    container.style.transform = `translateZ(${-radius}px) rotateY(${angle}deg)`;
    
    cells.forEach((cell, index) => {
      const isActive = index === currentIndex;
      cell.setAttribute('aria-hidden', !isActive);
      updateFocusableElements(cell, isActive);
      if (isActive) {
        cell.classList.add('active');
      } else {
        cell.classList.remove('active');
      }
    });
    
    // Update slide counter - conditional based on config.
    if (config.showSlideCounter && slideNumberElement) {
      slideNumberElement.textContent = currentIndex + 1;
    }
    
    // Update dots navigation - conditional based on config.
    if (config.showDotsNavigation) {
      updateDotsNavigation(state);
    }
    
    // Announce to screen readers - conditional based on config.
    if (config.enableScreenReader && state.announcer) {
      announceSlide(state);
    }
    
    // Restart progress bar - conditional based on config.
    if (config.showProgressBar && state.rotationDelay > 0 && !state.isPaused) {
      startProgress(state);
    }
    
    // Update deep linking hash if enabled
    if (state.updateHash) {
      state.updateHash(currentIndex);
    }
    
    updateNavVisibility(state);
  }

  function updateFocusableElements(cell, isActive) {
    cell.querySelectorAll('a, button').forEach(el => {
      el.setAttribute('tabindex', isActive ? '0' : '-1');
    });
  }

  function updateNavVisibility(state) {
    const {
      currentIndex,
      prevButton,
      nextButton,
      cells,
      config
    } = state;
    
    // Only update arrow visibility if arrows are enabled.
    if (!config.showNavigationArrows) {
      return;
    }
    
    if (prevButton) {
      if (currentIndex === 0) {
        prevButton.classList.add('vvjc-hidden');
        prevButton.setAttribute('tabindex', '-1');
      } else {
        prevButton.classList.remove('vvjc-hidden');
        prevButton.setAttribute('tabindex', '0');
      }
    }
    if (nextButton) {
      if (currentIndex === cells.length - 1) {
        nextButton.classList.add('vvjc-hidden');
        nextButton.setAttribute('tabindex', '-1');
      } else {
        nextButton.classList.remove('vvjc-hidden');
        nextButton.setAttribute('tabindex', '0');
      }
    }
  }

  function togglePlayPause(state) {
    state.isPaused = !state.isPaused;
    updatePlayPauseButton(state, state.isPaused);
    
    if (state.isPaused) {
      pauseAutoPlay(state);
      if (state.config.showProgressBar) {
        pauseProgress(state);
      }
    } else {
      restartAutoPlay(state);
    }
  }

  function updatePlayPauseButton(state, isPaused) {
    if (!state.playPauseButton) {
      return;
    }
    state.playPauseButton.innerHTML = isPaused ? playIconSVG : pauseIconSVG;
    state.playPauseButton.setAttribute('aria-label', isPaused ? 'Play slideshow' : 'Pause slideshow');
    state.playPauseButton.setAttribute('aria-pressed', isPaused ? 'false' : 'true');
  }

  function setupTouchEvents(state) {
    let touchStartX = 0;
    let touchEndX = 0;
    
    // Security: Use passive listeners for better performance.
    state.container.addEventListener('touchstart', (e) => {
      touchStartX = e.touches[0].clientX;
    }, {passive: true});
    
    state.container.addEventListener('touchmove', (e) => {
      touchEndX = e.touches[0].clientX;
    }, {passive: true});
    
    state.container.addEventListener('touchend', () => {
      handleSwipe(state, touchStartX, touchEndX);
    }, {passive: true});
  }

  function handleSwipe(state, startX, endX) {
    // Security: Validate numeric inputs.
    if (typeof startX !== 'number' || typeof endX !== 'number') {
      return;
    }
    
    const swipeThreshold = 50;
    const swipeLeft = startX - endX > swipeThreshold;
    const swipeRight = endX - startX > swipeThreshold;
    
    if (swipeLeft) {
      // Swipe left: next in LTR, previous in RTL
      manualAdvance(state, state.isRTL ? -1 : 1);
    } else if (swipeRight) {
      // Swipe right: previous in LTR, next in RTL
      manualAdvance(state, state.isRTL ? 1 : -1);
    }
  }

  function startAutoPlay(state) {
    clearInterval(state.intervalId);
    if (state.rotationDelay > 0 && !state.isPaused) {
      state.intervalId = setInterval(() => advanceSlide(state), state.rotationDelay);
      
      // Start progress bar if enabled.
      if (state.config.showProgressBar) {
        startProgress(state);
      }
    }
  }

  function pauseAutoPlay(state) {
    clearInterval(state.intervalId);
    
    // Also pause progress bar if enabled.
    if (state.config.showProgressBar) {
      pauseProgress(state);
    }
  }

  function resumeAutoPlay(state) {
    if (!state.isPaused) {
      startAutoPlay(state);
    }
  }

  function handleVisibilityChange() {
    carouselStates.forEach((state, id) => {
      const currentlyVisible = !document.hidden && isMostlyVisible(state.container);
      if (currentlyVisible !== previousVisibility.get(id)) {
        previousVisibility.set(id, currentlyVisible);
        currentlyVisible ? startAutoPlay(state) : pauseAutoPlay(state);
      }
    });
  }

  function restartAutoPlay(state) {
    pauseAutoPlay(state);
    startAutoPlay(state);
  }
  const resetCarousel = () => {
    carouselStates.forEach((state) => {
      state.currentIndex = 0;
      state.direction = 1;
      buildCarousel(state);
      updateSlide(state);
      restartAutoPlay(state);
    });
  };
  
  const isMostlyVisible = (element) => {
    const rect = element.getBoundingClientRect();
    const visibleHeight = Math.min(rect.bottom, window.innerHeight) - Math.max(rect.top, 0);
    return visibleHeight / rect.height > 0.2;
  };

  /**
   * Setup dots navigation click handlers.
   */
  function setupDotsNavigation(state) {
    if (!state.dotsContainer) {
      return;
    }
    
    const dotButtons = state.dotsContainer.querySelectorAll('.dots-numbers-button');
    dotButtons.forEach((dot, index) => {
      dot.addEventListener('click', () => {
        goToSlide(state, index);
        restartAutoPlay(state);
      });
    });
  }

  /**
   * Update dots navigation active state.
   */
  function updateDotsNavigation(state) {
    if (!state.dotsContainer) {
      return;
    }
    
    const dotButtons = state.dotsContainer.querySelectorAll('.dots-numbers-button');
    dotButtons.forEach((dot, index) => {
      const isActive = index === state.currentIndex;
      dot.classList.toggle('active', isActive);
      dot.setAttribute('aria-selected', isActive ? 'true' : 'false');
      dot.setAttribute('tabindex', isActive ? '0' : '-1');
    });
  }

  /**
   * Announce slide change to screen readers.
   */
  function announceSlide(state) {
    if (!state.announcer) {
      return;
    }
    
    const currentSlide = state.currentIndex + 1;
    const totalSlides = state.cells.length;
    state.announcer.textContent = `Slide ${currentSlide} of ${totalSlides}`;
  }

  /**
   * Announce custom message to screen readers.
   */
  function announceMessage(state, message) {
    if (!state.announcer) {
      return;
    }
    
    // Security: Use textContent to prevent XSS.
    const originalText = state.announcer.textContent;
    state.announcer.textContent = message;
    
    // Restore original text after brief delay.
    setTimeout(() => {
      state.announcer.textContent = originalText;
    }, 1000);
  }

  /**
   * Start progress bar animation.
   */
  function startProgress(state) {
    if (!state.progressBar || state.rotationDelay <= 0) {
      return;
    }
    
    // Clear any existing progress interval.
    clearInterval(state.progressIntervalId);
    
    // Reset progress bar.
    state.progressBar.style.setProperty('--progress', '0%');
    state.progressBar.setAttribute('aria-valuenow', '0');
    
    const startTime = Date.now();
    
    // Performance: Use 50ms interval for smooth animation without excessive CPU.
    state.progressIntervalId = setInterval(() => {
      if (state.isPaused) {
        clearInterval(state.progressIntervalId);
        return;
      }
      
      const elapsed = Date.now() - startTime;
      const progress = Math.min(100, (elapsed / state.rotationDelay) * 100);
      
      state.progressBar.style.setProperty('--progress', `${progress}%`);
      state.progressBar.setAttribute('aria-valuenow', Math.round(progress));
      
      if (progress >= 100) {
        clearInterval(state.progressIntervalId);
      }
    }, 50);
  }

  /**
   * Pause progress bar animation.
   */
  function pauseProgress(state) {
    if (state.progressIntervalId) {
      clearInterval(state.progressIntervalId);
      state.progressIntervalId = null;
    }
  }

  function handleCarouselVisibility() {
    carouselStates.forEach((state, carouselId) => {
      const currentlyVisible = isMostlyVisible(state.container);
      const wasVisible = previousVisibility.get(carouselId) ?? false;
      if (currentlyVisible !== wasVisible) {
        previousVisibility.set(carouselId, currentlyVisible);
        if (currentlyVisible && !state.isPaused) {
          startAutoPlay(state);
        } else {
          pauseAutoPlay(state);
        }
      }
    });
  }
  
  const handleResizeOrRotate = () => {
    clearTimeout(resizeTimeout);
    resizeTimeout = setTimeout(() => {
      const newWidth = window.innerWidth;
      if (Math.abs(newWidth - lastKnownWidth) >= 50) {
        resetCarousel();
        lastKnownWidth = newWidth;
      }
    }, 200);
  };
  
  // Performance: Use debounced event listeners to prevent excessive calls.
  document.addEventListener('scroll', debounce(handleCarouselVisibility, 200));
  window.addEventListener('orientationchange', debounce(handleResizeOrRotate, 200));
  window.addEventListener('resize', debounce(() => {
    handleCarouselVisibility();
    handleResizeOrRotate();
  }, 200));

  /**
   * Deep linking functionality
   */
  function initializeDeepLinking(state) {
    const container = state.container;
    const deeplinkEnabled = container.getAttribute('data-deeplink-enabled') === 'true';
    const deeplinkId = container.getAttribute('data-deeplink-id');

    if (!deeplinkEnabled || !deeplinkId) {
      return;
    }

    // Check URL hash on page load
    const hash = window.location.hash;
    if (hash && hash.startsWith(`#carousel3d-${deeplinkId}-`)) {
      const slideNumber = parseInt(hash.split('-').pop(), 10);
      if (slideNumber >= 1 && slideNumber <= state.cells.length) {
        // Navigate to the slide (0-indexed)
        goToSlide(state, slideNumber - 1);
      }
    }

    // Update URL hash when navigating
    state.updateHash = (slideIndex) => {
      const newHash = `#carousel3d-${deeplinkId}-${slideIndex + 1}`;
      // Use replaceState to avoid cluttering browser history
      if (window.history && window.history.replaceState) {
        window.history.replaceState(null, '', newHash);
      } else {
        window.location.hash = newHash;
      }
    };

    // Listen for hash changes (browser back/forward)
    window.addEventListener('hashchange', () => {
      const currentHash = window.location.hash;
      if (currentHash && currentHash.startsWith(`#carousel3d-${deeplinkId}-`)) {
        const slideNumber = parseInt(currentHash.split('-').pop(), 10);
        if (slideNumber >= 1 && slideNumber <= state.cells.length) {
          goToSlide(state, slideNumber - 1);
        }
      }
    });
  }

  /**
   * Global utility functions for external access
   */
  Drupal.vvjc = Drupal.vvjc || {};

  /**
   * Get carousel instance by container element or selector.
   */
  Drupal.vvjc.getInstance = function(containerOrSelector) {
    let container;

    if (typeof containerOrSelector === 'string') {
      container = document.querySelector(containerOrSelector);
    } else {
      container = containerOrSelector;
    }

    if (!container) {
      return null;
    }

    // Find the .vvjc-items element
    const carouselItems = container.classList.contains('vvjc-items') 
      ? container 
      : container.querySelector('.vvjc-items');

    if (!carouselItems) {
      return null;
    }

    return carouselStates.get(carouselItems.id) || null;
  };

  /**
   * Get all active carousel instances.
   */
  Drupal.vvjc.getAllInstances = function() {
    return Array.from(carouselStates.values());
  };

  /**
   * Helper function to get carousel container by identifier.
   */
  function getContainerByIdentifier(identifier) {
    let container;

    // Try deep link identifier first (if not a CSS selector)
    if (!identifier.startsWith('.') && !identifier.startsWith('#')) {
      container = document.querySelector(`[data-deeplink-id="${identifier}"]`);
    }

    // Fallback to CSS selector
    if (!container) {
      container = document.querySelector(identifier);
    }

    return container;
  }

  /**
   * Helper function to get carousel state from identifier.
   */
  function getStateByIdentifier(identifier) {
    const container = getContainerByIdentifier(identifier);

    if (!container) {
      return null;
    }

    // Get the carousel items container
    const carouselItems = container.classList.contains('vvjc-items')
      ? container
      : container.querySelector('.vvjc-items');

    if (!carouselItems) {
      return null;
    }

    return carouselStates.get(carouselItems.id) || null;
  }

  /**
   * Navigate to a specific slide by identifier.
   */
  Drupal.vvjc.goToSlide = function(identifier, slideIndex) {
    const state = getStateByIdentifier(identifier);

    if (!state) {
      if (typeof console !== 'undefined' && console.warn) {
        console.warn(`VVJC: Carousel "${identifier}" not found`);
      }
      return false;
    }

    if (slideIndex < 1 || slideIndex > state.cells.length) {
      if (typeof console !== 'undefined' && console.warn) {
        console.warn(`VVJC: Invalid slide index ${slideIndex}. Must be between 1 and ${state.cells.length}`);
      }
      return false;
    }

    goToSlide(state, slideIndex - 1);
    if (!state.isPaused && state.rotationDelay > 0) {
      startAutoPlay(state);
    }
    return true;
  };

  /**
   * Get the current slide index for a carousel.
   */
  Drupal.vvjc.getCurrentSlide = function(identifier) {
    const state = getStateByIdentifier(identifier);
    return state ? state.currentIndex + 1 : null;
  };

  /**
   * Get total number of slides in a carousel.
   */
  Drupal.vvjc.getTotalSlides = function(identifier) {
    const state = getStateByIdentifier(identifier);
    return state ? state.cells.length : null;
  };

  /**
   * Navigate to next slide.
   */
  Drupal.vvjc.nextSlide = function(identifier) {
    const state = getStateByIdentifier(identifier);

    if (state) {
      const nextIndex = (state.currentIndex + 1) % state.cells.length;
      goToSlide(state, nextIndex);
      if (!state.isPaused && state.rotationDelay > 0) {
        startAutoPlay(state);
      }
      return true;
    }

    return false;
  };

  /**
   * Navigate to previous slide.
   */
  Drupal.vvjc.prevSlide = function(identifier) {
    const state = getStateByIdentifier(identifier);

    if (state) {
      const prevIndex = (state.currentIndex - 1 + state.cells.length) % state.cells.length;
      goToSlide(state, prevIndex);
      if (!state.isPaused && state.rotationDelay > 0) {
        startAutoPlay(state);
      }
      return true;
    }

    return false;
  };

  /**
   * Pause a specific carousel.
   */
  Drupal.vvjc.pause = function(identifier) {
    const state = getStateByIdentifier(identifier);

    if (state && !state.isPaused) {
      state.isPaused = true;
      pauseAutoPlay(state);
      updatePlayPauseButton(state, true);
      return true;
    }

    return false;
  };

  /**
   * Resume a specific carousel.
   */
  Drupal.vvjc.resume = function(identifier) {
    const state = getStateByIdentifier(identifier);

    if (state && state.isPaused && state.rotationDelay > 0) {
      state.isPaused = false;
      startAutoPlay(state);
      updatePlayPauseButton(state, false);
      return true;
    }

    return false;
  };

  /**
   * Pause all carousels on the page.
   */
  Drupal.vvjc.pauseAll = function() {
    carouselStates.forEach(state => {
      if (!state.isPaused) {
        state.isPaused = true;
        pauseAutoPlay(state);
        updatePlayPauseButton(state, true);
      }
    });
  };

  /**
   * Resume all carousels on the page.
   */
  Drupal.vvjc.resumeAll = function() {
    carouselStates.forEach(state => {
      if (state.isPaused && state.rotationDelay > 0) {
        state.isPaused = false;
        startAutoPlay(state);
        updatePlayPauseButton(state, false);
      }
    });
  };

})(Drupal, drupalSettings, once);

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc