UNPKG

@elrayes/dynamic-form-builder

Version:

A flexible and customizable dynamic form builder with theming support

1,038 lines 63.4 kB
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var _DynamicForm_instances, _DynamicForm_initialize, _DynamicForm_render, _DynamicForm_validateField, _DynamicForm_validateInputs, _DynamicForm_clearValidation, _DynamicForm_clearAllValidation, _DynamicForm_validateForm, _DynamicForm_handleSubmit, _DynamicForm_checkRequiredPackages, _DynamicForm_checkPackageAvailability, _DynamicForm_isBoolean; // IMPORTANT: CKEditor must be globally available (e.g. via CDN in your Blade, see below) // You may also use CKEditor 5/4 npm but for simplicity, CDN is often easier for use in forms import $ from 'jquery'; import ThemeManager from './themes/ThemeManager.js'; import Theme from './themes/Theme.js'; class DynamicForm { /** * @param {DynamicFormOptions} options */ constructor({ config, mount = null, modalOptions = {}, onSubmit, onInitialized = undefined, theme = null, waitForDOMReady = false, allowEmpty = false, returnNullAsEmpty = true }) { _DynamicForm_instances.add(this); this._ckeditors = []; // Hold field configs for CKEditor this._modalInstance = null; this._modal = null; this._requiredPackages = new Set(); this._hasSelect2 = false; // Flag to track if select2 is available // Global defaults for submission behavior this._allowEmptyDefault = false; this._returnNullAsEmptyDefault = true; this._config = config; this._mount = typeof mount === 'string' ? document.getElementById(mount) : mount; this._onSubmit = onSubmit; this._onInitialized = onInitialized; // Set global behavior defaults this._allowEmptyDefault = !!allowEmpty; this._returnNullAsEmptyDefault = !!returnNullAsEmpty; // Check for required packages based on field types __classPrivateFieldGet(this, _DynamicForm_instances, "m", _DynamicForm_checkRequiredPackages).call(this); // Initialize theme if (theme instanceof Theme) { this._theme = theme; } else if (typeof theme === 'string') { this._theme = ThemeManager.get(theme); } else { this._theme = ThemeManager.getDefaultTheme(); } this._modalOptions = Object.assign({ id: 'dynamicFormModal', title: 'Form Submission', show: true, type: 'modal' }, modalOptions); // If waitForDOMReady is true, wait for DOM to be fully loaded before initializing if (waitForDOMReady) { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => __classPrivateFieldGet(this, _DynamicForm_instances, "m", _DynamicForm_initialize).call(this, mount)); } else { // DOM already loaded __classPrivateFieldGet(this, _DynamicForm_instances, "m", _DynamicForm_initialize).call(this, mount); } } else { // Initialize immediately __classPrivateFieldGet(this, _DynamicForm_instances, "m", _DynamicForm_initialize).call(this, mount); } } /** * Get the form element * @returns {HTMLFormElement} */ getForm() { return this._form; } /** * Get form data * @returns {Array<FieldConfig | any>} */ getData() { return this._config; } /** * Get the modal instance * @returns {ModalInstance|null} */ getModalInstance() { return this._modalInstance; } /** * Collect all form inputs * @returns {Record<string, HTMLElement | HTMLElement[]>} */ collectFormInputs() { // Returns a map { fieldName: inputElement } const inputNodes = {}; if (this._form) { // Includes input, select, textarea, radio and checkbox groups const fields = this._form.querySelectorAll('input[name], select[name], textarea[name]'); fields.forEach(el => { const name = el.getAttribute('name') || ''; // For radio/checkbox group, collect as array if (el.type === 'radio' || el.type === 'checkbox') { if (!inputNodes[name]) inputNodes[name] = []; inputNodes[name].push(el); } else { inputNodes[name] = el; } }); } return inputNodes; } /** * Destroy the form and clean up resources */ destroy() { // Cleanup logic, e.g. remove event listeners, null references, etc. if (this._ckeditors) { this._ckeditors.forEach(field => { field.ckeditorInstance?.destroy(); }); } if (this._modal) { this._modal.remove(); } } /** * Clears all form inputs and validations, effectively reinitializing the form * @returns {DynamicForm} The form instance for chaining */ clearForm() { // Clear all validations __classPrivateFieldGet(this, _DynamicForm_instances, "m", _DynamicForm_clearAllValidation).call(this); // Reset all input values this._config.forEach(field => { if (field.type === 'submit') return; switch (field.type) { case 'checkbox': { const checkbox = this._form.querySelector(`[name="${field.name}"]`); if (checkbox) checkbox.checked = false; break; } case 'radio': { const radios = this._form.querySelectorAll(`input[name="${field.name}"]`); radios.forEach(radio => radio.checked = false); break; } case 'select': { const select = this._form.querySelector(`select[name="${field.name}"]`); if (select) { if (select.multiple) { Array.from(select.options).forEach(option => option.selected = false); } else { select.selectedIndex = 0; } } break; } case 'select2': { if (field.select2Instance) { $(field.select2Instance).val(null).trigger('change'); } break; } case 'file': { const fileInput = this._form.querySelector(`input[name="${field.name}"] , input[name="${field.name}[]"]`); if (fileInput) { fileInput.value = ''; // Clear file preview if exists const fileInfo = fileInput.parentElement?.querySelector('.' + this._theme.getFileInfoClasses()); if (fileInfo) { fileInfo.classList.add('d-none'); fileInfo.innerHTML = ''; } } break; } case 'dropzone': { if (field.dropzoneInstance && typeof field.dropzoneInstance.removeAllFiles === 'function') { field.dropzoneInstance.removeAllFiles(true); } break; } case 'ckeditor': { if (field.ckeditorInstance) { field.ckeditorInstance.setData(''); } break; } case 'textarea': { const textarea = this._form.querySelector(`textarea[name="${field.name}"]`); if (textarea) textarea.value = ''; break; } default: { const input = this._form.querySelector(`input[name="${field.name}"]`); if (input) input.value = ''; } } // Clear any custom field state if (typeof field.onClear === 'function') { field.onClear(field.input, field); } }); return this; } } _DynamicForm_instances = new WeakSet(), _DynamicForm_initialize = function _DynamicForm_initialize(mount) { // If mount not provided, create modal and use its body as mount if (!mount) { const modalResult = this._theme.createModal(this._modalOptions); this._modal = modalResult.modal; this._mount = modalResult.modalBody; } else { this._mount = typeof mount === 'string' ? document.getElementById(mount) : mount; } if (!this._mount) { throw new Error('Mount element not found'); } this._form = __classPrivateFieldGet(this, _DynamicForm_instances, "m", _DynamicForm_render).call(this); // Gather all input DOM elements by name const inputNodes = this.collectFormInputs(); // Call the initialized event AFTER rendering, BEFORE showing modal if (typeof this._onInitialized === 'function') { // Provide both form instance and form DOM for maximum flexibility this._onInitialized(this, this._form, inputNodes); } // Show modal if we created it and modalOptions.show is true if (!mount && this._modal) { // Initialize the modal using the theme's initializeModal method this._modalInstance = this._theme.initializeModal(this._modal, { staticBackdrop: true }); // Listen for the container hidden events (modal/offcanvas) if (this._modal) { this._modal.addEventListener('modal:hidden', () => { this.destroy(); }); this._modal.addEventListener('offcanvas:hidden', () => { this.destroy(); }); } // Show the modal if modalOptions.show is true if (this._modalOptions.show) { this._modalInstance.show(); } // Blur active element when modal is hidden document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && document.activeElement instanceof HTMLElement) { document.activeElement.blur(); } }); } }, _DynamicForm_render = function _DynamicForm_render() { if (!this._mount) { throw new Error('Mount element not found'); } // Cleanup old this._mount.innerHTML = ''; // Form const form = document.createElement('form'); form.className = this._theme.getFormClasses(); form.noValidate = true; this._config.forEach((field, idx) => { if (field.type === 'submit') { const btn = document.createElement('button'); btn.type = 'submit'; btn.className = this._theme.getSubmitButtonClasses(); btn.textContent = field.label || 'Submit'; form.appendChild(btn); return; } field.inputs = []; // Form group const group = document.createElement('div'); group.className = this._theme.getFormGroupClasses(); // Label if (field.type !== 'checkbox' && field.type !== 'radio') { const label = document.createElement('label'); label.textContent = field.label || field.name; if (field.type !== 'dropzone') { label.setAttribute('for', field.name || ''); } label.className = this._theme.getLabelClasses(); group.appendChild(label); } // Create input element let input; switch (field.type) { case 'textarea': case 'ckeditor': { input = document.createElement('textarea'); input.className = this._theme.getTextareaClasses(); input.name = field.name; input.id = field.name || `ckeditor_${idx}`; if (field.placeholder) input.placeholder = field.placeholder; if (field.rows) input.rows = field.rows; if (field.required) input.required = true; if (field.value) input.value = field.value; if (field.readonly) input.readOnly = true; group.appendChild(input); if (typeof field.onCreate === 'function') { field.onCreate(input, field, idx); } break; } case 'select2': { input = document.createElement('select'); input.className = this._theme.getSelectClasses(field.multiple || false); input.id = field.name || ''; input.multiple = field.multiple || false; input.name = input.multiple ? `${field.name}[]` : field.name; if (field.required) input.required = true; if (field.readonly) input.disabled = true; if (Array.isArray(field.options)) { field.options.forEach((opt) => { const option = document.createElement('option'); if (typeof opt === 'object') { option.value = String(opt.value); option.textContent = opt.label; option.selected = !!opt.selected; } else { option.value = opt; option.textContent = opt; option.selected = false; } input && input.appendChild(option); }); } group.appendChild(input); // Delay select2 init until after it's appended to DOM: setTimeout(() => { try { // Check again if select2 is available now - more thorough check const select2Available = !(typeof $ === 'undefined' || typeof $.fn === 'undefined' || typeof $.fn.select2 === 'undefined' || typeof $.fn.select2 !== 'function'); this._hasSelect2 = select2Available; if (input && select2Available) { // Merge custom select2 options if provided const parentElement = input?.parentElement; field.select2Instance = $(input).select2({ width: '100%', dropdownParent: parentElement || document.body, ...field.select2Options }).on('change', () => { __classPrivateFieldGet(this, _DynamicForm_instances, "m", _DynamicForm_clearValidation).call(this, field); __classPrivateFieldGet(this, _DynamicForm_instances, "m", _DynamicForm_validateField).call(this, field); }); const select2Selection = $(field.select2Instance).data('select2'); field.$select2Container = select2Selection.$container.get(0); } else if (input && !select2Available && this._requiredPackages.has('select2')) { // If select2 is not available, try to initialize it again after a delay // This helps with production builds where select2 might be loaded asynchronously console.warn(`DynamicFormBuilder: Select2 not immediately available for field "${field.name}". Retrying...`); // Try again after a longer delay (500ms) setTimeout(() => { // Final attempt to check if select2 is available const finalSelect2Available = !(typeof $ === 'undefined' || typeof $.fn === 'undefined' || typeof $.fn.select2 === 'undefined' || typeof $.fn.select2 !== 'function'); if (input && finalSelect2Available) { // Merge custom select2 options if provided const parentElement = input?.parentElement; field.select2Instance = $(input).select2({ width: '100%', dropdownParent: parentElement || document.body, ...field.select2Options }).on('change', () => { __classPrivateFieldGet(this, _DynamicForm_instances, "m", _DynamicForm_clearValidation).call(this, field); __classPrivateFieldGet(this, _DynamicForm_instances, "m", _DynamicForm_validateField).call(this, field); }); const select2Selection = $(field.select2Instance).data('select2'); field.$select2Container = select2Selection.$container.get(0); } else { console.warn(`DynamicFormBuilder: Could not initialize select2 for field "${field.name}" because select2 is not available.`); } }, 500); } } catch (e) { console.error(`DynamicFormBuilder: Error initializing select2 for field "${field.name}":`, e); } // onCreate event per field: if (typeof field.onCreate === 'function' && input) { field.onCreate(input, field, idx); } }); break; } case 'select': { input = document.createElement('select'); input.className = this._theme.getSelectClasses(field.multiple || false); input.id = field.name || ''; input.multiple = field.multiple || false; input.name = input.multiple ? `${field.name}[]` : field.name; if (field.required) input.required = true; if (field.readonly) input.disabled = true; if (Array.isArray(field.options)) { field.options.forEach((opt) => { const option = document.createElement('option'); if (typeof opt === 'object') { option.value = String(opt.value); option.textContent = opt.label; if (field.value !== undefined) { if (Array.isArray(field.value)) { option.selected = field.value.includes(opt.value); } else { option.selected = field.value === opt.value || !!field.selected; } } } else { option.value = opt; option.textContent = opt; if (field.value !== undefined) { if (Array.isArray(field.value)) { option.selected = field.value.includes(opt); } else { option.selected = field.value === opt || !!field.selected; } } } input && input.appendChild(option); }); } group.appendChild(input); if (typeof field.onCreate === 'function') { field.onCreate(input, field, idx); } break; } case 'checkbox': { input = document.createElement('input'); input.type = 'checkbox'; input.className = this._theme.getCheckboxInputClasses(); input.name = field.name; input.id = field.name || ''; if (field.required) input.required = true; if (field.value) input.checked = Boolean(field.value); if (field.readonly) input.disabled = true; // Label after input const cLabel = document.createElement('label'); cLabel.className = this._theme.getCheckboxLabelClasses(); cLabel.setAttribute('for', field.name || ''); cLabel.textContent = field.label || field.name; const cDiv = document.createElement('div'); cDiv.className = this._theme.getCheckboxContainerClasses(); cDiv.appendChild(input); cDiv.appendChild(cLabel); group.appendChild(cDiv); if (typeof field.onCreate === 'function') { field.onCreate(input, field, idx); } break; } case 'radio': { if (Array.isArray(field.options)) { field.options.forEach((opt, i) => { const radioDiv = document.createElement('div'); radioDiv.className = this._theme.getRadioContainerClasses(); const radioInput = document.createElement('input'); radioInput.type = 'radio'; radioInput.className = this._theme.getRadioInputClasses(); radioInput.name = field.name; radioInput.id = field.name + '_' + i; radioInput.value = (typeof opt === 'object') ? String(opt.value) : opt; if (field.required) radioInput.required = true; // Check if this option should be selected if (field.value !== undefined) { if (typeof opt === 'object') { radioInput.checked = field.value === opt.value; } else { radioInput.checked = field.value === opt; } } if (field.readonly) radioInput.disabled = true; const radioLabel = document.createElement('label'); radioLabel.className = this._theme.getRadioLabelClasses(); radioLabel.setAttribute('for', field.name + '_' + i); radioLabel.textContent = (typeof opt === 'object') ? opt.label : opt; radioDiv.appendChild(radioInput); radioDiv.appendChild(radioLabel); (field.inputs = field.inputs || []).push(radioInput); group.appendChild(radioDiv); }); if (typeof field.onCreate === 'function') { field.onCreate(field.inputs || [], field, idx); } } break; } case 'file': { input = document.createElement('input'); input.type = 'file'; input.className = this._theme.getInputClasses('file'); // Support multiple files input.multiple = Boolean(field.multiple); input.name = field.multiple ? `${field.name}[]` : field.name; input.id = field.name || ''; if (field.required) input.required = true; if (field.accept) input.accept = field.accept; if (field.readonly) input.disabled = true; // For preview/info const fileInfo = document.createElement('div'); fileInfo.className = this._theme.getFileInfoClasses() + ' d-none'; // On file selection if (input) { input.addEventListener('change', async (e) => { __classPrivateFieldGet(this, _DynamicForm_instances, "m", _DynamicForm_clearValidation).call(this, field); fileInfo.classList.add('d-none'); fileInfo.innerHTML = ''; const fileInput = e.target; if (!fileInput.files || !fileInput.files.length) return; // Validate accept for all files if provided const validateFile = (f) => { if (!field.accept) return true; const acceptArr = field.accept.split(',').map((item) => item.trim().toLowerCase()); return acceptArr.some((acc) => { if (acc.startsWith('.')) { return f.name.toLowerCase().endsWith(acc); } if (acc.endsWith('/*')) { return f.type.startsWith(acc.replace('/*', '/')); } return f.type === acc; }); }; const files = Array.from(fileInput.files); if (!files.every(validateFile)) { input.setCustomValidity('File type not allowed.'); input.classList.add(this._theme.getInvalidInputClasses()); const feedback = input.parentElement?.querySelector('.' + this._theme.getValidationErrorClasses()); if (feedback) { feedback.textContent = 'Selected file type is not permitted.'; } return; } // If multiple, do not render images/videos previews if (field.multiple) { const list = document.createElement('ul'); list.className = this._theme.getFileInfoTextClasses(); files.forEach(f => { const li = document.createElement('li'); li.textContent = `${f.name} (${(f.size / 1024).toFixed(1)} KB)`; list.appendChild(li); }); fileInfo.innerHTML = ''; fileInfo.appendChild(list); fileInfo.classList.remove('d-none'); return; } const file = files[0]; if (file.type.startsWith('image/')) { const reader = new FileReader(); reader.onload = (ev) => { const img = document.createElement('img'); if (ev.target?.result) { img.src = ev.target.result; } img.className = this._theme.getFileThumbnailClasses(); img.style.maxWidth = '150px'; img.onload = function () { const self = this; if (self && self._theme) { fileInfo.innerHTML = ` <div class="${self._theme.getFileInfoTextClasses()}">Name: ${file.name}</div> <div class="${self._theme.getFileInfoTextClasses()}">Size: ${(file.size / 1024).toFixed(1)} KB</div> <div class="${self._theme.getFileInfoTextClasses()}">Dimensions: ${img.naturalWidth} x ${img.naturalHeight}</div> `; fileInfo.prepend(img); fileInfo.classList.remove('d-none'); } }.bind(this); }; reader.readAsDataURL(file); } else { fileInfo.innerHTML = ` <div class="${this._theme.getFileInfoTextClasses()}">Name: ${file.name}</div> <div class="${this._theme.getFileInfoTextClasses()}">Size: ${(file.size / 1024).toFixed(1)} KB</div> `; fileInfo.classList.remove('d-none'); } }); } group.appendChild(input); group.appendChild(fileInfo); if (typeof field.onCreate === 'function') { field.onCreate(input, field, idx); } break; } case 'dropzone': { // Create a container for Dropzone const dzDiv = document.createElement('div'); dzDiv.id = field.name ? `${field.name}_dropzone` : `dropzone_${idx}`; dzDiv.className = 'dropzone'; if (field.readonly) dzDiv.classList.add('pointer-events-none', 'opacity-50'); group.appendChild(dzDiv); input = dzDiv; // Try to initialize Dropzone if available setTimeout(() => { try { const hasDropzone = typeof window.Dropzone !== 'undefined'; if (!hasDropzone) { console.warn(`DynamicFormBuilder: Dropzone not available for field "${field.name}". Make sure to include Dropzone assets.`); } else { // Build options const hasUrl = field.dropzoneOptions && typeof field.dropzoneOptions.url === 'string' && field.dropzoneOptions.url.length > 0; const opts = { url: hasUrl ? field.dropzoneOptions.url : '/', autoProcessQueue: hasUrl ? (field.dropzoneOptions.autoProcessQueue ?? true) : false, uploadMultiple: Boolean(field.multiple), maxFiles: field.multiple ? null : 1, autoDiscover: false, paramName: field.name, addRemoveLinks: true, clickable: true, hiddenInputContainer: group, ...(field.accept ? { acceptedFiles: field.accept } : {}), ...field.dropzoneOptions }; const DZ = window.Dropzone; field.dropzoneInstance = new DZ(dzDiv, opts); field.input = field.dropzoneInstance.hiddenFileInput; field.input.id = field.name; // Basic validation clear on add/remove field.dropzoneInstance.on('addedfile', () => { __classPrivateFieldGet(this, _DynamicForm_instances, "m", _DynamicForm_clearValidation).call(this, field); }); field.dropzoneInstance.on('removedfile', () => { __classPrivateFieldGet(this, _DynamicForm_instances, "m", _DynamicForm_clearValidation).call(this, field); }); } } catch (e) { console.error(`DynamicFormBuilder: Error initializing Dropzone for field "${field.name}":`, e); } if (typeof field.onCreate === 'function' && input) { field.onCreate(input, field, idx); } }); break; } default: { input = document.createElement('input'); input.type = field.type; input.className = this._theme.getInputClasses(field.type); input.name = field.name; input.id = field.name || ''; if (field.placeholder) input.placeholder = field.placeholder; if (field.required) input.required = true; if ('min' in field) input.min = String(field.min); if ('max' in field) input.max = String(field.max); if ('value' in field) input.value = field.value; if (field.readonly) input.readOnly = true; if (typeof field.onCreate === 'function') { field.onCreate(input, field, idx); } group.appendChild(input); } } // Add to group (except checkbox/radio already done) if (field.type !== 'checkbox' && field.type !== 'radio' && field.type !== 'submit' && field.type !== 'ckeditor' && field.type !== 'select2') { // Attach events here for validation if (input) { input.addEventListener('blur', () => __classPrivateFieldGet(this, _DynamicForm_instances, "m", _DynamicForm_validateField).call(this, field)); input.addEventListener('input', () => __classPrivateFieldGet(this, _DynamicForm_instances, "m", _DynamicForm_clearValidation).call(this, field)); } } else { if (['radio'].includes(field.type) && field.inputs && field.inputs.length > 0) { field.inputs.forEach((inputElement) => { if (inputElement) { inputElement.addEventListener('change', () => { __classPrivateFieldGet(this, _DynamicForm_instances, "m", _DynamicForm_clearValidation).call(this, field); __classPrivateFieldGet(this, _DynamicForm_instances, "m", _DynamicForm_validateField).call(this, field); }); } }); } } // Validation message const invalidFeedback = document.createElement('div'); invalidFeedback.className = this._theme.getValidationErrorClasses(); group.appendChild(invalidFeedback); // Helper text (if provided) if (field.helper) { const helperText = document.createElement('div'); helperText.className = this._theme.getHelperTextClasses(); helperText.innerHTML = field.helper; group.appendChild(helperText); } form.appendChild(group); if (field.type === 'radio' || field.type === 'checkbox') { const inputs = group.querySelector('input'); field.input = inputs; } else { field.input = input; } field.group = group; // Remember CKEditor fields for activation if (field.type === 'ckeditor') this._ckeditors.push(field); }); // Submit handler form.addEventListener('submit', async (e) => { e.preventDefault(); await __classPrivateFieldGet(this, _DynamicForm_instances, "m", _DynamicForm_handleSubmit).call(this); }); this._mount.appendChild(form); // Initialize CKEditors if any if (window.initializeEditor && this._ckeditors.length > 0) { this._ckeditors.forEach(async (field) => { const textarea = field.input; if (textarea && !textarea.ckeditorInitialized) { try { // Initialize CKEditor and store the instance on the config field if (window.initializeEditor) { await window.initializeEditor(textarea).then(editor => { // Find the field config and attach the instance const fieldConfig = this._config.find(f => f.name === field.name); if (fieldConfig) { fieldConfig.ckeditorInstance = editor; } editor.editing.view.document.on('blur', () => __classPrivateFieldGet(this, _DynamicForm_instances, "m", _DynamicForm_validateField).call(this, field)); editor.editing.view.document.on('input', () => __classPrivateFieldGet(this, _DynamicForm_instances, "m", _DynamicForm_clearValidation).call(this, field)); }); } textarea.ckeditorInitialized = true; } catch (err) { console.error(`Failed to initialize CKEditor for ${field.name}:`, err); } } }); } return form; }, _DynamicForm_validateField = function _DynamicForm_validateField(field) { let value; let isValid = true; let message = ''; let inputs = field.input; // Normalize input collection for grouped fields if (field.type === 'radio') { if (!Array.isArray(inputs)) { inputs = Array.from(this._form.querySelectorAll(`input[name="${field.name}"]`)); } const checkedRadio = inputs.find(r => r.checked); value = checkedRadio ? checkedRadio.value : ''; } else if (field.type === 'checkbox') { if (!Array.isArray(inputs)) { inputs = Array.from(this._form.querySelectorAll(`input[type="checkbox"][name="${field.name}"]`)); } if (inputs.length > 1) { value = inputs.filter(c => c.checked).map(c => c.value); } else { value = inputs[0]?.checked || false; } } else if (field.type === 'select2') { const select = field.select2Instance; if (select) { const selectedValue = $(select).val(); if (field.multiple) { value = Array.isArray(selectedValue) ? selectedValue : [selectedValue]; } else { value = selectedValue ?? ''; } } } else if (field.type === 'select') { const select = Array.isArray(inputs) ? inputs[0] : inputs; if (select && select instanceof HTMLSelectElement) { if (select.multiple) { value = Array.from(select.selectedOptions).map(opt => opt.value); } else { value = select.value ?? ''; } } } else if (field.type === 'ckeditor') { if (field.ckeditorInstance) { inputs = field.ckeditorInstance.ui.view.element; value = field.ckeditorInstance.getData(); } else { value = ''; } } else if (field.type === 'textarea') { const input = Array.isArray(inputs) ? inputs[0] : inputs; value = input?.value ?? ''; } else { const input = Array.isArray(inputs) ? inputs[0] : inputs; value = input?.value ?? ''; } // ===== Validation rules ===== // Required if (field.required) { let missing = false; if (field.type === 'checkbox') { if (Array.isArray(value)) { missing = value.length === 0; } else { // Single checkbox: required means it must be checked (true) missing = value !== true; } } else if (field.type === 'select' && Array.isArray(value)) { missing = value.length === 0 || value.every(v => !v); } else if (field.type === 'radio') { missing = !value; } else { missing = value === undefined || value === null || (typeof value === 'string' && !value.trim()) || (Array.isArray(value) && value.length === 0); } if (missing) { isValid = false; message = field.validation?.required || `${field.label || field.name} is required`; } } // minLength if (isValid && field.validation?.minLength && ((typeof value === 'string' && value.length < field.validation.minLength) || (Array.isArray(value) && value.length < field.validation.minLength))) { isValid = false; message = field.validation.minLengthMsg || `Minimum length is ${field.validation.minLength}`; } // maxLength if (isValid && field.validation?.maxLength && ((typeof value === 'string' && value.length > field.validation.maxLength) || (Array.isArray(value) && value.length > field.validation.maxLength))) { isValid = false; message = field.validation.maxLengthMsg || `Maximum length is ${field.validation.maxLength}`; } // Validate inputs if (isValid) { const validation = __classPrivateFieldGet(this, _DynamicForm_instances, "m", _DynamicForm_validateInputs).call(this, field, value); isValid = validation.isValid; if (!isValid) { message = validation.message; } } // Pattern (only for strings) if (isValid && field.validation?.pattern && typeof value === 'string') { const pattern = field.validation.pattern instanceof RegExp ? field.validation.pattern : new RegExp(field.validation.pattern); if (!pattern.test(value)) { isValid = false; message = field.validation.patternMsg || 'Invalid format'; } } // Custom validator if (isValid && typeof field.validation?.custom === 'function') { const customResult = field.validation.custom(value, inputs, field); if (customResult !== true) { isValid = false; message = typeof customResult === 'string' ? customResult : 'Invalid'; } } // ===== Feedback handling ===== // For groups (radio/checkbox), use the first input for feedback targeting // Normalize feedbackTarget to always be a DOM node let feedbackTarget = Array.isArray(inputs) ? inputs[0] : inputs; // If it's a jQuery object, extract the first DOM node if (feedbackTarget && feedbackTarget.jquery) { feedbackTarget = feedbackTarget[0]; } // Find group container and feedback div if (feedbackTarget && typeof feedbackTarget.closest === 'function') { const group = field.group; const feedbackDiv = group?.querySelector('.' + this._theme.getValidationErrorClasses()); if (field.type === 'select2' && field.$select2Container) { const $container = field.$select2Container; const select2SelectionElement = $container.querySelector('.select2-selection'); if (select2SelectionElement) { select2SelectionElement.classList.toggle(this._theme.getInvalidInputClasses(), !isValid); } } else if (Array.isArray(inputs) && inputs.length > 0) { // Set validity classes for all group items (radios/checkboxes) inputs.forEach(input => { if (input) { input.classList.toggle(this._theme.getInvalidInputClasses(), !isValid); input.classList.toggle(this._theme.getValidInputClasses(), isValid); } }); } else if (feedbackTarget) { feedbackTarget.classList.toggle(this._theme.getInvalidInputClasses(), !isValid); feedbackTarget.classList.toggle(this._theme.getValidInputClasses(), isValid); } // Set feedback message if (feedbackDiv) { feedbackDiv.textContent = isValid ? '' : message; if (!isValid) { feedbackDiv.style.display = 'block'; } else { feedbackDiv.style.display = ''; } } } return isValid; }, _DynamicForm_validateInputs = function _DynamicForm_validateInputs(field, value) { switch (field.type) { case 'text': case 'password': case 'search': case 'hidden': return { isValid: typeof value === 'string', message: 'Invalid string format.' }; case 'number': if (typeof value === 'string' && value.trim() === '') { // Allow empty number when not required return { isValid: !field.required, message: 'Invalid number format.' }; } if (value === null || typeof value === 'undefined') { return { isValid: !field.required, message: 'Invalid number format.' }; } if (isNaN(Number(value))) { return { isValid: false, message: 'Invalid number format.' }; } return { isValid: true, message: '' }; case 'email': if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) || (value.trim() === '' && !field.required)) { return { isValid: true, message: '' }; } return { isValid: false, message: 'Invalid email format.' }; case 'url': try { new URL(value); return { isValid: true, message: '' }; } catch { return { isValid: false, message: 'Invalid URL format.' }; } case 'tel': if (/^[\d\-\+\(\) ]+$/.test(value)) { return { isValid: true, message: '' }; } return { isValid: false, message: 'Invalid telephone format.' }; case 'date': if (/^\d{4}-\d{2}-\d{2}$/.test(value) && !isNaN(Date.parse(value))) { return { isValid: true, message: '' }; } return { isValid: false, message: 'Invalid date format. Use YYYY-MM-DD.' }; case 'color': if (/^#[0-9a-fA-F]{6}$/.test(value)) { return { isValid: true, message: '' }; } return { isValid: false, message: 'Invalid color format. Use #RRGGBB.' }; case 'checkbox': if (Array.isArray(value)) { if (!field.required) return { isValid: true, message: '' }; return { isValid: value.length > 0, message: 'No value selected.' }; } if (typeof value === 'boolean') { if (!field.required) return { isValid: true, message: '' }; return { isValid: value === true, message: 'No value selected.' }; } // Fallback for unexpected types return { isValid: !field.required, message: 'No value selected.' }; case 'radio': if (!field.required && (value === '' || value === null || typeof value === 'undefined')) { return { isValid: true, message: '' }; } return { isValid: !!value, message: 'No value selected.' }; default: return { isValid: true, message: '' }; } }, _DynamicForm_clearValidation = function _DynamicForm_clearValidation(field) { if (field.type === 'radio') { // Get all radios in the group const group = field.group; if (group) { group.classList.remove(this._theme.getInvalidInputClasses()); // Hide the feedback message const invalidFeedback = group.querySelector('.' + this._theme.getValidationErrorClasses()); if (invalidFeedback) { invalidFeedback.style.display = 'none'; } } } else { let input = field.input; if (input) { input.classList.remove(this._theme.getInvalidInputClasses()); input.classList.remove(this._theme.getValidInputClasses()); const feedbackElement = input.parentElement?.querySelector('.' + this._theme.getValidationErrorClasses()); if (feedbackElement) { feedbackElement.textContent = '';