/* global window, document */ (function () { 'use strict'; // ============================================================ // Lightbox Module // ============================================================ var Lightbox = (function () { var lb = null; var img = null; var cap = null; var items = []; var index = 0; // Zoom/pan state var scale = 1; var panX = 0; var panY = 0; var minScale = 1; var maxScale = 5; var isPanning = false; var panStartX = 0; var panStartY = 0; // History state var pushedHistory = false; var closingFromPopstate = false; function ensureElement() { if (lb) return; lb = document.createElement('div'); lb.className = 'luxtools-lightbox'; lb.setAttribute('role', 'dialog'); lb.setAttribute('aria-modal', 'true'); lb.setAttribute('aria-hidden', 'true'); lb.innerHTML = '
' + '
' + '' + '' + '' + '
' + '' + '
' + '
' + '
'; document.body.appendChild(lb); img = lb.querySelector('img.luxtools-lightbox-img'); cap = lb.querySelector('.luxtools-lightbox-caption'); lb.addEventListener('click', onClick); } function clampIndex(n) { if (n < 0) return items.length - 1; if (n >= items.length) return 0; return n; } function applyTransform() { if (scale <= 1 && panX === 0 && panY === 0) { img.style.transform = ''; } else { img.style.transform = 'scale(' + scale + ') translate(' + panX + 'px, ' + panY + 'px)'; } img.style.cursor = scale > 1 ? 'grab' : ''; } function resetZoom() { scale = 1; panX = 0; panY = 0; applyTransform(); } function render() { var it = items[index]; img.src = it.full; img.setAttribute('data-luxtools-index', String(index)); if (cap) cap.textContent = (it.name || '').trim(); resetZoom(); } function next() { index = clampIndex(index + 1); render(); } function prev() { index = clampIndex(index - 1); render(); } // Event handlers function onWheel(e) { e.preventDefault(); var delta = e.deltaY > 0 ? -0.15 : 0.15; scale = Math.max(minScale, Math.min(maxScale, scale + delta)); if (scale <= 1) { panX = 0; panY = 0; } applyTransform(); } function onDblClick(e) { e.preventDefault(); if (scale > 1) { scale = 1; panX = 0; panY = 0; } else { scale = 2.5; } applyTransform(); } function onMouseDown(e) { if (scale > 1 && e.button === 0) { isPanning = true; panStartX = e.clientX - panX * scale; panStartY = e.clientY - panY * scale; img.style.cursor = 'grabbing'; e.preventDefault(); } } function onMouseMove(e) { if (isPanning && scale > 1) { panX = (e.clientX - panStartX) / scale; panY = (e.clientY - panStartY) / scale; applyTransform(); img.style.cursor = 'grabbing'; } } function onMouseUp() { isPanning = false; img.style.cursor = scale > 1 ? 'grab' : ''; } function onKeyDown(e) { if (!lb || !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 || !lb.classList.contains('is-open')) return; closingFromPopstate = true; try { close(); } finally { closingFromPopstate = false; } } function onClick(e) { var t = e.target; if (!t || !t.getAttribute) return; var action = t.getAttribute('data-luxtools-action') || ''; if (action === 'close') { e.preventDefault(); close(); return; } if (action === 'next') { e.preventDefault(); next(); return; } if (action === 'prev') { e.preventDefault(); prev(); return; } if (t.closest && t.closest('button.luxtools-lightbox-zone')) return; if (t.closest && t.closest('img.luxtools-lightbox-img')) return; e.preventDefault(); close(); } function attachListeners() { document.addEventListener('keydown', onKeyDown, true); window.addEventListener('popstate', onPopState, true); img.addEventListener('wheel', onWheel, { passive: false }); img.addEventListener('dblclick', onDblClick); img.addEventListener('mousedown', onMouseDown); document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); } function detachListeners() { document.removeEventListener('keydown', onKeyDown, true); window.removeEventListener('popstate', onPopState, true); img.removeEventListener('wheel', onWheel); img.removeEventListener('dblclick', onDblClick); img.removeEventListener('mousedown', onMouseDown); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); } function open(galleryEl, startEl) { var links = galleryEl.querySelectorAll('a.luxtools-gallery-item[data-luxtools-full]'); 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 }); }); if (!items.length) return; index = 0; for (var i = 0; i < items.length; i++) { if (items[i].el === startEl) { index = i; break; } } ensureElement(); pushedHistory = false; closingFromPopstate = false; 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) {} attachListeners(); try { if (window.history && window.history.pushState) { window.history.pushState({ luxtoolsLightbox: 1 }, '', window.location.href); pushedHistory = true; } } catch (e) {} render(); } function close() { if (!lb) return; 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) {} img.src = ''; resetZoom(); detachListeners(); if (pushedHistory && !closingFromPopstate) { try { if (window.history && window.history.state && window.history.state.luxtoolsLightbox === 1) { window.history.back(); } } catch (e) {} } items = []; } return { open: open, close: close }; })(); // ============================================================ // Gallery Thumbnails // ============================================================ 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); } } // ============================================================ // Open Service (file:// links) // ============================================================ 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 initScratchpads() { var pads = document.querySelectorAll('div.luxtools-scratchpad[data-luxtools-scratchpad="1"]'); if (!pads || !pads.length) return; function setEditMode(root, isEditing) { if (!root || !root.classList) return; var view = root.querySelector('.luxtools-scratchpad-view'); var editor = root.querySelector('.luxtools-scratchpad-editor'); if (isEditing) { root.classList.add('is-editing'); if (view) view.hidden = true; if (editor) editor.hidden = false; } else { root.classList.remove('is-editing'); if (view) view.hidden = false; if (editor) editor.hidden = true; } } function setStatus(root, msg) { var el = root.querySelector('.luxtools-scratchpad-status'); if (!el) return; el.textContent = msg || ''; } function getSectok(root) { // Prefer a token embedded with the rendered scratchpad. try { if (root && root.getAttribute) { var t = String(root.getAttribute('data-sectok') || '').trim(); if (t) return t; } } catch (e) {} // Fall back to DokuWiki's global JSINFO. try { if (window.JSINFO && window.JSINFO.sectok) return String(window.JSINFO.sectok); } catch (e) {} // Last resort: find any security token input on the page. try { var inp = document.querySelector('input[name="sectok"], input[name="securitytoken"]'); if (inp && inp.value) return String(inp.value); } catch (e2) {} return ''; } function loadPad(root) { var endpoint = (root.getAttribute('data-endpoint') || '').trim(); var pad = (root.getAttribute('data-pad') || '').trim(); var pageId = (root.getAttribute('data-pageid') || '').trim(); if (!endpoint || !pad || !pageId) return Promise.reject(new Error('missing params')); var url = endpoint + '?cmd=load&pad=' + encodeURIComponent(pad) + '&id=' + encodeURIComponent(pageId); return window.fetch(url, { method: 'GET', credentials: 'same-origin' }).then(function (res) { return res.json().catch(function () { return null; }).then(function (body) { if (!res.ok || !body || body.ok !== true) { var msg = (body && body.error) ? body.error : ('HTTP ' + res.status); throw new Error(msg); } return body.text || ''; }); }); } function savePad(root, text) { var endpoint = (root.getAttribute('data-endpoint') || '').trim(); var pad = (root.getAttribute('data-pad') || '').trim(); var pageId = (root.getAttribute('data-pageid') || '').trim(); if (!endpoint || !pad || !pageId) return Promise.reject(new Error('missing params')); var params = new window.URLSearchParams(); params.set('cmd', 'save'); params.set('pad', pad); params.set('id', pageId); params.set('text', text || ''); params.set('sectok', getSectok(root)); return window.fetch(endpoint, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, body: params.toString() }).then(function (res) { return res.json().catch(function () { return null; }).then(function (body) { if (!res.ok || !body || body.ok !== true) { var msg = (body && body.error) ? body.error : ('HTTP ' + res.status); throw new Error(msg); } return true; }); }); } function openEditor(root) { var editor = root.querySelector('.luxtools-scratchpad-editor'); var textarea = root.querySelector('textarea.luxtools-scratchpad-text'); if (!editor || !textarea) return; setEditMode(root, true); setStatus(root, 'Loading…'); textarea.disabled = true; loadPad(root).then(function (text) { textarea.value = text; textarea.disabled = false; setStatus(root, ''); textarea.focus(); }).catch(function (e) { textarea.disabled = false; setStatus(root, 'Load failed: ' + (e && e.message ? e.message : 'error')); }); } function closeEditor(root) { var editor = root.querySelector('.luxtools-scratchpad-editor'); if (!editor) return; setEditMode(root, false); setStatus(root, ''); } document.addEventListener('click', function (e) { var t = e.target; if (!t) return; var edit = t.closest ? t.closest('a.luxtools-scratchpad-edit') : null; if (edit) { var root = edit.closest('div.luxtools-scratchpad'); if (!root) return; e.preventDefault(); openEditor(root); return; } var save = t.closest ? t.closest('button.luxtools-scratchpad-save') : null; if (save) { var rootS = save.closest('div.luxtools-scratchpad'); if (!rootS) return; e.preventDefault(); var textareaS = rootS.querySelector('textarea.luxtools-scratchpad-text'); if (!textareaS) return; textareaS.disabled = true; setStatus(rootS, 'Saving…'); savePad(rootS, textareaS.value).then(function () { setStatus(rootS, 'Saved. Reloading…'); try { window.location.reload(); } catch (err) {} }).catch(function (err) { textareaS.disabled = false; setStatus(rootS, 'Save failed: ' + (err && err.message ? err.message : 'error')); }); return; } var cancel = t.closest ? t.closest('button.luxtools-scratchpad-cancel') : null; if (cancel) { var rootC = cancel.closest('div.luxtools-scratchpad'); if (!rootC) return; e.preventDefault(); closeEditor(rootC); } }, true); } // ============================================================ // Click Handlers // ============================================================ 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(); Lightbox.open(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); } } }); } // ============================================================ // Initialize // ============================================================ document.addEventListener('click', onClick, false); document.addEventListener('DOMContentLoaded', initGalleryThumbs, false); document.addEventListener('DOMContentLoaded', initScratchpads, false); })();