gallery.js

import { queryOne, queryAll } from '@ecl/dom-utils';
import { createFocusTrap } from 'focus-trap';

/**
 * @param {HTMLElement} element DOM element for component instantiation and scope
 * @param {Object} options
 * @param {String} options.galleryItemSelector Selector for gallery element
 * @param {String} options.descriptionSelector Selector for gallery description element
 * @param {String} options.titleSelector Selector for gallery title element
 * @param {String} options.closeButtonSelector Selector for close button element
 * @param {String} options.allButtonSelector Selector for view all button element
 * @param {String} options.overlaySelector Selector for gallery overlay element
 * @param {String} options.overlayHeaderSelector Selector for gallery overlay header element
 * @param {String} options.overlayFooterSelector Selector for gallery overlay footer element
 * @param {String} options.overlayMediaSelector Selector for gallery overlay media element
 * @param {String} options.overlayCounterCurrentSelector Selector for gallery overlay current number element
 * @param {String} options.overlayCounterMaxSelector Selector for display of number of elements in the gallery overlay
 * @param {String} options.overlayDownloadSelector Selector for gallery overlay download element
 * @param {String} options.overlayShareSelector Selector for gallery overlay share element
 * @param {String} options.overlayDescriptionSelector Selector for gallery overlay description element
 * @param {String} options.overlayPreviousSelector Selector for gallery overlay previous link element
 * @param {String} options.overlayNextSelector Selector for gallery overlay next link element
 * @param {String} options.videoTitleSelector Selector for video title
 * @param {Boolean} options.attachClickListener Whether or not to bind click events
 * @param {Boolean} options.attachKeyListener Whether or not to bind keyup events
 */
export class Gallery {
  /**
   * @static
   * Shorthand for instance creation and initialisation.
   *
   * @param {HTMLElement} root DOM element for component instantiation and scope
   *
   * @return {Gallery} An instance of Gallery.
   */
  static autoInit(root, { GALLERY: defaultOptions = {} } = {}) {
    const gallery = new Gallery(root, defaultOptions);
    gallery.init();
    root.ECLGallery = gallery;
    return gallery;
  }

