const CONTENT_IMAGES_LIST_ROLE = "content-images-list"; const CONTENT_IMAGES_DIALOG_ROLE = "content-images-dialog"; const CONTENT_IMAGES_CLOSE_ROLE = "content-images-close"; const CONTENT_IMAGES_FULL_ROLE = "content-images-full"; const CONTENT_IMAGES_DELETE_DIALOG_ROLE = "content-images-delete-dialog"; const CONTENT_IMAGES_DELETE_CONFIRM_ROLE = "content-images-delete-confirm"; const CONTENT_IMAGES_DELETE_CANCEL_ROLE = "content-images-delete-cancel"; const CONTENT_IMAGES_DELETE_NAME_ROLE = "content-images-delete-name"; const THUMB_PARAM = "300x0"; const FULL_PARAM = "0x1000"; const buildThumbUrl = (rawUrl, size) => { if (!rawUrl) { return ""; } if (rawUrl.includes("thumb=")) { return rawUrl; } const separator = rawUrl.includes("?") ? "&" : "?"; return `${rawUrl}${separator}thumb=${size}`; }; const buildFullUrl = (rawUrl) => { return buildThumbUrl(rawUrl, FULL_PARAM); }; const extractFileName = (rawUrl) => { if (!rawUrl) { return ""; } const cleanUrl = rawUrl.split("?")[0] || ""; const parts = cleanUrl.split("/"); return parts[parts.length - 1] || ""; }; const normalizeImages = (rawImages, rawFiles) => { const files = Array.isArray(rawFiles) ? rawFiles : []; return (Array.isArray(rawImages) ? rawImages : []).map((item, index) => { if (typeof item === "string") { const name = files[index] || extractFileName(item); return { url: item, name }; } if (item && typeof item === "object") { const url = item.url || ""; const name = item.name || files[index] || extractFileName(url); return { url, name }; } return { url: "", name: "" }; }); }; export class ContentImages extends HTMLElement { connectedCallback() { if (this.dataset.init === "true") { return; } this.dataset.init = "true"; this._pendingFiles = []; this._pendingUrls = []; this._pendingDeletes = new Set(); this._wireUpload(); const raw = this.getAttribute("data-images") || "[]"; const rawFiles = this.getAttribute("data-files") || "[]"; let images = []; let files = []; try { images = JSON.parse(raw); } catch { images = []; } try { files = JSON.parse(rawFiles); } catch { files = []; } const normalized = normalizeImages(images, files); this._render(normalized); } _wireUpload() { const panel = this.closest("[data-role='content-images-panel']"); if (!panel) { return; } const uploadInput = panel.querySelector("[data-role='content-images-upload-input']"); if (!uploadInput || uploadInput.dataset.bound === "true") { return; } uploadInput.dataset.bound = "true"; uploadInput.addEventListener("change", () => { this._setPendingFiles(Array.from(uploadInput.files || [])); }); } _setPendingFiles(files) { this._clearPendingPreviews(); this._pendingFiles = Array.isArray(files) ? files : []; this._pendingUrls = this._pendingFiles.map((file) => URL.createObjectURL(file)); this._render(this._currentImages || []); } _render(images) { this._currentImages = images; this.classList.add("block"); this.style.display = "block"; this.style.width = "100%"; const list = this._ensureList(); const uploadProxy = this._ensureUploadProxy(); if (uploadProxy && uploadProxy.parentElement === list) { uploadProxy.remove(); } list.querySelectorAll("[data-role='content-images-item'], [data-role='content-images-pending']").forEach((node) => { node.remove(); }); const deleteEndpoint = this.getAttribute("data-delete-endpoint") || ""; const contentId = this.getAttribute("data-content-id") || ""; const csrfToken = this.getAttribute("data-csrf-token") || ""; const canDelete = deleteEndpoint && contentId && csrfToken; images.forEach((image, index) => { const wrapper = document.createElement("div"); wrapper.className = "group relative"; wrapper.dataset.role = "content-images-item"; const isPendingDelete = this._pendingDeletes.has(image.name); if (isPendingDelete) { wrapper.classList.add("content-image-pending"); } const button = document.createElement("button"); button.type = "button"; button.className = [ "relative", "rounded", "border", "border-slate-200", "bg-white", "p-1", "shadow-sm", "transition", "hover:border-slate-400", "hover:shadow-md", ].join(" "); button.dataset.imageUrl = image.url; button.dataset.imageIndex = String(index); if (isPendingDelete) { button.setAttribute("aria-disabled", "true"); button.classList.add("content-image-pending-button"); } const img = document.createElement("img"); img.src = buildThumbUrl(image.url, THUMB_PARAM); img.alt = "Digitalisat"; img.loading = "lazy"; img.className = "h-28 w-28 object-cover"; button.appendChild(img); wrapper.appendChild(button); if (canDelete && image.name) { const deleteButton = document.createElement("button"); deleteButton.type = "button"; deleteButton.className = [ "absolute", "right-1", "top-1", "hidden", "rounded-full", "border", "border-red-200", "bg-white/90", "px-2", "py-1", "text-xs", "font-semibold", "text-red-700", "z-20", "shadow-sm", "transition", "group-hover:flex", "hover:text-red-900", "hover:border-red-300", ].join(" "); if (isPendingDelete) { deleteButton.classList.remove("border-red-200", "text-red-700"); deleteButton.classList.add("border-amber-300", "bg-amber-100", "text-amber-900", "hover:border-amber-400", "hover:text-amber-950"); deleteButton.innerHTML = 'Rueckgaengig'; } else { deleteButton.innerHTML = 'Entfernen'; } deleteButton.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); this._togglePendingDelete(image.name); }); wrapper.appendChild(deleteButton); } list.appendChild(wrapper); }); this._pendingUrls.forEach((url, index) => { const wrapper = document.createElement("div"); wrapper.className = "group relative"; wrapper.dataset.role = "content-images-pending"; const button = document.createElement("button"); button.type = "button"; button.className = [ "rounded", "border", "border-dashed", "border-slate-300", "bg-stone-50", "p-1", "shadow-sm", ].join(" "); button.dataset.imageUrl = url; button.dataset.imageIndex = `pending-${index}`; const img = document.createElement("img"); img.src = url; img.alt = "Digitalisat (neu)"; img.loading = "lazy"; img.className = "h-28 w-28 object-cover opacity-70"; button.appendChild(img); const badge = document.createElement("span"); badge.className = "absolute left-1 top-1 rounded bg-amber-200 px-1.5 py-0.5 text-[10px] font-semibold text-amber-900"; badge.textContent = "Neu"; wrapper.appendChild(button); wrapper.appendChild(badge); const removeButton = document.createElement("button"); removeButton.type = "button"; removeButton.className = "absolute right-1 top-1 hidden rounded-full border border-red-200 bg-white/90 px-2 py-1 text-xs font-semibold text-red-700 shadow-sm transition group-hover:flex hover:text-red-900 hover:border-red-300"; removeButton.innerHTML = 'Entfernen'; removeButton.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); this._removePendingFile(index); }); wrapper.appendChild(removeButton); list.appendChild(wrapper); }); if (uploadProxy && uploadProxy.parentElement !== list) { list.appendChild(uploadProxy); } const dialog = this._ensureDialog(); const fullImage = dialog.querySelector(`[data-role='${CONTENT_IMAGES_FULL_ROLE}']`); list.addEventListener("click", (event) => { const target = event.target.closest("button[data-image-url]"); if (!target || !fullImage) { return; } const url = target.dataset.imageUrl || ""; const fullUrl = url.startsWith("blob:") ? url : buildFullUrl(url); fullImage.src = fullUrl; fullImage.alt = "Digitalisat"; if (dialog.showModal) { dialog.showModal(); } else { dialog.setAttribute("open", "true"); } }); } _ensureList() { let list = this.querySelector(`[data-role='${CONTENT_IMAGES_LIST_ROLE}']`); if (!list) { list = document.createElement("div"); list.dataset.role = CONTENT_IMAGES_LIST_ROLE; this.appendChild(list); } list.className = "grid gap-2"; list.style.gridTemplateColumns = "repeat(auto-fill, minmax(7rem, 1fr))"; list.style.width = "100%"; return list; } _findUploadTile() { const panel = this.closest("[data-role='content-images-panel']"); if (!panel) { return null; } const upload = panel.querySelector("[data-role='content-images-upload']"); if (!upload) { return null; } return upload; } _ensureUploadProxy() { const panel = this.closest("[data-role='content-images-panel']"); if (!panel) { return null; } const uploadInput = panel.querySelector("[data-role='content-images-upload-input']"); if (!uploadInput) { return null; } let proxy = panel.querySelector("[data-role='content-images-upload-proxy']"); if (!proxy) { proxy = document.createElement("button"); proxy.type = "button"; proxy.dataset.role = "content-images-upload-proxy"; proxy.className = "flex h-28 w-28 items-center justify-center rounded-xs border-2 border-dashed border-slate-300 bg-stone-50 text-lg font-semibold text-slate-600 transition hover:border-slate-400 hover:text-slate-800"; proxy.setAttribute("aria-label", "Bilder hinzufuegen"); proxy.innerHTML = ''; proxy.addEventListener("click", () => { uploadInput.click(); }); } return proxy; } _togglePendingDelete(fileName) { if (!fileName) { return; } if (this._pendingDeletes.has(fileName)) { this._pendingDeletes.delete(fileName); } else { this._pendingDeletes.add(fileName); } this._render(this._currentImages || []); } _removePendingFile(index) { if (index < 0 || index >= this._pendingFiles.length) { return; } const url = this._pendingUrls[index]; if (url) { URL.revokeObjectURL(url); } this._pendingFiles.splice(index, 1); this._pendingUrls.splice(index, 1); this._render(this._currentImages || []); } getPendingFiles() { return Array.isArray(this._pendingFiles) ? this._pendingFiles : []; } getPendingDeletes() { return Array.from(this._pendingDeletes || []); } _clearPendingPreviews() { if (Array.isArray(this._pendingUrls)) { this._pendingUrls.forEach((url) => URL.revokeObjectURL(url)); } this._pendingUrls = []; } disconnectedCallback() { this._clearPendingPreviews(); } _ensureDialog() { let dialog = this.querySelector(`[data-role='${CONTENT_IMAGES_DIALOG_ROLE}']`); if (dialog) { return dialog; } dialog = document.createElement("dialog"); dialog.dataset.role = CONTENT_IMAGES_DIALOG_ROLE; dialog.className = [ "fixed", "inset-0", "m-auto", "w-full", "max-w-5xl", "rounded-md", "border", "border-slate-200", "bg-white", "p-0", "shadow-xl", "backdrop:bg-black/60", ].join(" "); dialog.innerHTML = `
Das Digitalisat wird dauerhaft entfernt.