Files
org_web/assets/scripts/ui/gallery-init.js
gitea-actions 2bb816b2ae
All checks were successful
Build Org Website / build (push) Successful in 30s
change mode and fix build
2026-07-08 22:42:05 +01:00

316 lines
9.9 KiB
JavaScript
Executable File
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.
document.addEventListener('DOMContentLoaded', () => {
if (typeof window.BiggerPicture !== 'function') {
console.error('[gallery-init] BiggerPicture not found. Check script path.');
return;
}
const VIDEO_EXTENSIONS = ['mp4', 'webm', 'mov', 'm4v', 'ogv'];
const isVideo = (href) => {
if (!href) return false;
href = href.toLowerCase();
return VIDEO_EXTENSIONS.some(ext => href.endsWith('.' + ext));
};
// 1b) Convert MP4 links into inline <video> players
// Convert video links into inline <video> players BUT keep the <a>
const videoLinks = document.querySelectorAll('a[href]');
videoLinks.forEach(a => {
const href = a.getAttribute("href");
if (!isVideo(href)) return;
const video = document.createElement("video");
video.controls = true;
video.preload = "metadata";
video.style.maxWidth = "100%";
video.style.borderRadius = "6px";
const source = document.createElement("source");
source.src = href;
source.type = "video/" + href.split('.').pop(); // guesses correct mime
video.appendChild(source);
// Replace the link with the inline video
a.parentNode.replaceChild(video, a);
});
// 1) Wrap Org-exported images so theyre clickable
const imgs = document.querySelectorAll('.figure img, img.org-svg');
imgs.forEach((img) => {
if (img.closest('a')) return; // already wrapped
const a = document.createElement('a');
const href = img.currentSrc || img.src;
a.href = href;
a.dataset.img = href; // lets BP pre-size/raster slides
a.dataset.alt = img.alt || '';
const setDims = () => {
a.dataset.width = img.naturalWidth || img.width || 1920;
a.dataset.height = img.naturalHeight || img.height || 1080;
};
if (img.complete) setDims(); else img.addEventListener('load', setDims);
img.style.cursor = 'zoom-in';
img.parentElement.insertBefore(a, img);
a.appendChild(img);
});
// 2) One global BP instance
const bp = BiggerPicture({ target: document.body });
// SVG pan/zoom handle
let activePanZoom = null;
const destroyPanZoom = () => { try { activePanZoom?.destroy(); } catch(_){} activePanZoom = null; };
// Simple rotate state (for non-SVG images)
let activeContainer = null;
let currentRotation = 0;
let rotateControls = null;
// 3) Build galleries per content container
const containers = document.querySelectorAll('main, article, .content, body');
containers.forEach((container) => {
const links = Array.from(container.querySelectorAll(
'.figure a, a:has(img.org-svg), a[href$=".mp4"], a[href$=".webm"], a[href$=".mov"], a[href$=".m4v"], a[href$=".ogv"]'
));
if (!links.length) return;
// Start the lightbox on click
links.forEach((link, index) => {
link.addEventListener('click', (e) => {
e.preventDefault();
document.querySelectorAll(".theme-toggle").forEach(el => {
el.classList.add("hidden");
});
bp.open({
// IMPORTANT: pass the anchor ELEMENTS, not custom objects
items: links,
el: link,
caption: (el) => el.querySelector('img')?.alt || el.title || '',
maxZoom: 40, // for raster images (PNG/JPG); SVG handled separately
// Fade-out polish + cleanup
onClose(containerEl) {
destroyPanZoom();
teardownRotation();
if (containerEl) containerEl.classList.add('bp-fadeout');
const themeToggle = document.querySelector(".theme-toggle");
if (themeToggle) {
themeToggle.classList.remove("hidden");
}
},
// Called once after open and on every slide change
//onOpen(containerEl) { setupRotation(containerEl); enhanceSVG(containerEl); }
onOpen(containerEl) {
const linkEl = bp.currItem;
const href = linkEl?.href || "";
if (href.endsWith(".mp4")) {
// Turn off image rotation (not applicable for video)
teardownRotation();
const htmlLayer = containerEl.querySelector(".bp-html");
const imgLayer = containerEl.querySelector(".bp-img");
if (!htmlLayer) return;
// Hide the default image layer
if (imgLayer) imgLayer.style.display = "none";
// Insert video player
htmlLayer.innerHTML = `
<video controls autoplay style="max-width:95vw; max-height:95vh">
<source src="${href}" type="video/mp4">
</video>
`;
return; // Do not run image/SVG enhancements
}
// Image behaviour
setupRotation(containerEl);
enhanceSVG(containerEl);
} ,
onUpdate(containerEl){ setupRotation(containerEl); enhanceSVG(containerEl); }
});
});
});
});
// 4) Simple rotate buttons for raster images
function ensureRotateControls() {
if (rotateControls) return rotateControls;
const wrapper = document.createElement('div');
wrapper.className = 'bp-rotate-controls';
Object.assign(wrapper.style, {
position: 'fixed',
bottom: '1.5rem',
right: '1.5rem',
display: 'flex',
gap: '0.5rem',
zIndex: '9999',
pointerEvents: 'auto'
});
const mkBtn = (label, title) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = label;
btn.title = title;
btn.setAttribute('aria-label', title);
Object.assign(btn.style, {
padding: '0.4rem 0.6rem',
borderRadius: '999px',
border: 'none',
fontSize: '1.2rem',
cursor: 'pointer',
background: 'rgba(30,30,30,0.8)',
color: '#fff'
});
return btn;
};
const left = mkBtn('⟲', 'Rotate image 90° left');
const right = mkBtn('⟳', 'Rotate image 90° right');
left.addEventListener('click', (e) => {
e.stopPropagation(); // dont close the lightbox
if (!activeContainer) return;
currentRotation = (currentRotation - 90 + 360) % 360;
applyRotation();
});
right.addEventListener('click', (e) => {
e.stopPropagation();
if (!activeContainer) return;
currentRotation = (currentRotation + 90) % 360;
applyRotation();
});
wrapper.append(left, right);
document.body.appendChild(wrapper);
rotateControls = wrapper;
return rotateControls;
}
function ensureRotateWrapper() {
if (!activeContainer) return null;
const imgRoot = activeContainer.querySelector('.bp-img');
if (!imgRoot) return null;
let imgEl = imgRoot.querySelector('img');
if (!imgEl) return null;
const src = (imgEl.currentSrc || imgEl.src || '').toLowerCase();
// We only rotate raster images; SVGs are handled via svg-pan-zoom
if (src.endsWith('.svg')) return null;
let wrapper = imgRoot.querySelector('.bp-rotate-wrapper');
if (!wrapper) {
wrapper = document.createElement('div');
wrapper.className = 'bp-rotate-wrapper';
wrapper.style.display = 'inline-block';
wrapper.style.transformOrigin = 'center center';
imgRoot.appendChild(wrapper);
wrapper.appendChild(imgEl);
} else if (!wrapper.contains(imgEl)) {
// Slide changed and BiggerPicture replaced the <img>
wrapper.innerHTML = '';
wrapper.appendChild(imgEl);
}
return wrapper;
}
function applyRotation() {
const wrapper = ensureRotateWrapper();
if (!wrapper) return;
wrapper.style.transform = `rotate(${currentRotation}deg)`;
}
function setupRotation(containerEl) {
activeContainer = containerEl;
currentRotation = 0;
const controls = ensureRotateControls();
controls.style.display = 'flex';
applyRotation();
}
function teardownRotation() {
activeContainer = null;
currentRotation = 0;
if (rotateControls) {
rotateControls.style.display = 'none';
}
}
// 4) If current slide is an SVG, swap to inline + enable svg-pan-zoom
async function enhanceSVG(containerEl) {
try {
destroyPanZoom();
const imgEl = containerEl.querySelector('.bp-img img');
if (!imgEl) return;
const src = imgEl.currentSrc || imgEl.src || '';
const isSVG = src.toLowerCase().endsWith('.svg');
const htmlLayer = containerEl.querySelector('.bp-html');
if (!isSVG || !htmlLayer) {
// ensure any previous holder is removed and bitmap is visible
const old = htmlLayer?.querySelector('.bp-svg-holder');
if (old) old.remove();
imgEl.style.visibility = '';
return;
}
// Create/clear holder
let holder = htmlLayer.querySelector('.bp-svg-holder');
if (!holder) {
holder = document.createElement('div');
holder.className = 'bp-svg-holder';
holder.style.maxWidth = '95vw';
holder.style.maxHeight = '95vh';
htmlLayer.appendChild(holder);
}
holder.innerHTML = '';
// Hide the bitmap so only the inline SVG shows
imgEl.style.visibility = 'hidden';
// Inline the SVG
const res = await fetch(src, { cache: 'force-cache' });
const text = await res.text();
holder.innerHTML = text;
const svg = holder.querySelector('svg');
if (!svg) { imgEl.style.visibility = ''; return; }
svg.style.maxWidth = '95vw';
svg.style.maxHeight = '95vh';
svg.style.display = 'block';
if (typeof window.svgPanZoom === 'function') {
activePanZoom = svgPanZoom(svg, {
zoomEnabled: true,
controlIconsEnabled: true,
fit: true,
center: true,
minZoom: 0.05,
maxZoom: 400, // effectively "unlimited"
zoomScaleSensitivity: 0.25,
dblClickZoomEnabled: true
});
// Keep wheel inside lightbox
holder.addEventListener('wheel', (e) => e.stopPropagation(), { passive: true });
} else {
console.warn('[gallery-init] svg-pan-zoom not loaded');
}
} catch (err) {
console.error('[gallery-init] SVG enhance failed:', err);
}
}
});