';
foreach ($this->files as $item) {
$url = $this->itemWebUrl($item, !empty($params['randlinks']));
@@ -98,6 +98,10 @@ class Output
$safeThumbUrl = hsc($thumbUrl);
$safePlaceholderUrl = hsc($placeholderUrl);
$label = hsc($item['name']);
+ $caption = hsc(basename((string)($item['name'] ?? '')));
+ if ($caption === '') {
+ $caption = $label;
+ }
$initialSrc = $safePlaceholderUrl;
$dataThumb = ' data-thumb-src="' . $safeThumbUrl . '"';
@@ -108,7 +112,14 @@ class Output
$dataThumb = '';
}
- $renderer->doc .= '
';
+ $renderer->doc .= '';
$renderer->doc .= '
';
+ $renderer->doc .= '' . $caption . '';
$renderer->doc .= '';
}
diff --git a/script.js b/script.js
index acc5e59..e7acb8f 100644
--- a/script.js
+++ b/script.js
@@ -43,6 +43,171 @@
}
}
+ function ensureLightbox() {
+ var existing = document.querySelector('.luxtools-lightbox');
+ if (existing) return existing;
+
+ var root = document.createElement('div');
+ root.className = 'luxtools-lightbox';
+ root.setAttribute('role', 'dialog');
+ root.setAttribute('aria-modal', 'true');
+ root.setAttribute('aria-hidden', 'true');
+
+ root.innerHTML =
+ '
' +
+ '
';
+
+ document.body.appendChild(root);
+ return root;
+ }
+
+ function getGalleryItems(galleryEl) {
+ var links = galleryEl.querySelectorAll('a.luxtools-gallery-item[data-luxtools-full]');
+ var items = [];
+ links.forEach(function (a) {
+ var full = a.getAttribute('data-luxtools-full') || a.getAttribute('href') || '';
+ var name = a.getAttribute('data-luxtools-name') || a.getAttribute('title') || '';
+ if (!full) return;
+ items.push({ el: a, full: full, name: name });
+ });
+ return items;
+ }
+
+ function openLightboxFor(galleryEl, startEl) {
+ var items = getGalleryItems(galleryEl);
+ if (!items.length) return;
+
+ var index = 0;
+ for (var i = 0; i < items.length; i++) {
+ if (items[i].el === startEl) { index = i; break; }
+ }
+
+ var lb = ensureLightbox();
+ var img = lb.querySelector('img.luxtools-lightbox-img');
+ var cap = lb.querySelector('.luxtools-lightbox-caption');
+
+ var pushedHistory = false;
+ var closingFromPopstate = false;
+
+ function clamp(n) {
+ if (n < 0) return items.length - 1;
+ if (n >= items.length) return 0;
+ return n;
+ }
+
+ function render() {
+ var it = items[index];
+ img.src = it.full;
+ img.setAttribute('data-luxtools-index', String(index));
+ cap.textContent = (it.name || '').trim();
+ }
+
+ function openInNewTab() {
+ var it = items[index];
+ if (!it || !it.full) return;
+ try {
+ window.open(it.full, '_blank', 'noopener');
+ } catch (e) {
+ // ignore
+ }
+ }
+
+ function close() {
+ lb.classList.remove('is-open');
+ lb.setAttribute('aria-hidden', 'true');
+ try { document.documentElement.classList.remove('luxtools-noscroll'); } catch (e) {}
+ try { document.body.style.overflow = ''; } catch (e) {}
+ // Clear src to stop downloads on close.
+ img.src = '';
+ document.removeEventListener('keydown', onKeyDown, true);
+ window.removeEventListener('popstate', onPopState, true);
+
+ // If we opened by pushing a state, pop it when closing via UI.
+ if (pushedHistory && !closingFromPopstate) {
+ try {
+ if (window.history && window.history.state && window.history.state.luxtoolsLightbox === 1) {
+ window.history.back();
+ }
+ } catch (e) {
+ // ignore
+ }
+ }
+ }
+
+ function next() {
+ index = clamp(index + 1);
+ render();
+ }
+
+ function prev() {
+ index = clamp(index - 1);
+ render();
+ }
+
+ function onKeyDown(e) {
+ if (!lb.classList.contains('is-open')) return;
+ var key = e.key || '';
+ if (key === 'Escape') {
+ e.preventDefault();
+ close();
+ } else if (key === 'ArrowRight') {
+ e.preventDefault();
+ next();
+ } else if (key === 'ArrowLeft') {
+ e.preventDefault();
+ prev();
+ }
+ }
+
+ function onPopState() {
+ if (!lb.classList.contains('is-open')) return;
+ closingFromPopstate = true;
+ try { close(); } finally { closingFromPopstate = false; }
+ }
+
+ lb.onclick = function (e) {
+ var t = e.target;
+ if (!t || !t.getAttribute) return;
+ var action = t.getAttribute('data-luxtools-action') || '';
+ if (action === 'close') { e.preventDefault(); close(); }
+ if (action === 'next') { e.preventDefault(); next(); }
+ if (action === 'prev') { e.preventDefault(); prev(); }
+ if (action === 'newtab') { e.preventDefault(); openInNewTab(); }
+
+ // Click outside the image closes (but don't interfere with controls).
+ if (t.closest && t.closest('button.luxtools-lightbox-btn')) return;
+ if (t.closest && t.closest('img.luxtools-lightbox-img')) return;
+ e.preventDefault();
+ close();
+ };
+
+ // Open
+ lb.classList.add('is-open');
+ lb.setAttribute('aria-hidden', 'false');
+ try { document.documentElement.classList.add('luxtools-noscroll'); } catch (e) {}
+ try { document.body.style.overflow = 'hidden'; } catch (e) {}
+ document.addEventListener('keydown', onKeyDown, true);
+ window.addEventListener('popstate', onPopState, true);
+
+ // Allow closing via browser back button.
+ try {
+ if (window.history && window.history.pushState) {
+ window.history.pushState({ luxtoolsLightbox: 1 }, '', window.location.href);
+ pushedHistory = true;
+ }
+ } catch (e) {
+ // ignore
+ }
+ render();
+ }
+
function getServiceUrl(el) {
var url = el.getAttribute('data-service-url') || '';
url = (url || '').trim();
@@ -128,7 +293,27 @@
return null;
}
+ function findGalleryItem(target) {
+ var el = target;
+ while (el && el !== document) {
+ if (el.classList && el.classList.contains('luxtools-gallery-item')) return el;
+ el = el.parentNode;
+ }
+ return null;
+ }
+
function onClick(event) {
+ // Image gallery lightbox: intercept clicks so we don't navigate away.
+ var galleryItem = findGalleryItem(event.target);
+ if (galleryItem) {
+ var gallery = galleryItem.closest ? galleryItem.closest('div.luxtools-gallery[data-luxtools-gallery="1"]') : null;
+ if (gallery) {
+ event.preventDefault();
+ openLightboxFor(gallery, galleryItem);
+ return;
+ }
+ }
+
var el = findOpenElement(event.target);
if (!el) return;
diff --git a/style.css b/style.css
index 637bfd8..06434ae 100644
--- a/style.css
+++ b/style.css
@@ -52,9 +52,161 @@ div.luxtools-plugin .luxtools-empty {
/* Image gallery spacing. */
div.luxtools-gallery {
padding-bottom: 0.5em;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.35em;
}
-div.luxtools-gallery a.media {
+div.luxtools-gallery a.media.luxtools-gallery-item {
display: inline-block;
- margin: 0 0.35em 0.35em 0;
+ position: relative;
+ border: 1px solid @ini_border;
+ background-color: @ini_background_alt;
+ overflow: hidden;
+ line-height: 0;
+ text-decoration: none;
+}
+
+div.luxtools-gallery a.media.luxtools-gallery-item:hover,
+div.luxtools-gallery a.media.luxtools-gallery-item:focus {
+ border-color: @ini_text;
+}
+
+div.luxtools-gallery img.luxtools-thumb {
+ display: block;
+ width: 150px;
+ height: 150px;
+ object-fit: cover;
+}
+
+/* Filename overlay (single line, muted). */
+div.luxtools-gallery .luxtools-gallery-caption {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ font-size: 0.75em;
+ line-height: 1.3;
+ padding: 0.25em 0.4em;
+ color: #fff;
+ background: rgba(0, 0, 0, 0.35);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ pointer-events: none;
+}
+
+/* Disable background scrolling while the lightbox is open. */
+html.luxtools-noscroll,
+html.luxtools-noscroll body {
+ overflow: hidden !important;
+}
+
+/* Fullscreen lightbox viewer (client-side). */
+.luxtools-lightbox {
+ position: fixed;
+ inset: 0;
+ z-index: 9999;
+ display: none;
+}
+
+.luxtools-lightbox.is-open {
+ display: block;
+}
+
+.luxtools-lightbox .luxtools-lightbox-backdrop {
+ position: absolute;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.85);
+}
+
+.luxtools-lightbox .luxtools-lightbox-stage {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0.8em;
+}
+
+.luxtools-lightbox img.luxtools-lightbox-img {
+ max-width: 100%;
+ max-height: 100%;
+}
+
+.luxtools-lightbox .luxtools-lightbox-caption {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ padding: 0.6em 1em;
+ color: #fff;
+ opacity: 0.9;
+ background: rgba(0, 0, 0, 0.35);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ text-align: center;
+}
+
+/* Top-left "open in new tab" button. */
+.luxtools-lightbox button.luxtools-lightbox-opennew {
+ left: 0.6em;
+ top: 0.6em;
+ bottom: auto;
+ width: 2.4em;
+ height: 2.4em;
+ background: transparent;
+ border: none;
+ border-radius: 0.2em;
+ z-index: 2;
+}
+
+
+.luxtools-lightbox button.luxtools-lightbox-btn {
+ position: absolute;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #fff;
+}
+
+/* Navigation: transparent left/right tap zones (default everywhere). */
+.luxtools-lightbox button.luxtools-lightbox-prev,
+.luxtools-lightbox button.luxtools-lightbox-next {
+ top: 0;
+ bottom: 0;
+ width: 30%;
+ background: transparent;
+ border: none;
+ padding: 0;
+ font-size: 2em;
+ color: rgba(255, 255, 255, 0.7);
+}
+
+.luxtools-lightbox button.luxtools-lightbox-prev {
+ left: 0;
+}
+
+.luxtools-lightbox button.luxtools-lightbox-next {
+ right: 0;
+}
+
+/* Close button overlays content. */
+.luxtools-lightbox button.luxtools-lightbox-close {
+ top: 0.6em;
+ right: 0.6em;
+ width: 2.2em;
+ height: 2.2em;
+ background: transparent;
+ border: none;
+ z-index: 2;
+}
+
+@media (max-width: 600px) {
+ .luxtools-lightbox button.luxtools-lightbox-prev,
+ .luxtools-lightbox button.luxtools-lightbox-next {
+ font-size: 1.6em;
+ }
}