/* global grecaptcha */ (function () { const loadingClass = "is-loading"; function formatError(error) { if (Array.isArray(error) && error.length) { return error.map((e) => e.message).join("
"); } if (typeof error === "object") return error.message; } function interceptResponse(response) { return response.json().then((body) => { if (!response.ok) { throw new Error(formatError(body.data)); } return body; }); } function postData(url, data) { const { makeAjaxRequest } = window.BreakdanceFrontend.utils; const payload = { method: "POST", credentials: "same-origin", // Needed in order to store cookies. body: data, }; return makeAjaxRequest(url, payload).then(interceptResponse); } function createMessage(text, type = "success", onDismiss = null) { const node = document.createElement("div"); node.innerHTML = text; node.classList.add("breakdance-form-message"); node.classList.add("breakdance-form-message--" + type); if (typeof onDismiss === "function") { node.classList.add("breakdance-form-message-dismissable"); const dismissWrapper = document.createElement("div"); dismissWrapper.classList.add("breakdance-form-message-dismiss"); const dismissButton = document.createElement("button"); dismissButton.classList.add("breakdance-form-message-dismiss-button"); dismissButton.innerHTML = "×"; dismissButton.addEventListener("click", onDismiss); dismissWrapper.appendChild(dismissButton); node.appendChild(dismissWrapper); } return node; } function createErrorMessage(text) { return createMessage(text, "error"); } function resetForm(form) { form.reset(); // Reset file fields const fileFields = form.querySelectorAll(".breakdance-form-field--file"); fileFields.forEach((field) => { const fileText = field.querySelector(".breakdance-form-file-upload__text"); if (fileText) { const emptyText = field.querySelector(".breakdance-form-file-upload__input").dataset.i18nEmptyText; fileText.textContent = emptyText; } const list = field.querySelector(".breakdance-form-files-list"); if (list) { list.innerHTML = ""; list.classList.remove("is-files-visible"); } }); // Reset conditional fields and steps resetConditionalFields(form); if (form.dataset.steps >= 1) resetSteps(form); } function safeEval(code, form, formData) { const formValues = { post_id: formData.get("post_id"), form_id: formData.get("form_id"), }; for (const [name, value] of formData.entries()) { if (name.startsWith("fields[")) { const parts = name.match(/\[/g).length; const index = name.match(/\[(.*?)\]/)[1]; if (parts >= 2) { if (!formValues[index]) { formValues[index] = []; } formValues[index].push(value); } else { formValues[index] = value; } } } try { eval(code); } catch (e) { console.warn("Could not run Custom JavaScript.", e); } } function getRecaptchaToken(apiKey) { const payload = { action: "breakdance_submit" }; return new Promise((resolve, reject) => { grecaptcha.ready(() => { try { const token = grecaptcha.execute(apiKey, payload); return resolve(token); } catch (error) { return reject(error); } }); }); } function getOptions(form) { const options = JSON.parse(form.getAttribute("data-options")); const defaultOptions = { name: "empty", ajaxUrl: null, successMessage: null, errorMessage: null, clearOnSuccess: true, hideOnSuccess: false, redirectUrl: null, customJavaScript: {}, popupsOnSuccess: [], popupsOnError: [], recaptcha: { key: null, enabled: false, }, }; if (!options) { return defaultOptions; } return Object.assign({}, defaultOptions, options); } async function onSubmit(event) { event.preventDefault(); const form = event.currentTarget; const loading = form.classList.contains(loadingClass); const options = getOptions(form); if (loading) return; const formData = new FormData(form); formData.append("action", `breakdance_form_${options.slug}`); beforeSubmit(form); if (options.recaptcha.enabled) { try { const token = await getRecaptchaToken(options.recaptcha.key); formData.append("recaptcha_token", token); } catch (error) { console.error(error); } } postData(options.ajaxUrl, formData) .then((response) => onRequestSuccess(response, form, formData)) .catch((error) => onRequestError(error, form, formData)) .finally(() => afterSubmit(form)); } function beforeSubmit(form) { form.classList.add(loadingClass); const messages = form.parentElement.querySelectorAll(".breakdance-form-message"); messages.forEach((msg) => { msg.remove(); }) } function afterSubmit(form) { form.classList.remove(loadingClass); } function onRequestError(error, form, formData) { console.debug("[BREAKDANCE] Received a form error:", error.message); const options = getOptions(form); const message = error.message || options.errorMessage || "An unexpected error occurred."; const errorNode = createErrorMessage(message); form.after(errorNode); if (options.popupsOnError.length > 0) { options.popupsOnError.forEach((errorPopup) => { if (errorPopup.popup && errorPopup.action) { BreakdancePopup.runAction(errorPopup.popup, errorPopup.action); } }); } safeEval(options.customJavaScript?.js_on_error, form, formData); } function onRequestSuccess(response, form, formData) { console.debug("[BREAKDANCE] Received form response:", response); const options = getOptions(form); const redirectOnSuccess = options.redirect && options.redirectUrl; if (options.successMessage && !redirectOnSuccess) { let messageNode = createMessage(options.successMessage); if (options.hideOnSuccess) { form.classList.add("breakdance-form--hidden"); messageNode = createMessage(options.successMessage, "success", () => { form.classList.remove("breakdance-form--hidden"); messageNode.remove(); }); } form.after(messageNode); } safeEval(options.customJavaScript?.js_on_success, form, formData); if (options.clearOnSuccess) { resetForm(form); } if (options.popupsOnSuccess.length > 0) { options.popupsOnSuccess.forEach((successPopup) => { if (successPopup.popup && successPopup.action) { BreakdancePopup.runAction(successPopup.popup, successPopup.action); } }); } if (redirectOnSuccess) { location.href = options.redirectUrl; } } function onInputUpdate(event) { const parent = event.currentTarget.closest(".breakdance-form-field"); const activeClass = "breakdance-form-field--filled"; if (!parent) return; if (event.currentTarget.value.length) { parent.classList.add(activeClass); } else { parent.classList.remove(activeClass); } } function removeFile(input, fileToRemove, fileIndex) { const dt = new DataTransfer(); for (let file of input.files) { if (file !== fileToRemove) { dt.items.add(file); } } input.files = dt.files; onFileInputChange(input); } function maybeShowInlineFileValidation({ input, numOfFiles, maxNumOfFiles, i18nError }) { const parentField = input.closest(".breakdance-form-field"); const oldMsg = parentField.querySelector(".breakdance-form-message"); // input.setCustomValidity(""); if (oldMsg) oldMsg.remove(); if (!maxNumOfFiles) return; // No max value set, unlimited files is allowed. if (numOfFiles <= maxNumOfFiles) return; const errorMessage = createErrorMessage(i18nError.replace("%n", `${maxNumOfFiles}`)); // Only %n file(s) allowed. parentField.appendChild(errorMessage); // input.setCustomValidity(i18nError.replace("%n", `${maxNumOfFiles}`)); } function updateFilesList(input, files, listNode) { const fragment = document.createDocumentFragment(); files.forEach((file, index) => { const li = document.createElement("li"); const size = file.size < 1000000 ? Math.floor(file.size / 1000) + " KB" : Math.floor(file.size / 1000000) + " MB"; // Name const nameNode = document.createElement("span"); nameNode.appendChild(document.createTextNode(file.name)); nameNode.classList.add("breakdance-form-files-list-item__name"); // Size const sizeNode = document.createElement("span"); sizeNode.appendChild(document.createTextNode(size)); sizeNode.classList.add("breakdance-form-files-list-item__size"); // Delete Button const deleteBtn = document.createElement("button"); deleteBtn.classList.add("breakdance-form-files-list-item__delete"); deleteBtn.setAttribute("aria-label", "Delete File"); deleteBtn.setAttribute("type", "button"); deleteBtn.addEventListener("click", () => { removeFile(input, file, index); }) li.appendChild(nameNode); li.appendChild(sizeNode); li.appendChild(deleteBtn); li.classList.add("breakdance-form-files-list-item"); fragment.appendChild(li); }); listNode.innerHTML = ""; listNode.appendChild(fragment); listNode.classList.add("is-files-visible"); } function onFileInputChange(input) { const files = Array.from(input.files); const names = files.map((f) => f.name); const maxNumOfFiles = Number(input.dataset.maxFiles); const i18nFilledText = input.dataset.i18nFilledText; const i18nError = input.dataset.i18nErrorText; maybeShowInlineFileValidation({ input, numOfFiles: files.length, maxNumOfFiles, i18nError }); // Inline Upload const textNode = input.parentElement.querySelector( ".breakdance-form-file-upload__text" ); if (textNode) { if (names.length > 1) { textNode.innerHTML = i18nFilledText.replace("%n", `${names.length}`); // Text: %n files } else { textNode.innerHTML = names[0]; // Show file name if only one selected } } // Drag and Drop const listNode = input.closest(".breakdance-form-field--file").querySelector(".breakdance-form-files-list"); if (!listNode) return; updateFilesList(input, files, listNode); } function bindEvents(form) { bindFileEvents(form); const inputs = Array.from(form.querySelectorAll("input, select, textarea")); inputs.forEach((input) => { input.addEventListener("input", onInputUpdate); }); form.addEventListener("submit", onSubmit); } function bindDropzone(dropzone) { const dragOver = (event) => { event.preventDefault(); event.stopPropagation(); dropzone.classList.add("is-dragging"); }; const dragLeave = (event) => { event.preventDefault(); event.stopPropagation(); dropzone.classList.remove("is-dragging"); }; const drop = (event) => { event.preventDefault(); event.stopPropagation(); const fileInput = dropzone.querySelector("input[type='file']"); if (fileInput) { fileInput.files = event.dataTransfer.files; onFileInputChange(fileInput); } }; dropzone.addEventListener("dragover", dragOver); dropzone.addEventListener("dragenter", dragOver); dropzone.addEventListener("dragleave", dragLeave); dropzone.addEventListener("dragend", dragLeave); dropzone.addEventListener("drop", dragLeave); dropzone.addEventListener("drop", drop); } function bindFileEvents(form) { const dropZones = Array.from( form.querySelectorAll(".breakdance-form-file-upload") ); const inputs = Array.from( form.querySelectorAll(".breakdance-form-file-upload__input") ); inputs.forEach((input) => input.addEventListener("change", () => { if (!input.files.length) return; onFileInputChange(input); }) ); dropZones.forEach((dropzone) => bindDropzone(dropzone)); } function unbindEvents(form) { const inputs = Array.from(form.querySelectorAll("input, select, textarea")); inputs.forEach((input) => { input.removeEventListener("input", onInputUpdate); }); form.removeEventListener("submit", onSubmit); form.bdMask?.destroy(); } function scrollIntoViewIfNeeded(element) { const rect = element.getBoundingClientRect(); const notVisible = rect.top < 0; if (notVisible) { element.scrollIntoView({ behavior: "smooth", block: "start" }); } } function initConditionalFields(form, attachEventListeners = false) { const inputs = Array.from( form.querySelectorAll("input, select, textarea, .form-input-html") ); inputs.forEach((input) => { const { conditionalFieldId, conditionalValue, conditionalOperand } = input.dataset; if (!conditionalFieldId) { return; } const conditionalFields = form.querySelectorAll( `[name="fields[${conditionalFieldId}]"],[name="fields[${conditionalFieldId}][]"]` ); const wrapper = input.closest(".breakdance-form-field"); // Show or hide conditional field on change conditionalFields.forEach((conditionalField) => { const runValidation = () => { showOrHideConditionalField( form, input, wrapper, conditionalFieldId, conditionalValue, conditionalOperand ); }; if (attachEventListeners) { const useChangeEvent = conditionalField.matches( "input[type=radio], input[type=checkbox], input[type=file], select, input[type=number], input[type=date]" ); if (useChangeEvent) { conditionalField.addEventListener("input", () => runValidation()); conditionalField.addEventListener("change", () => runValidation()); } else { conditionalField.addEventListener("keyup", () => runValidation()); } } // also run on init to set the initial state runValidation(); }); }); } function showOrHideConditionalField( form, inputElement, fieldElement, conditionalFieldId, conditionalValue, conditionalOperand ) { const fieldValue = getConditionalFieldValue(form, conditionalFieldId); if (shouldShowField(fieldValue, conditionalOperand, conditionalValue)) { inputElement.removeAttribute("disabled"); fieldElement.classList.remove("breakdance-form-field--condition-hidden"); return; } inputElement.disabled = true; fieldElement.classList.add("breakdance-form-field--condition-hidden"); } function getConditionalFieldValue(form, conditionalFieldId) { const selector = `[name="fields[${conditionalFieldId}]"],[name="fields[${conditionalFieldId}][]"], [type='file'][name="fields[${conditionalFieldId}][]"]`; const conditionalFields = Array.from(form.querySelectorAll(selector)); if (!conditionalFields.length) return null; const values = conditionalFields.map((field) => { const isCheckboxOrRadio = ["checkbox", "radio"].includes(field.type); if (isCheckboxOrRadio && !field.checked) { return null; } return field.value; }); return values?.filter((v) => v !== null).join(","); } function resetConditionalFields(form) { initConditionalFields(form, false); } function shouldShowField(aValue, operand, bValue) { const aValueIsEmpty = aValue === "" || aValue === null; const bValueIsEmpty = bValue === "" || bValue === null; if (operand === "equals") { return aValue == bValue; } else if (operand === "not equals") { return aValue != bValue; } else if (operand === "is set") { return aValue; } else if (operand === "is not set") { return !aValue; } else if (operand === "is one of") { const bValueArray = bValue.split(","); return bValueArray.some((x) => x.trim() == aValue); } else if (operand === "is none of") { const bValueArray = bValue.split(","); return !bValueArray.some((x) => x.trim() == aValue); } else if (operand === "contains") { if (typeof aValue === "string" && typeof bValue === "string") { return aValue.toLowerCase().includes(bValue.toLowerCase()); } return false; } else if (operand === "does not contain") { if (typeof aValue === "string" && typeof bValue === "string") { return !aValue.toLowerCase().includes(bValue.toLowerCase()); } return true; } else if (operand === "is greater than") { const aValueInteger = parseInt(aValue); const bValueInteger = parseInt(bValue); if (isNaN(aValueInteger) || isNaN(bValueInteger)) { return false; } return aValueInteger > bValueInteger; } else if (operand === "is less than") { const aValueInteger = parseInt(aValue); const bValueInteger = parseInt(bValue); if (isNaN(aValueInteger) || isNaN(bValueInteger)) { return false; } return aValueInteger < bValueInteger; } else if (operand === "is before date") { if (aValueIsEmpty || bValueIsEmpty) { return false; } const aDateValue = new Date(aValue); const bDateValue = new Date(bValue); const aValueIsValidDate = aDateValue instanceof Date && isFinite(aDateValue); const bValueIsValidDate = bDateValue instanceof Date && isFinite(bDateValue); if (!aValueIsValidDate || !bValueIsValidDate) { return false; } return aDateValue < bDateValue; } else if (operand === "is after date") { if (aValueIsEmpty || bValueIsEmpty) { return false; } const aDateValue = new Date(aValue); const bDateValue = new Date(bValue); const aValueIsValidDate = aDateValue instanceof Date && isFinite(aDateValue); const bValueIsValidDate = bDateValue instanceof Date && isFinite(bDateValue); if (!aValueIsValidDate || !bValueIsValidDate) { return false; } return aDateValue > bDateValue; } else if (operand === "is before time") { if (aValueIsEmpty || bValueIsEmpty) { return false; } const todaysDate = new Date().toDateString(); const aDateValue = new Date(`${todaysDate} ${aValue}`); const bDateValue = new Date(`${todaysDate} ${bValue}`); const aValueIsValidDate = aDateValue instanceof Date && isFinite(aDateValue); const bValueIsValidDate = bDateValue instanceof Date && isFinite(bDateValue); if (!aValueIsValidDate || !bValueIsValidDate) { return false; } return aDateValue < bDateValue; } else if (operand === "is after time") { if (aValueIsEmpty || bValueIsEmpty) { return false; } const todaysDate = new Date().toDateString(); const aDateValue = new Date(`${todaysDate} ${aValue}`); const bDateValue = new Date(`${todaysDate} ${bValue}`); const aValueIsValidDate = aDateValue instanceof Date && isFinite(aDateValue); const bValueIsValidDate = bDateValue instanceof Date && isFinite(bDateValue); if (!aValueIsValidDate || !bValueIsValidDate) { return false; } return aDateValue > bDateValue; } return true; } function destroy(selector) { const form = document.querySelector(selector); if (!form) { console.warn("[BREAKDANCE] Could not find form to destroy:", selector); return; } unbindEvents(form); } function initSteps(form, isBuilder = false) { if (form.dataset.steps == 0) return; const nextStepButtons = form.querySelectorAll( ".breakdance-form-button__next-step" ); nextStepButtons.forEach((button) => button.addEventListener("click", () => { nextStep(form, isBuilder); }) ); const previousStepButtons = form.querySelectorAll( ".breakdance-form-button__previous-step" ); previousStepButtons.forEach((button) => button.addEventListener("click", () => { previousStep(form); }) ); showOrHideSteps(form); } function validateStep(form, step) { const inputs = Array.from( form.querySelectorAll( `[data-form-step='${step}'] input,[data-form-step='${step}'] select, [data-form-step='${step}'] textarea` ) ); return inputs.every((input) => { input.reportValidity(); return input.checkValidity(); }); } function getFirstInputInStep(form, step) { const firstFieldInStep = form.querySelector( `.breakdance-form-field[data-form-step="${step}"]:not(.breakdance-form-field--hidden, .breakdance-form-field--html, .breakdance-form-field--condition-hidden)` ); if (firstFieldInStep) { return firstFieldInStep.querySelector( `input, textarea, select` ); } return null; } function nextStep(form, isBuilder) { const currentStep = parseInt(form.dataset.currentStep); if (!validateStep(form, currentStep) && !isBuilder) { return; } const nextStep = currentStep + 1; setStep(form, nextStep); // Set focus on first element in active step const firstInput = getFirstInputInStep(form, nextStep); if (firstInput) { firstInput.focus(); } scrollIntoViewIfNeeded(form); } function previousStep(form) { const currentStep = parseInt(form.dataset.currentStep); setStep(form, currentStep - 1); scrollIntoViewIfNeeded(form); } function setStep(form, step) { form.dataset.currentStep = step.toString(); showOrHideSteps(form); } function showOrHideSteps(form) { const currentStep = parseInt(form.dataset.currentStep); const totalSteps = parseInt(form.dataset.steps); const fields = form.querySelectorAll( ".breakdance-form-field:not(.breakdance-form-footer)" ); fields.forEach((field) => { const formStep = parseInt(field.dataset.formStep); if (formStep === currentStep) { field.classList.remove("hidden-step"); } else { field.classList.add("hidden-step"); } }); const submitButton = form.querySelector(".breakdance-form-button__submit"); const nextStepButton = form.querySelector( `.breakdance-form-field[data-form-step="${currentStep}"] .breakdance-form-button__next-step` ); const previousStepButton = form.querySelector( `.breakdance-form-field[data-form-step="${currentStep}"] .breakdance-form-button__previous-step` ); if (currentStep === totalSteps) { submitButton.classList.remove("hidden"); nextStepButton.classList.add("hidden"); } else { submitButton.classList.add("hidden"); nextStepButton.classList.remove("hidden"); } if (currentStep > 1) { previousStepButton.classList.remove("hidden"); } else { previousStepButton.classList.add("hidden"); } const steps = form.querySelectorAll(".breakdance-form-stepper__step"); if (steps) { steps.forEach((step) => { const stepperStep = parseInt(step.dataset.stepperStep); if (stepperStep <= currentStep) { step.classList.add("is-active"); } else { step.classList.remove("is-active"); } }); const currentStepper = form.querySelector( `[data-stepper-step="${currentStep}"]` ); if (currentStepper) { currentStepper.classList.add("is-active"); } } } function resetSteps(form) { setStep(form, 1); } function initMask(form, selector) { if (typeof Maska === "undefined") return; const tokens = { 'z': { pattern: /[a-z|A-Z]/, multiple: true }, 'Z': { pattern: /[A-Z]/, multiple: true, transform: (chr) => chr.toLocaleUpperCase(), }, 0: { pattern: /[0-9]/, optional: true }, 9: { pattern: /[0-9]/, repeated: true }, } form.bdMask = new Maska.MaskInput(`${selector} [data-maska]`, { tokens }); } function init(selector) { const form = document.querySelector(selector); if (!form) { console.warn("[BREAKDANCE] Could not find form:", selector); return; } bindEvents(form); initConditionalFields(form, true); initSteps(form); initMask(form, selector); } window.breakdanceForm = { init, destroy, initMask, initConditionalFields, initSteps, }; })();