1/* 2** Authoring format ** 3Form block 4Row 1, col 1â3 â used for form-description-cell 5Row 2, col 1â4 â used as form-field-label 6Row 2, col 5 â used as submit button label 7Row 3, col 1â4 â used as input placeholder/content 8 9Row 4, col 1 â form-consent-text for whatsapp 10Row 4, col 2 â display / hide 11 12Row 5, col 1 â form-consent-note rich text â authored markup (incl. <a> 13 links) is kept as-is; see extractConsentNoteHtml(). Links get 14 styled/hardened by applyConsentLinkAttributes() (blue, opens in a new 15 tab). No note is rendered at all when this row/cell is empty. 16 17Row 6, col 1 â label ("Is it a BHPS form") 18Row 6, col 2 â isBhpsForm flag ("true" / "false") â see extractIsBhpsForm() 19 20Row 7, col 1 â form-consent-text for construction 21Row 7, col 2 â display / hide 22 23Row 8, col 1 â form-consent-text for local painter 24Row 8, col 2 â display / hide 25 26Row 9, col 1 â BHPS disclaimer question 1 text (e.g. "When do you plan 27 to start with the painting?") 28Row 9, col 2 â bullet list of radio option labels for question 1 29Row 9, col 3 â display / hide 30Row 10 â same shape as row 9, for disclaimer question 2 (e.g. "Is there 31 a local painter hired?") 32 â only rendered when isBhpsForm is true; see getDisclaimerQuestionConfig() 33 34Row 11, col 1 â desktop error image 35Row 11, col 2 â mobile error image 36 37Row 12, col 1 â label (ERROR_CTA_LINK) 38Row 12, col 2 â error popup CTA link 39 40Row 13, col 1 â label (ERROR_DOWNLOAD_LINK) 41Row 13, col 2 â error popup colour book download link 42 43Row 14, col 1 â label (THANK_YOU_TWO_DESKTOP) 44Row 14, col 2 â thank-you (variant 2) desktop image 45 46Row 15, col 1 â label (THANK_YOU_TWO_MOB) 47Row 15, col 2 â thank-you (variant 2) mobile image 48 49Row 16, col 1 â label (THANK_YOU_CTA_LABEL) 50Row 16, col 2 â thank-you "Book FREE site visit" CTA label 51 52Row 17, col 1 â label (THANK_YOU_BANNER_TITLE) 53Row 17, col 2 â thank-you banner title 54 55Row 18, col 1 â label (THANK_YOU_DESKTOP_VIDEO) 56Row 18, col 2 â thank-you desktop video link 57 58Row 19, col 1 â label (THANK_YOU_MOBILE_VIDEO) 59Row 19, col 2 â thank-you mobile video link 60 61Row 20, col 1 â label (THANK_YOU_CTA_LINK) 62Row 20, col 2 â thank-you "Book FREE site visit" CTA link 63 64Form Config block 65Row 1, col 1-2 â campaign ID (to pass as form parameter and for tracking) 66Row 2, col 1-2 â node (to pass as form parameter node path) 67Row 3, col 1-2 â BH Interested campaign ID (to pass as form parameter campaign ID) 68 69Two variant: White form, Blue form 70*/ 71 72import { submitLeadFromForm, checkCookieValues, setFormFieldsToCookie } from '../../scripts/form-submit-common.js'; 73import { getDigitalData, trackEvent, trackFormError } from '../../scripts/analytics_1.js'; 74import { firePixelConversion } from '../../scripts/third-party-pixels.js'; 75 76// Thank-you "Book FREE site visit" CTA â label + banner title used for 77// custom_cta_click (event132). parentTitle (eVar67) must be the banner 78// heading shown in the thank-you video, not the form's own title. 79// Populated from rows 15â16 of the form block in DA â see extractFormExtraLinks(). 80let THANK_YOU_CTA_LABEL = ''; 81let THANK_YOU_BANNER_TITLE = ''; 82// Populated from rows 17â19 of the form block in DA â see extractFormExtraLinks(). 83let THANK_YOU_DESKTOP_VIDEO = ''; 84let THANK_YOU_MOBILE_VIDEO = ''; 85let THANK_YOU_CTA_LINK = ''; 86 87let ERROR_DESKTOP_IMAGE = ''; 88let ERROR_MOBILE_IMAGE = ''; 89// Populated from rows 11â14 of the form block in DA â see extractFormExtraLinks(). 90let ERROR_CTA_LINK = ''; 91let ERROR_DOWNLOAD_LINK = ''; 92let THANK_YOU_TWO_DESKTOP = ''; 93let THANK_YOU_TWO_MOB = ''; 94 95let formName = ""; 96let campaignId = ""; 97let formNodePath = ''; 98let bhInterestedCampaignId = ''; 99let formStarted = false; 100 101// When true, the default WhatsApp consent + construction/local-painter 102// consent options are hidden and replaced with the BHPS (Beautiful Homes) 103// WhatsApp toggle + disclaimer questions markup below. Authored in row 5, 104// col 2 of the form block ("true"/"false") â see extractIsBhpsForm(). 105// Defaults to true when the row is absent or its value isn't recognized. 106let isBhpsForm = true; 107 108function getRowValue(row) { 109 if (!row) return ''; 110 111 const cells = [...row.children]; 112 const preferred = cells[1]?.textContent?.trim(); 113 if (preferred) return preferred; 114 115 return cells.map((cell) =>
115 cell.textContent?.trim()).filter(Boolean).join(' ').trim(); 116} 117 118function getCellText(cell) { 119 return cell?.textContent?.trim() || ''; 120} 121 122/** 123 * Read the isBhpsForm flag from row 6, col 2 ("true"/"false") and hide 124 * the row. Defaults to true (the module-level default) unless the row is 125 * present and its value is explicitly "false". 126 */ 127function extractIsBhpsForm(row) { 128 if (!row) return; 129 130 const cells = [...row.children]; 131 const value = getCellText(cells[1]).toLowerCase(); 132 isBhpsForm = value !== 'false'; 133 134 row.style.display = 'none'; 135} 136 137/** 138 * Row 5, col 1 â form-consent-note rich text, replacing the hardcoded 139 * disclaimer copy. Authored markup (e.g. <a> links) is kept as-is, the 140 * same trusted-DA-content pattern populateDescriptionCell() already uses 141 * via cell.innerHTML. Returns '' when the row/cell is absent or empty â 142 * callers render no consent note at all in that case (no fallback copy). 143 */ 144function extractConsentNoteHtml(row) { 145 if (!row) return ''; 146 147 const cell = row.children[0]; 148 const html = cell?.innerHTML?.trim(); 149 return html || ''; 150} 151 152/** 153 * Harden/style any links inside the (possibly authored) consent note: 154 * open in a new tab safely, and mark them for the blue/pointer styling 155 * in form.css. 156 * 157 * Also handles the open explicitly via window.open() instead of relying 158 * solely on target="_blank" â formRow's click listener (bubbled up from 159 * this same click) only runs its form_start tracking on the very first 160 * click anywhere in the form, and that first call synchronously injects 161 * the full GTM script (see fireGTM() in third-party-pixels.js), which 162 * can itself mutate the DOM / add listeners as its tags execute. That 163 * was observed to occasionally swallow the link's native navigation on 164 * that first click only â explicit window.open() inside this handler is 165 * unaffected by whatever else runs during the same event dispatch. 166 */ 167function applyConsentLinkAttributes(container) { 168 container?.querySelectorAll('a').forEach((link) => { 169 link.target = '_blank'; 170 link.rel = 'noopener noreferrer'; 171 link.classList.add('form-consent-note-link'); 172 173 const open = (e) => { 174 if (!link.href) return; 175 e.preventDefault(); 176 window.open(link.href, '_blank', 'noopener,noreferrer'); 177 const cell = link.closest('.form-row.form-attributes'); 178 179 const small = cell?.querySelector('.form-title-small')?.textContent?.trim(); 180 const large = cell?.querySelector('.form-title-large')?.textContent?.trim(); 181 182 const parentTitle = small || large || ''; 183 trackEvent('cta_link_text', { 184 cta_: link.textContent?.trim(), 185 parentTitle, 186 param1: link ? link.href : '' 187 }); 188 }; 189 190 // Open on pointerdown so it fires before any layout shift (blur 191 // validation error line, GTM injection) can move the link out from 192 // under the pointer and make the click miss. 193 link.addEventListener('pointerdown', (e) => { 194 if (e.button !== 0) return; // primary button only 195 link.dataset.opened = 'true'; 196 setTimeout(() => { delete link.dataset.opened; }, 300); 197 open(e); 198 }); 199 200 // Keep click for keyboard (Enter), but skip it when pointerdown 201 // already handled this interaction so it doesn't open twice. 202 link.addEventListener('click', (e) => { 203 if (link.dataset.opened === 'true') { e.preventDefault(); return; } 204 open(e); 205 }); 206 }); 207} 208 209/** 210 * Extract a URL from an authored cell â an anchor href, an image src, 211 * or plain text, in that order of preference. 212 */ 213function extractLinkFromCell(cell) { 214 if (!cell) return ''; 215 216 const anchor = cell.querySelector('a'); 217 if (anchor?.href) return anchor.href; 218 219 const img = cell.querySelector('img'); 220 if (img?.src) return img.src; 221 222 return cell.textContent?.trim() || ''; 223} 224 225/** 226 * Row shape: col 1 = label (for authors' reference), col 2 = the link. 227 */ 228function getRowLink(row) { 229 if (!row) return ''; 230 231 const cells = [...row.children]; 232 return extractLinkFromCell(cells[1]) || extractLinkFromCell(cells[0]); 233} 234 235/** 236 * Read error/thank-you-variant links (rows 12â15), thank-you CTA 237 * label/banner title (rows 16â17), and thank-you videos/CTA link 238 * (rows 18â20) from authored content, then hide those rows. Falls 239 * back to the module-level defaults when a row is absent or empty. 240 */ 241function extractFormExtraLinks(rows) { 242 const [ 243 errorCtaRow, errorDownloadRow, thankYouTwoDesktopRow, thankYouTwoMobileRow, 244 thankYouCtaLabelRow, thankYouBannerTitleRow,
245 thankYouDesktopVideoRow, thankYouMobileVideoRow, thankYouCtaLinkRow, 246 ] = rows.slice(11, 20); 247 248 const errorCtaLink = getRowLink(errorCtaRow); 249 if (errorCtaLink) ERROR_CTA_LINK = errorCtaLink; 250 251 const errorDownloadLink = getRowLink(errorDownloadRow); 252 if (errorDownloadLink) ERROR_DOWNLOAD_LINK = errorDownloadLink; 253 254 const thankYouTwoDesktopLink = getRowLink(thankYouTwoDesktopRow); 255 if (thankYouTwoDesktopLink) THANK_YOU_TWO_DESKTOP = thankYouTwoDesktopLink; 256 257 const thankYouTwoMobileLink = getRowLink(thankYouTwoMobileRow); 258 if (thankYouTwoMobileLink) THANK_YOU_TWO_MOB = thankYouTwoMobileLink; 259 260 const thankYouCtaLabel = getRowValue(thankYouCtaLabelRow); 261 if (thankYouCtaLabel) THANK_YOU_CTA_LABEL = thankYouCtaLabel; 262 263 const thankYouBannerTitle = getRowValue(thankYouBannerTitleRow); 264 if (thankYouBannerTitle) THANK_YOU_BANNER_TITLE = thankYouBannerTitle; 265 266 const thankYouDesktopVideoLink = getRowLink(thankYouDesktopVideoRow); 267 if (thankYouDesktopVideoLink) THANK_YOU_DESKTOP_VIDEO = thankYouDesktopVideoLink; 268 269 const thankYouMobileVideoLink = getRowLink(thankYouMobileVideoRow); 270 if (thankYouMobileVideoLink) THANK_YOU_MOBILE_VIDEO = thankYouMobileVideoLink; 271 272 const thankYouCtaLink = getRowLink(thankYouCtaLinkRow); 273 if (thankYouCtaLink) THANK_YOU_CTA_LINK = thankYouCtaLink; 274 275 [ 276 errorCtaRow, errorDownloadRow, thankYouTwoDesktopRow, thankYouTwoMobileRow, 277 thankYouCtaLabelRow, thankYouBannerTitleRow, 278 thankYouDesktopVideoRow, thankYouMobileVideoRow, thankYouCtaLinkRow, 279 ].forEach((row) => { 280 if (row) row.style.display = 'none'; 281 }); 282} 283 284function findNearestFormConfigBlock(block) { 285 const section = block.closest('.section'); 286 if (!section) return null; 287 288 const inCurrentSection = section.querySelector('.form-config, .form-configs'); 289 if (inCurrentSection) return inCurrentSection; 290 291 // Blue form pages may author form-config in a nearby sibling section. 292 let previous = section.previousElementSibling; 293 while (previous) { 294 const candidate = previous.querySelector?.('.form-config, .form-configs'); 295 if ( 296 candidate && 297 !previous.classList.contains('form-banner-container') 298 ) { 299 return candidate; 300 } 301 previous = previous.previousElementSibling; 302 } 303 304 let next = section.nextElementSibling; 305 while (next) { 306 const candidate = next.querySelector?.('.form-config, .form-configs'); 307 if ( 308 candidate && 309 !next.classList.contains('form-banner-container') 310 ) { 311 return candidate; 312 } 313 next = next.nextElementSibling; 314 } 315 316 return null; 317} 318 319function extractFormConfig(block) { 320 const section = block.closest('.section'); 321 const formConfigBlock = findNearestFormConfigBlock(block); 322 323 let configuredCampaignId = ''; 324 let configuredNodePath = ''; 325 let configuredBhInterestedCampaignId = ''; 326 327 if (formConfigBlock) { 328 const configRows = [...formConfigBlock.children]; 329 configuredCampaignId = getRowValue(configRows[0]); 330 configuredNodePath = getRowValue(configRows[1]); 331 configuredBhInterestedCampaignId = getRowValue(configRows[2]); 332 333 const formConfigWrapper = formConfigBlock.closest('.form-config-wrapper, .form-configs-wrapper'); 334 const formConfigSection = formConfigBlock.closest('.section'); 335 336 formConfigBlock.style.display = 'none'; 337 if (formConfigWrapper) { 338 formConfigWrapper.style.display = 'none'; 339 } 340 if ( 341 formConfigSection 342 && formConfigSection !== section 343 && !formConfigSection.querySelector('.form, .form-banner') 344 ) { 345 formConfigSection.style.display = 'none'; 346 } 347 } 348 349 configuredCampaignId = configuredCampaignId 350 || block.getAttribute('data-campaign-id')?.trim() 351 || ''; 352 353 configuredNodePath = configuredNodePath 354 || block.getAttribute('data-node')?.trim() 355 || window.location.pathname 356 || ''; 357 358 configuredBhInterestedCampaignId = configuredBhInterestedCampaignId 359 || block.getAttribute('data-attr-bhsInterestedCampaignId')?.trim() 360 || 'DECOR_ORGANIC_AP.com'; 361 362 // Prioritize utm_campaign from URL over configured campaign ID 363 const urlParams = new URLSearchParams(window.location.search);
364 const utmCampaign = urlParams.get('utm_campaign'); 365 366 campaignId = utmCampaign || configuredCampaignId; 367 formNodePath = configuredNodePath; 368 bhInterestedCampaignId = configuredBhInterestedCampaignId; 369} 370 371/** 372 * Ensure bootstrap utility classes where useful 373 */ 374function applyBootstrapHelpers(block) { 375 const section = block.closest('.section.form-container'); 376 const wrapper = block.closest('.form-wrapper'); 377 378 // section?.classList.add('bg-transparent'); 379 wrapper?.classList.add('m-0', 'p-0', 'w-100'); 380 block.classList.add('position-relative', 'overflow-hidden', 'w-100'); 381} 382 383/** 384 * Create inline error message element 385 */ 386function createErrorMessageElement() { 387 const error = document.createElement('div'); 388 error.className = 'form-field-error'; 389 error.setAttribute('aria-live', 'polite'); 390 return error; 391} 392 393/** 394 * Show field error 395 */ 396function showFieldError(field, input, message) { 397 if (!field || !input) return; 398 399 let errorEl = field.querySelector('.form-field-error'); 400 if (!errorEl) { 401 errorEl = createErrorMessageElement(); 402 field.appendChild(errorEl); 403 } 404 405 errorEl.textContent = message; 406 field.classList.add('has-error'); 407 input.classList.add('is-invalid'); 408 input.setAttribute('aria-invalid', 'true'); 409} 410 411/** 412 * Clear field error 413 */ 414function clearFieldError(field, input) { 415 if (!field || !input) return; 416 417 const errorEl = field.querySelector('.form-field-error'); 418 if (errorEl) { 419 errorEl.textContent = ''; 420 } 421 422 field.classList.remove('has-error'); 423 input.classList.remove('is-invalid'); 424 input.removeAttribute('aria-invalid'); 425 input.setCustomValidity(''); 426} 427 428/** 429 * Show/clear a "Field is required" error for a disclaimer radio group 430 * (BHPS form only) â mirrors showFieldError/clearFieldError above, but 431 * targets a fieldset-based radio group instead of a single .form-field. 432 */ 433function showDisclaimerGroupError(group) { 434 if (!group) return; 435 436 let errorEl = group.querySelector('.form-field-error'); 437 if (!errorEl) { 438 errorEl = createErrorMessageElement(); 439 // Append inside the fieldset (after the radio row), not as a sibling 440 // of it â .disclaimer-group is a flex row, so a sibling would sit 441 // beside the fieldset instead of stacking below the radio buttons. 442 (group.querySelector('fieldset') || group).appendChild(errorEl); 443 } 444 445 errorEl.textContent = 'Field is required'; 446 group.classList.add('has-error'); 447} 448 449function clearDisclaimerGroupError(group) { 450 if (!group) return; 451 452 const errorEl = group.querySelector('.form-field-error'); 453 if (errorEl) { 454 errorEl.textContent = ''; 455 } 456 457 group.classList.remove('has-error'); 458} 459 460/** 461 * Validate a disclaimer radio group â valid when one of its radios is 462 * checked. 463 */ 464function validateDisclaimerGroup(group, showError = true) { 465 if (!group) return true; 466 467 const isChecked = !!group.querySelector('.form-radio-input__field:checked'); 468 469 if (!isChecked) { 470 if (showError) showDisclaimerGroupError(group); 471 return false; 472 } 473 474 clearDisclaimerGroupError(group); 475 return true; 476} 477 478/** 479 * Restrict numeric-only inputs 480 */ 481function bindNumericInputFilter(input) { 482 if (!input) return; 483 484 input.addEventListener('input', () => { 485 const numericValue = input.value.replace(/\D/g, ''); 486 487 if (input.value !== numericValue) { 488 input.value = numericValue; 489 } 490 }); 491} 492 493/** 494 * Get custom validation message for input 495 */ 496function getValidationMessage(input) { 497 if (!input) return ''; 498 499 const value = input.value.trim(); 500 const fieldName = `${input.name || ''} ${input.id || ''}`.toLowerCase(); 501 502 const isEmail = fieldName.includes('email'); 503 const isPincode = fieldName.includes('pincode') || fieldName.includes('pin'); 504 const isMobile = fieldName.includes('mobile') || fieldName.includes('phone'); 505 506 if (!value) { 507 return 'Field is required'; 508 } 509 510 if (isEmail) { 511 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 512 if (!emailRegex.test(value)) { 513 return 'Email is invalid'; 514 } 515 } 516 517 if (isPincode) { 518 if (!/^\d{6}$/.test(value)) { 519 return 'Enter a valid 6 digit Zip Code'; 520 } 521 } 522 523 if (isMobile) { 524 if (!/^\d{10}$/.test(value)) {
525 return 'Phone number is invalid'; 526 } 527 } 528 529 return ''; 530} 531 532/** 533 * Validate single input 534 */ 535function validateInputField(input, showError = true) { 536 if (!input) return true; 537 538 const field = input.closest('.form-field'); 539 const message = getValidationMessage(input); 540 541 if (message) { 542 input.setCustomValidity(message); 543 544 if (showError) { 545 showFieldError(field, input, message); 546 } 547 548 return false; 549 } 550 551 clearFieldError(field, input); 552 return true; 553} 554 555/** 556 * Create default form field 557 */ 558function createDefaultField(cell, rowIdx, cellIdx, labelText, placeholderText) { 559 const field = document.createElement('div'); 560 field.className = 'form-field position-relative w-100'; 561 562 let inputId = `form-input-${rowIdx}-${cellIdx}`; 563 564 // Map common labels to specific ids 565 const lowerLabel = (labelText || '').toLowerCase(); 566 const isPincode = lowerLabel.includes('pincode') || lowerLabel.includes('pin'); 567 if (lowerLabel.includes('name')) { 568 inputId = 'enquire-name'; 569 } else if (lowerLabel.includes('email')) { 570 inputId = 'enquire-email'; 571 } else if (lowerLabel.includes('mobile') || lowerLabel.includes('phone')) { 572 inputId = 'enquire-mobile'; 573 } else if (lowerLabel.includes('pincode') || lowerLabel.includes('pin')) { 574 inputId = 'enquire-pincode'; 575 } 576 577 const label = document.createElement('label'); 578 label.className = 'form-field-label'; 579 label.setAttribute('for', inputId); 580 const displayLabel = 581 lowerLabel.includes('phone') || lowerLabel.includes('mobile') 582 ? 'Mobile Number' 583 : lowerLabel.includes('pincode') || lowerLabel.includes('pin') 584 ? 'PIN Code' 585 : (labelText || placeholderText); 586 587 label.innerHTML = `${displayLabel}<span class="form-field-required">*</span>`; 588 589 const input = document.createElement('input'); 590 let inputType = 'text'; 591 592 if (labelText) { 593 const lowerLabel = labelText.toLowerCase(); 594 if (lowerLabel.includes('phone') || lowerLabel.includes('mobile')) { 595 inputType = 'tel'; 596 } else if (lowerLabel.includes('email')) { 597 inputType = 'email'; 598 } 599 } 600 601 input.type = inputType; 602 input.id = inputId; 603 input.name = labelText || placeholderText; 604 input.placeholder = placeholderText; 605 input.className = 'form-input form-control'; 606 input.required = true; 607 input.noValidate = true; 608 609 const errorEl = createErrorMessageElement(); 610 611 612 const updateFieldState = () => { 613 const isFocused = document.activeElement === input; 614 const hasValue = !!input.value.trim(); 615 616 field.classList.toggle('is-focused', isFocused); 617 field.classList.toggle('has-value', hasValue); 618 619 if (hasValue) { 620 field.classList.add('show-label'); 621 } 622 }; 623 624 updateFieldState(); 625 626 input.addEventListener('focus', () => { 627 field.classList.add('show-label'); 628 }); 629 630 input.addEventListener('blur', () => { 631 updateFieldState(); 632 validateInputField(input, true); 633 }); 634 635 input.addEventListener('input', () => { 636 updateFieldState(); 637 638 if (field.classList.contains('has-error')) { 639 validateInputField(input, true); 640 } 641 }); 642 643 field.append(label, input, errorEl); 644 645 cell.textContent = ''; 646 cell.appendChild(field); 647} 648 649/** 650 * Build consent config from authoring rows 651 */ 652function getConsentConfig(consentRows = []) { 653 const [whatsappRow, constructionRow, localPainterRow] = consentRows; 654 655 const getConsentItem = (row, fallbackText) => { 656 const cells = [...(row?.children || [])]; 657 const text = getCellText(cells[0]) || fallbackText; 658 const visibility = (getCellText(cells[1]) || 'display').toLowerCase(); 659 const isHidden = visibility === 'hide'; 660 661 return { 662 text, 663 isHidden, 664 }; 665 }; 666 667 return { 668 whatsapp: getConsentItem(whatsappRow, 'Update me on WhatsApp'), 669 construction: getConsentItem(constructionRow, 'Construction work going on at my house'), 670 localPainter: getConsentItem(localPainterRow, 'Local painter hired'), 671 }; 672} 673 674/** 675 * Escape text for safe interpolation into innerHTML. 676 */ 677function escapeHtml(text) { 678 return String(text ?? '').replace(/[&<>"']/g, (char) => ({ 679 '&': '&', 680 '<': '<', 681 '>': '>', 682 '"': '"', 683 "'": ''', 684 }[char])); 685} 686 687/** 688 * Read one BHPS disclaimer question (row shape: col 1 = question text, 689 * col 2 = bullet list of radio options, col 3 = display/hide) from 690 * authoring. Falls back to no options (question is skipped) when the row 691 * is absent or has no options authored. 692 */ 693function getDisclaimerQuestionConfig(row) { 694 if (!row) return null; 695 696 const cells = [...row.children]; 697 const question = getCellText(cells[0]); 698 const options = [...(cells[1]?.querySelectorAll('li') || [])] 699 .map((li) => li.textContent?.trim()) 700 .filter(Boolean); 701 const visibility = (getCellText(cells[2]) || 'display').toLowerCase(); 702 const isHidden = visibility === 'hide'; 703 704 if (!question || !options.length) return null; 705 706 return { question, options, isHidden }; 707} 708 709/**
710 * Build one disclaimer question's markup (fieldset + radio options) from 711 * its authored config. Returns '' when the config is missing/hidden, so 712 * the question is simply omitted from the form. 713 */ 714function buildDisclaimerQuestionHtml(config, groupClass, fieldName, getValue = (label) => label) { 715 if (!config || config.isHidden) return ''; 716 717 const optionsHtml = config.options.map((label) => { 718 const value = getValue(label); 719 return ` 720 <label class="disclaimer-radio-label" tabindex="-1"> 721 <input type="radio" name="${fieldName}" value="${escapeHtml(value)}" tabindex="-1" class="form-radio-input__field"> 722 <span tabindex="0" role="radio" class="form-radio-input__custom-element track_field_focus focus-visible-auto-imp" aria-label="${escapeHtml(label)}" aria-checked="false"></span> 723 <span class="form-global__custom-label-text">${escapeHtml(label)}</span> 724 </label> 725 `; 726 }).join(''); 727 728 return ` 729 <div class="${groupClass} disclaimer-group d-flex"> 730 <fieldset> 731 <legend> 732 ${escapeHtml(config.question)} 733 <span class="form-global__required">*</span> 734 </legend> 735 <div class="radio-options-wrapper">${optionsHtml}</div> 736 </fieldset> 737 </div> 738 `; 739} 740 741/** 742 * Create BHPS-specific WhatsApp toggle + disclaimer questions markup, 743 * shown instead of the default consent options when isBhpsForm is true. 744 * disclaimerConfig: { questionOne, questionTwo } â each either a config 745 * object from getDisclaimerQuestionConfig() or null (row 8/9 in DA). 746 */ 747function createBhpsQuestionsWrapper(disclaimerConfig = {}) { 748 const { questionOne, questionTwo } = disclaimerConfig; 749 750 const wrapper = document.createElement('div'); 751 wrapper.className = 'new-questions-wrapper'; 752 753 // "Immediate" and "Within a month" are business-required to submit the 754 // same merged value the backend expects, even though DA authors them as 755 // two distinct option labels (a bullet list can't express "different 756 // label, same value" on its own). 757 const questionOneValue = (label) => { 758 const normalized = label.trim().toLowerCase(); 759 if (normalized === 'immediate' || normalized === 'within a month') { 760 return 'Immediate/Within 1 Month'; 761 } 762 return label; 763 }; 764 765 const questionOneHtml = buildDisclaimerQuestionHtml( 766 questionOne, 767 'disclaimer-ques-one', 768 'NEW_CUSTOM_CHECKBOX_customer_response_one', 769 questionOneValue, 770 ); 771 const questionTwoHtml = buildDisclaimerQuestionHtml( 772 questionTwo, 773 'disclaimer-ques-two', 774 'NEW_CUSTOM_CHECKBOX_customer_response_two', 775 ); 776 777 wrapper.innerHTML = ` 778 <!-- WhatsApp Toggle --> 779 <label class="form-whatsapp-consent" tabindex="-1"> 780 <input type="checkbox" name="CUSTOM_CHECKBOX" tabindex="-1" class="form-checkbox-input__field whatsAppConsentFormFld"> 781 <span tabindex="0" role="checkbox" class="form-checkbox-input__custom--check track_field_focus whatsAppCheckbox-checked focus-visible-auto-imp" aria-label="whatsapp toggle button" aria-checked="true"></span> 782 <span class="form-global__custom-label-text">Get updates on WhatsApp</span> 783 </label> 784 785 <!-- New Disclaimer Questions (rows 8â9 in DA) --> 786 <div class="new-disclaimer-questions"> 787 ${questionOneHtml} 788 ${questionTwoHtml} 789 </div> 790 `; 791 792 // Wire up the WhatsApp toggle: the visible control is a decorative span 793 // (role="checkbox") next to a visually-hidden real checkbox input. Keep 794 // both in sync and let click/keyboard (Enter/Space) toggle the state. 795 const whatsappInput = wrapper.querySelector('.form-whatsapp-consent .form-checkbox-input__field'); 796 const whatsappCheck = wrapper.querySelector('.form-whatsapp-consent .form-checkbox-input__custom--check'); 797 798 if (whatsappInput && whatsappCheck) { 799 const syncWhatsAppToggle = () => { 800 const { checked } = whatsappInput; 801 whatsappCheck.classList.toggle('whatsAppCheckbox-checked', checked); 802 whatsappCheck.setAttribute('aria-checked', String(checked)); 803 }; 804 805 whatsappInput.checked = true; 806 syncWhatsAppToggle(); 807 808 whatsappInput.addEventListener('change', syncWhatsAppToggle); 809 810 whatsappCheck.addEventListener('keydown', (event) => { 811 if (event.key !== 'Enter' && event.key !== ' ') return; 812 event.preventDefault();
813 whatsappInput.checked = !whatsappInput.checked; 814 whatsappInput.dispatchEvent(new Event('change', { bubbles: true })); 815 }); 816 } 817 818 // Clear a disclaimer group's "Field is required" error as soon as the 819 // user picks an option in it (submit-time validation shows it â see 820 // validateDisclaimerGroup / bindSubmitHandler). 821 wrapper.querySelectorAll('.disclaimer-group .form-radio-input__field').forEach((radio) => { 822 radio.addEventListener('change', () => { 823 clearDisclaimerGroupError(radio.closest('.disclaimer-group')); 824 }); 825 }); 826 827 return wrapper; 828} 829 830/** 831 * Create consent / checkbox area 832 */ 833function createConsentArea(consentConfig, disclaimerConfig, consentNoteHtml) { 834 const consentArea = document.createElement('div'); 835 consentArea.className = 'form-consent-area d-flex flex-column'; 836 837 consentArea.innerHTML = ` 838 <div class="form-consent-options d-flex flex-wrap align-items-center"> 839 <label class="form-consent-item form-consent-item--toggle" data-consent="whatsapp"> 840 <input 841 type="checkbox" 842 class="form-consent-input whatsAppConsentFormFld" 843 name="updateMeOnWhatsapp" 844 value="true" 845 /> 846 <span class="form-consent-toggle" aria-hidden="true"></span> 847 <span class="form-consent-text form-consent-text-whatsapp">${consentConfig?.whatsapp?.text || 'Update me on WhatsApp'}</span> 848 </label> 849 850 <div class="form-consent-options-right"> 851 <label class="form-consent-item" data-consent="construction"> 852 <input 853 type="checkbox" 854 class="form-consent-input" 855 name="constructionWorkGoingOn" 856 value="true" 857 /> 858 <span class="form-consent-box" aria-hidden="true"></span> 859 <span class="form-consent-text">${consentConfig?.construction?.text || 'Construction work going on at my house'}</span> 860 </label> 861 862 <label class="form-consent-item" data-consent="localPainter"> 863 <input 864 type="checkbox" 865 class="form-consent-input" 866 name="localPainterHired" 867 value="true" 868 /> 869 <span class="form-consent-box" aria-hidden="true"></span> 870 <span class="form-consent-text">${consentConfig?.localPainter?.text || 'Local painter hired'}</span> 871 </label> 872 </div> 873 </div> 874 875 `; 876 877 if (consentNoteHtml) { 878 const consentNote = document.createElement('div'); 879 consentNote.className = 'form-consent-note'; 880 consentNote.innerHTML = consentNoteHtml; 881 consentArea.append(consentNote); 882 applyConsentLinkAttributes(consentNote); 883 } 884 885 const whatsappItem = consentArea.querySelector('[data-consent="whatsapp"]'); 886 const constructionItem = consentArea.querySelector('[data-consent="construction"]'); 887 const localPainterItem = consentArea.querySelector('[data-consent="localPainter"]'); 888 const rightWrapper = consentArea.querySelector('.form-consent-options-right'); 889 890 if (consentConfig?.whatsapp?.isHidden && whatsappItem) { 891 whatsappItem.classList.add('d-none'); 892 whatsappItem.hidden = true; 893 } 894 895 if (consentConfig?.construction?.isHidden && constructionItem) { 896 constructionItem.classList.add('d-none'); 897 constructionItem.hidden = true; 898 } 899 900 if (consentConfig?.localPainter?.isHidden && localPainterItem) { 901 localPainterItem.classList.add('d-none'); 902 localPainterItem.hidden = true; 903 } 904 905 // Hide the right-side wrapper completely when both right consent items are hidden 906 const isConstructionHidden = !!consentConfig?.construction?.isHidden; 907 const isLocalPainterHidden = !!consentConfig?.localPainter?.isHidden; 908 909 if (isConstructionHidden && isLocalPainterHidden && rightWrapper) { 910 rightWrapper.classList.add('d-none'); 911 rightWrapper.hidden = true; 912 } 913 914 if (isBhpsForm) { 915 // Hide the default WhatsApp consent + construction/local-painter options... 916 if (whatsappItem) { 917 whatsappItem.classList.add('d-none'); 918 whatsappItem.hidden = true; 919 // Strip the tracking hook so buildLeadPayload's form.querySelector 920 // ('.whatsAppConsentFormFld') resolves to the live BHPS toggle below 921 // instead of this hidden, unused checkbox (first match wins). 922 whatsappItem.querySelector('.whatsAppConsentFormFld')?.classList.remove('whatsAppConsentFormFld'); 923 } 924 if (rightWrapper) { 925 rightWrapper.classList.add('d-none'); 926 rightWrapper.hidden = true; 927 } 928 929 // ...and show the BHPS WhatsApp toggle + disclaimer questions instead, 930 // inserted above the consent note so the note stays the last item. 931 const consentNote = consentArea.querySelector('.form-consent-note'); 932 consentArea.insertBefore(createBhpsQuestionsWrapper(disclaimerConfig), consentNote); 933 } 934 935 return consentArea; 936} 937 938/** 939 * Add a fixed +91 prefix inside the phone input without changing 940 * the actual input value submitted to the backend. 941 * 942 * DOM after enhancement: 943 * .form-field 944 * label 945 * .form-input-prefix-wrap 946 * span.form-input-prefix 947 * input.form-input 948 * .form-field-error 949 * 950 * This keeps the prefix visible both with placeholder and while typing. 951 */ 952function applyPhonePrefix(field, input) { 953 if (!field || !input || field.querySelector('.form-input-prefix-wrap')) return; 954 955 const inputWrap = document.createElement('div'); 956 inputWrap.className = 'form-input-prefix-wrap'; 957 958 const prefix = document.createElement('span'); 959 prefix.className = 'form-input-prefix'; 960 prefix.setAttribute('aria-hidden', 'true'); 961 prefix.textContent = '+91'; 962 963 field.insertBefore(inputWrap, input); 964 inputWrap.appendChild(prefix); 965 inputWrap.appendChild(input); 966} 967 968/** 969 * Apply field-specific attributes 970 * 971 * Note: 972 * - Keep only one applyFieldConfig function in this file. 973 * - The phone field gets a visual +91 prefix, but input value remains 974 * only the 10-digit number expected by validation/submission logic. 975 */ 976function applyFieldConfig(input, headerCell, content, block) { 977 if (!input || !headerCell) return; 978 979 const headerLabel = headerCell.textContent.toLowerCase(); 980 981 input.placeholder = content; 982 input.required = true; 983 984 if (headerLabel.includes('phone')) { 985 input.type = 'tel'; 986 input.id = 'enquire-mobile'; 987 input.name = 'ENQUIRE_MOBILE'; 988 input.inputMode = 'numeric'; 989 input.pattern = '[0-9]{10}'; 990 input.maxLength = 10; 991 bindNumericInputFilter(input); 992 993 const field = input.closest('.form-field'); 994 const isBlueForm = block.classList.contains('blue-form'); 995 if (isBlueForm) { 996 applyPhonePrefix(field, input); 997 input.classList.add('form-input--with-prefix'); 998 } 999 } else if (headerLabel.includes('email')) { 1000 input.type = 'email'; 1001 input.id = 'enquire-email'; 1002 input.name = 'ENQUIRE_EMAIL'; 1003 } else if (headerLabel.includes('pin')) { 1004 input.type = 'text'; 1005 input.id = 'enquire-pincode'; 1006 input.name = 'ENQUIRE_PINCODE'; 1007 input.inputMode = 'numeric'; 1008 input.pattern = '[0-9]{6}'; 1009 input.maxLength = 6; 1010 bindNumericInputFilter(input); 1011 } else if (headerLabel.includes('name')) { 1012 input.type = 'text'; 1013 input.id = 'enquire-name'; 1014 input.name = 'ENQUIRE_NAME'; 1015 input.pattern = '[A-Za-z ]{2,}'; 1016 } 1017} 1018 1019/** 1020 * Create one description item inside description cell 1021 */ 1022function createDescriptionItem(cell, className) { 1023 const item = document.createElement('div'); 1024 item.className = className; 1025 item.innerHTML = cell?.innerHTML || ''; 1026 return item; 1027} 1028 1029/** 1030 * Populate description cell using row 1 col 1, 2, 3 1031 */ 1032function populateDescriptionCell(cell, descriptionCells) { 1033 cell.textContent = ''; 1034 1035 const smallTitle = createDescriptionItem(descriptionCells[0], 'form-title-small'); 1036 const largeTitle = createDescriptionItem(descriptionCells[1], 'form-title-large'); 1037 const subtitle = createDescriptionItem(descriptionCells[2], 'form-subtitle'); 1038 1039 if (descriptionCells[0]?.innerHTML?.trim()) { 1040 formName = smallTitle.textContent?.trim(); 1041 cell.appendChild(smallTitle); 1042 } 1043 1044 if (descriptionCells[1]?.innerHTML?.trim()) { 1045 cell.appendChild(largeTitle); 1046 } 1047 1048 if (descriptionCells[2]?.innerHTML?.trim()) { 1049 cell.appendChild(subtitle); 1050 } 1051} 1052 1053/** 1054 * Cleanup video 1055 */ 1056function cleanupVideo(video) { 1057 if (!video) return; 1058 video.pause(); 1059 video.removeAttribute('src'); 1060 video.load(); 1061} 1062 1063/** 1064 * Previously locked page scroll behind the full-screen mobile popup modal 1065 * (see .form.block.popup-active in form.css, <=600px) while it was open. 1066 * Now a deliberate no-op: the page must stay scrollable behind the 1067 * success/error popup on mobile even without closing it first. Kept (with 1068 * unlockBodyScroll) so call sites don't need to change. 1069 */ 1070function lockBodyScroll() { 1071 // Intentionally does not lock document.body.style.overflow anymore. 1072} 1073 1074function unlockBodyScroll() { 1075 document.body.style.overflow = ''; 1076} 1077 1078/** 1079 * Close active popup 1080 */ 1081function closeActivePopup(block, overlay, video = null) { 1082 cleanupVideo(video); 1083 overlay?.remove(); 1084 block.classList.remove('thank-you-active', 'error-active', 'popup-active'); 1085 delete block.dataset.thankYouShown; 1086 delete block.dataset.errorShown; 1087 unlockBodyScroll(); 1088} 1089 1090/** 1091 * Create close button 1092 */ 1093function createCloseButton(labelText) { 1094 const button = document.createElement('button'); 1095 button.type = 'button'; 1096 button.className = 'form-popup-close'; 1097 button.setAttribute('aria-label', labelText); 1098 button.innerHTML = '×'; 1099 return button; 1100} 1101 1102function trackInterestCTA(currentCampaignId, currentFormName) { 1103 trackEvent('Form_thank_you_pop_up', { 1104 cid: currentCampaignId, 1105 formName: currentFormName, 1106 }); 1107} 1108 1109function trackExploreNowCTA(ctaLink) { 1110 if (!ctaLink) return; 1111 1112 // eVar67 (parentTitle) must carry the real banner/form title, not a 1113 // fixed label. This inline form already resolves its visible heading 1114 // into the module-level `formName` (e.g. "Looking For Expert Guidance 1115 // To Design Your Dream Home?"). Fall back to the old literal if empty.
1116 const bannerTitle = formName || 'Form Thank you pop-up'; 1117 1118 trackEvent('cta_link_text', { 1119 cta_: 'Explore now', 1120 parentTitle: bannerTitle, 1121 param1: ctaLink, 1122 redirectionLink: ctaLink, 1123 }); 1124} 1125 1126/** 1127 * Create thank you overlay 1128 */ 1129function createThankYouScreen(block, row) { 1130 const overlay = document.createElement('div'); 1131 overlay.className = 'form-popup-overlay form-popup-overlay--success'; 1132 1133 const isMobile = window.matchMedia('(max-width: 991px)').matches; 1134 const videoSrc = isMobile ? THANK_YOU_MOBILE_VIDEO : THANK_YOU_DESKTOP_VIDEO; 1135 1136 const closeBtn = createCloseButton('Close thank you popup'); 1137 1138 const video = document.createElement('video'); 1139 video.className = 'form-popup-success-video'; 1140 video.setAttribute('playsinline', ''); 1141 video.setAttribute('autoplay', ''); 1142 video.setAttribute('muted', ''); 1143 video.setAttribute('preload', 'auto'); 1144 video.setAttribute('aria-label', 'thank-you'); 1145 video.src = videoSrc; 1146 video.loop = false; 1147 1148 const cta = document.createElement('a'); 1149 cta.className = 'form-popup-success-cta'; 1150 cta.href = THANK_YOU_CTA_LINK; 1151 cta.target = '_blank'; 1152 cta.rel = 'noopener noreferrer'; 1153 cta.setAttribute('aria-label', THANK_YOU_CTA_LABEL); 1154 cta.addEventListener('click', () => { 1155 // Thank-you "Book FREE site visit" CTA fires custom_cta_click (event132): 1156 // cta_ -> eVar45, parentTitle -> eVar67 (the banner title, not the form 1157 // title), redirectionLink -> eVar86. 1158 trackEvent('custom_cta_click', { 1159 cta_: THANK_YOU_CTA_LABEL, 1160 parentTitle: THANK_YOU_BANNER_TITLE, 1161 redirectionLink: THANK_YOU_CTA_LINK, 1162 }); 1163 }); 1164 1165 const ins = document.createElement('a'); 1166 ins.className = 'intereseted-cta'; 1167 ins.href = 'javascript:void(0)'; 1168 ins.setAttribute('aria-label', 'Yes I am Interseted'); 1169 ins.addEventListener('click', () => { 1170 trackInterestCTA(bhInterestedCampaignId, formName); 1171 //Added chnages for form submission on click of interested CTA and BHPS campaignID need to pass 1172 submitLeadFromForm(row, { 1173 resourcePath: row.getAttribute('data-attr-resourcePath') || formNodePath || window.location.pathname || '', 1174 campaignId: bhInterestedCampaignId}); 1175 const parent = ins.closest('.form-popup-overlay'); 1176 if (!parent) return; 1177 1178 const videoEl = parent.querySelector('video'); 1179 const ctaEl = parent.querySelector('.form-popup-success-cta'); 1180 const insEl = parent.querySelector('.intereseted-cta'); 1181 1182 if (videoEl) videoEl.style.display = 'none'; 1183 if (ctaEl) ctaEl.style.display = 'none'; 1184 if (insEl) insEl.style.display = 'none'; 1185 1186 const wrapper = document.createElement('div'); 1187 wrapper.className = 'thank-you-pop-two'; 1188 1189 wrapper.innerHTML = ` 1190 <img class="thank-img-desk" src="${THANK_YOU_TWO_DESKTOP}" alt="thank you" /> 1191 <img class="thank-img-mob" src="${THANK_YOU_TWO_MOB}" alt="thank you" /> 1192 <a class="explore-now-rewamp" href="${THANK_YOU_CTA_LINK}" target="_blank"> 1193 Explore Now 1194 </a> 1195 `; 1196 1197 const rewampCta = wrapper.querySelector('.explore-now-rewamp'); 1198 rewampCta?.addEventListener('click', () => { 1199 trackExploreNowCTA(THANK_YOU_CTA_LINK); 1200 }); 1201 1202 parent.appendChild(wrapper); 1203 }); 1204 1205 video.addEventListener('ended', () => { 1206 video.pause(); 1207 }); 1208 1209 closeBtn.addEventListener('click', () => { 1210 closeActivePopup(block, overlay, video); 1211 }); 1212 1213 overlay.append(closeBtn, video, cta, ins); 1214 1215 return overlay; 1216} 1217 1218/** 1219 * Create error overlay 1220 */ 1221function createErrorScreen(block) { 1222 const overlay = document.createElement('div'); 1223 overlay.className = 'form-popup-overlay form-popup-overlay--error'; 1224 1225 const closeBtn = createCloseButton('Close error popup'); 1226 1227 overlay.innerHTML = ` 1228 <div class="form-error-popup"> 1229 <div class="form-error-popup__left"> 1230 <div class="form-error-popup__copy"> 1231 <h2 class="form-error-popup__title">Try again!</h2> 1232 <p class="form-error-popup__text"> 1233 Oops! Something went wrong. We couldn't process your request. Please try again? 1234 </p> 1235 </div> 1236 </div> 1237 1238 <div class="form-error-popup__right"> 1239 <div 1240 class="form-error-popup__media" 1241 style=" 1242 --form-error-image-desktop: url('${ERROR_DESKTOP_IMAGE}'); 1243 --form-error-image-mobile: url('${ERROR_MOBILE_IMAGE}'); 1244 " 1245 > 1246 <div class="form-error-popup__content"> 1247 <p class="form-error-popup__content-title"> 1248 Are you looking for home interiors? 1249 </p> 1250 <p class="form-error-popup__content-subtitle"> 1251 Get expert execution for your dream home from Beautiful Homes Interior Design service by asian paints. 1252 </p> 1253 <div class="form-error-popup__actions"> 1254 <a 1255 href="${ERROR_CTA_LINK}" 1256 target="_self" 1257 class="form-error-popup__button trackCTA" 1258 > 1259 Get FREE Design Consultation 1260 </a> 1261 </div> 1262 <a 1263 class="form-error-popup__download" 1264 href="${ERROR_DOWNLOAD_LINK}" 1265 download 1266 > 1267 Download the asianpaints colour books 1268 </a> 1269 </div> 1270 </div> 1271 </div> 1272 </div> 1273 `; 1274 1275 overlay.prepend(closeBtn); 1276 1277 closeBtn.addEventListener('click', () => { 1278 closeActivePopup(block, overlay); 1279 }); 1280 1281 return overlay; 1282} 1283 1284/** 1285 * Show thank you overlay 1286 */ 1287function showThankYouScreen(block, row) { 1288 if (!block || block.dataset.thankYouShown === 'true') return; 1289 1290 const existingOverlay = block.querySelector('.form-popup-overlay--success'); 1291 if (existingOverlay) { 1292 block.dataset.thankYouShown = 'true'; 1293 block.classList.add('thank-you-active', 'popup-active'); 1294 lockBodyScroll(); 1295 return; 1296 } 1297
1298 const overlay = createThankYouScreen(block, row); 1299 block.appendChild(overlay); 1300 block.dataset.thankYouShown = 'true'; 1301 block.classList.add('thank-you-active', 'popup-active'); 1302 lockBodyScroll(); 1303} 1304 1305/** 1306 * Resolve a disclaimer question's answer for analytics (eVar73/eVar110): 1307 * the checked radio's original authored label (e.g. "Immediate", 1308 * "Within a month", "After 1 month") via its custom-element aria-label, 1309 * NOT its `value` attribute â questionOneValue() collapses "Immediate" 1310 * and "Within a month" into one merged backend value ("Immediate/Within 1311 * 1 Month"), which would otherwise hide which option was actually picked. 1312 * Falls back to the legacy plain checkbox (non-BHPS forms) when the 1313 * radio group isn't present. 1314 */ 1315function getDisclaimerAnswer(form, radioName, checkboxName) { 1316 const checkedRadio = form.querySelector(`[name="${radioName}"]:checked`); 1317 if (checkedRadio) { 1318 const labelSpan = checkedRadio.closest('.disclaimer-radio-label') 1319 ?.querySelector('.form-radio-input__custom-element'); 1320 return labelSpan?.getAttribute('aria-label') || checkedRadio.value; 1321 } 1322 1323 const checkbox = form.querySelector(`[name="${checkboxName}"]`); 1324 return checkbox ? (checkbox.checked ? 'Yes' : 'No') : 'No'; 1325} 1326 1327function trackFormSubmission(form) { 1328 // BHPS forms (isBhpsForm, the default) render the WhatsApp toggle as 1329 // .whatsAppConsentFormFld rather than [name="updateMeOnWhatsapp"] â see 1330 // createConsentArea(), which strips that class off the hidden default 1331 // checkbox and moves it onto the live BHPS input so this selector 1332 // always resolves to whichever toggle is actually visible. 1333 const isWhatsappChecked = form.querySelector('.whatsAppConsentFormFld')?.checked; 1334 const contruction = getDisclaimerAnswer(form, 'NEW_CUSTOM_CHECKBOX_customer_response_one', 'constructionWorkGoingOn'); 1335 const localpainter = getDisclaimerAnswer(form, 'NEW_CUSTOM_CHECKBOX_customer_response_two', 'localPainterHired'); 1336 const formData = { 1337 "formName": formName, 1338 "campaignId": campaignId, 1339 // Send explicit 'unchecked' (not undefined) so eVar108 always reflects the 1340 // real opt-in state on the beacon instead of being dropped by trackEvent's 1341 // undefined-filter. QA relies on the presence of the value for reporting. 1342 "whatsappOptIn": isWhatsappChecked ? 'checked' : 'unchecked', 1343 "contruction": contruction, 1344 "localpainter": localpainter, 1345 // Spec field â see lead-form.js for rationale. Default 'both'. 1346 "dataDestination": form.dataset?.dataDestination || 'both' 1347 }; 1348 getDigitalData().form = formData; 1349 trackEvent("Form Submit", { 1350 formName, 1351 campaignId, 1352 ...formData, 1353 }); 1354 // Mirror to marketing pixels via image beacons â no SDK library load 1355 firePixelConversion('form_submit', { 1356 event_category: formName, 1357 value: 1, 1358 currency: 'INR', 1359 }); 1360} 1361 1362/** 1363 * Show error overlay 1364 */ 1365function showErrorScreen(block) { 1366 if (!block || block.dataset.errorShown === 'true') return; 1367 1368 const existingOverlay = block.querySelector('.form-popup-overlay--error'); 1369 if (existingOverlay) { 1370 block.dataset.errorShown = 'true'; 1371 block.classList.add('error-active', 'popup-active'); 1372 lockBodyScroll(); 1373 return; 1374 } 1375 1376 const overlay = createErrorScreen(block); 1377 block.appendChild(overlay); 1378 block.dataset.errorShown = 'true'; 1379 block.classList.add('error-active', 'popup-active'); 1380 lockBodyScroll(); 1381} 1382 1383/** 1384 * Bind submit handler 1385 */ 1386function bindSubmitHandler(block, row, buttonCell) { 1387 const submitBtn = buttonCell?.querySelector('button.form-submit'); 1388 if (!submitBtn || submitBtn.dataset.submitBound === 'true') return; 1389 1390 const loaderEl = submitBtn.querySelector('.rotating'); 1391 submitBtn.dataset.submitBound = 'true'; 1392 1393 submitBtn.addEventListener('click', (event) => { 1394 event.preventDefault(); 1395 1396 if (row.dataset.submitting === 'true') return; 1397 1398 const inputs = row.querySelectorAll('.form-input'); 1399 let isValid = true; 1400 const failedFields = []; 1401 1402 const fieldNameMap = { 1403 ENQUIRE_NAME: 'Name', 1404 ENQUIRE_EMAIL: 'Email', 1405 ENQUIRE_MOBILE: 'MobileNumber', 1406 ENQUIRE_PINCODE: 'PINCode', 1407 }; 1408
1409 inputs.forEach((input) => { 1410 const validField = validateInputField(input, true); 1411 if (!validField) { 1412 isValid = false; 1413 const label = fieldNameMap[input.name] || input.name || input.id || input.getAttribute('aria-label') || 'unknown'; 1414 failedFields.push(label); 1415 } 1416 }); 1417 1418 // BHPS form: each disclaimer question (radio group) is required too. 1419 row.querySelectorAll('.disclaimer-group').forEach((group) => { 1420 const validGroup = validateDisclaimerGroup(group, true); 1421 if (!validGroup) { 1422 isValid = false; 1423 const label = group.querySelector('legend')?.firstChild?.textContent?.trim() || 'disclaimer question'; 1424 failedFields.push(label); 1425 } 1426 }); 1427 1428 if (!isValid) { 1429 trackFormError({ campaignId, formName, formError: failedFields }); 1430 return; 1431 } 1432 1433 row.dataset.submitting = 'true'; 1434 submitBtn.disabled = true; 1435 loaderEl?.classList.remove('d-none'); 1436 1437 submitLeadFromForm(row, { 1438 resourcePath: row.getAttribute('data-attr-resourcePath') || formNodePath || window.location.pathname || '', 1439 campaignId, 1440 selectors: { 1441 name: '#enquire-name', 1442 email: '#enquire-email', 1443 mobile: '#enquire-mobile', 1444 pincode: '#enquire-pincode', 1445 }, 1446 }) 1447 .then(() => { 1448 setFormFieldsToCookie(row); 1449 showThankYouScreen(block, row); 1450 trackFormSubmission(row); 1451 }) 1452 .catch((err) => { 1453 // eslint-disable-next-line no-console 1454 console.error('Default form submission failed', err); 1455 showErrorScreen(block); 1456 }) 1457 .finally(() => { 1458 row.dataset.submitting = 'false'; 1459 submitBtn.disabled = false; 1460 loaderEl?.classList.add('d-none'); 1461 }); 1462 }); 1463} 1464 1465/** 1466 * Render form block 1467 */ 1468function renderForm(block) { 1469 extractFormConfig(block); 1470 1471 const rows = [...block.children]; 1472 if (rows.length < 3) return; 1473 1474 const descriptionRow = rows[0]; 1475 const labelsRow = rows[1]; 1476 const inputsRow = rows[2]; 1477 1478 if (!descriptionRow || !labelsRow || !inputsRow) return; 1479 1480 const descriptionCells = [...descriptionRow.children]; 1481 const labelCells = [...labelsRow.children]; 1482 const inputCellsSource = [...inputsRow.children]; 1483 1484 // Row 5, col 1 â form-consent-note rich text (replaces the hardcoded 1485 // default), and hide the row. 1486 const consentNoteHtml = extractConsentNoteHtml(rows[4]); 1487 if (rows[4]) rows[4].style.display = 'none'; 1488 1489 // Row 6 (col 2) â sets the module-level isBhpsForm flag. 1490 extractIsBhpsForm(rows[5]); 1491 1492 // Consent rows now come from authoring. Row 6 (the isBhpsForm flag, 1493 // handled above) sits between the whatsapp row and the construction/ 1494 // local-painter rows, so this is no longer a contiguous slice. 1495 const consentRows = [rows[3], rows[6], rows[7]]; 1496 const consentConfig = getConsentConfig(consentRows); 1497 1498 // Rows 9â10: BHPS disclaimer questions (question text, bullet-list of 1499 // radio options, display/hide) â only meaningful when isBhpsForm is 1500 // true, but harmless to read/hide otherwise. 1501 const disclaimerConfig = { 1502 questionOne: getDisclaimerQuestionConfig(rows[8]), 1503 questionTwo: getDisclaimerQuestionConfig(rows[9]), 1504 }; 1505 [rows[8], rows[9]].forEach((row) => { 1506 if (row) row.style.display = 'none'; 1507 }); 1508 1509 // Extract error images from row 11 1510 if (rows.length >= 11) { 1511 const errorImagesRow = rows[10]; 1512 const errorImagesCells = [...errorImagesRow.children]; 1513 1514 const desktopImgEl = errorImagesCells[0]?.querySelector('img'); 1515 const desktopLinkEl = errorImagesCells[0]?.querySelector('a'); 1516 const desktopErrorImage = desktopImgEl?.src || desktopLinkEl?.href || errorImagesCells[0]?.textContent?.trim(); 1517 1518 if (desktopErrorImage) { 1519 ERROR_DESKTOP_IMAGE = desktopErrorImage; 1520 } 1521 1522 const mobileImgEl = errorImagesCells[1]?.querySelector('img'); 1523 const mobileLinkEl = errorImagesCells[1]?.querySelector('a'); 1524 const mobileErrorImage = mobileImgEl?.src || mobileLinkEl?.href || errorImagesCells[1]?.textContent?.trim(); 1525 1526 if (mobileErrorImage) { 1527 ERROR_MOBILE_IMAGE = mobileErrorImage; 1528 } 1529 1530 errorImagesRow.style.display = 'none'; 1531 } 1532 1533 // Rows 12â20: error popup CTA/download links, thank-you (variant 2) images, 1534 // thank-you CTA label/banner title, and thank-you videos/CTA link 1535 extractFormExtraLinks(rows); 1536 1537 descriptionRow.style.display = 'none'; 1538 labelsRow.style.display = 'none'; 1539 inputsRow.style.display = 'none'; 1540 1541 consentRows.forEach((row) => { 1542 if (row) row.style.display = 'none'; 1543 }); 1544 1545 const formRow = document.createElement('div'); 1546 formRow.className = 'form-row form-attributes'; 1547 formRow.setAttribute('data-attr-resourcePath', formNodePath || window.location.pathname || ''); 1548 1549 const descriptionCell = document.createElement('div'); 1550 descriptionCell.className = 'form-description-cell'; 1551 populateDescriptionCell(descriptionCell, descriptionCells); 1552 1553 const fieldsArea = document.createElement('div'); 1554 fieldsArea.className = 'form-fields-area d-flex flex-column'; 1555 1556 const inputsGrid = document.createElement('div'); 1557 inputsGrid.className = 'form-inputs-grid'; 1558 1559 for (let i = 0; i < 4; i += 1) { 1560 const labelCell = labelCells[i]; 1561 const inputSourceCell = inputCellsSource[i]; 1562 const content = inputSourceCell?.textContent.trim(); 1563 1564 if (!content) continue; 1565 1566 const inputCell = document.createElement('div'); 1567 inputCell.className = 'form-input-cell'; 1568 1569 const headerText = labelCell ? labelCell.textContent.trim() : ''; 1570 createDefaultField(inputCell, 2, i, headerText, content); 1571 1572 const input = inputCell.querySelector('.form-input'); 1573 1574 if (input && labelCell) { 1575 applyFieldConfig(input, labelCell, content, block); 1576 } 1577 1578 inputsGrid.appendChild(inputCell); 1579 } 1580 1581 const buttonCell = document.createElement('div'); 1582 buttonCell.className = 'form-button-cell'; 1583 1584 const buttonLabelCell = labelCells[4]; 1585 const buttonLabel = buttonLabelCell?.textContent.trim(); 1586 1587 if (buttonLabel) { 1588 const button = document.createElement('button'); 1589 button.type = 'button'; 1590 button.className = 'form-submit btn'; 1591 button.innerHTML = `
1592 <span class="form-submit__label">${buttonLabel}</span> 1593 <span class="form-submit__arrow" aria-hidden="true"></span> 1594 <span class="rotating d-none" aria-hidden="true">â»</span> 1595 `; 1596 buttonCell.appendChild(button); 1597 } 1598 1599 if (!inputsGrid.children.length) return; 1600 1601 fieldsArea.appendChild(inputsGrid); 1602 1603 const consentArea = createConsentArea(consentConfig, disclaimerConfig, consentNoteHtml); 1604 fieldsArea.appendChild(consentArea); 1605 1606 // Check WhatsApp by default only if the field is visible 1607 const whatsappCheckbox = consentArea.querySelector('[name="updateMeOnWhatsapp"]'); 1608 const whatsappWrapper = whatsappCheckbox?.closest('.form-consent-item'); 1609 if (whatsappCheckbox && whatsappWrapper && !whatsappWrapper.classList.contains('d-none')) { 1610 whatsappCheckbox.checked = true; 1611 whatsappCheckbox.classList.toggle('whatsAppCheckbox-checked'); 1612 } 1613 1614 if (buttonCell.children.length) { 1615 fieldsArea.appendChild(buttonCell); 1616 } 1617 1618 // BHPS form: the consent note lives inside .form-consent-area while the 1619 // submit button is a separate sibling, so they stack on their own lines 1620 // by default. Pull the note out and pair it with the button in a shared 1621 // row so they can sit side by side on desktop (see .form-bhps-footer). 1622 if (isBhpsForm) { 1623 const consentNote = consentArea.querySelector('.form-consent-note'); 1624 if (consentNote && buttonCell.children.length) { 1625 const footerRow = document.createElement('div'); 1626 footerRow.className = 'form-bhps-footer'; 1627 fieldsArea.insertBefore(footerRow, buttonCell); 1628 footerRow.appendChild(consentNote); 1629 footerRow.appendChild(buttonCell); 1630 } 1631 } 1632 1633 formRow.appendChild(descriptionCell); 1634 formRow.appendChild(fieldsArea); 1635 1636 formRow.addEventListener('focusin', function (e) { 1637 if (!formStarted && e.target.tagName === 'INPUT') { 1638 formStarted = true; 1639 trackEvent("Form start", { 1640 "formName": formName, 1641 "campaignId": campaignId 1642 }); 1643 // Mirror to marketing pixels via image beacons â no SDK library load 1644 firePixelConversion('form_start', { 1645 event_category: formName, 1646 campaign: campaignId, 1647 }); 1648 } 1649 }); 1650 1651 // Spec: form_start must fire when the user clicks ANYWHERE on the form 1652 // to start (not only when an input gains focus). Guarded by the same 1653 // formStarted flag so it never double-fires with the focusin handler. 1654 formRow.addEventListener('click', function () { 1655 if (!formStarted) { 1656 formStarted = true; 1657 trackEvent("Form start", { 1658 "formName": formName, 1659 "campaignId": campaignId 1660 }); 1661 firePixelConversion('form_start', { 1662 event_category: formName, 1663 campaign: campaignId, 1664 }); 1665 } 1666 }); 1667 1668 block.appendChild(formRow); 1669 1670 if (buttonCell.children.length) { 1671 bindSubmitHandler(block, formRow, buttonCell); 1672 } 1673 1674 // Prefill form fields from cookies 1675 checkCookieValues(formRow); 1676 formRow.querySelectorAll('.form-input').forEach((input) => { 1677 if (input.value.trim()) { 1678 input.closest('.form-field')?.classList.add('show-label'); 1679 input.closest('.form-field')?.classList.add('has-value'); 1680 } 1681 }); 1682} 1683 1684export default function decorate(block) { 1685 applyBootstrapHelpers(block); 1686 renderForm(block); 1687}
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.