1document.addEventListener("DOMContentLoaded", function () { 2 function setFooterButtonVisibility(container) { 3 if (!container) return; 4 const footerBtn = container.querySelector(".reply-button-footer"); 5 const replyForm = container.querySelector(".reply-form"); 6 if (!footerBtn || !replyForm) return; 7 const isFormVisible = 8 replyForm.style.display && replyForm.style.display !== "none"; 9 footerBtn.style.display = isFormVisible ? "none" : ""; 10 } 11 12 function ensureReplyFooter(container) { 13 if (!container) return; 14 const blockId = (container.id || "").replace(/^thread-/, ""); 15 const footer = container.querySelector(".thread-reply-footer"); 16 let replyForm = null; 17 if (blockId) { 18 replyForm = 19 Array.from(container.querySelectorAll(".reply-form")).find(function ( 20 form 21 ) { 22 return form.dataset && form.dataset.blockId === blockId; 23 }) || null; 24 } else { 25 replyForm = container.querySelector(".reply-form"); 26 } 27 if (footer) container.appendChild(footer); 28 if (replyForm) container.appendChild(replyForm); 29 setFooterButtonVisibility(container); 30 } 31 32 function ensureAllReplyFooters() { 33 document.querySelectorAll(".thread-container").forEach(ensureReplyFooter); 34 } 35 36 ensureAllReplyFooters(); 37 38 // Handle thread controls 39 document.addEventListener("click", function (ev) { 40 const viewBtn = ev.target.closest(".view-conversation"); 41 const replyBtn = ev.target.closest(".reply-button"); 42 const cancelBtn = ev.target.closest(".cancel-button"); 43 44 if (viewBtn) { 45 const targetId = viewBtn.dataset.target; 46 const thread = document.getElementById(targetId); 47 if (thread) { 48 const isExpanded = thread.classList.toggle("expanded"); 49 console.log("View-conversation clicked. Thread ID:", targetId, "Expanded:", isExpanded); 50 if (isExpanded) { 51 ensureReplyFooter(thread); 52 } else { 53 // When collapsing, hide all reply forms in this thread 54 const replyForms = thread.querySelectorAll(".reply-form"); 55 replyForms.forEach((form) => { 56 form.style.display = "none"; 57 const textarea = form.querySelector("textarea"); 58 if (textarea) textarea.value = ""; 59 }); 60 // Remove inline style to let CSS class handle display 61 thread.style.removeProperty("display"); 62 setFooterButtonVisibility(thread); 63 } 64 // Toggle chevron direction on the button icon 65 const icon = viewBtn.querySelector("i"); 66 if (icon) { 67 if (isExpanded) { 68 icon.classList.remove("fa-chevron-down"); 69 icon.classList.add("fa-chevron-up"); 70 } else { 71 icon.classList.remove("fa-chevron-up"); 72 icon.classList.add("fa-chevron-down"); 73 } 74 } 75 // Toggle thread-controls bottom radius when expanded/collapsed 76 const controls = viewBtn.closest(".thread-controls"); 77 if (controls) { 78 controls.classList.toggle("open", isExpanded); 79 } 80 // Accessibility hint 81 viewBtn.setAttribute("aria-expanded", isExpanded ? "true" : "false"); 82 } else { 83 console.error("Thread element not found with ID:", targetId); 84 } 85 } 86 87 if (replyBtn) { 88 // Hide all other reply forms first 89 document.querySelectorAll(".reply-form").forEach((form) => { 90 if (form.id !== replyBtn.dataset.target) { 91 form.style.display = "none"; 92 const container = form.closest(".thread-container"); 93 setFooterButtonVisibility(container); 94 } 95 }); 96 97 // Toggle the clicked reply form 98 const targetId = replyBtn.dataset.target; 99 const replyForm = document.getElementById(targetId); 100 if (replyForm) { 101 const container = replyForm.closest(".thread-container"); 102 103 // Ensure the thread container is expanded/visible 104 if (container) { 105 container.classList.add("expanded"); 106 container.style.display = "block"; 107 108 // Sync the corresponding view-conversation button + chevron + controls state 109 const blockId = (container.id || "").replace(/^thread-/, ""); 110 if (blockId) { 111 const controls = document.querySelector( 112 '.thread-controls[data-block-id="' + blockId + '"]' 113 );
114 if (controls) { 115 controls.classList.add("open"); 116 const vcBtn = controls.querySelector(".view-conversation"); 117 if (vcBtn) { 118 vcBtn.setAttribute("aria-expanded", "true"); 119 const icon = vcBtn.querySelector("i"); 120 if (icon) { 121 icon.classList.remove("fa-chevron-down"); 122 icon.classList.add("fa-chevron-up"); 123 } 124 } 125 } 126 } 127 } 128 129 // Show the reply form and move focus into it 130 replyForm.style.display = "block"; 131 ensureReplyFooter(container); 132 setFooterButtonVisibility(container); 133 134 // Focus the textarea and scroll into view for better UX 135 const textarea = replyForm.querySelector("textarea"); 136 if (textarea) { 137 textarea.focus({ preventScroll: true }); 138 replyForm.scrollIntoView({ behavior: "smooth", block: "center" }); 139 } 140 } 141 } 142 143 if (cancelBtn) { 144 const replyForm = cancelBtn.closest(".reply-form"); 145 if (replyForm) { 146 replyForm.style.display = "none"; 147 const textarea = replyForm.querySelector("textarea"); 148 if (textarea) textarea.value = ""; 149 setFooterButtonVisibility(replyForm.closest(".thread-container")); 150 } 151 } 152 }); 153 154 document.addEventListener("lalith:thread-message-added", function (event) { 155 if (!event || !event.detail) { 156 ensureAllReplyFooters(); 157 return; 158 } 159 const blockId = event.detail.blockId; 160 if (!blockId) return ensureAllReplyFooters(); 161 const container = document.getElementById("thread-" + blockId); 162 ensureReplyFooter(container); 163 setFooterButtonVisibility(container); 164 }); 165 166 document.addEventListener("lalith:thread-message-inserted", function (event) { 167 if (!event || !event.detail) return; 168 const container = document.getElementById("thread-" + event.detail.blockId); 169 ensureReplyFooter(container); 170 setFooterButtonVisibility(container); 171 }); 172 173 const select = document.getElementById("requirement-select"); 174 const mapping = { 175 one: "text-ece", 176 two: "text-iee", 177 spantran: "text-spantran", 178 wes: "text-wes", 179 other: "text-other", 180 }; 181 182 const hideAll = () => 183 Object.values(mapping).forEach((id) => { 184 document.getElementById(id).classList.add("hidden"); 185 }); 186 187 if (select) { 188 select.addEventListener("change", () => { 189 hideAll(); 190 const selected = mapping[select.value]; 191 if (selected) 192 document.getElementById(selected).classList.remove("hidden"); 193 }); 194 } 195 196 // File upload preview with remove functionality (per-upload-container) 197 // Supports multiple upload boxes on the same page. Each container keeps its own selected files 198 function escapeHtml(s) { 199 return String(s) 200 .replace(/&/g, "&") 201 .replace(/</g, "<") 202 .replace(/>/g, ">") 203 .replace(/"/g, """) 204 .replace(/'/g, "'"); 205 } 206 207 // No-op global for older callers; per-container logic updates real inputs directly 208 function updateFileInput() { 209 /* intentionally empty - per-container update handled separately */ 210 } 211 212 // Wire only plugin-specific file inputs (not all file inputs on the page) 213 // Only target file inputs inside plugin-specific containers to avoid conflicts with other plugins like Forminator 214 const pluginContainers = document.querySelectorAll( 215 ".lalith-upload-container, .upload-box, .lalith-upload-box, #evalForm" 216 ); 217 const allFileInputs = []; 218 pluginContainers.forEach((container) => { 219 const inputs = container.querySelectorAll('input[type="file"]'); 220 inputs.forEach((input) => allFileInputs.push(input)); 221 }); 222 223 allFileInputs.forEach((fileInput) => { 224 // determine logical container for this input; include lalith-specific classes 225 const container = 226 fileInput.closest(".lalith-upload-container") || 227 fileInput.closest(".upload-box") || 228 fileInput.closest(".lalith-upload-box") || 229 fileInput.closest("#evalForm") || 230 fileInput.parentElement; 231 if (!container) return; 232 // mark wiring on the input itself so other inputs are not blocked if container falls back to body 233 if (fileInput.dataset.uploadWired) return; 234 fileInput.dataset.uploadWired = "1"; 235 236 // Skip file preview creation for lalith-upload-box since it has its own preview system (reupload-stage3.js)
237 const isLalithUploadBox = container.classList.contains("lalith-upload-box"); 238 if (isLalithUploadBox) { 239 // lalith-upload-box is managed by reupload-stage3.js, skip this handler 240 return; 241 } 242 243 // Ensure there's a file-names container to show selected filenames 244 // Accept multiple possible class/id patterns produced by PHP templates 245 let namesWrap = 246 container.querySelector(".file-names-container") || 247 container.querySelector(".lalith-file-names") || 248 container.querySelector("#lalith-upload-file-names-list"); 249 // Detect existing file list either by class or common ids used in templates 250 const existingFileList = 251 container.querySelector(".file-list") || 252 container.querySelector("#file-list") || 253 container.querySelector(".lalith-file-names"); 254 if (!namesWrap) { 255 namesWrap = document.createElement("div"); 256 namesWrap.className = "file-names-container"; 257 if (existingFileList && existingFileList.parentNode) 258 existingFileList.parentNode.insertBefore( 259 namesWrap, 260 existingFileList.nextSibling 261 ); 262 else container.appendChild(namesWrap); 263 } 264 265 // Wire choose-files-link that targets this input if present 266 // prefer label[for=input.id] or .choose-files-link inside container 267 // Prefer label inside the same container that targets this input, fallback to global label[for=] 268 const chooseLabel = 269 container.querySelector(".choose-files-link") || 270 (fileInput.id 271 ? container.querySelector('label[for="' + fileInput.id + '"]') 272 : null) || 273 (fileInput.id 274 ? document.querySelector('label[for="' + fileInput.id + '"]') 275 : null); 276 if (chooseLabel) { 277 chooseLabel.addEventListener("click", function (e) { 278 e.preventDefault(); 279 fileInput.click(); 280 }); 281 } 282 283 let selectedFilesLocal = []; 284 fileInput.addEventListener("change", function () { 285 const newFiles = Array.from(this.files || []); 286 newFiles.forEach((file) => { 287 const isDuplicate = selectedFilesLocal.some( 288 (f) => f.name === file.name && f.size === file.size 289 ); 290 if (!isDuplicate) selectedFilesLocal.push(file); 291 }); 292 renderNames(); 293 }); 294 295 function renderNames() { 296 namesWrap.innerHTML = ""; 297 selectedFilesLocal.forEach((file, idx) => { 298 const row = document.createElement("div"); 299 row.className = "file-name-row"; 300 row.style.display = "flex"; 301 row.style.justifyContent = "space-between"; 302 row.style.alignItems = "center"; 303 row.style.padding = "8px 10px"; 304 row.style.marginTop = "8px"; 305 row.style.background = "#f1f3f6"; 306 row.style.borderRadius = "8px"; 307 308 const left = document.createElement("div"); 309 left.style.display = "flex"; 310 left.style.alignItems = "center"; 311 left.style.gap = "10px"; 312 left.innerHTML = 313 '<i class="fa-solid fa-file file-icon" style="color:#95a5a6;font-size:18px;"></i>'; 314 315 const details = document.createElement("div"); 316 details.style.minWidth = "0"; 317 details.innerHTML = 318 '<div class="name" style="font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:360px;">' + 319 escapeHtml(file.name) + 320 "</div>" + 321 '<div class="meta" style="font-size:12px;color:#6b7280;">' + 322 (file.size / 1024).toFixed(1) + 323 " KB Ready</div>"; 324 325 left.appendChild(details); 326 327 const removeBtn = document.createElement("button"); 328 removeBtn.type = "button"; 329 removeBtn.className = "file-remove-btn"; 330 removeBtn.setAttribute("data-index", idx); 331 removeBtn.setAttribute("aria-label", "Remove file"); 332 removeBtn.style.background = "none"; 333 removeBtn.style.border = "none"; 334 removeBtn.style.color = "red"; 335 removeBtn.style.cursor = "pointer"; 336 removeBtn.style.fontSize = "18px"; 337 removeBtn.innerHTML = '<i class="fa-solid fa-xmark"></i>'; 338 339 row.appendChild(left); 340 row.appendChild(removeBtn); 341 namesWrap.appendChild(row); 342 }); 343 344 // update actual file input FileList to match selectedFilesLocal 345 const dt = new DataTransfer(); 346 selectedFilesLocal.forEach((f) => dt.items.add(f)); 347 fileInput.files = dt.files;
348 // show or hide the namesWrap depending on whether we have files 349 if (selectedFilesLocal.length > 0) { 350 namesWrap.style.display = ""; 351 } else { 352 namesWrap.style.display = "none"; 353 } 354 } 355 356 // remove handling 357 namesWrap.addEventListener("click", function (e) { 358 const btn = e.target.closest(".file-remove-btn"); 359 if (!btn) return; 360 const idx = parseInt(btn.getAttribute("data-index")); 361 if (!isNaN(idx)) { 362 selectedFilesLocal.splice(idx, 1); 363 renderNames(); 364 } 365 }); 366 367 // drag/drop on the upload-box inside this container if present 368 const uploadBox = container.querySelector(".upload-box") || container; 369 if (uploadBox) { 370 uploadBox.addEventListener("dragover", function (e) { 371 e.preventDefault(); 372 this.style.backgroundColor = "#e6f6ff"; 373 }); 374 uploadBox.addEventListener("dragleave", function (e) { 375 e.preventDefault(); 376 this.style.backgroundColor = ""; 377 }); 378 uploadBox.addEventListener("drop", function (e) { 379 e.preventDefault(); 380 this.style.backgroundColor = ""; 381 const dropped = Array.from(e.dataTransfer.files || []); 382 dropped.forEach((file) => { 383 const isDuplicate = selectedFilesLocal.some( 384 (f) => f.name === file.name && f.size === file.size 385 ); 386 if (!isDuplicate) selectedFilesLocal.push(file); 387 }); 388 renderNames(); 389 }); 390 } 391 }); 392 393 // Stage-specific wiring handled by the generic per-input logic above 394 395 hideAll(); // Initialize 396 397 // Backup: Direct handler for view-conversation buttons to ensure toggle works even if event delegation fails 398 // This is a safety net to ensure threads always collapse/expand properly 399 document.addEventListener("DOMContentLoaded", function() { 400 const allViewBtns = document.querySelectorAll(".view-conversation"); 401 console.log("Setting up backup toggle handlers for", allViewBtns.length, "view-conversation buttons"); 402 403 allViewBtns.forEach(function(btn) { 404 // Only add if not already wired 405 if (btn.dataset.toggleWired) return; 406 btn.dataset.toggleWired = "1"; 407 408 btn.addEventListener("click", function(e) { 409 console.log("Backup toggle handler fired for button:", btn); 410 e.preventDefault(); 411 e.stopPropagation(); 412 413 const targetId = btn.dataset.target; 414 const thread = document.getElementById(targetId); 415 416 if (thread) { 417 const isExpanded = thread.classList.toggle("expanded"); 418 console.log("Backup toggle: Thread ID", targetId, "is now", isExpanded ? "expanded" : "collapsed"); 419 420 if (isExpanded) { 421 ensureReplyFooter(thread); 422 } else { 423 // When collapsing, hide all reply forms in this thread 424 const replyForms = thread.querySelectorAll(".reply-form"); 425 replyForms.forEach((form) => { 426 form.style.display = "none"; 427 const textarea = form.querySelector("textarea"); 428 if (textarea) textarea.value = ""; 429 }); 430 // Remove inline style to let CSS class handle display 431 thread.style.removeProperty("display"); 432 setFooterButtonVisibility(thread); 433 } 434 435 // Update chevron icon 436 const icon = btn.querySelector("i"); 437 if (icon) { 438 if (isExpanded) { 439 icon.classList.remove("fa-chevron-down"); 440 icon.classList.add("fa-chevron-up"); 441 } else { 442 icon.classList.remove("fa-chevron-up"); 443 icon.classList.add("fa-chevron-down"); 444 } 445 } 446 447 // Update thread-controls state 448 const controls = btn.closest(".thread-controls"); 449 if (controls) { 450 controls.classList.toggle("open", isExpanded); 451 } 452 453 // Update aria-expanded 454 btn.setAttribute("aria-expanded", isExpanded ? "true" : "false"); 455 } else { 456 console.error("Backup toggle: Thread element not found with ID:", targetId); 457 } 458 }); 459 }); 460 }); 461 462 // Form validation for required fields 463 function setupValidation() { 464 const requiredFields = document.querySelectorAll( 465 "input[required], select[required]" 466 ); 467 468 requiredFields.forEach((field) => { 469 field.addEventListener("blur", function () { 470 validateField(this); 471 }); 472 473 field.addEventListener("input", function () { 474 // Clear error when user starts typing 475 if (this.value.trim() !== "") { 476 clearError(this); 477 } 478 }); 479 }); 480 } 481 482 function validateField(field) { 483 const formGroup = field.closest(".form-group"); 484 const errorMessage = formGroup 485 ? formGroup.querySelector(".error-message") 486 : null; 487 488 if (!field.value.trim()) { 489 showError(field, errorMessage); 490 return false;
491 } else if (field.type === "email" && !isValidEmail(field.value)) { 492 showError(field, errorMessage); 493 return false; 494 } else { 495 clearError(field); 496 return true; 497 } 498 } 499 500 function showError(field, errorMessage) { 501 field.classList.add("error"); 502 if (errorMessage) { 503 errorMessage.classList.add("show"); 504 } 505 } 506 507 function clearError(field) { 508 field.classList.remove("error"); 509 const formGroup = field.closest(".form-group"); 510 const errorMessage = formGroup 511 ? formGroup.querySelector(".error-message") 512 : null; 513 if (errorMessage) { 514 errorMessage.classList.remove("show"); 515 } 516 } 517 518 function isValidEmail(email) { 519 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 520 return emailRegex.test(email); 521 } 522 523 // No global file-input change handler; per-container handlers manage state and display. 524 525 // Initialize validation 526 setupValidation(); 527 528 // Form submission validation and AJAX 529 const evalForm = document.getElementById("evalForm"); 530 if (evalForm) 531 evalForm.addEventListener("submit", function (e) { 532 e.preventDefault(); // Prevent default submission 533 534 let isFormValid = true; 535 const requiredFields = this.querySelectorAll( 536 "input[required], select[required]" 537 ); 538 let firstErrorField = null; 539 540 // Validate all required fields 541 requiredFields.forEach((field) => { 542 // Skip hidden fields (conditional requirement fields) 543 const parentDiv = field.closest('[id^="text-"]'); 544 if (parentDiv && parentDiv.classList.contains("hidden")) { 545 clearError(field); // Ensure error is cleared for hidden fields 546 return; // Skip validation for hidden conditional fields 547 } 548 // Validate and show error for empty/invalid fields 549 if (!validateField(field)) { 550 isFormValid = false; 551 if (!firstErrorField) firstErrorField = field; 552 } else { 553 clearError(field); 554 } 555 }); 556 557 // Validate conditional fields (ECE, IEE, SpanTran, WES, Other) 558 const conditionalIds = [ 559 "text-ece", 560 "text-iee", 561 "text-spantran", 562 "text-wes", 563 "text-other", 564 ]; 565 conditionalIds.forEach((id) => { 566 const div = document.getElementById(id); 567 if (div && !div.classList.contains("hidden")) { 568 const input = div.querySelector('input[type="text"]'); 569 if (input && !input.value.trim()) { 570 showError(input, div.querySelector(".error-message")); 571 isFormValid = false; 572 if (!firstErrorField) firstErrorField = input; 573 } else if (input) { 574 clearError(input); 575 } 576 } 577 }); 578 579 // Special validation for file upload: ensure at least one file input inside this form has files 580 const fileInputs = this.querySelectorAll('input[type="file"]'); 581 let anyFileSelected = false; 582 fileInputs.forEach((fi) => { 583 if (fi.files && fi.files.length > 0) anyFileSelected = true; 584 }); 585 if (!anyFileSelected) { 586 // mark first file input (if present) with error 587 const firstFile = fileInputs[0]; 588 if (firstFile) { 589 const formGroup = firstFile.closest(".form-group"); 590 const errorMessage = formGroup 591 ? formGroup.querySelector(".error-message") 592 : null; 593 showError(firstFile, errorMessage); 594 if (!firstErrorField) firstErrorField = firstFile; 595 } 596 isFormValid = false; 597 } else { 598 // clear errors on all file inputs 599 fileInputs.forEach((fi) => clearError(fi)); 600 } 601 602 // Check if terms checkbox is checked 603 const termsCheckbox = document.getElementById("terms"); 604 if (!termsCheckbox.checked) { 605 // Add error style to label for visual feedback 606 const label = termsCheckbox.nextElementSibling; 607 if (label) label.style.color = "#dc3545"; 608 isFormValid = false; 609 if (!firstErrorField) firstErrorField = termsCheckbox; 610 } else { 611 const label = termsCheckbox.nextElementSibling; 612 if (label) label.style.color = ""; 613 } 614 615 if (!isFormValid) { 616 // No alert, just show error messages (already handled by showError) 617 // Scroll to first error field 618 if (firstErrorField) { 619 firstErrorField.scrollIntoView({ 620 behavior: "smooth", 621 block: "center", 622 }); 623 firstErrorField.focus(); 624 } 625 return; 626 } 627 628 // Native submission (no AJAX): update real file input from selectedFiles and submit the form 629 const form = this; 630 // Ensure the real file input has the current selected files 631 updateFileInput(); 632 // Disable submit button and show submitting indicator 633 if (submitBtn) submitBtn.disabled = true; 634 if (submittingMsg) submittingMsg.classList.remove("hidden"); 635 // Submit the form normally so server-side handler (admin-post.php) stores data and redirects to tracking URL 636 form.submit(); 637 }); 638 639 // Add submitting indicator 640 const submitBtn = document.querySelector('button[type="submit"]'); 641 let submittingMsg = null; 642 if (submitBtn && submitBtn.parentNode) { 643 submittingMsg = document.createElement("div");
644 submittingMsg.id = "submitting-indicator"; 645 submittingMsg.style.marginTop = "10px"; 646 submittingMsg.style.fontWeight = "bold"; 647 submittingMsg.style.color = "#007bff"; 648 submittingMsg.innerHTML = 649 'Submitting<span class="dot">.</span><span class="dot">.</span><span class="dot">.</span>'; 650 submittingMsg.classList.add("hidden"); 651 submitBtn.parentNode.insertBefore(submittingMsg, submitBtn.nextSibling); 652 } 653 654 // Add CSS for blinking dots 655 const style = document.createElement("style"); 656 style.innerHTML = ` 657 #submitting-indicator .dot { 658 opacity: 0.2; 659 animation: blink 1.2s infinite; 660 } 661 #submitting-indicator .dot:nth-child(2) { animation-delay: 0.2s; } 662 #submitting-indicator .dot:nth-child(3) { animation-delay: 0.4s; } 663 @keyframes blink { 664 0%, 80%, 100% { opacity: 0.2; } 665 40% { opacity: 1; } 666 } 667 `; 668 document.head.appendChild(style); 669 670 // Initialize reply form submissions using event delegation with capture phase 671 document.addEventListener("submit", function (e) { 672 const form = e.target.closest(".reply-form"); 673 if (!form) return; 674 675 // Don't handle timeline-reply-form (those are handled separately) 676 if (form.classList.contains("timeline-reply-form")) return; 677 678 // Check if the inline handler from new.php is already handling this 679 // BUT always handle view_stage=4 forms since inline handler isn't loaded for them 680 if (window.__lalithReplyFormHandlerAttached && !form.dataset.stage) { 681 console.log("Inline handler is active, skipping form-script.js handler"); 682 return; 683 } 684 685 console.log("Reply form submit intercepted:", form.id); 686 e.preventDefault(); 687 e.stopPropagation(); 688 e.stopImmediatePropagation(); // Prevent other handlers from running 689 690 // Additional prevention: return false to stop form submission 691 if (e.cancelable !== false) { 692 e.preventDefault(); 693 } 694 695 // Find the send button (try multiple selectors) 696 let sendButton = form.querySelector(".send-button"); 697 if (!sendButton) { 698 sendButton = form.querySelector('button[type="submit"]'); 699 } 700 if (!sendButton) { 701 sendButton = form.querySelector('button'); 702 } 703 704 if (sendButton) { 705 console.log("Changing send button state to sending...", sendButton); 706 sendButton.dataset.originalText = 707 sendButton.dataset.originalText || sendButton.textContent; 708 sendButton.textContent = "sending.."; 709 sendButton.disabled = true; 710 } else { 711 console.warn("No send button found in form:", form); 712 } 713 714 const formData = new FormData(form); 715 716 // Get the AJAX URL from form attribute 717 const ajaxUrl = form.dataset.ajaxUrl; 718 if (!ajaxUrl) { 719 console.error("No AJAX URL found in form"); 720 // Reset button state if there's an error 721 if (sendButton) { 722 sendButton.disabled = false; 723 sendButton.textContent = sendButton.dataset.originalText || "Send"; 724 } 725 return false; // Explicitly return false 726 } 727 728 fetch(ajaxUrl, { 729 method: "POST", 730 body: formData, 731 headers: { 732 "X-Requested-With": "XMLHttpRequest", 733 }, 734 }) 735 .then((response) => response.json()) 736 .then((data) => { 737 if (data.success) { 738 const blockIdFromServer = data.block_id || formData.get("block_id"); 739 const thread = document.getElementById( 740 "thread-" + blockIdFromServer 741 ); 742 const replyForm = form; 743 744 // 1. Remove no-replies placeholder FIRST 745 if (thread) { 746 const empty = thread.querySelector(".no-replies"); 747 if (empty) empty.remove(); 748 } 749 750 // 2. Append user-side reply HTML to thread IMMEDIATELY for fast rendering 751 if (thread && data.html) { 752 const wrap = document.createElement("div"); 753 wrap.innerHTML = data.html; 754 const footer = thread.querySelector(".thread-reply-footer"); 755 while (wrap.firstChild) { 756 const node = wrap.firstChild; 757 if (footer) { 758 thread.insertBefore(node, footer); 759 } else { 760 thread.appendChild(node); 761 } 762 } 763 } 764 765 // 3. Do remaining operations after rendering (hide form, show button) 766 // Hide and reset the reply form 767 replyForm.style.display = "none"; 768 const textarea = replyForm.querySelector("textarea"); 769 if (textarea) textarea.value = ""; 770 771 // Show the reply footer button again 772 const container = replyForm.closest(".thread-container"); 773 if (container) { 774 setFooterButtonVisibility(container); 775 } 776 777 // 4. Handle admin-side timeline reply HTML (wrap in timeline-thread-box if needed) 778 const adminThreadBox = document.querySelector( 779 '[data-timeline-entry="' + blockIdFromServer + '"]' 780 ); 781 if (adminThreadBox && data.timeline_html) { 782 // Check if timeline-thread-list exists in this timeline-thread-box 783 let threadList = adminThreadBox.querySelector( 784 ".timeline-thread-list" 785 ); 786 787 // If no thread list exists yet, create the full timeline-thread-box structure 788 if (!threadList) { 789 // Create the entire timeline-thread-box structure 790 const threadBoxContent = document.createElement("div"); 791 threadBoxContent.style.marginTop = "10px"; 792 threadBoxContent.innerHTML = ` 793 <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;"> 794 <button type="button" class="timeline-thread-toggle" data-target="tl-thread-${blockIdFromServer}" style="background:none;border:none;color:#2563eb;font-weight:600;cursor:pointer;">View conversation <i class="fa-solid fa-chevron-down"></i></button> 795 </div> 796 <div id="tl-thread-${blockIdFromServer}" class="timeline-thread-list" style="display:block;margin-bottom:10px;" data-thread-container="${blockIdFromServer}"> 797 </div> 798 <button type="button" class="timeline-reply-btn" style="background:#3b82f6;color:white;border:none;border-radius:4px;padding:6px 12px;font-size:13px;
798cursor:pointer;display:flex;align-items:center;gap:4px;margin-top:10px;" data-block-id="${blockIdFromServer}" data-timeline-entry="${blockIdFromServer}"> 799 <i class="fa-solid fa-reply" style="font-size:12px;"></i> Reply 800 </button> 801 `; 802 803 // Get the thread list from the newly created structure 804 threadList = threadBoxContent.querySelector( 805 ".timeline-thread-list" 806 ); 807 808 // Append the new structure to the timeline entry 809 adminThreadBox.appendChild(threadBoxContent); 810 } 811 812 // Now append the timeline reply to the thread list 813 if (threadList && data.timeline_html) { 814 const wrap = document.createElement("div"); 815 wrap.innerHTML = data.timeline_html; 816 while (wrap.firstChild) threadList.appendChild(wrap.firstChild); 817 } 818 } 819 } else { 820 console.error("Reply failed:", data); 821 alert(data.data?.message || "Failed to send reply"); 822 } 823 }) 824 .catch((error) => { 825 console.error("Error:", error); 826 alert("An error occurred while sending the reply"); 827 }) 828 .finally(() => { 829 // Find the send button again to reset state 830 const resetButton = form.querySelector(".send-button") || form.querySelector('button[type="submit"]') || form.querySelector('button'); 831 if (resetButton) { 832 resetButton.disabled = false; 833 resetButton.textContent = resetButton.dataset.originalText || "Send"; 834 } 835 }); 836 }, true); // Use capture phase to ensure this runs before other handlers 837 838 // Additional fallback: Handle dynamically loaded reply forms 839 // This ensures reply forms loaded after initial page load (like in view_stage=4) work properly 840 const observer = new MutationObserver(function(mutations) { 841 mutations.forEach(function(mutation) { 842 if (mutation.addedNodes) { 843 mutation.addedNodes.forEach(function(node) { 844 if (node.nodeType === Node.ELEMENT_NODE) { 845 // Check if this node or its children contain reply forms 846 const replyForms = node.classList && node.classList.contains('reply-form') 847 ? [node] 848 : (node.querySelectorAll ? node.querySelectorAll('.reply-form') : []); 849 850 replyForms.forEach(function(form) { 851 console.log('Dynamically found reply form:', form.id); 852 853 // Remove the form action to prevent default submission 854 if (form.getAttribute('action')) { 855 console.log('Removing form action to prevent page reload'); 856 form.removeAttribute('action'); 857 } 858 859 // Add inline submit handler as backup 860 form.setAttribute('onsubmit', 'return false;'); 861 862 // Ensure the form has proper event handling by logging its presence 863 if (!form.dataset.ajaxUrl) { 864 console.warn('Reply form missing ajaxUrl:', form); 865 } 866 }); 867 } 868 }); 869 } 870 }); 871 }); 872 873 // Also handle existing reply forms on the page 874 document.addEventListener('DOMContentLoaded', function() { 875 const existingForms = document.querySelectorAll('.reply-form'); 876 existingForms.forEach(function(form) { 877 console.log('Found existing reply form:', form.id); 878 879 // Remove the form action to prevent default submission 880 if (form.getAttribute('action')) { 881 console.log('Removing form action to prevent page reload'); 882 form.removeAttribute('action'); 883 } 884 885 // Add inline submit handler as backup 886 form.setAttribute('onsubmit', 'return false;'); 887 }); 888 }); 889 890 // Start observing the document body for changes 891 observer.observe(document.body, { 892 childList: true, 893 subtree: true 894 }); 895 896 // Backup: Add click handlers to send buttons as additional protection 897 document.addEventListener("click", function(e) { 898 const sendButton = e.target.closest(".send-button"); 899 if (!sendButton) return; 900 901 const form = sendButton.closest(".reply-form"); 902 if (!form) return; 903 904 // Don't handle timeline-reply-form (those are handled separately) 905 if (form.classList.contains("timeline-reply-form")) return; 906 907 // Check if the inline handler from new.php is already handling this 908 if (window.__lalithReplyFormHandlerAttached && !form.dataset.stage) { 909 console.log("Inline handler is active, skipping click handler"); 910 return; 911 } 912 913 console.log("Send button clicked, preventing default form submission"); 914 e.preventDefault(); 915 e.stopPropagation(); 916 917 // Trigger the submit event manually to use our existing handler 918 const submitEvent = new Event("submit", { 919 bubbles: true, 920 cancelable: true 921 }); 922 form.dispatchEvent(submitEvent); 923 }); 924}); 925; 926document.addEventListener("DOMContentLoaded", function () { 927 // Initialize uploader for a given set of element IDs 928 function initUploader( 929 inputId, 930 targetId, 931 uploadBtnId, 932 trackingIdFieldId, 933 nonceFieldId 934 ) { 935 // Prefer scoping by element references if multiple upload boxes exist on the page. 936 const input = document.getElementById(inputId); 937 const target = document.getElementById(targetId); 938 const uploadBtn = document.getElementById(uploadBtnId); 939 // If one of the expected elements is missing, attempt to find any elements with matching class inside the same parent container. 940 if (!input) return; 941 // Allow target to be optional; if missing, create one next to input 942 let effectiveTarget = target; 943 if (!effectiveTarget) { 944 // create a preview container after input 945 effectiveTarget = document.createElement("div"); 946 effectiveTarget.id = targetId; 947 effectiveTarget.className = "lalith-file-names"; 948 effectiveTarget.style.marginTop = "8px"; 949 input.parentNode && 950 input.parentNode.insertBefore(effectiveTarget, input.nextSibling); 951 } 952 953 function escapeHtml(s) { 954 return String(s) 955 .replace(/&/g, "&") 956 .replace(/</g, "<") 957 .replace(/>/g, ">") 958 .replace(/"/g, """) 959 .replace(/'/g, "'"); 960 } 961 962 // keep a local list per uploader instance so new selections append instead of replace 963 const selected = []; 964 965 // Stage3 upload should always remain active; previous logic that disabled the UI after upload 966 // (using localStorage flag) has been removed to keep the upload controls clickable. 967 968 function syncInputFiles() { 969 const dt = new DataTransfer(); 970 selected.forEach((f) => dt.items.add(f)); 971 input.files = dt.files; 972 } 973 974 function render() { 975 // use the effectiveTarget (may have been created) when rendering 976 effectiveTarget.innerHTML = ""; 977 if (selected.length === 0) { 978 effectiveTarget.style.display = "none"; 979 return; 980 } 981 effectiveTarget.style.display = "block"; 982 selected.forEach((f, idx) => {
983 const row = document.createElement("div"); 984 row.className = "reupload-file-row"; 985 row.style.display = "flex"; 986 row.style.justifyContent = "space-between"; 987 row.style.alignItems = "center"; 988 row.style.padding = "6px 8px"; 989 row.style.marginTop = "6px"; 990 row.style.background = "#fff"; 991 row.style.border = "1px solid #e5eef8"; 992 row.style.borderRadius = "6px"; 993 994 const left = document.createElement("div"); 995 left.style.display = "flex"; 996 left.style.alignItems = "center"; 997 left.style.gap = "8px"; 998 999 const icon = document.createElement("i"); 1000 icon.className = "fa-solid fa-file"; 1001 icon.style.color = "#95a5a6"; 1002 icon.style.fontSize = "16px"; 1003 1004 const nameWrap = document.createElement("div"); 1005 nameWrap.style.color = "#374151"; 1006 nameWrap.style.fontSize = "13px"; 1007 nameWrap.style.overflow = "hidden"; 1008 nameWrap.style.textOverflow = "ellipsis"; 1009 nameWrap.style.whiteSpace = "nowrap"; 1010 // textContent is safe and prevents HTML injection; no manual escaping needed 1011 nameWrap.textContent = f.name; 1012 1013 left.appendChild(icon); 1014 left.appendChild(nameWrap); 1015 1016 const right = document.createElement("div"); 1017 const remove = document.createElement("button"); 1018 remove.type = "button"; 1019 remove.style.background = "none"; 1020 remove.style.border = "none"; 1021 remove.style.cursor = "pointer"; 1022 remove.style.fontSize = "16px"; 1023 remove.setAttribute("aria-label", "Remove file"); 1024 remove.innerHTML = 1025 '<i class="fa-solid fa-xmark" style="color:#dc3545;"></i>'; 1026 remove.addEventListener("click", function () { 1027 selected.splice(idx, 1); 1028 syncInputFiles(); 1029 render(); 1030 }); 1031 right.appendChild(remove); 1032 1033 row.appendChild(left); 1034 row.appendChild(right); 1035 target.appendChild(row); 1036 }); 1037 } 1038 1039 input.addEventListener("change", function (e) { 1040 const files = Array.from(e.target.files || []); 1041 files.forEach((f) => { 1042 const dup = selected.some( 1043 (s) => s.name === f.name && s.size === f.size 1044 ); 1045 if (!dup) selected.push(f); 1046 }); 1047 syncInputFiles(); 1048 render(); 1049 }); 1050 1051 // initial render in case external code pre-populated files 1052 if (input.files && input.files.length) { 1053 Array.from(input.files).forEach((f) => selected.push(f)); 1054 render(); 1055 } 1056 1057 // Upload selected files to server when user clicks the Upload Files button 1058 const uploadBtnRef = uploadBtn || document.getElementById(uploadBtnId); 1059 if (uploadBtnRef) { 1060 uploadBtnRef.addEventListener("click", function () { 1061 if (selected.length === 0) return; 1062 const trackingId = document.getElementById(trackingIdFieldId) 1063 ? document.getElementById(trackingIdFieldId).value 1064 : ""; 1065 const nonce = document.getElementById(nonceFieldId) 1066 ? document.getElementById(nonceFieldId).value 1067 : ""; 1068 const adminPost = document.getElementById("lalith_admin_post_url") 1069 ? document.getElementById("lalith_admin_post_url").value 1070 : window.location.origin + "/wp-admin/admin-post.php"; 1071 const fd = new FormData(); 1072 fd.append("action", "lalith_reupload"); 1073 fd.append("tracking_id", trackingId); 1074 if (nonce) fd.append("lalith_reupload_nonce", nonce); 1075 if (inputId.indexOf("stage4") !== -1) fd.append("university", "1"); 1076 selected.forEach((f) => fd.append("reupload_files[]", f, f.name)); 1077 uploadBtnRef.disabled = true; 1078 uploadBtnRef.textContent = "Uploading..."; 1079 fetch(adminPost, { 1080 method: "POST", 1081 body: fd, 1082 credentials: "same-origin", 1083 }) 1084 .then((res) => { 1085 if (!res.ok) throw new Error("Upload failed"); 1086 return res.text(); 1087 }) 1088 .then((body) => { 1089 // success: mark rows in this uploader only 1090 const rows = effectiveTarget.querySelectorAll(".reupload-file-row"); 1091 rows.forEach((r) => { 1092 if (!r.querySelector(".reuploaded-badge")) { 1093 const badge = document.createElement("span"); 1094 badge.className = "reuploaded-badge"; 1095 badge.style.marginLeft = "8px"; 1096 badge.style.color = "#2563eb"; 1097 badge.style.fontSize = "12px"; 1098 badge.textContent = " (reuploaded)"; 1099 r.querySelector("div").appendChild(badge); 1100 } 1101 }); 1102 // clear selected and sync input 1103 selected.length = 0; 1104 syncInputFiles(); 1105 // Previously we set a localStorage flag here to disable Stage3 UI after upload. 1106 // That behavior has been removed so we don't set any flag. 1107 // After success, briefly show badges then reload the page so server-side rendering shows the final state 1108 try { 1109 const flowStep = uploadBtnRef 1110 ? uploadBtnRef.closest(".flow-step") 1111 : null; 1112 if (flowStep) { 1113 const now = new Date();
1114 const timeStr = now.toLocaleString(); 1115 flowStep.innerHTML = 1116 "" + 1117 '<div class="step-icon" style="width:48px;height:48px;border-radius:50%;background:#f3f4f6;display:flex;align-items:center;justify-content:center;flex-shrink:0;">' + 1118 '<i class="fa-solid fa-cloud-arrow-up" style="font-size:16px;color:#0369a1;"></i>' + 1119 "</div>" + 1120 '<div class="step-body" style="flex:1;">' + 1121 '<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:12px;">' + 1122 '<h3 style="margin:0;font-size:16px;color:#374151;line-height:1.1;">Additional documents required</h3>' + 1123 '<small style="color:#6b7280;">' + 1124 timeStr + 1125 "</small>" + 1126 "</div>" + 1127 '<p style="margin:8px 0 0;color:#6b7280;font-size:14px;">You have already uploaded files.</p>' + 1128 "</div>"; 1129 } 1130 } catch (e) { 1131 // if DOM replacement fails, we still reload below 1132 } 1133 // Reload after a short delay so server-side UI is shown and keeps things in sync 1134 setTimeout(() => { 1135 window.location.reload(); 1136 }, 400); 1137 }) 1138 .catch((err) => { 1139 console.error(err); 1140 alert("Upload failed. Please try again."); 1141 uploadBtnRef.disabled = false; 1142 uploadBtnRef.textContent = "Upload Files"; 1143 }); 1144 }); 1145 } 1146 } 1147 1148 // initialize stage3 uploader (existing ids) 1149 initUploader( 1150 "lalith_documents_stage3", 1151 "lalith_file_names_stage3", 1152 "lalith_upload_stage3", 1153 "lalith_stage3_tracking_id", 1154 "lalith_stage3_reupload_nonce" 1155 ); 1156 // initialize stage4 uploader (new ids) 1157 initUploader( 1158 "lalith_documents_stage4", 1159 "lalith_file_names_stage4", 1160 "lalith_upload_stage4", 1161 "lalith_stage4_tracking_id", 1162 "lalith_stage4_reupload_nonce" 1163 ); 1164}); 1165;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.