(function (){
const loadingClass="is-loading";
function formatError(error){
if(Array.isArray(error)&&error.length){
return error.map((e)=> e.message).join("<br />");
}
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",
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="&times;";
dismissButton.addEventListener("click", onDismiss);
dismissWrapper.appendChild(dismissButton);
node.appendChild(dismissWrapper);
}
return node;
}
function createErrorMessage(text){
return createMessage(text, "error");
}
function resetForm(form){
form.reset();
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");
}});
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");
if(oldMsg) oldMsg.remove();
if(!maxNumOfFiles) return;
if(numOfFiles <=maxNumOfFiles) return;
const errorMessage=createErrorMessage(i18nError.replace("%n", `${maxNumOfFiles}`));
parentField.appendChild(errorMessage);
}
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";
const nameNode=document.createElement("span");
nameNode.appendChild(document.createTextNode(file.name));
nameNode.classList.add("breakdance-form-files-list-item__name");
const sizeNode=document.createElement("span");
sizeNode.appendChild(document.createTextNode(size));
sizeNode.classList.add("breakdance-form-files-list-item__size");
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
});
const textNode=input.parentElement.querySelector(".breakdance-form-file-upload__text"
);
if(textNode){
if(names.length > 1){
textNode.innerHTML=i18nFilledText.replace("%n", `${names.length}`);
}else{
textNode.innerHTML=names[0];
}}
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");
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());
}}
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);
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,
};})();