  constructor(
    element,
    {
      expandableSelector = 'data-ecl-gallery-not-expandable',
      galleryItemSelector = '[data-ecl-gallery-item]',
      descriptionSelector = '[data-ecl-gallery-description]',
      titleSelector = '[data-ecl-gallery-title]',
      noOverlaySelector = 'data-ecl-gallery-no-overlay',
      itemsLimitSelector = 'data-ecl-gallery-visible-items',
      closeButtonSelector = '[data-ecl-gallery-close]',
      viewAllSelector = '[data-ecl-gallery-all]',
      viewAllLabelSelector = 'data-ecl-gallery-collapsed-label',
      viewAllExpandedLabelSelector = 'data-ecl-gallery-expanded-label',
      countSelector = '[data-ecl-gallery-count]',
      overlaySelector = '[data-ecl-gallery-overlay]',
      overlayHeaderSelector = '[data-ecl-gallery-overlay-header]',
      overlayFooterSelector = '[data-ecl-gallery-overlay-footer]',
      overlayMediaSelector = '[data-ecl-gallery-overlay-media]',
      overlayCounterCurrentSelector = '[data-ecl-gallery-overlay-counter-current]',
      overlayCounterMaxSelector = '[data-ecl-gallery-overlay-counter-max]',
      overlayDownloadSelector = '[data-ecl-gallery-overlay-download]',
      overlayShareSelector = '[data-ecl-gallery-overlay-share]',
      overlayDescriptionSelector = '[data-ecl-gallery-overlay-description]',
      overlayPreviousSelector = '[data-ecl-gallery-overlay-previous]',
      overlayNextSelector = '[data-ecl-gallery-overlay-next]',
      videoTitleSelector = 'data-ecl-gallery-item-video-title',
      attachClickListener = true,
      attachKeyListener = true,
      attachResizeListener = true,
    } = {},
  ) {
    // Check element
    if (!element || element.nodeType !== Node.ELEMENT_NODE) {
      throw new TypeError(
        'DOM element should be given to initialize this widget.',
      );
    }

    this.element = element;

    // Options
    this.galleryItemSelector = galleryItemSelector;
    this.descriptionSelector = descriptionSelector;
    this.titleSelector = titleSelector;
    this.closeButtonSelector = closeButtonSelector;
    this.viewAllSelector = viewAllSelector;
    this.countSelector = countSelector;
    this.itemsLimitSelector = itemsLimitSelector;
    this.overlaySelector = overlaySelector;
    this.noOverlaySelector = noOverlaySelector;
    this.overlayHeaderSelector = overlayHeaderSelector;
    this.overlayFooterSelector = overlayFooterSelector;
    this.overlayMediaSelector = overlayMediaSelector;
    this.overlayCounterCurrentSelector = overlayCounterCurrentSelector;
    this.overlayCounterMaxSelector = overlayCounterMaxSelector;
    this.overlayDownloadSelector = overlayDownloadSelector;
    this.overlayShareSelector = overlayShareSelector;
    this.overlayDescriptionSelector = overlayDescriptionSelector;
    this.overlayPreviousSelector = overlayPreviousSelector;
    this.overlayNextSelector = overlayNextSelector;
    this.attachClickListener = attachClickListener;
    this.attachKeyListener = attachKeyListener;
    this.attachResizeListener = attachResizeListener;
    this.viewAllLabelSelector = viewAllLabelSelector;
    this.viewAllExpandedLabelSelector = viewAllExpandedLabelSelector;
    this.expandableSelector = expandableSelector;
    this.videoTitleSelector = videoTitleSelector;

    // Private variables
    this.galleryItems = null;
    this.closeButton = null;
    this.viewAll = null;
    this.count = null;
    this.overlay = null;
    this.overlayHeader = null;
    this.overlayFooter = null;
    this.overlayMedia = null;
    this.overlayCounterCurrent = null;
    this.overlayCounterMax = null;
    this.overlayDownload = null;
    this.overlayShare = null;
    this.overlayDescription = null;
    this.overlayPrevious = null;
    this.overlayNext = null;
    this.selectedItem = null;
    this.focusTrap = null;
    this.isDesktop = false;
    this.resizeTimer = null;
    this.visibleItems = 0;
    this.breakpointMd = 768;
    this.breakpointLg = 996;
    this.imageHeight = 185;
    this.imageHeightBig = 260;

    // Bind `this` for use in callbacks
    this.iframeResize = this.iframeResize.bind(this);
    this.handleClickOnCloseButton = this.handleClickOnCloseButton.bind(this);
    this.handleClickOnViewAll = this.handleClickOnViewAll.bind(this);
    this.handleClickOnItem = this.handleClickOnItem.bind(this);
    this.preventClickOnItem = this.preventClickOnItem.bind(this);
    this.handleKeyPressOnItem = this.handleKeyPressOnItem.bind(this);
    this.handleClickOnPreviousButton =
      this.handleClickOnPreviousButton.bind(this);
    this.handleClickOnNextButton = this.handleClickOnNextButton.bind(this);
    this.handleKeyboard = this.handleKeyboard.bind(this);
    this.handleResize = this.handleResize.bind(this);
  }

