1$(document).ready(function () { 2 grecaptcha.ready(function () { 3 grecaptcha 4 .execute("6LexOQssAAAAAAI80tgLm8ht96LIh7LKrlglSDJD", { 5 action: "submit_form", 6 }) 7 .then(function (token) { 8 document.getElementById("recaptchaToken").value = token; 9 }); 10 }); 11 12 $('[name="contactPhone"]') 13 .off("keypress paste") 14 .on("keypress", function (e) { 15 16 const char = String.fromCharCode(e.which); 17 18 // Digits ONLY 19 if (!/\d/.test(char)) { 20 e.preventDefault(); 21 return; 22 } 23 24 // Maximum 15 digits 25 const digits = this.value.replace(/\D/g, ""); 26 27 if (digits.length >= 15) { 28 e.preventDefault(); 29 return; 30 } 31 32 // Repeated numbers are allowed 33 // Example: 222222222222222 â 34 }) 35 36 .on("paste", function (e) { 37 38 e.preventDefault(); 39 40 const input = this; 41 42 const pasted = (e.originalEvent || e) 43 .clipboardData 44 .getData("text"); 45 46 // Keep digits ONLY 47 const pastedDigits = pasted.replace(/\D/g, ""); 48 49 const currentDigits = input.value.replace(/\D/g, ""); 50 51 // Maximum 15 digits 52 const remaining = 15 - currentDigits.length; 53 54 const allowed = pastedDigits.slice( 55 0, 56 Math.max(0, remaining) 57 ); 58 59 const start = input.selectionStart; 60 const end = input.selectionEnd; 61 62 input.setRangeText( 63 allowed, 64 start, 65 end, 66 "end" 67 ); 68 69 $(input).trigger("input"); 70 }); 71 72 // âââââââââââââââââââââââââââââââââââââââââââââ 73 // GIBBERISH DETECTION ENGINE 74 // âââââââââââââââââââââââââââââââââââââââââââââ 75 76 /** 77 * Returns a score 0â1 indicating how "English-like" a string is. 78 * Based on trigram frequency analysis of common English trigrams. 79 * Score close to 1 = real text, close to 0 = gibberish. 80 */ 81 var COMMON_ENGLISH_TRIGRAMS = new Set([ 82 "the", 83 "and", 84 "ing", 85 "ion", 86 "tio", 87 "ent", 88 "ati", 89 "for", 90 "her", 91 "ter", 92 "hat", 93 "thi", 94 "nth", 95 "int", 96 "ere", 97 "tho", 98 "ese", 99 "ste", 100 "est", 101 "his", 102 "was", 103 "you", 104 "tha", 105 "not", 106 "are", 107 "wit", 108 "all", 109 "can", 110 "our", 111 "one", 112 "had", 113 "ver", 114 "but", 115 "whi", 116 "has", 117 "hin", 118 "ith", 119 "have",
120 "eve", 121 "out", 122 "fro", 123 "pro", 124 "com", 125 "con", 126 "ect", 127 "men", 128 "per", 129 "ted", 130 "ive", 131 "res", 132 "rea", 133 "str", 134 "tra", 135 "ans", 136 "ons", 137 "use", 138 "ple", 139 "ble", 140 "sta", 141 "tin", 142 "ple", 143 "igh", 144 "age", 145 "rea", 146 "ual", 147 "tic", 148 "nes", 149 "ite", 150 "oth", 151 "ome", 152 "hou", 153 "nts", 154 "led", 155 "lin", 156 "ess", 157 "ame", 158 "oun", 159 "end", 160 "nce", 161 "ove", 162 "ser", 163 "ill", 164 "tur", 165 "als", 166 "eri", 167 "ore", 168 "les", 169 "ali", 170 "eld", 171 "ell", 172 "ven", 173 "ren", 174 "ati", 175 "ali", 176 "hor", 177 "ear", 178 "bec", 179 "wor", 180 "bet", 181 "pon", 182 ]); 183 184 function trigramScore(text) { 185 var letters = text.toLowerCase().replace(/[^a-z]/g, ""); 186 if (letters.length < 3) return 1; // too short to judge, pass through 187 var total = 0; 188 var hits = 0; 189 for (var i = 0; i <= letters.length - 3; i++) { 190 var tri = letters.substring(i, i + 3); 191 total++; 192 if (COMMON_ENGLISH_TRIGRAMS.has(tri)) hits++; 193 } 194 return total === 0 ? 1 : hits / total; 195 } 196 197 /** 198 * Detects excessive consonant clusters (5+ consonants in a row). 199 * Real English rarely has more than 3 consonants together. 200 */ 201 function hasExcessiveConsonantClusters(text) { 202 var letters = text.toLowerCase().replace(/[^a-z]/g, ""); 203 return /[bcdfghjklmnpqrstvwxyz]{5,}/.test(letters); 204 } 205 206 /** 207 * Detects common keyboard-walk patterns like qwerty, asdf, zxcv, etc. 208 */ 209 var KEYBOARD_PATTERNS = [ 210 /qwert/i, 211 /werty/i, 212 /ertyu/i, 213 /rtyui/i, 214 /tyuio/i, 215 /yuiop/i, 216 /asdfg/i, 217 /sdfgh/i, 218 /dfghj/i, 219 /fghjk/i, 220 /ghjkl/i, 221 /zxcvb/i, 222 /xcvbn/i, 223 /cvbnm/i, 224 /qazwsx/i, 225 /wsxedc/i, 226 /edcrfv/i, 227 /12345/i, 228 /23456/i, 229 /34567/i, 230 /45678/i, 231 /56789/i, 232 /abcde/i, 233 /bcdef/i, 234 /cdefg/i, 235 ]; 236 237 function hasKeyboardPattern(text) { 238 return KEYBOARD_PATTERNS.some(function (pattern) { 239 return pattern.test(text); 240 }); 241 } 242 243 /** 244 * Detects repeated word-level gibberish like "aaa bbb ccc" or "haha haha haha". 245 */ 246 function hasRepeatedWords(text) { 247 var words = text.trim().toLowerCase().split(/\s+/); 248 if (words.length < 3) return false; 249 var counts = {}; 250 words.forEach(function (w) { 251 counts[w] = (counts[w] || 0) + 1; 252 }); 253 var maxCount = Math.max.apply(null, Object.values(counts)); 254 // If any single word makes up >60% of the message, it's suspicious 255 return maxCount / words.length > 0.6; 256 } 257 258 /** 259 * Checks for strings of random-looking characters with no vowels at all. 260 */ 261 function hasNoVowels(word) { 262 return word.length >= 4 && !/[aeiou]/i.test(word); 263 } 264 265 /** 266 * Master gibberish check â combines all signals. 267 * Returns { isGibberish: bool, reason: string } 268 */ 269 function detectGibberish(text) { 270 var trimmed = text.trim(); 271 var words = trimmed.split(/\s+/); 272 273 // 1. Keyboard pattern check 274 if (hasKeyboardPattern(trimmed)) { 275 return { isGibberish: true, reason: "keyboard pattern" }; 276 } 277 278 // 2. Excessive consonant clusters 279 if (hasExcessiveConsonantClusters(trimmed)) { 280 return { isGibberish: true, reason: "consonant cluster" }; 281 } 282 283 // 3. Repeated word pattern 284 if (hasRepeatedWords(trimmed)) { 285 return { isGibberish: true, reason: "repeated words" }; 286 } 287 288 // 4. Check each word individually for no-vowel gibberish 289 var noVowelCount = 0; 290 words.forEach(function (word) { 291 var clean = word.replace(/[^a-zA-Z]/g, ""); 292 if (hasNoVowels(clean)) noVowelCount++; 293 }); 294 if (words.length >= 2 && noVowelCount / words.length > 0.5) { 295 return { isGibberish: true, reason: "no vowels in words" }; 296 } 297 298 // 5. Trigram frequency scoring (most powerful signal) 299 var score = trigramScore(trimmed); 300 // Threshold: below 8% trigram hit rate â likely gibberish 301 // Real English messages typically score 15â40% 302 if (score < 0.08 && trimmed.replace(/[^a-zA-Z]/g, "").length >= 8) { 303 return { 304 isGibberish: true, 305 reason: "low trigram score (" + (score * 100).toFixed(1) + "%)", 306 }; 307 } 308 309 return { isGibberish: false, reason: "" }; 310 } 311 312 // âââââââââââââââââââââââââââââââââââââââââââââ 313 // UTILITY FUNCTION 314 // âââââââââââââââââââââââââââââââââââââââââââââ 315 316 function handleFormSubmit(form, token) { 317 const $submitButton = $(form).find('button[type="submit"]'); 318 const formData = new FormData(form); 319 var resourcesDownload1 = $("#resourcesDownload").val(); 320 321 if (token) formData.append("recaptchaResponse", token); 322 formData.append("resourcesDownload1", resourcesDownload1); 323 324 $.ajax({
325 url: "contact-form-submit-backend.php", 326 method: "POST", 327 data: formData, 328 processData: false, 329 contentType: false, 330 beforeSend: function () { 331 // $(".loader").fadeIn(); 332 $submitButton.prop("disabled", true).text("Submitting..."); 333 }, 334 success: function (response) { 335 $(".loader").fadeOut(); 336 try { 337 response = JSON.parse(response); 338 if (response.status === "success") { 339 if ($("#web-form").hasClass("download-resources")) { 340 var resourcesDownload = $("#resourcesDownload").val(); 341 var createElement = document.createElement("a"); 342 createElement.href = resourcesDownload; 343 var filename = resourcesDownload.substring( 344 resourcesDownload.lastIndexOf("/") + 1, 345 ); 346 filename = filename.split("?")[0]; 347 createElement.download = filename; 348 // createElement.click(); 349 } 350 $(form).trigger("reset"); 351 Swal.fire({ 352 title: "Thank you for contacting us", 353 html: "We will get back to you soon.<br><b>Closing in <span id='countdown'>5</span> seconds...</b>", 354 icon: "success", 355 timer: 5000, 356 timerProgressBar: true, 357 allowOutsideClick: false, 358 didOpen: () => { 359 const countdownEl = 360 Swal.getHtmlContainer().querySelector("#countdown"); 361 let timeLeft = 5; 362 const timerInterval = setInterval(() => { 363 timeLeft--; 364 if (countdownEl) countdownEl.textContent = timeLeft; 365 if (timeLeft <= 0) clearInterval(timerInterval); 366 }, 1000); 367 }, 368 }); 369 setTimeout(() => (window.location.href = "thank-you.php"), 5000); 370 hideFormLoader(); 371 } else { 372 alert("Something went wrong. Please try again."); 373 $submitButton.prop("disabled", false).text("Submit"); 374 hideFormLoader(); 375 } 376 } catch (error) { 377 console.error("Invalid JSON response:", response); 378 alert("Unexpected error. Please try again."); 379 $submitButton.prop("disabled", false).text("Submit"); 380 hideFormLoader(); 381 } 382 }, 383 error: function (err) { 384 $(".loader").hide(); 385 alert("Error submitting the form."); 386 console.error(err); 387 $submitButton.prop("disabled", false).text("Submit"); 388 hideFormLoader(); 389 }, 390 }); 391 } 392 393 // âââââââââââââââââââââââââââââââââââââââââââââ 394 // VALIDATORS 395 // âââââââââââââââââââââââââââââââââââââââââââââ 396 397 $.validator.addMethod( 398 "noLeadingSpace", 399 function (value) { 400 return !/^\s/.test(value); 401 }, 402 "Don't enter space.", 403 ); 404 405 406 $.validator.addMethod( 407 "validPhoneNumber", 408 function (value) { 409 const cleaned = value.replace(/\D/g, ""); 410 if (cleaned.length < 10 || cleaned.length > 15) return false; 411 412 const repeatedDigitPattern = /^(\d)\1{4,}$/; 413 const repeatedPattern = /(\d{2,})\1+/; 414 const sequentialPattern = /(\d{2,4})(?=\d)\1+/; 415 416 // const fakeNumbers = new Set([ 417 // "0000000000", 418 // "1111111111", 419 // "1234567890", 420 // "1234567891", 421 // "9999999999", 422 // "0123456789", 423 // "99999", 424 // "+11234567890", 425 // "+10000000000", 426 // "+19999999999", 427 // ]); 428 429 // if (fakeNumbers.has(cleaned)) return false; 430 // if ( 431 // repeatedDigitPattern.test(cleaned) || 432 // repeatedPattern.test(cleaned) || 433 // sequentialPattern.test(cleaned) 434 // ) 435 // return false; 436 437 const reg = /^\+?[\d\s\-()]{10,20}$/; 438 return reg.test(value); 439 }, 440 "Please enter a valid phone number.", 441 ); 442 443 $.validator.addMethod( 444 "validEmail", 445 function (value) { 446 const reg = /^[a-zA-Z0-9._%+-]{2,}@[a-zA-Z0-9.-]{2,}\.[a-zA-Z]{2,}$/; 447 const blockedEmails = new Set([ 448 "[email protected]", 449 "[email protected]", 450 "[email protected]", 451 "[email protected]", 452 "[email protected]", 453 "[email protected]", 454 "[email protected]", 455 "[email protected]", 456 "[email protected]", 457 "[email protected]", 458 "[email protected]", 459 "[email protected]", 460 "[email protected]", 461 "[email protected]", 462 "[email protected]", 463 "[email protected]", 464 "[email protected]", 465 "[email protected]", 466 "[email protected]", 467 "[email protected]", 468 "[email protected]", 469 "[email protected]", 470 "[email protected]", 471 "[email protected]", 472 "[email protected]", 473 "[email protected]", 474 "[email protected]", 475 ]); 476 const blockedDomains = new Set([ 477 "@test.com", 478 "@example.com", 479 "@demo.com", 480 "@abc.com", 481 "@email.com", 482 "@theguestpostshop.com", 483 "@sharklasers.com", 484 "@guerrillamail.com", 485 "@guerrillamailblock.com", 486 "@guerrillamail.net", 487 "@guerrillamail.de", 488 "@spam4.me", 489 "@maildrop.cc", 490 "@10minutemail.com", 491 "@dispostable.com", 492 "@mailinator.com", 493 "@technodreamoutsourcing.com", 494 "@guestpostmate.com", 495 "@50pros.com", 496 "@web.de", 497 "@sbcglobal.net", 498 ]); 499 500 const emailLower = value.toLowerCase(); 501 const parts = emailLower.split('@'); 502 if (parts.length !== 2) return false; 503 const local = parts[0]; 504 const dotCount = (local.match(/\./g) || []).length; 505 if (dotCount > 2) return false; 506 const domain = "@" + emailLower.split("@")[1]; 507 return ( 508 reg.test(value) && 509 !blockedEmails.has(emailLower) && 510 !blockedDomains.has(domain) 511 ); 512 }, 513 "Please enter a valid email address.", 514 ); 515 516 $.validator.addMethod( 517 "meaningfulMessage", 518 function (value, element) { 519 const message = $.trim(value); 520 521 // Empty 522 if (!message) { 523 return false; 524 } 525 526 // Minimum 2 words
527 const words = message.split(/\s+/).filter(Boolean); 528 529 if (words.length < 2) { 530 return false; 531 } 532 533 // Must contain at least 2 letters 534 const letters = message.match(/[a-zA-Z]/g) || []; 535 536 if (letters.length < 2) { 537 return false; 538 } 539 540 // Reject obvious repeated characters 541 if (/(.)\1{4,}/i.test(message)) { 542 return false; 543 } 544 545 // Reject obvious keyboard spam 546 const lowerMessage = message.toLowerCase(); 547 548 const spamPatterns = [ 549 "qwerty", 550 "asdfgh", 551 "zxcvbn", 552 "qwert", 553 "asdf", 554 "zxcv" 555 ]; 556 557 for (const pattern of spamPatterns) { 558 if (lowerMessage.includes(pattern)) { 559 return false; 560 } 561 } 562 563 return true; 564 }, 565 "Please enter a meaningful message." 566 ); 567 568 569 570 $.validator.addMethod("minchar", function (value, element, param) { 571 return value.replace(/\s/g, "").length >= param; 572 }, "Please enter at least {0} characters."); 573 574 // âââââââââââââââââââââââââââââââââââââââââââââ 575 // INPUT SANITIZATIONS 576 // âââââââââââââââââââââââââââââââââââââââââââââ 577 578 function sanitizeInput(input, removeDigits = false) { 579 let val = input.val(); 580 val = val.replace(/^\s+/, ""); 581 if (removeDigits) val = val.replace(/\d/g, ""); 582 input.val(val); 583 } 584 585 $('[name="contactMessage"]').on("keypress", function (e) { 586 const char = String.fromCharCode(e.which); 587 if (this.value.length === 0 && char === " ") { 588 e.preventDefault(); 589 } 590 }); 591 592 // Fixed: was missing the $ selector prefix in original code 593 $('[name="contactMessage"]').on("paste", function (e) { 594 e.preventDefault(); 595 const pasted = (e.originalEvent || e).clipboardData.getData("text"); 596 const clean = pasted.replace(/^\s+/, ""); 597 const input = this; 598 const start = input.selectionStart; 599 const end = input.selectionEnd; 600 input.setRangeText(clean, start, end, "end"); 601 }); 602 603 $('[name="contactName"]').on('keypress', function (e) { 604 const char = String.fromCharCode(e.which); 605 const value = this.value; 606 607 // Allow only letters, spaces and apostrophe 608 if (!/[a-zA-Z\s']/.test(char)) { 609 e.preventDefault(); 610 return; 611 } 612 613 // Prevent space or apostrophe as first character 614 if (value.length === 0 && /[\s']/.test(char)) { 615 e.preventDefault(); 616 return; 617 } 618 619 // Allow only ONE apostrophe 620 if (char === "'" && value.includes("'")) { 621 e.preventDefault(); 622 return; 623 } 624 625 // Apostrophe must come after a letter 626 if (char === "'" && !/[a-zA-Z]$/.test(value)) { 627 e.preventDefault(); 628 return; 629 } 630 631 // Prevent multiple consecutive spaces 632 if (char === ' ' && value.endsWith(' ')) { 633 e.preventDefault(); 634 return; 635 } 636 }); 637 638 639 $('[name="contactName"]').on('paste', function (e) { 640 e.preventDefault(); 641 642 const pasted = (e.originalEvent || e) 643 .clipboardData 644 .getData('text'); 645 646 // Allow only letters, spaces and apostrophe 647 let clean = pasted.replace(/[^a-zA-Z\s']/g, ''); 648 649 // Remove leading space or apostrophe 650 clean = clean.replace(/^[\s']+/, ''); 651 652 // Multiple spaces -> single space 653 clean = clean.replace(/\s+/g, ' '); 654 655 // Keep only ONE apostrophe 656 const apostropheIndex = clean.indexOf("'"); 657 658 if (apostropheIndex !== -1) { 659 clean = 660 clean.substring(0, apostropheIndex + 1) + 661 clean.substring(apostropheIndex + 1).replace(/'/g, ''); 662 } 663 664 // Apostrophe must come after a letter 665 clean = clean.replace(/'(?=$|[^a-zA-Z])/g, ''); 666 clean = clean.replace(/([^a-zA-Z])'/g, '$1'); 667 668 const input = this; 669 const start = input.selectionStart; 670 const end = input.selectionEnd; 671 672 input.setRangeText(clean, start, end, 'end'); 673 });
674 675 function showFormLoader() { 676 if (!document.getElementById("form-loader")) { 677 const loader = document.createElement("div"); 678 loader.id = "form-loader"; 679 loader.innerHTML = ` 680 <style> 681 #form-loader { 682 position: fixed; 683 inset: 0; 684 background: rgba(255, 255, 255, 0.75); 685 z-index: 99999; 686 display: none; 687 align-items: center; 688 justify-content: center; 689 flex-direction: column; 690 gap: 10px; 691 } 692 693 .form-loader-spinner { 694 width: 40px; 695 height: 40px; 696 border: 4px solid #ddd; 697 border-top-color: #333; 698 border-radius: 50%; 699 animation: formLoaderSpin 0.8s linear infinite; 700 } 701 702 @keyframes formLoaderSpin { 703 to { 704 transform: rotate(360deg); 705 } 706 } 707 </style> 708 <div class="form-loader-spinner"></div> 709 <span>Submitting...</span> 710 `; 711 712 document.body.appendChild(loader); 713 } 714 715 document.getElementById("form-loader").style.display = "flex"; 716 } 717 718 function hideFormLoader() { 719 const loader = document.getElementById("form-loader"); 720 721 if (loader) { 722 loader.style.display = "none"; 723 } 724 } 725 726 727 728 // âââââââââââââââââââââââââââââââââââââââââââââ 729 // INITIALIZE FORMS 730 // âââââââââââââââââââââââââââââââââââââââââââââ 731 732 const forms = [ 733 { selector: "#footer-form", recaptcha: true }, 734 { selector: "#web-form", recaptcha: true }, 735 { selector: "#store-footer-form", recaptcha: true }, 736 ]; 737 738 forms.forEach((f) => { 739 const form = $(f.selector); 740 if (!form.length) return; 741 742 // Hidden fields: UTM, device, landing page 743 form 744 .find("input[name='website_source']") 745 .val(document.referrer || "Direct"); 746 form.find("input[name='landing_page']").val(window.location.href); 747 748 const urlParams = new URLSearchParams(window.location.search); 749 let source = urlParams.get("utm_source"); 750 let medium = urlParams.get("utm_medium"); 751 let campaign = urlParams.get("utm_campaign"); 752 753 if (!source && document.referrer) { 754 const refHost = new URL(document.referrer).hostname; 755 source = refHost; 756 if (/google|bing/.test(refHost)) medium = "organic"; 757 else if (/facebook|instagram|linkedin|pinterest/.test(refHost)) 758 medium = "social"; 759 else medium = "referral"; 760 } else if (!source) { 761 source = "direct"; 762 medium = "none"; 763 } 764 765 form.find("input[name='utm_source']").val(source); 766 form.find("input[name='utm_medium']").val(medium); 767 form.find("input[name='utm_campaign']").val(campaign); 768 form.find("input[name='campaign']").val(campaign); 769 form 770 .find("input[name='keyword_trigger']") 771 .val(urlParams.get("keyword") || document.title); 772 773 let deviceType = "Desktop"; 774 if (/Mobi|Android/i.test(navigator.userAgent)) deviceType = "Mobile"; 775 else if (/Tablet|iPad/i.test(navigator.userAgent)) deviceType = "Tablet"; 776 form.find("input[name='device']").val(deviceType); 777 778 // Repeat lead and email type 779 const emailInput = form.find("input[name='contactEmail']"); 780 const repeatLeadField = form.find("input[name='repeat_lead']"); 781 const emailTypeField = form.find("input[name='email_type']"); 782 783 emailInput.on("blur", function () { 784 const email = $(this).val().trim(); 785 if (!email) return;
786 fetch("https://www.actowizsolutions.com/assets/check-lead.php", { 787 method: "POST", 788 headers: { "Content-Type": "application/x-www-form-urlencoded" }, 789 body: "email=" + encodeURIComponent(email), 790 }) 791 .then((res) => res.json()) 792 .then((data) => { 793 if (data.repeat_lead) repeatLeadField.val(data.repeat_lead); 794 }) 795 .catch((err) => console.error(err)); 796 797 const freeDomains = [ 798 "gmail.com", 799 "yahoo.com", 800 "hotmail.com", 801 "outlook.com", 802 ]; 803 emailTypeField.val( 804 freeDomains.includes(email.split("@")[1]) ? "Free" : "Business", 805 ); 806 }); 807 808 // Validation config 809 const validationConfig = { 810 rules: { 811 contactName: { required: true, minchar: 2, validName: false, }, 812 contactEmail: { required: true, email: true, validEmail: true }, 813 contactPhone: { required: true, validPhoneNumber: true, minlength: 7 }, 814 serviceiweb: { required: false }, 815 contactMessage: { 816 required: true, 817 noLeadingSpace: true, 818 minchar: 2, 819 meaningfulMessage: true, 820 }, 821 }, 822 messages: { 823 contactName: { 824 required: "Please enter your full name.", 825 minchar: "At least 2 characters.", 826 validName: "Please enter a valid name.", 827 }, 828 contactEmail: { 829 required: "Please provide your email.", 830 email: "Enter valid email.", 831 validEmail: "Please enter a valid email.", 832 }, 833 contactPhone: { 834 required: "Provide phone number.", 835 minlength: "At least 7 digits.", 836 }, 837 serviceiweb: { required: "Please select a service." }, 838 contactMessage: { 839 required: "Tell us about your project.", 840 minlength: "At least 2 characters.", 841 meaningfulMessage: 842 "Please enter your requirements in detail.", 843 }, 844 }, 845 errorElement: "div", 846 errorClass: "error-message", 847 highlight: function (el) { 848 $(el).addClass("border-danger"); 849 }, 850 unhighlight: function (el) { 851 $(el).removeClass("border-danger"); 852 }, 853 errorPlacement: function (error, element) { 854 if (element.closest(".input-group").length) 855 element.closest(".input-group").after(error); 856 else element.after(error); 857 }, 858 invalidHandler: function (event, validator) { 859 // 1. Collect all form data (including invalid fields) 860 const form = validator.currentForm; // or $(this)[0] 861 const formData = new FormData(form); 862 863 // 2. Optionally add a flag to indicate this is an incomplete submission 864 formData.append("status", "incomplete"); // server can store with this status 865 866 // 3. Send the data to a dedicated endpoint for partial submissions 867 // (or reuse the same endpoint with a flag) 868 $.ajax({
869 url: "https://www.iwebdatascraping.com/contact-form-submit-backend-invalid.php", // or a different endpoint 870 method: "POST", 871 data: formData, 872 processData: false, 873 contentType: false, 874 success: function (response) { 875 // You can silently log success, or display a subtle message 876 console.log("Partial inquiry saved:", response); 877 }, 878 error: function (err) { 879 console.warn("Failed to save partial inquiry", err); 880 } 881 }); 882 883 // 4. Keep the validation errors visible (they are already shown by the plugin) 884 // No need to prevent anything â the form will not submit because of validation errors. 885 }, 886 submitHandler: function (formEl) { 887 showFormLoader(); 888 if (f.recaptcha) { 889 grecaptcha.ready(function () { 890 grecaptcha 891 .execute("6LexOQssAAAAAAI80tgLm8ht96LIh7LKrlglSDJD", { 892 action: "submit_form", 893 }) 894 .then(function (token) { 895 handleFormSubmit(formEl, token); 896 }); 897 }); 898 } else { 899 handleFormSubmit(formEl, null); 900 } 901 }, 902 }; 903 904 if (f.recaptcha) 905 validationConfig.rules.recaptcha = { validateRecaptcha: true }; 906 907 form.validate(validationConfig); 908 }); 909});
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.