vvjs-1.0.1/js/slideshow-transitions.js

js/slideshow-transitions.js
/**
 * @file
 * Slideshow transition effects module.
 *
 * Handles slide transitions including instant and crossfade variants.
 * Uses CSS-based transitions for GPU acceleration and themability.
 */

((Drupal) => {
  'use strict';

  /**
   * Slideshow Transitions class.
   *
   * Manages visual transition effects between slides using CSS classes
   * and data attributes. Coordinates with core module via events.
   */
  class SlideshowTransitions {
    /**
     * Constructor.
     *
     * @param {HTMLElement} container
     *   The slideshow container element.
     * @param {Object} slideshowCore
     *   Reference to the core slideshow instance.
     */
    constructor(container, slideshowCore) {
      this.container = container;
      this.core = slideshowCore;
      this.slideshow = container.querySelector('.vvjs-items');
      this.slides = this.slideshow?.querySelectorAll('.vvjs-item') || [];

      // Transition configuration
      this.transitionType = container.dataset.transition || 'instant';
      this.transitionDuration = parseInt(container.dataset.transitionDuration, 10) || 600;
      this.isHeroMode = container.classList.contains('hero-slideshow');

      // Active transition tracking for cleanup
      this.activeTransition = null;

      // Debugging flag (set to false in production)
      this.debug = false;

      this.init();
    }

    /**
     * Initialize the transitions module.
     */
    init() {
      if (!this.slideshow || !this.slides.length) {
        return;
      }

      this.setupSlides();
      this.bindEvents();

      if (this.debug) {
        console.log('[VVJS Transitions] Initialized', {
          type: this.transitionType,
          duration: this.transitionDuration,
          heroMode: this.isHeroMode,
          slides: this.slides.length,
        });
      }
    }

    /**
     * Set up initial slide positioning based on transition type.
     */
    setupSlides() {
      // Instant mode uses existing display:none logic - no changes needed
      if (this.transitionType === 'instant') {
        return;
      }

      // Crossfade modes: position all slides absolutely, set initial states
      if (this.transitionType.startsWith('crossfade')) {
        this.slides.forEach((slide, index) => {
          const isActive = index === 0;

          // Set initial opacity and z-index
          slide.style.opacity = isActive ? '1' : '0';
          slide.style.zIndex = isActive ? '2' : '1';

          // Add state classes (CSS handles positioning)
          slide.classList.toggle('vvjs-active', isActive);
          slide.classList.toggle('vvjs-previous', !isActive);
        });
      }
    }

    /**
     * Bind event listeners.
     */
    bindEvents() {
      // Listen for slide changes from core
      this.container.addEventListener('vvjs:slideChanging', (e) => {
        this.performTransition(e.detail.fromIndex, e.detail.toIndex);
      });

      // Listen for reduced motion preference changes
      const reducedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
      reducedMotionQuery.addEventListener('change', (e) => {
        if (e.matches) {
          this.handleReducedMotion();
        }
      });

      // Initial reduced motion check
      if (reducedMotionQuery.matches) {
        this.handleReducedMotion();
      }
    }

    /**
     * Perform transition between slides.
     *
     * @param {number} fromIndex
     *   The index of the outgoing slide (1-based).
     * @param {number} toIndex
     *   The index of the incoming slide (1-based).
     */
    performTransition(fromIndex, toIndex) {
      // Clean up any active transition
      this.cleanupActiveTransition();

      const outgoing = fromIndex ? this.slides[fromIndex - 1] : null;
      const incoming = this.slides[toIndex - 1];

      if (!incoming) {
        return;
      }

      if (this.debug) {
        console.log('[VVJS Transitions] Transitioning', {
          from: fromIndex,
          to: toIndex,
          type: this.transitionType,
        });
      }

      // Route to appropriate transition method
      if (this.transitionType === 'instant') {
        this.transitionInstant(outgoing, incoming);
      }
      else if (this.transitionType === 'crossfade-classic') {
        this.transitionCrossfadeClassic(outgoing, incoming);
      }
      else if (this.transitionType === 'crossfade-staged') {
        this.transitionCrossfadeStaged(outgoing, incoming);
      }
      else if (this.transitionType === 'crossfade-dynamic') {
        this.transitionCrossfadeDynamic(outgoing, incoming);
      }
      else {
        // Fallback to instant
        this.transitionInstant(outgoing, incoming);
      }
    }

    /**
     * Instant transition (backward compatible default).
     *
     * @param {HTMLElement|null} outgoing
     *   The outgoing slide element.
     * @param {HTMLElement} incoming
     *   The incoming slide element.
     */
    transitionInstant(outgoing, incoming) {
      // Instant transition uses core's display:none logic
      // Just dispatch completion event immediately
      this.onTransitionComplete();
    }

    /**
     * Classic crossfade: both slides fade at same speed.
     *
     * @param {HTMLElement|null} outgoing
     *   The outgoing slide element.
     * @param {HTMLElement} incoming
     *   The incoming slide element.
     */
    transitionCrossfadeClassic(outgoing, incoming) {
      this.applyCrossfadeTransition(outgoing, incoming, 'classic');
    }

    /**
     * Staged crossfade: outgoing fades quickly, incoming slowly with overlap.
     *
     * @param {HTMLElement|null} outgoing
     *   The outgoing slide element.
     * @param {HTMLElement} incoming
     *   The incoming slide element.
     */
    transitionCrossfadeStaged(outgoing, incoming) {
      this.applyCrossfadeTransition(outgoing, incoming, 'staged');
    }

    /**
     * Dynamic crossfade: fast fade-out, slow fade-in.
     *
     * @param {HTMLElement|null} outgoing
     *   The outgoing slide element.
     * @param {HTMLElement} incoming
     *   The incoming slide element.
     */
    transitionCrossfadeDynamic(outgoing, incoming) {
      this.applyCrossfadeTransition(outgoing, incoming, 'dynamic');
    }

    /**
     * Apply crossfade transition with proper timing and cleanup.
     *
     * @param {HTMLElement|null} outgoing
     *   The outgoing slide element.
     * @param {HTMLElement} incoming
     *   The incoming slide element.
     * @param {string} variant
     *   The crossfade variant (classic|staged|dynamic).
     */
    applyCrossfadeTransition(outgoing, incoming, variant) {
      // Set up z-index stacking
      if (outgoing) {
        outgoing.style.zIndex = '1';
        outgoing.classList.remove('vvjs-active');
        outgoing.classList.add('vvjs-previous');
      }

      incoming.style.zIndex = '2';
      incoming.classList.remove('vvjs-previous');
      incoming.classList.add('vvjs-active');

      // Trigger opacity transitions via CSS classes
      // CSS handles the actual animation based on data-transition attribute
      // Animations (zoom, slide) are handled by CSS selectors targeting .vvjs-active
      if (outgoing) {
        outgoing.style.opacity = '0';
      }
      incoming.style.opacity = '1';

      // Set up transition completion detection
      this.setupTransitionCompletion(incoming, variant);
    }

    /**
     * Set up transition completion detection with fallback.
     *
     * @param {HTMLElement} element
     *   The element to watch for transitionend.
     * @param {string} variant
     *   The transition variant for duration calculation.
     */
    setupTransitionCompletion(element, variant) {
      let transitionEnded = false;

      const cleanup = () => {
        if (transitionEnded) {
          return;
        }
        transitionEnded = true;

        // Remove event listener
        if (this.activeTransition?.listener) {
          element.removeEventListener('transitionend', this.activeTransition.listener);
        }

        this.onTransitionComplete();
      };

      // Calculate total duration based on variant
      let totalDuration = this.transitionDuration;
      if (variant === 'staged') {
        // Staged has overlap, so total time is longer
        totalDuration = this.transitionDuration * 1.3;
      }

      // Store transition data for cleanup
      this.activeTransition = {
        element,
        listener: (e) => {
          // Only trigger on opacity transitions, not other properties
          if (e.propertyName === 'opacity' && e.target === element) {
            cleanup();
          }
        },
        timeout: setTimeout(cleanup, totalDuration + 100), // 100ms buffer
      };

      // Listen for transitionend (preferred method)
      element.addEventListener('transitionend', this.activeTransition.listener);
    }

    /**
     * Handle transition completion.
     *
     * Dispatches event for other modules to respond to.
     */
    onTransitionComplete() {
      // Clean up active transition tracking
      this.cleanupActiveTransition();

      // Dispatch completion event
      this.container.dispatchEvent(new CustomEvent('vvjs:transitionComplete', {
        detail: {
          slideIndex: this.core.slideIndex,
          transitionType: this.transitionType,
        },
      }));

      if (this.debug) {
        console.log('[VVJS Transitions] Transition complete');
      }
    }

    /**
     * Clean up active transition resources.
     */
    cleanupActiveTransition() {
      if (this.activeTransition) {
        if (this.activeTransition.timeout) {
          clearTimeout(this.activeTransition.timeout);
        }
        if (this.activeTransition.element && this.activeTransition.listener) {
          this.activeTransition.element.removeEventListener(
            'transitionend',
            this.activeTransition.listener
          );
        }
        this.activeTransition = null;
      }
    }

    /**
     * Handle reduced motion preference.
     *
     * Automatically switches to instant transitions when user prefers reduced motion.
     */
    handleReducedMotion() {
      // Store original transition type
      if (!this.originalTransitionType) {
        this.originalTransitionType = this.transitionType;
      }

      // Switch to instant mode
      this.transitionType = 'instant';
      this.container.dataset.transition = 'instant';

      // Add visual class for CSS
      this.container.classList.add('reduced-motion');

      if (this.debug) {
        console.log('[VVJS Transitions] Reduced motion enabled');
      }
    }

    /**
     * Update transition configuration.
     *
     * @param {string} type
     *   New transition type.
     * @param {number} duration
     *   New transition duration in milliseconds.
     */
    updateConfig(type, duration) {
      this.transitionType = type;
      this.transitionDuration = duration;
      this.container.dataset.transition = type;
      this.container.dataset.transitionDuration = duration;

      // Re-setup slides if switching between instant and crossfade
      this.setupSlides();
    }

    /**
     * Get current transition state.
     *
     * @return {Object}
     *   Current transition configuration.
     */
    getState() {
      return {
        type: this.transitionType,
        duration: this.transitionDuration,
        isHeroMode: this.isHeroMode,
        hasActiveTransition: this.activeTransition !== null,
      };
    }

    /**
     * Clean up when module is destroyed.
     */
    destroy() {
      this.cleanupActiveTransition();

      // Remove any added classes
      this.slides.forEach((slide) => {
        slide.classList.remove('vvjs-active', 'vvjs-previous');
        slide.style.opacity = '';
        slide.style.zIndex = '';
      });

      if (this.debug) {
        console.log('[VVJS Transitions] Destroyed');
      }
    }
  }

  // Export to global namespace
  Drupal.vvjs = Drupal.vvjs || {};
  Drupal.vvjs.SlideshowTransitions = SlideshowTransitions;

})(Drupal);

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

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