marketo_suite-1.0.x-dev/js/e3_marketo_forms.js
js/e3_marketo_forms.js
/**
* @file
* Handles Marketo form injections and general functionality.
*/
(function (Drupal, cookies) {
'use strict';
/**
* Global object to hold some global Marketo Forms data.
*
* @type {Drupal~marketoForms}
*/
Drupal.marketoForms = Drupal.marketoForms || {};
/**
* Array of already loaded forms to prevent third-party Marketo scripts form
* creating duplicates.
*
* @type {Array}
*/
Drupal.marketoForms.loadedForms = Drupal.marketoForms.loadedForms || [];
/**
* Marketo forms base logic.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Inject all Marketo forms and initialize submission behaviors.
*/
Drupal.behaviors.marketoForms = {
attach: function (context, settings) {
// Marketo tracking cookie.
const marketoCookie = cookies.get('_mkto_trk');
/**
* Embed Marketo form.
*
* @param {Array} marketoConfig
* Marketo form configuration array.
*/
const init = function (marketoConfig) {
// Integrate with the GDPR module if it is installed.
let cookiesAccepted = true;
if (typeof GDPR !== 'undefined') {
cookiesAccepted = GDPR.cookiesAccepted();
}
if (cookiesAccepted && typeof Munchkin !== 'undefined') {
// Make sure Marketo cookie is added if Munchkin is set.
if (marketoCookie === undefined) {
Munchkin.init(marketoConfig.munchkinId);
}
}
else if (!cookiesAccepted) {
cookies.remove('_mkto_trk');
}
// Initialize and chain-inject Marketo Forms.
const formInstances = context.querySelectorAll("form." + marketoConfig.htmlClass);
if (formInstances.length) {
injectMarketoForms(marketoConfig);
}
// Operations to perform when form is ready after being injected.
MktoForms2.whenReady(function (form) {
const formEl = context.querySelector('.' + marketoConfig.htmlClass);
if (formEl.length && !formEl?.classList.contains('processed-marketo')) {
// Add field name class for form row.
form.getFormElem().find('.mktoField').each(function (e) {
const formField = this;
const fieldType = formField.getAttribute('type');
const elementName = formField.getAttribute('name');
if (typeof elementName !== 'undefined') {
formField.closest('.mktoFormRow')?.classList.add('mktoField' + elementName);
}
// Track the value state of text inputs.
if (fieldType !== 'checkbox' && fieldType !== 'radio') {
setInputStateTracker(formField);
}
// Track the value state of select elements.
if (formField.tagName.toLowerCase() === 'select') {
formField.closest('.mktoFieldWrap')?.classList.add('marketo-form-item');
setSelectStateTracker(formField);
}
// Track the value state of text areas.
if (formField.tagName.toLowerCase() === 'textarea') {
formField.closest('.mktoFieldWrap')?.classList.add('marketo-form-item');
setInputStateTracker(formField);
}
// Track checkboxes.
if (fieldType === 'checkbox') {
formField.closest('.mktoFieldWrap')?.classList.add('marketo-checkbox');
}
// Track radios.
if (fieldType === 'radio') {
formField.closest('.mktoFieldWrap')?.classList.add('marketo-radio');
}
});
}
// Setting a unique event for use in external scripts
const elementRenderEventName = 'whenFormElRendered' + formEl.getAttribute('data-form-id');
const elementRenderEvent = new Event(elementRenderEventName);
formEl.dispatchEvent(elementRenderEvent);
if (typeof(marketoConfig.removeSourceStyles) !== 'undefined' && marketoConfig.removeSourceStyles) {
removeMarketoSourceStylesheets(form, marketoConfig);
}
else {
// Reveal the form once it's been processed.
formEl?.classList.add('processed-marketo');
formEl.closest('.marketo-steps-wrapper')?.classList.remove('hidden');
}
});
};
/**
* Retrieve Marketo Settings.
*
* @return {Array}
* Array of defined Marketo Settings.
*/
const getMarketoFormsSettings = function () {
if (typeof(settings.marketoForms) !== 'undefined' && settings.marketoForms) {
return settings.marketoForms;
}
return [];
};
/**
* Initialise focus and value trackers for theming.
*
* @param {object} input
* Input object to process.
*/
const setInputStateTracker = function (input) {
input.closest('.mktoFieldWrap')?.classList.add('marketo-form-item');
input.addEventListener('focus', (event) => {
event.currentTarget.closest('.marketo-form-item')?.classList.add('marketo-focus-form-item');
});
input.addEventListener('blur', (event) => {
event.currentTarget.closest('.marketo-form-item')?.classList.remove('marketo-focus-form-item');
});
input.addEventListener('propertychange change paste input', function () {
const focusedItem = this;
const textVal = this.val();
if (textVal === "" || textVal.length < 1) {
focusedItem.closest('.marketo-form-item').removeClass('has-value');
} else {
focusedItem.closest('.marketo-form-item').addClass('has-value');
}
});
if (input.value) {
input.closest('.marketo-form-item')?.classList.add('has-value');
}
};
/**
* Initialize focus and state trackers for select theming.
*
* @param {object} select
* Select object to process.
*/
const setSelectStateTracker = function (select) {
select.closest('.mktoFieldWrap')?.classList.add('marketo-form-item', 'marketo-form-item-select');
select.addEventListener('focus', (event) => {
event.currentTarget.closest('.marketo-form-item')?.classList.add('marketo-focus-form-item');
});
select.addEventListener('blur', (event) => {
event.currentTarget.closest('.marketo-form-item')?.classList.remove('marketo-focus-form-item');
});
select.addEventListener('change', function (event) {
event.currentTarget.closest('.marketo-form-item')?.classList.add('has-value');
});
if (select.value) {
select.closest('.marketo-form-item')?.classList.add('has-value');
}
};
/**
* Chain load and inject all Marketo Forms.
*
* @param {Array} marketoConfig
* Configuration settings the for Form instance.
*/
const injectMarketoForms = function (marketoConfig) {
const arrayFrom = Function.prototype.call.bind(Array.prototype.slice),
marketoFormDataAttr = "data-form-id";
// Make labels unique for accessibility.
MktoForms2.whenRendered(function (form) {
const formEl = form.getFormElem()[0],
randomSuffix = "_" + new Date().getTime() + Math.random();
arrayFrom(formEl.querySelectorAll("label[for]")).forEach(function (labelEl) {
const forEl = formEl.querySelector('[id="' + labelEl.htmlFor + '"]');
if (forEl) {
labelEl.htmlFor = forEl.id = forEl.id + randomSuffix;
}
});
});
const loadForm = MktoForms2.loadForm.bind(MktoForms2, marketoConfig.instanceHost, marketoConfig.munchkinId, marketoConfig.formId),
formEls = arrayFrom(document.querySelectorAll('form[' + marketoFormDataAttr + '="' + marketoConfig.formId + '"]:not(.mktoHasWidth)'));
let dataInstances = [];
formEls.forEach(function (element, index) {
const dataInstance = element.getAttribute('data-instance');
if (dataInstance && dataInstances.indexOf(dataInstance) > -1) {
formEls.splice(index, 1);
}
else {
dataInstances.push(dataInstance);
}
});
// Chain load forms. This will ensure the same form can be loaded on a
// page multiple times.
(function loadFormCb(formEls) {
// Retrieve the form
const formEl = formEls.shift();
if (typeof(formEl) !== 'undefined') {
const dataInstance = formEl.getAttribute('data-instance');
formEl.id = "mktoForm_" + marketoConfig.formId;
// Only load the form if the form instance hasn't been loaded yet.
// Also make sure that there's no form with the ID we're about to add
// in the DOM already.
const formInstance = context.querySelector('form#marketo-form-' + marketoConfig.formId + "-" + dataInstance);
if ((!formInstance || formInstance.length < 1) &&
Drupal.marketoForms.loadedForms.indexOf(marketoConfig.formId + "-" + dataInstance) === -1) {
// Load the form.
loadForm(function (form) {
// Save loaded form for future reference.
Drupal.marketoForms.loadedForms.push(marketoConfig.formId + "-" + dataInstance);
formEl.id = 'marketo-form-' + marketoConfig.formId + "-" + dataInstance;
// Pre-fill the form if enabled.
if (marketoConfig.enablePrefill) {
prefillMarketoForm(form);
}
// Execute all post-load stuff for the form.
marketoFormPostLoad(form, marketoConfig, dataInstance);
if (formEls.length > 0) {
loadFormCb(formEls);
}
});
}
}
})(formEls);
};
/**
* Pre-fill Marketo Form.
*
* @param {Object} form
* Loaded Marketo Form object.
*/
const prefillMarketoForm = function (form) {
if (marketoCookie !== undefined) {
const prefillRequest = new XMLHttpRequest();
prefillRequest.setRequestHeader('Content-Type', 'application/json');
prefillRequest.onreadystatechange = function() {
if (prefillRequest.readyState === 4 && prefillRequest.status === 200) {
const prefillValues = {};
const currentValues = form.getValues();
const data = JSON.parse(prefillRequest.responseText);
for (let key in data) {
// Skip loop if the property is from prototype
if (!data.hasOwnProperty(key)) continue;
// Skip if the filled has already been pre-filled.
if (currentValues[key]) continue;
prefillValues[key] = data[key];
}
// Prefill data.
form.setValues(prefillValues);
// Mark pre-filled elements as having data. For theming.
let filledValues = form.vals(),
formElem = form.getFormElem();
formElem.find('input,textarea,select').each(function (el) {
let elemName = el.getAttribute('name');
if (
typeof(elemName) !== 'undefined'
&& el.getAttribute('type') !== 'hidden'
&& typeof(filledValues[elemName]) !== 'undefined'
&& filledValues[elemName] !== ''
) {
el.closest('.marketo-form-item')?.classList.add('has-value');
}
});
}
}
prefillRequest.open('POST', '/marketo/prefill', true);
prefillRequest.send(JSON.stringify({
trkValue: marketoCookie,
formFields: form.getValues()
}));
}
};
/**
* Execute all post-load Marketo operations.
*
* @param {Object} form
* Marketo Form.
* @param {Array} marketoConfig
* Marketo Form configuration.
* @param {String} dataInstance
* Configuration instance number.
*/
const marketoFormPostLoad = function(form, marketoConfig, dataInstance) {
let instanceConfig = marketoConfig[dataInstance];
// Add/set hidden fields if settings for them were added.
if (typeof(instanceConfig.hiddenFields) !== 'undefined' && instanceConfig.hiddenFields) {
form.addHiddenFields(instanceConfig.hiddenFields);
}
// Submit button text override by component.
if (typeof(instanceConfig.overrideSubmitText) !== 'undefined' && instanceConfig.overrideSubmitText) {
form.getFormElem().find('button.mktoButton').html(instanceConfig.overrideSubmitText);
}
// On successful form submission, check for settings to determine
// next steps.
form.onSuccess(function (values, followUpUrl) {
const submitEvent = new CustomEvent('marketoSubmit', { detail: {
'form': form,
'values': values,
'followUpUrl': followUpUrl,
}});
window.dispatchEvent(submitEvent);
// Run all specified submission callbacks.
if (typeof(instanceConfig.submissionCallbacks) !== 'undefined' && instanceConfig.submissionCallbacks) {
for (let marketoSubmissionCallback in instanceConfig.submissionCallbacks) {
const marketoSubmissionCallbackData = instanceConfig.submissionCallbacks[marketoSubmissionCallback];
if (typeof Drupal.behaviors.marketoForms[marketoSubmissionCallback] === "function") {
Drupal.behaviors.marketoForms[marketoSubmissionCallback](form, marketoSubmissionCallbackData, values, marketoConfig, dataInstance);
}
}
}
// Prevent the default redirects that could be set from within
// Marketo if a form has been set to use a different submission
// behavior.
if (typeof(instanceConfig.skipMarketoRedirects) !== 'undefined' && instanceConfig.skipMarketoRedirects) {
return false;
}
});
};
/**
* Remove Marketo-sourced stylesheets.
*
* @param {object} form
* Marketo form object.
* @param {Array} marketoConfig
* Marketo form config.
*
* @see http://developers.marketo.com/javascript-api/forms/api-reference/
*/
const removeMarketoSourceStylesheets = function (form, marketoConfig) {
const formElement = context.querySelector("form." + marketoConfig.htmlClass);
// Remove inline styles that Marketo adds to most elements.
document.querySelectorAll('*[class^="mkto"][style]').forEach(e => e.removeAttribute('style'));
// Remove some core Marketo css classes in favor of our own.
if (formElement) {
formElement.removeAttribute('style');
formElement.classList.remove('mktoForm')
formElement.querySelector('.mktoButton')?.classList.remove('mktoButton');
}
// Remove Marketo "required" divs and add Drupal's "form-required" class
// to form labels instead. Remove mktoClear, mktoOffset, mktoGutter
// empty div containers.
context.querySelectorAll('.mktoAsterix, .mktoClear, .mktoOffset, .mktoGutter').forEach(e => e.remove());
context.querySelectorAll('.mktoRequiredField .mktoLabel').forEach(e => e.classList.add('form-required'));
// Disable remote stylesheets and local form styles.
const arrayFrom = Function.prototype.call.bind(Array.prototype.slice);
const styleSheets = arrayFrom(document.styleSheets);
styleSheets.forEach(function (ss) {
if ([mktoForms2BaseStyle, mktoForms2ThemeStyle].indexOf(ss.ownerNode) != -1 || form.getFormElem()[0].contains(ss.ownerNode)) {
ss.disabled = true;
}
});
// Remove inline marketo custom fonts.
document.querySelectorAll('#mktoFontUrl').forEach(e => e.remove());
// Remove the rest of inline styles.
const mktoFormsHeaderStyle = document.querySelector('#mktoForms2ThemeStyle');
if (mktoFormsHeaderStyle) {
const mktoFormsHeaderStyleExtra = mktoFormsHeaderStyle.nextElementSibling;
if (mktoFormsHeaderStyleExtra) {
if (mktoFormsHeaderStyleExtra.length > 0 && mktoFormsHeaderStyleExtra.tagName.toLowerCase() === 'style') {
mktoFormsHeaderStyleExtra.remove();
}
}
}
if (formElement) {
formElement.querySelectorAll('style').forEach(e => e.remove());
// Reveal the form once it's been processed.
formElement.classList.add('processed-marketo');
formElement.closest('.marketo-steps-wrapper')?.classList.remove('hidden');
}
};
/**
* Initiate Marketo through an alternative instance host.
*
* This is mostly used to bypass Firefox tracking protection, but will
* also fire in case original forms js was not successfully loaded.
*/
const initProxyMarketo = function (marketoConfig) {
const s = document.createElement('script');
s.onload = marketoPreInit;
s.setAttribute('type', 'text/javascript');
s.setAttribute('src', marketoConfig.instanceHost + '/js/forms2/js/forms2.min.js');
document.getElementsByTagName('head')[0].appendChild(s);
};
/**
* Run final checks before initializing the Marketo Forms injection.
*
* This function is here to provide possible integration with other
* modules if there's a need to delay Marketo initialization.
*/
const marketoPreInit = function (marketoConfig) {
// If we still don't have Marketo Assets loaded at this time, load
// custom message and prevent further actions.
const form = context.querySelector("form." + marketoConfig.htmlClass);
// If the form didn't load - it got blocked by the adblock.
setTimeout(function () {
if (form && form.childElementCount < 1 && typeof marketoConfig.loadErrorMessage !== 'undefined') {
form.closest('.marketo-steps-wrapper')?.classList.remove('hidden');
form.parentElement.innerHTML = marketoConfig.loadErrorMessage;
}
}, 10000);
// Delay init if GDPR module is installed.
if (typeof GDPR !== 'undefined' && !GDPR.initialisationComplete) {
window.addEventListener("gdpr:load", function () {
init(marketoConfig);
});
}
else {
init(marketoConfig);
}
};
// Init Marketo embed procedure.
const marketoSettingsAll = getMarketoFormsSettings();
for (let marketoProp in marketoSettingsAll) {
if (!marketoSettingsAll.hasOwnProperty(marketoProp)) {
continue;
}
if (marketoProp === 'munchkinId') {
continue;
}
let marketoConfig = marketoSettingsAll[marketoProp];
if (typeof MktoForms2 === 'undefined') {
marketoConfig.instanceHost = marketoConfig.alternativeInstanceHost;
initProxyMarketo(marketoConfig);
}
else {
marketoPreInit(marketoConfig);
}
}
},
/**
* Submission callback to replace the form with a confirmation.
*
* @param {object} form
* Marketo form object.
* @param {string} behaviorData
* Submission behavior configuration.
* @param {Array} values
* Submitted values.
* @param {object} marketoConfig
* General marketo configuration.
*/
replaceWithConfirmation: function replaceWithConfirmation(form, behaviorData, values, marketoConfig) {
// Replace the form with a confirmation if it has been provided.
if (behaviorData) {
const formElem = form.getFormElem();
const marketoWrapper = formElem.closest(marketoConfig.entityWrapper);
marketoWrapper.replaceWith(behaviorData).addClass('marketo-form-submitted');
}
},
/**
* Submission callback to redirect to a page after the submission.
*
* @param {object} form
* Marketo form object.
* @param {string} behaviorData
* Submission behavior configuration.
* @param {Array} values
* Submitted values.
* @param {object} marketoConfig
* General marketo configuration.
*/
redirectToPage: function redirectToPage(form, behaviorData, values, marketoConfig) {
// Replace the form with a confirmation if it has been provided.
if (behaviorData) {
window.location.href = behaviorData;
}
}
};
})(Drupal, window.Cookies);
