Add lightbox for image gallery
Some checks failed
DokuWiki Default Tasks / all (push) Has been cancelled
Some checks failed
DokuWiki Default Tasks / all (push) Has been cancelled
This commit is contained in:
16
Output.php
16
Output.php
@@ -83,7 +83,7 @@ class Output
|
|||||||
|
|
||||||
/** @var \Doku_Renderer_xhtml $renderer */
|
/** @var \Doku_Renderer_xhtml $renderer */
|
||||||
$renderer = $this->renderer;
|
$renderer = $this->renderer;
|
||||||
$renderer->doc .= '<div class="luxtools-plugin luxtools-gallery">';
|
$renderer->doc .= '<div class="luxtools-plugin luxtools-gallery" data-luxtools-gallery="1">';
|
||||||
|
|
||||||
foreach ($this->files as $item) {
|
foreach ($this->files as $item) {
|
||||||
$url = $this->itemWebUrl($item, !empty($params['randlinks']));
|
$url = $this->itemWebUrl($item, !empty($params['randlinks']));
|
||||||
@@ -98,6 +98,10 @@ class Output
|
|||||||
$safeThumbUrl = hsc($thumbUrl);
|
$safeThumbUrl = hsc($thumbUrl);
|
||||||
$safePlaceholderUrl = hsc($placeholderUrl);
|
$safePlaceholderUrl = hsc($placeholderUrl);
|
||||||
$label = hsc($item['name']);
|
$label = hsc($item['name']);
|
||||||
|
$caption = hsc(basename((string)($item['name'] ?? '')));
|
||||||
|
if ($caption === '') {
|
||||||
|
$caption = $label;
|
||||||
|
}
|
||||||
|
|
||||||
$initialSrc = $safePlaceholderUrl;
|
$initialSrc = $safePlaceholderUrl;
|
||||||
$dataThumb = ' data-thumb-src="' . $safeThumbUrl . '"';
|
$dataThumb = ' data-thumb-src="' . $safeThumbUrl . '"';
|
||||||
@@ -108,7 +112,14 @@ class Output
|
|||||||
$dataThumb = '';
|
$dataThumb = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$renderer->doc .= '<a href="' . $safeUrl . '" class="media" title="' . $label . '" aria-label="' . $label . '">';
|
$renderer->doc .= '<a'
|
||||||
|
. ' href="' . $safeUrl . '"'
|
||||||
|
. ' class="media luxtools-gallery-item"'
|
||||||
|
. ' title="' . $label . '"'
|
||||||
|
. ' aria-label="' . $label . '"'
|
||||||
|
. ' data-luxtools-full="' . $safeUrl . '"'
|
||||||
|
. ' data-luxtools-name="' . $caption . '"'
|
||||||
|
. '>';
|
||||||
$renderer->doc .= '<img'
|
$renderer->doc .= '<img'
|
||||||
. ' class="luxtools-thumb"'
|
. ' class="luxtools-thumb"'
|
||||||
. ' src="' . $initialSrc . '"'
|
. ' src="' . $initialSrc . '"'
|
||||||
@@ -119,6 +130,7 @@ class Output
|
|||||||
. ' loading="lazy"'
|
. ' loading="lazy"'
|
||||||
. ' decoding="async"'
|
. ' decoding="async"'
|
||||||
. ' />';
|
. ' />';
|
||||||
|
$renderer->doc .= '<span class="luxtools-gallery-caption">' . $caption . '</span>';
|
||||||
$renderer->doc .= '</a>';
|
$renderer->doc .= '</a>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
185
script.js
185
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 =
|
||||||
|
'<div class="luxtools-lightbox-backdrop" data-luxtools-action="close"></div>' +
|
||||||
|
'<div class="luxtools-lightbox-stage">' +
|
||||||
|
'<button type="button" class="luxtools-lightbox-btn luxtools-lightbox-close" data-luxtools-action="close" aria-label="Close">×</button>' +
|
||||||
|
'<button type="button" class="luxtools-lightbox-btn luxtools-lightbox-prev" data-luxtools-action="prev" aria-label="Previous">‹</button>' +
|
||||||
|
'<img class="luxtools-lightbox-img" alt="" />' +
|
||||||
|
'<button type="button" class="luxtools-lightbox-btn luxtools-lightbox-next" data-luxtools-action="next" aria-label="Next">›</button>' +
|
||||||
|
'<button type="button" class="luxtools-lightbox-btn luxtools-lightbox-opennew" data-luxtools-action="newtab" aria-label="Open in new tab">↗</button>' +
|
||||||
|
'<div class="luxtools-lightbox-caption"></div>' +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
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) {
|
function getServiceUrl(el) {
|
||||||
var url = el.getAttribute('data-service-url') || '';
|
var url = el.getAttribute('data-service-url') || '';
|
||||||
url = (url || '').trim();
|
url = (url || '').trim();
|
||||||
@@ -128,7 +293,27 @@
|
|||||||
return null;
|
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) {
|
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);
|
var el = findOpenElement(event.target);
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
||||||
|
|||||||
156
style.css
156
style.css
@@ -52,9 +52,161 @@ div.luxtools-plugin .luxtools-empty {
|
|||||||
/* Image gallery spacing. */
|
/* Image gallery spacing. */
|
||||||
div.luxtools-gallery {
|
div.luxtools-gallery {
|
||||||
padding-bottom: 0.5em;
|
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;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user