From d10b0c0ad5e9e9f881128cbc72fd460347e12998 Mon Sep 17 00:00:00 2001 From: petheanraj-mitrah Date: Mon, 31 Aug 2026 12:12:41 +0530 Subject: [PATCH 01/17] feat: implement Marketo form block and dynamic loader for external form data and configuration --- hlx_statics/blocks/marketo/marketo.css | 157 +++++ hlx_statics/blocks/marketo/marketo.js | 434 ++++++++++++ hlx_statics/scripts/marketo-form-loader.js | 766 +++++++++++++++++++++ 3 files changed, 1357 insertions(+) create mode 100644 hlx_statics/blocks/marketo/marketo.css create mode 100644 hlx_statics/blocks/marketo/marketo.js create mode 100644 hlx_statics/scripts/marketo-form-loader.js diff --git a/hlx_statics/blocks/marketo/marketo.css b/hlx_statics/blocks/marketo/marketo.css new file mode 100644 index 00000000..0c3c3bb1 --- /dev/null +++ b/hlx_statics/blocks/marketo/marketo.css @@ -0,0 +1,157 @@ +.marketo-form-wrapper { + display: flex; + flex-direction: column; + width: calc(100% - 40px) !important; + max-width: none !important; + margin: 0 auto !important; +} + +.marketo-form-wrapper .mktoForm { + display: grid !important; + grid-template-columns: 1fr 1fr !important; + gap: 16px !important; + width: 100% !important; + margin-bottom: 0 !important; + padding-bottom: 0 !important; +} + +.marketo-form-wrapper .mktoFormRow:not(.mktoHidden), +.marketo-form-wrapper .mktoFormCol:not(.mktoHidden) { + display: contents !important; +} + +.marketo-form-wrapper .mktoHidden { + display: none !important; +} + +.marketo-form-wrapper .mktoClear, +.marketo-form-wrapper .mktoOffset, +.marketo-form-wrapper .mktoGutter, +.marketo-form-wrapper .mktoHtmlText:empty { + display: none !important; +} + +.marketo-form-wrapper .mktoFieldWrap { + display: flex !important; + flex-direction: column !important; + width: 100% !important; + margin: 0 !important; +} + +.marketo-form-wrapper .mktoFieldWrap:not(:has(.mktoField:not([type="hidden"]))) { + display: none !important; +} + +.marketo-form-wrapper .mktoLabel { + display: block !important; + font-size: 14px !important; + font-weight: normal !important; + color: #464646 !important; + margin-bottom: 4px !important; + width: auto !important; +} + +.marketo-form-wrapper .mktoAsterix { + display: none !important; +} + +.marketo-form-wrapper .mktoRequiredField .mktoLabel::after { + content: " *"; +} + +.marketo-form-wrapper .mktoField { + width: 100% !important; + border: 1px solid var(--color-grey-300, #909090) !important; + border-radius: 4px !important; + padding: 10px 12px !important; + font-size: 16px !important; + line-height: 24px !important; + height: auto !important; + min-height: 44px !important; + color: var(--color-black, #333) !important; + background-color: var(--color-white, #fff) !important; + transition: border-color 0.2s ease !important; + box-sizing: border-box !important; +} + +.marketo-form-wrapper .mktoField:focus { + outline: none !important; + border-color: var(--color-aem-blue, #1473e6) !important; +} + +.marketo-form-wrapper .mktoField.mktoInvalid { + border-color: var(--color-status-error, #d0021b) !important; +} + +.marketo-form-wrapper .use-case-textarea { + height: 120px; + resize: vertical; +} + +.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 !important; + background-color: var(--color-aem-blue, #1473e6) !important; + color: var(--color-white, #fff) !important; + font-weight: 700 !important; + font-size: 15px !important; + line-height: 24px; + cursor: pointer; + transition: all 0.2s ease; + border: none !important; +} + +.marketo-form-wrapper .custom-submit:hover { + background-color: var(--color-aem-blue-hover, #0054b6) !important; + box-shadow: 0 4px 12px rgba(20, 55, 100, 0.2); +} + +.marketo-form-wrapper .custom-submit:active { + background-color: var(--color-aem-blue-active, #004494) !important; +} + +.marketo-form-wrapper .custom-submit:disabled { + background-color: var(--color-grey-300, #c8c8c8) !important; + cursor: not-allowed; +} + +.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; +} + +.marketo-form-wrapper .success-message.show { + display: block; +} + +.marketo-form-wrapper .mktoButtonRow { + display: none !important; +} + +.marketo-form-wrapper .mktoFormRow.by-supplyingmycontac, +.marketo-form-wrapper .mktoFormRow.adobe-privacy, +.marketo-form-wrapper .mktoFormRow:has(.mktoHtmlText) { + display: none !important; +} + +.marketo-form-wrapper .mktoErrorMsg, +.marketo-form-wrapper .mktoError, +.marketo-form-wrapper .custom-field-error { + color: var(--color-status-error, #d0021b) !important; +} + +.marketo-form-wrapper .mktoFormRow.mktoCleanedScript, +.marketo-form-wrapper .mktoHtmlText span { + display: none !important; +} \ 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..b6a7cd0e --- /dev/null +++ b/hlx_statics/blocks/marketo/marketo.js @@ -0,0 +1,434 @@ +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); + } + + // Clear the block's default HTML table so it doesn't render on the screen + block.innerHTML = ''; + + // Create the wrapper for the Marketo form + 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(); + + // Clear Marketo's sessionStorage prefill so form starts fresh each load + sessionStorage.removeItem('mktoPreFillFields'); + + // Ensure marketo-form-loader.js is loaded + 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'); + + // Clear Marketo cookie-prefilled fields using the proper API + form.vals({ + FirstName: '', LastName: '', Email: '', Phone: '', + mktoFormsCompany: '', Website: '', Country: '', + State: '', PostalCode: '', mktoCompanySize: '', + Industry: '', mktoFormsJobTitle: '', mktoFormsFunctionalArea: '', + }); + + // Inject everything outside the form to avoid Marketo's grid/style stripping + + const selectColorStyle = document.createElement('style'); + selectColorStyle.textContent = ` + form.mktoForm .mktoFormRow.mktoCleanedScript, + form.mktoForm .mktoHtmlText span { + display: none !important; + } + `; + 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 = `#${formId} select[name="${name}"] { color: ${color} !important; }`; + const replaced = existing.replace(new RegExp(`#${formId} select\\[name="${name}"\\][^}]+}`, 'g'), ''); + selectColorStyle.textContent = replaced + rule; + }, true); + // Timeout needed to wait for MCZ to render the consent row legend + setTimeout(() => { + const consentRow = formEl.querySelector('.mktoFormRow.by-supplyingmycontac'); + if (consentRow) consentRow.style.setProperty('display', 'none', 'important'); + 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: 8px 0;'; + + let html = ''; + if (field.label) { + const labelCss = field.labelCss || 'display:block;font-size:14px;font-weight:700;margin-bottom:4px;'; + html += ``; + } + + 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); + }); + + // Setup Custom submit button + 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 = ``; + } + + // Setup Consent text + const consentDiv = document.createElement('div'); + consentDiv.className = 'aem-injected-element'; + consentDiv.style.cssText = 'font-size:12px; color:#444; line-height:1.5; margin: 8px 0;'; + if (legend) consentDiv.innerHTML = legend.innerHTML; + + // Inject based on configured position + 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); + } + + // Aggressive cleanup: Marketo hides fields using various methods (visibility, opacity, display:none on rows). + // Because we use display: contents on rows to enable CSS Grid, some wrappers might be left exposed. + // This interval ensures any wrapper containing a hidden field is also strictly display: none. + setInterval(() => { + formEl.querySelectorAll('.mktoFieldWrap').forEach(wrap => { + const input = wrap.querySelector('.mktoField'); + if (input) { + const style = window.getComputedStyle(input); + if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0' || input.type === 'hidden') { + wrap.style.setProperty('display', 'none', 'important'); + } else { + // Only remove if we explicitly set it + if (wrap.style.display === 'none') { + wrap.style.removeProperty('display'); + } + } + } else { + // Wrapper has no input (e.g. just empty html text) + const htmlText = wrap.querySelector('.mktoHtmlText'); + if (!htmlText || htmlText.innerHTML.trim() === '') { + wrap.style.setProperty('display', 'none', 'important'); + } + } + }); + }, 500); + + const customSubmitBtn = document.getElementById('custom-submit'); + + function showFieldErr(field, msg) { + const id = 'err-' + field.name; + field.style.setProperty('border-color', '#d0021b', 'important'); + 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 !important; 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 = {}; + + // Validate all Marketo required fields + formEl.querySelectorAll('.mktoRequired').forEach(field => { + // Skip hidden fields (Marketo sometimes hides fields dynamically) + if (field.offsetWidth === 0 && field.offsetHeight === 0) { + // If Marketo still requires them, we must fill them with dummy data + // otherwise Marketo's built-in validation will fail silently. + 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 !important; 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; + } + + // Hide all custom injected elements (custom fields, submit button, consent) + 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); + }); +} 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 ` +
+
+
+
+ +
+
+
+
+
+
+
+`; +} + +/** + * + * + * + * + * + * + * 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 }); +} From 21b1923a2500c752e629b3ca795fe8b4e04eb38b Mon Sep 17 00:00:00 2001 From: petheanraj-mitrah Date: Wed, 2 Sep 2026 15:03:31 +0530 Subject: [PATCH 02/17] feat: add styles for marketo form block --- hlx_statics/blocks/marketo/marketo.css | 104 ++++++++++++------------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/hlx_statics/blocks/marketo/marketo.css b/hlx_statics/blocks/marketo/marketo.css index 0c3c3bb1..84019191 100644 --- a/hlx_statics/blocks/marketo/marketo.css +++ b/hlx_statics/blocks/marketo/marketo.css @@ -1,58 +1,58 @@ .marketo-form-wrapper { display: flex; flex-direction: column; - width: calc(100% - 40px) !important; - max-width: none !important; - margin: 0 auto !important; + width: calc(100% - 40px); + max-width: none; + margin: 0 auto; } .marketo-form-wrapper .mktoForm { - display: grid !important; - grid-template-columns: 1fr 1fr !important; - gap: 16px !important; - width: 100% !important; - margin-bottom: 0 !important; - padding-bottom: 0 !important; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + width: 100%; + margin-bottom: 0; + padding-bottom: 0; } .marketo-form-wrapper .mktoFormRow:not(.mktoHidden), .marketo-form-wrapper .mktoFormCol:not(.mktoHidden) { - display: contents !important; + display: contents; } .marketo-form-wrapper .mktoHidden { - display: none !important; + display: none; } .marketo-form-wrapper .mktoClear, .marketo-form-wrapper .mktoOffset, .marketo-form-wrapper .mktoGutter, .marketo-form-wrapper .mktoHtmlText:empty { - display: none !important; + display: none; } .marketo-form-wrapper .mktoFieldWrap { - display: flex !important; - flex-direction: column !important; - width: 100% !important; - margin: 0 !important; + display: flex; + flex-direction: column; + width: 100%; + margin: 0; } .marketo-form-wrapper .mktoFieldWrap:not(:has(.mktoField:not([type="hidden"]))) { - display: none !important; + display: none; } .marketo-form-wrapper .mktoLabel { - display: block !important; - font-size: 14px !important; - font-weight: normal !important; - color: #464646 !important; - margin-bottom: 4px !important; - width: auto !important; + display: block; + font-size: 14px; + font-weight: normal; + color: #464646; + margin-bottom: 4px; + width: auto; } .marketo-form-wrapper .mktoAsterix { - display: none !important; + display: none; } .marketo-form-wrapper .mktoRequiredField .mktoLabel::after { @@ -60,27 +60,27 @@ } .marketo-form-wrapper .mktoField { - width: 100% !important; - border: 1px solid var(--color-grey-300, #909090) !important; - border-radius: 4px !important; - padding: 10px 12px !important; - font-size: 16px !important; - line-height: 24px !important; - height: auto !important; - min-height: 44px !important; - color: var(--color-black, #333) !important; - background-color: var(--color-white, #fff) !important; - transition: border-color 0.2s ease !important; - box-sizing: border-box !important; + 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; } .marketo-form-wrapper .mktoField:focus { - outline: none !important; - border-color: var(--color-aem-blue, #1473e6) !important; + outline: none; + border-color: var(--color-aem-blue, #1473e6); } .marketo-form-wrapper .mktoField.mktoInvalid { - border-color: var(--color-status-error, #d0021b) !important; + border-color: var(--color-status-error, #d0021b); } .marketo-form-wrapper .use-case-textarea { @@ -97,28 +97,28 @@ max-width: 280px; height: 48px; padding: 12px 24px; - border-radius: 24px !important; - background-color: var(--color-aem-blue, #1473e6) !important; - color: var(--color-white, #fff) !important; - font-weight: 700 !important; - font-size: 15px !important; + 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 !important; + border: none; } .marketo-form-wrapper .custom-submit:hover { - background-color: var(--color-aem-blue-hover, #0054b6) !important; + background-color: var(--color-aem-blue-hover, #0054b6); box-shadow: 0 4px 12px rgba(20, 55, 100, 0.2); } .marketo-form-wrapper .custom-submit:active { - background-color: var(--color-aem-blue-active, #004494) !important; + background-color: var(--color-aem-blue-active, #004494); } .marketo-form-wrapper .custom-submit:disabled { - background-color: var(--color-grey-300, #c8c8c8) !important; + background-color: var(--color-grey-300, #c8c8c8); cursor: not-allowed; } @@ -136,22 +136,22 @@ } .marketo-form-wrapper .mktoButtonRow { - display: none !important; + display: none; } .marketo-form-wrapper .mktoFormRow.by-supplyingmycontac, .marketo-form-wrapper .mktoFormRow.adobe-privacy, .marketo-form-wrapper .mktoFormRow:has(.mktoHtmlText) { - display: none !important; + display: none; } .marketo-form-wrapper .mktoErrorMsg, .marketo-form-wrapper .mktoError, .marketo-form-wrapper .custom-field-error { - color: var(--color-status-error, #d0021b) !important; + color: var(--color-status-error, #d0021b); } .marketo-form-wrapper .mktoFormRow.mktoCleanedScript, .marketo-form-wrapper .mktoHtmlText span { - display: none !important; + display: none; } \ No newline at end of file From ef05f666ccd13a74a240c60228ecb7a5db05177d Mon Sep 17 00:00:00 2001 From: petheanraj-mitrah Date: Wed, 2 Sep 2026 15:06:38 +0530 Subject: [PATCH 03/17] feat: add styling for Marketo form block --- hlx_statics/blocks/marketo/marketo.css | 66 +++++++++++++------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/hlx_statics/blocks/marketo/marketo.css b/hlx_statics/blocks/marketo/marketo.css index 84019191..689a9e13 100644 --- a/hlx_statics/blocks/marketo/marketo.css +++ b/hlx_statics/blocks/marketo/marketo.css @@ -1,4 +1,4 @@ -.marketo-form-wrapper { +body .marketo-form-wrapper { display: flex; flex-direction: column; width: calc(100% - 40px); @@ -6,7 +6,7 @@ margin: 0 auto; } -.marketo-form-wrapper .mktoForm { +body .marketo-form-wrapper form.mktoForm { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; @@ -15,34 +15,34 @@ padding-bottom: 0; } -.marketo-form-wrapper .mktoFormRow:not(.mktoHidden), -.marketo-form-wrapper .mktoFormCol:not(.mktoHidden) { +body .marketo-form-wrapper form.mktoForm .mktoFormRow:not(.mktoHidden), +body .marketo-form-wrapper form.mktoForm .mktoFormCol:not(.mktoHidden) { display: contents; } -.marketo-form-wrapper .mktoHidden { +body .marketo-form-wrapper form.mktoForm .mktoHidden { display: none; } -.marketo-form-wrapper .mktoClear, -.marketo-form-wrapper .mktoOffset, -.marketo-form-wrapper .mktoGutter, -.marketo-form-wrapper .mktoHtmlText:empty { +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 .mktoHtmlText:empty { display: none; } -.marketo-form-wrapper .mktoFieldWrap { +body .marketo-form-wrapper form.mktoForm .mktoFieldWrap { display: flex; flex-direction: column; width: 100%; margin: 0; } -.marketo-form-wrapper .mktoFieldWrap:not(:has(.mktoField:not([type="hidden"]))) { +body .marketo-form-wrapper form.mktoForm .mktoFieldWrap:not(:has(.mktoField:not([type="hidden"]))) { display: none; } -.marketo-form-wrapper .mktoLabel { +body .marketo-form-wrapper form.mktoForm .mktoLabel { display: block; font-size: 14px; font-weight: normal; @@ -51,15 +51,15 @@ width: auto; } -.marketo-form-wrapper .mktoAsterix { +body .marketo-form-wrapper form.mktoForm .mktoAsterix { display: none; } -.marketo-form-wrapper .mktoRequiredField .mktoLabel::after { +body .marketo-form-wrapper form.mktoForm .mktoRequiredField .mktoLabel::after { content: " *"; } -.marketo-form-wrapper .mktoField { +body .marketo-form-wrapper form.mktoForm .mktoField { width: 100%; border: 1px solid var(--color-grey-300, #909090); border-radius: 4px; @@ -74,21 +74,21 @@ box-sizing: border-box; } -.marketo-form-wrapper .mktoField:focus { +body .marketo-form-wrapper form.mktoForm .mktoField:focus { outline: none; border-color: var(--color-aem-blue, #1473e6); } -.marketo-form-wrapper .mktoField.mktoInvalid { +body .marketo-form-wrapper form.mktoForm .mktoField.mktoInvalid { border-color: var(--color-status-error, #d0021b); } -.marketo-form-wrapper .use-case-textarea { +body .marketo-form-wrapper .use-case-textarea { height: 120px; resize: vertical; } -.marketo-form-wrapper .custom-submit { +body .marketo-form-wrapper .custom-submit { display: flex; align-items: center; justify-content: center; @@ -108,21 +108,21 @@ border: none; } -.marketo-form-wrapper .custom-submit:hover { +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); } -.marketo-form-wrapper .custom-submit:active { +body .marketo-form-wrapper .custom-submit:active { background-color: var(--color-aem-blue-active, #004494); } -.marketo-form-wrapper .custom-submit:disabled { +body .marketo-form-wrapper .custom-submit:disabled { background-color: var(--color-grey-300, #c8c8c8); cursor: not-allowed; } -.marketo-form-wrapper .success-message { +body .marketo-form-wrapper .success-message { display: none; padding: 24px; background-color: var(--color-status-success-bg, #e6f4e6); @@ -131,27 +131,27 @@ text-align: center; } -.marketo-form-wrapper .success-message.show { +body .marketo-form-wrapper .success-message.show { display: block; } -.marketo-form-wrapper .mktoButtonRow { +body .marketo-form-wrapper form.mktoForm .mktoButtonRow { display: none; } -.marketo-form-wrapper .mktoFormRow.by-supplyingmycontac, -.marketo-form-wrapper .mktoFormRow.adobe-privacy, -.marketo-form-wrapper .mktoFormRow:has(.mktoHtmlText) { +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; } -.marketo-form-wrapper .mktoErrorMsg, -.marketo-form-wrapper .mktoError, -.marketo-form-wrapper .custom-field-error { +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); } -.marketo-form-wrapper .mktoFormRow.mktoCleanedScript, -.marketo-form-wrapper .mktoHtmlText span { +body .marketo-form-wrapper form.mktoForm .mktoFormRow.mktoCleanedScript, +body .marketo-form-wrapper form.mktoForm .mktoHtmlText span { display: none; } \ No newline at end of file From 3a4c20325bdd5d5d388dce8e37703080124c93d3 Mon Sep 17 00:00:00 2001 From: petheanraj-mitrah Date: Wed, 2 Sep 2026 15:23:34 +0530 Subject: [PATCH 04/17] feat: add Marketo form block with dynamic configuration and custom field injection --- hlx_statics/blocks/marketo/marketo.js | 35 +++++++++++++++++++-------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/hlx_statics/blocks/marketo/marketo.js b/hlx_statics/blocks/marketo/marketo.js index b6a7cd0e..fc8e5146 100644 --- a/hlx_statics/blocks/marketo/marketo.js +++ b/hlx_statics/blocks/marketo/marketo.js @@ -161,9 +161,9 @@ export default async function decorate(block) { const selectColorStyle = document.createElement('style'); selectColorStyle.textContent = ` - form.mktoForm .mktoFormRow.mktoCleanedScript, - form.mktoForm .mktoHtmlText span { - display: none !important; + body form.mktoForm .mktoFormRow.mktoCleanedScript, + body form.mktoForm .mktoHtmlText span { + display: none; } `; document.head.appendChild(selectColorStyle); @@ -173,14 +173,14 @@ export default async function decorate(block) { const color = e.target.value ? '#2c2c2c' : '#959595'; const existing = selectColorStyle.textContent; const formId = formEl.id || 'mktoForm'; - const rule = `#${formId} select[name="${name}"] { color: ${color} !important; }`; + 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); // Timeout needed to wait for MCZ to render the consent row legend setTimeout(() => { const consentRow = formEl.querySelector('.mktoFormRow.by-supplyingmycontac'); - if (consentRow) consentRow.style.setProperty('display', 'none', 'important'); + if (consentRow) consentRow.style.display = 'none'; const legend = consentRow && consentRow.querySelector('legend'); const container = formEl.parentNode; @@ -241,12 +241,27 @@ export default async function decorate(block) { // Because we use display: contents on rows to enable CSS Grid, some wrappers might be left exposed. // This interval ensures any wrapper containing a hidden field is also strictly display: none. setInterval(() => { + // Strip Marketo injected inline styles that break flex/grid layout + const structuralEls = formEl.querySelectorAll('.mktoFormRow, .mktoFormCol, .mktoFieldWrap, .mktoLabel, .mktoGutter, .mktoOffset, .mktoClear'); + structuralEls.forEach(el => { + if (el.style) { + el.style.removeProperty('width'); + el.style.removeProperty('margin'); + el.style.removeProperty('margin-bottom'); + el.style.removeProperty('padding'); + el.style.removeProperty('clear'); + } + }); + if (formEl.style) { + formEl.style.removeProperty('width'); + } + formEl.querySelectorAll('.mktoFieldWrap').forEach(wrap => { const input = wrap.querySelector('.mktoField'); if (input) { const style = window.getComputedStyle(input); if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0' || input.type === 'hidden') { - wrap.style.setProperty('display', 'none', 'important'); + wrap.style.display = 'none'; } else { // Only remove if we explicitly set it if (wrap.style.display === 'none') { @@ -257,7 +272,7 @@ export default async function decorate(block) { // Wrapper has no input (e.g. just empty html text) const htmlText = wrap.querySelector('.mktoHtmlText'); if (!htmlText || htmlText.innerHTML.trim() === '') { - wrap.style.setProperty('display', 'none', 'important'); + wrap.style.display = 'none'; } } }); @@ -267,13 +282,13 @@ export default async function decorate(block) { function showFieldErr(field, msg) { const id = 'err-' + field.name; - field.style.setProperty('border-color', '#d0021b', 'important'); + 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 !important; font-size:12px; margin-top:4px; display:block; clear:both; width:100%;'; + 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) { @@ -347,7 +362,7 @@ export default async function decorate(block) { err = document.createElement('div'); err.id = field.id + '-error'; err.className = 'custom-field-error'; - err.style.cssText = field.errorCss || 'color:#d0021b !important; font-size:12px; margin-top:4px;'; + err.style.cssText = field.errorCss || 'color:#d0021b; font-size:12px; margin-top:4px;'; err.textContent = field.errorText || 'This field is required.'; inputEl.after(err); } From f93623c9d30cc9995e1c8546445ecd299012ea1d Mon Sep 17 00:00:00 2001 From: petheanraj-mitrah Date: Wed, 2 Sep 2026 15:26:55 +0530 Subject: [PATCH 05/17] feat: implement Marketo form block with dynamic configuration and custom field injection --- hlx_statics/blocks/marketo/marketo.js | 33 ++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/hlx_statics/blocks/marketo/marketo.js b/hlx_statics/blocks/marketo/marketo.js index fc8e5146..8ae928c9 100644 --- a/hlx_statics/blocks/marketo/marketo.js +++ b/hlx_statics/blocks/marketo/marketo.js @@ -241,21 +241,52 @@ export default async function decorate(block) { // Because we use display: contents on rows to enable CSS Grid, some wrappers might be left exposed. // This interval ensures any wrapper containing a hidden field is also strictly display: none. setInterval(() => { + // Remove Marketo's injected style block inside the form which sets margins and widths + const inlineStyle = formEl.querySelector('style'); + if (inlineStyle) inlineStyle.remove(); + + // Forcefully hide spacing elements that Marketo might try to show + formEl.querySelectorAll('.mktoGutter, .mktoOffset, .mktoClear, .mktoButtonRow, .mktoAsterix').forEach(el => { + el.style.display = 'none'; + }); + + formEl.querySelectorAll('.mktoFormRow.mktoCleanedScript, .mktoFormRow.by-supplyingmycontac, .mktoFormRow.adobe-privacy').forEach(row => { + row.style.display = 'none'; + }); + // Strip Marketo injected inline styles that break flex/grid layout - const structuralEls = formEl.querySelectorAll('.mktoFormRow, .mktoFormCol, .mktoFieldWrap, .mktoLabel, .mktoGutter, .mktoOffset, .mktoClear'); + const structuralEls = formEl.querySelectorAll('.mktoFormRow, .mktoFormCol, .mktoFieldWrap, .mktoLabel'); structuralEls.forEach(el => { if (el.style) { el.style.removeProperty('width'); el.style.removeProperty('margin'); el.style.removeProperty('margin-bottom'); + el.style.removeProperty('margin-top'); el.style.removeProperty('padding'); + el.style.removeProperty('padding-left'); + el.style.removeProperty('padding-right'); el.style.removeProperty('clear'); + el.style.removeProperty('height'); + el.style.removeProperty('min-height'); + + // If Marketo applied display block/flex inline, it overrides our CSS 'display: contents'. + // Strip it unless it is explicitly 'none' (which Marketo uses to hide elements). + if (el.style.display && el.style.display !== 'none') { + el.style.removeProperty('display'); + } } }); if (formEl.style) { formEl.style.removeProperty('width'); } + // Ensure rows containing only html text (like privacy policy) are hidden if we want them out of the grid + formEl.querySelectorAll('.mktoFormRow').forEach(row => { + if (row.querySelector('.mktoHtmlText')) { + row.style.display = 'none'; + } + }); + formEl.querySelectorAll('.mktoFieldWrap').forEach(wrap => { const input = wrap.querySelector('.mktoField'); if (input) { From f5441336c590342989d2dd3f9518ea4b0c913386 Mon Sep 17 00:00:00 2001 From: petheanraj-mitrah Date: Wed, 2 Sep 2026 15:29:40 +0530 Subject: [PATCH 06/17] feat: add Marketo block with configurable form rendering and CSS styles --- hlx_statics/blocks/marketo/marketo.css | 1 + hlx_statics/blocks/marketo/marketo.js | 25 +++++++++++++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/hlx_statics/blocks/marketo/marketo.css b/hlx_statics/blocks/marketo/marketo.css index 689a9e13..bc62829e 100644 --- a/hlx_statics/blocks/marketo/marketo.css +++ b/hlx_statics/blocks/marketo/marketo.css @@ -1,6 +1,7 @@ body .marketo-form-wrapper { display: flex; flex-direction: column; + gap: 16px; width: calc(100% - 40px); max-width: none; margin: 0 auto; diff --git a/hlx_statics/blocks/marketo/marketo.js b/hlx_statics/blocks/marketo/marketo.js index 8ae928c9..eb61f9e9 100644 --- a/hlx_statics/blocks/marketo/marketo.js +++ b/hlx_statics/blocks/marketo/marketo.js @@ -194,7 +194,7 @@ export default async function decorate(block) { const fieldDiv = document.createElement('div'); fieldDiv.className = 'aem-injected-element'; - fieldDiv.style.cssText = field.wrapperCss || 'margin: 8px 0;'; + fieldDiv.style.cssText = field.wrapperCss || 'margin: 0;'; let html = ''; if (field.label) { @@ -225,7 +225,7 @@ export default async function decorate(block) { // Setup Consent text const consentDiv = document.createElement('div'); consentDiv.className = 'aem-injected-element'; - consentDiv.style.cssText = 'font-size:12px; color:#444; line-height:1.5; margin: 8px 0;'; + consentDiv.style.cssText = 'font-size:12px; color:#444; line-height:1.5; margin: 0;'; if (legend) consentDiv.innerHTML = legend.innerHTML; // Inject based on configured position @@ -254,20 +254,28 @@ export default async function decorate(block) { row.style.display = 'none'; }); + // Aggressively strip margins and min-heights from ALL elements inside the form + 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'); + } + }); + // Strip Marketo injected inline styles that break flex/grid layout const structuralEls = formEl.querySelectorAll('.mktoFormRow, .mktoFormCol, .mktoFieldWrap, .mktoLabel'); structuralEls.forEach(el => { if (el.style) { el.style.removeProperty('width'); - el.style.removeProperty('margin'); - el.style.removeProperty('margin-bottom'); - el.style.removeProperty('margin-top'); el.style.removeProperty('padding'); el.style.removeProperty('padding-left'); el.style.removeProperty('padding-right'); el.style.removeProperty('clear'); el.style.removeProperty('height'); - el.style.removeProperty('min-height'); // If Marketo applied display block/flex inline, it overrides our CSS 'display: contents'. // Strip it unless it is explicitly 'none' (which Marketo uses to hide elements). @@ -278,6 +286,11 @@ export default async function decorate(block) { }); 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'); } // Ensure rows containing only html text (like privacy policy) are hidden if we want them out of the grid From f44335cb1cc8ea4eb7575108027a6c2511362a99 Mon Sep 17 00:00:00 2001 From: petheanraj-mitrah Date: Wed, 2 Sep 2026 15:32:12 +0530 Subject: [PATCH 07/17] feat: add Marketo block with custom configuration loader and styling support --- hlx_statics/blocks/marketo/marketo.css | 15 +++++++++++++++ hlx_statics/blocks/marketo/marketo.js | 5 ++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/hlx_statics/blocks/marketo/marketo.css b/hlx_statics/blocks/marketo/marketo.css index bc62829e..0e37e723 100644 --- a/hlx_statics/blocks/marketo/marketo.css +++ b/hlx_statics/blocks/marketo/marketo.css @@ -25,9 +25,24 @@ body .marketo-form-wrapper form.mktoForm .mktoHidden { display: none; } +/* + * CRITICAL FIX: Marketo's forms2.css adds clearfix ::before/::after pseudo-elements to rows and cols. + * Because we use 'display: contents' on these elements to flatten the DOM for CSS Grid, + * these pseudo-elements become direct children of the Grid and take up invisible grid cells, + * creating massive empty gaps. We MUST hide them. + */ +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 input[type="hidden"], body .marketo-form-wrapper form.mktoForm .mktoHtmlText:empty { display: none; } diff --git a/hlx_statics/blocks/marketo/marketo.js b/hlx_statics/blocks/marketo/marketo.js index eb61f9e9..94199dc0 100644 --- a/hlx_statics/blocks/marketo/marketo.js +++ b/hlx_statics/blocks/marketo/marketo.js @@ -241,9 +241,8 @@ export default async function decorate(block) { // Because we use display: contents on rows to enable CSS Grid, some wrappers might be left exposed. // This interval ensures any wrapper containing a hidden field is also strictly display: none. setInterval(() => { - // Remove Marketo's injected style block inside the form which sets margins and widths - const inlineStyle = formEl.querySelector('style'); - if (inlineStyle) inlineStyle.remove(); + // Remove Marketo's injected style blocks inside the form which set margins and widths + formEl.querySelectorAll('style').forEach(s => s.remove()); // Forcefully hide spacing elements that Marketo might try to show formEl.querySelectorAll('.mktoGutter, .mktoOffset, .mktoClear, .mktoButtonRow, .mktoAsterix').forEach(el => { From d5dfff32b417f6eb07e21959062b31f2539f9a2b Mon Sep 17 00:00:00 2001 From: petheanraj-mitrah Date: Wed, 2 Sep 2026 15:37:35 +0530 Subject: [PATCH 08/17] feat: add Marketo form block with custom styling and configuration loader --- hlx_statics/blocks/marketo/marketo.css | 1 - hlx_statics/blocks/marketo/marketo.js | 17 ++++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/hlx_statics/blocks/marketo/marketo.css b/hlx_statics/blocks/marketo/marketo.css index 0e37e723..b4d5273e 100644 --- a/hlx_statics/blocks/marketo/marketo.css +++ b/hlx_statics/blocks/marketo/marketo.css @@ -1,7 +1,6 @@ body .marketo-form-wrapper { display: flex; flex-direction: column; - gap: 16px; width: calc(100% - 40px); max-width: none; margin: 0 auto; diff --git a/hlx_statics/blocks/marketo/marketo.js b/hlx_statics/blocks/marketo/marketo.js index 94199dc0..277af0b5 100644 --- a/hlx_statics/blocks/marketo/marketo.js +++ b/hlx_statics/blocks/marketo/marketo.js @@ -244,13 +244,20 @@ export default async function decorate(block) { // Remove Marketo's injected style blocks inside the form which set margins and widths formEl.querySelectorAll('style').forEach(s => s.remove()); - // Forcefully hide spacing elements that Marketo might try to show + // Forcefully rip spacing elements and button row out of the document flow. + // Using position:absolute ensures they NEVER become grid items, even if Marketo + // or Author CSS overrides 'display: none' with 'display: block !important'. formEl.querySelectorAll('.mktoGutter, .mktoOffset, .mktoClear, .mktoButtonRow, .mktoAsterix').forEach(el => { - el.style.display = 'none'; + el.style.position = 'absolute'; + el.style.visibility = 'hidden'; + el.style.width = '0'; + el.style.height = '0'; + el.style.overflow = 'hidden'; }); + // Completely remove privacy/consent rows from DOM since we extract and inject their content manually formEl.querySelectorAll('.mktoFormRow.mktoCleanedScript, .mktoFormRow.by-supplyingmycontac, .mktoFormRow.adobe-privacy').forEach(row => { - row.style.display = 'none'; + row.remove(); }); // Aggressively strip margins and min-heights from ALL elements inside the form @@ -292,10 +299,10 @@ export default async function decorate(block) { formEl.style.removeProperty('min-height'); } - // Ensure rows containing only html text (like privacy policy) are hidden if we want them out of the grid + // Ensure rows containing only html text (like privacy policy) are completely removed formEl.querySelectorAll('.mktoFormRow').forEach(row => { if (row.querySelector('.mktoHtmlText')) { - row.style.display = 'none'; + row.remove(); } }); From cbb151ff72e7b17265540fbbb49edcdd9948a919 Mon Sep 17 00:00:00 2001 From: petheanraj-mitrah Date: Wed, 2 Sep 2026 15:45:27 +0530 Subject: [PATCH 09/17] feat: add Marketo form block with dynamic configuration and custom field injection --- hlx_statics/blocks/marketo/marketo.js | 53 ++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/hlx_statics/blocks/marketo/marketo.js b/hlx_statics/blocks/marketo/marketo.js index 277af0b5..1abf8bfe 100644 --- a/hlx_statics/blocks/marketo/marketo.js +++ b/hlx_statics/blocks/marketo/marketo.js @@ -244,16 +244,29 @@ export default async function decorate(block) { // Remove Marketo's injected style blocks inside the form which set margins and widths formEl.querySelectorAll('style').forEach(s => s.remove()); - // Forcefully rip spacing elements and button row out of the document flow. + // Forcefully rip spacing elements, hidden rows, and button row out of the document flow. // Using position:absolute ensures they NEVER become grid items, even if Marketo - // or Author CSS overrides 'display: none' with 'display: block !important'. - formEl.querySelectorAll('.mktoGutter, .mktoOffset, .mktoClear, .mktoButtonRow, .mktoAsterix').forEach(el => { + // or Author CSS overrides 'display: none'. + formEl.querySelectorAll('.mktoGutter, .mktoOffset, .mktoClear, .mktoButtonRow, .mktoAsterix, .mktoPlaceholder').forEach(el => { el.style.position = 'absolute'; el.style.visibility = 'hidden'; el.style.width = '0'; el.style.height = '0'; el.style.overflow = 'hidden'; }); + + // Ensure the Marketo submit button's container is entirely ripped from the grid + const originalBtn = formEl.querySelector('.mktoButton'); + if (originalBtn) { + const btnWrap = originalBtn.closest('.mktoButtonRow') || originalBtn.closest('.mktoFormRow'); + if (btnWrap) { + btnWrap.style.position = 'absolute'; + btnWrap.style.visibility = 'hidden'; + btnWrap.style.width = '0'; + btnWrap.style.height = '0'; + btnWrap.style.overflow = 'hidden'; + } + } // Completely remove privacy/consent rows from DOM since we extract and inject their content manually formEl.querySelectorAll('.mktoFormRow.mktoCleanedScript, .mktoFormRow.by-supplyingmycontac, .mktoFormRow.adobe-privacy').forEach(row => { @@ -308,21 +321,41 @@ export default async function decorate(block) { 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') { - wrap.style.display = 'none'; - } else { - // Only remove if we explicitly set it - if (wrap.style.display === 'none') { - wrap.style.removeProperty('display'); - } + shouldHide = true; } } else { // Wrapper has no input (e.g. just empty html text) const htmlText = wrap.querySelector('.mktoHtmlText'); if (!htmlText || htmlText.innerHTML.trim() === '') { - wrap.style.display = 'none'; + shouldHide = true; + } + } + + if (shouldHide) { + // Rip hidden fields completely out of the grid using position:absolute! + // This is the ONLY way to guarantee they don't leave empty cells at the bottom of the form + // if their display property gets overridden. + 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 { + // Restore if we need to show it again dynamically + 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'); } } }); From 21fe335c7642920840fe175a53eed6d794cc305b Mon Sep 17 00:00:00 2001 From: petheanraj-mitrah Date: Wed, 2 Sep 2026 16:09:36 +0530 Subject: [PATCH 10/17] fix: position absolute on hidden inputs --- hlx_statics/blocks/marketo/marketo.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hlx_statics/blocks/marketo/marketo.js b/hlx_statics/blocks/marketo/marketo.js index 1abf8bfe..b54d4ea8 100644 --- a/hlx_statics/blocks/marketo/marketo.js +++ b/hlx_statics/blocks/marketo/marketo.js @@ -247,12 +247,14 @@ export default async function decorate(block) { // Forcefully rip spacing elements, hidden rows, and button row out of the document flow. // Using position:absolute ensures they NEVER become grid items, even if Marketo // or Author CSS overrides 'display: none'. - formEl.querySelectorAll('.mktoGutter, .mktoOffset, .mktoClear, .mktoButtonRow, .mktoAsterix, .mktoPlaceholder').forEach(el => { + formEl.querySelectorAll('.mktoGutter, .mktoOffset, .mktoClear, .mktoButtonRow, .mktoAsterix, .mktoPlaceholder, input[type="hidden"]').forEach(el => { el.style.position = 'absolute'; el.style.visibility = 'hidden'; el.style.width = '0'; el.style.height = '0'; el.style.overflow = 'hidden'; + el.style.margin = '0'; + el.style.padding = '0'; }); // Ensure the Marketo submit button's container is entirely ripped from the grid From cb4962b9a942124bfa09c84c4ba82fdeac4bc29b Mon Sep 17 00:00:00 2001 From: petheanraj-mitrah Date: Wed, 2 Sep 2026 16:14:39 +0530 Subject: [PATCH 11/17] fix: move position absolute to CSS for bulletproof rip-out --- hlx_statics/blocks/marketo/marketo.css | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/hlx_statics/blocks/marketo/marketo.css b/hlx_statics/blocks/marketo/marketo.css index b4d5273e..5fdd2b68 100644 --- a/hlx_statics/blocks/marketo/marketo.css +++ b/hlx_statics/blocks/marketo/marketo.css @@ -38,12 +38,22 @@ body .marketo-form-wrapper form.mktoForm .mktoFormCol::after { content: none; } +/* Bulletproof rip-out for hidden elements. + Using position: absolute ensures they never become grid items, + even if Marketo dynamically applies inline display: block */ 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 { - display: none; + position: absolute; + visibility: hidden; + width: 0; + height: 0; + overflow: hidden; + margin: 0; + padding: 0; } body .marketo-form-wrapper form.mktoForm .mktoFieldWrap { From c93c00b71c3224d917e6fcb10bf7b3b4bb537acf Mon Sep 17 00:00:00 2001 From: petheanraj-mitrah Date: Wed, 2 Sep 2026 16:20:26 +0530 Subject: [PATCH 12/17] fix: aggressively rip out zombie mktoFormRow containers that house hidden inputs to eliminate grid gaps --- hlx_statics/blocks/marketo/marketo.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/hlx_statics/blocks/marketo/marketo.js b/hlx_statics/blocks/marketo/marketo.js index b54d4ea8..4d0b9659 100644 --- a/hlx_statics/blocks/marketo/marketo.js +++ b/hlx_statics/blocks/marketo/marketo.js @@ -321,6 +321,32 @@ export default async function decorate(block) { } }); + // Final aggressive sweep: Any mktoFormRow that only contains hidden/absolute field wraps + // must also be ripped out. Marketo wraps tracking inputs in empty rows, and if left as + // display: block or display: contents, they generate empty grid gaps! + formEl.querySelectorAll('.mktoFormRow').forEach(row => { + const wraps = row.querySelectorAll('.mktoFieldWrap'); + let hasVisibleField = false; + wraps.forEach(wrap => { + const style = window.getComputedStyle(wrap); + // If it's not absolutely positioned and not display none, it's a visible field + if (style.position !== 'absolute' && style.display !== 'none' && style.visibility !== 'hidden') { + hasVisibleField = true; + } + }); + + // If the row has field wrappers, but ALL of them are hidden, the row is a zombie! Rip it out! + if (wraps.length > 0 && !hasVisibleField) { + row.style.position = 'absolute'; + row.style.visibility = 'hidden'; + row.style.width = '0'; + row.style.height = '0'; + row.style.overflow = 'hidden'; + row.style.margin = '0'; + row.style.padding = '0'; + } + }); + formEl.querySelectorAll('.mktoFieldWrap').forEach(wrap => { const input = wrap.querySelector('.mktoField'); let shouldHide = false; From 79401ad9e9557ed296473629654f08ebfdcfef01 Mon Sep 17 00:00:00 2001 From: petheanraj-mitrah Date: Wed, 2 Sep 2026 18:48:30 +0530 Subject: [PATCH 13/17] fix: use custom zombie-row class to permanently rip out empty rows via CSS --- hlx_statics/blocks/marketo/marketo.css | 3 ++- hlx_statics/blocks/marketo/marketo.js | 14 ++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/hlx_statics/blocks/marketo/marketo.css b/hlx_statics/blocks/marketo/marketo.css index 5fdd2b68..271ac68d 100644 --- a/hlx_statics/blocks/marketo/marketo.css +++ b/hlx_statics/blocks/marketo/marketo.css @@ -46,7 +46,8 @@ 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 .mktoHtmlText:empty, +body .marketo-form-wrapper form.mktoForm .mktoFormRow.zombie-row { position: absolute; visibility: hidden; width: 0; diff --git a/hlx_statics/blocks/marketo/marketo.js b/hlx_statics/blocks/marketo/marketo.js index 4d0b9659..ab44e96e 100644 --- a/hlx_statics/blocks/marketo/marketo.js +++ b/hlx_statics/blocks/marketo/marketo.js @@ -335,15 +335,13 @@ export default async function decorate(block) { } }); - // If the row has field wrappers, but ALL of them are hidden, the row is a zombie! Rip it out! + // If the row has field wrappers, but ALL of them are hidden, the row is a zombie! + // We add a custom class instead of inline styles because Marketo's JS loop + // continuously strips inline styles for rows. if (wraps.length > 0 && !hasVisibleField) { - row.style.position = 'absolute'; - row.style.visibility = 'hidden'; - row.style.width = '0'; - row.style.height = '0'; - row.style.overflow = 'hidden'; - row.style.margin = '0'; - row.style.padding = '0'; + row.classList.add('zombie-row'); + } else { + row.classList.remove('zombie-row'); } }); From 191a5a07763c2d3cb4daedc7ba7dae9a3dcacc0a Mon Sep 17 00:00:00 2001 From: petheanraj-mitrah Date: Wed, 2 Sep 2026 18:53:05 +0530 Subject: [PATCH 14/17] fix: make mktoPlaceholder absolutely positioned to prevent placeholder-only rows from generating empty grid cells --- hlx_statics/blocks/marketo/marketo.css | 1 + 1 file changed, 1 insertion(+) diff --git a/hlx_statics/blocks/marketo/marketo.css b/hlx_statics/blocks/marketo/marketo.css index 271ac68d..2433f5d1 100644 --- a/hlx_statics/blocks/marketo/marketo.css +++ b/hlx_statics/blocks/marketo/marketo.css @@ -47,6 +47,7 @@ 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; From 2e73926efd6f41f7624d593138e0f89f3d9d48cf Mon Sep 17 00:00:00 2001 From: petheanraj-mitrah Date: Wed, 2 Sep 2026 18:54:47 +0530 Subject: [PATCH 15/17] feat: add Marketo form block with dynamic configuration and custom field injection --- hlx_statics/blocks/marketo/marketo.js | 42 +++++++++++++-------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/hlx_statics/blocks/marketo/marketo.js b/hlx_statics/blocks/marketo/marketo.js index ab44e96e..4bc70f6b 100644 --- a/hlx_statics/blocks/marketo/marketo.js +++ b/hlx_statics/blocks/marketo/marketo.js @@ -194,7 +194,7 @@ export default async function decorate(block) { const fieldDiv = document.createElement('div'); fieldDiv.className = 'aem-injected-element'; - fieldDiv.style.cssText = field.wrapperCss || 'margin: 0;'; + fieldDiv.style.cssText = field.wrapperCss || 'margin-top: 16px;'; let html = ''; if (field.label) { @@ -269,7 +269,7 @@ export default async function decorate(block) { btnWrap.style.overflow = 'hidden'; } } - + // Completely remove privacy/consent rows from DOM since we extract and inject their content manually formEl.querySelectorAll('.mktoFormRow.mktoCleanedScript, .mktoFormRow.by-supplyingmycontac, .mktoFormRow.adobe-privacy').forEach(row => { row.remove(); @@ -297,7 +297,7 @@ export default async function decorate(block) { el.style.removeProperty('padding-right'); el.style.removeProperty('clear'); el.style.removeProperty('height'); - + // If Marketo applied display block/flex inline, it overrides our CSS 'display: contents'. // Strip it unless it is explicitly 'none' (which Marketo uses to hide elements). if (el.style.display && el.style.display !== 'none') { @@ -325,24 +325,24 @@ export default async function decorate(block) { // must also be ripped out. Marketo wraps tracking inputs in empty rows, and if left as // display: block or display: contents, they generate empty grid gaps! formEl.querySelectorAll('.mktoFormRow').forEach(row => { - const wraps = row.querySelectorAll('.mktoFieldWrap'); - let hasVisibleField = false; - wraps.forEach(wrap => { - const style = window.getComputedStyle(wrap); - // If it's not absolutely positioned and not display none, it's a visible field - if (style.position !== 'absolute' && style.display !== 'none' && style.visibility !== 'hidden') { - hasVisibleField = true; - } - }); - - // If the row has field wrappers, but ALL of them are hidden, the row is a zombie! - // We add a custom class instead of inline styles because Marketo's JS loop - // continuously strips inline styles for rows. - if (wraps.length > 0 && !hasVisibleField) { - row.classList.add('zombie-row'); - } else { - row.classList.remove('zombie-row'); - } + const wraps = row.querySelectorAll('.mktoFieldWrap'); + let hasVisibleField = false; + wraps.forEach(wrap => { + const style = window.getComputedStyle(wrap); + // If it's not absolutely positioned and not display none, it's a visible field + if (style.position !== 'absolute' && style.display !== 'none' && style.visibility !== 'hidden') { + hasVisibleField = true; + } + }); + + // If the row has field wrappers, but ALL of them are hidden, the row is a zombie! + // We add a custom class instead of inline styles because Marketo's JS loop + // continuously strips inline styles for rows. + if (wraps.length > 0 && !hasVisibleField) { + row.classList.add('zombie-row'); + } else { + row.classList.remove('zombie-row'); + } }); formEl.querySelectorAll('.mktoFieldWrap').forEach(wrap => { From 73e78ce62c06bb9ac7a5fc84b5a4ad8479e55bed Mon Sep 17 00:00:00 2001 From: petheanraj-mitrah Date: Wed, 2 Sep 2026 18:57:00 +0530 Subject: [PATCH 16/17] refactor: remove redundant javascript absolute positioning overrides since they are now natively handled by marketo.css --- hlx_statics/blocks/marketo/marketo.js | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/hlx_statics/blocks/marketo/marketo.js b/hlx_statics/blocks/marketo/marketo.js index 4bc70f6b..4ac6889c 100644 --- a/hlx_statics/blocks/marketo/marketo.js +++ b/hlx_statics/blocks/marketo/marketo.js @@ -244,31 +244,6 @@ export default async function decorate(block) { // Remove Marketo's injected style blocks inside the form which set margins and widths formEl.querySelectorAll('style').forEach(s => s.remove()); - // Forcefully rip spacing elements, hidden rows, and button row out of the document flow. - // Using position:absolute ensures they NEVER become grid items, even if Marketo - // or Author CSS overrides 'display: none'. - formEl.querySelectorAll('.mktoGutter, .mktoOffset, .mktoClear, .mktoButtonRow, .mktoAsterix, .mktoPlaceholder, input[type="hidden"]').forEach(el => { - el.style.position = 'absolute'; - el.style.visibility = 'hidden'; - el.style.width = '0'; - el.style.height = '0'; - el.style.overflow = 'hidden'; - el.style.margin = '0'; - el.style.padding = '0'; - }); - - // Ensure the Marketo submit button's container is entirely ripped from the grid - const originalBtn = formEl.querySelector('.mktoButton'); - if (originalBtn) { - const btnWrap = originalBtn.closest('.mktoButtonRow') || originalBtn.closest('.mktoFormRow'); - if (btnWrap) { - btnWrap.style.position = 'absolute'; - btnWrap.style.visibility = 'hidden'; - btnWrap.style.width = '0'; - btnWrap.style.height = '0'; - btnWrap.style.overflow = 'hidden'; - } - } // Completely remove privacy/consent rows from DOM since we extract and inject their content manually formEl.querySelectorAll('.mktoFormRow.mktoCleanedScript, .mktoFormRow.by-supplyingmycontac, .mktoFormRow.adobe-privacy').forEach(row => { From d81530734e5c29abb5ec90f78c11e2a5034c36e0 Mon Sep 17 00:00:00 2001 From: petheanraj-mitrah Date: Wed, 2 Sep 2026 18:58:36 +0530 Subject: [PATCH 17/17] chore: remove all comments from marketo.js and marketo.css --- hlx_statics/blocks/marketo/marketo.css | 185 +-------- hlx_statics/blocks/marketo/marketo.js | 534 +------------------------ 2 files changed, 2 insertions(+), 717 deletions(-) diff --git a/hlx_statics/blocks/marketo/marketo.css b/hlx_statics/blocks/marketo/marketo.css index 2433f5d1..c753b0eb 100644 --- a/hlx_statics/blocks/marketo/marketo.css +++ b/hlx_statics/blocks/marketo/marketo.css @@ -1,184 +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; -} - -/* - * CRITICAL FIX: Marketo's forms2.css adds clearfix ::before/::after pseudo-elements to rows and cols. - * Because we use 'display: contents' on these elements to flatten the DOM for CSS Grid, - * these pseudo-elements become direct children of the Grid and take up invisible grid cells, - * creating massive empty gaps. We MUST hide them. - */ -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; -} - -/* Bulletproof rip-out for hidden elements. - Using position: absolute ensures they never become grid items, - even if Marketo dynamically applies inline display: block */ -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 +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 index 4ac6889c..423c9114 100644 --- a/hlx_statics/blocks/marketo/marketo.js +++ b/hlx_statics/blocks/marketo/marketo.js @@ -1,533 +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); - } - - // Clear the block's default HTML table so it doesn't render on the screen - block.innerHTML = ''; - - // Create the wrapper for the Marketo form - 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(); - - // Clear Marketo's sessionStorage prefill so form starts fresh each load - sessionStorage.removeItem('mktoPreFillFields'); - - // Ensure marketo-form-loader.js is loaded - 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'); - - // Clear Marketo cookie-prefilled fields using the proper API - form.vals({ - FirstName: '', LastName: '', Email: '', Phone: '', - mktoFormsCompany: '', Website: '', Country: '', - State: '', PostalCode: '', mktoCompanySize: '', - Industry: '', mktoFormsJobTitle: '', mktoFormsFunctionalArea: '', - }); - - // Inject everything outside the form to avoid Marketo's grid/style stripping - - 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); - // Timeout needed to wait for MCZ to render the consent row legend - 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 += ``; - } - - 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); - }); - - // Setup Custom submit button - 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 = ``; - } - - // Setup Consent text - 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; - - // Inject based on configured position - 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); - } - - // Aggressive cleanup: Marketo hides fields using various methods (visibility, opacity, display:none on rows). - // Because we use display: contents on rows to enable CSS Grid, some wrappers might be left exposed. - // This interval ensures any wrapper containing a hidden field is also strictly display: none. - setInterval(() => { - // Remove Marketo's injected style blocks inside the form which set margins and widths - formEl.querySelectorAll('style').forEach(s => s.remove()); - - - // Completely remove privacy/consent rows from DOM since we extract and inject their content manually - formEl.querySelectorAll('.mktoFormRow.mktoCleanedScript, .mktoFormRow.by-supplyingmycontac, .mktoFormRow.adobe-privacy').forEach(row => { - row.remove(); - }); - - // Aggressively strip margins and min-heights from ALL elements inside the form - 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'); - } - }); - - // Strip Marketo injected inline styles that break flex/grid layout - 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 Marketo applied display block/flex inline, it overrides our CSS 'display: contents'. - // Strip it unless it is explicitly 'none' (which Marketo uses to hide elements). - 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'); - } - - // Ensure rows containing only html text (like privacy policy) are completely removed - formEl.querySelectorAll('.mktoFormRow').forEach(row => { - if (row.querySelector('.mktoHtmlText')) { - row.remove(); - } - }); - - // Final aggressive sweep: Any mktoFormRow that only contains hidden/absolute field wraps - // must also be ripped out. Marketo wraps tracking inputs in empty rows, and if left as - // display: block or display: contents, they generate empty grid gaps! - formEl.querySelectorAll('.mktoFormRow').forEach(row => { - const wraps = row.querySelectorAll('.mktoFieldWrap'); - let hasVisibleField = false; - wraps.forEach(wrap => { - const style = window.getComputedStyle(wrap); - // If it's not absolutely positioned and not display none, it's a visible field - if (style.position !== 'absolute' && style.display !== 'none' && style.visibility !== 'hidden') { - hasVisibleField = true; - } - }); - - // If the row has field wrappers, but ALL of them are hidden, the row is a zombie! - // We add a custom class instead of inline styles because Marketo's JS loop - // continuously strips inline styles for rows. - 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 { - // Wrapper has no input (e.g. just empty html text) - const htmlText = wrap.querySelector('.mktoHtmlText'); - if (!htmlText || htmlText.innerHTML.trim() === '') { - shouldHide = true; - } - } - - if (shouldHide) { - // Rip hidden fields completely out of the grid using position:absolute! - // This is the ONLY way to guarantee they don't leave empty cells at the bottom of the form - // if their display property gets overridden. - 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 { - // Restore if we need to show it again dynamically - 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 = {}; - - // Validate all Marketo required fields - formEl.querySelectorAll('.mktoRequired').forEach(field => { - // Skip hidden fields (Marketo sometimes hides fields dynamically) - if (field.offsetWidth === 0 && field.offsetHeight === 0) { - // If Marketo still requires them, we must fill them with dummy data - // otherwise Marketo's built-in validation will fail silently. - 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; - } - - // Hide all custom injected elements (custom fields, submit button, consent) - 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); - }); -} +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 += ``; } 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 = ``; } 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