This commit is contained in:
2026-03-23 02:09:14 +01:00
parent 705329d3c2
commit d8d46ed8e9
61 changed files with 6054 additions and 3116 deletions

View File

@@ -0,0 +1,165 @@
/** Download-URL (href der API) → dieselbe URL mit ?inline=1 für Anzeige */
export function hrefToInlineView(href) {
if (!href) return href;
const u = new URL(href, window.location.origin);
u.searchParams.set('inline', '1');
return u.pathname + u.search + u.hash;
}
const TEXT_PREVIEW_MAX = 512 * 1024;
/** @param {string} mime @param {string} fileName */
export function attachmentPreviewKind(mime, fileName) {
const m = (mime || '').toLowerCase().trim();
const ext = (fileName || '').split('.').pop()?.toLowerCase() || '';
if (m.startsWith('image/')) return 'image';
if (m === 'application/pdf' || ext === 'pdf') return 'pdf';
if (m.startsWith('video/')) return 'video';
if (m.startsWith('audio/')) return 'audio';
if (
m.startsWith('text/') ||
m === 'application/json' ||
m === 'application/xml' ||
['csv', 'json', 'xml', 'txt', 'log', 'md', 'svg'].includes(ext)
) {
return 'text';
}
return 'other';
}
let dialogEl;
let bodyEl;
let titleEl;
let downloadLink;
function setPageScrollLocked(locked) {
if (locked) {
document.documentElement.style.overflow = 'hidden';
document.body.style.overflow = 'hidden';
} else {
document.documentElement.style.overflow = '';
document.body.style.overflow = '';
}
}
function ensureDialog() {
if (dialogEl) return;
dialogEl = document.createElement('dialog');
dialogEl.className = 'attachment-preview-dialog';
dialogEl.setAttribute('aria-modal', 'true');
dialogEl.innerHTML = `
<div class="attachment-preview-inner">
<header class="attachment-preview-header">
<h3 class="attachment-preview-title"></h3>
<button type="button" class="attachment-preview-close" aria-label="Schließen">×</button>
</header>
<div class="attachment-preview-body"></div>
<footer class="attachment-preview-footer">
<a class="button secondary attachment-preview-download" href="#" download>Herunterladen</a>
</footer>
</div>`;
document.body.appendChild(dialogEl);
bodyEl = dialogEl.querySelector('.attachment-preview-body');
titleEl = dialogEl.querySelector('.attachment-preview-title');
downloadLink = dialogEl.querySelector('.attachment-preview-download');
dialogEl.querySelector('.attachment-preview-close').addEventListener('click', () => {
dialogEl.close();
});
dialogEl.addEventListener('click', (e) => {
if (e.target === dialogEl) dialogEl.close();
});
dialogEl.addEventListener('close', () => {
if (bodyEl) bodyEl.innerHTML = '';
setPageScrollLocked(false);
});
}
/**
* Klick-Delegation: Links mit .js-attachment-preview öffnen das Modal.
*/
export function bindAttachmentPreview(root = document.body) {
ensureDialog();
root.addEventListener('click', (e) => {
const a = e.target.closest('a.js-attachment-preview');
if (!a) return;
e.preventDefault();
openAttachmentPreview(a);
});
}
/**
* @param {HTMLAnchorElement} a
*/
export async function openAttachmentPreview(a) {
ensureDialog();
const name = a.getAttribute('data-name') || a.textContent?.trim() || 'Datei';
const mime = a.getAttribute('data-mime') || '';
const rawHref = a.getAttribute('href') || '';
const viewUrl = hrefToInlineView(rawHref);
const kind = attachmentPreviewKind(mime, name);
titleEl.textContent = name;
downloadLink.href = rawHref;
downloadLink.setAttribute('download', name);
bodyEl.className = 'attachment-preview-body attachment-preview-body--scroll';
bodyEl.innerHTML = '<p class="muted attachment-preview-loading">Lade Vorschau …</p>';
try {
if (kind === 'image') {
bodyEl.innerHTML = '';
const img = document.createElement('img');
img.className = 'attachment-preview-img';
img.alt = name;
img.src = viewUrl;
img.referrerPolicy = 'same-origin';
bodyEl.appendChild(img);
} else if (kind === 'pdf') {
bodyEl.className = 'attachment-preview-body attachment-preview-body--embed';
bodyEl.innerHTML = '';
const iframe = document.createElement('iframe');
iframe.className = 'attachment-preview-iframe';
iframe.title = name;
iframe.src = viewUrl;
bodyEl.appendChild(iframe);
} else if (kind === 'video') {
bodyEl.innerHTML = '';
const v = document.createElement('video');
v.className = 'attachment-preview-video';
v.controls = true;
v.playsInline = true;
v.src = viewUrl;
bodyEl.appendChild(v);
} else if (kind === 'audio') {
bodyEl.innerHTML = '';
const v = document.createElement('audio');
v.className = 'attachment-preview-audio';
v.controls = true;
v.src = viewUrl;
bodyEl.appendChild(v);
} else if (kind === 'text') {
const res = await fetch(viewUrl, { credentials: 'include' });
if (!res.ok) throw new Error('Laden fehlgeschlagen');
let text = await res.text();
if (text.length > TEXT_PREVIEW_MAX) {
text = `${text.slice(0, TEXT_PREVIEW_MAX)}\n\n… (gekürzt)`;
}
bodyEl.innerHTML = '';
const pre = document.createElement('pre');
pre.className = 'attachment-preview-text';
pre.textContent = text;
bodyEl.appendChild(pre);
} else {
bodyEl.innerHTML =
'<p class="muted">Für diesen Dateityp gibt es keine eingebaute Vorschau. Nutzen Sie „Herunterladen“.</p>';
}
} catch (err) {
bodyEl.className = 'attachment-preview-body attachment-preview-body--scroll';
bodyEl.innerHTML = `<p class="error">Vorschau konnte nicht geladen werden: ${err.message || err}</p>`;
}
if (typeof dialogEl.showModal === 'function') {
setPageScrollLocked(true);
dialogEl.showModal();
}
}