Files
xxx-sphere-web/bin/main/static/js/shared.js
Mario e35b095c18
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Locations können nun auch Posten - bugfixes im Feed
2026-04-13 23:04:15 +02:00

577 lines
33 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.
// ─────────────────────────────────────────────────────────────────────────────
// shared.js Gemeinsame Helfer & Komponenten
// Einbinden: <script src="/js/shared.js"></script>
// (vor allen Seiten-Skripten, nach CSS-Links)
// ─────────────────────────────────────────────────────────────────────────────
// ── CSS-Injection (Comment + Carousel) ────────────────────────────────────────
(function injectSharedStyles() {
if (document.getElementById('shared-styles')) return;
const s = document.createElement('style');
s.id = 'shared-styles';
s.textContent = `
/* ── Karussell ── */
.post-carousel{position:relative;margin-top:0.5rem}
.car-slide{display:none}
.car-slide.active{display:block}
.car-btn{position:absolute;top:50%;transform:translateY(-50%);background:rgba(0,0,0,0.55);border:none;color:#fff;font-size:2.2rem;width:auto;min-width:2.4rem;height:3.2rem;border-radius:8px;cursor:pointer;z-index:5;display:flex;align-items:center;justify-content:center;padding:0 0.5rem;margin:0;line-height:1}
.car-prev{left:0.3rem}
.car-next{right:0.3rem}
.car-indicator{text-align:center;font-size:0.75rem;color:var(--color-muted);margin-top:0.25rem}
/* ── Bilder-Grid (nur im Feed) ── */
.post-img-grid{display:grid;gap:2px;margin:0.5rem auto 0;border-radius:6px;overflow:hidden;flex-shrink:0}
.pig-item{position:relative;overflow:hidden;min-width:0;min-height:0}
.pig-item img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
.pig-contain img{object-fit:contain}
.pig-dark{background:#111}.pig-dark .pig-item{background:#111}
.pig-more{position:absolute;inset:0;background:rgba(0,0,0,0.55);display:flex;align-items:center;justify-content:center;font-size:1.8rem;font-weight:700;color:#fff;pointer-events:none}
/* ── Like / Löschen-Buttons ── */
.btn-like{background:none;border:1px solid rgba(255,255,255,0.15);border-radius:20px;padding:0.2rem 0.65rem;color:var(--color-muted);font-size:0.78rem;cursor:pointer;display:inline-flex;align-items:center;gap:0.3rem;margin:0;width:auto;transition:border-color 0.15s,color 0.15s}
.btn-like:hover,.btn-like.liked{border-color:var(--color-primary);color:var(--color-primary)}
.btn-delete-small{background:none;border:none;color:rgba(200,50,50,0.6);font-size:0.78rem;cursor:pointer;margin:0;width:auto;padding:0}
.btn-delete-small:hover{color:var(--color-primary)}
.btn-text{background:none;border:none;color:var(--color-muted);font-size:0.78rem;cursor:pointer;margin:0;width:auto;padding:0;text-decoration:underline;text-decoration-color:rgba(255,255,255,0.2)}
.btn-text:hover{color:var(--color-text)}
/* ── Kommentare ── */
.comment-item{display:flex;gap:0.5rem;margin-bottom:0.5rem}
.comment-avatar{width:28px;height:28px;border-radius:50%;background:var(--color-secondary);display:flex;align-items:center;justify-content:center;font-size:0.75rem;flex-shrink:0;overflow:hidden}
.comment-avatar img{width:100%;height:100%;object-fit:cover}
.comment-body{flex:1;background:rgba(255,255,255,0.04);border-radius:6px;padding:0.5rem 0.65rem}
.comment-author{font-size:0.8rem;font-weight:600;color:var(--color-text)}
.comment-date{font-size:0.72rem;color:var(--color-muted);margin-left:0.4rem}
.comment-text{font-size:0.85rem;color:rgba(255,255,255,0.75);margin-top:0.2rem;line-height:1.45;white-space:pre-wrap;word-break:break-word}
.comment-actions{display:flex;gap:0.4rem;margin-top:0.3rem;align-items:center}
.replies-section{margin-top:0.5rem;padding-left:0.5rem;border-left:2px solid rgba(255,255,255,0.06)}
.comment-write{display:flex;gap:0.4rem;margin-top:0.5rem}
.comment-write input{flex:1;padding:0.4rem 0.75rem;font-size:0.85rem}
.comment-write button{width:auto;padding:0.4rem 0.75rem;font-size:0.82rem;white-space:nowrap}
`;
document.head.appendChild(s);
})();
// ── HTML-Escape ────────────────────────────────────────────────────────────────
function esc(str) {
if (!str) return '';
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/\n/g, '<br>');
}
// ── Datum-Format ──────────────────────────────────────────────────────────────
function fmtDate(iso) {
if (!iso) return '';
const d = new Date(iso);
return d.toLocaleDateString('de-DE') + ' ' + d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
}
// ── Emoji-Picker ──────────────────────────────────────────────────────────────
const EMOJIS = ['😊','😂','❤️','😍','🔥','👍','🥰','😎','🤔','😘','💕','🎉','✨','💋','😈','🫦','🍑','🍆','🔞','🥵','😭','😢','😤','🙄','🤦','🤷','🙏','💪','😏','🤩'];
let _emojiTarget = null;
function toggleEmojiPicker(btn, targetId) {
_emojiTarget = document.getElementById(targetId);
let picker = document.getElementById('sharedEmojiPicker');
if (!picker) {
picker = document.createElement('div');
picker.id = 'sharedEmojiPicker';
picker.style.cssText = 'position:fixed;z-index:9000;background:var(--color-card);border:1px solid var(--color-secondary);border-radius:10px;padding:0.5rem;display:flex;flex-wrap:wrap;gap:0.2rem;max-width:260px;box-shadow:0 4px 20px rgba(0,0,0,0.5);';
EMOJIS.forEach(em => {
const b = document.createElement('button');
b.textContent = em;
b.style.cssText = 'background:none;border:none;font-size:1.3rem;cursor:pointer;padding:0.2rem;margin:0;width:auto;line-height:1;';
b.onclick = e => { e.stopPropagation(); insertEmoji(em); };
picker.appendChild(b);
});
document.body.appendChild(picker);
}
if (picker.style.display === 'flex') { picker.style.display = 'none'; return; }
picker.style.display = 'flex';
requestAnimationFrame(() => {
const rect = btn.getBoundingClientRect();
const ph = picker.offsetHeight, pw = picker.offsetWidth;
let top = rect.top - ph - 8;
let left = rect.left;
if (top < 8) top = rect.bottom + 8;
if (left + pw > window.innerWidth - 8) left = window.innerWidth - pw - 8;
picker.style.top = top + 'px';
picker.style.left = left + 'px';
});
}
function insertEmoji(emoji) {
if (!_emojiTarget) return;
const start = _emojiTarget.selectionStart ?? _emojiTarget.value.length;
const end = _emojiTarget.selectionEnd ?? start;
_emojiTarget.value = _emojiTarget.value.slice(0, start) + emoji + _emojiTarget.value.slice(end);
_emojiTarget.selectionStart = _emojiTarget.selectionEnd = start + emoji.length;
_emojiTarget.focus();
}
document.addEventListener('click', e => {
const picker = document.getElementById('sharedEmojiPicker');
if (picker && picker.style.display === 'flex'
&& !picker.contains(e.target)
&& !e.target.closest('[onclick*="toggleEmojiPicker"]')) {
picker.style.display = 'none';
}
});
// ── Bild-Karussell (Lightbox/Detail-Ansicht) ─────────────────────────────────
function bilderCarousel(bilder) {
if (!bilder || bilder.length === 0) return '';
const slides = bilder.map((b, i) =>
`<div class="car-slide${i === 0 ? ' active' : ''}"><img class="post-bild" src="data:image/jpeg;base64,${b}" alt=""></div>`
).join('');
const nav = bilder.length > 1
? `<button class="car-btn car-prev" onclick="event.stopPropagation();carNav(this,-1)">&#8249;</button>
<button class="car-btn car-next" onclick="event.stopPropagation();carNav(this,1)">&#8250;</button>
<div class="car-indicator"><span class="car-cur">1</span>&#8202;/&#8202;${bilder.length}</div>`
: '';
return `<div class="post-carousel">${slides}${nav}</div>`;
}
/** Richtet die Lightbox-Inhalte ein:
* View-Area bekommt Flex-Layout damit das Bild den Platz füllt
* Bild-Container bekommt lb-ic-Klasse + Carousel (für alle Bildanzahlen)
* Post-Text bekommt scrollbare Höhenbegrenzung
*/
function _lbSetupContent(postId, prefix, bilder) {
const body = document.getElementById('lbPostBody');
const va = body.querySelector(`#${prefix}va-${postId}`);
if (va) va.classList.add('lb-va');
const hasImages = bilder && bilder.length > 0;
const pbi = body.querySelector(`#${prefix}bi-${postId}`);
if (pbi) {
if (hasImages) {
pbi.classList.add('lb-ic');
pbi.innerHTML = bilderCarousel(bilder);
} else {
pbi.style.display = 'none';
}
}
if (va) va.querySelector('.post-text')?.classList.add('lb-text');
// Text-only layout: kein Bild → Kommentare unterhalb, volle Breite
const layout = document.querySelector('#postLightbox .lb-layout');
if (layout) layout.classList.toggle('lb-text-only', !hasImages);
}
// ── Bilder-Grid (Feed-Karten, orientierungsabhängig) ──────────────────────────
const POST_IMG_SIZE = 500; // px — Breite und Höhe des Bild-Containers
let _pigSeq = 0;
const _pigStore = new Map(); // id → bilder[]
function bilderGrid(bilder) {
if (!bilder || bilder.length === 0) return '';
const S = POST_IMG_SIZE;
const id = 'pig-' + (++_pigSeq);
if (bilder.length === 1) {
// Längere Seite = S, kürzere letterboxed
return `<div class="post-img-grid pig-contain" id="${id}" style="width:${S}px;height:${S}px;grid-template-columns:1fr;grid-template-rows:1fr;">
<div class="pig-item pig-contain"><img src="data:image/jpeg;base64,${bilder[0]}" alt=""></div>
</div>`;
}
// 2+ Bilder: Orientierung des ersten Bilds bestimmt das Layout → deferred init
_pigStore.set(id, bilder);
return `<div class="post-img-grid" id="${id}" style="width:${S}px;height:${S}px;">` +
`<img src="data:image/jpeg;base64,${bilder[0]}" style="display:none;position:absolute;" alt="" onload="_pigInit('${id}',this)">` +
`</div>`;
}
function _pigInit(id, probe) {
const bilder = _pigStore.get(id);
if (!bilder) return;
_pigStore.delete(id);
const grid = document.getElementById(id);
if (!grid) return;
const land = probe.naturalWidth >= probe.naturalHeight; // quer = landscape
const n = bilder.length;
const extra = n - 3;
const moreHtml = extra > 0 ? `<div class="pig-more">+${extra}</div>` : '';
grid.innerHTML = ''; // Probe-Bild entfernen
// Fraktionale Werte → Browser berücksichtigt gap automatisch, Trennstrich immer genau in der Mitte
if (n === 2) {
if (land) {
// Quer-erstes Bild → beide übereinander (Trennstrich horizontal in der Mitte)
grid.style.gridTemplateColumns = '1fr';
grid.style.gridTemplateRows = '1fr 1fr';
} else {
// Hochkant-erstes Bild → beide nebeneinander (Trennstrich vertikal in der Mitte)
grid.style.gridTemplateColumns = '1fr 1fr';
grid.style.gridTemplateRows = '1fr';
}
grid.insertAdjacentHTML('beforeend',
`<div class="pig-item"><img src="data:image/jpeg;base64,${bilder[0]}" alt=""></div>` +
`<div class="pig-item"><img src="data:image/jpeg;base64,${bilder[1]}" alt=""></div>`);
} else {
// 3+ Bilder (ab 4 gleiche Darstellung wie bei 3, +N-Overlay auf Zelle 3)
// Dunkler Hintergrund nur hier, damit der Spalt zwischen den Zellen nicht auffällt
grid.classList.add('pig-dark');
grid.style.gridTemplateColumns = '1fr 1fr';
grid.style.gridTemplateRows = '1fr 1fr';
if (land) {
// Quer → Bild 1 oben (volle Breite), Bilder 2+3 nebeneinander unten
grid.insertAdjacentHTML('beforeend',
`<div class="pig-item" style="grid-column:1/3"><img src="data:image/jpeg;base64,${bilder[0]}" alt=""></div>` +
`<div class="pig-item"><img src="data:image/jpeg;base64,${bilder[1]}" alt=""></div>` +
`<div class="pig-item"><img src="data:image/jpeg;base64,${bilder[2]}" alt="">${moreHtml}</div>`);
} else {
// Hochkant → Bild 1 links (volle Höhe), Bilder 2+3 übereinander rechts
grid.insertAdjacentHTML('beforeend',
`<div class="pig-item" style="grid-row:1/3"><img src="data:image/jpeg;base64,${bilder[0]}" alt=""></div>` +
`<div class="pig-item"><img src="data:image/jpeg;base64,${bilder[1]}" alt=""></div>` +
`<div class="pig-item"><img src="data:image/jpeg;base64,${bilder[2]}" alt="">${moreHtml}</div>`);
}
}
}
function carNav(btn, dir) {
const car = btn.closest('.post-carousel');
const slides = Array.from(car.querySelectorAll('.car-slide'));
const cur = slides.findIndex(s => s.classList.contains('active'));
slides[cur].classList.remove('active');
const next = (cur + dir + slides.length) % slides.length;
slides[next].classList.add('active');
const ind = car.querySelector('.car-cur');
if (ind) ind.textContent = next + 1;
}
// ── Kommentar-Rendering ───────────────────────────────────────────────────────
// opts: { myUserId, showReplies }
// Seite muss definieren: deleteKommentar(kommentarId, targetType, targetId)
function renderKommentarHtml(k, targetType, targetId, opts) {
const { myUserId = null, showReplies = false } = opts || {};
const avatarHtml = k.authorPicture
? `<img src="data:image/png;base64,${k.authorPicture}" alt="">`
: '◉';
const canDelete = k.authorId === myUserId;
const replyLabel = k.replyCount > 0 ? `Antworten (${k.replyCount})` : 'Antworten';
return `<div class="comment-item" id="kom-${k.kommentarId}">
<div class="comment-avatar">${avatarHtml}</div>
<div class="comment-body">
<span class="comment-author">${esc(k.authorName)}</span>
<span class="comment-date">${fmtDate(k.createdAt)}</span>
<div class="comment-text">${esc(k.text)}</div>
<div class="comment-actions">
<button class="btn-like${k.likedByMe ? ' liked' : ''}" id="lk-kom-${k.kommentarId}"
onclick="toggleKommentarLike('${k.kommentarId}')">♥ <span id="lkc-kom-${k.kommentarId}">${k.likeCount}</span></button>
${showReplies ? `<button class="btn-text" onclick="toggleReplies('${k.kommentarId}')">${replyLabel}</button>` : ''}
${canDelete ? `<button class="btn-delete-small" onclick="deleteKommentar('${k.kommentarId}','${targetType}','${targetId}')">✕</button>` : ''}
</div>
${showReplies ? `<div class="replies-section" id="replies-${k.kommentarId}" style="display:none;"></div>` : ''}
</div>
</div>`;
}
function renderReplyHtml(r, parentId) {
const avatarHtml = r.authorPicture
? `<img src="data:image/png;base64,${r.authorPicture}" alt="">`
: '◉';
const canDelete = typeof window.myUserId !== 'undefined' && r.authorId === window.myUserId;
return `<div class="comment-item" id="kom-${r.kommentarId}" style="margin-bottom:0.35rem;">
<div class="comment-avatar" style="width:22px;height:22px;font-size:0.75rem;">${avatarHtml}</div>
<div class="comment-body" style="padding:0.35rem 0.55rem;">
<span class="comment-author">${esc(r.authorName)}</span>
<span class="comment-date">${fmtDate(r.createdAt)}</span>
<div class="comment-text">${esc(r.text)}</div>
<div class="comment-actions">
<button class="btn-like${r.likedByMe ? ' liked' : ''}" id="lk-kom-${r.kommentarId}"
onclick="toggleKommentarLike('${r.kommentarId}')">♥ <span id="lkc-kom-${r.kommentarId}">${r.likeCount}</span></button>
${canDelete ? `<button class="btn-delete-small" onclick="deleteReply('${r.kommentarId}','${parentId}')">✕</button>` : ''}
</div>
</div>
</div>`;
}
async function toggleKommentarLike(kommentarId) {
await fetch('/social/kommentare/' + kommentarId + '/like', { method: 'POST' });
const btn = document.getElementById('lk-kom-' + kommentarId);
const lc = document.getElementById('lkc-kom-' + kommentarId);
if (!btn || !lc) return;
const was = btn.classList.contains('liked');
btn.classList.toggle('liked', !was);
lc.textContent = parseInt(lc.textContent) + (was ? -1 : 1);
}
async function toggleReplies(kommentarId) {
const section = document.getElementById('replies-' + kommentarId);
if (section.style.display === 'none') {
section.style.display = '';
await loadReplies(kommentarId);
} else {
section.style.display = 'none';
}
}
async function loadReplies(kommentarId) {
const res = await fetch(`/social/kommentare?targetType=KOMMENTAR&targetId=${kommentarId}`);
const replies = await res.json();
const section = document.getElementById('replies-' + kommentarId);
replies.reverse();
section.innerHTML = (replies.length === 0
? '<p style="color:var(--color-muted);font-size:0.78rem;margin-bottom:0.35rem;">Noch keine Antworten.</p>'
: replies.map(r => renderReplyHtml(r, kommentarId)).join(''))
+ `<div class="comment-write">
<input type="text" id="ri-${kommentarId}" placeholder="Antwort schreiben…" maxlength="500"
onkeydown="if(event.key==='Enter') postReply('${kommentarId}')">
<button onclick="postReply('${kommentarId}')">Senden</button>
</div>`;
}
async function postReply(kommentarId) {
const input = document.getElementById('ri-' + kommentarId);
const text = input.value.trim();
if (!text) return;
await fetch('/social/kommentare', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ targetType: 'KOMMENTAR', targetId: kommentarId, text })
});
input.value = '';
await loadReplies(kommentarId);
}
async function deleteReply(replyId, parentId) {
await fetch('/social/kommentare/' + replyId, { method: 'DELETE' });
await loadReplies(parentId);
}
// ─────────────────────────────────────────────────────────────────────────────
// Gemeinsame Lightbox (standardisierte IDs: #postLightbox, #lbPostBody,
// #lbCommentsList, #lbCommentInput auf allen Feed-Seiten gleich)
// ─────────────────────────────────────────────────────────────────────────────
let _lbMyUserId = null;
let _lbPostId = null;
let _lbPostType = null;
/** Muss nach dem Login mit der eigenen userId aufgerufen werden. */
function initLb(userId) { _lbMyUserId = userId; }
function closeLb() {
document.getElementById('postLightbox')?.classList.remove('open');
document.body.style.overflow = '';
_lbPostId = null; _lbPostType = null;
}
// Escape schließt, Pfeiltasten navigieren das Karussell
document.addEventListener('keydown', e => {
const lb = document.getElementById('postLightbox');
if (!lb?.classList.contains('open')) return;
if (e.key === 'Escape') { closeLb(); return; }
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
const car = lb.querySelector('.post-carousel');
if (!car) return;
const slides = Array.from(car.querySelectorAll('.car-slide'));
if (slides.length <= 1) return;
const cur = slides.findIndex(s => s.classList.contains('active'));
const next = (cur + (e.key === 'ArrowLeft' ? -1 : 1) + slides.length) % slides.length;
slides[cur].classList.remove('active');
slides[next].classList.add('active');
const ind = car.querySelector('.car-cur');
if (ind) ind.textContent = next + 1;
});
async function loadLbComments(postId, postType) {
_lbPostId = postId; _lbPostType = postType;
const targetType = postType === 'GROUP' ? 'GROUP_POST' : 'FEED_POST';
try {
const res = await fetch(`/social/kommentare?targetType=${targetType}&targetId=${postId}`);
const comments = await res.json();
comments.reverse();
document.getElementById('lbCommentsList').innerHTML = comments.length === 0
? '<p style="color:var(--color-muted);font-size:0.82rem;margin:0.4rem;">Noch keine Kommentare.</p>'
: comments.map(k => renderKommentarHtml(k, targetType, postId, { myUserId: _lbMyUserId })).join('');
} catch (_) {}
}
async function postLbComment() {
if (!_lbPostId) return;
const input = document.getElementById('lbCommentInput');
const text = input.value.trim();
if (!text) return;
const targetType = _lbPostType === 'GROUP' ? 'GROUP_POST' : 'FEED_POST';
await fetch('/social/kommentare', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ targetType, targetId: _lbPostId, text })
});
input.value = '';
await loadLbComments(_lbPostId, _lbPostType);
const kcEl = document.getElementById('kc-' + _lbPostId) || document.getElementById('hkc-' + _lbPostId);
if (kcEl) kcEl.textContent = parseInt(kcEl.textContent || '0') + 1;
}
async function deleteKommentar(kommentarId, targetType, targetId) {
await fetch('/social/kommentare/' + kommentarId, { method: 'DELETE' });
await loadLbComments(targetId, _lbPostType);
}
// ─────────────────────────────────────────────────────────────────────────────
// Bild-Verarbeitung (Compose & Edit)
// ─────────────────────────────────────────────────────────────────────────────
/** Liest eine Bilddatei, skaliert auf max. 1024px, komprimiert auf JPEG 85 %
* und hängt den Base64-String an bilderArr an, dann ruft renderFn() auf. */
function processImageFile(file, bilderArr, renderFn) {
if (!file || !file.type.startsWith('image/')) return;
const reader = new FileReader();
reader.onload = e => {
const img = new Image();
img.onload = () => {
const MAX = 1024, canvas = document.createElement('canvas');
const s = Math.min(MAX / img.width, MAX / img.height, 1);
canvas.width = Math.round(img.width * s); canvas.height = Math.round(img.height * s);
canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height);
bilderArr.push(canvas.toDataURL('image/jpeg', 0.85).split(',')[1]);
renderFn();
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
/** Rendert Vorschau-Thumbnails in containerId; rmCallback(idx) wird beim ✕ aufgerufen. */
function renderBilderThumbs(bilderArr, containerId, rmCallback) {
const c = document.getElementById(containerId);
if (!c) return;
c.innerHTML = '';
bilderArr.forEach((b, i) => {
const div = document.createElement('div');
div.className = 'compose-thumb';
const img = document.createElement('img');
img.src = `data:image/jpeg;base64,${b}`;
const btn = document.createElement('button');
btn.className = 'compose-thumb-remove';
btn.textContent = '✕'; btn.title = 'Entfernen';
btn.onclick = ev => { ev.stopPropagation(); rmCallback(i); };
div.appendChild(img); div.appendChild(btn);
c.appendChild(div);
});
c.style.display = bilderArr.length > 0 ? 'flex' : 'none';
}
// ─────────────────────────────────────────────────────────────────────────────
// Post-Bearbeitung (gemeinsam, über Präfix parametrisiert)
// Präfixe: 'p' (feed), 'hp' (userhome), 'gp' (gruppe)
// IDs-Muster: ${prefix}va-, ${prefix}bi-, ${prefix}ea-, ${prefix}um-,
// ${prefix}m-, ${prefix}et-, ${prefix}et-tb-, ${prefix}eo-, ${prefix}mc-
// ─────────────────────────────────────────────────────────────────────────────
/**
* cfg: { postId, prefix, data, editBilderMap,
* saveFn, cancelFn, addImgFn, addOptionFn, rmImgFn }
* Alle Fn-Namen sind Strings (globale Funktionsnamen auf der jeweiligen Seite).
*/
function startPostEdit(cfg) {
const { postId, prefix, data, editBilderMap, saveFn, cancelFn, addImgFn, addOptionFn, rmImgFn } = cfg;
editBilderMap.set(postId, [...(data.bilder || [])]);
document.getElementById(`${prefix}va-${postId}`).style.display = 'none';
document.getElementById(`${prefix}um-${postId}`).style.display = 'none';
const isUmfrage = data.beitragTyp === 'UMFRAGE';
const optionenHtml = isUmfrage
? `<div id="${prefix}eo-${postId}" style="margin-top:0.5rem;">${(data.optionen || []).map(o =>
`<div class="umfrage-option-row">
<input type="text" value="${esc(o.text)}" maxlength="200" data-option-id="${o.optionId}"
style="flex:1;padding:0.4rem 0.6rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.9rem;"
onmousedown="event.stopPropagation()" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()">
<button onmousedown="event.stopPropagation()" onclick="event.stopPropagation();this.closest('.umfrage-option-row').remove()" style="width:auto;margin:0;padding:0.3rem 0.6rem;font-size:0.8rem;">✕</button>
</div>`).join('')}
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:0.3rem;" onclick="event.stopPropagation()">
<button onmousedown="event.stopPropagation()" onclick="event.stopPropagation();${addOptionFn}('${postId}')" style="width:auto;margin:0;padding:0.3rem 0.75rem;font-size:0.8rem;">+ Option</button>
<label class="multi-toggle" onmousedown="event.stopPropagation()" onclick="event.stopPropagation()">
<input type="checkbox" id="${prefix}mc-${postId}" ${data.multiChoice ? 'checked' : ''}> Mehrfachauswahl möglich
</label>
</div>
</div>`
: '';
const actionRow = `<div style="display:flex;gap:0.5rem;align-items:center;margin-top:0.5rem;" onclick="event.stopPropagation()">
<label class="compose-action-btn" title="Fotos hinzufügen">📷
<input type="file" accept="image/*" multiple style="display:none;" onchange="event.stopPropagation();${addImgFn}(this,'${postId}')">
</label>
<button onclick="event.stopPropagation();${saveFn}('${postId}')" style="width:auto;margin:0;">Speichern</button>
<button onclick="event.stopPropagation();${cancelFn}('${postId}')" style="width:auto;margin:0;background:var(--color-secondary);color:var(--color-text);">Abbrechen</button>
</div>`;
const ea = document.getElementById(`${prefix}ea-${postId}`);
ea.style.display = ''; ea.onclick = e => e.stopPropagation();
ea.innerHTML = `<textarea id="${prefix}et-${postId}" style="width:100%;box-sizing:border-box;padding:0.6rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;resize:vertical;min-height:70px;" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()">${esc(data.text || '')}</textarea>
<div class="compose-thumbs" id="${prefix}et-tb-${postId}" style="margin-top:0.4rem;"></div>
${optionenHtml}${actionRow}`;
_renderEditThumbs(editBilderMap, postId, prefix, rmImgFn);
}
function cancelPostEdit(postId, prefix, editBilderMap) {
document.getElementById(`${prefix}va-${postId}`).style.display = '';
document.getElementById(`${prefix}um-${postId}`).style.display = '';
document.getElementById(`${prefix}ea-${postId}`).style.display = 'none';
editBilderMap.delete(postId);
}
function _renderEditThumbs(editBilderMap, postId, prefix, rmFn) {
const bilder = editBilderMap.get(postId) || [];
const c = document.getElementById(`${prefix}et-tb-${postId}`);
if (!c) return;
c.innerHTML = bilder.map((b, i) =>
`<div class="compose-thumb"><img src="data:image/jpeg;base64,${b}" alt="">
<button class="compose-thumb-remove" onclick="event.stopPropagation();${rmFn}('${postId}',${i})">✕</button></div>`
).join('');
c.style.display = bilder.length > 0 ? 'flex' : 'none';
}
function editAddOptionRow(containerId) {
const container = document.getElementById(containerId);
if (!container) return;
const count = container.querySelectorAll('input[type=text]').length;
const row = document.createElement('div');
row.className = 'umfrage-option-row';
row.innerHTML = `<input type="text" placeholder="Option ${count + 1}" maxlength="200"
style="flex:1;padding:0.4rem 0.6rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.9rem;"
onmousedown="event.stopPropagation()" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()">
<button onmousedown="event.stopPropagation()" onclick="event.stopPropagation();this.closest('.umfrage-option-row').remove()" style="width:auto;margin:0;padding:0.3rem 0.6rem;font-size:0.8rem;">✕</button>`;
container.insertBefore(row, container.querySelector('div:last-child'));
}
/**
* cfg: { postId, prefix, endpoint, isUmfrage, editBilderMap, onSuccess }
* onSuccess(updated) Seite aktualisiert Cache und DOM.
*/
async function savePostEdit(cfg) {
const { postId, prefix, endpoint, isUmfrage, editBilderMap, onSuccess } = cfg;
const text = document.getElementById(`${prefix}et-${postId}`).value.trim();
if (!text) return;
const bilder = editBilderMap.get(postId) || [];
const optionen = isUmfrage
? Array.from(document.querySelectorAll(`#${prefix}eo-${postId} input[type=text]`))
.map(inp => ({ optionId: inp.dataset.optionId || null, text: inp.value.trim() }))
.filter(o => o.text)
: null;
const multiChoice = isUmfrage ? (document.getElementById(`${prefix}mc-${postId}`)?.checked ?? false) : null;
const res = await fetch(endpoint, {
method: 'PUT', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, bilder, optionen, multiChoice })
});
if (!res.ok) return;
editBilderMap.delete(postId);
onSuccess(await res.json());
}
/** Aktualisiert Text, Bilder, Edit-Area, Umfrage und (bearbeitet)-Label im DOM. */
function applyPostEditDom(postId, prefix, updated, umfrageHtml) {
document.getElementById(`${prefix}va-${postId}`).querySelector('.post-text').innerHTML = renderTextWithHashtags(updated.text);
document.getElementById(`${prefix}bi-${postId}`).innerHTML = bilderGrid(updated.bilder);
document.getElementById(`${prefix}va-${postId}`).style.display = '';
document.getElementById(`${prefix}ea-${postId}`).style.display = 'none';
const pum = document.getElementById(`${prefix}um-${postId}`);
if (pum) { pum.innerHTML = umfrageHtml || ''; pum.style.display = ''; }
const meta = document.getElementById(`${prefix}m-${postId}`);
if (meta && !meta.querySelector('.edited-label')) {
meta.insertAdjacentHTML('beforeend', ' <span class="edited-label" style="font-size:0.75rem;color:var(--color-muted);">(bearbeitet)</span>');
}
}