Files
luxtools-plugin/script.js

515 lines
16 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-close" data-luxtools-action="close" aria-label="Close">×</button>' +
'<button type="button" class="luxtools-lightbox-zone luxtools-lightbox-zone-prev" data-luxtools-action="prev" aria-label="Previous"></button>' +
'<button type="button" class="luxtools-lightbox-zone luxtools-lightbox-zone-next" data-luxtools-action="next" aria-label="Next"></button>' +
'<div class="luxtools-lightbox-media">' +
'<img class="luxtools-lightbox-img" alt="" />' +
'</div>' +
'<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));
if (cap) cap.textContent = (it.name || '').trim();
}
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(); return; }
if (action === 'next') { e.preventDefault(); next(); return; }
if (action === 'prev') { e.preventDefault(); prev(); return; }
// Click outside the image closes (but don't interfere with controls).
if (t.closest && t.closest('button.luxtools-lightbox-zone')) 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 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);
}
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);
document.addEventListener('DOMContentLoaded', initScratchpads, false);
})();