PageSourceSearch

https://100transcripts.com/_static/??wp-content/plugins/Lalith-Bac…/js/reupload-stage3.js?m=1764054435

js 100transcripts.com collected 2026-10-08 23:04:03 UTC 44,031 bytes, 1,165 lines download raw bytes

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, "&lt;")
202      .replace(/>/g, "&gt;")
203      .replace(/"/g, "&quot;")
204      .replace(/'/g, "&#39;");
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, "&amp;")
956        .replace(/</g, "&lt;")
957        .replace(/>/g, "&gt;")
958        .replace(/"/g, "&quot;")
959        .replace(/'/g, "&#39;");
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.