| | |
| | | this.selectors = { |
| | | tabs: { |
| | | nav: 'nav.tabs', |
| | | sections: '.tab.content', //querySelectorAll |
| | | sections: '.tab-content', //querySelectorAll |
| | | progress: { |
| | | progress: '.progress', |
| | | fill: '.progress .fill', |
| | |
| | | }, |
| | | repeater: { |
| | | repeater: '.repeater', //querySelectorAll |
| | | row: '.repeater-row', |
| | | handle: '.drag-handle', |
| | | header: '.repeater-row-header', |
| | | remove: '.remove-row', |
| | | add: '.add-repeater-row', |
| | |
| | | case 'dismiss-restore': |
| | | form.ui.status.status.hidden = true; |
| | | break; |
| | | case 'next-step': |
| | | e.preventDefault(); |
| | | form.tabs.ui.buttons.forEach((btn, index) => { |
| | | if (btn.dataset.tab === form.tabs.activeTab) { |
| | | let next = form.tabs.ui.buttons[index + 1]??false; |
| | | if (next) { |
| | | window.jvbTabs.switchTab(next.dataset.tab, window.jvbTabs.getConfig(next)); |
| | | } |
| | | } |
| | | }); |
| | | break; |
| | | case 'prev-step': |
| | | e.preventDefault(); |
| | | form.tabs.ui.buttons.forEach((btn, index) => { |
| | | if (btn.dataset.tab === form.tabs.activeTab) { |
| | | let prev = form.tabs.ui.buttons[index - 1]??false; |
| | | if (prev) { |
| | | window.jvbTabs.switchTab(prev.dataset.tab, window.jvbTabs.getConfig(prev)); |
| | | } |
| | | } |
| | | }); |
| | | break; |
| | | } |
| | | } |
| | | } |
| | |
| | | }); |
| | | } |
| | | const collectionName = collectionField.dataset.field; |
| | | this.maybeUpdateCollectionDisplay(field); |
| | | window.debouncer.schedule( |
| | | `collection:${collectionName}`, |
| | | () => this.updateCollectionField(collectionField), |
| | |
| | | data: this.changes.get(form.id)?.changes??{}, |
| | | }); |
| | | } |
| | | if (form.options.endpoint) { |
| | | if (form.options.endpoint && !form.options.handled) { |
| | | console.log('Submitting form...'); |
| | | await this.handleServerSave(form, this.collectFormData(form.form)); |
| | | } |
| | |
| | | changes.changes[name] = value; |
| | | } |
| | | |
| | | if (form.tabs) { |
| | | console.log('Updating step progress on change'); |
| | | this.updateStepProgress(form); |
| | | } |
| | | |
| | | this.changes.set(form.id, changes); |
| | | if (form.options.cache) { |
| | | this.scheduleBackup(); |
| | | } |
| | | |
| | | if (form.options.endpoint) { |
| | | if (form.options.endpoint && !form.options.handled) { |
| | | this.scheduleServerSave(form, changes); |
| | | } |
| | | } |
| | | updateStepProgress(form) { |
| | | console.log('First check in updateStepProgress'); |
| | | if (!form.tabs) return; |
| | | let currentStep = this.getCurrentStep(form); |
| | | console.log('Current step: ', currentStep); |
| | | if (!currentStep) return; |
| | | |
| | | //The progress root will be 1 less than whatever the current step is |
| | | currentStep = currentStep - 1; |
| | | |
| | | let section = this.getCurrentSection(form); |
| | | console.log('Current section: ', section); |
| | | if (!section) return; |
| | | |
| | | let total = 0; |
| | | let filled = 0; |
| | | section.childNodes.forEach(f => { |
| | | console.log('Child node: ', f); |
| | | if (f.classList.contains('field')) { |
| | | total++; |
| | | let value = this.getFieldValue(f.querySelector(this.inputSelectors)); |
| | | console.log('Value', value); |
| | | if (value.length > 0) { |
| | | filled++; |
| | | } |
| | | } |
| | | }); |
| | | //Shouldn't happen, but simplifies things if it does |
| | | if (total === 0) return; |
| | | let current = currentStep + (filled / total); |
| | | |
| | | let totalSteps = form.ui.tabs.sections.length; |
| | | window.showProgress( |
| | | form.ui.tabs.progress, |
| | | current, |
| | | totalSteps, |
| | | `Step ${currentStep +1} of ${totalSteps}` |
| | | ); |
| | | } |
| | | getCurrentStep(form) { |
| | | let step = null; |
| | | form.tabs.ui.buttons.forEach(btn => { |
| | | if (btn.dataset.tab === form.tabs.activeTab) { |
| | | step = btn.dataset.step; |
| | | } |
| | | }); |
| | | return step; |
| | | } |
| | | |
| | | getCurrentSection(form) { |
| | | console.log('Getting current section: ', form.tabs.ui.sections); |
| | | let section = null; |
| | | form.tabs.ui.sections.forEach(s => { |
| | | if (s.dataset.tab === form.tabs.activeTab) { |
| | | section = s; |
| | | } |
| | | }); |
| | | return section; |
| | | } |
| | | |
| | | scheduleBackup() { |
| | | window.debouncer.schedule( |
| | |
| | | endpoint: Object.hasOwn(form.dataset, 'save') ? form.dataset.save: '', |
| | | showStatus: true, |
| | | showSummary: false, |
| | | handled: Object.hasOwn(form.dataset, 'handled'), |
| | | cache: true, |
| | | ignore: [], |
| | | ... options |
| | |
| | | } |
| | | } |
| | | checkForRepeaters(form) { |
| | | |
| | | if (!form.querySelector(this.selectors.repeater.repeater)) return; |
| | | |
| | | form.querySelectorAll(this.selectors.repeater.repeater).forEach(repeater => { |
| | | let config = { |
| | | id: repeater.querySelector('template').className??window.generateID('repeater'), |
| | |
| | | ); |
| | | |
| | | if (window.Sortable) { |
| | | config.sortable = new Sortable(repeater, { |
| | | handle: this.selectors.repeater.header, |
| | | config.sortable = new Sortable(config.ui.items, { |
| | | animation: 150, |
| | | draggable: this.selectors.repeater.row, |
| | | handle: this.selectors.repeater.handle, |
| | | ghostClass: 'repeater-row-ghost', |
| | | chosenClass: 'repeater-row-chosen', |
| | | dragClass: 'dragging', |
| | | onEnd: () => { |
| | | this.reindexList(repeater); |
| | | this.reindexList(config.ui.items); |
| | | } |
| | | }); |
| | | } |
| | |
| | | |
| | | let form = this.getForm(repeater); |
| | | this.initializeFields(repeater, form); |
| | | this.reindexList(config.ui.items); |
| | | this.a11y.announce('Row added'); |
| | | } |
| | | removeRepeaterRow(row) { |
| | | let repeater = row.closest('[data-repeater-id]'); |
| | | row.remove(); |
| | | this.reindexList(repeater); |
| | | let items = this.repeaters.get(repeater); |
| | | if (items) { |
| | | this.reindexList(items.ui.items); |
| | | } |
| | | |
| | | this.a11y.announce('Row removed'); |
| | | } |
| | | checkForTagLists(form) { |
| | |
| | | |
| | | checkForTabs(form, config) { |
| | | if (window.jvbTabs && form.querySelector('nav.tabs')) { |
| | | |
| | | config.tabs = window.jvbTabs.registerTab(form, { |
| | | preCheck: (section, tabConfig) => { |
| | | return this.validateStep(section, config); |
| | | preCheck: (activeSection, switchTo, tabConfig) => { |
| | | return this.validateStep(activeSection, switchTo, tabConfig); |
| | | } |
| | | }); |
| | | config.ui.tabs = window.uiFromSelectors(this.selectors.tabs, form); |
| | | config.ui.tabs.sections = Array.from(form.querySelectorAll(this.selectors.tabs.sections)); |
| | | config.ui.tabs.inputs = {}; |
| | | config.ui.tabs.sections.forEach(section => { |
| | | config.ui.tabs.inputs[section.dataset.tab] = Array.from(section.querySelectorAll(this.inputs)); |
| | | config.ui.tabs.inputs[section.dataset.tab] = Array.from(section.querySelectorAll(this.inputSelectors)); |
| | | }); |
| | | config.ui.tabs.buttons = Array.from(form.querySelectorAll(this.selectors.tabs.buttons)); |
| | | |
| | |
| | | if (config.ui.tabs.progress) { |
| | | const section = config.ui.tabs.sections.filter(section => section.dataset.tab === data.current)[0]??false; |
| | | if (!section) return; |
| | | const step = section.dataset.step; |
| | | const total = config.ui.sections.length; |
| | | const step = section.dataset.step - 1; |
| | | const total = config.ui.tabs.sections.length; |
| | | |
| | | window.showProgress( |
| | | config.ui.tabs.progress, |
| | | step, |
| | | total |
| | | total, |
| | | `Step ${step + 1} of ${total}` |
| | | ); |
| | | } |
| | | } |
| | |
| | | this.forms.set(config.id, config); |
| | | } |
| | | } |
| | | validateStep(section, config) { |
| | | const formId = section.closest('[data-form-id]')?.dataset.formId; |
| | | validateStep(currentSection, switchTo, config) { |
| | | let doit = false; |
| | | if (config.ui.sections) { |
| | | config.ui.sections.forEach((section, index) => { |
| | | if (section === currentSection) { |
| | | let prev = index -1; |
| | | if (prev >= 0) { |
| | | let prevSection = config.ui.sections[prev]; |
| | | if (prevSection && prevSection.dataset.tab === switchTo) { |
| | | doit = true; |
| | | } |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | if (doit) return true; |
| | | |
| | | const formId = currentSection.closest('[data-form-id]')?.dataset.formId; |
| | | if (!formId) return true; |
| | | |
| | | const form = this.forms.get(formId); |
| | |
| | | .filter(item => |
| | | item && |
| | | item.form === formId && |
| | | item.section === section.dataset.tab && |
| | | item.section === currentSection.dataset.tab && |
| | | !item.element.closest('[hidden]') |
| | | ); |
| | | |
| | |
| | | * @param {HTMLElement} container |
| | | */ |
| | | reindexList(container) { |
| | | const fieldName = container.dataset.field || container.dataset.repeaterId || container.dataset.tagListId; |
| | | let fieldName = container.dataset.field || container.dataset.repeaterId || container.dataset.tagListId; |
| | | if (!fieldName) { |
| | | let parent = container.closest('[data-field]'); |
| | | fieldName = parent.dataset.field || parent.dataset.repeaterId || parent.dataset.tagListId; |
| | | } |
| | | |
| | | Array.from(container.children).forEach((item, index) => { |
| | | item.dataset.index = `${index}`; |
| | | |
| | | let rowNum = item.querySelector('.row-number'); |
| | | if (rowNum) { |
| | | rowNum.textContent = `#${index + 1}`; |
| | | } |
| | | let rowTitle = item.querySelector('.row-title'); |
| | | let rowLabel = rowTitle ? rowTitle.dataset.label : null; |
| | | // Find ALL inputs within this item, not just direct children |
| | | const inputs = item.querySelectorAll('input, select, textarea'); |
| | | |
| | |
| | | if (input.type === 'file') return; |
| | | |
| | | // Get the field name from the input's data-field or name |
| | | const inputField = input.dataset.field || input.name.split(':').pop(); |
| | | const inputField = Object.hasOwn(input.dataset, 'field') ? input.dataset.field : input.name.split(':').pop(); |
| | | if (inputField === rowLabel) { |
| | | rowTitle.textContent = window.escapeHtml(input.value); |
| | | } |
| | | |
| | | // Re-prefix with the new index, passing item as wrapper |
| | | window.prefixInput( |
| | | input, |
| | | `${fieldName}:${index}:`, |
| | | `${fieldName}:${index}:${inputField}`, |
| | | item, |
| | | false, |
| | | true, |
| | | true |
| | | ); |
| | | }); |
| | |
| | | const value = this.getFieldValue(field); |
| | | this.updateItem(field.dataset.field, value, form); |
| | | } |
| | | |
| | | //TODO: Store these selectors in the repeater object maybe? |
| | | maybeUpdateCollectionDisplay(field) { |
| | | let parent = field.closest('[data-repeater-id],[data-tag-list-id]'); |
| | | if (!parent) { |
| | | return; |
| | | } |
| | | if (parent.dataset.fieldType === 'repeater') { |
| | | let row = field.closest('.repeater-row'); |
| | | if (!row) return; |
| | | |
| | | let title = row.querySelector('.row-title'); |
| | | if (!title || !title.dataset.label) return; |
| | | console.log(field.dataset.field.split(':').pop()); |
| | | console.log(title.dataset.label); |
| | | if (field.dataset.field.split(':').pop() === title.dataset.label) { |
| | | title.textContent = this.getFieldValue(field.querySelector(this.inputSelectors)); |
| | | } |
| | | } |
| | | |
| | | } |
| | | /********************************************************************** |
| | | VALIDATION |
| | | **********************************************************************/ |
| | |
| | | let value = []; |
| | | Array.from(items.children).forEach(row => { |
| | | let rowData = {}; |
| | | row.querySelectorAll('input[type="hidden"]').forEach(hidden => { |
| | | rowData[hidden.name] = hidden.value; |
| | | }); |
| | | |
| | | row.querySelectorAll('[data-field]').forEach(field => { |
| | | if (!ignore.includes(field.dataset.field)) { |
| | | const input = this.getFieldInput(field); |