Split js into separate files
This commit is contained in:
@@ -55,6 +55,8 @@ This repository follows DokuWiki's plugin conventions at the top level (e.g. `sy
|
|||||||
Reusable PHP code lives in `src/` and is loaded via `autoload.php`.
|
Reusable PHP code lives in `src/` and is loaded via `autoload.php`.
|
||||||
When adding new internal classes under the `dokuwiki\plugin\luxtools\` namespace, place them in `src/<ClassName>.php`.
|
When adding new internal classes under the `dokuwiki\plugin\luxtools\` namespace, place them in `src/<ClassName>.php`.
|
||||||
|
|
||||||
|
JavaScript is split into small modules under `js/` and registered via `action.php` so DokuWiki loads them in order.
|
||||||
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
|||||||
43
action.php
Normal file
43
action.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use dokuwiki\Extension\ActionPlugin;
|
||||||
|
use dokuwiki\Extension\Event;
|
||||||
|
use dokuwiki\Extension\EventHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* luxtools action plugin: register JS assets.
|
||||||
|
*/
|
||||||
|
class action_plugin_luxtools extends ActionPlugin
|
||||||
|
{
|
||||||
|
/** @inheritdoc */
|
||||||
|
public function register(EventHandler $controller)
|
||||||
|
{
|
||||||
|
$controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'addScripts');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add plugin JavaScript files in a deterministic order.
|
||||||
|
*
|
||||||
|
* @param Event $event
|
||||||
|
* @param mixed $param
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addScripts(Event $event, $param)
|
||||||
|
{
|
||||||
|
$base = DOKU_BASE . 'lib/plugins/luxtools/js/';
|
||||||
|
$scripts = array(
|
||||||
|
'lightbox.js',
|
||||||
|
'gallery-thumbnails.js',
|
||||||
|
'open-service.js',
|
||||||
|
'scratchpads.js',
|
||||||
|
'main.js'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($scripts as $script) {
|
||||||
|
$event->data['script'][] = array(
|
||||||
|
'type' => 'text/javascript',
|
||||||
|
'src' => $base . $script
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
js/gallery-thumbnails.js
Normal file
56
js/gallery-thumbnails.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/* global window, document */
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var Luxtools = window.Luxtools || (window.Luxtools = {});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Gallery Thumbnails Module
|
||||||
|
// ============================================================
|
||||||
|
Luxtools.GalleryThumbnails = (function () {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
var imgs = document.querySelectorAll('div.luxtools-gallery img[data-thumb-src]');
|
||||||
|
if (!imgs || !imgs.length) return;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
init: init
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
})();
|
||||||
265
js/lightbox.js
Normal file
265
js/lightbox.js
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
/* global window, document */
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var Luxtools = window.Luxtools || (window.Luxtools = {});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Lightbox Module
|
||||||
|
// ============================================================
|
||||||
|
Luxtools.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 =
|
||||||
|
'<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(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
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
})();
|
||||||
92
js/main.js
Normal file
92
js/main.js
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/* global window, document */
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var Luxtools = window.Luxtools || (window.Luxtools = {});
|
||||||
|
var Lightbox = Luxtools.Lightbox;
|
||||||
|
var OpenService = Luxtools.OpenService;
|
||||||
|
var GalleryThumbnails = Luxtools.GalleryThumbnails;
|
||||||
|
var Scratchpads = Luxtools.Scratchpads;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 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 && Lightbox && Lightbox.open) {
|
||||||
|
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;
|
||||||
|
if (!OpenService || !OpenService.openViaService) return;
|
||||||
|
|
||||||
|
// Prefer local client service.
|
||||||
|
OpenService.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.
|
||||||
|
if (OpenService && OpenService.pingOpenViaImage) {
|
||||||
|
OpenService.pingOpenViaImage(el, raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to old behavior (often blocked in modern browsers).
|
||||||
|
var url = OpenService && OpenService.normalizeToFileUrl ? OpenService.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', function () {
|
||||||
|
if (GalleryThumbnails && GalleryThumbnails.init) GalleryThumbnails.init();
|
||||||
|
}, false);
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
if (Scratchpads && Scratchpads.init) Scratchpads.init();
|
||||||
|
}, false);
|
||||||
|
})();
|
||||||
94
js/open-service.js
Normal file
94
js/open-service.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
/* global window */
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var Luxtools = window.Luxtools || (window.Luxtools = {});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Open Service Module (file:// links)
|
||||||
|
// ============================================================
|
||||||
|
Luxtools.OpenService = (function () {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
openViaService: openViaService,
|
||||||
|
pingOpenViaImage: pingOpenViaImage,
|
||||||
|
normalizeToFileUrl: normalizeToFileUrl
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
})();
|
||||||
187
js/scratchpads.js
Normal file
187
js/scratchpads.js
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
/* global window, document */
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var Luxtools = window.Luxtools || (window.Luxtools = {});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Scratchpads Module
|
||||||
|
// ============================================================
|
||||||
|
Luxtools.Scratchpads = (function () {
|
||||||
|
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, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClick(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
var pads = document.querySelectorAll('div.luxtools-scratchpad[data-luxtools-scratchpad="1"]');
|
||||||
|
if (!pads || !pads.length) return;
|
||||||
|
document.addEventListener('click', onClick, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
init: init
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
})();
|
||||||
654
script.js
654
script.js
@@ -1,653 +1 @@
|
|||||||
/* global window, document */
|
/* Deprecated: JavaScript modules moved into the js/ folder. */
|
||||||
|
|
||||||
(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 =
|
|
||||||
'<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(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 Module
|
|
||||||
// ============================================================
|
|
||||||
var GalleryThumbnails = (function () {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
var imgs = document.querySelectorAll('div.luxtools-gallery img[data-thumb-src]');
|
|
||||||
if (!imgs || !imgs.length) return;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
init: init
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Open Service Module (file:// links)
|
|
||||||
// ============================================================
|
|
||||||
var OpenService = (function () {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
openViaService: openViaService,
|
|
||||||
pingOpenViaImage: pingOpenViaImage,
|
|
||||||
normalizeToFileUrl: normalizeToFileUrl
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Scratchpads Module
|
|
||||||
// ============================================================
|
|
||||||
var Scratchpads = (function () {
|
|
||||||
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, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function onClick(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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
var pads = document.querySelectorAll('div.luxtools-scratchpad[data-luxtools-scratchpad="1"]');
|
|
||||||
if (!pads || !pads.length) return;
|
|
||||||
document.addEventListener('click', onClick, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
init: init
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 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.
|
|
||||||
OpenService.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.
|
|
||||||
OpenService.pingOpenViaImage(el, raw);
|
|
||||||
|
|
||||||
// Fallback to old behavior (often blocked in modern browsers).
|
|
||||||
var url = OpenService.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', GalleryThumbnails.init, false);
|
|
||||||
document.addEventListener('DOMContentLoaded', Scratchpads.init, false);
|
|
||||||
})();
|
|
||||||
|
|||||||
Reference in New Issue
Block a user