1/** 2 * Multi-Step Popup Form Scripts 3 * Two-Step Form 4 */ 5 6(function ($) { 7 'use strict'; 8 9 // Form Controller 10 class PTFMultiStepForm { 11 constructor(form) { 12 this.form = $(form); 13 this.currentStep = 1; 14 this.totalSteps = 2; 15 this.isSubmitting = false; 16 17 this.init(); 18 } 19 20 init() { 21 this.bindEvents(); 22 this.updateProgress(); 23 this.form.attr('data-current-step', this.currentStep); 24 this.updateSubmitButtonState(); 25 this.initRecaptchaV2(); 26 } 27 28 /** 29 * Initialize reCAPTCHA v2 widget for Salesforce W2L browser-direct submission 30 */ 31 initRecaptchaV2() { 32 if (!ptfForm.sfW2lRecaptcha || !ptfForm.sfW2lRecaptcha.active) return; 33 34 const widgetContainer = document.getElementById('ptf-recaptcha-v2-widget'); 35 if (!widgetContainer) return; 36 37 let retryCount = 0; 38 const maxRetries = 20; // 10 seconds max 39 40 const renderWidget = () => { 41 if (typeof grecaptcha !== 'undefined' && typeof grecaptcha.render === 'function') { 42 // Use grecaptcha.ready() for compatibility with both v2 and v3 script loading 43 grecaptcha.ready(() => { 44 try { 45 this.recaptchaV2WidgetId = grecaptcha.render('ptf-recaptcha-v2-widget', { 46 'sitekey': ptfForm.sfW2lRecaptcha.siteKey, 47 'size': 'normal' 48 }); 49 } catch (e) { 50 console.error('reCAPTCHA v2 render error:', e); 51 } 52 }); 53 } else if (retryCount < maxRetries) { 54 retryCount++; 55 setTimeout(renderWidget, 500); 56 } else { 57 console.error('PTF: reCAPTCHA script failed to load after ' + maxRetries + ' retries.'); 58 } 59 }; 60 61 // Delay render to ensure script is loaded 62 if (document.readyState === 'complete') { 63 setTimeout(renderWidget, 300); 64 } else { 65 window.addEventListener('load', () => setTimeout(renderWidget, 300)); 66 } 67 } 68 69 bindEvents() { 70 // Next button 71 this.form.on('click', '.ptf-btn-next', (e) => { 72 e.preventDefault(); 73 this.nextStep(); 74 }); 75 76 // Previous button 77 this.form.on('click', '.ptf-btn-prev', (e) => { 78 e.preventDefault(); 79 this.prevStep(); 80 }); 81 82 // Form submission 83 this.form.on('submit', (e) => { 84 e.preventDefault(); 85 this.submitForm(); 86 }); 87 88 // Real-time validation 89 this.form.on('blur', 'input, select, textarea', (e) => { 90 this.validateField($(e.target)); 91 this.updateSubmitButtonState(); 92 }); 93 94 // Email special check (during input) 95 this.form.on('input', 'input[data-corporate-only="true"]', (e) => { 96 this.checkCorporateEmail($(e.target)); 97 this.updateSubmitButtonState(); 98 }); 99 100 // Checkbox change 101 this.form.on('change', 'input[type="checkbox"]', (e) => { 102 this.updateSubmitButtonState(); 103 104 // Clear error for privacy consent checkbox 105 if ($(e.target).attr('name') === 'kvkk_consent') { 106 const formGroup = $(e.target).closest('.ptf-consent-group'); 107 if ($(e.target).is(':checked')) { 108 formGroup.removeClass('has-error'); 109 formGroup.find('.ptf-field-error').text(''); 110 } 111 } 112 }); 113 114 // Clear error (on focus) 115 this.form.on('focus', 'input, select, textarea', (e) => { 116 const formGroup = $(e.target).closest('.ptf-form-group'); 117 if (!$(e.target).attr('data-corporate-only')) { 118 formGroup.removeClass('has-error'); 119 } 120 }); 121 122 // Enter key to go to next step 123 this.form.on('keypress', 'input', (e) => { 124 if (e.which === 13 && this.currentStep < this.totalSteps) { 125 e.preventDefault(); 126 this.nextStep(); 127 } 128 }); 129 130 // Close error message 131 this.form.on('click', '.ptf-message-close', (e) => { 132 $(e.target).closest('.ptf-form-message').slideUp(200); 133 }); 134 } 135 136 nextStep() { 137 if (this.validateStep(this.currentStep)) { 138 this.currentStep++; 139 this.showStep(this.currentStep); 140 this.updateProgress(); 141 this.updateSubmitButtonState(); 142 } 143 } 144 145 prevStep() { 146 if (this.currentStep > 1) { 147 this.currentStep--; 148 this.showStep(this.currentStep); 149 this.updateProgress(); 150 } 151 } 152 153 showStep(step) { 154 // Hide all steps 155 this.form.find('.ptf-form-step').removeClass('active'); 156 157 // Show target step 158 this.form.find(`.ptf-form-step[data-step="${step}"]`).addClass('active'); 159 160 // Update data attribute (for CSS animations) 161 this.form.attr('data-current-step', step); 162 163 // Scroll to form top (if not popup) 164 if (!this.form.closest('.ptf-popup-container').length) { 165 $('html, body').animate({ 166 scrollTop: this.form.offset().top - 100 167 }, 300); 168 } 169 } 170 171 updateProgress() { 172 const progressSteps = this.form.find('.ptf-progress-step'); 173 174 progressSteps.each((index, el) => { 175 const stepNum = $(el).data('step'); 176 $(el).removeClass('active completed'); 177 178 if (stepNum < this.currentStep) { 179 $(el).addClass('completed'); 180 } else if (stepNum === this.currentStep) { 181 $(el).addClass('active'); 182 } 183 }); 184 } 185 186 validateStep(step) { 187 const stepEl = this.form.find(`.ptf-form-step[data-step="${step}"]`); 188 let isValid = true; 189 190 stepEl.find('[required]').each((index, field) => { 191 const $field = $(field); 192 193 if ($field.attr('name') === 'kvkk_consent') { 194 return; 195 } 196 197 if (!this.validateField($field)) { 198 isValid = false;
199 } 200 }); 201 202 // Required multi-select question groups. 203 stepEl.find('.ptf-checkbox-group-inline[data-required="1"]').each((index, group) => { 204 const $group = $(group); 205 const formGroup = $group.closest('.ptf-form-group'); 206 const errorEl = formGroup.find('.ptf-field-error'); 207 const checkedCount = $group.find('input[type="checkbox"]:checked').length; 208 209 if (checkedCount === 0) { 210 isValid = false; 211 formGroup.addClass('has-error'); 212 errorEl.text(ptfForm.messages.required); 213 } else { 214 formGroup.removeClass('has-error'); 215 errorEl.text(''); 216 } 217 }); 218 219 if (step === 1) { 220 const emailField = stepEl.find('input[name="email"]'); 221 if (emailField.val() && !this.isCorporateEmail(emailField.val())) { 222 isValid = false; 223 } 224 225 const kvkkCheckbox = stepEl.find('input[name="kvkk_consent"]'); 226 if (!kvkkCheckbox.is(':checked')) { 227 isValid = false; 228 const formGroup = kvkkCheckbox.closest('.ptf-consent-group'); 229 formGroup.addClass('has-error'); 230 formGroup.find('.ptf-field-error').text(ptfForm.messages.checkbox_required); 231 } 232 } 233 234 if (!isValid) { 235 stepEl.find('.ptf-form-group.has-error:first input, .ptf-form-group.has-error:first select, .ptf-form-group.has-error:first textarea').focus(); 236 } 237 238 return isValid; 239 } 240 241 validateField(field) { 242 const formGroup = field.closest('.ptf-form-group'); 243 const errorEl = formGroup.find('.ptf-field-error'); 244 const type = field.attr('type'); 245 let value = field.val() ? String(field.val()).trim() : ''; 246 247 if (type === 'radio') { 248 const name = field.attr('name'); 249 const checked = formGroup.find(`input[type="radio"][name="${name}"]:checked`); 250 value = checked.length ? String(checked.val()).trim() : ''; 251 } 252 253 let isValid = true; 254 let errorMessage = ''; 255 256 // Required field validation 257 if (field.prop('required') && !value) { 258 isValid = false; 259 errorMessage = ptfForm.messages.required; 260 } 261 262 // Email format validation 263 else if (type === 'email' && value) { 264 if (!this.isValidEmail(value)) { 265 isValid = false; 266 errorMessage = ptfForm.messages.email; 267 } else if (field.attr('data-corporate-only') === 'true' && !this.isCorporateEmail(value)) { 268 isValid = false; 269 errorMessage = ptfForm.messages.corporate_email; 270 } 271 } 272 273 // Phone format validation 274 else if (type === 'tel' && value && !this.isValidPhone(value)) { 275 isValid = false; 276 errorMessage = ptfForm.messages.phone; 277 } 278 279 // Update error state 280 if (!isValid) { 281 formGroup.addClass('has-error'); 282 errorEl.text(errorMessage); 283 } else { 284 formGroup.removeClass('has-error'); 285 errorEl.text(''); 286 } 287 288 return isValid; 289 } 290 291 checkCorporateEmail(field) { 292 const value = field.val().trim(); 293 const formGroup = field.closest('.ptf-form-group'); 294 const errorEl = formGroup.find('.ptf-field-error'); 295 296 if (value && value.includes('@')) { 297 if (!this.isCorporateEmail(value)) { 298 formGroup.addClass('has-error'); 299 errorEl.text(ptfForm.messages.corporate_email); 300 return false; 301 } else if (this.isValidEmail(value)) { 302 formGroup.removeClass('has-error'); 303 errorEl.text(''); 304 return true; 305 } 306 } 307 return true; 308 } 309 310 isCorporateEmail(email) { 311 if (!email || !email.includes('@')) return false;
312 313 const domain = email.split('@')[1].toLowerCase(); 314 return !ptfForm.blockedDomains.includes(domain); 315 } 316 317 isValidEmail(email) { 318 const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 319 return regex.test(email); 320 } 321 322 isValidPhone(phone) { 323 const regex = /^[\d\s+\-()]{10,20}$/; 324 return regex.test(phone); 325 } 326 327 updateSubmitButtonState() { 328 const submitBtn = this.form.find('.ptf-btn-submit'); 329 submitBtn.prop('disabled', false); 330 } 331 332 submitForm() { 333 // Validate all steps 334 for (let step = 1; step <= this.totalSteps; step++) { 335 if (!this.validateStep(step)) { 336 this.showStep(step); 337 this.currentStep = step; 338 this.updateProgress(); 339 return; 340 } 341 } 342 343 if (this.isSubmitting) return; 344 this.isSubmitting = true; 345 346 // Show loading 347 this.showLoading(true); 348 349 // Validate reCAPTCHA v2 for Salesforce W2L (user must click "I'm not a robot" checkbox) 350 if (ptfForm.sfW2lRecaptcha && ptfForm.sfW2lRecaptcha.active) { 351 let v2Response = ''; 352 if (typeof grecaptcha !== 'undefined' && this.recaptchaV2WidgetId !== undefined) { 353 v2Response = grecaptcha.getResponse(this.recaptchaV2WidgetId); 354 } 355 if (!v2Response) { 356 this.isSubmitting = false; 357 this.showLoading(false); 358 this.showError(ptfForm.messages.recaptcha_error || 'Please complete the reCAPTCHA verification.'); 359 return; 360 } 361 } 362 363 // Check if reCAPTCHA should be used 364 const recaptchaEnabled = ptfForm.recaptcha && 365 ptfForm.recaptcha.enabled === true && 366 ptfForm.recaptcha.siteKey && 367 ptfForm.recaptcha.siteKey.length > 0 && 368 typeof grecaptcha !== 'undefined'; 369 370 // If reCAPTCHA is enabled and configured, get token 371 if (recaptchaEnabled) { 372 try { 373 grecaptcha.ready(() => { 374 try { 375 grecaptcha.execute(ptfForm.recaptcha.siteKey, {action: 'submit_quote_form'}) 376 .then((token) => { 377 this.sendFormData(token); 378 }) 379 .catch((error) => { 380 console.error('reCAPTCHA error:', error); 381 // Show error to user instead of silent fail 382 this.isSubmitting = false; 383 this.showLoading(false); 384 this.showError(ptfForm.messages.recaptcha_error || 'reCAPTCHA verification failed. Please try again.'); 385 }); 386 } catch (error) { 387 console.error('reCAPTCHA execute error:', error); 388 this.isSubmitting = false; 389 this.showLoading(false); 390 this.showError(ptfForm.messages.recaptcha_error || 'reCAPTCHA verification failed. Please check site configuration.'); 391 } 392 }); 393 } catch (error) { 394 console.error('reCAPTCHA ready error:', error); 395 this.isSubmitting = false; 396 this.showLoading(false); 397 this.showError(ptfForm.messages.recaptcha_error || 'reCAPTCHA failed to initialize.'); 398 } 399 } else { 400 // Submit directly if reCAPTCHA is not configured or not enabled 401 this.sendFormData(''); 402 } 403 } 404 405 sendFormData(recaptchaToken) { 406 // Collect form data 407 let formData = this.form.serialize(); 408 409 // Add reCAPTCHA token 410 if (recaptchaToken) {
411 formData += '&recaptcha_token=' + encodeURIComponent(recaptchaToken); 412 } 413 414 // Send via AJAX 415 $.ajax({ 416 url: ptfForm.ajaxurl, 417 type: 'POST', 418 data: formData, 419 success: (response) => { 420 this.isSubmitting = false; 421 this.showLoading(false); 422 423 if (response.success) { 424 // If W2L browser-direct data is returned, submit to Salesforce from browser 425 if (response.data.sfW2lData && ptfForm.sfW2lRecaptcha && ptfForm.sfW2lRecaptcha.active) { 426 this.submitToSalesforceViaIframe(response.data.sfW2lData); 427 } 428 this.showSuccess(response.data.message); 429 } else { 430 // Special reCAPTCHA error message 431 if (response.data.recaptcha_failed) { 432 this.showError(ptfForm.messages.recaptcha_error || response.data.message); 433 } else { 434 this.showError(response.data.message, response.data.errors); 435 } 436 } 437 }, 438 error: (xhr, status, error) => { 439 this.isSubmitting = false; 440 this.showLoading(false); 441 this.showError(ptfForm.messages.error); 442 console.error('Form submission error:', error); 443 } 444 }); 445 } 446 447 /** 448 * Submit mapped W2L data directly to Salesforce from the browser via hidden iframe. 449 * This bypasses server-side POST limitations and allows Salesforce to verify the reCAPTCHA v2 token. 450 */ 451 submitToSalesforceViaIframe(w2lData) { 452 try { 453 // Get the reCAPTCHA v2 response token from the widget 454 let recaptchaResponse = ''; 455 if (typeof grecaptcha !== 'undefined' && this.recaptchaV2WidgetId !== undefined) { 456 recaptchaResponse = grecaptcha.getResponse(this.recaptchaV2WidgetId); 457 } 458 459 if (!recaptchaResponse) { 460 console.warn('PTF: No reCAPTCHA v2 response token available for Salesforce W2L submission.'); 461 return; 462 } 463 464 // Create a hidden form targeting the hidden iframe 465 const hiddenForm = document.createElement('form'); 466 hiddenForm.method = 'POST'; 467 hiddenForm.action = 'https://webto.salesforce.com/servlet/servlet.WebToLead?encoding=UTF-8'; 468 hiddenForm.target = 'ptf_sf_w2l_iframe'; 469 hiddenForm.style.display = 'none'; 470 471 // Add all mapped W2L fields 472 for (const [key, value] of Object.entries(w2lData)) { 473 const input = document.createElement('input'); 474 input.type = 'hidden'; 475 input.name = key; 476 input.value = value; 477 hiddenForm.appendChild(input); 478 } 479 480 // Add captcha_settings with fresh timestamp 481 if (ptfForm.sfW2lRecaptcha.captchaSettings) { 482 let captchaObj; 483 try { 484 captchaObj = JSON.parse(ptfForm.sfW2lRecaptcha.captchaSettings); 485 } catch (e) { 486 captchaObj = null; 487 } 488 if (captchaObj) { 489 // Salesforce expects Unix timestamp in milliseconds as a stringified number 490 captchaObj.ts = JSON.stringify(new Date().getTime()); 491 const captchaInput = document.createElement('input'); 492 captchaInput.type = 'hidden'; 493 captchaInput.name = 'captcha_settings'; 494 captchaInput.value = JSON.stringify(captchaObj); 495 hiddenForm.appendChild(captchaInput); 496 } 497 } 498 499 // Add the reCAPTCHA v2 response token 500 const recaptchaInput = document.createElement('input'); 501 recaptchaInput.type = 'hidden'; 502 recaptchaInput.name = 'g-recaptcha-response'; 503 recaptchaInput.value = recaptchaResponse; 504 hiddenForm.appendChild(recaptchaInput); 505 506 // Append, submit, and cleanup 507 document.body.appendChild(hiddenForm); 508 hiddenForm.submit(); 509 510 // Remove the form after a short delay 511 setTimeout(() => { 512 if (hiddenForm.parentNode) { 513 hiddenForm.parentNode.removeChild(hiddenForm); 514 } 515 }, 2000); 516 517 console.log('PTF: Salesforce W2L browser-direct submission sent via hidden iframe.'); 518 } catch (error) { 519 console.error('PTF: Salesforce W2L browser-direct submission failed:', error); 520 } 521 } 522 523 showLoading(show) { 524 const loadingEl = this.form.find('.ptf-form-loading'); 525 const submitBtn = this.form.find('.ptf-btn-submit'); 526 527 if (show) { 528 loadingEl.show(); 529 submitBtn.prop('disabled', true).text(ptfForm.messages.sending); 530 } else { 531 loadingEl.hide(); 532 submitBtn.prop('disabled', false); 533 } 534 } 535 536 showSuccess(message) { 537 // Hide form steps and progress 538 this.form.find('.ptf-form-step').hide(); 539 this.form.find('.ptf-form-progress').hide(); 540 541 // Show success message 542 const successEl = this.form.find('.ptf-form-success'); 543 if (message) { 544 successEl.find('p').text(message); 545 } 546 successEl.show(); 547 548 // Close popup after 5 seconds (if it's a popup) 549 const popup = this.form.closest('.ptf-popup-overlay'); 550 if (popup.length) { 551 setTimeout(() => { 552 PTFPopup.close(); 553 // Reset form 554 setTimeout(() => this.resetForm(), 500); 555 }, 3000); 556 } 557 } 558 559 showError(message, fieldErrors = {}) { 560 // Show general error message in the form 561 if (message) { 562 const errorEl = this.form.find('.ptf-form-error-message'); 563 errorEl.find('.ptf-message-text').text(message); 564 errorEl.slideDown(200); 565 566 // Auto hide after 5 seconds 567 setTimeout(() => { 568 errorEl.slideUp(200); 569 }, 5000); 570 } 571 572 // Field-specific errors 573 if (fieldErrors && Object.keys(fieldErrors).length > 0) { 574 for (const field in fieldErrors) { 575 let input = this.form.find(`[name="${field}"]`); 576 if (!input.length) { 577 input = this.form.find(`[name="${field}[]"]`); 578 } 579 const formGroup = input.first().closest('.ptf-form-group'); 580 formGroup.addClass('has-error'); 581 formGroup.find('.ptf-field-error').text(fieldErrors[field]); 582 } 583 584 // Go to first error step 585 const firstError = this.form.find('.ptf-form-group.has-error:first'); 586 const errorStep = firstError.closest('.ptf-form-step').data('step'); 587 if (errorStep && errorStep !== this.currentStep) { 588 this.showStep(errorStep); 589 this.currentStep = errorStep; 590 this.updateProgress(); 591 } 592 } 593 } 594 595 resetForm() { 596 // Reset form 597 this.form[0].reset(); 598 this.currentStep = 1; 599 600 // Reset view 601 this.form.find('.ptf-form-step').hide(); 602 this.form.find(`.ptf-form-step[data-step="1"]`).addClass('active').show(); 603 this.form.find('.ptf-form-progress').show(); 604 this.form.find('.ptf-form-success').hide(); 605 this.form.find('.ptf-form-error-message').hide(); 606 this.form.find('.ptf-form-group').removeClass('has-error'); 607 this.form.find('.ptf-btn-submit').prop('disabled', true); 608 609 this.updateProgress(); 610 this.form.attr('data-current-step', 1); 611 } 612 } 613 614 // Popup Controller 615 const PTFPopup = { 616 overlay: null, 617 618 init() {
619 this.overlay = $('#ptf-popup-overlay'); 620 this.bindEvents(); 621 }, 622 623 bindEvents() { 624 // Popup triggers 625 $(document).on('click', '.ptf-popup-trigger', (e) => { 626 e.preventDefault(); 627 this.open(); 628 }); 629 630 // Close button 631 $(document).on('click', '.ptf-popup-close', (e) => { 632 e.preventDefault(); 633 this.close(); 634 }); 635 636 // Close on overlay click 637 this.overlay.on('click', (e) => { 638 if ($(e.target).is('.ptf-popup-overlay')) { 639 this.close(); 640 } 641 }); 642 643 // Close on ESC key 644 $(document).on('keydown', (e) => { 645 if (e.key === 'Escape' && this.overlay.hasClass('active')) { 646 this.close(); 647 } 648 }); 649 }, 650 651 open() { 652 this.overlay.css('display', 'flex'); 653 654 // Small delay for animation 655 setTimeout(() => { 656 this.overlay.addClass('active'); 657 }, 10); 658 659 // Prevent body scroll 660 $('body').css('overflow', 'hidden'); 661 662 // Focus first input 663 setTimeout(() => { 664 this.overlay.find('input:visible:first').focus(); 665 }, 300); 666 }, 667 668 close() { 669 this.overlay.removeClass('active'); 670 671 // Hide after animation 672 setTimeout(() => { 673 this.overlay.css('display', 'none'); 674 }, 300); 675 676 // Restore body scroll 677 $('body').css('overflow', ''); 678 } 679 }; 680 681 // Initialize on page load 682 $(document).ready(function () { 683 // Initialize all forms 684 $('.ptf-multistep-form').each(function () { 685 new PTFMultiStepForm(this); 686 }); 687 688 // Initialize popup 689 PTFPopup.init(); 690 }); 691 692 // Global access 693 window.PTFPopup = PTFPopup; 694 695})(jQuery);
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.