/* 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 = '
' + '
' + '' + '' + '' + '' + '' + '
' + '
'; 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); })();