{"version":3,"file":"product-filter-category.ce4a3a6c.js","sources":["../../../vendor/verbb/formie/src/web/assets/frontend/src/js/utils/utils.js","../../../vendor/verbb/formie/src/web/assets/frontend/src/js/utils/bouncer.js","../../../vendor/verbb/formie/src/web/assets/frontend/src/js/formie-form-theme.js","../../../vendor/verbb/formie/src/web/assets/frontend/src/js/formie-form-base.js","../../../vendor/verbb/formie/src/web/assets/frontend/src/js/formie-lib.js","../../../src/js/archive/product-category/product-filter-category.ts"],"sourcesContent":["export const isEmpty = function(obj) {\n return obj && Object.keys(obj).length === 0 && obj.constructor === Object;\n};\n\nexport const toBoolean = function(val) {\n return !/^(?:f(?:alse)?|no?|0+)$/i.test(val) && !!val;\n};\n\nexport const eventKey = function(eventName, namespace = null) {\n if (!namespace) {\n namespace = Math.random().toString(36).substr(2, 5);\n }\n\n return `${eventName}.${namespace}`;\n};\n\nexport const t = function(string, replacements = {}) {\n if (window.FormieTranslations) {\n string = window.FormieTranslations[string] || string;\n }\n\n return string.replace(/{([a-zA-Z0-9]+)}/g, (match, p1) => {\n if (replacements[p1]) {\n return replacements[p1];\n }\n\n return match;\n });\n};\n\nexport const ensureVariable = function(variable, timeout = 100000) {\n const start = Date.now();\n\n // Function to allow us to wait for a global variable to be available. Useful for third-party scripts.\n const waitForVariable = function(resolve, reject) {\n if (window[variable]) {\n resolve(window[variable]);\n } else if (timeout && (Date.now() - start) >= timeout) {\n reject(new Error('timeout'));\n } else {\n setTimeout(waitForVariable.bind(this, resolve, reject), 30);\n }\n };\n\n return new Promise(waitForVariable);\n};\n\nexport const waitForElement = function(selector, $element) {\n $element = $element || document;\n\n return new Promise((resolve) => {\n if ($element.querySelector(selector)) {\n return resolve($element.querySelector(selector));\n }\n\n const observer = new MutationObserver((mutations) => {\n if ($element.querySelector(selector)) {\n observer.disconnect();\n resolve($element.querySelector(selector));\n }\n });\n\n observer.observe($element, {\n childList: true,\n subtree: true,\n });\n });\n};\n","/* eslint-disable */\n\n/*!\n * formbouncerjs v1.4.6\n * A lightweight form validation script that augments native HTML5 form validation elements and attributes.\n * (c) 2020 Chris Ferdinandi\n * MIT License\n * http://github.com/cferdinandi/bouncer\n */\n\n/**\n * The plugin constructor\n * @param {DOMElement} formElement The DOM Element to use for forms to be validated\n * @param {Object} options User settings [optional]\n */\nexport const Bouncer = function(formElement, options) {\n //\n // Variables\n //\n\n var defaults = {\n\n // Classes & IDs\n\n fieldClass: 'error',\n errorClass: 'error-message',\n fieldPrefix: 'bouncer-field_',\n errorPrefix: 'bouncer-error_',\n\n // Patterns\n patterns: {\n email: /^([^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+|\\x22([^\\x0d\\x22\\x5c\\x80-\\xff]|\\x5c[\\x00-\\x7f])*\\x22)(\\x2e([^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+|\\x22([^\\x0d\\x22\\x5c\\x80-\\xff]|\\x5c[\\x00-\\x7f])*\\x22))*\\x40([^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+|\\x5b([^\\x0d\\x5b-\\x5d\\x80-\\xff]|\\x5c[\\x00-\\x7f])*\\x5d)(\\x2e([^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+|\\x5b([^\\x0d\\x5b-\\x5d\\x80-\\xff]|\\x5c[\\x00-\\x7f])*\\x5d))*(\\.\\w{2,})+$/,\n url: /^(?:(?:https?|HTTPS?|ftp|FTP):\\/\\/)(?:\\S+(?::\\S*)?@)?(?:(?!(?:10|127)(?:\\.\\d{1,3}){3})(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-zA-Z\\u00a1-\\uffff0-9]-*)*[a-zA-Z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-zA-Z\\u00a1-\\uffff0-9]-*)*[a-zA-Z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-zA-Z\\u00a1-\\uffff]{2,}))\\.?)(?::\\d{2,5})?(?:[/?#]\\S*)?$/,\n number: /^(?:[-+]?[0-9]*[.,]?[0-9]+)$/,\n color: /^#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/,\n date: /(?:19|20)[0-9]{2}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-9])|(?:(?!02)(?:0[1-9]|1[0-2])-(?:30))|(?:(?:0[13578]|1[02])-31))/,\n time: /^(?:(0[0-9]|1[0-9]|2[0-3])(:[0-5][0-9]))$/,\n month: /^(?:(?:19|20)[0-9]{2}-(?:(?:0[1-9]|1[0-2])))$/,\n },\n\n // Custom Validations\n customValidations: {},\n\n // Messages\n messageAfterField: true,\n messageCustom: 'data-bouncer-message',\n messageTarget: 'data-bouncer-target',\n // messages: {\n // missingValue: {\n // checkbox: 'This field is required.',\n // radio: 'Please select a value.',\n // select: 'Please select a value.',\n // 'select-multiple': 'Please select at least one value.',\n // default: 'Please fill out this field.',\n // },\n // patternMismatch: {\n // email: 'Please enter a valid email address.',\n // url: 'Please enter a URL.',\n // number: 'Please enter a number',\n // color: 'Please match the following format: #rrggbb',\n // date: 'Please use the YYYY-MM-DD format',\n // time: 'Please use the 24-hour time format. Ex. 23:00',\n // month: 'Please use the YYYY-MM format',\n // default: 'Please match the requested format.',\n // },\n // outOfRange: {\n // over: 'Please select a value that is no more than {max}.',\n // under: 'Please select a value that is no less than {min}.',\n // },\n // wrongLength: {\n // over: 'Please shorten this text to no more than {maxLength} characters. You are currently using {length} characters.',\n // under: 'Please lengthen this text to {minLength} characters or more. You are currently using {length} characters.',\n // },\n // fallback: 'There was an error with this field.',\n // },\n\n // Form Submission\n disableSubmit: false,\n \n // Allow blur/click/input events to be opt-out\n validateOnBlur: true,\n\n // Allow validation to be turned off altogether. Useful for server-side validation use.\n validateOnSubmit: true,\n\n // Custom Events\n emitEvents: true,\n\n };\n\n\n //\n // Methods\n //\n\n /**\n * A wrapper for Array.prototype.forEach() for non-arrays\n * @param {Array-like} arr The array-like object\n * @param {Function} callback The callback to run\n */\n var forEach = function(arr, callback) {\n Array.prototype.forEach.call(arr, callback);\n };\n\n /**\n * Merge two or more objects together.\n * @param {Object} objects The objects to merge together\n * @returns {Object} Merged values of defaults and options\n */\n var extend = function() {\n var merged = {};\n forEach(arguments, ((obj) => {\n for (var key in obj) {\n if (!obj.hasOwnProperty(key)) return;\n if (Object.prototype.toString.call(obj[key]) === '[object Object]') {\n merged[key] = extend(merged[key], obj[key]);\n } else {\n merged[key] = obj[key];\n }\n // merged[key] = obj[key];\n }\n }));\n return merged;\n };\n\n /**\n * Emit a custom event\n * @param {String} type The event type\n * @param {Object} options The settings object\n * @param {Node} anchor The anchor element\n * @param {Node} toggle The toggle element\n */\n var emitEvent = function(elem, type, details) {\n if (typeof window.CustomEvent !== 'function') return;\n var event = new CustomEvent(type, {\n bubbles: true,\n detail: details || {},\n });\n elem.dispatchEvent(event);\n };\n\n /**\n * Add the `novalidate` attribute to all forms\n * @param {Boolean} remove If true, remove the `novalidate` attribute\n */\n var addNoValidate = function(form) {\n form.setAttribute('novalidate', true);\n };\n\n /**\n * Remove the `novalidate` attribute to all forms\n */\n var removeNoValidate = function(form) {\n form.removeAttribute('novalidate');\n };\n\n /**\n * Check if a required field is missing its value\n * @param {Node} field The field to check\n * @return {Boolean} It true, field is missing it's value\n */\n var missingValue = function(field) {\n\n // If not required, bail\n if (!field.hasAttribute('required')) return false;\n\n // Handle checkboxes\n if (field.type === 'checkbox') {\n // Watch out for grouped checkboxes. Only validate the group as a whole\n var checkboxInputs = field.form.querySelectorAll('[name=\"' + escapeCharacters(field.name) + '\"]:not([type=\"hidden\"])');\n\n if (checkboxInputs.length) {\n var checkedInputs = Array.prototype.filter.call(checkboxInputs, ((btn) => {\n return btn.checked;\n })).length;\n\n return !checkedInputs;\n }\n\n return !field.checked;\n }\n\n // Don't validate any hidden fields\n if (field.type === 'hidden') {\n return false;\n }\n\n // Get the field value length\n var { length } = field.value;\n\n // Handle radio buttons\n if (field.type === 'radio') {\n length = Array.prototype.filter.call(field.form.querySelectorAll('[name=\"' + escapeCharacters(field.name) + '\"]'), ((btn) => {\n return btn.checked;\n })).length;\n }\n\n // Check for value\n return length < 1;\n\n };\n\n /**\n * Check if field value doesn't match a patter.\n * @param {Node} field The field to check\n * @param {Object} settings The plugin settings\n * @see https://www.w3.org/TR/html51/sec-forms.html#the-pattern-attribute\n * @return {Boolean} If true, there's a pattern mismatch\n */\n var patternMismatch = function(field, settings) {\n\n // Check if there's a pattern to match\n var pattern = field.getAttribute('pattern');\n pattern = pattern ? new RegExp('^(?:' + pattern + ')$') : settings.patterns[field.type];\n if (!pattern || !field.value || field.value.length < 1) return false;\n\n // Validate the pattern\n return field.value.match(pattern) ? false : true;\n\n };\n\n /**\n * Check if field value is out-of-range\n * @param {Node} field The field to check\n * @return {String} Returns 'over', 'under', or false\n */\n var outOfRange = function(field) {\n\n // Make sure field has value\n if (!field.value || field.value.length < 1) return false;\n\n // Check for range\n var max = field.getAttribute('max');\n var min = field.getAttribute('min');\n\n // Check validity\n var num = parseFloat(field.value);\n if (max && num > max) return 'over';\n if (min && num < min) return 'under';\n return false;\n\n };\n\n /**\n * Check if the field value is too long or too short\n * @param {Node} field The field to check\n * @return {String} Returns 'over', 'under', or false\n */\n var wrongLength = function(field) {\n\n // Make sure field has value\n if (!field.value || field.value.length < 1) return false;\n\n // Check for min/max length\n var max = field.getAttribute('maxlength');\n var min = field.getAttribute('minlength');\n\n // Check validity\n var { length } = field.value;\n if (max && length > max) return 'over';\n if (min && length < min) return 'under';\n return false;\n\n };\n\n /**\n * Test for standard field validations\n * @param {Node} field The field to test\n * @param {Object} settings The plugin settings\n * @return {Object} The tests and their results\n */\n var runValidations = function(field, settings) {\n return {\n missingValue: missingValue(field),\n patternMismatch: patternMismatch(field, settings),\n outOfRange: outOfRange(field),\n wrongLength: wrongLength(field),\n };\n };\n\n /**\n * Run any provided custom validations\n * @param {Node} field The field to test\n * @param {Object} errors The existing errors\n * @param {Object} validations The custom validations to run\n * @param {Object} settings The plugin settings\n * @return {Object} The tests and their results\n */\n var customValidations = function(field, errors, validations, settings) {\n for (var test in validations) {\n if (validations.hasOwnProperty(test)) {\n errors[test] = validations[test](field, settings);\n }\n }\n return errors;\n };\n\n /**\n * Check if a field has any errors\n * @param {Object} errors The validation test results\n * @return {Boolean} Returns true if there are errors\n */\n var hasErrors = function(errors) {\n for (var type in errors) {\n if (errors[type]) return true;\n }\n return false;\n };\n\n /**\n * Check a field for errors\n * @param {Node} field The field to test\n * @param {Object} settings The plugin settings\n * @return {Object} The field validity and errors\n */\n var getErrors = function(field, settings) {\n\n // Get standard validation errors\n var errors = runValidations(field,settings);\n\n // Check for custom validations\n errors = customValidations(field, errors, settings.customValidations, settings);\n\n return {\n valid: !hasErrors(errors),\n errors,\n };\n\n };\n\n /**\n * Escape special characters for use with querySelector\n * @author Mathias Bynens\n * @link https://github.com/mathiasbynens/CSS.escape\n * @param {String} id The anchor ID to escape\n */\n var escapeCharacters = function(id) {\n\n var string = String(id);\n var { length } = string;\n var index = -1;\n var codeUnit;\n var result = '';\n var firstCodeUnit = string.charCodeAt(0);\n while (++index < length) {\n codeUnit = string.charCodeAt(index);\n // Note: there’s no need to special-case astral symbols, surrogate\n // pairs, or lone surrogates.\n\n // If the character is NULL (U+0000), then throw an\n // `InvalidCharacterError` exception and terminate these steps.\n if (codeUnit === 0x0000) {\n throw new InvalidCharacterError(\n 'Invalid character: the input contains U+0000.'\n );\n }\n\n if (\n // If the character is in the range [\\1-\\1F] (U+0001 to U+001F) or is\n // U+007F, […]\n (codeUnit >= 0x0001 && codeUnit <= 0x001F) || codeUnit == 0x007F ||\n // If the character is the first character and is in the range [0-9]\n // (U+0030 to U+0039), […]\n (index === 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) ||\n // If the character is the second character and is in the range [0-9]\n // (U+0030 to U+0039) and the first character is a `-` (U+002D), […]\n (\n index === 1 &&\n codeUnit >= 0x0030 && codeUnit <= 0x0039 &&\n firstCodeUnit === 0x002D\n )\n ) {\n // http://dev.w3.org/csswg/cssom/#escape-a-character-as-code-point\n result += '\\\\' + codeUnit.toString(16) + ' ';\n continue;\n }\n\n // If the character is not handled by one of the above rules and is\n // greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or\n // is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to\n // U+005A), or [a-z] (U+0061 to U+007A), […]\n if (\n codeUnit >= 0x0080 ||\n codeUnit === 0x002D ||\n codeUnit === 0x005F ||\n codeUnit >= 0x0030 && codeUnit <= 0x0039 ||\n codeUnit >= 0x0041 && codeUnit <= 0x005A ||\n codeUnit >= 0x0061 && codeUnit <= 0x007A\n ) {\n // the character itself\n result += string.charAt(index);\n continue;\n }\n\n // Otherwise, the escaped character.\n // http://dev.w3.org/csswg/cssom/#escape-a-character\n result += '\\\\' + string.charAt(index);\n\n }\n\n // Return sanitized hash\n return result;\n\n };\n\n /**\n * Get or create an ID for a field\n * @param {Node} field The field\n * @param {Object} settings The plugin settings\n * @param {Boolean} create If true, create an ID if there isn't one\n * @return {String} The field ID\n */\n var getFieldID = function(field, settings, create) {\n var id = field.name ? field.name : field.id;\n if (!id && create) {\n id = settings.fieldPrefix + Math.floor(Math.random() * 999);\n field.id = id;\n }\n // if (field.type === 'checkbox') {\n // id += '_' + (field.value || field.id);\n // }\n return id;\n };\n\n /**\n * Special handling for radio buttons and checkboxes wrapped in labels.\n * @param {Node} field The field with the error\n * @return {Node} The field to show the error on\n */\n var getErrorField = function(field) {\n\n // If the field is a radio button, get the last item in the radio group\n // @todo if location is before, get first item\n if (field.type === 'radio' && field.name) {\n var group = field.form.querySelectorAll('[name=\"' + escapeCharacters(field.name) + '\"]');\n field = group[group.length - 1];\n }\n\n // Get the associated label for radio button or checkbox\n // if (field.type === 'radio') {\n // var label = field.closest('label') || field.form.querySelector('[for=\"' + field.id + '\"]');\n // field = label || field;\n // }\n\n if (field.type === 'checkbox' || field.type === 'radio') {\n field = field.closest('[data-field-handle]').firstChild;\n }\n\n return field;\n\n };\n\n /**\n * Get the location for a field's error message\n * @param {Node} field The field\n * @param {Node} target The target for error message\n * @param {Object} settings The plugin settings\n * @return {Node} The error location\n */\n var getErrorLocation = function(field, target, settings) {\n\n // Check for a custom error message\n var selector = field.getAttribute(settings.messageTarget);\n if (selector) {\n var location = field.form.querySelector(selector);\n if (location) {\n // @bugfix by @HaroldPutman\n // https://github.com/cferdinandi/bouncer/pull/28\n return location.firstChild || location.appendChild(document.createTextNode(''));\n }\n }\n\n // If the message should come after the field\n if (settings.messageAfterField) {\n if (!target) {\n target = field;\n }\n\n // If there's no next sibling, create one\n if (!target.nextSibling) {\n target.parentNode.appendChild(document.createTextNode(''));\n }\n\n return target.nextSibling;\n\n }\n\n // If it should come before\n return target;\n\n };\n\n /**\n * Create a validation error message node\n * @param {Node} field The field\n * @param {Object} settings The plugin settings\n * @return {Node} The error message node\n */\n var createError = function(field, settings) {\n\n // Create the error message\n var error = document.createElement('div');\n error.className = settings.errorClass;\n error.setAttribute('data-error-message', '');\n error.id = settings.errorPrefix + getFieldID(field, settings, true);\n\n // Set for accessibility\n error.setAttribute('aria-live', 'polite');\n error.setAttribute('aria-atomic', true);\n\n // If the field is a radio button or checkbox, grab the last field label\n var fieldTarget = getErrorField(field);\n\n // Inject the error message into the DOM\n var location = getErrorLocation(field, fieldTarget, settings);\n location.parentNode.insertBefore(error, location);\n\n return error;\n\n };\n\n /**\n * Get the error message test\n * @param {Node} field The field to get an error message for\n * @param {Object} errors The errors on the field\n * @param {Object} settings The plugin settings\n * @return {String|Function} The error message\n */\n var getErrorMessage = function(field, errors, settings) {\n\n // Variables\n var { messages } = settings;\n\n // Missing value error\n if (errors.missingValue) {\n return messages.missingValue[field.type] || messages.missingValue.default;\n }\n\n // Numbers that are out of range\n if (errors.outOfRange) {\n return messages.outOfRange[errors.outOfRange].replace('{max}', field.getAttribute('max')).replace('{min}', field.getAttribute('min')).replace('{length}', field.value.length);\n }\n\n // Values that are too long or short\n if (errors.wrongLength) {\n return messages.wrongLength[errors.wrongLength].replace('{maxLength}', field.getAttribute('maxlength')).replace('{minLength}', field.getAttribute('minlength')).replace('{length}', field.value.length);\n }\n\n // Pattern mismatch error\n if (errors.patternMismatch) {\n var custom = field.getAttribute(settings.messageCustom);\n if (custom) return custom;\n return messages.patternMismatch[field.type] || messages.patternMismatch.default;\n }\n\n // Custom validations\n for (var test in settings.customValidations) {\n if (settings.customValidations.hasOwnProperty(test)) {\n if (errors[test] && messages[test]) return messages[test];\n }\n }\n\n // Custom message, passed directly in\n if (errors.customMessage) {\n return errors.customMessage;\n }\n\n // Fallback error message\n return messages.fallback;\n\n };\n\n /**\n * Add error attributes to a field\n * @param {Node} field The field with the error message\n * @param {Node} error The error message\n * @param {Object} settings The plugin settings\n */\n var addErrorAttributes = function(field, error, settings) {\n field.classList.add(settings.fieldClass);\n field.setAttribute('aria-describedby', error.id);\n field.setAttribute('aria-invalid', true);\n\n var $fieldNode = field.closest('[data-field-handle]');\n\n if ($fieldNode) {\n $fieldNode.classList.add(settings.fieldClass);\n }\n };\n\n /**\n * Show error attributes on a field or radio/checkbox group\n * @param {Node} field The field with the error message\n * @param {Node} error The error message\n * @param {Object} settings The plugin settings\n */\n var showErrorAttributes = function(field, error, settings) {\n\n // If field is a radio button, add attributes to every button in the group\n if (field.type === 'radio' && field.name) {\n Array.prototype.forEach.call(document.querySelectorAll('[name=\"' + field.name + '\"]'), ((button) => {\n addErrorAttributes(button, error, settings);\n }));\n }\n\n // Otherwise, add an error class and aria attribute to the field\n addErrorAttributes(field, error, settings);\n\n };\n\n /**\n * Show an error message in the DOM\n * @param {Node} field The field to show an error message for\n * @param {Object} errors The errors on the field\n * @param {Object} settings The plugin settings\n */\n var showError = function(field, errors, settings) {\n\n // Get/create an error message\n var error = field.form.querySelector('#' + escapeCharacters(settings.errorPrefix + getFieldID(field, settings))) || createError(field, settings);\n var msg = getErrorMessage(field, errors, settings);\n error.textContent = typeof msg === 'function' ? msg(field, settings) : msg;\n\n // Add error attributes\n showErrorAttributes(field, error, settings);\n\n // Emit custom event\n if (settings.emitEvents) {\n emitEvent(field, 'bouncerShowError', {\n errors,\n });\n }\n\n };\n\n /**\n * Remove error attributes from a field\n * @param {Node} field The field with the error message\n * @param {Node} error The error message\n * @param {Object} settings The plugin settings\n */\n var removeAttributes = function(field, settings) {\n field.classList.remove(settings.fieldClass);\n field.removeAttribute('aria-describedby');\n field.removeAttribute('aria-invalid');\n\n var $fieldNode = field.closest('[data-field-handle]');\n\n if ($fieldNode) {\n $fieldNode.classList.remove(settings.fieldClass);\n }\n };\n\n /**\n * Remove error attributes from the field or radio group\n * @param {Node} field The field with the error message\n * @param {Node} error The error message\n * @param {Object} settings The plugin settings\n */\n var removeErrorAttributes = function(field, settings) {\n\n // If field is a radio button, remove attributes from every button in the group\n if (field.type === 'radio' && field.name) {\n Array.prototype.forEach.call(document.querySelectorAll('[name=\"' + field.name + '\"]'), ((button) => {\n removeAttributes(button, settings);\n }));\n return;\n }\n\n // Otherwise, add an error class and aria attribute to the field\n removeAttributes(field, settings);\n\n };\n\n /**\n * Remove an error message from the DOM\n * @param {Node} field The field with the error message\n * @param {Object} settings The plugin settings\n */\n var removeError = function(field, settings) {\n\n // Get the error message for this field\n var error = field.form.querySelector('#' + escapeCharacters(settings.errorPrefix + getFieldID(field, settings)));\n if (!error) return;\n\n // Remove the error\n error.parentNode.removeChild(error);\n\n // Remove error and a11y from the field\n removeErrorAttributes(field, settings);\n\n // Emit custom event\n if (settings.emitEvents) {\n emitEvent(field, 'bouncerRemoveError');\n }\n\n };\n\n /**\n * Remove errors from all fields\n * @param {String} selector The selector for the form\n * @param {Object} settings The plugin settings\n */\n var removeAllErrors = function(form, settings) {\n forEach(form.querySelectorAll('input, select, textarea'), ((field) => {\n removeError(field, settings);\n }));\n };\n\n //\n // Variables\n //\n\n var publicAPIs = {};\n var settings;\n\n\n //\n // Methods\n //\n \n /**\n * Show an error message in the DOM\n * @param {Node} field The field to show an error message for\n * @param {Object} errors The errors on the field\n * @param {Object} options Additional plugin settings\n */\n publicAPIs.showError = function(field, errors, options) {\n var _settings = extend(settings, options || {});\n\n return showError(field, errors, _settings);\n };\n\n /**\n * Remove an error message from the DOM\n * @param {Node} field The field with the error message\n * @param {Object} settings The plugin settings\n */\n publicAPIs.removeError = function(field, options) {\n var _settings = extend(settings, options || {});\n\n return removeError(field, _settings);\n };\n\n /**\n * Validate a field\n * @param {Node} field The field to validate\n * @param {Object} options Validation options\n * @return {Object} The validity state and errors\n */\n publicAPIs.validate = function(field, options) {\n\n // Don't validate submits, buttons, file and reset inputs, and disabled and readonly fields\n if (field.disabled || field.readOnly || field.type === 'reset' || field.type === 'submit' || field.type === 'button') return;\n\n // Local settings\n var _settings = extend(settings, options || {});\n\n // Check for errors\n var isValid = getErrors(field, _settings);\n\n // If valid, remove any error messages\n if (isValid.valid) {\n removeError(field, _settings);\n return;\n }\n\n // Otherwise, show an error message\n showError(field, isValid.errors, _settings);\n\n return isValid;\n\n };\n\n /**\n * Validate all fields in a form or section\n * @param {Node} target The form or section to validate fields in\n * @return {Array} An array of fields with errors\n */\n publicAPIs.validateAll = function(target) {\n return Array.prototype.filter.call(target.querySelectorAll('input, select, textarea'), ((field) => {\n var validate = publicAPIs.validate(field);\n return validate && !validate.valid;\n }));\n };\n\n /**\n * Run a validation on field blur\n */\n var blurHandler = function(event) {\n\n // Only run if the field is in a form to be validated\n if (!event.target.form || !event.target.form.isSameNode(formElement)) return;\n\n // Special-case for file field, blurs as soon as the selector kicks in\n if (event.target.type === 'file') return;\n\n // Don't trigger click event handling for checkbox/radio. We should use the change.\n if (event.target.type === 'checkbox' || event.target.type === 'radio') return;\n\n // Validate the field\n publicAPIs.validate(event.target);\n\n };\n\n // Leave this as opt-in for the moment, for better file-support\n var changeHandler = function(event) {\n\n // Only run if the field is in a form to be validated\n if (!event.target.form || !event.target.form.isSameNode(formElement)) return;\n\n // Only handle change events for some fields\n if (event.target.type !== 'file' && event.target.type !== 'checkbox' && event.target.type !== 'radio') return;\n\n // Validate the field\n publicAPIs.validate(event.target);\n\n };\n\n /**\n * Run a validation on a fields with errors when the value changes\n */\n var inputHandler = function(event) {\n\n // Only run if the field is in a form to be validated\n if (!event.target.form || !event.target.form.isSameNode(formElement)) return;\n\n // Only run on fields with errors\n if (!event.target.classList.contains(settings.fieldClass)) return;\n\n // Don't trigger click event handling for checkbox/radio. We should use the change.\n if (event.target.type === 'checkbox' || event.target.type === 'radio') return;\n\n // Validate the field\n publicAPIs.validate(event.target);\n\n };\n\n /**\n * Run a validation on a fields with errors when the value changes\n */\n var clickHandler = function(event) {\n\n // Only run if the field is in a form to be validated\n if (!event.target.form || !event.target.form.isSameNode(formElement)) return;\n\n // Only run on fields with errors\n if (!event.target.classList.contains(settings.fieldClass)) return;\n\n // Don't trigger click event handling for checkbox/radio. We should use the change.\n if (event.target.type === 'checkbox' || event.target.type === 'radio') return;\n\n // Validate the field\n publicAPIs.validate(event.target);\n\n };\n\n /**\n * Validate an entire form when it's submitted\n */\n var submitHandler = function(event) {\n\n // Only run on matching elements\n if (!event.target.isSameNode(formElement)) return;\n\n // Prevent form submission\n event.preventDefault();\n\n // Validate each field\n var errors = publicAPIs.validateAll(event.target);\n\n // If there are errors, focus on the first one\n if (errors.length > 0) {\n errors[0].focus();\n emitEvent(event.target, 'bouncerFormInvalid', { errors });\n return;\n }\n\n // Otherwise, submit if not disabled\n if (!settings.disableSubmit) {\n event.target.submit();\n }\n\n // Emit custom event\n if (settings.emitEvents) {\n emitEvent(event.target, 'bouncerFormValid');\n }\n\n };\n\n /**\n * Destroy the current plugin instantiation\n */\n publicAPIs.destroy = function() {\n\n // Remove event listeners\n if (settings.validateOnBlur) {\n document.removeEventListener('blur', blurHandler, true);\n document.removeEventListener('input', inputHandler, false);\n document.removeEventListener('change', changeHandler, false);\n document.removeEventListener('click', clickHandler, false);\n }\n \n if (settings.validateOnSubmit) {\n document.removeEventListener('submit', submitHandler, false);\n }\n\n // Remove all errors\n removeAllErrors(formElement, settings);\n\n // Remove novalidate attribute\n removeNoValidate(formElement);\n\n // Emit custom event\n if (settings.emitEvents) {\n emitEvent(document, 'bouncerDestroyed', {\n settings,\n });\n }\n\n // Reset settings\n settings = null;\n\n };\n\n /**\n * Instantiate a new instance of the plugin\n */\n var init = function() {\n\n // Create settings\n settings = extend(defaults, options || {});\n\n // Add novalidate attribute\n addNoValidate(formElement);\n\n // Event Listeners\n if (settings.validateOnBlur) {\n document.addEventListener('blur', blurHandler, true);\n document.addEventListener('input', inputHandler, false);\n document.addEventListener('change', changeHandler, false);\n document.addEventListener('click', clickHandler, false);\n }\n \n if (settings.validateOnSubmit) {\n document.addEventListener('submit', submitHandler, false);\n }\n\n // Emit custom event\n if (settings.emitEvents) {\n emitEvent(document, 'bouncerInitialized', {\n settings,\n });\n }\n\n };\n\n //\n // Inits & Event Listeners\n //\n\n init();\n \n return publicAPIs;\n};\n","import { t } from './utils/utils';\nimport { Bouncer } from './utils/bouncer';\n\nexport class FormieFormTheme {\n constructor($form, config = {}) {\n this.$form = $form;\n this.config = config;\n this.settings = config.settings;\n this.validationOnSubmit = !!this.settings.validationOnSubmit;\n this.validationOnFocus = !!this.settings.validationOnFocus;\n\n this.setCurrentPage(this.settings.currentPageId);\n\n if (!this.$form) {\n return;\n }\n\n this.$form.formTheme = this;\n this.form = this.$form.form;\n\n // Setup classes according to theme config\n this.loadingClass = this.form.getClasses('loading');\n this.tabErrorClass = this.form.getClasses('tabError');\n this.tabActiveClass = this.form.getClasses('tabActive');\n this.tabCompleteClass = this.form.getClasses('tabComplete');\n this.errorMessageClass = this.form.getClasses('errorMessage');\n this.successMessageClass = this.form.getClasses('successMessage');\n this.alertClass = this.form.getClasses('alert');\n this.alertErrorClass = this.form.getClasses('alertError');\n this.alertSuccessClass = this.form.getClasses('alertSuccess');\n this.tabClass = this.form.getClasses('tab');\n\n this.initValidator();\n\n // Check if this is a success page and if we need to hide the notice\n // This is for non-ajax forms, where the page has reloaded\n this.hideSuccess();\n\n // Hijack the form's submit handler, in case we need to do something\n this.addSubmitEventListener();\n\n // Save the form's current state so we can tell if its changed later on\n this.updateFormHash();\n\n // Listen to form changes if the user tries to reload\n if (this.settings.enableUnloadWarning) {\n this.addFormUnloadEventListener();\n }\n\n // Listen to tabs being clicked for ajax-enabled forms\n if (this.settings.submitMethod === 'ajax') {\n this.formTabEventListener();\n }\n }\n\n initValidator() {\n // Kick off validation - use this even if disabling client-side validation\n // so we can use a nice API handle server-side errprs\n const validatorSettings = {\n fieldClass: 'fui-error',\n errorClass: this.form.getClasses('fieldError'),\n fieldPrefix: 'fui-field-',\n errorPrefix: 'fui-error-',\n messageAfterField: true,\n messageCustom: 'data-fui-message',\n messageTarget: 'data-fui-target',\n validateOnBlur: this.validationOnFocus,\n\n // Call validation on-demand\n validateOnSubmit: false,\n disableSubmit: false,\n\n customValidations: {},\n\n messages: {\n missingValue: {\n checkbox: t('This field is required.'),\n radio: t('Please select a value.'),\n select: t('Please select a value.'),\n 'select-multiple': t('Please select at least one value.'),\n default: t('Please fill out this field.'),\n },\n\n patternMismatch: {\n email: t('Please enter a valid email address.'),\n url: t('Please enter a URL.'),\n number: t('Please enter a number'),\n color: t('Please match the following format: #rrggbb'),\n date: t('Please use the YYYY-MM-DD format'),\n time: t('Please use the 24-hour time format. Ex. 23:00'),\n month: t('Please use the YYYY-MM format'),\n default: t('Please match the requested format.'),\n },\n\n outOfRange: {\n over: t('Please select a value that is no more than {max}.'),\n under: t('Please select a value that is no less than {min}.'),\n },\n\n wrongLength: {\n over: t('Please shorten this text to no more than {maxLength} characters. You are currently using {length} characters.'),\n under: t('Please lengthen this text to {minLength} characters or more. You are currently using {length} characters.'),\n },\n\n fallback: t('There was an error with this field.'),\n },\n };\n\n // Allow other modules to modify our validator settings (for custom rules and messages)\n const registerFormieValidation = new CustomEvent('registerFormieValidation', {\n bubbles: true,\n detail: {\n validatorSettings,\n },\n });\n\n // Give a small amount of time for other JS scripts to register validations. These are lazy-loaded.\n // Maybe re-think this so we don't have to deal with event listener registration before/after dispatch?\n setTimeout(() => {\n this.$form.dispatchEvent(registerFormieValidation);\n\n this.validator = new Bouncer(this.$form, registerFormieValidation.detail.validatorSettings);\n }, 500);\n\n // After we clear any error, validate the fielset again. Mostly so we can remove global errors\n this.form.addEventListener(this.$form, 'bouncerRemoveError', (e) => {\n // Prevent an infinite loop (check behaviour with an Agree field)\n // https://github.com/verbb/formie/issues/905\n if (!this.submitDebounce) {\n // Only trigger this when live validation is disabled\n if (!this.validationOnFocus) {\n this.validate(false);\n }\n }\n });\n\n // Override error messages defined in DOM - Bouncer only uses these as a last resort\n // In future updates, we can probably remove this\n this.form.addEventListener(this.$form, 'bouncerShowError', (e) => {\n let message = null;\n const $field = e.target;\n const $fieldContainer = $field.closest('[data-field-type]');\n\n // Check if we need to move the error out of the .fui-input-container node.\n // Only the input itself should be in here.\n const $errorToMove = $field.parentNode.querySelector('[data-error-message]');\n\n if ($errorToMove && $errorToMove.parentNode.parentNode) {\n $errorToMove.parentNode.parentNode.appendChild($errorToMove);\n }\n\n // Only swap out any custom error message for \"required\" fields, so as not to override other messages\n if (e.detail && e.detail.errors && (e.detail.errors.missingValue || e.detail.errors.serverMessage)) {\n // Get the error message as defined on the input element. Use the parent to find the element\n // just to cater for some edge-cases where there might be multiple inputs (Datepicker).\n const $message = $field.parentNode.querySelector('[data-fui-message]');\n\n if ($message) {\n message = $message.getAttribute('data-fui-message');\n }\n\n // If there's a server error, it takes priority.\n if (e.detail.errors.serverMessage) {\n message = e.detail.errors.serverMessage;\n }\n\n // The error has been moved, find it again\n if ($fieldContainer) {\n const $error = $fieldContainer.querySelector('[data-error-message]');\n\n if ($error && message) {\n $error.textContent = message;\n }\n }\n }\n }, false);\n }\n\n addSubmitEventListener() {\n const $submitBtns = this.$form.querySelectorAll('[type=\"submit\"]');\n\n // Forms can have multiple submit buttons, and its easier to assign the currently clicked one\n // than tracking it through the submit handler.\n $submitBtns.forEach(($submitBtn) => {\n this.form.addEventListener($submitBtn, 'click', (e) => {\n this.$submitBtn = e.target;\n\n // Store for later if we're using text spinner\n this.originalButtonText = this.$submitBtn.textContent.trim();\n\n const submitAction = this.$submitBtn.getAttribute('data-submit-action') || 'submit';\n\n // Each submit button can do different things, to store that\n this.updateSubmitAction(submitAction);\n });\n });\n\n this.form.addEventListener(this.$form, 'onBeforeFormieSubmit', this.onBeforeSubmit.bind(this));\n this.form.addEventListener(this.$form, 'onFormieValidate', this.onValidate.bind(this));\n this.form.addEventListener(this.$form, 'onFormieSubmit', this.onSubmit.bind(this));\n this.form.addEventListener(this.$form, 'onFormieSubmitError', this.onSubmitError.bind(this));\n }\n\n onBeforeSubmit(e) {\n this.beforeSubmit();\n\n // Save for later to trigger real submit\n this.submitHandler = e.detail.submitHandler;\n }\n\n onValidate(e) {\n // If invalid, we only want to stop if we're submitting.\n if (!this.validate()) {\n this.onFormError();\n\n // Set a flag on the event, so other listeners can potentially do something\n e.detail.invalid = true;\n\n e.preventDefault();\n }\n }\n\n onSubmit(e) {\n // Stop base behaviour of just submitting the form\n e.preventDefault();\n\n // Either staight submit, or use Ajax\n if (this.settings.submitMethod === 'ajax') {\n this.ajaxSubmit();\n } else {\n // Before a server-side submit, refresh the saved hash immediately. Otherwise, the native submit\n // handler - which technically unloads the page - will trigger the changed alert.\n // But trigger an alert if we're going back, and back-submission data isn't set\n if (!this.settings.enableBackSubmission && this.form.submitAction === 'back') {\n // Don't reset the hash, trigger a warning if content has changed, because we're not submitting\n } else {\n this.updateFormHash();\n }\n\n // Triger any JS events for this page, only if submitting (not going back/saving)\n if (this.form.submitAction === 'submit') {\n this.triggerJsEvents();\n }\n\n this.$form.submit();\n }\n }\n\n onSubmitError(e) {\n this.onFormError();\n }\n\n addFormUnloadEventListener() {\n this.form.addEventListener(window, 'beforeunload', (e) => {\n if (this.savedFormHash !== this.hashForm()) {\n e.preventDefault();\n\n return e.returnValue = t('Are you sure you want to leave?');\n }\n });\n }\n\n formTabEventListener() {\n const $tabs = this.$form.querySelectorAll('[data-fui-page-tab-anchor]');\n\n $tabs.forEach(($tab) => {\n this.form.addEventListener($tab, 'click', (e) => {\n e.preventDefault();\n\n const pageIndex = e.target.getAttribute('data-fui-page-index');\n const pageId = e.target.getAttribute('data-fui-page-id');\n\n this.togglePage({\n nextPageIndex: pageIndex,\n nextPageId: pageId,\n totalPages: this.settings.pages.length,\n });\n\n // Ensure we still update the current page server-side\n const xhr = new XMLHttpRequest();\n xhr.open('GET', e.target.getAttribute('href'), true);\n xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');\n xhr.setRequestHeader('Accept', 'application/json');\n xhr.setRequestHeader('Cache-Control', 'no-cache');\n xhr.send();\n });\n });\n }\n\n hashForm() {\n const hash = {};\n const formData = new FormData(this.$form);\n\n // Exlcude some params from the hash, that are programatically changed\n // TODO, allow some form of registration for captchas.\n const excludedItems = [\n 'g-recaptcha-response',\n 'h-captcha-response',\n 'CRAFT_CSRF_TOKEN',\n '__JSCHK',\n '__DUP',\n 'beesknees',\n 'cf-turnstile-response',\n 'frc-captcha-solution',\n 'submitAction',\n ];\n\n for (const pair of formData.entries()) {\n const isExcluded = excludedItems.filter((item) => { return pair[0].startsWith(item); });\n\n if (!isExcluded.length) {\n // eslint-disable-next-line\n hash[pair[0]] = pair[1];\n }\n }\n\n return JSON.stringify(hash);\n }\n\n updateFormHash() {\n this.savedFormHash = this.hashForm();\n }\n\n validate(focus = true) {\n if (!this.validationOnSubmit) {\n return true;\n }\n\n // Only validate on submit actions\n if (this.form.submitAction !== 'submit') {\n return true;\n }\n\n let $fieldset = this.$form;\n\n if (this.$currentPage) {\n $fieldset = this.$currentPage;\n }\n\n const invalidFields = this.validator.validateAll($fieldset);\n\n // If there are errors, focus on the first one\n if (invalidFields.length > 0 && focus) {\n invalidFields[0].focus();\n }\n\n // Remove any global errors if none - just in case\n if (invalidFields.length === 0) {\n this.removeFormAlert();\n }\n\n // Set the debounce after a little bit, to prevent an infinite loop, as this method\n // is called on `bouncerRemoveError`.\n this.submitDebounce = true;\n\n setTimeout(() => {\n this.submitDebounce = false;\n }, 500);\n\n return !invalidFields.length;\n }\n\n hideSuccess() {\n const $successMessage = this.$form.parentNode.querySelector(`.${this.successMessageClass}`);\n\n if ($successMessage && this.settings.submitActionMessageTimeout) {\n const timeout = parseInt(this.settings.submitActionMessageTimeout, 10) * 1000;\n\n setTimeout(() => {\n $successMessage.remove();\n }, timeout);\n }\n }\n\n addLoading() {\n if (this.$submitBtn) {\n // Always disable the button\n this.$submitBtn.setAttribute('disabled', true);\n\n if (this.settings.loadingIndicator === 'spinner') {\n this.$submitBtn.classList.add(this.loadingClass);\n }\n\n if (this.settings.loadingIndicator === 'text') {\n this.$submitBtn.textContent = this.settings.loadingIndicatorText;\n }\n }\n }\n\n removeLoading() {\n if (this.$submitBtn) {\n // Always enable the button\n this.$submitBtn.removeAttribute('disabled');\n\n if (this.settings.loadingIndicator === 'spinner') {\n this.$submitBtn.classList.remove(this.loadingClass);\n }\n\n if (this.settings.loadingIndicator === 'text') {\n this.$submitBtn.textContent = this.originalButtonText;\n }\n }\n }\n\n onFormError(errorMessage) {\n if (errorMessage) {\n this.showFormAlert(errorMessage, 'error');\n } else {\n this.showFormAlert(this.settings.errorMessage, 'error');\n }\n\n this.removeLoading();\n }\n\n showFormAlert(text, type) {\n let $alert = this.$form.parentNode.querySelector('[data-fui-alert]');\n\n if ($alert) {\n // We have to cater for HTML entities - quick-n-dirty\n if ($alert.innerHTML !== this.decodeHtml(text)) {\n $alert.innerHTML = `${$alert.innerHTML}
${text}`;\n }\n } else {\n $alert = document.createElement('div');\n $alert.className = this.alertClass;\n $alert.setAttribute('role', 'alert');\n $alert.setAttribute('data-fui-alert', 'true');\n $alert.innerHTML = text;\n\n // Set attributes on the alert according to theme config\n this.form.applyThemeConfig($alert, 'alert', false);\n\n // For error notices, we have potential special handling on position\n if (type == 'error') {\n this.form.applyThemeConfig($alert, 'alertError', false);\n\n $alert.className += ` ${this.alertErrorClass} ${this.alertClass}-${this.settings.errorMessagePosition}`;\n\n if (this.settings.errorMessagePosition == 'bottom-form') {\n this.$submitBtn.parentNode.parentNode.insertBefore($alert, this.$submitBtn.parentNode);\n } else if (this.settings.errorMessagePosition == 'top-form') {\n this.$form.parentNode.insertBefore($alert, this.$form);\n }\n } else {\n this.form.applyThemeConfig($alert, 'alertSuccess', false);\n\n $alert.className += ` ${this.alertSuccessClass} ${this.alertClass}-${this.settings.submitActionMessagePosition}`;\n\n if (this.settings.submitActionMessagePosition == 'bottom-form') {\n // An even further special case when hiding the form!\n if (this.settings.submitActionFormHide) {\n this.$form.parentNode.insertBefore($alert, this.$form);\n } else if (this.$submitBtn.parentNode) {\n // Check if there's a submit button still. Might've been removed for multi-page, ajax.\n this.$submitBtn.parentNode.parentNode.insertBefore($alert, this.$submitBtn.parentNode);\n } else {\n this.$form.parentNode.insertBefore($alert, this.$form.nextSibling);\n }\n } else if (this.settings.submitActionMessagePosition == 'top-form') {\n this.$form.parentNode.insertBefore($alert, this.$form);\n }\n }\n }\n }\n\n showTabErrors(errors) {\n Object.keys(errors).forEach((pageId, index) => {\n const $tab = this.$form.parentNode.querySelector(`[data-fui-page-id=\"${pageId}\"]`);\n\n if ($tab) {\n $tab.parentNode.classList.add(this.tabErrorClass);\n }\n });\n }\n\n decodeHtml(html) {\n const txt = document.createElement('textarea');\n txt.innerHTML = html;\n return txt.value;\n }\n\n removeFormAlert() {\n const $alert = this.$form.parentNode.querySelector(`.${this.alertClass}`);\n\n if ($alert) {\n $alert.remove();\n }\n }\n\n removeTabErrors() {\n const $tabs = this.$form.parentNode.querySelectorAll('[data-fui-page-tab]');\n\n $tabs.forEach(($tab) => {\n $tab.classList.remove(this.tabErrorClass);\n });\n }\n\n beforeSubmit() {\n // Remove all validation errors\n Array.prototype.filter.call(this.$form.querySelectorAll('input, select, textarea'), (($field) => {\n this.validator.removeError($field);\n }));\n\n this.removeFormAlert();\n this.removeTabErrors();\n\n // Don't set a loading if we're going back and the unload warning appears, because there's no way to re-enable\n // the button after the user cancels the unload event\n if (!this.settings.enableBackSubmission && this.form.submitAction === 'back') {\n // Do nothing\n } else {\n this.addLoading();\n }\n }\n\n ajaxSubmit() {\n const formData = new FormData(this.$form);\n const method = this.$form.getAttribute('method');\n const action = this.$form.getAttribute('action');\n\n const xhr = new XMLHttpRequest();\n xhr.open(method ? method : 'POST', action ? action : window.location.href, true);\n xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');\n xhr.setRequestHeader('Accept', 'application/json');\n xhr.setRequestHeader('Cache-Control', 'no-cache');\n xhr.timeout = (this.settings.ajaxTimeout || 10) * 1000;\n\n this.beforeSubmit();\n\n xhr.ontimeout = () => {\n this.onAjaxError(t('The request timed out.'));\n };\n\n xhr.onerror = (e) => {\n this.onAjaxError(t('The request encountered a network error. Please try again.'));\n };\n\n xhr.onload = () => {\n if (xhr.status >= 200 && xhr.status < 300) {\n try {\n const response = JSON.parse(xhr.responseText);\n\n if (response.errors) {\n this.onAjaxError(response.errorMessage, response);\n } else {\n this.onAjaxSuccess(response);\n }\n } catch (e) {\n this.onAjaxError(t('Unable to parse response `{e}`.', { e }));\n }\n } else {\n this.onAjaxError(`${xhr.status}: ${xhr.statusText}`);\n }\n };\n\n xhr.send(formData);\n }\n\n afterAjaxSubmit(data) {\n // Reset the submit action, immediately, whether fail or success\n this.updateSubmitAction('submit');\n\n this.updateSubmissionInput(data);\n\n // Check if there's any events in the response back, and fire them\n if (data.events && Array.isArray(data.events) && data.events.length) {\n // An error message may be shown in some cases (for 3D secure) so remove the form-global level error notice.\n this.removeFormAlert();\n\n data.events.forEach((eventData) => {\n this.$form.dispatchEvent(new CustomEvent(eventData.event, {\n bubbles: true,\n detail: {\n data: eventData.data,\n },\n }));\n });\n }\n }\n\n onAjaxError(errorMessage, data = {}) {\n const errors = data.errors || {};\n const pageFieldErrors = data.pageFieldErrors || {};\n\n // Show an error message at the top of the form\n this.onFormError(errorMessage);\n\n // Update the page tabs (if any) to show error state\n this.showTabErrors(pageFieldErrors);\n\n // Fire a fail event\n this.submitHandler.formSubmitError(data);\n\n // Fire cleanup methods after _any_ ajax call\n this.afterAjaxSubmit(data);\n\n // Show server-side errors for each field\n Object.keys(errors).forEach((handle, index) => {\n const [error] = errors[handle];\n let selector = handle.split('.');\n selector = selector.join('][');\n\n let $field = this.$form.querySelector(`[name=\"fields[${selector}]\"]`);\n\n // Check for multiple fields\n if (!$field) {\n $field = this.$form.querySelector(`[name=\"fields[${selector}][]\"]`);\n }\n\n // Handle Repeater/Groups - a little more complicated to translate `group[0].field.handle`\n if (!$field && handle.includes('[')) {\n const blockIndex = handle.match(/\\[(.*?)\\]/)[1] || null;\n let regexString = `fields[${handle.replace(/\\./g, '][').replace(']]', ']').replace(/\\[.*?\\]/, '][rows][.*][fields]')}]`;\n regexString = regexString.replace(/\\[/g, '\\\\[').replace(/\\]/g, '\\\\]');\n\n const $targets = this.querySelectorAllRegex(new RegExp(regexString), 'name');\n\n if ($targets.length && $targets[blockIndex]) {\n $field = $targets[blockIndex];\n }\n }\n\n if ($field) {\n this.validator.showError($field, { serverMessage: error });\n\n // Focus on the first error\n if (index === 0) {\n $field.focus();\n }\n }\n });\n\n // Go to the first page with an error, for good UX\n this.togglePage(data, false);\n }\n\n onAjaxSuccess(data) {\n // Fire the event, because we've overridden the handler\n this.submitHandler.formAfterSubmit(data);\n\n // Fire cleanup methods after _any_ ajax call\n this.afterAjaxSubmit(data);\n\n // Reset the form hash, as all has been saved\n this.updateFormHash();\n\n // Triger any JS events for this page, right away before navigating away\n if (this.form.submitAction === 'submit') {\n this.triggerJsEvents();\n }\n\n // Check if we need to proceed to the next page\n if (data.nextPageId) {\n this.removeLoading();\n\n this.togglePage(data);\n\n return;\n }\n\n // If people have provided a redirect behaviour to handle their own redirecting\n if (data.redirectCallback) {\n data.redirectCallback();\n\n return;\n }\n\n // If we're redirecting away, do it immediately for nicer UX\n if (data.redirectUrl) {\n if (this.settings.submitActionTab === 'new-tab') {\n // Reset values if in a new tab. No need when in the same tab.\n this.resetForm();\n\n // Allow people to modify the target from `window` with `redirectTarget`\n data.redirectTarget.open(data.redirectUrl, '_blank');\n } else {\n data.redirectTarget.location.href = data.redirectUrl;\n }\n\n return;\n }\n\n // Delay this a little, in case we're redirecting away - better UX to just keep it loading\n this.removeLoading();\n\n // For multi-page ajax forms, deal with them a little differently.\n if (data.totalPages > 1) {\n // If we have a success message at the top, go to the first page\n if (this.settings.submitActionMessagePosition == 'top-form') {\n this.togglePage({\n nextPageIndex: 0,\n nextPageId: this.settings.pages[0].id,\n totalPages: this.settings.pages.length,\n });\n } else {\n // Otherwise, we want to hide the buttons because we have to stay on the last page\n // to show the success message at the bottom of the form. Otherwise, showing it on the\n // first page of an empty form is just plain weird UX.\n if (this.$submitBtn) {\n this.$submitBtn.remove();\n }\n\n // Remove the back button - not great UX to go back to a finished form\n // Remember, its the button and the hidden input\n const $backButtonInputs = this.$form.querySelectorAll('[data-submit-action=\"back\"]');\n\n $backButtonInputs.forEach(($backButtonInput) => {\n $backButtonInput.remove();\n });\n }\n }\n\n if (this.settings.submitAction === 'message') {\n // Allow the submit action message to be sent from the response, or fallback to static.\n const submitActionMessage = data.submitActionMessage || this.settings.submitActionMessage;\n\n this.showFormAlert(submitActionMessage, 'success');\n\n // Check if we need to remove the success message\n this.hideSuccess();\n\n if (this.settings.submitActionFormHide) {\n this.$form.style.display = 'none';\n }\n\n // Smooth-scroll to the top of the form.\n if (this.settings.scrollToTop) {\n this.scrollToForm();\n }\n }\n\n // Reset values regardless, for the moment\n this.resetForm();\n\n // Remove the submission ID input in case we want to go again\n this.removeHiddenInput('submissionId');\n\n // Reset the form hash, as all has been saved\n this.updateFormHash();\n }\n\n updateSubmitAction(action) {\n // All buttons should have a `[data-submit-action]` but just for backward-compatibility\n // assume when not present, we're submitting\n if (!action) {\n action = 'submit';\n }\n\n // Update the submit action on the form while we're at it. Store on the `$form`\n // for each of lookup on event hooks like captchas.\n this.form.submitAction = action;\n\n this.updateOrCreateHiddenInput('submitAction', action);\n }\n\n updateSubmissionInput(data) {\n if (!data.submissionId || !data.nextPageId) {\n return;\n }\n\n // Add the hidden submission input, if it doesn't exist\n this.updateOrCreateHiddenInput('submissionId', data.submissionId);\n }\n\n updateOrCreateHiddenInput(name, value) {\n let $input = this.$form.querySelector(`[name=\"${name}\"][type=\"hidden\"]`);\n\n if (!$input) {\n $input = document.createElement('input');\n $input.setAttribute('type', 'hidden');\n $input.setAttribute('name', name);\n this.$form.appendChild($input);\n }\n\n $input.setAttribute('value', value);\n }\n\n resetForm() {\n // `$form.reset()` will do most, but programatically setting `checked` for checkboxes won't be cleared\n this.$form.reset();\n\n this.$form.querySelectorAll('[type=\"checkbox\"]').forEach(($checkbox) => {\n $checkbox.removeAttribute('checked');\n });\n }\n\n removeHiddenInput(name) {\n const $input = this.$form.querySelector(`[name=\"${name}\"][type=\"hidden\"]`);\n\n if ($input) {\n $input.parentNode.removeChild($input);\n }\n }\n\n togglePage(data, scrollToTop = true) {\n // Trigger an event when a page is toggled\n this.$form.dispatchEvent(new CustomEvent('onFormiePageToggle', {\n bubbles: true,\n detail: {\n data,\n },\n }));\n\n // Hide all pages\n const $allPages = this.$form.querySelectorAll('[data-fui-page]');\n\n if (data.nextPageId) {\n $allPages.forEach(($page) => {\n // Show the current page\n if ($page.id === `${this.getPageId(data.nextPageId)}`) {\n $page.removeAttribute('data-fui-page-hidden');\n } else {\n $page.setAttribute('data-fui-page-hidden', true);\n }\n });\n }\n\n // Update tabs and progress bar if we're using them\n const $progress = this.$form.querySelector('[data-fui-progress-bar]');\n\n if ($progress && data.nextPageIndex >= 0) {\n const pageIndex = parseInt(data.nextPageIndex, 10) + 1;\n const progress = Math.round((pageIndex / data.totalPages) * 100);\n\n $progress.style.width = `${progress}%`;\n $progress.setAttribute('aria-valuenow', progress);\n $progress.textContent = `${progress}%`;\n }\n\n const $tabs = this.$form.querySelectorAll('[data-fui-page-tab]');\n\n if (data.nextPageId) {\n $tabs.forEach(($tab) => {\n // Show the current page\n if ($tab.id === `${this.tabClass}-${data.nextPageId}`) {\n $tab.classList.add(this.tabActiveClass);\n } else {\n $tab.classList.remove(this.tabActiveClass);\n }\n });\n\n let isComplete = true;\n\n $tabs.forEach(($tab) => {\n if ($tab.classList.contains(this.tabActiveClass)) {\n isComplete = false;\n }\n\n if (isComplete) {\n $tab.classList.add(this.tabCompleteClass);\n } else {\n $tab.classList.remove(this.tabCompleteClass);\n }\n });\n\n // Update the current page\n this.setCurrentPage(data.nextPageId);\n }\n\n // Smooth-scroll to the top of the form.\n if (this.settings.scrollToTop) {\n this.scrollToForm();\n }\n }\n\n setCurrentPage(pageId) {\n this.settings.currentPageId = pageId;\n this.$currentPage = this.$form.querySelector(`#${this.getPageId(pageId)}`);\n }\n\n getCurrentPage() {\n return this.settings.pages.find((page) => {\n return page.id == this.settings.currentPageId;\n });\n }\n\n getCurrentPageIndex() {\n const currentPage = this.getCurrentPage();\n\n if (currentPage) {\n return this.settings.pages.indexOf(currentPage);\n }\n\n return 0;\n }\n\n getPageId(pageId) {\n return `${this.config.formHashId}-p-${pageId}`;\n }\n\n scrollToForm() {\n // Check for scroll-padding-top or `scroll-margin-top`\n const extraPadding = parseInt(getComputedStyle(document.documentElement).scrollPaddingTop) || 0;\n const extraMargin = parseInt(getComputedStyle(document.documentElement).scrollMarginTop) || 0;\n\n // Because the form can be hidden, use the parent wrapper\n window.scrollTo({\n top: this.$form.parentNode.getBoundingClientRect().top + window.pageYOffset - 100 - extraPadding - extraMargin,\n behavior: 'smooth',\n });\n }\n\n triggerJsEvents() {\n const currentPage = this.getCurrentPage();\n\n // Find any JS events for the current page and fire\n if (currentPage && currentPage.settings.enableJsEvents) {\n const payload = {};\n\n currentPage.settings.jsGtmEventOptions.forEach((option) => {\n payload[option.label] = option.value;\n });\n\n // Push to the datalayer\n window.dataLayer = window.dataLayer || [];\n window.dataLayer.push(payload);\n }\n }\n\n querySelectorAllRegex(regex, attributeToSearch) {\n const output = [];\n\n for (const element of this.$form.querySelectorAll(`[${attributeToSearch}]`)) {\n if (regex.test(element.getAttribute(attributeToSearch))) {\n output.push(element);\n }\n }\n\n return output;\n }\n}\n","import { t } from './utils/utils';\n\nimport { FormieFormTheme } from './formie-form-theme';\n\nexport class FormieFormBase {\n constructor($form, config = {}) {\n this.$form = $form;\n this.config = config;\n this.settings = config.settings;\n this.listeners = {};\n\n if (!this.$form) {\n return;\n }\n\n this.$form.form = this;\n\n if (this.settings.outputJsTheme) {\n this.formTheme = new FormieFormTheme(this.$form, this.config);\n }\n\n // Add helper classes to fields when their inputs are focused, have values etc.\n this.registerFieldEvents(this.$form);\n\n // Hijack the form's submit handler, in case we need to do something\n this.addEventListener(this.$form, 'submit', (e) => {\n e.preventDefault();\n\n this.initSubmit();\n }, false);\n }\n\n initSubmit() {\n const beforeSubmitEvent = this.eventObject('onBeforeFormieSubmit', {\n submitHandler: this,\n });\n\n if (!this.$form.dispatchEvent(beforeSubmitEvent)) {\n return;\n }\n\n this.processSubmit();\n }\n\n processSubmit(skip = []) {\n // Add a little delay for UX\n setTimeout(() => {\n // Call the validation hooks\n if (!this.validate() || !this.afterValidate()) {\n return;\n }\n\n // Trigger Captchas\n if (!skip.includes('captcha') && !this.validateCaptchas()) {\n return;\n }\n\n // Trigger Payment Integrations\n if (!skip.includes('payment') && !this.validatePayment()) {\n return;\n }\n\n // Proceed with submitting the form, which raises other validation events\n this.submitForm();\n }, 300);\n }\n\n validate() {\n // Create an event for front-end validation (our own JS)\n const validateEvent = this.eventObject('onFormieValidate', {\n submitHandler: this,\n });\n\n return this.$form.dispatchEvent(validateEvent);\n }\n\n afterValidate() {\n // Create an event for after validation. This is mostly for third-parties.\n const afterValidateEvent = this.eventObject('onAfterFormieValidate', {\n submitHandler: this,\n });\n\n return this.$form.dispatchEvent(afterValidateEvent);\n }\n\n validateCaptchas() {\n // Create an event for captchas, separate to validation\n const validateEvent = this.eventObject('onFormieCaptchaValidate', {\n submitHandler: this,\n });\n\n return this.$form.dispatchEvent(validateEvent);\n }\n\n validatePayment() {\n // Create an event for payments, separate to validation\n const validateEvent = this.eventObject('onFormiePaymentValidate', {\n submitHandler: this,\n });\n\n return this.$form.dispatchEvent(validateEvent);\n }\n\n submitForm() {\n const submitEvent = this.eventObject('onFormieSubmit', {\n submitHandler: this,\n });\n\n if (!this.$form.dispatchEvent(submitEvent)) {\n return;\n }\n\n if (this.settings.submitMethod === 'ajax') {\n this.formAfterSubmit();\n } else {\n this.$form.submit();\n }\n }\n\n formAfterSubmit(data = {}) {\n // Add redirect behaviour for iframes to control the target\n data.redirectTarget = data.redirectTarget || window;\n\n this.$form.dispatchEvent(new CustomEvent('onAfterFormieSubmit', {\n bubbles: true,\n detail: data,\n }));\n\n // Ensure that once completed, we re-fetch the captcha value, which will have expired\n if (!data.nextPageId) {\n // Use `this.config.Formie` just in case we're not loading thie script in the global window\n // (i.e. when users import this script in their own).\n this.config.Formie.refreshFormTokens(this);\n }\n }\n\n formSubmitError(data = {}) {\n this.$form.dispatchEvent(new CustomEvent('onFormieSubmitError', {\n bubbles: true,\n detail: data,\n }));\n }\n\n formDestroy(data = {}) {\n this.$form.dispatchEvent(new CustomEvent('onFormieDestroy', {\n bubbles: true,\n detail: data,\n }));\n }\n\n registerFieldEvents($element) {\n const $wrappers = $element.querySelectorAll('[data-field-type]');\n\n $wrappers.forEach(($wrapper) => {\n const $input = $wrapper.querySelector('input, select');\n\n if ($input) {\n this.addEventListener($input, 'input', (event) => {\n $wrapper.dispatchEvent(new CustomEvent('input', {\n bubbles: false,\n detail: {\n input: event.target,\n },\n }));\n });\n\n this.addEventListener($input, 'focus', (event) => {\n $wrapper.dispatchEvent(new CustomEvent('focus', {\n bubbles: false,\n detail: {\n input: event.target,\n },\n }));\n });\n\n this.addEventListener($input, 'blur', (event) => {\n $wrapper.dispatchEvent(new CustomEvent('blur', {\n bubbles: false,\n detail: {\n input: event.target,\n },\n }));\n });\n\n $wrapper.dispatchEvent(new CustomEvent('init', {\n bubbles: false,\n detail: {\n input: $input,\n },\n }));\n }\n });\n }\n\n addEventListener(element, event, func) {\n // If the form is marked as destroyed, don't add any more event listeners.\n // This can often happen with captchas or payment integrations which are done as they appear on page.\n if (!this.destroyed) {\n this.listeners[event] = { element, func };\n const eventName = event.split('.')[0];\n\n element.addEventListener(eventName, this.listeners[event].func);\n }\n }\n\n removeEventListener(event) {\n const eventInfo = this.listeners[event] || {};\n\n if (eventInfo && eventInfo.element && eventInfo.func) {\n const eventName = event.split('.')[0];\n\n eventInfo.element.removeEventListener(eventName, eventInfo.func);\n delete this.listeners[event];\n }\n }\n\n eventObject(name, detail) {\n return new CustomEvent(name, {\n bubbles: true,\n cancelable: true,\n detail,\n });\n }\n\n getThemeConfigAttributes(key) {\n const attributes = this.settings.themeConfig || {};\n\n return attributes[key] || {};\n }\n\n getClasses(key) {\n return this.getThemeConfigAttributes(key).class || [];\n }\n\n applyThemeConfig($element, key, applyClass = true) {\n const attributes = this.getThemeConfigAttributes(key);\n\n if (attributes) {\n Object.entries(attributes).forEach(([attribute, value]) => {\n if (attribute === 'class' && !applyClass) {\n return;\n }\n\n $element.setAttribute(attribute, value);\n });\n }\n }\n}\n","import { t, isEmpty, waitForElement } from './utils/utils';\n\nimport { FormieFormBase } from './formie-form-base';\n\nexport class Formie {\n constructor() {\n this.forms = [];\n }\n\n initForms() {\n this.$forms = document.querySelectorAll('form[data-fui-form]') || [];\n\n // We use this in the CP, where it's a bit tricky to add a form ID. So check just in case.\n // Might also be handy for front-end too!\n if (!this.$forms.length) {\n this.$forms = document.querySelectorAll('div[data-fui-form]') || [];\n }\n\n this.$forms.forEach(($form) => {\n this.initForm($form);\n });\n\n // Emit a custom event to let scripts know the Formie class is ready\n document.dispatchEvent(new CustomEvent('onFormieInit', {\n bubbles: true,\n detail: {\n formie: this,\n },\n }));\n }\n\n async initForm($form, formConfig = {}) {\n if (isEmpty(formConfig)) {\n // Initialize the form class with the `data-fui-form` param on the form\n formConfig = JSON.parse($form.getAttribute('data-fui-form'));\n }\n\n if (isEmpty(formConfig)) {\n console.error('Unable to parse `data-fui-form` form attribute for config. Ensure this attribute exists on your form and contains valid JSON.');\n\n return;\n }\n\n // Check if we are initializing a form multiple times\n const initializeForm = this.getFormByHashId(formConfig.formHashId);\n\n if (initializeForm) {\n // Wait until the form is destroyed first before initializing again\n await this.destroyForm(initializeForm);\n }\n\n // See if we need to init additional, conditional JS (field, captchas, etc)\n const registeredJs = formConfig.registeredJs || [];\n\n // Add an instance to this factory to the form config\n formConfig.Formie = this;\n\n // Create the form class, save it to our collection\n const form = new FormieFormBase($form, formConfig);\n\n this.forms.push(form);\n\n // Find all `data-field-config` attributes for the current page and form\n // and build an object of them to initialize when loaded.\n form.fieldConfigs = this.parseFieldConfig($form, $form);\n\n // Is there any additional JS config registered for this form?\n if (registeredJs.length) {\n // Check if we've already loaded scripts for this form\n if (document.querySelector(`[data-fui-scripts=\"${formConfig.formHashId}\"]`)) {\n console.warn(`Formie scripts already loaded for form #${formConfig.formHashId}.`);\n\n return;\n }\n\n // Create a container to add these items to, so we can destroy them later\n form.$registeredJs = document.createElement('div');\n form.$registeredJs.setAttribute('data-fui-scripts', formConfig.formHashId);\n document.body.appendChild(form.$registeredJs);\n\n // Create a `