166 lines
5.7 KiB
JavaScript
166 lines
5.7 KiB
JavaScript
/** 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();
|
||
}
|
||
}
|