  /**
   * Initialise component.
   */
  init() {
    if (!ECL) {
      throw new TypeError('Called init but ECL is not present');
    }
    ECL.components = ECL.components || new Map();
    // Query elements
    this.expandable = !this.element.hasAttribute(this.expandableSelector);
    this.visibleItems = this.element.getAttribute(this.itemsLimitSelector);
    this.galleryItems = queryAll(this.galleryItemSelector, this.element);
    this.closeButton = queryOne(this.closeButtonSelector, this.element);
    this.noOverlay = this.element.hasAttribute(this.noOverlaySelector);
    if (this.expandable) {
      this.viewAll = queryOne(this.viewAllSelector, this.element);
      this.viewAllLabel =
        this.viewAll.getAttribute(this.viewAllLabelSelector) ||
        this.viewAll.innerText;
      this.viewAllLabelExpanded =
        this.viewAll.getAttribute(this.viewAllExpandedLabelSelector) ||
        this.viewAllLabel;
    }
    this.count = queryOne(this.countSelector, this.element);

    // Bind click event on view all (open first item)
    if (this.attachClickListener && this.viewAll) {
      this.viewAll.addEventListener('click', this.handleClickOnViewAll);
    }
    if (!this.noOverlay) {
      this.overlay = queryOne(this.overlaySelector, this.element);
      this.overlayHeader = queryOne(this.overlayHeaderSelector, this.overlay);
      this.overlayFooter = queryOne(this.overlayFooterSelector, this.overlay);
      this.overlayMedia = queryOne(this.overlayMediaSelector, this.overlay);
      this.overlayCounterCurrent = queryOne(
        this.overlayCounterCurrentSelector,
        this.overlay,
      );
      this.overlayCounterMax = queryOne(
        this.overlayCounterMaxSelector,
        this.overlay,
      );
      this.overlayDownload = queryOne(
        this.overlayDownloadSelector,
        this.overlay,
      );
      this.overlayShare = queryOne(this.overlayShareSelector, this.overlay);
      this.overlayDescription = queryOne(
        this.overlayDescriptionSelector,
        this.overlay,
      );
      this.overlayPrevious = queryOne(
        this.overlayPreviousSelector,
        this.overlay,
      );
      this.overlayNext = queryOne(this.overlayNextSelector, this.overlay);

      // Create focus trap
      this.focusTrap = createFocusTrap(this.overlay, {
        escapeDeactivates: false,
        returnFocusOnDeactivate: false,
      });

      // Polyfill to support <dialog>
      this.isDialogSupported = true;
      if (!window.HTMLDialogElement) {
        this.isDialogSupported = false;
      }

      // Bind click event on close button
      if (this.attachClickListener && this.closeButton) {
        this.closeButton.addEventListener(
          'click',
          this.handleClickOnCloseButton,
        );
      }

      // Bind click event on gallery items
      if (this.attachClickListener && this.galleryItems) {
        this.galleryItems.forEach((galleryItem) => {
          if (this.attachClickListener) {
            galleryItem.addEventListener('click', this.handleClickOnItem);
          }
          if (this.attachKeyListener) {
            galleryItem.addEventListener('keyup', this.handleKeyPressOnItem);
          }
        });
      }

      // Bind click event on previous button
      if (this.attachClickListener && this.overlayPrevious) {
        this.overlayPrevious.addEventListener(
          'click',
          this.handleClickOnPreviousButton,
        );
      }

      // Bind click event on next button
      if (this.attachClickListener && this.overlayNext) {
        this.overlayNext.addEventListener(
          'click',
          this.handleClickOnNextButton,
        );
      }

      // Bind other close event
      if (!this.isDialogSupported && this.attachKeyListener && this.overlay) {
        this.overlay.addEventListener('keyup', this.handleKeyboard);
      }
      if (this.isDialogSupported && this.overlay) {
        this.overlay.addEventListener('close', this.handleClickOnCloseButton);
      }
    } else {
      this.galleryItems.forEach((galleryItem) => {
        galleryItem.classList.add('ecl-gallery__item__link--frozen');
        galleryItem.addEventListener('click', this.preventClickOnItem);
      });
    }

    // Bind resize events
    if (this.attachResizeListener) {
      window.addEventListener('resize', this.handleResize);
    }

    // Init display of gallery items
    if (this.expandable) {
      this.checkScreen();
      this.hideItems();
    }

    // Add number to gallery items
    this.galleryItems.forEach((galleryItem, key) => {
      galleryItem.setAttribute('data-ecl-gallery-item-id', key);
    });

    // Update counter
    if (this.count) {
      this.count.innerHTML = this.galleryItems.length;
    }

    // Set ecl initialized attribute
    this.element.setAttribute('data-ecl-auto-initialized', 'true');
    ECL.components.set(this.element, this);
  }

  /**
   * Destroy component.
   */
  destroy() {
    if (this.attachClickListener && this.closeButton) {
      this.closeButton.removeEventListener(
        'click',
        this.handleClickOnCloseButton,
      );
    }

    if (this.attachClickListener && this.viewAll) {
      this.viewAll.removeEventListener('click', this.handleClickOnViewAll);
    }

    if (this.attachClickListener && this.galleryItems) {
      this.galleryItems.forEach((galleryItem) => {
        galleryItem.removeEventListener('click', this.handleClickOnItem);
      });
    }

    if (this.attachClickListener && this.overlayPrevious) {
      this.overlayPrevious.removeEventListener(
        'click',
        this.handleClickOnPreviousButton,
      );
    }

    if (this.attachClickListener && this.overlayNext) {
      this.overlayNext.removeEventListener(
        'click',
        this.handleClickOnNextButton,
      );
    }

    if (!this.isDialogSupported && this.attachKeyListener && this.overlay) {
      this.overlay.removeEventListener('keyup', this.handleKeyboard);
    }
    if (this.isDialogSupported && this.overlay) {
      this.overlay.removeEventListener('close', this.handleClickOnCloseButton);
    }

    if (this.attachResizeListener) {
      window.removeEventListener('resize', this.handleResize);
    }

    if (this.element) {
      this.element.removeAttribute('data-ecl-auto-initialized');
      ECL.components.delete(this.element);
    }
  }

