chunk-013-lightbox.js

I’ll make this as a standalone advanced lightbox chunk for RX Theme: accessible modal, gallery navigation, keyboard control, touch swipe, zoom, captions, download/share hooks, lazy image support, and safe initialization without breaking pages.

Below is a complete advanced-level JavaScript file for:

assets/static-js/static-chunks-js/chunk-013-lightbox.js

It is written as a theme-safe standalone chunk. It will work without jQuery and can auto-detect image links, galleries, captions, keyboard, swipe, zoom, fullscreen, download, share, and accessibility.

/*!
 * RX Theme - Advanced Lightbox Chunk
 * File: assets/static-js/static-chunks-js/chunk-013-lightbox.js
 * Version: 1.0.0
 *
 * Purpose:
 * - Advanced image lightbox for RX Theme
 * - No jQuery required
 * - Accessible modal
 * - Keyboard navigation
 * - Swipe support
 * - Zoom and pan
 * - Gallery grouping
 * - Captions
 * - Fullscreen
 * - Share button
 * - Download button
 * - Lazy loading support
 * - Safe auto initialization
 */

(function () {
  'use strict';

  /**
   * Prevent double loading.
   */
  if (window.RXThemeLightboxLoaded) {
    return;
  }

  window.RXThemeLightboxLoaded = true;

  /**
   * Main namespace.
   */
  window.RXTheme = window.RXTheme || {};

  /**
   * Default settings.
   */
  const DEFAULTS = {
    selector:
      'a[href$=".jpg"], a[href$=".jpeg"], a[href$=".png"], a[href$=".webp"], a[href$=".gif"], a[href$=".avif"], a[data-rx-lightbox]',
    galleryAttribute: 'data-rx-gallery',
    captionAttribute: 'data-rx-caption',
    titleAttribute: 'title',
    altAttribute: 'alt',
    closeOnOverlayClick: true,
    closeOnEscape: true,
    loop: true,
    keyboard: true,
    swipe: true,
    zoom: true,
    fullscreen: true,
    share: true,
    download: true,
    counter: true,
    preload: true,
    preloadNext: true,
    preloadPrev: true,
    animationDuration: 220,
    maxZoom: 4,
    minZoom: 1,
    zoomStep: 0.5,
    dragFriction: 0.9,
    swipeThreshold: 45,
    classPrefix: 'rx-lightbox',
    bodyOpenClass: 'rx-lightbox-open',
    activeClass: 'is-active',
    loadingClass: 'is-loading',
    zoomedClass: 'is-zoomed',
    fullscreenClass: 'is-fullscreen',
    hiddenClass: 'is-hidden'
  };

  /**
   * Utility helpers.
   */
  const RXUtils = {
    extend(target, source) {
      const output = Object.assign({}, target || {});
      if (!source) return output;

      Object.keys(source).forEach(function (key) {
        output[key] = source[key];
      });

      return output;
    },

    qs(selector, context) {
      return (context || document).querySelector(selector);
    },

    qsa(selector, context) {
      return Array.prototype.slice.call(
        (context || document).querySelectorAll(selector)
      );
    },

    isElement(value) {
      return value instanceof Element || value instanceof HTMLDocument;
    },

    isImageLink(element) {
      if (!element || !element.getAttribute) return false;

      const href = element.getAttribute('href') || '';
      const dataSrc = element.getAttribute('data-rx-src') || '';

      return /\.(jpg|jpeg|png|webp|gif|avif)(\?.*)?$/i.test(href || dataSrc);
    },

    getImageSrc(element) {
      if (!element) return '';

      return (
        element.getAttribute('data-rx-src') ||
        element.getAttribute('href') ||
        element.getAttribute('src') ||
        ''
      );
    },

    getFileNameFromUrl(url) {
      try {
        const clean = url.split('?')[0].split('#')[0];
        return decodeURIComponent(clean.substring(clean.lastIndexOf('/') + 1));
      } catch (error) {
        return 'rx-image';
      }
    },

    clamp(value, min, max) {
      return Math.min(Math.max(value, min), max);
    },

    createElement(tag, className, attrs) {
      const element = document.createElement(tag);

      if (className) {
        element.className = className;
      }

      if (attrs && typeof attrs === 'object') {
        Object.keys(attrs).forEach(function (key) {
          if (key === 'text') {
            element.textContent = attrs[key];
          } else if (key === 'html') {
            element.innerHTML = attrs[key];
          } else {
            element.setAttribute(key, attrs[key]);
          }
        });
      }

      return element;
    },

    lockBody(className) {
      document.documentElement.classList.add(className);
      document.body.classList.add(className);
    },

    unlockBody(className) {
      document.documentElement.classList.remove(className);
      document.body.classList.remove(className);
    },

    supportsFullscreen() {
      return Boolean(
        document.fullscreenEnabled ||
          document.webkitFullscreenEnabled ||
          document.mozFullScreenEnabled ||
          document.msFullscreenEnabled
      );
    },

    requestFullscreen(element) {
      if (!element) return;

      if (element.requestFullscreen) {
        element.requestFullscreen();
      } else if (element.webkitRequestFullscreen) {
        element.webkitRequestFullscreen();
      } else if (element.mozRequestFullScreen) {
        element.mozRequestFullScreen();
      } else if (element.msRequestFullscreen) {
        element.msRequestFullscreen();
      }
    },

    exitFullscreen() {
      if (document.exitFullscreen) {
        document.exitFullscreen();
      } else if (document.webkitExitFullscreen) {
        document.webkitExitFullscreen();
      } else if (document.mozCancelFullScreen) {
        document.mozCancelFullScreen();
      } else if (document.msExitFullscreen) {
        document.msExitFullscreen();
      }
    },

    isFullscreen() {
      return Boolean(
        document.fullscreenElement ||
          document.webkitFullscreenElement ||
          document.mozFullScreenElement ||
          document.msFullscreenElement
      );
    },

    prefersReducedMotion() {
      return window.matchMedia &&
        window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    },

    safeText(value) {
      if (!value) return '';
      return String(value).replace(/\s+/g, ' ').trim();
    },

    getFocusableElements(container) {
      if (!container) return [];

      return RXUtils.qsa(
        'a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])',
        container
      );
    }
  };

  /**
   * Main Lightbox class.
   */
  class RXAdvancedLightbox {
    constructor(options) {
      this.settings = RXUtils.extend(DEFAULTS, options || {});
      this.items = [];
      this.currentIndex = 0;
      this.isOpen = false;
      this.isLoading = false;
      this.lastFocusedElement = null;

      this.scale = 1;
      this.translateX = 0;
      this.translateY = 0;
      this.dragStartX = 0;
      this.dragStartY = 0;
      this.dragCurrentX = 0;
      this.dragCurrentY = 0;
      this.isDragging = false;
      this.hasDragged = false;

      this.touchStartX = 0;
      this.touchStartY = 0;
      this.touchEndX = 0;
      this.touchEndY = 0;

      this.boundHandleDocumentClick = this.handleDocumentClick.bind(this);
      this.boundHandleKeydown = this.handleKeydown.bind(this);
      this.boundHandleResize = this.handleResize.bind(this);
      this.boundTrapFocus = this.trapFocus.bind(this);

      this.init();
    }

    init() {
      this.collectItems();
      this.build();
      this.bindGlobalEvents();
      this.prepareAutoElements();
      this.dispatch('rxLightboxReady', {
        total: this.items.length
      });
    }

    collectItems() {
      const selector = this.settings.selector;
      const elements = RXUtils.qsa(selector);

      this.items = elements
        .filter(function (element) {
          return RXUtils.isImageLink(element);
        })
        .map((element, index) => {
          const image = element.querySelector('img');
          const src = RXUtils.getImageSrc(element);

          let caption =
            element.getAttribute(this.settings.captionAttribute) ||
            element.getAttribute(this.settings.titleAttribute) ||
            '';

          if (!caption && image) {
            caption =
              image.getAttribute(this.settings.altAttribute) ||
              image.getAttribute(this.settings.titleAttribute) ||
              '';
          }

          const gallery =
            element.getAttribute(this.settings.galleryAttribute) ||
            element.closest('[data-rx-gallery]')?.getAttribute('data-rx-gallery') ||
            'rx-default-gallery';

          element.setAttribute('data-rx-lightbox-index', String(index));
          element.setAttribute('data-rx-lightbox-ready', 'true');

          return {
            index,
            element,
            src,
            thumb: image ? image.currentSrc || image.src : src,
            caption: RXUtils.safeText(caption),
            title: RXUtils.safeText(element.getAttribute('title') || ''),
            gallery,
            width: element.getAttribute('data-rx-width') || '',
            height: element.getAttribute('data-rx-height') || '',
            download:
              element.getAttribute('data-rx-download') ||
              element.getAttribute('download') ||
              ''
          };
        });
    }

    refresh() {
      this.collectItems();
      this.prepareAutoElements();

      if (this.isOpen) {
        this.updateCounter();
      }

      this.dispatch('rxLightboxRefresh', {
        total: this.items.length
      });
    }

    build() {
      const prefix = this.settings.classPrefix;

      this.overlay = RXUtils.createElement('div', `${prefix} ${prefix}--hidden`, {
        role: 'dialog',
        'aria-modal': 'true',
        'aria-label': 'Image lightbox',
        tabindex: '-1'
      });

      this.backdrop = RXUtils.createElement('div', `${prefix}__backdrop`);

      this.stage = RXUtils.createElement('div', `${prefix}__stage`);

      this.mediaWrap = RXUtils.createElement('div', `${prefix}__media-wrap`);

      this.loader = RXUtils.createElement('div', `${prefix}__loader`, {
        'aria-hidden': 'true'
      });

      this.image = RXUtils.createElement('img', `${prefix}__image`, {
        alt: '',
        draggable: 'false'
      });

      this.caption = RXUtils.createElement('div', `${prefix}__caption`, {
        'aria-live': 'polite'
      });

      this.counter = RXUtils.createElement('div', `${prefix}__counter`, {
        'aria-live': 'polite'
      });

      this.toolbar = RXUtils.createElement('div', `${prefix}__toolbar`);

      this.btnClose = this.createButton('close', 'Close lightbox', '×');
      this.btnPrev = this.createButton('prev', 'Previous image', '‹');
      this.btnNext = this.createButton('next', 'Next image', '›');
      this.btnZoomIn = this.createButton('zoom-in', 'Zoom in', '+');
      this.btnZoomOut = this.createButton('zoom-out', 'Zoom out', '−');
      this.btnZoomReset = this.createButton('zoom-reset', 'Reset zoom', '1:1');
      this.btnFullscreen = this.createButton('fullscreen', 'Toggle fullscreen', '⛶');
      this.btnShare = this.createButton('share', 'Share image', '↗');
      this.btnDownload = this.createButton('download', 'Download image', '↓');

      this.toolbar.appendChild(this.btnZoomOut);
      this.toolbar.appendChild(this.btnZoomReset);
      this.toolbar.appendChild(this.btnZoomIn);

      if (this.settings.fullscreen && RXUtils.supportsFullscreen()) {
        this.toolbar.appendChild(this.btnFullscreen);
      }

      if (this.settings.share) {
        this.toolbar.appendChild(this.btnShare);
      }

      if (this.settings.download) {
        this.toolbar.appendChild(this.btnDownload);
      }

      this.toolbar.appendChild(this.btnClose);

      this.mediaWrap.appendChild(this.loader);
      this.mediaWrap.appendChild(this.image);

      this.stage.appendChild(this.btnPrev);
      this.stage.appendChild(this.mediaWrap);
      this.stage.appendChild(this.btnNext);

      this.overlay.appendChild(this.backdrop);
      this.overlay.appendChild(this.toolbar);

      if (this.settings.counter) {
        this.overlay.appendChild(this.counter);
      }

      this.overlay.appendChild(this.stage);
      this.overlay.appendChild(this.caption);

      document.body.appendChild(this.overlay);

      this.bindLightboxEvents();
    }

    createButton(name, label, text) {
      const prefix = this.settings.classPrefix;

      return RXUtils.createElement('button', `${prefix}__button ${prefix}__button--${name}`, {
        type: 'button',
        'aria-label': label,
        title: label,
        text
      });
    }

    bindGlobalEvents() {
      document.addEventListener('click', this.boundHandleDocumentClick, false);
      document.addEventListener('keydown', this.boundHandleKeydown, false);
      window.addEventListener('resize', this.boundHandleResize, false);
    }

    bindLightboxEvents() {
      this.btnClose.addEventListener('click', () => this.close());
      this.btnPrev.addEventListener('click', () => this.prev());
      this.btnNext.addEventListener('click', () => this.next());
      this.btnZoomIn.addEventListener('click', () => this.zoomIn());
      this.btnZoomOut.addEventListener('click', () => this.zoomOut());
      this.btnZoomReset.addEventListener('click', () => this.resetZoom());
      this.btnFullscreen.addEventListener('click', () => this.toggleFullscreen());
      this.btnShare.addEventListener('click', () => this.shareCurrent());
      this.btnDownload.addEventListener('click', () => this.downloadCurrent());

      this.backdrop.addEventListener('click', () => {
        if (this.settings.closeOnOverlayClick) {
          this.close();
        }
      });

      this.stage.addEventListener('click', (event) => {
        if (
          this.settings.closeOnOverlayClick &&
          event.target === this.stage &&
          this.scale === 1
        ) {
          this.close();
        }
      });

      this.image.addEventListener('load', () => this.onImageLoad());
      this.image.addEventListener('error', () => this.onImageError());

      this.image.addEventListener('dblclick', (event) => {
        event.preventDefault();

        if (!this.settings.zoom) return;

        if (this.scale > 1) {
          this.resetZoom();
        } else {
          this.zoomTo(2, event.clientX, event.clientY);
        }
      });

      this.mediaWrap.addEventListener('wheel', (event) => {
        if (!this.settings.zoom) return;

        event.preventDefault();

        const direction = event.deltaY < 0 ? 1 : -1;
        const nextScale = this.scale + direction * this.settings.zoomStep;

        this.zoomTo(nextScale, event.clientX, event.clientY);
      }, { passive: false });

      this.mediaWrap.addEventListener('mousedown', (event) => this.startDrag(event));
      window.addEventListener('mousemove', (event) => this.moveDrag(event));
      window.addEventListener('mouseup', () => this.endDrag());

      this.mediaWrap.addEventListener('touchstart', (event) => this.handleTouchStart(event), {
        passive: true
      });

      this.mediaWrap.addEventListener('touchmove', (event) => this.handleTouchMove(event), {
        passive: false
      });

      this.mediaWrap.addEventListener('touchend', (event) => this.handleTouchEnd(event), {
        passive: true
      });

      document.addEventListener('fullscreenchange', () => this.syncFullscreenClass());
      document.addEventListener('webkitfullscreenchange', () => this.syncFullscreenClass());
      document.addEventListener('mozfullscreenchange', () => this.syncFullscreenClass());
      document.addEventListener('MSFullscreenChange', () => this.syncFullscreenClass());
    }

    prepareAutoElements() {
      this.items.forEach((item) => {
        item.element.classList.add('rx-lightbox-trigger');

        if (!item.element.getAttribute('aria-label')) {
          item.element.setAttribute('aria-label', 'Open image in lightbox');
        }

        if (!item.element.getAttribute('data-rx-lightbox-prepared')) {
          item.element.setAttribute('data-rx-lightbox-prepared', 'true');
        }
      });
    }

    handleDocumentClick(event) {
      const trigger = event.target.closest(this.settings.selector);

      if (!trigger || !RXUtils.isImageLink(trigger)) {
        return;
      }

      const indexValue = trigger.getAttribute('data-rx-lightbox-index');

      if (indexValue === null) {
        this.refresh();
      }

      const index = Number(trigger.getAttribute('data-rx-lightbox-index'));

      if (!Number.isFinite(index)) {
        return;
      }

      event.preventDefault();
      this.open(index);
    }

    open(index) {
      if (!this.items[index]) return;

      this.lastFocusedElement = document.activeElement;
      this.currentIndex = index;
      this.isOpen = true;

      RXUtils.lockBody(this.settings.bodyOpenClass);

      this.overlay.classList.remove(`${this.settings.classPrefix}--hidden`);
      this.overlay.classList.add(this.settings.activeClass);

      this.overlay.focus();
      this.loadCurrent();

      document.addEventListener('focus', this.boundTrapFocus, true);

      this.dispatch('rxLightboxOpen', {
        index: this.currentIndex,
        item: this.items[this.currentIndex]
      });
    }

    close() {
      if (!this.isOpen) return;

      this.isOpen = false;
      this.isLoading = false;

      this.resetZoom();

      this.overlay.classList.remove(this.settings.activeClass);
      this.overlay.classList.add(`${this.settings.classPrefix}--hidden`);

      RXUtils.unlockBody(this.settings.bodyOpenClass);

      document.removeEventListener('focus', this.boundTrapFocus, true);

      if (RXUtils.isFullscreen()) {
        RXUtils.exitFullscreen();
      }

      this.image.removeAttribute('src');
      this.image.removeAttribute('srcset');

      if (
        this.lastFocusedElement &&
        typeof this.lastFocusedElement.focus === 'function'
      ) {
        this.lastFocusedElement.focus();
      }

      this.dispatch('rxLightboxClose', {});
    }

    loadCurrent() {
      const item = this.items[this.currentIndex];

      if (!item) return;

      this.isLoading = true;
      this.overlay.classList.add(this.settings.loadingClass);

      this.resetZoom(false);

      this.image.alt = item.caption || item.title || 'Lightbox image';

      this.caption.textContent = item.caption || '';

      this.updateCounter();
      this.updateNavigation();
      this.updateToolbar();

      const img = new Image();

      img.onload = () => {
        this.image.src = item.src;
      };

      img.onerror = () => {
        this.image.src = item.src;
      };

      img.src = item.src;

      if (this.settings.preload) {
        this.preloadAround();
      }

      this.dispatch('rxLightboxChange', {
        index: this.currentIndex,
        item
      });
    }

    onImageLoad() {
      this.isLoading = false;
      this.overlay.classList.remove(this.settings.loadingClass);

      this.dispatch('rxLightboxImageLoaded', {
        index: this.currentIndex,
        item: this.items[this.currentIndex]
      });
    }

    onImageError() {
      this.isLoading = false;
      this.overlay.classList.remove(this.settings.loadingClass);

      this.caption.textContent = 'Image could not be loaded.';

      this.dispatch('rxLightboxImageError', {
        index: this.currentIndex,
        item: this.items[this.currentIndex]
      });
    }

    next() {
      if (!this.items.length) return;

      const galleryItems = this.getCurrentGalleryItems();
      const currentGalleryIndex = galleryItems.findIndex(
        (item) => item.index === this.currentIndex
      );

      let nextGalleryIndex = currentGalleryIndex + 1;

      if (nextGalleryIndex >= galleryItems.length) {
        if (!this.settings.loop) return;
        nextGalleryIndex = 0;
      }

      this.currentIndex = galleryItems[nextGalleryIndex].index;
      this.loadCurrent();
    }

    prev() {
      if (!this.items.length) return;

      const galleryItems = this.getCurrentGalleryItems();
      const currentGalleryIndex = galleryItems.findIndex(
        (item) => item.index === this.currentIndex
      );

      let prevGalleryIndex = currentGalleryIndex - 1;

      if (prevGalleryIndex < 0) {
        if (!this.settings.loop) return;
        prevGalleryIndex = galleryItems.length - 1;
      }

      this.currentIndex = galleryItems[prevGalleryIndex].index;
      this.loadCurrent();
    }

    getCurrentGalleryItems() {
      const current = this.items[this.currentIndex];

      if (!current) return [];

      return this.items.filter(function (item) {
        return item.gallery === current.gallery;
      });
    }

    getCurrentGalleryPosition() {
      const galleryItems = this.getCurrentGalleryItems();
      const currentGalleryIndex = galleryItems.findIndex(
        (item) => item.index === this.currentIndex
      );

      return {
        current: currentGalleryIndex + 1,
        total: galleryItems.length
      };
    }

    updateCounter() {
      if (!this.settings.counter || !this.counter) return;

      const position = this.getCurrentGalleryPosition();

      this.counter.textContent = `${position.current} / ${position.total}`;
    }

    updateNavigation() {
      const galleryItems = this.getCurrentGalleryItems();
      const shouldShow = galleryItems.length > 1;

      this.btnPrev.classList.toggle(this.settings.hiddenClass, !shouldShow);
      this.btnNext.classList.toggle(this.settings.hiddenClass, !shouldShow);
    }

    updateToolbar() {
      const current = this.items[this.currentIndex];

      if (!current) return;

      this.btnDownload.classList.toggle(
        this.settings.hiddenClass,
        !this.settings.download
      );

      this.btnShare.classList.toggle(
        this.settings.hiddenClass,
        !this.settings.share
      );
    }

    preloadAround() {
      const galleryItems = this.getCurrentGalleryItems();
      const galleryPosition = galleryItems.findIndex(
        (item) => item.index === this.currentIndex
      );

      if (galleryPosition === -1) return;

      const preloadIndexes = [];

      if (this.settings.preloadNext) {
        preloadIndexes.push(galleryPosition + 1);
      }

      if (this.settings.preloadPrev) {
        preloadIndexes.push(galleryPosition - 1);
      }

      preloadIndexes.forEach((galleryIndex) => {
        let targetIndex = galleryIndex;

        if (targetIndex >= galleryItems.length) {
          targetIndex = 0;
        }

        if (targetIndex < 0) {
          targetIndex = galleryItems.length - 1;
        }

        const item = galleryItems[targetIndex];

        if (!item || item.preloaded) return;

        const image = new Image();
        image.src = item.src;
        item.preloaded = true;
      });
    }

    zoomIn() {
      this.zoomTo(this.scale + this.settings.zoomStep);
    }

    zoomOut() {
      this.zoomTo(this.scale - this.settings.zoomStep);
    }

    zoomTo(value, originX, originY) {
      if (!this.settings.zoom) return;

      const nextScale = RXUtils.clamp(
        value,
        this.settings.minZoom,
        this.settings.maxZoom
      );

      this.scale = nextScale;

      if (this.scale <= 1) {
        this.translateX = 0;
        this.translateY = 0;
      } else if (originX && originY) {
        const rect = this.mediaWrap.getBoundingClientRect();
        const offsetX = originX - rect.left - rect.width / 2;
        const offsetY = originY - rect.top - rect.height / 2;

        this.translateX -= offsetX * 0.08;
        this.translateY -= offsetY * 0.08;
      }

      this.applyTransform();
    }

    resetZoom(animate) {
      this.scale = 1;
      this.translateX = 0;
      this.translateY = 0;

      if (animate === false) {
        this.image.style.transition = 'none';
        this.applyTransform();

        window.requestAnimationFrame(() => {
          this.image.style.transition = '';
        });
      } else {
        this.applyTransform();
      }
    }

    applyTransform() {
      const prefix = this.settings.classPrefix;

      this.image.style.transform = `translate3d(${this.translateX}px, ${this.translateY}px, 0) scale(${this.scale})`;

      this.overlay.classList.toggle(this.settings.zoomedClass, this.scale > 1);
      this.btnZoomReset.textContent = this.scale > 1 ? `${this.scale.toFixed(1)}x` : '1:1';

      this.dispatch('rxLightboxZoom', {
        scale: this.scale,
        x: this.translateX,
        y: this.translateY
      });

      if (this.scale > 1) {
        this.mediaWrap.classList.add(`${prefix}__media-wrap--draggable`);
      } else {
        this.mediaWrap.classList.remove(`${prefix}__media-wrap--draggable`);
      }
    }

    startDrag(event) {
      if (this.scale <= 1) return;

      event.preventDefault();

      this.isDragging = true;
      this.hasDragged = false;

      this.dragStartX = event.clientX - this.translateX;
      this.dragStartY = event.clientY - this.translateY;

      this.image.classList.add('is-dragging');
    }

    moveDrag(event) {
      if (!this.isDragging || this.scale <= 1) return;

      event.preventDefault();

      this.dragCurrentX = event.clientX - this.dragStartX;
      this.dragCurrentY = event.clientY - this.dragStartY;

      this.translateX = this.dragCurrentX;
      this.translateY = this.dragCurrentY;

      this.hasDragged = true;

      this.applyTransform();
    }

    endDrag() {
      if (!this.isDragging) return;

      this.isDragging = false;
      this.image.classList.remove('is-dragging');
    }

    handleTouchStart(event) {
      if (!event.touches || event.touches.length === 0) return;

      const touch = event.touches[0];

      this.touchStartX = touch.clientX;
      this.touchStartY = touch.clientY;
      this.touchEndX = touch.clientX;
      this.touchEndY = touch.clientY;

      if (this.scale > 1) {
        this.isDragging = true;
        this.dragStartX = touch.clientX - this.translateX;
        this.dragStartY = touch.clientY - this.translateY;
      }
    }

    handleTouchMove(event) {
      if (!event.touches || event.touches.length === 0) return;

      const touch = event.touches[0];

      this.touchEndX = touch.clientX;
      this.touchEndY = touch.clientY;

      if (this.scale > 1 && this.isDragging) {
        event.preventDefault();

        this.translateX = touch.clientX - this.dragStartX;
        this.translateY = touch.clientY - this.dragStartY;

        this.applyTransform();
      }
    }

    handleTouchEnd() {
      if (this.scale > 1) {
        this.isDragging = false;
        return;
      }

      if (!this.settings.swipe) return;

      const diffX = this.touchEndX - this.touchStartX;
      const diffY = this.touchEndY - this.touchStartY;

      if (Math.abs(diffX) < this.settings.swipeThreshold) {
        return;
      }

      if (Math.abs(diffY) > Math.abs(diffX)) {
        return;
      }

      if (diffX < 0) {
        this.next();
      } else {
        this.prev();
      }
    }

    handleKeydown(event) {
      if (!this.isOpen || !this.settings.keyboard) return;

      const key = event.key;

      if (key === 'Escape' && this.settings.closeOnEscape) {
        event.preventDefault();
        this.close();
      }

      if (key === 'ArrowRight') {
        event.preventDefault();
        this.next();
      }

      if (key === 'ArrowLeft') {
        event.preventDefault();
        this.prev();
      }

      if (key === '+' || key === '=') {
        event.preventDefault();
        this.zoomIn();
      }

      if (key === '-' || key === '_') {
        event.preventDefault();
        this.zoomOut();
      }

      if (key === '0') {
        event.preventDefault();
        this.resetZoom();
      }

      if (key.toLowerCase() === 'f') {
        event.preventDefault();
        this.toggleFullscreen();
      }

      if (key.toLowerCase() === 's') {
        event.preventDefault();
        this.shareCurrent();
      }

      if (key.toLowerCase() === 'd') {
        event.preventDefault();
        this.downloadCurrent();
      }

      if (key === 'Tab') {
        this.handleTabKey(event);
      }
    }

    handleTabKey(event) {
      const focusable = RXUtils.getFocusableElements(this.overlay);

      if (!focusable.length) {
        event.preventDefault();
        return;
      }

      const first = focusable[0];
      const last = focusable[focusable.length - 1];

      if (event.shiftKey && document.activeElement === first) {
        event.preventDefault();
        last.focus();
      } else if (!event.shiftKey && document.activeElement === last) {
        event.preventDefault();
        first.focus();
      }
    }

    trapFocus(event) {
      if (!this.isOpen) return;

      if (!this.overlay.contains(event.target)) {
        event.stopPropagation();
        this.overlay.focus();
      }
    }

    handleResize() {
      if (!this.isOpen) return;

      this.resetZoom(false);
    }

    toggleFullscreen() {
      if (!this.settings.fullscreen || !RXUtils.supportsFullscreen()) return;

      if (RXUtils.isFullscreen()) {
        RXUtils.exitFullscreen();
      } else {
        RXUtils.requestFullscreen(this.overlay);
      }
    }

    syncFullscreenClass() {
      this.overlay.classList.toggle(
        this.settings.fullscreenClass,
        RXUtils.isFullscreen()
      );
    }

    shareCurrent() {
      const item = this.items[this.currentIndex];

      if (!item) return;

      const shareData = {
        title: item.caption || document.title || 'Image',
        text: item.caption || '',
        url: item.src
      };

      if (navigator.share) {
        navigator.share(shareData).catch(function () {
          /**
           * User may cancel share. No need to throw error.
           */
        });
      } else {
        this.copyToClipboard(item.src);
      }

      this.dispatch('rxLightboxShare', {
        item
      });
    }

    copyToClipboard(text) {
      if (!text) return;

      if (navigator.clipboard && navigator.clipboard.writeText) {
        navigator.clipboard.writeText(text).then(() => {
          this.showTemporaryCaption('Image link copied.');
        });
      } else {
        const input = document.createElement('textarea');
        input.value = text;
        input.setAttribute('readonly', '');
        input.style.position = 'absolute';
        input.style.left = '-9999px';

        document.body.appendChild(input);
        input.select();

        try {
          document.execCommand('copy');
          this.showTemporaryCaption('Image link copied.');
        } catch (error) {
          this.showTemporaryCaption('Copy failed.');
        }

        document.body.removeChild(input);
      }
    }

    showTemporaryCaption(message) {
      const oldCaption = this.caption.textContent;

      this.caption.textContent = message;

      window.setTimeout(() => {
        if (this.isOpen) {
          const item = this.items[this.currentIndex];
          this.caption.textContent = item ? item.caption || oldCaption : oldCaption;
        }
      }, 1400);
    }

    downloadCurrent() {
      const item = this.items[this.currentIndex];

      if (!item) return;

      const link = document.createElement('a');

      link.href = item.src;
      link.download =
        item.download ||
        item.title ||
        item.caption ||
        RXUtils.getFileNameFromUrl(item.src);

      link.rel = 'noopener';
      link.target = '_blank';

      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);

      this.dispatch('rxLightboxDownload', {
        item
      });
    }

    dispatch(name, detail) {
      document.dispatchEvent(
        new CustomEvent(name, {
          bubbles: true,
          detail: detail || {}
        })
      );
    }

    destroy() {
      document.removeEventListener('click', this.boundHandleDocumentClick, false);
      document.removeEventListener('keydown', this.boundHandleKeydown, false);
      window.removeEventListener('resize', this.boundHandleResize, false);
      document.removeEventListener('focus', this.boundTrapFocus, true);

      if (this.overlay && this.overlay.parentNode) {
        this.overlay.parentNode.removeChild(this.overlay);
      }

      RXUtils.unlockBody(this.settings.bodyOpenClass);

      this.items = [];
      this.isOpen = false;

      this.dispatch('rxLightboxDestroy', {});
    }
  }

  /**
   * Auto CSS injection.
   * You can later move this CSS into:
   * assets/static-css/components/lightbox.css
   */
  function injectLightboxStyles() {
    if (document.getElementById('rx-lightbox-auto-css')) {
      return;
    }

    const css = `
      .rx-lightbox-open {
        overflow: hidden !important;
      }

      .rx-lightbox {
        position: fixed;
        inset: 0;
        z-index: 999999;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        opacity: 0;
        visibility: hidden;
        pointer-events: none;
        transition: opacity 220ms ease, visibility 220ms ease;
      }

      .rx-lightbox.is-active {
        opacity: 1;
        visibility: visible;
        pointer-events: auto;
      }

      .rx-lightbox--hidden {
        display: none;
      }

      .rx-lightbox__backdrop {
        position: absolute;
        inset: 0;
        background: rgba(0, 0, 0, 0.88);
        backdrop-filter: blur(8px);
      }

      .rx-lightbox__stage {
        position: relative;
        z-index: 2;
        width: 100%;
        height: 100%;
        max-width: 100vw;
        max-height: 100vh;
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 70px 72px 78px;
        box-sizing: border-box;
      }

      .rx-lightbox__media-wrap {
        position: relative;
        max-width: 100%;
        max-height: 100%;
        display: flex;
        align-items: center;
        justify-content: center;
        overflow: hidden;
        cursor: zoom-in;
        touch-action: pan-y;
      }

      .rx-lightbox__media-wrap--draggable {
        cursor: grab;
      }

      .rx-lightbox__image {
        display: block;
        max-width: 100%;
        max-height: calc(100vh - 160px);
        width: auto;
        height: auto;
        object-fit: contain;
        user-select: none;
        transform-origin: center center;
        transition: transform 180ms ease;
        will-change: transform;
      }

      .rx-lightbox__image.is-dragging {
        cursor: grabbing;
        transition: none;
      }

      .rx-lightbox__toolbar {
        position: absolute;
        z-index: 4;
        top: 16px;
        right: 16px;
        display: flex;
        gap: 8px;
        align-items: center;
      }

      .rx-lightbox__button {
        width: 42px;
        height: 42px;
        border: 0;
        border-radius: 999px;
        background: rgba(255, 255, 255, 0.14);
        color: #fff;
        font-size: 22px;
        line-height: 1;
        cursor: pointer;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        transition: background 160ms ease, transform 160ms ease, opacity 160ms ease;
      }

      .rx-lightbox__button:hover,
      .rx-lightbox__button:focus-visible {
        background: rgba(255, 255, 255, 0.28);
        outline: 2px solid rgba(255, 255, 255, 0.72);
        outline-offset: 2px;
      }

      .rx-lightbox__button:active {
        transform: scale(0.94);
      }

      .rx-lightbox__button--prev,
      .rx-lightbox__button--next {
        position: absolute;
        top: 50%;
        z-index: 4;
        width: 52px;
        height: 52px;
        margin-top: -26px;
        font-size: 42px;
      }

      .rx-lightbox__button--prev {
        left: 16px;
      }

      .rx-lightbox__button--next {
        right: 16px;
      }

      .rx-lightbox__button--zoom-reset {
        min-width: 48px;
        width: auto;
        padding-inline: 12px;
        font-size: 13px;
        font-weight: 700;
      }

      .rx-lightbox__caption {
        position: absolute;
        z-index: 3;
        left: 50%;
        bottom: 20px;
        max-width: min(860px, calc(100vw - 32px));
        transform: translateX(-50%);
        color: #fff;
        font-size: 15px;
        line-height: 1.5;
        text-align: center;
        background: rgba(0, 0, 0, 0.38);
        padding: 9px 14px;
        border-radius: 999px;
      }

      .rx-lightbox__caption:empty {
        display: none;
      }

      .rx-lightbox__counter {
        position: absolute;
        z-index: 3;
        top: 22px;
        left: 20px;
        color: #fff;
        font-size: 14px;
        font-weight: 700;
        background: rgba(255, 255, 255, 0.14);
        padding: 8px 12px;
        border-radius: 999px;
      }

      .rx-lightbox__loader {
        position: absolute;
        z-index: 2;
        width: 46px;
        height: 46px;
        border-radius: 999px;
        border: 4px solid rgba(255,255,255,0.25);
        border-top-color: #fff;
        opacity: 0;
        pointer-events: none;
        animation: rxLightboxSpin 850ms linear infinite;
      }

      .rx-lightbox.is-loading .rx-lightbox__loader {
        opacity: 1;
      }

      .rx-lightbox.is-loading .rx-lightbox__image {
        opacity: 0.35;
      }

      .rx-lightbox .is-hidden {
        display: none !important;
      }

      @keyframes rxLightboxSpin {
        to {
          transform: rotate(360deg);
        }
      }

      @media (max-width: 768px) {
        .rx-lightbox__stage {
          padding: 64px 18px 82px;
        }

        .rx-lightbox__toolbar {
          top: 10px;
          right: 10px;
          gap: 6px;
        }

        .rx-lightbox__button {
          width: 38px;
          height: 38px;
          font-size: 18px;
        }

        .rx-lightbox__button--prev,
        .rx-lightbox__button--next {
          top: auto;
          bottom: 18px;
          width: 44px;
          height: 44px;
          font-size: 34px;
        }

        .rx-lightbox__button--prev {
          left: 14px;
        }

        .rx-lightbox__button--next {
          right: 14px;
        }

        .rx-lightbox__caption {
          bottom: 18px;
          max-width: calc(100vw - 120px);
          font-size: 13px;
          border-radius: 14px;
        }

        .rx-lightbox__counter {
          top: 14px;
          left: 12px;
          font-size: 12px;
        }

        .rx-lightbox__image {
          max-height: calc(100vh - 170px);
        }
      }

      @media (prefers-reduced-motion: reduce) {
        .rx-lightbox,
        .rx-lightbox__image,
        .rx-lightbox__button {
          transition: none !important;
        }

        .rx-lightbox__loader {
          animation: none !important;
        }
      }
    `;

    const style = document.createElement('style');
    style.id = 'rx-lightbox-auto-css';
    style.textContent = css;
    document.head.appendChild(style);
  }

  /**
   * Public API.
   */
  window.RXTheme.Lightbox = {
    instance: null,

    init(options) {
      if (this.instance) {
        this.instance.refresh();
        return this.instance;
      }

      injectLightboxStyles();

      this.instance = new RXAdvancedLightbox(options || {});
      return this.instance;
    },

    refresh() {
      if (this.instance) {
        this.instance.refresh();
      }
    },

    open(index) {
      if (this.instance) {
        this.instance.open(index || 0);
      }
    },

    close() {
      if (this.instance) {
        this.instance.close();
      }
    },

    destroy() {
      if (this.instance) {
        this.instance.destroy();
        this.instance = null;
      }
    }
  };

  /**
   * Auto-init.
   */
  function autoInitRXLightbox() {
    window.RXTheme.Lightbox.init();
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', autoInitRXLightbox);
  } else {
    autoInitRXLightbox();
  }

  /**
   * Auto-refresh for dynamically loaded content.
   * Useful for AJAX posts, infinite scroll, related posts, Elementor/Gutenberg dynamic areas.
   */
  if ('MutationObserver' in window) {
    const observer = new MutationObserver(function (mutations) {
      let shouldRefresh = false;

      mutations.forEach(function (mutation) {
        if (!mutation.addedNodes || !mutation.addedNodes.length) return;

        Array.prototype.forEach.call(mutation.addedNodes, function (node) {
          if (!RXUtils.isElement(node)) return;

          if (
            node.matches &&
            node.matches('a[href$=".jpg"], a[href$=".jpeg"], a[href$=".png"], a[href$=".webp"], a[href$=".gif"], a[href$=".avif"], a[data-rx-lightbox]')
          ) {
            shouldRefresh = true;
          }

          if (
            node.querySelector &&
            node.querySelector('a[href$=".jpg"], a[href$=".jpeg"], a[href$=".png"], a[href$=".webp"], a[href$=".gif"], a[href$=".avif"], a[data-rx-lightbox]')
          ) {
            shouldRefresh = true;
          }
        });
      });

      if (shouldRefresh && window.RXTheme.Lightbox.instance) {
        window.requestAnimationFrame(function () {
          window.RXTheme.Lightbox.refresh();
        });
      }
    });

    observer.observe(document.documentElement, {
      childList: true,
      subtree: true
    });
  }
})();

Use image links like this:

<a href="image-large.jpg" data-rx-gallery="post-gallery" data-rx-caption="Medical diagram caption">
  <img src="image-small.jpg" alt="Medical diagram caption">
</a>

For WordPress content images, this script will also automatically detect normal image links ending with:

.jpg
.jpeg
.png
.webp
.gif
.avif

Best enqueue name for this file in RX Theme:

wp_enqueue_script(
    'rx-theme-lightbox-chunk',
    get_template_directory_uri() . '/assets/static-js/static-chunks-js/chunk-013-lightbox.js',
    array(),
    RX_THEME_VERSION,
    true
);

For performance, this file is best loaded with defer and only on pages where posts, medical images, galleries, or article content may contain clickable images.

Leave a Reply

Your email address will not be published. Required fields are marked *