1import { badWordsList } from './badwordlist.js'; 2 3const errorMessagesByType = { 4 checkbox: 'Field is required.', 5 date: 'Please select a date for {name}.', 6 email: 'Please enter a valid {name}.', 7 files: 'Please upload a file for {name}.', 8 matrix: 'Please complete the matrix for {name}.', 9 multi_select: 'Please select at least one option for {name}.', 10 'nf-code': 'Please enter the code for {name}.', 11 'nf-divider': '', 12 'nf-page-break': '', 13 'nf-text': 'Please enter a response for {name}.', 14 number: 'Please enter a number for {name}.', 15 phone_number: 'Please enter a phone number for {name}.', 16 rating: 'Please provide a rating for {name}.', 17 scale: 'Please select a scale value for {name}.', 18 select: 'Please select an option for {name}.', 19 signature: 'Please provide your signature for {name}.', 20 slider: 'Please adjust the slider for {name}.', 21 text: 'Please enter {name}.', 22 url: 'Please enter a valid URL for {name}.', 23 // â New types based on logic (if used) 24 exists_in_submissions: 'The {name} already exists. Please provide a unique value.', 25 does_not_exist_in_submissions: 'The {name} does not exist in previous submissions.', 26}; 27function getFormFields(formId) { 28 return window.formFieldsMap?.[formId] || []; 29} 30function buildFieldNameMap(formFields) { 31 const map = {}; 32 formFields.forEach(f => { 33 map[f.id] = f.name || f.id; 34 }); 35 return map; 36} 37window.formRuntime = window.formRuntime || {}; 38 39 40(window.formIds || []).forEach(function (formId) { 41 const formFields = getFormFields(formId); 42 window.formRuntime[formId] = { 43 formFields, 44 fieldNameMap: buildFieldNameMap(formFields) 45 }; 46 initFormBuilder(formId, formFields); 47}); 48//alert(formId); 49//const formFields = getFormFields(formId); 50function initFormBuilder(formId, formFields) { 51 const rules = {}; 52 const messages = {}; 53 formFields.forEach(field => { 54 const fieldId = field.id; 55 const $form = $('#formbuildername_' + formId); 56 const nameAttr = $form.find(`[id="${fieldId}"]`).attr('name'); 57 const fieldName = field.name || 'this field'; 58 const placeholderName = field.placeholder || 'this field'; 59 const inputType = field.type; 60 const multiLines = field?.multi_lines || false; 61 const actions = field?.logic?.actions || []; 62 const hasMakeItOptional = actions.includes('make-it-optional'); 63 const hasMakeItRequied = actions.includes('require-answer'); 64// alert(hasMakeItRequied); 65 66 let hasRequired = false; 67 if (typeof field.required === 'undefined') { 68 hasRequired = false; 69 } 70 if ((field.required === '1' || field.required === 1 || field.required === true || field.required == true)) { 71 hasRequired = true; 72 } 73// if ((field.required === '1' || field.required === 1 || field.required === true || hasMakeItRequied === true)) 74// { 75 if (inputType === 'select') { 76 const isWithoutDropdown = field?.without_dropdown; 77 if (isWithoutDropdown) { 78 if (hasRequired) { 79 rules[`${fieldId}`] = {checkedRequired: `${fieldId}`}; 80 let messageTemplate = errorMessagesByType[inputType] || 'Please complete {name}.'; 81 messageTemplate = messageTemplate.replace('{name}', fieldName.toLowerCase()); 82 messages[`${fieldId}`] = {checkedRequired: messageTemplate}; 83 } 84 } else { 85 rules[nameAttr] = {required: hasRequired}; 86 let messageTemplate = errorMessagesByType[inputType] || 'Please complete {name}.'; 87 messageTemplate = messageTemplate.replace('{name}', fieldName.toLowerCase()); 88 messages[nameAttr] = {required: messageTemplate}; 89 } 90 } else if (inputType === 'multi_select') { 91 const isWithoutDropdown = field?.without_dropdown; 92 if (isWithoutDropdown) { 93 if (hasRequired) { 94 rules[`${fieldId}[]`] = {checkedRequired: `${fieldId}[]`}; 95 let messageTemplate = errorMessagesByType[inputType] || 'Please complete {name}.'; 96 messageTemplate = messageTemplate.replace('{name}', fieldName.toLowerCase()); 97 messages[`${fieldId}[]`] = {checkedRequired: messageTemplate}; 98 } 99 } else { 100 rules[`${fieldId}[]`] = {required: hasRequired}; 101 let messageTemplate = errorMessagesByType[inputType] || 'Please complete {name}.'; 102 messageTemplate = messageTemplate.replace('{name}', fieldName.toLowerCase()); 103 messages[`${fieldId}[]`] = {required: messageTemplate}; 104 } 105 } else if (inputType === 'matrix') { 106 if (hasRequired) { 107 rules[fieldId] = {matrixRequired: fieldId}; 108 let messageTemplate = errorMessagesByType[inputType] || 'Please complete {name}.'; 109 messageTemplate = messageTemplate.replace('{name}', fieldName.toLowerCase()); 110 messages[fieldId] = {matrixRequired: messageTemplate}; 111 } 112 } else if (inputType === 'rating') { 113 if (hasRequired) { 114 rules[fieldId] = {ratingRequired: hasRequired}; 115 let messageTemplate = errorMessagesByType[inputType] || 'Please complete {name}.'; 116 messageTemplate = messageTemplate.replace('{name}', fieldName.toLowerCase()); 117 messages[fieldId] = {ratingRequired: messageTemplate}; 118 } 119 } else if (inputType === 'scale') { 120 if (hasRequired) { 121 rules[fieldId] = {scaleRequired: fieldId}; 122 let messageTemplate = errorMessagesByType[inputType] || 'Please complete {name}.'; 123 messageTemplate = messageTemplate.replace('{name}', fieldName.toLowerCase()); 124 messages[fieldId] = {scaleRequired: messageTemplate}; 125 } 126 } else if (inputType === 'slider') { 127 if (hasRequired) { 128 rules[fieldId] = {sliderRequired: hasRequired}; 129 let messageTemplate = errorMessagesByType[inputType] || 'Please complete {name}.'; 130 messageTemplate = messageTemplate.replace('{name}', fieldName.toLowerCase()); 131 messages[fieldId] = {sliderRequired: messageTemplate}; 132 } 133 } else if (inputType === 'files') { 134 rules[nameAttr] = { 135 required: hasRequired 136 }; 137 let messageTemplate = errorMessagesByType[inputType] || 'Please complete {name}.'; 138 messageTemplate = messageTemplate.replace('{name}', fieldName.toLowerCase()); 139 messages[nameAttr] = { 140 required: messageTemplate 141 };
142 // â Add file type rule if allowed_file_types is set 143 if (field.allowed_file_types && field.allowed_file_types.trim() !== '') { 144 rules[nameAttr].fileTypeAllowed = field.allowed_file_types; 145 messages[nameAttr].fileTypeAllowed = `Only files of type ${field.allowed_file_types} are allowed.`; 146 } 147 // â Add file size rule if max_file_size is set 148 if (field.max_file_size && !isNaN(field.max_file_size)) { 149 rules[nameAttr].maxFileSize = field.max_file_size; 150 messages[nameAttr].maxFileSize = `Each file must be less than ${field.max_file_size} MB.`; 151 } 152 } else { 153 // Add custom validation by inputType 154 let customRule = {}; 155 let customMsg = {}; 156 if (inputType === 'phone' || fieldName.toLowerCase().includes('phone') || placeholderName.toLowerCase().includes('phone') || fieldName.toLowerCase().includes('fax') || placeholderName.toLowerCase().includes('fax')) { 157 customRule = { customPhoneFax: true }; 158 // customMsg = { customPhoneFax: 'Only numbers, spaces, dash, plus, and parentheses allowed. Cannot be all zeros.' }; 159 } else if (inputType === 'text') { 160 if (multiLines === false) { 161 customRule = { customName: true }; 162 } else { 163 customRule = { customAddress: true }; 164 } 165 // customMsg = { customName: 'Only letters, spaces, dash, apostrophe, and underscore allowed.' }; 166 } else if (inputType === 'email') { 167 customRule = { customEmail: true }; 168 // customMsg = { customEmail: 'Only alphanumeric, @, ., _, dash, apostrophe allowed.' }; 169 } 170 rules[nameAttr] = Object.assign({required: hasRequired}, {filterBadWords: true}, customRule); 171 let messageTemplate = errorMessagesByType[inputType] || 'Please complete {name}.'; 172 messageTemplate = messageTemplate.replace('{name}', fieldName.toLowerCase()); 173 messages[nameAttr] = Object.assign({required: messageTemplate}, customMsg); 174 } 175// } 176 const hasLogicValidation = ( 177 field?.validation?.error_conditions?.conditions?.children?.length > 0 178 ); 179 if (hasLogicValidation) { 180 const logicFieldKey = field.id; 181 rules[logicFieldKey] = { 182 ...(rules[logicFieldKey] || {}), 183 logicValidation: {field} 184 }; 185 // â Leave out message so the callback above is used 186 messages[logicFieldKey] = { 187 ...(messages[logicFieldKey] || {}) 188 }; 189 } 190 } 191 ); 192 // â AFTER LOOP 193 rules['g-recaptcha-response'] = { 194 required: function () { 195 return grecaptcha.getResponse().length === 0; 196 } 197 }; 198 messages['g-recaptcha-response'] = { 199 required: "Please verify that you're not a robot." 200 }; 201 const $form = $('#formbuildername_' + formId); 202 $form.validate({ 203 errorElement: 'span', 204 errorClass: 'help-block error', 205 ignore: [], 206 rules: rules, 207 messages: messages, 208 errorPlacement: function (error, element) { 209 const inputType = element.prop("type"); // <-- reliably gets input type 210 var nameAttr = element.attr('name').replace(/\[\]$/, ''); 211 if ( 212 element.attr('id')?.startsWith('g-recaptcha-response') || 213 element.attr("name") === "category" 214 ) { 215 error.insertAfter(element.closest('.captcha_div')); 216 } else if (inputType == "checkbox") { 217 error.appendTo("#error_" + nameAttr); 218 } else if (inputType == "select-one") { 219 error.appendTo("#error_" + nameAttr); 220 } else if (inputType == "radio") { 221 error.appendTo("#error_" + nameAttr); 222 } else if (inputType == "select-multiple") { 223 error.appendTo("#error_" + nameAttr); 224 } else if (inputType == "hidden") { 225 error.appendTo("#error_" + nameAttr); 226 } else if (inputType == "range") { 227 error.appendTo("#error_" + nameAttr); 228 } else { 229 error.insertAfter(element); 230 } 231 }, 232 invalidHandler: function () { 233 $('#formbuildername_' + formId) 234 .find('.alert-danger') 235 .show(); 236 }, 237 highlight: function (element) { 238 const $form = $(element).closest('form'); 239 $form.find(`[name="${element.name}"]`) 240 .closest('.form-group') 241 .addClass('has-error'); 242 }, 243 unhighlight: function (element) { 244 const $form = $(element).closest('form'); 245 $form.find(`[name="${element.name}"]`) 246 .closest('.form-group') 247 .removeClass('has-error'); 248 }, 249 submitHandler: function (form) { 250 const $form = $('#formbuildername_' + formId); 251 const $disabledFields = $form.find(':input:disabled'); 252 $disabledFields.prop('disabled', false); 253 const isValid = $('#formbuildername_' + formId).valid(); 254 $disabledFields.prop('disabled', true); 255 if (!isValid) { 256 $disabledFields.each(function () {
257 $(this).valid(); 258 }); 259 return false; 260 } 261 checkLogic(formId); 262 form.submit(); 263 return false; // prevent default for demo/testing 264 } 265 }); 266} 267document.addEventListener('DOMContentLoaded', function () { 268 document.querySelectorAll('.char-count').forEach(function (counter) { 269 const targetId = counter.getAttribute('data-target'); 270 const input = document.getElementById(targetId); 271 if (input) { 272 const updateCount = () => { 273 counter.textContent = input.value.length; 274 }; 275 // Initial count 276 updateCount(); 277 // Listen for changes 278 input.addEventListener('input', updateCount); 279 } 280 }); 281}); 282$(document).on('change', '.scale-radio', function () { 283 $(this).closest('.form-group').find('input.scale-radio').first().valid(); 284}); 285$(document).on('input change', '.form-range', function () { 286 $(this).valid(); 287}); 288$(document).ready(function () { 289 // --- Custom Validation Methods as per rules sheet --- 290 // 1. Full Name, Company Name, Business Name 291 $.validator.addMethod('customName', function (value, element, param) { 292 if (!value) return true; 293 let regex; 294 if (param === 'company' || param === 'business') { 295 // Allow: A-Z, a-z, 0-9, space, dash, apostrophe, underscore, ampersand, iOS special 296 regex = /^[A-Za-z0-9 _'\-&ââ\-ââ]*$/; 297 } else { 298 // Full Name: A-Z, a-z, space, dash, apostrophe, underscore, iOS special 299 regex = /^[A-Za-z _'ââ\-ââ,]*$/; 300 } 301 return regex.test(value); 302 }, 'Contains invalid characters.'); 303 304 // 2. Phone/Fax Number 305 $.validator.addMethod('customPhoneFax', function (value, element) { 306 if (!value) return true; 307 // Allow: 0-9, space, dash, plus, parentheses, iOS special 308 const regex = /^[0-9 \-()+ââ]*$/; 309 if (!regex.test(value)) return false; 310 // Disallow only zero digits 311 return !/^0+$/.test(value); 312 }, 'Please enter valid format.'); 313 314 // 3. Email Address 315 $.validator.addMethod('customEmail', function (value, element) { 316 if (!value) return true; 317 // Allow: alphanumeric, @ . _ iOS special 318 const regex = /^[A-Za-z0-9@._'ââ\-ââ]*$/; 319 return regex.test(value); 320 }, 'Invalid email address.'); 321 322 // 4. Address 323 $.validator.addMethod('customAddress', function (value, element) { 324 if (!value) return true; 325 // Allow: alphanumeric, . , - # @ % & ( ) [ ] : space, iOS special 326 const regex = /^[A-Za-z0-9 .,#@%&()\[\]:_'ââ\-ââ]*$/; 327 // Disallow HTML tags 328 if (!regex.test(value)) return false;fetch('./badwordlist.js') 329 return !/<[^>]*>/.test(value); 330 }, 'Contains invalid characters.'); 331 332 $.validator.addMethod('filterBadWords', function (value, element, param) { 333 if (!value) return true; 334 var badWords = badWordsList; 335 var lowerValue = value.toLowerCase(); 336 337 // Check if any bad word is present in the input 338 var hasBadWord = badWords.some(function(word) { 339 return lowerValue.includes(word); 340 }); 341 342 return !hasBadWord; // valid only if no bad words found 343 }, 'Contains invalid characters.'); 344 345 $.validator.addMethod('maxFileSize', function (value, element, maxSizeMB) { 346 if (!value || !element.files || !element.files.length) 347 return true; 348 const maxSizeBytes = parseInt(maxSizeMB) * 1024 * 1024; 349 for (let i = 0; i < element.files.length; i++) { 350 if (element.files[i].size > maxSizeBytes) { 351 return false; 352 } 353 } 354 return true; 355 }, 'File size exceeds the maximum allowed.'); 356 $.validator.addMethod('fileTypeAllowed', function (value, element, allowedTypesStr) { 357 if (!value) 358 return true; // Allow empty input if not required 359 const allowedTypes = allowedTypesStr.split(',').map(type => type.trim().toLowerCase()); 360 const files = element.files; 361 for (let i = 0; i < files.length; i++) { 362 const ext = files[i].name.split('.').pop().toLowerCase(); 363 if (!allowedTypes.includes(ext)) { 364 return false;
365 } 366 } 367 return true; 368 }, 'Invalid file type.'); 369 $.validator.addMethod('checkedRequired', function (value, element, nameAttr) { 370 return $(`input[name="${nameAttr}"]:checked`).length > 0; 371 }, ''); 372 $.validator.addMethod('matrixRequired', function (value, element, fieldId) { 373 let isValid = true; 374 // Check each row group like: a4afd09a...[Row 1] 375 $(`input[name^="${fieldId}["]`).each(function () { 376 const rowName = $(this).attr('name'); 377 if ($(`input[name="${rowName}"]:checked`).length === 0) { 378 isValid = false; 379 return false; // break loop 380 } 381 }); 382 return isValid; 383 }, ''); 384 $.validator.addMethod("ratingRequired", function (value, element, param) { 385 // Consider value "0" or empty as unselected 386 return parseInt(value) > 0; 387 }, ""); 388 $.validator.addMethod("scaleRequired", function (value, element, param) { 389 // Ensure one of the radios with this name is checked 390 return $(`input[name="${param}"]:checked`).length > 0; 391 }, ""); 392 $.validator.addMethod("sliderRequired", function (value, element, param) { 393 // Slider should have a value greater than 0 to be considered valid 394 return parseInt(value) > 0; 395 }, ""); 396 $.validator.addMethod('logicValidation', function (value, element, param) { 397 const field = param.field; 398 const $form = $(element).closest('form[data-form-id]'); 399 if (!$form.length) 400 return true; 401 const formId = $form.data('form-id'); 402 const {result} = buildMixedExpression( 403 field.validation.error_conditions.conditions, 404 field, 405 formId 406 ); 407 return !result; 408 }, function (params, element) { 409 const field = params.field; 410 // â Use static message if provided 411 if (field?.validation?.error_message?.trim()) { 412 return field.validation.error_message.trim(); 413 } 414 const label = field.name || 'This field'; 415 const conditions = field?.validation?.error_conditions?.conditions?.children || []; 416 if (!conditions.length) 417 return 'This field is invalid.'; 418 const reverseOpMap = { 419 equals: 'must not be', 420 does_not_equal: 'must be', 421 contains: 'must not contain', 422 does_not_contain: 'must contain', 423 starts_with: 'must not start with', 424 ends_with: 'must not end with', 425 is_empty: 'must not be empty', 426 is_not_empty: 'must be empty', 427 content_length_equals: 'length must not equal', 428 content_length_does_not_equal: 'length must equal', 429 content_length_greater_than: 'length must not be greater than', 430 content_length_greater_than_or_equal_to: 'length must be less than', 431 content_length_less_than: 'length must not be less than', 432 content_length_less_than_or_equal_to: 'length must be greater than', 433 matches_regex: 'must not match', 434 does_not_match_regex: 'must match', 435 before: 'must not be before', 436 after: 'must not be after', 437 on_or_before: 'must be after', 438 on_or_after: 'must be before', 439 past_week: 'must not be within the past week', 440 past_month: 'must not be within the past month', 441 past_year: 'must not be within the past year', 442 next_week: 'must not be within the next week', 443 next_month: 'must not be within the next month', 444 next_year: 'must not be within the next year', 445 is_checked: 'must not be checked', 446 is_not_checked: 'must be checked', 447 exists_in_submissions: 'must not already exist in submissions', 448 does_not_exist_in_submissions: 'must already exist in submissions', 449 }; 450 const parts = conditions.map(cond => { 451 const op = cond.value?.operator; 452 const expected = cond.value?.value; 453 const friendly = reverseOpMap[op] || `must not meet condition (${op})`;
454 if ([ 455 'is_empty', 'is_not_empty', 456 'past_week', 'past_month', 'past_year', 457 'next_week', 'next_month', 'next_year', 458 'is_checked', 'is_not_checked' 459 ].includes(op)) { 460 return `${label} ${friendly}`; 461 } 462 return `${label} ${friendly} "${expected}"`; 463 }).filter(Boolean); 464 return `This field is invalid because: ${parts.join(' and ')}`; 465 }); 466}); 467window.logicDebounce = window.logicDebounce || {}; 468//// Map field ID to label 469//window.formRuntime[formId] = { 470// formFields, 471// fieldNameMap: buildFieldNameMap(formFields) 472//}; 473function evaluateCondition(operator, actual, expected, field) { 474 actual = actual || ''; // safeguard 475 const isDateComparison = field?.type === 'date'; 476 if (isDateComparison) { 477 if (operator === 'is_empty') { 478 return actual === '' || actual === null || actual === undefined; 479 } 480 if (operator === 'is_not_empty') { 481 return actual !== '' && actual !== null && actual !== undefined; 482 } 483 if (actual === '' || expected === '') { 484 return false; 485 } 486 switch (operator) { 487 case 'equals': 488 return actual === expected; 489 case 'does_not_equal': 490 return actual !== expected; 491 case 'before': 492 return actual < expected; 493 case 'after': 494 return actual > expected; 495 case 'on_or_before': 496 return actual <= expected; 497 case 'on_or_after': 498 return actual >= expected; 499 case 'matches_regex': 500 try { 501 return new RegExp(expected).test(actual); 502 } catch (e) { 503 return false; 504 } 505 case 'does_not_match_regex': 506 try { 507 return !new RegExp(expected).test(actual); 508 } catch (e) { 509 return false; 510 } 511 case 'past_week': 512 { 513 const today = new Date(); 514 const pastWeek = new Date(); 515 pastWeek.setDate(pastWeek.getDate() - 7); 516 const todayStr = today.toISOString().split('T')[0]; 517 const pastWeekStr = pastWeek.toISOString().split('T')[0]; 518 return actual >= pastWeekStr && actual <= todayStr; 519 } 520 case 'past_month': 521 { 522 const today = new Date(); 523 const pastMonth = new Date(); 524 pastMonth.setMonth(pastMonth.getMonth() - 1); 525 const todayStr = today.toISOString().split('T')[0]; 526 const pastMonthStr = pastMonth.toISOString().split('T')[0]; 527 return actual >= pastMonthStr && actual <= todayStr; 528 } 529 case 'past_year': 530 { 531 const today = new Date(); 532 const pastYear = new Date(); 533 pastYear.setFullYear(pastYear.getFullYear() - 1); 534 const todayStr = today.toISOString().split('T')[0]; 535 const pastYearStr = pastYear.toISOString().split('T')[0]; 536 return actual >= pastYearStr && actual <= todayStr; 537 } 538 case 'next_week': 539 { 540 const today = new Date(); 541 const nextWeek = new Date(); 542 nextWeek.setDate(nextWeek.getDate() + 7); 543 const todayStr = today.toISOString().split('T')[0]; 544 const nextWeekStr = nextWeek.toISOString().split('T')[0]; 545 return actual >= todayStr && actual <= nextWeekStr; 546 } 547 case 'next_month': 548 { 549 const today = new Date(); 550 const nextMonth = new Date(); 551 nextMonth.setMonth(nextMonth.getMonth() + 1); 552 const todayStr = today.toISOString().split('T')[0]; 553 const nextMonthStr = nextMonth.toISOString().split('T')[0]; 554 return actual >= todayStr && actual <= nextMonthStr; 555 } 556 case 'next_year': 557 { 558 const today = new Date(); 559 const nextYear = new Date(); 560 nextYear.setFullYear(nextYear.getFullYear() + 1); 561 const todayStr = today.toISOString().split('T')[0]; 562 const nextYearStr = nextYear.toISOString().split('T')[0]; 563 return actual >= todayStr && actual <= nextYearStr; 564 } 565 default: 566 return false;
567 } 568 } else if (field?.type === 'phone_number') { 569 switch (operator) { 570 case 'equals': 571 return actual === expected; 572 case 'does_not_equal': 573 return actual !== expected; 574 case 'contains': 575 return actual.includes(expected); 576 case 'does_not_contain': 577 return !actual.includes(expected); 578 case 'starts_with': 579 return actual.startsWith(expected); 580 case 'ends_with': 581 return actual.endsWith(expected); 582 case 'is_empty': 583 return actual.trim() === ''; 584 case 'is_not_empty': 585 return actual.trim() !== ''; 586 case 'content_length_equals': 587 return actual.length === parseInt(expected); 588 case 'content_length_does_not_equal': 589 return actual.length !== parseInt(expected); 590 case 'content_length_greater_than': 591 return actual.length > parseInt(expected); 592 case 'content_length_greater_than_or_equal_to': 593 return actual.length >= parseInt(expected); 594 case 'content_length_less_than': 595 return actual.length < parseInt(expected); 596 case 'content_length_less_than_or_equal_to': 597 return actual.length <= parseInt(expected); 598 case 'matches_regex': 599 try { 600 return new RegExp(expected).test(actual); 601 } catch (e) { 602 return false; 603 } 604 case 'does_not_match_regex': 605 try { 606 return !new RegExp(expected).test(actual); 607 } catch (e) { 608 return false; 609 } 610 // â New operators for phone_number 611 case 'exists_in_submissions': 612 return checkIfExistsInSubmissions(field.id, actual); 613 case 'does_not_exist_in_submissions': 614 return !checkIfExistsInSubmissions(field.id, actual); 615 default: 616 return false; 617 } 618 } else if (field?.type === 'email') { 619 switch (operator) { 620 case 'equals': 621 return actual === expected; 622 case 'does_not_equal': 623 return actual !== expected; 624 case 'contains': 625 return actual.includes(expected); 626 case 'does_not_contain': 627 return !actual.includes(expected); 628 case 'starts_with': 629 return actual.startsWith(expected); 630 case 'ends_with': 631 return actual.endsWith(expected); 632 case 'is_empty': 633 return actual === ''; 634 case 'is_not_empty': 635 return actual !== ''; 636 case 'content_length_equals': 637 return actual.length === parseInt(expected); 638 case 'content_length_does_not_equal': 639 return actual.length !== parseInt(expected); 640 case 'content_length_greater_than': 641 return actual.length > parseInt(expected); 642 case 'content_length_greater_than_or_equal_to': 643 return actual.length >= parseInt(expected); 644 case 'content_length_less_than': 645 return actual.length < parseInt(expected); 646 case 'content_length_less_than_or_equal_to': 647 return actual.length <= parseInt(expected); 648 case 'matches_regex': 649 try { 650 return new RegExp(expected).test(actual); 651 } catch (e) { 652 return false; 653 } 654 case 'does_not_match_regex': 655 try { 656 return !new RegExp(expected).test(actual); 657 } catch (e) { 658 return false; 659 } 660 // â New operators for email 661 case 'exists_in_submissions': 662 return checkIfExistsInSubmissions(field.id, actual); 663 case 'does_not_exist_in_submissions': 664 return !checkIfExistsInSubmissions(field.id, actual); 665 default: 666 return false;
667 } 668 } else if (field?.type === 'checkbox') { 669 const isChecked = actual === '1' || actual === true || actual === 'true'; 670 switch (operator) { 671 case 'is_checked': 672 return isChecked; 673 case 'is_not_checked': 674 return !isChecked; 675 default: 676 return false; 677 } 678 } else if (field?.type === 'multi_select') { 679 switch (operator) { 680 case 'contains': 681 return actual.includes(expected); 682 case 'does_not_contain': 683 return !actual.includes(expected); 684 case 'is_empty': 685 return actual.length === 0; 686 case 'is_not_empty': 687 return actual.length > 0; 688 } 689 } else if (field?.type === 'matrix') { 690 const normalizedActual = {}; 691 const normalizedExpected = {}; 692 for (const key in expected) { 693 normalizedActual[key] = String(actual[key] ?? ''); 694 normalizedExpected[key] = String(expected[key]); 695 } 696 const actualStr = JSON.stringify(normalizedActual); 697 const expectedStr = JSON.stringify(normalizedExpected); 698 switch (operator) { 699 case 'equals': 700 return actualStr === expectedStr; 701 case 'does_not_equal': 702 return actualStr !== expectedStr; 703 case 'contains': 704 return Object.entries(expected).every(([row, val]) => actual[row] === val); 705 case 'does_not_contain': 706 return Object.entries(expected).every(([row, val]) => actual[row] !== val); 707 } 708 } else if (['number', 'rating', 'scale', 'slider'].includes(field?.type)) { 709 const actualNumber = parseFloat(actual); 710 const expectedNumber = parseFloat(expected); 711// alert(actualNumber); 712// alert(expectedNumber); 713 switch (operator) { 714 case 'equals': 715 return actualNumber === expectedNumber; 716 case 'does_not_equal': 717 return actualNumber !== expectedNumber; 718 case 'greater_than': 719 return actualNumber > expectedNumber; 720 case 'less_than': 721 return actualNumber < expectedNumber; 722 case 'greater_than_or_equal_to': 723 return actualNumber >= expectedNumber; 724 case 'less_than_or_equal_to': 725 return actualNumber <= expectedNumber; 726 case 'is_empty': 727 return actual === '' || isNaN(actualNumber); 728 case 'is_not_empty': 729 return actual !== ''; 730 case 'content_length_equals': 731 return String(actual).length === Number(expected); 732 case 'content_length_does_not_equal': 733 return String(actual).length !== Number(expected); 734 case 'content_length_greater_than': 735 return String(actual).length > Number(expected); 736 case 'content_length_greater_than_or_equal_to': 737 return String(actual).length >= Number(expected); 738 case 'content_length_less_than': 739 return String(actual).length < Number(expected); 740 case 'content_length_less_than_or_equal_to': 741 return String(actual).length <= Number(expected); 742 // â New operator cases 743 case 'exists_in_submissions': 744 return checkIfExistsInSubmissions(field.id, actual); 745 case 'does_not_exist_in_submissions': 746 return !checkIfExistsInSubmissions(field.id, actual); 747 default: 748 return false; 749 } 750 } else if (field?.type === 'files') { 751 switch (operator) { 752 case 'is_empty': 753 return actual === 0 || actual === '' || actual === null; 754 case 'is_not_empty': 755 return actual !== 0 && actual !== '' && actual !== null; 756 default: 757 return false; 758 } 759 } else { 760 switch (operator) { 761 case 'equals': 762 return actual === expected; 763 case 'does_not_equal': 764 return actual !== expected; 765 case 'contains': 766 return actual.toLowerCase().includes(expected.toLowerCase()); 767 case 'does_not_contain': 768 return !actual.toLowerCase().includes(expected.toLowerCase()); 769 case 'starts_with': 770 return actual.toLowerCase().startsWith(expected.toLowerCase()); 771 case 'ends_with': 772 return actual.toLowerCase().endsWith(expected.toLowerCase()); 773 case 'is_empty': 774 return actual.trim() === ''; 775 case 'is_not_empty': 776 return actual.trim() !== ''; 777 case 'content_length_equals': 778 return actual.length === parseInt(expected); 779 case 'content_length_does_not_equal': 780 return actual.length !== parseInt(expected); 781 case 'content_length_greater_than': 782 return actual.length > parseInt(expected); 783 case 'content_length_greater_than_or_equal_to': 784 return actual.length >= parseInt(expected); 785 case 'content_length_less_than': 786 return actual.length < parseInt(expected); 787 case 'content_length_less_than_or_equal_to': 788 return actual.length <= parseInt(expected); 789 case 'matches_regex': 790 try { 791 return new RegExp(expected).test(actual); 792 } catch (e) { 793 return false;
794 } 795 case 'does_not_match_regex': 796 try { 797 return !new RegExp(expected).test(actual); 798 } catch (e) { 799 return false; 800 } 801 // â New cases 802 case 'exists_in_submissions': 803 // You should implement this function based on actual stored submission data 804 return checkIfExistsInSubmissions(field.id, actual); // returns true/false 805 case 'does_not_exist_in_submissions': 806 return !checkIfExistsInSubmissions(field.id, actual); // returns true/false 807 default: 808 return false; 809 } 810 } 811} 812function checkIfExistsInSubmissions(fieldId, value) { 813 const existingValues = window.submissionDatabase?.[fieldId] || []; 814 return existingValues.includes(value); 815} 816function getReadableCondition(label, operator, expected, actual, result, isDate = false) { 817 const opMap = { 818 equals: '==', 819 does_not_equal: '!=', 820 contains: 'contains', 821 does_not_contain: 'does not contain', 822 starts_with: 'starts with', 823 ends_with: 'ends with', 824 is_empty: 'is empty', 825 is_not_empty: 'is not empty', 826 content_length_equals: 'length ==', 827 content_length_does_not_equal: 'length !=', 828 content_length_greater_than: 'length >', 829 content_length_greater_than_or_equal_to: 'length >=', 830 content_length_less_than: 'length <', 831 content_length_less_than_or_equal_to: 'length <=', 832 matches_regex: 'matches', 833 does_not_match_regex: 'does not match', 834 before: '<', 835 after: '>', 836 on_or_before: '<=', 837 on_or_after: '>=', 838 past_week: 'within past week', 839 past_month: 'within past month', 840 past_year: 'within past year', 841 next_week: 'within next week', 842 next_month: 'within next month', 843 next_year: 'within next year', 844 is_checked: 'is checked', 845 is_not_checked: 'is not checked', 846 exists_in_submissions: 'exists in submissions', 847 does_not_exist_in_submissions: 'does not exist in submissions' 848 }; 849 const symbol = opMap[operator] || operator; 850 let str; 851 if ( 852 [ 853 'is_empty', 854 'is_not_empty', 855 'is_checked', 856 'is_not_checked', 857 'exists_in_submissions', 858 'does_not_exist_in_submissions' 859 ].includes(operator) 860 ) { 861 str = `${label} ${symbol}`; 862 } else if (operator.includes('past_') || operator.includes('next_')) { 863 str = `${label} ${symbol} (actual: "${actual}")`; 864 } else { 865 str = `${label} ${symbol} "${expected}" (actual: "${actual}")`; 866 } 867 return {conditionStr: str, result}; 868} 869 870function evalNode(node, field, formId) { 871 if (node.value) { 872 let expected = node.value.value || ''; 873 const operator = node.value.operator || ''; 874 const $form = $('#formbuildername_' + formId); 875 let actual = $form.find(`[name="${node.identifier}"]`).val() || ''; 876 const {formFields} = window.formRuntime[formId]; 877 const sourceField = formFields.find(f => f.id === node.identifier) || {}; 878 if (sourceField.type === 'phone_number') { 879 const countryVal = $form.find(`[name="${node.identifier}_country"]`).val() || ''; 880 actual = `${countryVal}${actual}`; 881 } else if (sourceField.type === 'date') { 882 const dateFormat = sourceField?.date_format || 'dd/MM/yyyy'; 883 let fieldValue = $form.find(`[name="${node.identifier}"]`).val() 884 || $(`#${node.identifier}`).val() 885 || $(`[data-field-id="${node.identifier}"]`).val() || ''; 886 let actualDateStr = ''; 887 if (fieldValue.trim()) { 888 let actualDatePart = fieldValue.trim().split(' ')[0]; 889 if (dateFormat === 'dd/MM/yyyy') { 890 const [day, month, year] = actualDatePart.split('/'); 891 if (day && month && year) { 892 actualDateStr = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`; 893 } 894 } else if (dateFormat === 'MM-dd-yyyy') { 895 const [month, day, year] = actualDatePart.split('-'); 896 if (month && day && year) { 897 actualDateStr = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`; 898 } 899 } else { 900 actualDateStr = actualDatePart; 901 } 902 actual = actualDateStr; 903 } 904 if (expected != true) { 905 if (expected.includes('T')) { 906 expected = expected.split('T')[0]; 907 } else if (expected.includes(' ')) { 908 expected = expected.split(' ')[0]; 909 } 910 } 911 } else if (sourceField.type === 'checkbox') { 912 actual = $form.find(`[name="${node.identifier}"]`).is(':checked') ? '1' : '0'; 913 } else if (sourceField.type === 'select') { 914 const isWithoutDropdown = sourceField?.without_dropdown; 915 if (isWithoutDropdown) { 916 actual = ($form.find(`[name="${node.identifier}"]:checked`).val() || '').trim(); 917 } else { 918 actual = ($(`#${node.identifier}`).val() || '').trim(); 919 } 920 } else if (sourceField.type === 'multi_select') { 921 const isWithoutDropdown = sourceField?.without_dropdown; 922 actual = isWithoutDropdown 923 ? $(`input[name="${node.identifier}[]"]:checked`).map(function () { 924 return $(this).val().trim(); 925 }).get() 926 : ($(`#${node.identifier}`).val() || []).map(val => val.trim()); 927 } else if (sourceField.type === 'matrix') { 928 actual = {}; 929 $(`#div_${node.identifier} table tbody tr`).each(function () { 930 const rowLabel = $(this).find('th').text().trim(); 931 const selectedVal = $(this).find('input[type="radio"]:checked').val() || ''; 932 actual[rowLabel] = selectedVal; 933 }); 934 } else if (sourceField.type === 'number' || sourceField.type === 'scale' || sourceField.type === 'slider') { 935 actual = $form.find(`[name="${node.identifier}"]:checked`).val() || ''; 936 } else if (sourceField.type === 'files') { 937 const inputEl = document.getElementById(node.identifier); 938 actual = (inputEl?.files || []).length; 939 } 940 const result = evaluateCondition(operator, actual, expected, sourceField); 941 const {fieldNameMap} = window.formRuntime[formId]; 942 const label = fieldNameMap[node.identifier]; 943 const {conditionStr: expr} = getReadableCondition(label, operator, expected, actual, result); 944 return {result, expr: `(${expr})`}; 945 } 946 if (node.children && Array.isArray(node.children)) { 947 const logicOp = (node.operatorIdentifier || 'and').toUpperCase(); 948 const results = node.children.map(child => evalNode(child, field, formId)); 949 const combinedResult = logicOp === 'AND' 950 ? results.every(r => r.result) 951 : results.some(r => r.result); 952 const exprs = results.map(r =>
952 r.expr).filter(Boolean); 953 if (logicOp === 'OR' && exprs.length === 1) { 954 return {result: combinedResult, expr: exprs[0]}; 955 } else { 956 return {result: combinedResult, expr: exprs.join(` ${logicOp} `)}; 957 } 958 } 959 return {result: false, expr: ''}; 960} 961function buildMixedExpression(node, field, formId) { 962 if (!node.children) 963 return evalNode(node, field, formId); 964 const rootOperator = (node.operatorIdentifier || 'and').toUpperCase(); 965 const childResults = node.children.map(child => 966 child.children 967 ? buildMixedExpression(child, field, formId) 968 : evalNode(child, field, formId) 969 ); 970 const combinedResult = rootOperator === 'OR' 971 ? childResults.some(r => r.result) 972 : childResults.every(r => r.result); 973 return { 974 result: combinedResult, 975 expr: childResults.map(r => r.expr).join(` ${rootOperator} `) 976 }; 977} 978function checkLogic(formId) { 979 const {formFields, fieldNameMap} = window.formRuntime[formId]; 980 formFields.forEach(field => { 981 if (!field.logic?.conditions?.children) 982 return; 983 const {result: finalResult, expr: fullExpression} = buildMixedExpression(field.logic.conditions, field, formId); 984 const $form = $('#formbuildername_' + formId); 985 const fieldWrapper = $form.find(`#div_${field.id}`) 986 const {$fieldInput, nameAttr, $label} = getFieldElements(field, formId); 987 field.logic.actions.forEach(action => { 988 if (action === 'hide-block') { 989 if (finalResult) { 990 // â HIDE block 991 fieldWrapper.addClass('d-none'); 992 // Remove validation & errors when hidden 993 const {$fieldInput} = getFieldElements(field, formId); 994 if ($fieldInput?.length) { 995 $fieldInput.rules('remove'); 996 } 997 $form.find(`#error_${field.id}`).empty(); 998 } else { 999 // â SHOW block 1000 fieldWrapper.removeClass('d-none'); 1001 } 1002 } else if (action === 'show-block') { 1003 if (finalResult) { 1004 // â SHOW block 1005 fieldWrapper.removeClass('d-none').show(); 1006 // OPTIONAL: re-enable inputs if previously disabled 1007 fieldWrapper.find('input, textarea, select, button').each(function () { 1008 const $el = $(this); 1009 if (this.tomselect) { 1010 this.tomselect.enable(); 1011 } else { 1012 $el.prop('disabled', false); 1013 } 1014 }); 1015 } else { 1016 // â HIDE block 1017 fieldWrapper.addClass('d-none').hide(); 1018 // OPTIONAL: clear validation errors when hidden 1019 fieldWrapper.find('.error').remove(); 1020 // OPTIONAL: remove validation rules to avoid hidden-field validation 1021 const {$fieldInput} = getFieldElements(field, formId); 1022 if ($fieldInput?.length) { 1023 $fieldInput.rules('remove'); 1024 } 1025 } 1026 } else if (action === 'disable-block') { 1027 fieldWrapper.find('input, textarea, select, button').each(function () { 1028 const $el = $(this); 1029 const shouldApply = finalResult; 1030 if (this.tomselect) { 1031 shouldApply ? this.tomselect.disable() : this.tomselect.enable(); 1032 } else if (field?.type === 'rating') { 1033 const ratingId = $el.attr('id'); 1034 $(`#${ratingId}-rating .star`).css({ 1035 'pointer-events': shouldApply ? 'none' : '', 1036 'opacity': shouldApply ? '0.4' : '' 1037 }); 1038 } else if (field?.type === 'scale') { 1039 $(`#scale-group-${field.id} label.scale-box`).css({ 1040 'pointer-events': shouldApply ? 'none' : '', 1041 'opacity': shouldApply ? '0.4' : '' 1042 }); 1043 } else { 1044 $el.prop('disabled', shouldApply); 1045 } 1046 }); 1047 } else if (action === 'require-answer') { 1048 const $requiredMark = $label.find('span.text-danger'); 1049 if (finalResult) { 1050 if ($label.length && !$requiredMark.length) { 1051 $label.append('<span class="text-danger">*</span>'); 1052 } 1053 const rulesObj = {}; 1054 const messagesObj = {}; 1055 const messageTemplate = errorMessagesByType[field.type] || 'Please complete {name}.'; 1056 const messageText = messageTemplate.replace('{name}', (field.name || 'this field').toLowerCase()); 1057 if (['select', 'multi_select'].includes(field.type) && field.without_dropdown) { 1058 rulesObj.checkedRequired = resolveValidationKey(field, formId); 1059 messagesObj.checkedRequired = messageText; 1060 } else if (field.type === 'matrix') { 1061 rulesObj.matrixRequired = field.id; 1062 messagesObj.matrixRequired = messageText; 1063 } else if (field.type === 'rating') { 1064 rulesObj.ratingRequired = true; 1065 messagesObj.ratingRequired = messageText; 1066 } else if (field.type === 'scale') { 1067 rulesObj.scaleRequired = field.id; 1068 messagesObj.scaleRequired = messageText; 1069 } else if (field.type === 'slider') { 1070 rulesObj.sliderRequired = true; 1071 messagesObj.sliderRequired = messageText; 1072 } else if (field.type === 'files') { 1073 rulesObj.required = true; 1074 messagesObj.required = messageText; 1075 } else { 1076 rulesObj.required = true; 1077 messagesObj.required = messageText; 1078 } 1079 applyValidationRules(field, rulesObj, messagesObj, formId); 1080 } else { 1081 $requiredMark.remove(); 1082 if ($fieldInput?.length) { 1083 $fieldInput.rules('remove'); 1084 } 1085 $form.find(`#error_${field.id}`).empty(); 1086 } 1087 } else if (action === 'make-it-optional') { 1088 const $requiredMark = $label.find('span.text-danger'); 1089 // ð« Do nothing if field is hidden 1090 if (fieldWrapper.hasClass('d-none')) { 1091 return; 1092 } 1093 if (finalResult) { 1094 // â MAKE OPTIONAL 1095 $requiredMark.remove(); 1096 if ($fieldInput?.length) { 1097 $fieldInput.rules('remove'); 1098 } 1099 // Clear existing error 1100 $form.find(`#error_${field.id}`).empty(); 1101 } else { 1102 // â MAKE REQUIRED 1103 if ($label.length && !$requiredMark.length) { 1104 $label.append('<span class="text-danger">*</span>'); 1105 } 1106 if ($fieldInput?.length) { 1107 const messageTemplate = 1108 errorMessagesByType[field.type] || 'Please complete {name}.'; 1109 const messageText = messageTemplate.replace( 1110 '{name}', 1111 (field.name || 'this field').toLowerCase() 1112 ); 1113 // Add rule silently (no immediate validation) 1114 const validator = $form.data('validator'); 1115 const prevOnKeyUp = validator?.settings.onkeyup; 1116 const prevOnBlur = validator?.settings.onblur; 1117 if (validator) { 1118 validator.settings.onkeyup = false;
1119 validator.settings.onblur = false; 1120 } 1121 $fieldInput.rules('add', { 1122 required: true, 1123 messages: { 1124 required: messageText 1125 } 1126 }); 1127 if (validator) { 1128 validator.settings.onkeyup = prevOnKeyUp; 1129 validator.settings.onblur = prevOnBlur; 1130 } 1131 } 1132 } 1133 } 1134 }); 1135 }); 1136} 1137function applyValidationRules(field, rulesObj, messagesObj, formId) { 1138 const validationKey = resolveValidationKey(field, formId); 1139 const $form = $('#formbuildername_' + formId); 1140 const $input = $form.find(`[name="${validationKey}"]`); 1141 if ($input.length) { 1142 $input.rules('add', { 1143 ...rulesObj, 1144 messages: messagesObj 1145 }); 1146// $input.valid(); 1147 } 1148} 1149 1150function resolveValidationKey(field, formId) { 1151 const $form = $('#formbuildername_' + formId); 1152 const inputType = field.type; 1153 const fieldId = field.id; 1154 switch (inputType) { 1155 case 'select': 1156 return field.without_dropdown ? `${fieldId}` : $form.find(`[id="${fieldId}"]`).attr('name'); 1157 case 'multi_select': 1158 return field.without_dropdown ? `${fieldId}[]` : `${fieldId}[]`; 1159 case 'matrix': 1160 case 'rating': 1161 case 'scale': 1162 case 'slider': 1163 return `${fieldId}`; 1164 case 'files': 1165 const name = $form.find(`[id="${fieldId}"]`).attr('name'); 1166 return name || fieldId; 1167 default: 1168 const name1 = $form.find(`[id="${fieldId}"]`).attr('name'); 1169 return name1 || fieldId; 1170 } 1171} 1172function getFieldElements(field, formId) { 1173 const $form = $('#formbuildername_' + formId); 1174 const fieldId = field.id; 1175 let $fieldInput = $form.find(`[id="${fieldId}"]`); 1176 if ($fieldInput.length === 0) { 1177 $fieldInput = $form.find(`[name="${fieldId}"]`).first(); 1178 if ($fieldInput.length === 0) { 1179 $fieldInput = $form.find(`[name="${fieldId}[]"]`).first(); 1180 } 1181 } 1182 const nameAttr = $fieldInput.attr('name'); 1183 const $label = $form.find(`#div_${fieldId} .cm-label`); 1184 return {$fieldInput, nameAttr, $label}; 1185} 1186 1187$(document).on('input change', 'input, textarea, select', function () { 1188 const $form = $(this).closest('form[data-form-id]'); 1189 if (!$form.length) 1190 return; 1191 const formId = $form.data('form-id'); 1192 clearTimeout(window.logicDebounce[formId]); 1193 window.logicDebounce[formId] = setTimeout(() => { 1194 checkLogic(formId); 1195 }, 500); 1196}); 1197function KeycheckOnlyPhonenumber(e) { 1198 var t = 0; 1199 t = document.all ? 3 : document.getElementById ? 1 : document.layers ? 2 : 0; 1200 if (document.all) 1201 e = window.event; 1202 var n = ""; 1203 var r = ""; 1204 if (t == 2) { 1205 if (e.which > 0) 1206 n = "(" + String.fromCharCode(e.which) + ")"; 1207 r = e.which 1208 } else { 1209 if (t == 3) { 1210 r = window.event ? event.keyCode : e.which 1211 } else { 1212 if (e.charCode > 0) 1213 n = "(" + String.fromCharCode(e.charCode) + ")"; 1214 r = e.charCode 1215 } 1216 } 1217 if (r >= 65 && r <= 90 || r >= 97 && r <= 122 || r >= 33 && r <= 39 || r >= 42 && r <= 42 || r >= 44 && r <= 44 || r >= 46 && r <= 47 || r >= 58 && r <= 64 || r >= 91 && r <= 96 || r >= 123 && r <= 126) { 1218 return false 1219 } 1220 return true 1221} 1222$(document).on('click', 'button[type="submit"]', function (e) { 1223 const $form = $(this).closest('form[data-form-id]'); 1224 if (!$form.length) 1225 return; 1226 const formId = $form.data('form-id'); 1227 checkLogic(formId); // â scoped to correct form 1228}); 1229 1230//Break Page 1231//document.addEventListener("DOMContentLoaded", function () { 1232// const $formRoot = $('#form-root'); 1233// if (!$formRoot.length) 1234// return; 1235// const $children = $formRoot.children().toArray(); 1236// const sections = []; 1237// let $currentSection = $('<div class="nf-section row mx-auto"></div>'); 1238// let hasPageBreak = false;
1239// // Build sections 1240// $children.forEach(function (el) { 1241// const $el = $(el); 1242// if ($el.hasClass('nf-page-break')) { 1243// hasPageBreak = true; 1244// if ($currentSection.children().length) { 1245// sections.push($currentSection); 1246// $formRoot.append($currentSection); 1247// } 1248// $currentSection = $('<div class="nf-section row mx-auto"></div>'); 1249// $el.remove(); 1250// } else { 1251// $currentSection.append($el); 1252// } 1253// }); 1254// if ($currentSection.children().length) { 1255// sections.push($currentSection); 1256// $formRoot.append($currentSection); 1257// } 1258// /** 1259// * ------------------------------------------------ 1260// * CASE 2: NO PAGE BREAK â SIMPLE FORM 1261// * ------------------------------------------------ 1262// */ 1263// if (!hasPageBreak) { 1264// const $submitDiv = $('#submitDiv'); 1265// if ($submitDiv.length) { 1266// const $submitBtn = $('<button type="submit" class="btn btn-primary default">Submit</button>'); 1267// $submitDiv.append($submitBtn); 1268// } 1269// return; 1270// } 1271// /** 1272// * ------------------------------------------------ 1273// * CASE 1: PAGE BREAK EXISTS â MULTI STEP FORM 1274// * ------------------------------------------------ 1275// */ 1276// // Add Prev / Next buttons to each section 1277// sections.forEach(function ($section, index) { 1278// const $nav = $('<div class="nf-nav-buttons d-flex justify-content-between gap-2 mt-4"></div>'); 1279// const $prevBtn = $('<button type="button" class="btn btn-tertiary nf-prev-btn">Previous</button>'); 1280// const $nextBtn = $('<button type="button" class="btn btn-primary nf-next-btn">Next</button>'); 1281// $prevBtn.on('click', function () { 1282// showSection(index - 1); 1283// }); 1284// $nextBtn.on('click', function () { 1285// showSection(index + 1); 1286// }); 1287// $nav.append($prevBtn).append($nextBtn); 1288// const $captchaMarker = $section.find('.nf-captcha-marker'); 1289// if ($captchaMarker.length) { 1290// $nav.insertBefore($captchaMarker); 1291// } else { 1292// $section.append($nav); 1293// } 1294// }); 1295// // â Add Submit button INTO the same nav row on the last section 1296// const $lastSection = sections[sections.length - 1]; 1297// const $lastNav = $lastSection.find('.nf-nav-buttons').first(); 1298// const $submitBtn = $('<button type="submit" class="btn btn-primary nf-submit-btn">Submit</button>'); 1299// $lastNav.append($submitBtn); 1300// let currentIndex = 0; 1301// function showSection(index) { 1302// sections.forEach(function ($sec, i) { 1303// $sec.toggleClass('d-none', i !== index); 1304// }); 1305// const $nav = sections[index].find('.nf-nav-buttons'); 1306// const $prevBtn = $nav.find('.nf-prev-btn'); 1307// const $nextBtn = $nav.find('.nf-next-btn'); 1308// // Prev visibility 1309// if (index === 0) { 1310// $prevBtn.addClass('d-none'); 1311// } else { 1312// $prevBtn.removeClass('d-none'); 1313// } 1314// // Next visibility (kept visible as per requirement) 1315// if (index === sections.length - 1) { 1316// $nextBtn.addClass('d-none'); // remove this line if you want Next visible 1317// } else { 1318// $nextBtn.removeClass('d-none'); 1319// } 1320// currentIndex = index; 1321// } 1322// // Init 1323// sections.forEach($sec => $sec.addClass('d-none')); 1324// showSection(0); 1325//}); 1326 1327document.addEventListener("DOMContentLoaded", function () { 1328 1329 // ð LOOP EACH FORM (THIS IS THE ONLY REAL CHANGE) 1330// document.querySelectorAll('#form-root').forEach(form => { 1331// 1332// const $formRoot = $(form); 1333// if (!$formRoot.length) 1334// return; 1335// 1336// // ð´ SAME AS OLD CODE 1337// const $children = $formRoot.children().toArray(); 1338 document.querySelectorAll('#form-root').forEach(form => { 1339 1340 const $formRoot = $(form); 1341 if (!$formRoot.length) 1342 return; 1343 1344 const $children = $formRoot.children().toArray(); // keep if flat 1345 const sections = []; 1346 let $currentSection = $('<div class="nf-section row mx-auto"></div>'); 1347 let hasPageBreak = false;
1348 1349 // ---------------------------------- 1350 // BUILD SECTIONS (UNCHANGED LOGIC) 1351 // ---------------------------------- 1352 $children.forEach(function (el) { 1353 const $el = $(el); 1354 1355 if ($el.hasClass('nf-page-break')) { 1356 hasPageBreak = true; 1357 1358 if ($currentSection.children().length) { 1359 sections.push($currentSection); 1360 $formRoot.append($currentSection); 1361 } 1362 1363 $currentSection = $('<div class="nf-section row mx-auto"></div>'); 1364 $el.remove(); 1365 } else { 1366 $currentSection.append($el); 1367 } 1368 }); 1369 1370 if ($currentSection.children().length) { 1371 sections.push($currentSection); 1372 $formRoot.append($currentSection); 1373 } 1374 1375 /** 1376 * ------------------------------------------------ 1377 * CASE 2: NO PAGE BREAK â SIMPLE FORM 1378 * ------------------------------------------------ 1379 */ 1380 if (!hasPageBreak) { 1381 1382 // Prevent duplicate submit buttons 1383 if (!$formRoot.find('button[type="submit"]').length) { 1384 const $submitDiv = $formRoot.find('#submitDiv'); 1385 1386 if ($submitDiv.length) { 1387 $submitDiv.append( 1388 $('<button type="submit" class="btn btn-primary default">Submit</button>') 1389 ); 1390 } else { 1391 $formRoot.append( 1392 $('<button type="submit" class="btn btn-primary default mt-3">Submit</button>') 1393 ); 1394 } 1395 } 1396 1397 return; // â stop ONLY this form 1398 } 1399 1400 /** 1401 * ------------------------------------------------ 1402 * CASE 1: PAGE BREAK EXISTS â MULTI STEP FORM 1403 * ------------------------------------------------ 1404 */ 1405 sections.forEach(function ($section, index) { 1406 1407 const $nav = $('<div class="nf-nav-buttons d-flex justify-content-between gap-2 mt-4"></div>'); 1408 const $prevBtn = $('<button type="button" class="btn btn-tertiary nf-prev-btn">Previous</button>'); 1409 const $nextBtn = $('<button type="button" class="btn btn-primary nf-next-btn">Next</button>'); 1410 1411 $prevBtn.on('click', function () { 1412 showSection(index - 1); 1413 }); 1414 1415 $nextBtn.on('click', function () { 1416 showSection(index + 1); 1417 }); 1418 1419 $nav.append($prevBtn).append($nextBtn); 1420 1421 const $captchaMarker = $section.find('.nf-captcha-marker'); 1422 if ($captchaMarker.length) { 1423 $nav.insertBefore($captchaMarker); 1424 } else { 1425 $section.append($nav); 1426 } 1427 }); 1428 1429 // â SUBMIT BUTTON ONLY ON LAST SECTION (PER FORM) 1430 const $lastSection = sections[sections.length - 1]; 1431 const $lastNav = $lastSection.find('.nf-nav-buttons').first(); 1432 1433 if (!$lastNav.find('.nf-submit-btn').length) { 1434 $lastNav.append( 1435 $('<button type="submit" class="btn btn-primary nf-submit-btn">Submit</button>') 1436 ); 1437 } 1438 1439 let currentIndex = 0; 1440 1441 function showSection(index) { 1442 sections.forEach(function ($sec, i) { 1443 $sec.toggleClass('d-none', i !== index); 1444 }); 1445 1446 const $activeSection = sections[index]; 1447 1448 const $nav = $activeSection.find('.nf-nav-buttons'); 1449 const $prevBtn = $nav.find('.nf-prev-btn'); 1450 const $nextBtn = $nav.find('.nf-next-btn'); 1451 1452 // Button visibility 1453 $prevBtn.toggleClass('d-none', index === 0); 1454 $nextBtn.toggleClass('d-none', index === sections.length - 1); 1455 1456 // ð¿ SMOOTH SCROLL FIRST 1457 $('html, body').stop().animate( 1458 { 1459 scrollTop: $activeSection.offset().top - 40 1460 }, 1461 400, // ð scroll speed (increase = slower) 1462 'swing', 1463 function () { 1464 // ð§ FOCUS AFTER SCROLL FINISHES 1465 const $firstInput = $activeSection 1466 .find('input, select, textarea') 1467 .filter(':visible') 1468 .first(); 1469 1470 if ($firstInput.length) { 1471 $firstInput.trigger('focus'); 1472 } else { 1473 $activeSection.attr('tabindex', '-1').trigger('focus'); 1474 } 1475 } 1476 ); 1477 1478 currentIndex = index; 1479 } 1480 1481 1482 1483 // Init 1484 sections.forEach($sec => $sec.addClass('d-none')); 1485 showSection(0); 1486 1487 }); 1488 1489}); 1490 1491document.addEventListener('DOMContentLoaded', function () { 1492 // Find selects with either class 1493 const selects = document.querySelectorAll( 1494 '.js-tom-multiselect, .js-tom-select' 1495 ); 1496 // â If neither class exists, do nothing 1497 if (!selects.length) { 1498 return; 1499 }
1500 selects.forEach(function (el) { 1501 // â Prevent duplicate initialization 1502 if (el.tomselect !== undefined) { 1503 return; 1504 } 1505 const isMulti = el.classList.contains('js-tom-multiselect'); 1506 new TomSelect(el, { 1507 create: el.dataset.allowCreate === 'true', 1508 persist: false, 1509 maxOptions: isMulti ? 100 : 50, 1510 plugins: isMulti 1511 ? ['remove_button', 'dropdown_input'] 1512 : ['dropdown_input'], 1513 render: isMulti ? { 1514 option_create: function (data, escape) { 1515 return ( 1516 '<div class="create">Create <strong>' + 1517 escape(data.input) + 1518 '</strong>…</div>' 1519 ); 1520 } 1521 } : {} 1522 }); 1523 }); 1524}); 1525(function () { 1526 window.onRecaptchaApiLoaded = function () { 1527 window.renderRecaptchas(); 1528 }; 1529 window.renderRecaptchas = function () { 1530 if (!window.grecaptcha || typeof grecaptcha.render !== 'function') { 1531 return; 1532 } 1533 if (!window.recaptchaForms) { 1534 return; 1535 } 1536 Object.keys(window.recaptchaForms).forEach(function (formId) { 1537 const rec = window.recaptchaForms[formId]; 1538 if (rec.rendered) 1539 return; 1540 const el = document.getElementById(rec.elementId); 1541 if (!el) 1542 return; 1543 grecaptcha.render(el, { 1544 sitekey: window.GOOGLE_RECAPTCHA_SITE_KEY 1545 }); 1546 rec.rendered = true; 1547 }); 1548 }; 1549})(); 1550 1551 1552//FOR Copy Pase Div 1553//'<a href="javascript:;" class="js-copy-section">Add Director</a>' 1554//'<div data-repeat-start></div>' 1555//'<div data-repeat-end=""></div>' 1556(function () { 1557 1558 let cloneIndex = 1; 1559 const MAX_CLONES = 5; 1560 1561 document.addEventListener('click', function (e) { 1562 const trigger = e.target.closest('.js-copy-section'); 1563 if (!trigger) 1564 return; 1565 1566 e.preventDefault(); 1567 // ð« Stop if limit reached 1568 if (cloneIndex >= MAX_CLONES) { 1569 alert('You can add a maximum of ' + MAX_CLONES + '.'); 1570 return; 1571 } 1572 1573 // 1ï¸â£ Find START marker 1574 const startBlock = trigger.closest('.nf-code-block'); 1575 const startMarker = startBlock.querySelector('[data-repeat-start]'); 1576 1577 if (!startMarker) { 1578 console.warn('Repeat start marker missing'); 1579 return; 1580 } 1581 1582 // 2ï¸â£ Find END BLOCK (next nf-code-block that contains data-repeat-end) 1583 let endBlock = startBlock.nextElementSibling; 1584 let endMarker = null; 1585 1586 while (endBlock) { 1587 if ( 1588 endBlock.classList?.contains('nf-code-block') && 1589 endBlock.querySelector('[data-repeat-end]') 1590 ) { 1591 endMarker = endBlock.querySelector('[data-repeat-end]'); 1592 break; 1593 } 1594 endBlock = endBlock.nextElementSibling; 1595 } 1596 1597 if (!endMarker) { 1598 console.warn('Repeat end marker missing'); 1599 return; 1600 } 1601 1602 // 3ï¸â£ Collect elements BETWEEN start & end blocks 1603 const elementsToClone = []; 1604 let node = startBlock.nextElementSibling; 1605 1606 while (node && node !== endBlock) { 1607 elementsToClone.push(node); 1608 node = node.nextElementSibling; 1609 } 1610 1611 if (!elementsToClone.length) 1612 return; 1613 1614 const fragment = document.createDocumentFragment(); 1615 1616 elementsToClone.forEach(el => { 1617 const clone = el.cloneNode(true); 1618 1619 // ð Fix IDs 1620 clone.querySelectorAll('input, textarea, select').forEach(input => { 1621 1622 if (!input.id) 1623 return; 1624 1625 const oldId = input.id; 1626 const newId = oldId + '_clone_' + cloneIndex; 1627 1628 // Update input ID 1629 input.id = newId; 1630 1631 // Update matching label[for] 1632 const label = clone.querySelector(`label[for="${oldId}"]`); 1633 if (label) { 1634 label.setAttribute('for', newId); 1635 } 1636 }); 1637 1638 // ð Fix names (smart duplication) 1639 clone.querySelectorAll('[name]').forEach(input => { 1640 1641 const name = input.getAttribute('name'); 1642 1643 // Case 1ï¸â£ name="field" 1644 if (!name.includes('[')) { 1645 input.setAttribute('name', name + '_' + cloneIndex); 1646 return; 1647 } 1648 1649 // Case 2ï¸â£ name="field[]" 1650 if (name.endsWith('[]')) { 1651 input.setAttribute( 1652 'name', 1653 name.replace(/\[\]$/, '[' + cloneIndex + ']') 1654 ); 1655 return; 1656 } 1657 1658 // Case 3ï¸â£ name="field[0]" OR name="field[key]" 1659 input.setAttribute( 1660 'name', 1661 name.replace(/\[(.*?)\]/, '[' + cloneIndex + ']') 1662 ); 1663 }); 1664 1665 1666 // â» Reset values 1667 clone.querySelectorAll('input, textarea, select').forEach(i => { 1668 if (['checkbox', 'radio'].includes(i.type)) { 1669 i.checked = false;
1670 } else if (i.type !== 'hidden') { 1671 i.value = ''; 1672 } 1673 }); 1674 1675 fragment.appendChild(clone); 1676 }); 1677 1678 // 4ï¸â£ Insert AFTER end block 1679 endBlock.after(fragment); 1680 1681 cloneIndex++; 1682 }); 1683 1684})(); 1685
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.