  /**
   * Check if current display is desktop or mobile
   */
  checkScreen() {
    if (window.innerWidth > this.breakpointMd) {
      this.isDesktop = true;
    } else {
      this.isDesktop = false;
    }
    if (window.innerWidth > this.breakpointLg) {
      this.isLarge = true;
    }
  }

  iframeResize(iframe) {
    if (!iframe && this.overlay) {
      iframe = queryOne('iframe', this.overlay);
    }

    if (iframe) {
      const width = window.innerWidth;

      setTimeout(() => {
        const height =
          this.overlay.clientHeight -
          this.overlayHeader.clientHeight -
          this.overlayFooter.clientHeight;

        if (width > height) {
          iframe.setAttribute('height', `${height}px`);

          if ((height * 16) / 9 > width) {
            iframe.setAttribute('width', `${width - 0.05 * width}px`);
          } else {
            iframe.setAttribute('width', `${(height * 16) / 9}px`);
          }
        } else {
          iframe.setAttribute('width', `${width}px`);
          if ((width * 4) / 3 > height) {
            iframe.setAttribute('height', `${height - 0.05 * height}px`);
          } else {
            iframe.setAttribute('height', `${(width * 4) / 3}px`);
          }
        }
      }, 0);
    }
  }

  /**
   * @param {Int} rows/item number
   *
   * Hide several gallery items by default
   * - 2 "lines" of items on desktop
   * - only 3 items on mobile or the desired rows or items
   *   when using the view more button.
   */
  hideItems(plus = 0) {
    if (!this.viewAll || this.viewAll.expanded) return;

    if (this.isDesktop) {
      let hiddenItemIds = [];
      // We should browse the list first to mark the items to be hidden, and hide them later
      // otherwise, it will interfer with the calculus
      this.galleryItems.forEach((galleryItem, key) => {
        galleryItem.parentNode.classList.remove('ecl-gallery__item--hidden');
        if (key >= Number(this.visibleItems) + Number(plus)) {
          hiddenItemIds = [...hiddenItemIds, key];
        }
      });
      hiddenItemIds.forEach((id) => {
        this.galleryItems[id].parentNode.classList.add(
          'ecl-gallery__item--hidden',
        );
      });
      return;
    }

    this.galleryItems.forEach((galleryItem, key) => {
      if (key > 2 + Number(plus)) {
        galleryItem.parentNode.classList.add('ecl-gallery__item--hidden');
      } else {
        galleryItem.parentNode.classList.remove('ecl-gallery__item--hidden');
      }
    });
  }

  /**
   * Trigger events on resize
   * Uses a debounce, for performance
   */
  handleResize() {
    clearTimeout(this.resizeTimer);
    this.resizeTimer = setTimeout(() => {
      this.checkScreen();
      this.hideItems();
      this.iframeResize();
    }, 200);
  }

