Files
luxtools-plugin/script.js
luxick 16a07701ee
Some checks failed
DokuWiki Default Tasks / all (push) Has been cancelled
Add lightbox for image gallery
2026-01-07 15:26:04 +01:00

354 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* global window, document */
(function () {
'use strict';
function initGalleryThumbs() {
var imgs = document.querySelectorAll('div.luxtools-gallery img[data-thumb-src]');
if (!imgs || !imgs.length) return;
function loadThumb(img) {
var src = img.getAttribute('data-thumb-src') || '';
if (!src) return;
if (img.getAttribute('data-thumb-loading') === '1') return;
img.setAttribute('data-thumb-loading', '1');
var pre = new window.Image();
pre.onload = function () {
img.src = src;
img.removeAttribute('data-thumb-src');
img.removeAttribute('data-thumb-loading');
};
pre.onerror = function () {
img.removeAttribute('data-thumb-loading');
};
pre.src = src;
}
if ('IntersectionObserver' in window) {
var io = new window.IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (!entry.isIntersecting) return;
loadThumb(entry.target);
io.unobserve(entry.target);
});
}, { rootMargin: '200px' });
imgs.forEach(function (img) { io.observe(img); });
} else {
// Fallback: load soon after initial render
window.setTimeout(function () {
imgs.forEach(loadThumb);
}, 0);
}
}
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) {
var url = el.getAttribute('data-service-url') || '';
url = (url || '').trim();
if (!url) return '';
// strip trailing slashes
return url.replace(/\/+$/, '');
}
function pingOpenViaImage(el, rawPath) {
var baseUrl = getServiceUrl(el);
if (!baseUrl) return;
var url = baseUrl + '/open?path=' + encodeURIComponent(rawPath);
// Fire-and-forget without CORS.
try {
var img = new window.Image();
img.src = url;
} catch (e) {
// ignore
}
}
function openViaService(el, rawPath) {
var baseUrl = getServiceUrl(el);
if (!baseUrl) return Promise.reject(new Error('No client service configured'));
var headers = {
'Content-Type': 'application/json'
};
return window.fetch(baseUrl + '/open', {
method: 'POST',
mode: 'cors',
credentials: 'omit',
headers: headers,
body: JSON.stringify({ path: rawPath })
}).then(function (res) {
if (!res.ok) {
return res.json().catch(function () { return null; }).then(function (body) {
var msg = (body && body.message) ? body.message : ('HTTP ' + res.status);
throw new Error(msg);
});
}
return res.json().catch(function () { return { ok: true }; });
});
}
function normalizeToFileUrl(path) {
if (!path) return '';
// already a file URL
if (/^file:\/\//i.test(path)) return path;
// UNC path: \\server\share\path
if (/^\\\\/.test(path)) {
var p = path.replace(/^\\\\/, '');
p = p.replace(/\\/g, '/');
return 'file://///' + p;
}
// Windows drive: C:\path\to\file
if (/^[a-zA-Z]:\\/.test(path)) {
var drive = path[0].toUpperCase();
var rest = path.slice(2).replace(/\\/g, '/');
return 'file:///' + drive + ':' + rest;
}
// POSIX absolute: /home/user/file
if (path[0] === '/') {
return 'file://' + path;
}
// Fall back to using the provided string.
return path;
}
function findOpenElement(target) {
var el = target;
while (el && el !== document) {
if (el.classList && el.classList.contains('luxtools-open')) return el;
el = el.parentNode;
}
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;
// {{open>...}} renders as a link; avoid jumping to '#'.
if (el.tagName && el.tagName.toLowerCase() === 'a') {
event.preventDefault();
}
var raw = el.getAttribute('data-path') || '';
if (!raw) return;
// Prefer local client service.
openViaService(el, raw)
.catch(function (err) {
// If the browser blocks the request before it reaches localhost (mixed-content,
// extensions, stricter CORS handling), fall back to a no-CORS GET ping.
pingOpenViaImage(el, raw);
// Fallback to old behavior (often blocked in modern browsers).
var url = normalizeToFileUrl(raw);
if (!url) return;
console.warn('Local client service failed, falling back to file:// navigation:', err);
try {
window.open(url, '_blank', 'noopener');
} catch (e) {
try {
window.location.href = url;
} catch (e2) {
console.error('Failed to open file URL:', e2);
}
}
});
}
document.addEventListener('click', onClick, false);
document.addEventListener('DOMContentLoaded', initGalleryThumbs, false);
})();