function initMergePdf() { const filesInput = document.getElementById("mergePdfFiles"); const filesAddInput = document.getElementById("mergePdfFilesAdd"); const filesReplaceInput = document.getElementById("mergePdfFilesReplace"); const fileListEl = document.getElementById("mergePdfFileList"); const reorderHintEl = document.getElementById("mergePdfReorderHint"); const previewGridEl = document.getElementById("mergePdfPreviewGrid"); const titleEl = document.getElementById("mergePdfTitle"); const subtitleEl = document.getElementById("mergePdfSubtitle"); const chooseTextEl = document.getElementById("mergePdfChooseText"); const statusEl = document.getElementById("mergePdfStatus"); const mergeBtn = document.getElementById("mergePdfMergeBtn"); const cancelBtn = document.getElementById("mergePdfCancelBtn"); const downloadBtn = document.getElementById("mergePdfDownloadBtn"); const clearBtn = document.getElementById("mergePdfClearBtn"); const csrfTokenEl = document.getElementById("csrfToken"); const progressWrap = document.getElementById("mergePdfProgress"); const progressBar = document.getElementById("mergePdfProgressBar"); const progressText = document.getElementById("mergePdfProgressText"); const chooseLabelEl = document.getElementById("mergePdfChooseLabel"); const replaceLabelEl = document.getElementById("mergePdfReplaceLabel"); const addMoreLabelEl = document.getElementById("mergePdfAddMoreLabel"); if (!filesInput || !mergeBtn) return; const maxFileSize = 25 * 1024 * 1024; const maxTotalSize = 100 * 1024 * 1024; const maxFiles = 20; let selectedFiles = []; let mergedBlob = null; let mergedObjectUrl = null; let isMerged = false; let cardPreviewUrls = []; let dragFromIndex = -1; let activeXhr = null; let suppressAbortNotice = false; function setStatus(text, isError) { if (!statusEl) return; statusEl.textContent = text; statusEl.classList.toggle("status-error", !!isError); statusEl.classList.toggle("status-success", !isError); } function setUploadBoxCopy(mode) { if (!titleEl || !subtitleEl || !chooseTextEl) return; const showInitialChoose = () => { if (!chooseLabelEl) return; chooseLabelEl.hidden = false; chooseLabelEl.style.display = "inline-block"; if (replaceLabelEl) { replaceLabelEl.hidden = true; replaceLabelEl.style.display = "none"; } if (addMoreLabelEl) { addMoreLabelEl.hidden = true; addMoreLabelEl.style.display = "none"; } }; const showAddMoreOnly = () => { if (!chooseLabelEl) return; chooseLabelEl.hidden = true; chooseLabelEl.style.display = "none"; if (replaceLabelEl) { replaceLabelEl.hidden = true; replaceLabelEl.style.display = "none"; } if (addMoreLabelEl) { addMoreLabelEl.hidden = false; addMoreLabelEl.style.display = "inline-block"; } }; const showReplaceOnly = () => { if (!chooseLabelEl) return; chooseLabelEl.hidden = true; chooseLabelEl.style.display = "none"; if (replaceLabelEl) { replaceLabelEl.hidden = false; replaceLabelEl.style.display = "inline-block"; } if (addMoreLabelEl) { addMoreLabelEl.hidden = true; addMoreLabelEl.style.display = "none"; } }; const hideAllChoose = () => { if (!chooseLabelEl) return; chooseLabelEl.hidden = true; chooseLabelEl.style.display = "none"; if (replaceLabelEl) { replaceLabelEl.hidden = true; replaceLabelEl.style.display = "none"; } if (addMoreLabelEl) { addMoreLabelEl.hidden = true; addMoreLabelEl.style.display = "none"; } }; if (mode === "uploading") { titleEl.textContent = "Uploading your files..."; subtitleEl.textContent = "Please wait while the upload completes."; chooseTextEl.textContent = "Choose Different PDFs"; hideAllChoose(); return; } if (mode === "merging") { titleEl.textContent = "Merging files..."; subtitleEl.textContent = "Upload complete. Merge is running."; chooseTextEl.textContent = "Choose Different PDFs"; hideAllChoose(); return; } if (mode === "completed") { titleEl.textContent = "Merge complete"; subtitleEl.textContent = "Your merged PDF is ready."; chooseTextEl.textContent = "Choose PDF Files"; showReplaceOnly(); if (fileListEl) { fileListEl.textContent = ""; } if (previewGridEl) { previewGridEl.hidden = true; } return; } if (mode === "selected") { titleEl.textContent = "Files selected"; subtitleEl.textContent = "Click Merge PDFs to start."; chooseTextEl.textContent = "Choose PDF Files"; showAddMoreOnly(); return; } titleEl.textContent = "Upload PDF files"; subtitleEl.textContent = "Supported format: .pdf (max 25MB each, total 100MB)"; chooseTextEl.textContent = "Choose PDF Files"; showInitialChoose(); if (previewGridEl && selectedFiles.length) { previewGridEl.hidden = false; } } function clearCardPreviewUrls() { cardPreviewUrls.forEach((url) => URL.revokeObjectURL(url)); cardPreviewUrls = []; } function resetProgress() { if (progressWrap) progressWrap.style.display = "none"; if (progressText) progressText.style.display = "none"; if (progressBar) progressBar.style.width = "0%"; if (progressText) progressText.textContent = "0%"; if (progressWrap) progressWrap.classList.remove("processing"); } function startProgress() { if (progressWrap) progressWrap.style.display = "block"; if (progressText) progressText.style.display = "block"; if (progressBar) progressBar.style.width = "0%"; if (progressText) progressText.textContent = "0%"; if (progressWrap) progressWrap.classList.remove("processing"); } function startProcessingIndicator() { if (progressWrap) progressWrap.classList.add("processing"); if (progressBar) progressBar.style.width = "100%"; if (progressText) progressText.textContent = "Merging..."; } function updateFileList() { if (!fileListEl) return; clearCardPreviewUrls(); if (!selectedFiles.length) { fileListEl.textContent = "No files selected"; if (previewGridEl) { previewGridEl.innerHTML = ""; previewGridEl.hidden = true; } return; } if (isMerged) { fileListEl.textContent = ""; if (previewGridEl) { previewGridEl.innerHTML = ""; previewGridEl.hidden = true; } return; } fileListEl.textContent = `${selectedFiles.length} files selected`; if (previewGridEl) { previewGridEl.innerHTML = ""; previewGridEl.hidden = false; selectedFiles.forEach((file, index) => { const sizeMb = (file.size / (1024 * 1024)).toFixed(2); const fileUrl = URL.createObjectURL(file); cardPreviewUrls.push(fileUrl); const card = document.createElement("article"); card.className = "merge-pdf-preview-card"; card.draggable = true; card.setAttribute("data-index", String(index)); const frame = document.createElement("iframe"); frame.className = "merge-pdf-preview-frame"; frame.title = `Preview ${file.name}`; frame.src = `${fileUrl}#page=1&zoom=page-fit`; frame.loading = "lazy"; const removeBtn = document.createElement("button"); removeBtn.type = "button"; removeBtn.className = "merge-pdf-remove-icon"; removeBtn.innerHTML = "×"; removeBtn.title = "Remove file"; removeBtn.setAttribute("aria-label", "Remove file"); removeBtn.setAttribute("data-action", "remove"); removeBtn.setAttribute("data-index", String(index)); const name = document.createElement("div"); name.className = "merge-pdf-preview-name"; name.textContent = `${index + 1}. ${file.name}`; const meta = document.createElement("div"); meta.className = "merge-pdf-preview-meta"; meta.textContent = `${sizeMb} MB`; card.appendChild(removeBtn); card.appendChild(frame); card.appendChild(name); card.appendChild(meta); previewGridEl.appendChild(card); }); } } function moveSelectedFile(fromIndex, toIndex) { if ( fromIndex === toIndex || fromIndex < 0 || toIndex < 0 || fromIndex >= selectedFiles.length || toIndex >= selectedFiles.length ) { return; } const next = [...selectedFiles]; const [moved] = next.splice(fromIndex, 1); next.splice(toIndex, 0, moved); selectedFiles = next; clearMergedOutput(); setStatus(selectedFiles.length < 2 ? "Select at least 2 PDF files." : "Ready to merge.", selectedFiles.length < 2); setUploadBoxCopy("selected"); updateUi(); } function updateUi() { const canMerge = selectedFiles.length >= 2 && !isMerged; mergeBtn.disabled = !canMerge; if (cancelBtn) { const showCancel = !!activeXhr; cancelBtn.hidden = !showCancel; cancelBtn.disabled = !showCancel; } if (downloadBtn) { downloadBtn.disabled = !mergedBlob; downloadBtn.hidden = !mergedBlob; } if (clearBtn) clearBtn.disabled = !selectedFiles.length; if (reorderHintEl) { reorderHintEl.hidden = !(selectedFiles.length > 1 && !isMerged); } updateFileList(); } function clearMergedOutput() { mergedBlob = null; isMerged = false; if (mergedObjectUrl) { URL.revokeObjectURL(mergedObjectUrl); mergedObjectUrl = null; } } function setActiveRequest(xhr) { activeXhr = xhr; updateUi(); } function clearActiveRequest() { activeXhr = null; updateUi(); } function validateFiles(files) { if (files.length > maxFiles) { return `You can select up to ${maxFiles} files.`; } let totalSize = 0; for (const file of files) { totalSize += file.size; if (!file.name.toLowerCase().endsWith(".pdf")) { return `${file.name}: only .pdf files are supported.`; } if (file.size > maxFileSize) { return `${file.name}: file must be under 25MB.`; } } if (totalSize > maxTotalSize) { return "Total size must be under 100MB."; } return ""; } function applySelectedFiles(newFiles, mode) { const validationMessage = validateFiles(newFiles); if (validationMessage) { setStatus(validationMessage, true); return false; } selectedFiles = newFiles; clearMergedOutput(); if (selectedFiles.length < 2) { setStatus("Select at least 2 PDF files.", true); } else { setStatus("Ready to merge.", false); } setUploadBoxCopy(mode || "selected"); resetProgress(); updateUi(); return true; } function mergeFiles() { if (selectedFiles.length < 2 || isMerged) return; const formData = new FormData(); selectedFiles.forEach((file) => formData.append("files", file)); clearMergedOutput(); startProgress(); setStatus("Merging PDF files...", false); setUploadBoxCopy("uploading"); mergeBtn.disabled = true; if (downloadBtn) downloadBtn.disabled = true; const xhr = new XMLHttpRequest(); setActiveRequest(xhr); xhr.open("POST", "/api/merge-pdf/", true); xhr.responseType = "blob"; if (csrfTokenEl?.value) { xhr.setRequestHeader("X-CSRFToken", csrfTokenEl.value); } xhr.upload.onprogress = (event) => { if (!event.lengthComputable || !progressBar || !progressText) return; const percent = Math.min(100, Math.round((event.loaded / event.total) * 100)); progressBar.style.width = `${percent}%`; progressText.textContent = `${percent}% uploaded`; }; xhr.upload.onload = () => { startProcessingIndicator(); setStatus("Upload complete. Merging...", false); setUploadBoxCopy("merging"); }; xhr.onload = async () => { clearActiveRequest(); if (xhr.status >= 400) { let errorMessage = "Merge failed."; try { const text = await xhr.response.text(); const data = JSON.parse(text || "{}"); if (data.error) errorMessage = data.error; if (data.detail) errorMessage = `${errorMessage} (${data.detail})`; } catch { // keep default } setStatus(errorMessage, true); setUploadBoxCopy("selected"); clearMergedOutput(); resetProgress(); updateUi(); return; } mergedBlob = xhr.response; mergedObjectUrl = URL.createObjectURL(mergedBlob); isMerged = true; setStatus("Merge completed. Click Download Merged PDF.", false); setUploadBoxCopy("completed"); resetProgress(); updateUi(); }; xhr.onerror = () => { clearActiveRequest(); setStatus("Merge failed.", true); setUploadBoxCopy("selected"); clearMergedOutput(); resetProgress(); updateUi(); }; xhr.onabort = () => { clearActiveRequest(); if (suppressAbortNotice) { suppressAbortNotice = false; return; } setStatus("Merge cancelled.", true); setUploadBoxCopy("selected"); resetProgress(); updateUi(); }; xhr.send(formData); } function downloadMergedPdf() { if (!mergedBlob || !mergedObjectUrl) return; const link = document.createElement("a"); link.href = mergedObjectUrl; link.download = "merged.pdf"; link.click(); } filesInput.addEventListener("change", () => { const files = filesInput.files ? Array.from(filesInput.files) : []; filesInput.value = ""; if (!files.length) return; applySelectedFiles(files, "selected"); }); if (filesAddInput) { filesAddInput.addEventListener("change", () => { const files = filesAddInput.files ? Array.from(filesAddInput.files) : []; filesAddInput.value = ""; if (!files.length) return; applySelectedFiles([...selectedFiles, ...files], "selected"); }); } if (filesReplaceInput) { filesReplaceInput.addEventListener("change", () => { const files = filesReplaceInput.files ? Array.from(filesReplaceInput.files) : []; filesReplaceInput.value = ""; if (!files.length) return; applySelectedFiles(files, "selected"); }); } mergeBtn.addEventListener("click", mergeFiles); if (cancelBtn) { cancelBtn.addEventListener("click", () => { if (activeXhr) { activeXhr.abort(); } }); } if (downloadBtn) { downloadBtn.addEventListener("click", downloadMergedPdf); } if (clearBtn) { clearBtn.addEventListener("click", () => { selectedFiles = []; clearMergedOutput(); clearCardPreviewUrls(); if (activeXhr) { suppressAbortNotice = true; activeXhr.abort(); } filesInput.value = ""; if (filesAddInput) filesAddInput.value = ""; if (filesReplaceInput) filesReplaceInput.value = ""; setStatus("Choose 2 or more PDF files to merge.", false); setUploadBoxCopy("idle"); resetProgress(); updateUi(); }); } if (previewGridEl) { previewGridEl.addEventListener("dragstart", (event) => { const target = event.target; if (!(target instanceof HTMLElement)) return; const card = target.closest(".merge-pdf-preview-card"); if (!(card instanceof HTMLElement)) return; dragFromIndex = Number(card.getAttribute("data-index")); card.classList.add("is-dragging"); if (event.dataTransfer) { event.dataTransfer.effectAllowed = "move"; event.dataTransfer.setData("text/plain", String(dragFromIndex)); } }); previewGridEl.addEventListener("dragover", (event) => { const target = event.target; if (!(target instanceof HTMLElement)) return; const card = target.closest(".merge-pdf-preview-card"); if (!(card instanceof HTMLElement)) return; event.preventDefault(); if (event.dataTransfer) event.dataTransfer.dropEffect = "move"; }); previewGridEl.addEventListener("dragenter", (event) => { const target = event.target; if (!(target instanceof HTMLElement)) return; const card = target.closest(".merge-pdf-preview-card"); if (!(card instanceof HTMLElement)) return; card.classList.add("is-drop-target"); }); previewGridEl.addEventListener("dragleave", (event) => { const target = event.target; if (!(target instanceof HTMLElement)) return; const card = target.closest(".merge-pdf-preview-card"); if (!(card instanceof HTMLElement)) return; card.classList.remove("is-drop-target"); }); previewGridEl.addEventListener("drop", (event) => { const target = event.target; if (!(target instanceof HTMLElement)) return; const card = target.closest(".merge-pdf-preview-card"); if (!(card instanceof HTMLElement)) return; event.preventDefault(); card.classList.remove("is-drop-target"); const dragToIndex = Number(card.getAttribute("data-index")); moveSelectedFile(dragFromIndex, dragToIndex); dragFromIndex = -1; }); previewGridEl.addEventListener("dragend", () => { dragFromIndex = -1; previewGridEl.querySelectorAll(".merge-pdf-preview-card").forEach((card) => { card.classList.remove("is-dragging", "is-drop-target"); }); }); previewGridEl.addEventListener("click", (event) => { const target = event.target; if (!(target instanceof HTMLElement)) return; const action = target.getAttribute("data-action"); const index = Number(target.getAttribute("data-index")); if (!Number.isInteger(index) || index < 0 || index >= selectedFiles.length) return; if (action === "remove") { selectedFiles = selectedFiles.filter((_, i) => i !== index); clearMergedOutput(); if (!selectedFiles.length) { setStatus("Choose 2 or more PDF files to merge.", false); setUploadBoxCopy("idle"); } else { setStatus( selectedFiles.length < 2 ? "Select at least 2 PDF files." : "Ready to merge.", selectedFiles.length < 2 ); setUploadBoxCopy("selected"); } resetProgress(); updateUi(); } }); } setUploadBoxCopy("idle"); resetProgress(); updateUi(); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initMergePdf); } else { initMergePdf(); }