PageSourceSearch

https://www.asianpaints.com/eds/blocks/form/form.js

js asianpaints.com collected 2026-09-25 09:21:28 UTC 55,503 bytes, 1,687 lines download raw bytes

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    '&': '&amp;',
680    '<': '&lt;',
681    '>': '&gt;',
682    '"': '&quot;',
683    "'": '&#39;',
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 = '&times;';
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.