1"use strict"; 2 3/** 4 * FormMagicValidation 5 * Initialize JustValidate on a form with auto rules for inputs, file validation, and helper methods. 6 * @param {HTMLFormElement} form 7 * @returns {Object} { check, errors, destroy, validator } 8 */ 9function FormMagicValidation(form) { 10 if (!form) return null; 11 12 // Initialize JustValidate 13 const validator = new window.JustValidate(form, { 14 validateBeforeSubmitting: true, 15 lockForm: false, 16 focusInvalidField: false, 17 errorFieldCssClass: '', 18 errorLabelCssClass: 'just-validate-error-label', 19 errorLabelStyle: { color: 'red', fontSize: '0.85rem' }, 20 successFieldCssClass: '', 21 }); 22 23 // Remove all error/success labels dynamically 24 const observer = new MutationObserver(() => { 25 form.querySelectorAll('.just-validate-error-label, .just-validate-success-label') 26 .forEach(el => el.remove()); 27 }); 28 observer.observe(form, { childList: true, subtree: true }); 29 30 // Optional: live validation observer 31 validator.onValidate(({ isValid, isSubmitted, fields }) => { 32 // Object.entries(fields) 33 // .filter(([_, f]) => !f.isValid) 34 // .forEach(([name, fieldState]) => { 35 // if (fieldState.errors && fieldState.errors.length > 0) { 36 // // Show a toast for each error message 37 // fieldState.errors.forEach(msg => showErrorToast(msg)); 38 // } else { 39 // // Fallback message 40 // showErrorToast(`${name} is invalid`); 41 // } 42 // }); 43 44 // Debug individual field states (optional) 45 let errorsShowIndex = 0; 46 Object.entries(fields).forEach(([name, state]) => { 47 if (!state.isValid && state?.errorMessage) { 48 setTimeout(() => { 49 // toastMagic.error(state?.errorMessage) 50 }, errorsShowIndex * 500); 51 errorsShowIndex++; 52 } 53 }); 54 }); 55 56 // Helper: get field label for error messages 57 const getFieldLabel = (input) => input.closest('.form-group')?.querySelector('label')?.textContent?.trim() 58 || input.placeholder || 'This field'; 59 60 // Auto-add rules to inputs 61 form.querySelectorAll('input, textarea, select').forEach(input => { 62 if (!input.name || input.type === 'hidden' || input.hasAttribute('hidden')) return; 63 64 const name = input.name; 65 if (!name) return; 66 const rules = []; 67 if (form.classList.contains('form-advance-inputs-validation')) { 68 if (input.required && input.type === 'file') rules.push({ rule: 'required', errorMessage: input.dataset.requiredMsg || `${getFieldLabel(input)} is required` }); 69 if (input.type === 'email') rules.push({ rule: 'email', errorMessage: input.dataset.emailMsg || 'Please enter a valid email address' }); 70 if (input.type === 'number' || input.inputMode === 'numeric') rules.push({ rule: 'number', errorMessage: input.dataset.numberMsg || 'Please enter a valid number' }); 71 if (input.type === 'url') rules.push({ rule: 'customRegexp', value: /^https?:\/\/.+/, errorMessage: input.dataset.urlMsg || 'Please enter a valid URL' }); 72 if (input.minLength) rules.push({ rule: 'minLength', value: parseInt(input.minLength), errorMessage: input.dataset.minlengthMsg || `Minimum ${input.minLength} characters` }); 73 if (input.maxLength && parseInt(input.maxLength) > 0) rules.push({ rule: 'maxLength', value: parseInt(input.maxLength), errorMessage: input.dataset.maxlengthMsg || `Maximum ${input.maxLength} characters` }); 74 if (input.min) rules.push({ rule: 'minNumber', value: parseFloat(input.min), errorMessage: input.dataset.minMsg || `Minimum value is ${input.min}` }); 75 if (input.max) rules.push({ rule: 'maxNumber', value: parseFloat(input.max), errorMessage: input.dataset.maxMsg || `Maximum value is ${input.max}` }); 76 if (input.pattern) rules.push({ rule: 'customRegexp', value: new RegExp(input.pattern), errorMessage: input.dataset.patternMsg || 'Invalid format' }); 77 78 // Standard rules 79 // if (input.required && !input.classList.contains('select2-hidden-accessible')) { 80 // rules.push({ 81 // rule: 'required', 82 // errorMessage: input.dataset.requiredMsg || `${getFieldLabel(input)} is required` 83 // }); 84 // } 85 86 // Select2 auto-detect 87 if (input.classList.contains('select2-hidden-accessible')) { 88 rules.push({ 89 rule: 'function', 90 validator: (val, field) => { 91 if (!field) return false;
92 let value = $(field).val(); 93 94 // Ensure value is never undefined 95 if (value === undefined || value === null) return false; 96 97 // If it's an array (multi-select) 98 if (Array.isArray(value)) return value.length > 0; 99 100 // If it's a string 101 if (typeof value === 'string') return value.trim() !== ''; 102 103 return false; // fallback for anything else 104 }, 105 errorMessage: input.dataset.requiredMsg || `${getFieldLabel(input)} is required` 106 }); 107 } 108 109 // if (name.endsWith('[]')) { 110 // // JustValidate will select all matching inputs 111 // rules.push({ 112 // rule: 'function', 113 // validator: (val, field) => { 114 // // Get all inputs with this name 115 // const inputs = form.querySelectorAll(`input[name="${name}"]`); 116 // // Required: at least one filled 117 // return Array.from(inputs).some(i => i.value && i.value.trim() !== ''); 118 // }, 119 // errorMessage: input.dataset.requiredMsg || `${getFieldLabel(input)} is required` 120 // }); 121 // validator.addField(`[name="${name}"]`, rules); 122 // } else { 123 // validator.addField(`[name="${name}"]`, rules); 124 // } 125 126 if (name.endsWith('[]')) { 127 rules.push({ 128 rule: 'function', 129 validator: (val, field) => { 130 const inputs = form.querySelectorAll(`input[name="${name}"]`); 131 return Array.from(inputs).some(i => i.value && i.value.trim() !== ''); 132 }, 133 errorMessage: input.dataset.requiredMsg || `${getFieldLabel(input)} is required` 134 }); 135 } 136 } 137 138 if (form.classList.contains('form-advance-file-validation')) { 139 // --- File input rules using JustValidate native 'files' rule --- 140 if (input.type?.toString() === 'file') { 141 // Add required first 142 if (input.required) { 143 const accept = (input.getAttribute('accept') || '') 144 .replace(/\|/g, ',') 145 .split(',') 146 .map(t => t.trim()) 147 .filter(Boolean); 148 149 const allowedExtensions = accept 150 .filter(a => a.startsWith('.')) 151 .map(a => a.replace('.', '')); 152 153 const allowedTypes = accept 154 .filter(a => a.includes('/')) 155 .map(a => a.toLowerCase()); 156 157 const maxSizeMb = parseInt(input.dataset.maxSize, 10); 158 const maxSizeBytes = !isNaN(maxSizeMb) ? maxSizeMb * 1024 * 1024 : 5 * 1024 * 1024; 159 160 rules.push({ 161 rule: 'minFilesCount', 162 value: 1, 163 errorMessage: input.dataset.requiredMsg || `${getFieldLabel(input)} is required`, 164 }, 165 // { 166 // rule: 'files', 167 // value: { 168 // files: { 169 // extensions: allowedExtensions.length ? allowedExtensions : undefined, 170 // types: allowedTypes.length ? allowedTypes : undefined, 171 // maxSize: maxSizeBytes, // convert MB to bytes 172 // }, 173 // }, 174 // errorMessage: input.dataset.fileMsg || `${getFieldLabel(input)} is invalid`, 175 // } 176 ); 177 } 178 179 // Optional: min/max files count (if you want to add) 180 if (input.dataset.minFiles) { 181 rules.push({ 182 rule: 'minFilesCount', 183 value: parseInt(input.dataset.minFiles, 10), 184 errorMessage: `Select at least ${input.dataset.minFiles} file(s)` 185 }); 186 } 187 if (input.dataset.maxFiles) { 188 rules.push({ 189 rule: 'maxFilesCount', 190 value: parseInt(input.dataset.maxFiles, 10), 191 errorMessage: `Select at most ${input.dataset.maxFiles} file(s)` 192 }); 193 } 194 195 } 196 } 197 198 if (rules.length > 0) { 199 validator.addField(`[name="${name}"]`, rules); 200 } 201 202 if (!rules.length) { 203 console.warn(`Skipping "${name}" â no validation rules detected.`); 204 return; 205 } 206 }); 207 208 return { 209 /** Run validation and return boolean */ 210 async check() { 211 try { 212 this.lastResult = await validator.validate(); // save it 213 return this.lastResult?.isValid === true; 214 } catch (e) { 215 console.warn(e); 216 return false;
217 } 218 }, 219 220 errors() { 221 const list = []; 222 223 // â Get fields from validator instance, not lastResult 224 const fields = validator.fields; 225 226 if (!fields) return list; 227 228 // Convert numeric keys (1, 2, 3) to entries 229 Object.entries(fields).forEach(([id, fieldState]) => { 230 if (!fieldState.isValid) { 231 const message = fieldState.errorMessage || 232 (fieldState.errors?.[0]) || 233 `Field is invalid`; 234 235 list.push({ 236 element: fieldState.elem, 237 message: message, 238 fieldName: fieldState.elem?.name || id 239 }); 240 } 241 }); 242 243 return list; 244 }, 245 246 /** Destroy observer and validator */ 247 destroy() { 248 observer.disconnect(); 249 validator.destroy?.(); 250 }, 251 252 /** Expose JustValidate instance */ 253 validator 254 }; 255} 256 257// --- Global form initialization --- 258const formValidators = new WeakMap(); 259const formSubmitting = new WeakSet(); 260 261function initFormValidation() { 262 document.querySelectorAll('.form-advance-validation').forEach(form => { 263 if (!formValidators.has(form)) { 264 formValidators.set(form, FormMagicValidation(form)); 265 } 266 }); 267} 268 269if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initFormValidation); 270else initFormValidation(); 271 272if (typeof MutationObserver !== 'undefined') { 273 new MutationObserver(() => initFormValidation()).observe(document.body, { childList: true, subtree: true }); 274} 275 276// --- Non-AJAX form submit handling --- 277document.addEventListener('submit', async function (e) { 278 const form = e.target.closest('.form-advance-validation.non-ajax-form-validate'); 279 if (!form) return; 280 281 e.preventDefault(); 282 if (formSubmitting.has(form)) return false; 283 284 const validator = formValidators.get(form); 285 if (!validator) return true; 286 287 formSubmitting.add(form); 288 289 const valid = await validator.check(); // await result 290 const errors = validator.errors(); // only works if you store lastResult inside check 291 292 if (!valid && errors?.length > 0) { 293 validator.errors().forEach((err, i) => setTimeout(() => showErrorToast(err.message), i * 400)); 294 formSubmitting.delete(form); 295 return false; 296 } 297 298 formSubmitting.delete(form); 299 HTMLFormElement.prototype.submit.call(form); 300}); 301 302// --- Toast helper --- 303function showErrorToast(msg) { 304 if (typeof toastMagic !== 'undefined'){ 305 toastMagic.error(msg, '', true); 306 } else if(typeof toastr !== 'undefined'){ 307 toastr.error(msg, '', true); 308 } else{ 309 console.warn(msg); 310 } 311} 312 313 314/** 315 * Form validation helper 316 * @param {HTMLFormElement|jQuery} formElement 317 * @returns {Promise<boolean>} true if valid, false if invalid (and shows errors) 318 */ 319async function validateFormHelper(formElement) { 320 const form = formElement instanceof jQuery ? formElement : $(formElement); 321 // Prevent double submission 322 if (formSubmitting.has(form)) return false; 323 const validator = formValidators.get(form[0]); // always get native DOM element 324 if (!validator) return true; 325 formSubmitting.add(form); 326 327 const valid = await validator.check(); 328 329 if (!valid && validator.errors()?.length > 0) { 330 validator.errors().forEach((err, i) => setTimeout(() => showErrorToast(err.message), i * 400)); 331 formSubmitting.delete(form); 332 return false; 333 } 334 335 return true; 336} 337 338 339/** 340 * Form helper to cleanup after submission 341 * @param {jQuery|HTMLFormElement} formElement 342 */ 343function formSubmitCleanup(formElement) { 344 const form = formElement instanceof jQuery ? formElement : $(formElement); 345 formSubmitting.delete(form); 346} 347 348 349document.querySelectorAll('.form-advance-validation input[type="file"]').forEach(input => { 350 input.addEventListener('change', function () { 351 352 if (this.type !== 'file') return true; 353 354 const files = this.files; 355 const required = this.required; 356 const maxSizeMb = parseInt(this.dataset.maxSize || 3, 10); 357 const accept = (this.getAttribute('accept') || '') 358 .split(',') 359 .map(t => t.trim().toLowerCase()) 360 .filter(Boolean); 361 362 // â Clear any previous state or message if needed 363 let isValid = true; 364 365 // ð§© Required check 366 if (required && (!files || files.length === 0)) { 367 isValid = false;
368 showErrorToast(`${getFieldLabel(this)} is required`); 369 removeInputConnectedElements(this); 370 return; 371 } 372 373 // Skip if optional and empty 374 if (!files || files.length === 0) return; 375 376 for (const f of files) { 377 // ð§© Size check 378 if (f.size > maxSizeMb * 1024 * 1024) { 379 isValid = false; 380 showErrorToast(`${f.name} exceeds ${maxSizeMb}MB`); 381 removeInputConnectedElements(this); 382 break; 383 } 384 385 // ð§© Type check 386 if ( 387 accept.length && 388 !accept.some(a => 389 a.startsWith('.') 390 ? f.name.toLowerCase().endsWith(a) 391 : f.type.toLowerCase() === a 392 ) 393 ) { 394 isValid = false; 395 showErrorToast(`${f.name} has an invalid file type`); 396 removeInputConnectedElements(this); 397 break; 398 } 399 } 400 401 // ð« Reset the input if invalid 402 if (!isValid) { 403 this.value = ''; // clear selected file(s) 404 } 405 }); 406}); 407 408// â Helper: extract label 409function getFieldLabel(input) { 410 return ( 411 input.dataset.label || 412 input.name || 413 input.id || 414 'This field' 415 ).replace(/[_\[\]]+/g, ' ').trim(); 416} 417 418function removeInputConnectedElements(input) { 419 const container = input.closest('.custom_upload_input'); 420 if (container) { 421 const deleteSections = container.querySelectorAll('.delete_file_input_section'); 422 deleteSections.forEach(section => section.classList.add('d-none')); 423 } 424 425 const cardContainer = input.closest('.upload-file'); 426 if (cardContainer) { 427 setTimeout(() => { 428 const uploadImgs = cardContainer.querySelectorAll('.upload-file-img'); 429 const textboxes = cardContainer.querySelectorAll('.upload-file-textbox'); 430 const overlays = cardContainer.querySelectorAll('.overlay'); 431 const removeBtns = cardContainer.querySelectorAll('.remove_btn'); 432 433 uploadImgs.forEach(img => { 434 img.style.display = 'none'; 435 img.src = ''; 436 }); 437 438 textboxes.forEach(box => { 439 box.style.display = ''; 440 }); 441 442 overlays.forEach(overlay => { 443 overlay.classList.remove('show'); 444 }); 445 446 removeBtns.forEach(btn => { 447 btn.style.opacity = 0; 448 }); 449 450 cardContainer.classList.remove('input-disabled'); 451 }, 20) 452 } 453 454 let uploadZone = input.closest('.upload-zone'); 455 if (uploadZone) { 456 setTimeout(() => { 457 const textboxes = uploadZone.querySelectorAll('.text-box'); 458 459 textboxes.forEach(box => { 460 box.style.display = ''; 461 box.classList.remove('d-none'); 462 }); 463 464 }, 20) 465 } 466} 467 468 469function xhrResponseManager(xhr) { 470 if (xhr?.responseJSON && xhr.responseJSON.error) { 471 toastMagic.error(xhr.responseJSON.error); 472 } else if (xhr?.responseJSON && xhr.responseJSON.errors) { 473 $.each(xhr?.responseJSON.errors, function (key, value) { 474 toastMagic.error(value); 475 }); 476 } else { 477 toastMagic.error('An unexpected error occurred.'); 478 } 479} 480function ajaxResponseManager(response) { 481 if (response.status === "success") { 482 if (response.message) { 483 toastMagic.success(response.message); 484 } 485 486 setTimeout(() => { 487 if (response?.redirectRoute) { 488 location.href = response.redirectRoute; 489 } else if (response?.redirect_url) { 490 location.href = response?.redirect_url; 491 } 492 }, 500) 493 } else if (response.status === "error") { 494 if (response.message) { 495 toastMagic.error(response.message); 496 } 497 } else if (response.status === "warning") { 498 if (response.message) { 499 toastMagic.warning(response.message); 500 } 501 } 502 503 if (response.errors) { 504 for (let index = 0; index < response.errors.length; index++) { 505 toastMagic.error(response.errors[index].message, { 506 CloseButton: true, 507 ProgressBar: true, 508 }); 509 } 510 } else if (response.error) { 511 toastMagic.error(response.error, { 512 CloseButton: true, 513 ProgressBar: true, 514 }); 515 } 516 517 if (response?.reload) { 518 location.reload(); 519 } 520}
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.