document.addEventListener("DOMContentLoaded", () => { /* ========================= ELEMENT REFERENCES ========================= */ const tabs = document.querySelectorAll(".tool-tabs .tab"); const panels = document.querySelectorAll(".tool-panel"); const textInput = document.getElementById("textInput"); const wordsEl = document.getElementById("words"); const charsEl = document.getElementById("chars"); const charsNoSpaceEl = document.getElementById("charsNoSpace"); const linesEl = document.getElementById("lines"); const paragraphsEl = document.getElementById("paragraphs"); const readingTimeEl = document.getElementById("readingTime"); const clearBtn = document.getElementById("clearTextBtn"); const copyBtn = document.getElementById("copyTextBtn"); const printBtn = document.getElementById("printBtn"); const exportTxtBtn = document.getElementById("exportTxtBtn"); const exportDocxBtn = document.getElementById("exportDocxBtn"); const exportPdfBtn = document.getElementById("exportPdfBtn"); const fileInput = document.getElementById("fileInput"); const dropOverlay = document.getElementById("dropOverlay"); const uploadBox = document.querySelector(".upload-box"); const uploadProgress = document.querySelector(".upload-progress"); const uploadProgressBar = document.querySelector(".upload-progress-bar"); const uploadProgressText = document.querySelector(".upload-progress-text"); const csrfTokenInput = document.getElementById("csrfToken"); const csrftoken = csrfTokenInput ? csrfTokenInput.value : null; if (!textInput) return; /* ========================= TAB SWITCHING ========================= */ function activateTab(index) { tabs.forEach(t => t.classList.remove("active")); panels.forEach(p => p.classList.remove("active")); tabs[index].classList.add("active"); panels[index].classList.add("active"); } tabs.forEach((tab, index) => { tab.addEventListener("click", () => activateTab(index)); }); /* ========================= PREVENT BROWSER FILE OPEN ========================= */ ["dragenter", "dragover", "dragleave", "drop"].forEach(eventName => { window.addEventListener(eventName, e => { e.preventDefault(); e.stopPropagation(); }); }); /* ========================= DRAG OVERLAY LOGIC ========================= */ let dragCounter = 0; function showDropOverlay() { dragCounter++; dropOverlay.classList.add("active"); } function hideDropOverlay() { dragCounter = 0; dropOverlay.classList.remove("active"); } window.addEventListener("dragenter", showDropOverlay); window.addEventListener("dragleave", () => { dragCounter--; if (dragCounter <= 0) hideDropOverlay(); }); window.addEventListener("drop", hideDropOverlay); /* ========================= WORD COUNT (API + DEBOUNCE) ========================= */ let debounceTimer = null; const DEBOUNCE_DELAY = 300; textInput.addEventListener("input", () => { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { sendTextForCount(textInput.value); }, DEBOUNCE_DELAY); }); function sendTextForCount(text) { const formData = new FormData(); formData.append("text", text); fetch("/api/word-counter/", { method: "POST", headers: csrftoken ? { "X-CSRFToken": csrftoken } : {}, body: formData }) .then(res => res.json()) .then(data => { wordsEl.innerText = data.words ?? 0; charsEl.innerText = data.characters ?? 0; charsNoSpaceEl.innerText = data.characters_no_space ?? 0; linesEl.innerText = data.lines ?? 0; paragraphsEl.innerText = data.paragraphs ?? 0; updateReadingTime(data.words ?? 0); }) .catch(err => console.error("Word counter error:", err)); } /* ========================= READING TIME ========================= */ const WORDS_PER_MINUTE = 200; function updateReadingTime(words) { readingTimeEl.textContent = words === 0 ? "~ 0 min read" : `~ ${Math.ceil(words / WORDS_PER_MINUTE)} min read`; } /* ========================= CLEAR / COPY ========================= */ clearBtn?.addEventListener("click", () => { clearTimeout(debounceTimer); textInput.value = ""; wordsEl.innerText = 0; charsEl.innerText = 0; charsNoSpaceEl.innerText = 0; linesEl.innerText = 0; paragraphsEl.innerText = 0; updateReadingTime(0); }); copyBtn?.addEventListener("click", async () => { if (!textInput.value.trim()) return; await navigator.clipboard.writeText(textInput.value); const original = copyBtn.textContent; copyBtn.textContent = "Copied ✓"; setTimeout(() => copyBtn.textContent = original, 1500); }); /* ========================= PRINT / EXPORT ========================= */ printBtn?.addEventListener("click", () => { if (!textInput.value.trim()) return; const escaped = textInput.value .replace(/&/g, "&") .replace(//g, ">") .replace(/\n/g, "
"); const w = window.open("", "_blank", "width=800,height=600"); w.document.write(`${escaped}`); w.document.close(); w.onload = () => { w.print(); w.close(); }; }); exportTxtBtn?.addEventListener("click", () => { if (!textInput.value.trim()) return; const blob = new Blob([textInput.value], { type: "text/plain;charset=utf-8" }); const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = "word-counter.txt"; link.click(); URL.revokeObjectURL(link.href); }); function downloadFromApi(url, filename) { if (!textInput.value.trim()) return; const formData = new FormData(); formData.append("text", textInput.value); fetch(url, { method: "POST", headers: csrftoken ? { "X-CSRFToken": csrftoken } : {}, body: formData }) .then(res => { if (!res.ok) throw new Error("Export failed"); return res.blob(); }) .then(blob => { const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = filename; link.click(); URL.revokeObjectURL(link.href); }) .catch(() => alert("Export failed")); } exportDocxBtn?.addEventListener("click", () => { downloadFromApi("/api/export-docx/", "word-counter.docx"); }); exportPdfBtn?.addEventListener("click", () => { downloadFromApi("/api/export-pdf/", "word-counter.pdf"); }); /* ========================= FILE UPLOAD (BROWSE + DROP) ========================= */ function uploadTextFile(file) { const name = file.name.toLowerCase(); if (!(name.endsWith(".txt") || name.endsWith(".docx") || name.endsWith(".pdf"))) { alert("Only .txt, .docx, or .pdf files are supported"); hideDropOverlay(); return; } if (file.size > 10_000_000) { alert("File must be under 10MB"); hideDropOverlay(); return; } const formData = new FormData(); formData.append("file", file); if (uploadProgress && uploadProgressBar && uploadProgressText) { uploadProgress.style.display = "block"; uploadProgressText.style.display = "block"; uploadProgressBar.style.width = "0%"; uploadProgressText.textContent = "0%"; uploadProgress.classList.remove("processing"); } uploadBox?.classList.remove("processing"); const xhr = new XMLHttpRequest(); xhr.open("POST", "/api/upload-text/", true); if (csrftoken) { xhr.setRequestHeader("X-CSRFToken", csrftoken); } xhr.upload.onprogress = (event) => { if (!event.lengthComputable || !uploadProgressBar || !uploadProgressText) return; const percent = Math.min(100, Math.round((event.loaded / event.total) * 100)); uploadProgressBar.style.width = `${percent}%`; uploadProgressText.textContent = `${percent}%`; }; xhr.upload.onload = () => { if (!uploadProgress || !uploadProgressText) return; uploadProgress.classList.add("processing"); uploadProgressText.textContent = "Processing..."; uploadBox?.classList.add("processing"); }; xhr.onload = () => { let data = null; try { data = JSON.parse(xhr.responseText || "{}"); } catch { data = { error: "File upload failed" }; } if (xhr.status >= 400 || data.error) { alert(data.error || "File upload failed"); hideDropOverlay(); if (uploadProgress && uploadProgressText) { uploadProgress.style.display = "none"; uploadProgressText.style.display = "none"; uploadProgress.classList.remove("processing"); } uploadBox?.classList.remove("processing"); return; } textInput.value = data.text || ""; sendTextForCount(data.text || ""); activateTab(0); hideDropOverlay(); if (uploadProgress && uploadProgressText) { uploadProgress.style.display = "none"; uploadProgressText.style.display = "none"; uploadProgress.classList.remove("processing"); } uploadBox?.classList.remove("processing"); }; xhr.onerror = () => { alert("File upload failed"); hideDropOverlay(); if (uploadProgress && uploadProgressText) { uploadProgress.style.display = "none"; uploadProgressText.style.display = "none"; uploadProgress.classList.remove("processing"); } uploadBox?.classList.remove("processing"); }; xhr.send(formData); } fileInput?.addEventListener("change", () => { const file = fileInput.files[0]; if (file) uploadTextFile(file); fileInput.value = ""; }); dropOverlay?.addEventListener("drop", e => { const file = e.dataTransfer.files?.[0]; if (file) uploadTextFile(file); }); });