  /**
   * @param {HTMLElement} selectedItem Media element
   */
  updateOverlay(selectedItem) {
    this.selectedItem = selectedItem;
    const embeddedVideo = selectedItem.getAttribute(
      'data-ecl-gallery-item-embed-src',
    );
    const embeddedVideoAudio = selectedItem.getAttribute(
      'data-ecl-gallery-item-embed-audio',
    );
    const video = queryOne('video', selectedItem);
    let mediaElement = null;

    // Update media
    if (embeddedVideo != null) {
      // Media is a embedded video
      mediaElement = document.createElement('div');
      mediaElement.classList.add('ecl-gallery__slider-embed');

      const mediaAudio = document.createElement('div');
      mediaAudio.classList.add('ecl-gallery__slider-embed-audio');
      if (embeddedVideoAudio) {
        mediaAudio.innerHTML = embeddedVideoAudio;
      }

      const iframeUrl = new URL(embeddedVideo);

      const mediaIframe = document.createElement('iframe');
      mediaIframe.setAttribute('src', iframeUrl);
      mediaIframe.setAttribute('frameBorder', '0');

      // Update iframe title
      const videoTitle = selectedItem.getAttribute(this.videoTitleSelector);
      if (videoTitle) {
        mediaIframe.setAttribute('title', videoTitle);
      }

      if (this.overlayMedia) {
        if (embeddedVideoAudio) {
          mediaElement.appendChild(mediaAudio);
        }
        mediaElement.appendChild(mediaIframe);
        this.overlayMedia.innerHTML = '';
        this.overlayMedia.appendChild(mediaElement);
      }
      this.iframeResize(mediaIframe);
    } else if (video != null) {
      // Media is a video
      mediaElement = document.createElement('video');
      mediaElement.setAttribute('poster', video.poster);
      mediaElement.setAttribute('controls', 'controls');
      mediaElement.classList.add('ecl-gallery__slider-video');

      // Update video title
      const videoTitle = selectedItem.getAttribute(this.videoTitleSelector);
      if (videoTitle) {
        mediaElement.setAttribute('aria-label', videoTitle);
      }

      if (this.overlayMedia) {
        this.overlayMedia.innerHTML = '';
        this.overlayMedia.appendChild(mediaElement);
      }

      // Get sources
      const sources = queryAll('source', video);
      sources.forEach((source) => {
        const sourceTag = document.createElement('source');
        sourceTag.setAttribute('src', source.getAttribute('src'));
        sourceTag.setAttribute('type', source.getAttribute('type'));
        mediaElement.appendChild(sourceTag);
      });

      // Get tracks
      const tracks = queryAll('track', video);
      tracks.forEach((track) => {
        const trackTag = document.createElement('track');
        trackTag.setAttribute('src', track.getAttribute('src'));
        trackTag.setAttribute('kind', track.getAttribute('kind'));
        trackTag.setAttribute('srclang', track.getAttribute('srcLang'));
        trackTag.setAttribute('label', track.getAttribute('label'));
        mediaElement.appendChild(trackTag);
      });

      mediaElement.load();
    } else {
      // Media is an image
      const picture = queryOne('.ecl-gallery__picture', selectedItem);
      const image = queryOne('img', picture);
      if (picture) {
        image.classList.remove('ecl-gallery__image');
        mediaElement = picture.cloneNode(true);
      } else {
        // backward compatibility
        mediaElement = document.createElement('img');
        mediaElement.setAttribute('src', image.getAttribute('src'));
        mediaElement.setAttribute('alt', image.getAttribute('alt'));
      }
      mediaElement.classList.add('ecl-gallery__slider-image');

      if (this.overlayMedia) {
        this.overlayMedia.innerHTML = '';
        this.overlayMedia.appendChild(mediaElement);
      }
    }

    // Get id
    const id = selectedItem.getAttribute('id');

    // Update counter
    this.overlayCounterCurrent.innerHTML =
      +selectedItem.getAttribute('data-ecl-gallery-item-id') + 1;
    this.overlayCounterMax.innerHTML = this.galleryItems.length;

    // Prepare display of links for mobile
    const actionMobile = document.createElement('div');
    actionMobile.classList.add('ecl-gallery__detail-actions-mobile');

    // Update download link
    if (this.overlayDownload !== null && embeddedVideo === null) {
      this.overlayDownload.href = this.selectedItem.href;
      if (id) {
        this.overlayDownload.setAttribute('aria-describedby', `${id}-title`);
      }
      this.overlayDownload.hidden = false;
      actionMobile.appendChild(this.overlayDownload.cloneNode(true));
    } else if (this.overlayDownload !== null) {
      this.overlayDownload.hidden = true;
    }

    // Update share link
    const shareHref = this.selectedItem.getAttribute(
      'data-ecl-gallery-item-share',
    );
    if (shareHref != null) {
      this.overlayShare.href = shareHref;
      if (id) {
        this.overlayShare.setAttribute('aria-describedby', `${id}-title`);
      }
      this.overlayShare.hidden = false;
      actionMobile.appendChild(this.overlayShare.cloneNode(true));
    } else {
      this.overlayShare.hidden = true;
    }

    // Update description
    const description = queryOne(this.descriptionSelector, selectedItem);
    if (description) {
      this.overlayDescription.innerHTML = description.innerHTML;
    }
    if (actionMobile.childNodes.length > 0) {
      this.overlayDescription.prepend(actionMobile);
    }
  }

