diff --git a/hlx_statics/blocks/marketo/marketo.css b/hlx_statics/blocks/marketo/marketo.css new file mode 100644 index 00000000..c753b0eb --- /dev/null +++ b/hlx_statics/blocks/marketo/marketo.css @@ -0,0 +1 @@ +body .marketo-form-wrapper { display: flex; flex-direction: column; width: calc(100% - 40px); max-width: none; margin: 0 auto; } body .marketo-form-wrapper form.mktoForm { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; width: 100%; margin-bottom: 0; padding-bottom: 0; } body .marketo-form-wrapper form.mktoForm .mktoFormRow:not(.mktoHidden), body .marketo-form-wrapper form.mktoForm .mktoFormCol:not(.mktoHidden) { display: contents; } body .marketo-form-wrapper form.mktoForm .mktoHidden { display: none; } body .marketo-form-wrapper form.mktoForm .mktoFormRow::before, body .marketo-form-wrapper form.mktoForm .mktoFormRow::after, body .marketo-form-wrapper form.mktoForm .mktoFormCol::before, body .marketo-form-wrapper form.mktoForm .mktoFormCol::after { display: none; content: none; } body .marketo-form-wrapper form.mktoForm .mktoClear, body .marketo-form-wrapper form.mktoForm .mktoOffset, body .marketo-form-wrapper form.mktoForm .mktoGutter, body .marketo-form-wrapper form.mktoForm .mktoButtonRow, body .marketo-form-wrapper form.mktoForm input[type="hidden"], body .marketo-form-wrapper form.mktoForm .mktoHtmlText:empty, body .marketo-form-wrapper form.mktoForm .mktoPlaceholder, body .marketo-form-wrapper form.mktoForm .mktoFormRow.zombie-row { position: absolute; visibility: hidden; width: 0; height: 0; overflow: hidden; margin: 0; padding: 0; } body .marketo-form-wrapper form.mktoForm .mktoFieldWrap { display: flex; flex-direction: column; width: 100%; margin: 0; } body .marketo-form-wrapper form.mktoForm .mktoFieldWrap:not(:has(.mktoField:not([type="hidden"]))) { display: none; } body .marketo-form-wrapper form.mktoForm .mktoLabel { display: block; font-size: 14px; font-weight: normal; color: #464646; margin-bottom: 4px; width: auto; } body .marketo-form-wrapper form.mktoForm .mktoAsterix { display: none; } body .marketo-form-wrapper form.mktoForm .mktoRequiredField .mktoLabel::after { content: " *"; } body .marketo-form-wrapper form.mktoForm .mktoField { width: 100%; border: 1px solid var(--color-grey-300, #909090); border-radius: 4px; padding: 10px 12px; font-size: 16px; line-height: 24px; height: auto; min-height: 44px; color: var(--color-black, #333); background-color: var(--color-white, #fff); transition: border-color 0.2s ease; box-sizing: border-box; } body .marketo-form-wrapper form.mktoForm .mktoField:focus { outline: none; border-color: var(--color-aem-blue, #1473e6); } body .marketo-form-wrapper form.mktoForm .mktoField.mktoInvalid { border-color: var(--color-status-error, #d0021b); } body .marketo-form-wrapper .use-case-textarea { height: 120px; resize: vertical; } body .marketo-form-wrapper .custom-submit { display: flex; align-items: center; justify-content: center; gap: 12px; width: 100%; max-width: 280px; height: 48px; padding: 12px 24px; border-radius: 24px; background-color: var(--color-aem-blue, #1473e6); color: var(--color-white, #fff); font-weight: 700; font-size: 15px; line-height: 24px; cursor: pointer; transition: all 0.2s ease; border: none; } body .marketo-form-wrapper .custom-submit:hover { background-color: var(--color-aem-blue-hover, #0054b6); box-shadow: 0 4px 12px rgba(20, 55, 100, 0.2); } body .marketo-form-wrapper .custom-submit:active { background-color: var(--color-aem-blue-active, #004494); } body .marketo-form-wrapper .custom-submit:disabled { background-color: var(--color-grey-300, #c8c8c8); cursor: not-allowed; } body .marketo-form-wrapper .success-message { display: none; padding: 24px; background-color: var(--color-status-success-bg, #e6f4e6); border: 1px solid var(--color-status-success, #2e7d32); border-radius: 8px; text-align: center; } body .marketo-form-wrapper .success-message.show { display: block; } body .marketo-form-wrapper form.mktoForm .mktoButtonRow { display: none; } body .marketo-form-wrapper form.mktoForm .mktoFormRow.by-supplyingmycontac, body .marketo-form-wrapper form.mktoForm .mktoFormRow.adobe-privacy, body .marketo-form-wrapper form.mktoForm .mktoFormRow:has(.mktoHtmlText) { display: none; } body .marketo-form-wrapper form.mktoForm .mktoErrorMsg, body .marketo-form-wrapper form.mktoForm .mktoError, body .marketo-form-wrapper .custom-field-error { color: var(--color-status-error, #d0021b); } body .marketo-form-wrapper form.mktoForm .mktoFormRow.mktoCleanedScript, body .marketo-form-wrapper form.mktoForm .mktoHtmlText span { display: none; } \ No newline at end of file diff --git a/hlx_statics/blocks/marketo/marketo.js b/hlx_statics/blocks/marketo/marketo.js new file mode 100644 index 00000000..423c9114 --- /dev/null +++ b/hlx_statics/blocks/marketo/marketo.js @@ -0,0 +1 @@ +import { readBlockConfig } from '../../scripts/lib-helix.js'; export default async function decorate(block) { const authoredConfig = readBlockConfig(block); let configData = { marketoConfig: { form_config: {}, demo_ux: {}, form_architecture: {}, }, customFields: [], submitButton: null, successRedirect: null, successMessage: null, consentPosition: 'after-button' }; let fetchUrl = null; if (authoredConfig['form-data']) { try { const url = new URL(authoredConfig['form-data'], window.location.href); fetchUrl = url.pathname; } catch (e) { fetchUrl = authoredConfig['form-data']; } if (!fetchUrl.endsWith('.json')) { fetchUrl += '.json'; } } try { const resp = await fetch(fetchUrl); if (resp.ok) { const json = await resp.json(); if (json && json.data) { const blockConfig = {}; json.data.forEach(row => { const key = row['form-data'] || row['Key'] || row['key']; const value = row['value'] || row['Value']; if (key) { blockConfig[key] = value; } }); const safeParse = (str, fallback) => { if (!str) return fallback; try { const cleaned = str.trim().replace(/[“”]/g, '"').replace(/[‘’]/g, "'"); return JSON.parse(cleaned); } catch (e) { console.error("Failed to parse JSON for config:", str, e); return fallback; } }; if (blockConfig['form_id_base']) configData.marketoConfig.form_config.form_id_base = parseInt(blockConfig['form_id_base'], 10); if (blockConfig['form_id_program']) configData.marketoConfig.form_config.form_id_program = parseInt(blockConfig['form_id_program'], 10); if (blockConfig['poi']) configData.marketoConfig.form_config.poi = blockConfig['poi']; if (blockConfig['campaignIds']) configData.marketoConfig.form_config.campaignIds = safeParse(blockConfig['campaignIds'], {}); if (blockConfig['copartnernames'] !== undefined) configData.marketoConfig.form_config.copartnernames = blockConfig['copartnernames']; if (blockConfig['success']) configData.marketoConfig.form_config.success = safeParse(blockConfig['success'], {}); if (blockConfig['templateOverrides']) configData.marketoConfig.form_config.templateOverrides = safeParse(blockConfig['templateOverrides'], {}); if (blockConfig['prefillFields']) configData.marketoConfig.form_config.prefillFields = safeParse(blockConfig['prefillFields'], {}); if (blockConfig['demo_ux']) configData.marketoConfig.demo_ux = safeParse(blockConfig['demo_ux'], {}); if (blockConfig['form_architecture']) configData.marketoConfig.form_architecture = safeParse(blockConfig['form_architecture'], {}); if (blockConfig['additional-fields']) { configData.customFields = safeParse(blockConfig['additional-fields'], []); } if (blockConfig['submitButton']) { configData.submitButton = safeParse(blockConfig['submitButton'], null); } if (blockConfig['consentPosition']) { configData.consentPosition = blockConfig['consentPosition']; } const successConfig = configData.marketoConfig.form_config.success; console.log("Parsed successConfig:", successConfig); if (successConfig) { if (successConfig.type === 'redirect') { configData.successRedirect = successConfig.content; } else if (successConfig.type === 'message') { configData.successMessage = successConfig.content; } console.log("Set configData.successMessage to:", configData.successMessage); } if (blockConfig['custom-css']) { const style = document.createElement('style'); style.innerHTML = blockConfig['custom-css']; document.head.append(style); } } } } catch (err) { console.error("Failed to fetch marketo config from:", fetchUrl, err); } block.innerHTML = ''; const wrapper = document.createElement('section'); wrapper.className = 'marketo-form-wrapper'; block.append(wrapper); function getEnvironment() { const host = window.location.host; if (host.indexOf('local') >= 0 || host.indexOf('developer-stage') >= 0) { return 'stage'; } else if (host.indexOf('developer.adobe.com') >= 0) { return 'prod'; } } const env = getEnvironment(); sessionStorage.removeItem('mktoPreFillFields'); if (typeof window.loadMarketoForm !== 'function') { try { await new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = '/hlx_statics/scripts/marketo-form-loader.js'; script.onload = resolve; script.onerror = reject; document.head.append(script); }); } catch (err) { console.error('Failed to load marketo-form-loader.js', err); return; } } loadMarketoForm(configData.marketoConfig).then((form) => { const faasHeader = document.querySelector(".faas-header"); if (faasHeader) { faasHeader.style.display = "flex"; } const formEl = form.getFormElem && form.getFormElem()[0]; if (formEl) { formEl.setAttribute('autocomplete', 'off'); form.vals({ FirstName: '', LastName: '', Email: '', Phone: '', mktoFormsCompany: '', Website: '', Country: '', State: '', PostalCode: '', mktoCompanySize: '', Industry: '', mktoFormsJobTitle: '', mktoFormsFunctionalArea: '', }); const selectColorStyle = document.createElement('style'); selectColorStyle.textContent = ` body form.mktoForm .mktoFormRow.mktoCleanedScript, body form.mktoForm .mktoHtmlText span { display: none; } `; document.head.appendChild(selectColorStyle); document.addEventListener('change', (e) => { if (e.target.tagName !== 'SELECT' || !formEl.contains(e.target)) return; const name = e.target.name; const color = e.target.value ? '#2c2c2c' : '#959595'; const existing = selectColorStyle.textContent; const formId = formEl.id || 'mktoForm'; const rule = `body #${formId} select[name="${name}"] { color: ${color}; }`; const replaced = existing.replace(new RegExp(`#${formId} select\\[name="${name}"\\][^}]+}`, 'g'), ''); selectColorStyle.textContent = replaced + rule; }, true); setTimeout(() => { const consentRow = formEl.querySelector('.mktoFormRow.by-supplyingmycontac'); if (consentRow) consentRow.style.display = 'none'; const legend = consentRow && consentRow.querySelector('legend'); const container = formEl.parentNode; const after = formEl.nextSibling; const customInputs = []; (configData.customFields || []).forEach((field, index) => { const id = field.id || field.name || `custom-field-${index}`; field.id = id; const fieldDiv = document.createElement('div'); fieldDiv.className = 'aem-injected-element'; fieldDiv.style.cssText = field.wrapperCss || 'margin-top: 16px;'; let html = ''; if (field.label) { const labelCss = field.labelCss || 'display:block;font-size:14px;font-weight:700;margin-bottom:4px;'; html += `${field.label} ${field.required ? '*' : ''}`; } const inputCss = field.inputCss || 'width:100%;box-sizing:border-box;font-size:16px;padding:8px;border:1px solid #6e6e6e;border-radius:4px;'; if (field.type === 'textarea') { html += ``; } else { html += ``; } fieldDiv.innerHTML = html; container.insertBefore(fieldDiv, after); customInputs.push(field); }); let submitDiv = null; if (configData.submitButton) { submitDiv = document.createElement('div'); submitDiv.className = 'aem-injected-element'; if (configData.submitButton.wrapperCss) submitDiv.style.cssText = configData.submitButton.wrapperCss; submitDiv.innerHTML = `${configData.submitButton.text || 'Submit'}`; } const consentDiv = document.createElement('div'); consentDiv.className = 'aem-injected-element'; consentDiv.style.cssText = 'font-size:12px; color:#444; line-height:1.5; margin: 0;'; if (legend) consentDiv.innerHTML = legend.innerHTML; if (configData.consentPosition === 'before-button') { container.insertBefore(consentDiv, after); if (submitDiv) container.insertBefore(submitDiv, after); } else { if (submitDiv) container.insertBefore(submitDiv, after); container.insertBefore(consentDiv, after); } setInterval(() => { formEl.querySelectorAll('style').forEach(s => s.remove()); formEl.querySelectorAll('.mktoFormRow.mktoCleanedScript, .mktoFormRow.by-supplyingmycontac, .mktoFormRow.adobe-privacy').forEach(row => { row.remove(); }); formEl.querySelectorAll('*').forEach(el => { if (el.style) { el.style.removeProperty('margin'); el.style.removeProperty('margin-bottom'); el.style.removeProperty('margin-top'); el.style.removeProperty('margin-left'); el.style.removeProperty('margin-right'); el.style.removeProperty('min-height'); } }); const structuralEls = formEl.querySelectorAll('.mktoFormRow, .mktoFormCol, .mktoFieldWrap, .mktoLabel'); structuralEls.forEach(el => { if (el.style) { el.style.removeProperty('width'); el.style.removeProperty('padding'); el.style.removeProperty('padding-left'); el.style.removeProperty('padding-right'); el.style.removeProperty('clear'); el.style.removeProperty('height'); if (el.style.display && el.style.display !== 'none') { el.style.removeProperty('display'); } } }); if (formEl.style) { formEl.style.removeProperty('width'); formEl.style.removeProperty('margin'); formEl.style.removeProperty('margin-bottom'); formEl.style.removeProperty('padding'); formEl.style.removeProperty('padding-bottom'); formEl.style.removeProperty('min-height'); } formEl.querySelectorAll('.mktoFormRow').forEach(row => { if (row.querySelector('.mktoHtmlText')) { row.remove(); } }); formEl.querySelectorAll('.mktoFormRow').forEach(row => { const wraps = row.querySelectorAll('.mktoFieldWrap'); let hasVisibleField = false; wraps.forEach(wrap => { const style = window.getComputedStyle(wrap); if (style.position !== 'absolute' && style.display !== 'none' && style.visibility !== 'hidden') { hasVisibleField = true; } }); if (wraps.length > 0 && !hasVisibleField) { row.classList.add('zombie-row'); } else { row.classList.remove('zombie-row'); } }); formEl.querySelectorAll('.mktoFieldWrap').forEach(wrap => { const input = wrap.querySelector('.mktoField'); let shouldHide = false; if (input) { const style = window.getComputedStyle(input); if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0' || input.type === 'hidden') { shouldHide = true; } } else { const htmlText = wrap.querySelector('.mktoHtmlText'); if (!htmlText || htmlText.innerHTML.trim() === '') { shouldHide = true; } } if (shouldHide) { wrap.style.position = 'absolute'; wrap.style.visibility = 'hidden'; wrap.style.width = '0'; wrap.style.height = '0'; wrap.style.overflow = 'hidden'; wrap.style.margin = '0'; wrap.style.padding = '0'; } else { if (wrap.style.position === 'absolute') { wrap.style.removeProperty('position'); wrap.style.removeProperty('visibility'); wrap.style.removeProperty('width'); wrap.style.removeProperty('height'); wrap.style.removeProperty('overflow'); wrap.style.removeProperty('margin'); wrap.style.removeProperty('padding'); } } }); }, 500); const customSubmitBtn = document.getElementById('custom-submit'); function showFieldErr(field, msg) { const id = 'err-' + field.name; field.style.borderColor = '#d0021b'; const existing = document.getElementById(id); if (existing) { existing.textContent = msg || 'This field is required.'; return; } const err = document.createElement('div'); err.id = id; err.className = 'custom-field-error'; err.style.cssText = 'color:#d0021b; font-size:12px; margin-top:4px; display:block; clear:both; width:100%;'; err.textContent = msg || 'This field is required.'; const wrap = field.closest('.mktoFieldWrap'); if (wrap) { wrap.appendChild(err); } else { field.parentNode.appendChild(err); } } function clearFieldErr(field) { field.style.removeProperty('border-color'); const err = document.getElementById('err-' + field.name); if (err) err.remove(); } if (customSubmitBtn) { customSubmitBtn.addEventListener('click', (e) => { e.preventDefault(); let allValid = true; const hiddenFields = {}; formEl.querySelectorAll('.mktoRequired').forEach(field => { if (field.offsetWidth === 0 && field.offsetHeight === 0) { if (!field.value.trim()) { if (field.type === 'email' || field.name === 'Email') { field.value = 'dummy@example.com'; } else if (field.name === 'Country') { field.value = 'US'; } else if (field.tagName === 'SELECT' && field.options.length > 1) { field.value = field.options[1].value; } else { field.value = 'dummy'; } } return; } if (!field.value.trim()) { showFieldErr(field); allValid = false; } else if (field.type === 'email' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(field.value.trim())) { showFieldErr(field, 'Must be a valid email address.'); allValid = false; } else if (field.name === 'Phone' && !/^\+?[\d\s\-().]{7,20}$/.test(field.value.trim())) { showFieldErr(field, 'Please enter a valid phone number.'); allValid = false; } else { clearFieldErr(field); } }); customInputs.forEach(field => { const inputEl = document.getElementById(field.id); if (!inputEl) return; let isValid = true; if (field.required && !inputEl.value.trim()) { isValid = false; allValid = false; } if (!isValid) { inputEl.style.borderColor = '#d0021b'; let err = document.getElementById(field.id + '-error'); if (!err) { err = document.createElement('div'); err.id = field.id + '-error'; err.className = 'custom-field-error'; err.style.cssText = field.errorCss || 'color:#d0021b; font-size:12px; margin-top:4px;'; err.textContent = field.errorText || 'This field is required.'; inputEl.after(err); } } else { inputEl.style.borderColor = '#6e6e6e'; const err = document.getElementById(field.id + '-error'); if (err) err.remove(); const marketoName = field.name || field.marketoField; if (marketoName) { hiddenFields[marketoName] = inputEl.value; } } }); if (allValid) { if (Object.keys(hiddenFields).length > 0) { form.addHiddenFields(hiddenFields); } formEl.querySelector('.mktoButton').click(); } }); } }, 500); } form.onValidate(function (valid) { console.log("Marketo form onValidate triggered. valid:", valid); if (!valid) { console.log("Marketo built-in validation failed! The following Marketo fields are invalid:"); const invalidEls = form.getFormElem()[0].querySelectorAll('.mktoInvalid'); invalidEls.forEach(el => { console.log("- Field name:", el.name, "| id:", el.id, "| value:", el.value); }); } let customValid = true; let firstInvalid = null; (configData.customFields || []).forEach(field => { const inputEl = document.getElementById(field.id); if (field.required && inputEl && !inputEl.value.trim()) { customValid = false; if (!firstInvalid) firstInvalid = inputEl; } }); console.log("customValid:", customValid); if (valid && customValid) { console.log("Form is submittable (valid & customValid true)"); form.submittable(true); } else { console.log("Form is NOT submittable. valid:", valid, "customValid:", customValid); form.submittable(false); if (firstInvalid) { firstInvalid.focus(); } } }); form.onSubmit(function (form) { console.log("Marketo form onSubmit triggered! Validation passed, sending data..."); }); form.onSuccess(function () { console.log("Marketo form onSuccess triggered."); console.log("configData.successRedirect:", configData.successRedirect); if (configData.successRedirect) { top.location.href = configData.successRedirect; return false; } document.querySelectorAll('.aem-injected-element').forEach(el => el.style.display = 'none'); console.log("Falling back to Marketo default success behavior."); }); }).catch((error) => { console.error("Failed to load Marketo form:", error); }); } \ No newline at end of file diff --git a/hlx_statics/scripts/marketo-form-loader.js b/hlx_statics/scripts/marketo-form-loader.js new file mode 100644 index 00000000..17341633 --- /dev/null +++ b/hlx_statics/scripts/marketo-form-loader.js @@ -0,0 +1,766 @@ +/** + * Marketo form loader — v3 (module-aware). + * Exposes loadMarketoForm() globally. Call it from your page after IMS is ready. + * ========================================================================= */ + +// Demo call removed — loadMarketoForm() is called from ica-faas.html after IMS is ready. +/** + * Was OK as of 2026-07-22, + * but milo could have changed depending on how much time has passed, + * so you should check business.adobe.com for the latest stucture of the form, and the CSS rules. + * + * Build the form-host snippet from a dynamic title + paragraphs. The `.section` + * scaffolding and the `.marketo-form-wrapper` host are fixed (the wrapper must match + * the default formDestinationElement); only the heading and body copy vary. + * + * Text is escaped, so an apostrophe is safe but inline markup (links, ) is + * NOT rendered. If you need HTML in the copy, say so and I'll add a raw/allow-html mode. + * + * @param {string} [opts.title] heading text (omitted if falsy) + * @param {string|string[]} [opts.paragraphs] one string or an array of strings + */ +function buildFormSnippet({ title = "", paragraphs = [] } = {}) { + const paras = (Array.isArray(paragraphs) ? paragraphs : [paragraphs]) + .filter((p) => p != null && String(p).trim() !== "") + .map((p) => `${escapeHtml(p)}`) + .join("\n "); + const heading = title ? `${escapeHtml(title)}` : ""; + + return ` + + + + + + + + ${heading} + ${paras} + + + + + + + + + + +`; +} + +/** + * + * + * + * + * + * + * You "shouldn't" need to change anything below this line. + * + * + * + * + * + * + * + * + * + * ========================================================================= + * MAIN + * ========================================================================= */ + +async function loadMarketoForm(config) { + if (!config || typeof config !== "object") { + throw new Error("Configuration object is required."); + } + + const form_config = config.form_config || {}; + const demo_ux = config.demo_ux || {}; + const form_architecture = config.form_architecture || {}; + + // demo_ux — HTML + CSS + placement (formCss is resolved via settings below). + const { + formDestinationElement, + snippetDestination = null, + title = null, // dynamic heading for the built snippet + paragraphs = null, // dynamic body copy: string or string[] + formHTMLsnippet = null, // full-override; wins over title/paragraphs + loadCss = true, // whether to inject formCss (only ever injected on business.adobe.com) + } = demo_ux; + + // form_config — data-layer behaviour (assembled into window.mcz_marketoForm_pref). + const { + form_id_base, // base form id (renders the page UI) + form_id_program, // if set: submission target; base form supplies the UI + success, // -> form.success + poi, // -> program.poi + campaignIds, // -> program.campaignids + copartnernames, // -> program.copartnernames + templateOverrides = null, // { template, purpose, field_visibility, field_filters, program_id, known_visitor, auto_success } + configureDataLayer = null, + prefillFields = window.mktoPreFillFields, + } = form_config; + + // form_architecture — instance identity + plumbing + lifecycle hooks. + const { + mktoFormJS = "/js/forms2/js/forms2.min.js", + formSubmitPath = "/index.php/leadCapture/save2", + previewMode = false, + timeout = 10000, + transformSpec = null, + onFormReady = null, + } = form_architecture; + + const settings = resolveSettings({ + munchkinId: form_architecture.munchkinId, + formId: form_id_base, // base form id (stageSettings.formId can override) + instanceDomain: form_architecture.instanceDomain, + formCss: demo_ux.formCss ?? null, + stage: form_architecture.stage ?? false, + stageSettings: form_architecture.stageSettings ?? null, + }); + const { munchkinId, formId, instanceDomain, formCss } = settings; + + if (!munchkinId || !formId || !instanceDomain) { + throw new Error("munchkinId, form_id_base, and instanceDomain are required."); + } + + // Dedup guard on the id we'll actually render under. Only plan the program id if it + // passes the same validity gate applied below (a number, distinct from the base). + const programRequested = Number.isInteger(form_id_program) && form_id_program !== form_id_base; + const plannedFormId = programRequested ? form_id_program : formId; + if (document.getElementById(`mktoForm_${plannedFormId}`)) { + console.debug(`Form mktoForm_${plannedFormId} DOM element already exists. Re-using it.`); + } + + // Optional preview/QA mode. Mutates the address bar AND turns on the module + // bundle's verbose + Lana logging (see marketo_form_setup_rules.js). Off by default. + let previewUrl = document.location.href; + if (previewMode) { + const u = new URL(window.location.href); + if (!u.searchParams.has("preview")) { + u.searchParams.set("preview", "1"); + window.history.replaceState({}, "", u.toString()); + } + previewUrl = u.toString(); + } + + // Inject the form CSS only when enabled AND the page is business.adobe.com. Elsewhere + // (foreign host, bare page) it doesn't try — host string inlined to stay hoisting-safe. + if (loadCss) { + const host = window.location.hostname; + const isAllowedHost = host === "business.adobe.com" || + host === "adobe.com" || + host.includes(".aem.") || + host.includes(".hlx.") || + host === "localhost" || + host === "127.0.0.1"; + if (isAllowedHost) { + injectStylesheetOnce(formCss); + } else { + console.info( + `Marketo form CSS not injected: host is "${host}", not an allowed production or dev host.` + ); + } + } + + // A full formHTMLsnippet override wins; otherwise build one from title/paragraphs. + const effectiveSnippet = + formHTMLsnippet || buildFormSnippet({ title: title || "", paragraphs: paragraphs || [] }); + + // Resolve (and if needed create) the destination element. + let destinationEl = document.querySelector(formDestinationElement); + if (!destinationEl && snippetDestination && effectiveSnippet) { + const host = document.querySelector(snippetDestination); + if (host) { + host.insertAdjacentHTML("afterbegin", effectiveSnippet); + destinationEl = document.querySelector(formDestinationElement); + } else { + console.warn(`Snippet destination '${snippetDestination}' not found.`); + } + } + if (!destinationEl) { + throw new Error(`Form destination element '${formDestinationElement}' not found.`); + } + + // Ensure the Marketo library is present. + if (!window?.MktoForms2?.processForm) { + const scriptUrl = mktoFormJS.startsWith('http') || mktoFormJS.startsWith('/') + ? mktoFormJS + : `https://${instanceDomain}${mktoFormJS}`; + await loadScriptOnce(scriptUrl, { timeout: 15000 }); + } + if (!window?.MktoForms2 || typeof window.MktoForms2.processForm !== "function") { + throw new Error("MktoForms2.processForm unavailable after loading the library."); + } + + // Fetch the base spec — this supplies the page UI (fields, layout, styling). + const spec = await fetchFormSpecJSONP({ + instanceDomain, munchkinId, formId, previewUrl, timeout, + }); + const baseFetchedId = spec.Id ?? formId; + + // Program-form override: the base form renders the UI, but the program form takes the submission. + let programFormId = null; + if (form_id_program !== undefined && form_id_program !== null) { + if (!Number.isInteger(form_id_program)) { + console.warn( + `form_id_program must be a number (got ${typeof form_id_program}: ${JSON.stringify(form_id_program)}). ` + + `Using base form ${baseFetchedId}.` + ); + } else if (form_id_program === form_id_base) { + console.warn( + `form_id_program (${form_id_program}) matches form_id_base; no override applied. Using base form ${baseFetchedId}.` + ); + } else { + try { + const programSpec = await fetchFormSpecJSONP({ + instanceDomain, munchkinId, formId: form_id_program, previewUrl, timeout, + }); + if (programSpec && (programSpec.Id || Array.isArray(programSpec.rows))) { + reportProgramFields(form_id_program, baseFetchedId, extractProgramFields(programSpec)); + programFormId = form_id_program; // config exists → override confirmed + } else { + console.warn( + `Program form ${form_id_program} returned no usable configuration. Using base form ${baseFetchedId}.` + ); + } + } catch (e) { + console.warn( + `Could not load program form ${form_id_program} configuration (${e.message}). Using base form ${baseFetchedId}.` + ); + } + } + } + + // Normalise identity/submit target. Override Id and Vid to the program id only when confirmed; otherwise keep the base id and its fetched version. + const effectiveFormId = programFormId || baseFetchedId; + spec.munchkinId = munchkinId; + spec.Id = effectiveFormId; + spec.Vid = programFormId ? programFormId : (spec.Vid ?? effectiveFormId); + spec.name = `mktoForm_${effectiveFormId}`; + spec.action = `https://${instanceDomain}${formSubmitPath}`; + + // Configure the data layer BEFORE render so the modules read our values. + const pref = buildDataLayer(config.form_config?.pref ?? window.mcz_marketoForm_pref, { + success, poi, campaignIds, copartnernames, + }); + window.mcz_marketoForm_pref = pref; + const ctx = { config, settings, pref, spec, effectiveFormId }; + + setDataLayerFormId(pref, effectiveFormId); // deterministic id for getMktoFormID() + applyTemplateOverrides(pref, templateOverrides, form_id_program); // form.template + if (typeof configureDataLayer === "function") configureDataLayer(pref, ctx); // your seam + + // Reconcile the spec's known-lead template with the now-final known_visitor state. + applyKnownVisitorToSpec(spec, pref); + + const finalSpec = typeof transformSpec === "function" + ? (transformSpec(spec, ctx) || spec) + : spec; + + // Mount and render, resolving with the live form instance. + mountFormShell({ destinationEl, formId: finalSpec.Id }); + MktoForms2.setOptions({ + rootUrl: `https://${instanceDomain}`, + baseUrl: `https://${instanceDomain}/js/forms2/`, + }); + + return new Promise((resolve, reject) => { + let renderTimer = null; + if (timeout > 0) { + renderTimer = setTimeout( + () => reject(new Error(`Form render timed out after ${timeout}ms.`)), + timeout + ); + } + + const onRendered = (formData) => { + try { + const form = MktoForms2.getForm(formData.Id); + if (!form) { + if (renderTimer) clearTimeout(renderTimer); + return reject(new Error("Could not retrieve form instance after processForm.")); + } + + // Add hidden fields for the munchkinId. + form.addHiddenFields({ munchkinId }); + + // Call the onFormReady hook if provided. + if (typeof onFormReady === "function") onFormReady(form, ctx); + + form.render(); + + // Prefill AFTER render so we can tell which fields exist. + applyPrefill(form, prefillFields); + + // Force mktoInstantInquiry to the create_inquiry control value and hold it there + // Only when explicitly provided. + // The default logic on the baseline form is always-always true which is odd, + // so this fixes that... + if (templateOverrides && typeof templateOverrides.create_inquiry === "boolean") { + lockFieldToControl(form, "mktoInstantInquiry", templateOverrides.create_inquiry); + } + + // Conditional State/province requirement (asterisk + block-on-submit) for the + // countries that need it. The Milo module bundle on business.adobe.com does not wire + // this the way the Marketo-LP baseline (mss.js) does, so we add it here. + applyConditionalStateRequirement(form); + + if (renderTimer) clearTimeout(renderTimer); + resolve(form); + } catch (e) { + if (renderTimer) clearTimeout(renderTimer); + reject(new Error(`Error finalising Marketo form: ${e.message}`)); + } + }; + + try { + MktoForms2.processForm(finalSpec, onRendered); + } catch (e) { + if (renderTimer) clearTimeout(renderTimer); + reject(new Error(`Error processing Marketo form: ${e.message}`)); + } + }); +} + +/** + * Conditional State/province requirement — mirrors the R3 baseline (mss.js:248 / 637): + * State is required ONLY for US, CA, IN, AU, NZ, MX, CN and optional for every other country. + * The Milo marketo block on business.adobe.com does not wire this, so we replicate it: + * - on country change + on load, add/remove the required asterisk (*) + * and the "mktoRequiredField" class on the State label for those countries; + * - block submit (onValidate) when State is empty for those countries and show the error. + * The State field must be present (field_visibility.state = "visible") for this to run. + */ +function applyConditionalStateRequirement(form) { + const STATE_REQUIRED = ["US", "CA", "IN", "AU", "NZ", "MX", "CN"]; + const formEl = (form.getFormElem && form.getFormElem()[0]) || null; + if (!formEl) return; + + const isRequired = () => { + const vals = (form.getValues && form.getValues()) || {}; + return STATE_REQUIRED.indexOf(String(vals.Country || "")) > -1; + }; + + const stateLabel = () => { + let lbl = formEl.querySelector("#LblState") || formEl.querySelector('label[for="State"]'); + if (!lbl) { + const s = formEl.querySelector("#State"); + const row = s && s.closest ? s.closest(".mktoFormRow") : null; + lbl = row ? row.querySelector("label") : null; + } + return lbl; + }; + + const refresh = () => { + const lbl = stateLabel(); + if (!lbl) return; + const container = lbl.parentNode; + if (isRequired()) { + if (container && container.classList) container.classList.add("mktoRequiredField"); + if (!lbl.querySelector(".mktoAsterix")) { + const a = document.createElement("div"); + a.className = "mktoAsterix"; + a.textContent = "*"; + lbl.insertBefore(a, lbl.firstChild); + } + } else { + if (container && container.classList) container.classList.remove("mktoRequiredField"); + const ex = lbl.querySelector(".mktoAsterix"); + if (ex && ex.parentNode) ex.parentNode.removeChild(ex); + } + }; + + // Re-evaluate whenever the country changes, and a few times after render (the State + // dropdown is injected asynchronously by the geopicker for state-bearing countries). + const countryEl = formEl.querySelector("#Country"); + if (countryEl) countryEl.addEventListener("change", () => setTimeout(refresh, 0)); + [0, 300, 800, 1500].forEach((ms) => setTimeout(refresh, ms)); + + // Enforce required-on-submit for those countries (Milo may not). + if (typeof form.onValidate === "function") { + form.onValidate(() => { + if (!isRequired()) return; + const vals = (form.getValues && form.getValues()) || {}; + if (!vals.State) { + if (form.submittable) form.submittable(false); + try { + const stateElem = form.getFormElem().find("#State"); + if (form.showErrorMessage) form.showErrorMessage("This field is required.", stateElem); + } catch (_) { /* showErrorMessage unavailable */ } + refresh(); + } + }); + } +} + +/** + * ========================================================================= + * MARKETO HELPERS + * ========================================================================= */ + +/** Load an external script once; resolves when ready, rejects on error/timeout. */ +function loadScriptOnce(src, { timeout = 15000 } = {}) { + const existing = document.querySelector(`script[src="${src}"]`); + if (existing && existing.dataset.loaded === "true") return Promise.resolve(); + + return new Promise((resolve, reject) => { + const script = existing || document.createElement("script"); + let timer = null; + const done = (fn, arg) => { if (timer) clearTimeout(timer); fn(arg); }; + + script.onload = () => { script.dataset.loaded = "true"; done(resolve); }; + script.onerror = (e) => + done(reject, new Error(`Failed to load script: ${src} (${e?.message || "unknown error"})`)); + + if (timeout > 0) { + timer = setTimeout( + () => done(reject, new Error(`Script load timed out after ${timeout}ms: ${src}`)), + timeout + ); + } + if (!existing) { script.src = src; document.head.appendChild(script); } + }); +} + +/** Inject a stylesheet once. Non-fatal on failure. */ +function injectStylesheetOnce(href) { + if (!href) return; + try { + if (document.querySelector(`link[href="${href}"]`)) return; + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = href; + document.head.appendChild(link); + } catch (e) { + console.warn("Failed to inject Marketo form CSS:", e); + } +} + +/** Minimal HTML escaping for text injected into the snippet via insertAdjacentHTML. */ +function escapeHtml(value) { + return String(value) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + + +/** + * Collapse stage/prod into one effective instance. + * When stage is on, fetch AND submit from the stage instance (munchkinId/formId/ + * instanceDomain all come from stageSettings). If you truly need fetch-prod / + * submit-stage, add an explicit submitInstanceDomain override instead. + */ +function resolveSettings(config) { + const base = { + munchkinId: config.munchkinId, + formId: config.formId, + instanceDomain: config.instanceDomain, + formCss: config.formCss ?? null, + isStage: false, + }; + if (config.stage && config.stageSettings) { + const s = config.stageSettings; + return { + ...base, + munchkinId: s.munchkinId ?? base.munchkinId, + formId: s.formId ?? base.formId, + instanceDomain: s.instanceDomain ?? base.instanceDomain, + formCss: s.formCss ?? base.formCss, + isStage: true, + }; + } + return base; +} + +/** Fetch the form spec from Marketo. */ +function fetchFormSpecJSONP({ instanceDomain, munchkinId, formId, previewUrl, timeout = 10000 }) { + return new Promise((resolve, reject) => { + const callbackName = + `marketo_jsonp_callback_${Date.now()}_${Math.floor(Math.random() * 1e5)}`; + const script = document.createElement("script"); + let timer = null; + + const cleanup = () => { + delete window[callbackName]; + if (script.parentNode) script.remove(); + if (timer) clearTimeout(timer); + }; + + window[callbackName] = (data) => { + cleanup(); + if (!data || typeof data !== "object") { + return reject(new Error("Received invalid data from Marketo form API.")); + } + resolve(data); + }; + + script.onerror = () => { + cleanup(); + reject(new Error("Failed to load Marketo form spec. Check network or script URL.")); + }; + + if (timeout > 0) { + timer = setTimeout(() => { + cleanup(); + reject(new Error(`Marketo form spec request timed out after ${timeout}ms.`)); + }, timeout); + } + + try { + const url = + `https://${instanceDomain}/index.php/form/getForm` + + `?munchkinId=${encodeURIComponent(munchkinId)}` + + `&form=${encodeURIComponent(formId)}` + + `&url=${encodeURIComponent(previewUrl || document.location.href)}` + + `&callback=${callbackName}` + + `&_=${Date.now()}`; + script.src = url; + document.body.appendChild(script); + } catch (e) { + cleanup(); + reject(new Error(`Failed to initiate Marketo form request: ${e.message}`)); + } + }); +} + +/** + * Ensure the element exists. + */ +function mountFormShell({ destinationEl, formId, suppressMarketoBaseCss = true }) { + if (document.getElementById(`mktoForm_${formId}`)) return; // already mounted + + if (suppressMarketoBaseCss && !document.getElementById("mktoForms2BaseStyle")) { + const span = document.createElement("span"); + span.id = "mktoForms2BaseStyle"; + destinationEl.appendChild(span); + } + + const formEl = document.createElement("form"); + formEl.id = `mktoForm_${formId}`; + destinationEl.appendChild(formEl); +} + +/** + * Extract program-specific fields from the form spec. + * Ignores the identity fields (FirstName/LastName/Email) and the htmltext carrier + * @returns {Array<{Name,InputLabel,Datatype,Maxlength}>} + */ +function extractProgramFields(spec) { + const rows = Array.isArray(spec?.rows) ? spec.rows : []; + const IGNORE = new Set(["FirstName", "LastName", "Email"]); + return rows + .flat() + .filter((f) => f && typeof f === "object" && typeof f.Name === "string") + .filter((f) => !IGNORE.has(f.Name) && f.Datatype !== "htmltext") + .map((f) => ({ + Name: f.Name, + InputLabel: f.InputLabel ?? "", + Datatype: f.Datatype ?? "", + Maxlength: f.Maxlength ?? "", + })); +} + +/** Console notice + table for the program-form overrides. */ +function reportProgramFields(programId, baseId, fields) { + console.log( + `Program Form ID detected (${programId}): this overrides the base form ID (${baseId}). ` + + `The base form renders the page UI; the program form receives the submission. ` + + `Note you have additional fields which can be captured specific to this program.` + ); + if (fields.length && typeof console.table === "function") { + console.table(fields); + } else if (!fields.length) { + console.log("No additional program-specific fields found beyond FirstName/LastName/Email."); + } +} + +/* ========================================================================= + * DATA-LAYER CONFIGURATION + * ========================================================================= */ + +/** Ensure the nested data-layer objects the modules expect exist. */ +function ensureDataLayer(pref) { + pref = pref || {}; + pref.form = pref.form || {}; + pref.profile = pref.profile || {}; + pref.program = pref.program || {}; + return pref; +} + +/** + * Assemble window.mcz_marketoForm_pref from the loader's friendly config options, + * augmenting any pre-existing global. + */ +function buildDataLayer(base, opts = {}) { + const pref = ensureDataLayer(base || {}); + const { form, program } = pref; + const { success, poi, campaignIds, copartnernames } = opts; + + if (success !== undefined) form.success = { ...(form.success || {}), ...success }; + if (poi !== undefined) program.poi = poi; + if (campaignIds !== undefined) program.campaignids = { ...(program.campaignids || {}), ...campaignIds }; + if (copartnernames !== undefined) program.copartnernames = copartnernames; + + return pref; +} + +/** + * Make the form id deterministic for the module bundle. + */ +function setDataLayerFormId(pref, formId) { + ensureDataLayer(pref).form.id = formId; +} + +/** Generate a timestamp for synthesized template names. */ +function yymmddhhmmss(d = new Date()) { + const p = (n) => String(n).padStart(2, "0"); + return ( + String(d.getFullYear()).slice(2) + + p(d.getMonth() + 1) + p(d.getDate()) + + p(d.getHours()) + p(d.getMinutes()) + p(d.getSeconds()) + ); +} + +/** + * Register a synthesized template in window.templateRules. + * @param {string} name - The name of the template. + * @param {object} ov - The template overrides. + */ +function registerTemplateRule(name, ov) { + if (!Array.isArray(window.templateRules)) window.templateRules = []; + if (window.templateRules.some((t) => Object.keys(t)[0] === name)) return; // already registered + + const arr = (v) => (Array.isArray(v) ? v : v == null || v === "" ? [] : [String(v)]); + const arrMap = (m) => (m && typeof m === "object" + ? Object.fromEntries(Object.entries(m).map(([k, v]) => [k, arr(v)])) + : {}); + + window.templateRules.push({ + [name]: { + purpose: arr(ov.purpose), + field_visibility: arrMap(ov.field_visibility), + field_filters: arrMap(ov.field_filters), + program_id: String(ov.program_id ?? ""), // runtime reads rule.program_id (.length) — keep a string + known_visitor: ov.known_visitor ?? false, + auto_success: ov.auto_success ?? false, + }, + }); +} + +/** + * Resolve templateOverrides into form.template. + * @param {object} pref window.mcz_marketoForm_pref + * @param {object} ov templateOverrides { template, purpose, field_visibility, field_filters, program_id, known_visitor, auto_success, create_inquiry } + * @param {number} [form_id_program] used to name the synthesized template (form_) + */ +function applyTemplateOverrides(pref, ov, form_id_program) { + if (!ov) return; + const dl = ensureDataLayer(pref); + + const named = ov.template != null && String(ov.template).trim() !== ""; + if (named) { + dl.form.template = ov.template; // existing template holds the config; ignore the rest + return; + } + + const progId = Number.isInteger(form_id_program) ? form_id_program : null; + const tempName = progId ? `form_${progId}` : `temp${yymmddhhmmss()}`; + dl.form.template = tempName; + dl.form.known_visitor = ov.known_visitor ?? false; // mirror for pre-render knownLead.template reconciliation + + // create_inquiry -> form.mktoInstantInquiry[] (subtype derived from purpose). + // The runtime reads mktoInstantInquiry[form.subtype], so key it by this form's subtype. + const subtype = ov.purpose != null + ? String(Array.isArray(ov.purpose) ? ov.purpose[0] : ov.purpose).split(":")[0] + : ""; + if (subtype) { + dl.form.mktoInstantInquiry = { + ...(dl.form.mktoInstantInquiry || {}), + [subtype]: ov.create_inquiry ?? false, + }; + } + + registerTemplateRule(tempName, ov); +} + +/** + * Reconcile the fetched Marketo spec's known-lead template with the effective known-visitor state. + */ +function applyKnownVisitorToSpec(spec, pref) { + const knownLead = spec?.ProcessOptions?.knownLead; + if (!knownLead) return; // this form spec carries no known-lead block + + const enabled = pref?.form?.known_visitor === true; + if (!enabled) { + knownLead.template = ""; // disabled -> clear per intent + return; + } + if (!knownLead.template) { + console.warn( + "known_visitor enabled but ProcessOptions.knownLead.template is empty on this " + + "form spec — the known-visitor form has nothing to render." + ); + } +} + +/** + * Prefill the rendered form. Fields already present get their values set (setValuesCoerced). + * Fields not on the form are added as hidden fields so they still submit. + */ +function applyPrefill(form, prefillFields) { + if (!prefillFields || typeof prefillFields !== "object") return; + const entries = Object.entries(prefillFields).filter(([, v]) => v !== undefined); + if (!entries.length) return; + + const formEl = (form.getFormElem && form.getFormElem()[0]) || null; + if (!formEl) { form.setValuesCoerced(Object.fromEntries(entries)); return; } // can't inspect DOM; best-effort + + const missing = {}; + for (const [name, value] of entries) { + if (!formEl.querySelector(`[name="${name}"]`)) missing[name] = value; + } + if (Object.keys(missing).length) form.addHiddenFields(missing); // create the absent fields + form.setValuesCoerced(Object.fromEntries(entries)); // now coerce-set them all +} + +/** + * Force a form field to a control value and hold it there, even against programmatic rewrites by the module bundle. + */ +function lockFieldToControl(form, name, value) { + const formEl = form.getFormElem && form.getFormElem()[0]; + if (!formEl) return; + const control = String(value); + + const lock = (input) => { + if (!input || input.dataset.controlLocked === "1") return; + input.setAttribute("data-control", control); + input.setAttribute("value", control); + try { + Object.defineProperty(input, "value", { + configurable: true, + get() { return control; }, + set() { /* ignore — locked to data-control */ }, + }); + } catch (_) { /* getter/setter unavailable; attribute + observer still enforce */ } + input.addEventListener("change", () => input.setAttribute("value", control)); + input.dataset.controlLocked = "1"; + }; + + const ensure = () => { + let inputs = formEl.querySelectorAll(`[name="${name}"]`); + if (!inputs.length) { + form.addHiddenFields({ [name]: value }); + inputs = formEl.querySelectorAll(`[name="${name}"]`); + } + inputs.forEach(lock); + }; + + ensure(); + new MutationObserver(ensure).observe(formEl, { childList: true, subtree: true }); +}
${escapeHtml(p)}