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.