  /**
   * Handles keyboard events such as Escape and navigation.
   *
   * @param {Event} e
   */
  handleKeyboard(e) {
    // Detect press on Escape
    // Only used if the browser do not support <dialog>
    if (e.key === 'Escape' || e.key === 'Esc') {
      this.handleClickOnCloseButton();
    }
  }

  /**
   * Invoke listeners for close events.
   */
  handleClickOnCloseButton() {
    if (this.isDialogSupported) {
      this.overlay.close();
    } else {
      this.overlay.removeAttribute('open');
    }

    // Remove iframe
    const embeddedVideo = queryOne('iframe', this.overlayMedia);
    if (embeddedVideo) embeddedVideo.remove();

    // Stop video
    const video = queryOne('video', this.overlayMedia);
    if (video) video.pause();

    // Untrap focus
    this.focusTrap.deactivate();

    // Restore css class on items
    this.galleryItems.forEach((galleryItem) => {
      const image = queryOne('img', galleryItem);
      if (image) {
        image.classList.add('ecl-gallery__image');
      }
    });

    // Focus item
    this.selectedItem.focus();

    // Enable scroll on body
    document.body.classList.remove('ecl-u-disablescroll');
  }

  /**
   * Invoke listeners for on pressing the spacebar button.
   *
   * @param {Event} e
   */
  handleKeyPressOnItem(e) {
    if (e.keyCode === 32) {
      // If spacebar trigger the modal
      this.handleClickOnItem(e);
    }
  }

  /**
   * Invoke listeners for on click events on view all.
   *
   * @param {Event} e
   */
  handleClickOnViewAll(e) {
    e.preventDefault();
    if (!this.viewAll) return;

    if (this.viewAll.expanded) {
      delete this.viewAll.expanded;
      this.checkScreen();
      this.hideItems();
      this.viewAll.textContent = this.viewAllLabel;
    } else {
      this.viewAll.expanded = true;
      this.viewAll.textContent = this.viewAllLabelExpanded;

      const hidden = this.galleryItems.filter((item) =>
        item.parentNode.classList.contains('ecl-gallery__item--hidden'),
      );
      if (hidden.length > 0) {
        hidden.forEach((item) => {
          item.parentNode.classList.remove('ecl-gallery__item--hidden');
        });
        hidden[0].focus();
      }
    }
  }

  /**
   * Invoke listeners for on click events on the given gallery item.
   *
   * @param {Event} e
   */
  handleClickOnItem(e) {
    e.preventDefault();

    // Disable scroll on body
    document.body.classList.add('ecl-u-disablescroll');

    // Display overlay
    if (this.isDialogSupported) {
      this.overlay.showModal();
    } else {
      this.overlay.setAttribute('open', '');
    }

    // Update overlay
    this.updateOverlay(e.currentTarget);

    // Trap focus
    this.focusTrap.activate();
  }

  /**
   * handle click event on gallery items when no overlay.
   *
   * @param {Event} e
   */
  // eslint-disable-next-line class-methods-use-this
  preventClickOnItem(e) {
    e.preventDefault();
    e.stopPropagation();
  }

  /**
   * Invoke listeners for on click events on previous navigation link.
   */
  handleClickOnPreviousButton() {
    // Get current id
    const currentId = this.selectedItem.getAttribute(
      'data-ecl-gallery-item-id',
    );

    // Get previous id
    let previousId = +currentId - 1;
    if (previousId < 0) previousId = this.galleryItems.length - 1;

    // Stop video
    const video = queryOne('video', this.selectedItem);
    if (video) video.pause();

    // Update overlay
    this.updateOverlay(this.galleryItems[previousId]);
    this.selectedItem = this.galleryItems[previousId];

    return this;
  }

  /**
   * Invoke listeners for on click events on next navigation link.
   */
  handleClickOnNextButton() {
    // Get current id
    const currentId = this.selectedItem.getAttribute(
      'data-ecl-gallery-item-id',
    );

    // Get next id
    let nextId = +currentId + 1;
    if (nextId >= this.galleryItems.length) nextId = 0;

    // Stop video
    const video = queryOne('video', this.selectedItem);
    if (video) video.pause();

    // Update overlay
    this.updateOverlay(this.galleryItems[nextId]);
    this.selectedItem = this.galleryItems[nextId];

    return this;
  }
}

export default Gallery;