From 3b83905603d44b1a08f8b2b36a605808ce686ad6 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 02 Jun 2026 00:46:48 +0000
Subject: [PATCH] =double checking schema outputs for legacytattooremoval
---
assets/js/concise/FormController.js | 1644 ++++++++++++++++++++++++++++++----------------------------
1 files changed, 856 insertions(+), 788 deletions(-)
diff --git a/assets/js/concise/FormController.js b/assets/js/concise/FormController.js
index 70e5b6a..3ba05b4 100644
--- a/assets/js/concise/FormController.js
+++ b/assets/js/concise/FormController.js
@@ -19,6 +19,7 @@
this.isRestoring = false;
this.hasListeners = false;
+ this.hasUploads = false;
this.summaryTemplate = false;
this.init();
@@ -30,6 +31,20 @@
this.initListeners();
this.initStore();
this.initValidators();
+ this.initUploadSubscription();
+ }
+
+ initUploadSubscription() {
+ window.jvbUploads.subscribe((event, data) => {
+ if (!this.hasUploads) return;
+ if (event === 'upload-received') {
+ let form = this.getForm(data.field);
+ if (form) {
+ this.updateItem(`${data.field.dataset.field}_tempUpload`, data.id, form);
+ }
+
+ }
+ });
}
initElements() {
this.inputSelectors = 'input, textarea, select';
@@ -51,7 +66,12 @@
status: '.fstatus',
message: '.fstatus .message',
icon: '.fstatus .icon',
- actions: '.fstatus .actions'
+ actions: '.fstatus .actions',
+ },
+ restore: {
+ container: '.restore-form',
+ restore: '[data-action="restore"]',
+ clear: '[data-action="clear"]',
}
},
inputs: this.inputSelectors, //querySelectorAll
@@ -109,20 +129,20 @@
this.tagListClick = this.handleTagListClick.bind(this);
this.tagListInput = this.handleTagListInput.bind(this);
}
- addFormListeners(form) {
- form.addEventListener('click', this.clickHandler);
- form.addEventListener('change', this.changeHandler);
- form.addEventListener('input', this.inputHandler);
- form.addEventListener('blur', this.blurHandler);
- form.addEventListener('submit', this.submitHandler);
- }
- removeFormListeners(form) {
- form.removeEventListener('click', this.clickHandler);
- form.removeEventListener('change', this.changeHandler);
- form.removeEventListener('input', this.inputHandler);
- form.removeEventListener('blur', this.blurHandler);
- form.removeEventListener('submit', this.submitHandler);
- }
+ addFormListeners(form) {
+ form.addEventListener('click', this.clickHandler);
+ form.addEventListener('change', this.changeHandler);
+ form.addEventListener('input', this.inputHandler);
+ form.addEventListener('blur', this.blurHandler);
+ form.addEventListener('submit', this.submitHandler);
+ }
+ removeFormListeners(form) {
+ form.removeEventListener('click', this.clickHandler);
+ form.removeEventListener('change', this.changeHandler);
+ form.removeEventListener('input', this.inputHandler);
+ form.removeEventListener('blur', this.blurHandler);
+ form.removeEventListener('submit', this.submitHandler);
+ }
initStore() {
const store = window.jvbStore.register(
'forms',
@@ -153,41 +173,59 @@
}
});
}
- showPendingNotification(formId, changes) {
- let form = this.forms.get(formId);
+ showPendingNotification(formId, changes) {
+ let form = this.forms.get(formId);
+ if (!form) return;
+ let element = form.element;
+ if (!element) {
+ console.warn(`Form element not found for: ${formId}`);
+ return;
+ }
+
+ form.ui.restore.container.hidden = false;
+ const handleRestore = async (changes, element) => {
+ this.isRestoring = true;
+ let theChanges = {['fields']: changes};
+ await this.checkStoredUploads(changes, element);
+ this.populate.populate(element, theChanges);
+ this.a11y.announce('Previous changes restored');
+ this.isRestoring = false;
+ form.ui.restore.container.remove();
+ };
+ const clearRestore = async (formId) => {
+ await this.checkStoredUploads(changes, element, false);
+ await this.store.delete(formId);
+ this.a11y.announce('Previous changes discarded');
+ form.ui.restore.container.remove();
+ };
+ form.ui.restore.restore.addEventListener('click', () => handleRestore(changes, element));
+ form.ui.restore.clear.addEventListener('click', async () => clearRestore(formId));
+ }
+ async checkStoredUploads(changes, element, restore = true) {
+ let form = this.forms.get(element.dataset.formId);
if (!form) return;
- let element = form.element;
- if (!element) {
- console.warn(`Form element not found for: ${formId}`);
- return;
+ let uploads = [];
+ for (let [key, value] of Object.entries(changes)) {
+ if (key.includes('_tempUpload')) {
+ let field = key.replace('_tempUpload', '');
+
+ if (Object.hasOwn(form.ui.uploads, field)) {
+ uploads = [
+ ... uploads,
+ ... value
+ ];
+ }
+ }
}
- const notification = document.createElement('div');
- notification.className = 'pendingChanges';
- notification.innerHTML = `
- <p>We noticed unsaved changes from last time. Would you like to restore them?</p>
- <button class="restore" type="button" data-form-id="${formId}">Restore</button>
- <button class="discard" type="button" data-form-id="${formId}">Discard</button>`;
+ if (uploads.length > 0) {
+ if (restore) {
+ await window.jvbUploads.restoreUploads(uploads);
+ } else {
+ await window.jvbUploads.clearUploads(uploads);
+ }
- element.insertBefore(notification, form.ui.status.status);
-
- notification.querySelector('.restore').addEventListener('click', async () => {
- this.isRestoring = true;
-
- let theChanges = {['fields']: changes};
- this.populate.populate(element, theChanges);
- this.a11y.announce('Previous changes restored');
-
- this.isRestoring = false;
- notification.remove();
- });
-
- notification.querySelector('.discard').addEventListener('click', async () => {
- await this.store.delete(formId);
- this.a11y.announce('Previous changes discarded');
- notification.remove();
- });
-
+ }
}
initValidators() {
this.validators = {
@@ -333,7 +371,7 @@
// Dependencies still need checking
if (this.dependencies.has(field.dataset.field)) {
let dependency = this.dependencies.get(field.dataset.field);
- dependency.items.forEach(item => {
+ dependency.forEach(item => {
this.checkFieldDependency(item, field.dataset.field);
});
}
@@ -349,7 +387,7 @@
//Dependencies
if (this.dependencies.has(field.dataset.field)) {
let dependency = this.dependencies.get(field.dataset.field);
- dependency.items.forEach(item => {
+ dependency.forEach(item => {
this.checkFieldDependency(item, field.dataset.field);
});
}
@@ -438,6 +476,7 @@
* @param form
*/
updateItem(name, value, form) {
+ if (value === undefined) return;
if (!this.changes.has(form.id)) {
this.changes.set(form.id, {
id: form.id,
@@ -447,7 +486,16 @@
});
}
let changes = this.changes.get(form.id);
- changes.changes[name] = value;
+ //If it is temporary uploads, we need to store them all
+ if (name.includes('_tempUpload')) {
+ if (!Object.hasOwn(changes.changes, name)) {
+ changes.changes[name] = [];
+ }
+ changes.changes[name].push(value);
+ } else {
+ changes.changes[name] = value;
+ }
+
this.changes.set(form.id, changes);
if (form.options.cache) {
this.scheduleBackup();
@@ -514,6 +562,17 @@
* @param {object} options
*/
registerForm(form, options) {
+ options = {
+ autoUpload: false,
+ imageMeta: true,
+ delay: 1500,
+ endpoint: Object.hasOwn(form.dataset, 'save') ? form.dataset.save: '',
+ showStatus: true,
+ showSummary: false,
+ cache: true,
+ ignore: [],
+ ... options
+ };
//Bail if form already registered
if (Object.hasOwn(form.dataset, 'formId') && this.forms.has(form.dataset.formId)) return;
@@ -528,641 +587,640 @@
element: form,
id: formId,
status: '',
- options: {
- autoUpload: options.autoUpload??false,
- imageMeta: options.imageMeta??true,
- delay: options.delay??1500,
- endpoint: options.save??form.dataset.save??'',
- showStatus: options.showStatus??true,
- showSummary: options.showSummary??false,
- cache: options.cache??true,
- ignore: options.ignore??[]
- },
+ options: options,
ui: window.uiFromSelectors(this.selectors.forms, form)
};
+ config.ui.fields = {};
+ form.querySelectorAll('[data-field]').forEach((field) => {
+ config.ui.fields[field.dataset.field] = field;
+ });
+
this.initializeFields(form, config);
this.forms.set(formId, config);
return config;
}
- clearForm(formId) {
- const config = this.forms.get(formId);
- if (!config) return;
+ clearForm(formId) {
+ const config = this.forms.get(formId);
+ if (!config) return;
- if (config.unsubscribeTabs) {
- config.unsubscribeTabs();
+ if (config.unsubscribeTabs) {
+ config.unsubscribeTabs();
+ }
+ if(config.tabs) {
+ window.jvbTabs.removeTab(config.element);
+ }
+
+ if (config.cache && this.changes.has(formId)) this.saveCache(formId);
+
+ // Cleanup items
+ for (let [id, input] of this.inputs.entries()) {
+ if (input.form === formId) {
+ this.inputs.delete(id);
}
- if(config.tabs) {
- window.jvbTabs.removeTab(config.element);
+ }
+ // Clean up dependencies for this form
+ this.dependencies.forEach((dependency, fieldName) => {
+ dependency = dependency.filter(item => item.form !== formId);
+
+ // Remove the dependency entry entirely if no items left
+ if (dependency.length === 0) {
+ this.dependencies.delete(fieldName);
}
+ });
- if (config.cache && this.changes.has(formId)) this.saveCache(formId);
+ if (Object.hasOwn(config, 'hasQuill') && this.quillInstances.has(formId)) {
+ const instances = this.quillInstances.get(formId);
+ instances.forEach(quillInstance => {
+ // Disable the editor
+ quillInstance.disable();
- // Cleanup items
- for (let [id, input] of this.inputs.entries()) {
- if (input.form === formId) {
- this.inputs.delete(id);
+ // Remove all event listeners
+ quillInstance.off('text-change');
+ quillInstance.off('selection-change');
+
+ // Get the container elements
+ const container = quillInstance.container.parentElement;
+ const toolbar = container?.querySelector('.ql-toolbar');
+
+ // Remove toolbar
+ if (toolbar) {
+ toolbar.remove();
}
- }
- // Clean up dependencies for this form
- this.dependencies.forEach((dependency, fieldName) => {
- dependency.items = dependency.items.filter(item => item.form !== formId);
- // Remove the dependency entry entirely if no items left
- if (dependency.items.length === 0) {
- this.dependencies.delete(fieldName);
+ // Clear the editor content
+ quillInstance.setText('');
+
+ // Remove container
+ if (container && container.classList.contains('editor-container')) {
+ const textarea = container.nextElementSibling;
+ if (textarea?.tagName === 'TEXTAREA') {
+ textarea.style.display = '';
+ }
+ container.remove();
}
});
- if (Object.hasOwn(config, 'hasQuill') && this.quillInstances.has(formId)) {
- const instances = this.quillInstances.get(formId);
- instances.forEach(quillInstance => {
- // Disable the editor
- quillInstance.disable();
-
- // Remove all event listeners
- quillInstance.off('text-change');
- quillInstance.off('selection-change');
-
- // Get the container elements
- const container = quillInstance.container.parentElement;
- const toolbar = container?.querySelector('.ql-toolbar');
-
- // Remove toolbar
- if (toolbar) {
- toolbar.remove();
+ this.quillInstances.delete(formId);
+ }
+ let checks = {
+ repeater: this.repeaters,
+ tagList: this.tagLists,
+ charLimit: this.charLimits,
+ quantity: this.quantityFields
+ };
+ for (let [type, check] of Object.entries(checks)) {
+ if (check.size === 0) continue;
+ let hasAny = Array.from(check.values()).filter(item => item.form === formId);
+ if (hasAny.length > 0) {
+ hasAny.forEach(item => {
+ switch (type) {
+ case 'repeater':
+ this.removeRepeaterListeners(item.element);
+ break;
+ case 'tagList':
+ this.removeTagListListeners(item.element);
+ break;
+ case 'charLimit':
+ this.removeCharacterLimitListeners(item.element);
+ break;
+ case 'quantity':
+ this.removeQuantityListeners(item.element);
+ break;
}
- // Clear the editor content
- quillInstance.setText('');
-
- // Remove container
- if (container && container.classList.contains('editor-container')) {
- const textarea = container.nextElementSibling;
- if (textarea?.tagName === 'TEXTAREA') {
- textarea.style.display = '';
- }
- container.remove();
+ if (check.has(item.id)) {
+ check.delete(item.id);
}
});
-
- this.quillInstances.delete(formId);
}
- let checks = {
- repeater: this.repeaters,
- tagList: this.tagLists,
- charLimit: this.charLimits,
- quantity: this.quantityFields
- };
- for (let [type, check] of Object.entries(checks)) {
- if (check.size === 0) continue;
- let hasAny = Array.from(check.values()).filter(item => item.form === formId);
- if (hasAny.length > 0) {
- hasAny.forEach(item => {
- switch (type) {
- case 'repeater':
- this.removeRepeaterListeners(item.element);
- break;
- case 'tagList':
- this.removeTagListListeners(item.element);
- break;
- case 'charLimit':
- this.removeCharacterLimitListeners(item.element);
- break;
- case 'quantity':
- this.removeQuantityListeners(item.element);
- break;
- }
-
- if (check.has(item.id)) {
- check.delete(item.id);
- }
- });
- }
- }
-
-
- this.removeFormListeners(config.element);
- this.forms.delete(formId);
-
- window.debouncer.cancel(`form_changes`);
- }
- defineSummaryTemplate() {
- this.summaryTemplate = true;
- let form = this;
- this.templates.define(
- 'formSummary',
- {
- refs: {
- result: '.result',
- h3: 'h3',
- p: 'p',
- },
- setup({ el, refs, manyRefs, data }) {
- const skipFields = ['sendAll', ...data.config.options.ignore??[]];
-
- for (let [key, value] of Object.entries(data.changes)) {
- if (skipFields.includes(key) || form.isEmptyValue(value)) continue;
-
- let input = Array.from(form.inputs.values())
- .find(temp => temp.field?.dataset.field === key);
- if (!input) continue;
-
- let entry = refs.result.cloneNode(true);
- let title = entry.querySelector('h3');
- let p = entry.querySelector('p');
-
- // Get field label - prioritize legend for fieldsets, then label
- const legend = input.field?.querySelector('legend');
- title.textContent = legend
- ? legend.textContent.replace('*', '').trim()
- : input.ui.label?.textContent.replace('*', '').trim();
-
-
- const formattedValue = form.formatValueForSummary(value, input);
-
- if (formattedValue instanceof HTMLElement) {
- // If it's an HTML element (repeater, tag-list, etc.), replace <p>
- p.replaceWith(formattedValue);
- } else {
- // If it's a string, set text content
- p.textContent = formattedValue;
- }
-
- el.append(entry);
- }
- let uploads = data.config?.element?.querySelectorAll('[data-upload-field]');
- if (uploads) {
- uploads.forEach(upload => {
- let label = upload.querySelector('h2')?.textContent??'Upload:';
- let imgs = upload.querySelectorAll('.item-grid.preview img');
- let field = refs.result.cloneNode(true);
- if (imgs) {
- let entry = refs.result.cloneNode(true);
- let title = field.querySelector('h3');
- let p = field.querySelector('p');
- p?.remove();
- if (title) title.textContent = label;
- imgs.forEach(img => {
- img = img.cloneNode(true);
- entry.append(img);
- });
- el.append(entry);
- }
- });
- }
-
- refs.result?.remove();
- data.config.element.after(el);
- window.fade(data.config.element, false);
- }
- }
- );
}
- initializeFields(container, config = null) {
- const fieldHandlers = {
- '[data-editor]': () => this.checkForQuill(container,config),
- 'div.quantity': () => this.checkForQuantity(container),
- '.repeater': () => this.checkForRepeaters(container, config),
- '.field.tag-list': () => this.checkForTagLists(container),
- '[data-depends-on]': () => this.checkForConditionalFields(container),
- '[data-limit]': () => this.checkForCharacterLimits(container),
- '[data-uploader],[data-upload-field]': () => this.checkForImageUploads(container, config),
- 'nav.tabs': () => this.checkForTabs(container, config),
- '[data-type="selector"]': () => this.checkForSelectors(container)
- };
+ this.removeFormListeners(config.element);
+ this.forms.delete(formId);
- for (const [selector, handler] of Object.entries(fieldHandlers)) {
- if (container.querySelector(selector)) {
- handler();
- }
- }
+ window.debouncer.cancel(`form_changes`);
+ }
+ defineSummaryTemplate() {
+ this.summaryTemplate = true;
+ let form = this;
+ this.templates.define(
+ 'formSummary',
+ {
+ refs: {
+ result: '.result',
+ h3: 'h3',
+ p: 'p',
+ },
+ setup({ el, refs, manyRefs, data }) {
+ const skipFields = ['sendAll', ...data.config.options.ignore??[]];
- let inputs = Array.from(container.querySelectorAll(this.inputSelectors))
- .filter(input => !input.closest('.ql-clipboard'));
- inputs.map(input => {
- this.getItem(input, config?.id);
- });
- }
- checkForQuill(form, config) {
- if (!form.querySelector('[data-editor]')) return;
- if (config && !Object.hasOwn(config, 'hasQuill')){
- config.hasQuill = true;
- this.forms.set(config.id, config);
- }
+ for (let [key, value] of Object.entries(data.changes)) {
+ if (skipFields.includes(key) || form.isEmptyValue(value)) continue;
- if (!this.quillInstances.has(config.id)) {
- this.quillInstances.set(config.id, new Set());
- }
+ let input = Array.from(form.inputs.values())
+ .find(temp => temp.field?.dataset.field === key);
+ if (!input) continue;
- const instances = window.jvbQuill(form);
- instances.forEach(instance => {
- this.quillInstances.get(config.id).add(instance);
- });
- }
- checkForQuantity(form) {
- if (!form.querySelector(this.selectors.number.number)) return;
- form.querySelectorAll(this.selectors.number.number).forEach(num => {
- let config = {
- id: window.generateID('quant'),
- form: form.dataset.formId,
- ui: window.uiFromSelectors(this.selectors.number, num),
- element: num
- };
- num.dataset.numId = config.id;
- this.quantityFields.set(config.id, config);
- this.addQuantityListeners(num);
- });
- }
- addQuantityListeners(el) {
- el.addEventListener('click', this.quantityClick);
- }
- removeQuantityListeners(el) {
- el.removeEventListener('click', this.quantityClick);
- }
- handleQuantityClick(e) {
- let conf = this.quantityFields.get(e.target.closest('[data-num-id]')?.dataset.numId);
- if(!conf) return;
- let change = 0;
- if (conf.ui.increase.contains(e.target)) {
- change++;
- } else if (conf.ui.decrease.contains(e.target)) {
- change--;
+ let entry = refs.result.cloneNode(true);
+ let title = entry.querySelector('h3');
+ let p = entry.querySelector('p');
+
+ // Get field label - prioritize legend for fieldsets, then label
+ const legend = input.field?.querySelector('legend');
+ title.textContent = legend
+ ? legend.textContent.replace('*', '').trim()
+ : input.ui.label?.textContent.replace('*', '').trim();
+
+
+ const formattedValue = form.formatValueForSummary(value, input);
+
+ if (formattedValue instanceof HTMLElement) {
+ // If it's an HTML element (repeater, tag-list, etc.), replace <p>
+ p.replaceWith(formattedValue);
+ } else {
+ // If it's a string, set text content
+ p.textContent = formattedValue;
+ }
+
+ el.append(entry);
}
- if (change === 0) return;
- let field = this.getField(e.target);
- let step = conf.ui.input.step;
- step = Math.max(step, 1);
- if (e.ctrlKey && e.shiftKey) {
- step = step * 50;
- } else if (e.ctrlKey) {
- step = step *5;
- } else if (e.shiftKey) {
- step = step * 10;
- }
- let value = (conf.ui.input.value === '') ? 0 : parseFloat(conf.ui.input.value);
- conf.ui.input.value = (value + (step * change));
-
- value = parseFloat(conf.ui.input.value);
-
- if (conf.ui.input.min && value < conf.ui.input.min) {
- conf.ui.input.value = conf.ui.input.min;
- conf.ui.decrease.disabled = true;
- } else if (conf.ui.input.max && value > conf.ui.input.max) {
- conf.ui.input.value = conf.ui.input.max;
- conf.ui.increase.disabled = true;
- } else {
- if (conf.ui.decrease.disabled) conf.ui.decrease.disabled = false;
- if (conf.ui.increase.disabled) conf.ui.increase.disabled = false;
- }
- }
- 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'),
- ui: window.uiFromSelectors(this.selectors.repeater, repeater),
- form: form.dataset.formId,
- element: repeater,
- field: this.getField(repeater),
- sortable: false,
- rows: []
- };
-
- if (!config.ui.add) return;
-
- let template = repeater.querySelector('template');
- this.templates.define(
- template.className,
- {
- manyRefs: {
- inputs: this.inputSelectors,
- },
- setup({el, refs, manyRefs, data}) {
- let index = config.ui.items?.children?.length??0;
- el.dataset.index = index;
-
- manyRefs.inputs?.forEach(input => {
- window.prefixInput(input, `${data.repeater.dataset.field}:${index}:`, el, false, true);
+ let uploads = data.config?.element?.querySelectorAll('[data-upload-field]');
+ if (uploads) {
+ uploads.forEach(upload => {
+ let label = upload.querySelector('h2')?.textContent??'Upload:';
+ let imgs = upload.querySelectorAll('.item-grid.preview img');
+ let field = refs.result.cloneNode(true);
+ if (imgs) {
+ let entry = refs.result.cloneNode(true);
+ let title = field.querySelector('h3');
+ let p = field.querySelector('p');
+ p?.remove();
+ if (title) title.textContent = label;
+ imgs.forEach(img => {
+ img = img.cloneNode(true);
+ entry.append(img);
});
- }
- },
- );
-
- if (window.Sortable) {
- config.sortable = new Sortable(repeater, {
- handle: this.selectors.repeater.header,
- animation: 150,
- onEnd: () => {
- this.reindexList(repeater);
+ el.append(entry);
}
});
}
- repeater.dataset.repeaterId = config.id;
- this.addRepeaterListeners(repeater);
- this.repeaters.set(config.id, config);
- });
-
+ refs.result?.remove();
+ data.config.element.after(el);
+ window.fade(data.config.element, false);
+ }
}
- addRepeaterListeners(el) {
- el.addEventListener('click', this.repeaterClick);
- }
- removeRepeaterListeners(el) {
- el.removeEventListener('click', this.repeaterClick);
- }
- handleRepeaterClick(e) {
- if (e.target.matches(this.selectors.repeater.add)) {
- this.addRepeaterRow(e.target.closest('[data-repeater-id]'));
- } else if (e.target.matches(this.selectors.repeater.remove)) {
- this.removeRepeaterRow(e.target.closest('[data-index]'));
- }
- }
- addRepeaterRow(repeater) {
- let data = {};
- data.repeater = repeater;
- let config = this.repeaters.get(repeater.dataset.repeaterId);
+ );
+ }
- let row = this.templates.create(repeater.dataset.repeaterId, data);
- config.rows.push({
- element: row,
- fields: Array.from(row.querySelectorAll('[data-field]'))
- });
- this.repeaters.set(config.id, config);
- config.ui.items.append(row);
- let form = this.getForm(repeater);
- this.initializeFields(repeater, form);
- this.a11y.announce('Row added');
- }
- removeRepeaterRow(row) {
- let repeater = row.closest('[data-repeater-id]');
- row.remove();
- this.reindexList(repeater);
- this.a11y.announce('Row removed');
- }
- checkForTagLists(form) {
- form.querySelectorAll(this.selectors.tagList.tagList)?.forEach(field=> {
- let config = {
- id: field.querySelector('template').className??window.generateID('tagList'),
- ui: window.uiFromSelectors(this.selectors.tagList, field),
- element: field,
- form: form.dataset.formId,
- format: field.dataset.tagFormat??'first_field'
- };
- if (!config.ui.input || !config.ui.add || !config.ui.items) return;
+ initializeFields(container, config = null) {
+ const fieldHandlers = {
+ '[data-editor]': () => this.checkForQuill(container,config),
+ 'div.quantity': () => this.checkForQuantity(container),
+ '.repeater': () => this.checkForRepeaters(container, config),
+ '.field.tag-list': () => this.checkForTagLists(container),
+ '[data-depends-on]': () => this.checkForConditionalFields(container),
+ '[data-limit]': () => this.checkForCharacterLimits(container),
+ '[data-uploader],[data-upload-field]': () => this.checkForImageUploads(container, config),
+ 'nav.tabs': () => this.checkForTabs(container, config),
+ '[data-type="selector"]': () => this.checkForSelectors(container)
+ };
- field.dataset.tagListId = config.id;
- config.fieldName = field.dataset.field;
-
- let template = field.querySelector('template');
- this.templates.define(
- template.className,
- {
- refs: {
- label: this.selectors.tagList.label,
- },
- manyRefs: {
- inputs: this.inputSelectors,
- },
- setup({el, refs, manyRefs, data}) {
- let index = config.ui.items?.children?.length??0;
- el.dataset.index = index;
- manyRefs.inputs?.forEach(input => {
- let wrapper = input.closest('.tag-item');
- window.prefixInput(input, `${data.fieldName}:${index}:`, wrapper, false, true)
- });
-
- if (refs.label) {
- refs.label.textContent = data.label;
- }
- }
- },
- );
- config.ui.inputs = Array.from(field.querySelectorAll(this.selectors.tagList.inputs));
- config.ui.value = Array.from(field.querySelectorAll(this.selectors.tagList.value));
- this.tagLists.set(config.id, config);
- this.addTagListListeners(field);
- });
-
+ for (const [selector, handler] of Object.entries(fieldHandlers)) {
+ if (container.querySelector(selector)) {
+ handler();
}
- addTagListListeners(el) {
- el.addEventListener('click', this.tagListClick);
- el.addEventListener('keypress', this.tagListInput);
- }
- removeTagListListeners(el) {
- el.removeEventListener('click', this.tagListClick);
- el.removeEventListener('keypress', this.tagListInput);
- }
+ }
- handleTagListClick(e) {
- if (window.targetCheck(e,this.selectors.tagList.add)) {
- this.addTagListItem(e.target.closest('[data-tag-list-id]'));
- } else if (window.targetCheck(e, this.selectors.tagList.remove)) {
- this.removeTagListItem(e.target.closest(this.selectors.tagList.item));
+ let inputs = Array.from(container.querySelectorAll(this.inputSelectors))
+ .filter(input => !input.closest('.ql-clipboard'));
+ inputs.map(input => {
+ this.getItem(input, config?.id);
+ });
+ }
+ checkForQuill(form, config) {
+ if (!form.querySelector('[data-editor]')) return;
+ if (config && !Object.hasOwn(config, 'hasQuill')){
+ config.hasQuill = true;
+ this.forms.set(config.id, config);
+ }
+
+ if (!this.quillInstances.has(config.id)) {
+ this.quillInstances.set(config.id, new Set());
+ }
+
+ const instances = window.jvbQuill(form);
+ instances.forEach(instance => {
+ this.quillInstances.get(config.id).add(instance);
+ });
+ }
+ checkForQuantity(form) {
+ if (!form.querySelector(this.selectors.number.number)) return;
+ form.querySelectorAll(this.selectors.number.number).forEach(num => {
+ let config = {
+ id: window.generateID('quant'),
+ form: form.dataset.formId,
+ ui: window.uiFromSelectors(this.selectors.number, num),
+ element: num
+ };
+ num.dataset.numId = config.id;
+ this.quantityFields.set(config.id, config);
+ this.addQuantityListeners(num);
+ });
+ }
+ addQuantityListeners(el) {
+ el.addEventListener('click', this.quantityClick);
+ }
+ removeQuantityListeners(el) {
+ el.removeEventListener('click', this.quantityClick);
+ }
+ handleQuantityClick(e) {
+ let conf = this.quantityFields.get(e.target.closest('[data-num-id]')?.dataset.numId);
+ if(!conf) return;
+ let change = 0;
+ if (conf.ui.increase.contains(e.target)) {
+ change++;
+ } else if (conf.ui.decrease.contains(e.target)) {
+ change--;
+ }
+ if (change === 0) return;
+ let field = this.getField(e.target);
+ let step = conf.ui.input.step;
+ step = Math.max(step, 1);
+ if (e.ctrlKey && e.shiftKey) {
+ step = step * 50;
+ } else if (e.ctrlKey) {
+ step = step *5;
+ } else if (e.shiftKey) {
+ step = step * 10;
+ }
+ let value = (conf.ui.input.value === '') ? 0 : parseFloat(conf.ui.input.value);
+ conf.ui.input.value = (value + (step * change));
+
+ value = parseFloat(conf.ui.input.value);
+
+ if (conf.ui.input.min && value < conf.ui.input.min) {
+ conf.ui.input.value = conf.ui.input.min;
+ conf.ui.decrease.disabled = true;
+ } else if (conf.ui.input.max && value > conf.ui.input.max) {
+ conf.ui.input.value = conf.ui.input.max;
+ conf.ui.increase.disabled = true;
+ } else {
+ if (conf.ui.decrease.disabled) conf.ui.decrease.disabled = false;
+ if (conf.ui.increase.disabled) conf.ui.increase.disabled = false;
+ }
+ }
+ 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'),
+ ui: window.uiFromSelectors(this.selectors.repeater, repeater),
+ form: form.dataset.formId,
+ element: repeater,
+ field: this.getField(repeater),
+ sortable: false,
+ rows: []
+ };
+
+ if (!config.ui.add) return;
+
+ let template = repeater.querySelector('template');
+ this.templates.define(
+ template.className,
+ {
+ manyRefs: {
+ inputs: this.inputSelectors,
+ },
+ setup({el, refs, manyRefs, data}) {
+ let index = config.ui.items?.children?.length??0;
+ el.dataset.index = index;
+
+ manyRefs.inputs?.forEach(input => {
+ window.prefixInput(input, `${data.repeater.dataset.field}:${index}:`, el, false, true);
+ });
}
- }
- addTagListItem(tagList) {
- let config = this.tagLists.get(tagList.dataset.tagListId);
- if (!config) return;
+ },
+ );
- let data = {};
- let hasValue = false;
- let isValid = true;
-
- // First pass: validate all inputs
- for (let input of config.ui.inputs) {
- const isRequired = input.required || input.dataset.required === 'true';
- const value = this.getFieldValue(input);
-
- if (value) hasValue = true;
-
- // Validate and check for errors
- const valid = this.validateField(input);
-
- if (isRequired && !value) {
- this.showError(input, 'This field is required');
- isValid = false;
- } else if (!valid) {
- isValid = false;
+ if (window.Sortable) {
+ config.sortable = new Sortable(repeater, {
+ handle: this.selectors.repeater.header,
+ animation: 150,
+ onEnd: () => {
+ this.reindexList(repeater);
}
-
- const fieldName = input.name.replace('new_','');
- data[fieldName] = value;
- }
-
- // Stop if validation failed
- if (!isValid) {
- this.a11y.announce('Please correct the errors before adding');
- const firstInvalid = config.ui.inputs.find(input => {
- const isRequired = input.required || input.dataset.required === 'true';
- return (isRequired && !this.getFieldValue(input));
- });
- if (firstInvalid) firstInvalid.focus();
- return;
- }
-
- if (!hasValue) {
- this.a11y.announce('Please fill in at least one field');
- config.ui.inputs[0].focus();
- return;
- }
-
- // Build label
- let label;
- switch (config.format) {
- case 'first_field':
- label = Object.values(data)[0];
- break;
- case 'all_fields':
- label = Object.values(data).join(', ');
- break;
- default:
- if (config.format.includes('{')) {
- label = config.format;
- for (const [key, value] of Object.entries(data)) {
- label = label.replace(`{${key}}`, value);
- }
- } else {
- label = data[config.format]??Object.values(data)[0];
- }
- break;
- }
-
- let newItem = this.templates.create(tagList.dataset.tagListId, {
- label: label,
- fieldName: config.fieldName
- });
-
- const index = config.ui.items?.children?.length ?? 0;
- newItem?.querySelectorAll('input[type=hidden]')?.forEach(input => {
- const fieldKey = input.dataset.field;
- input.name = `${config.fieldName}:${index}:${fieldKey}`;
- input.id = `${config.fieldName}:${index}:${fieldKey}`;
- input.value = data[fieldKey] || '';
- });
-
- config.ui.items.append(newItem);
-
- // Clear inputs AFTER success
- for (let input of config.ui.inputs) {
- if (['checkbox', 'radio'].includes(input.type)) {
- input.checked = false;
- } else {
- input.value = '';
- }
- this.clearValidation(input);
- }
-
- config.ui.inputs[0]?.focus();
- this.updateCollectionField(tagList);
- this.a11y.announce('Item added');
- }
- removeTagListItem(item) {
- let tagList = item.closest('[data-tag-list-id]');
- if (!tagList) return;
- item.remove();
- this.reindexList(tagList);
- this.updateCollectionField(tagList);
- this.a11y.announce('Item removed');
- }
- handleTagListInput(e) {
- let target = e.target;
- let field = target.closest('[data-tag-list-id]');
- if (!field) return;
- let config = this.tagLists.get(field.dataset.tagListId);
- if (!config) return;
-
- if (e.key === 'Enter') {
- if (target === config.ui.inputs[config.ui.inputs.length - 1]) {
- e.preventDefault();
- this.addTagListItem(target.closest('[data-tag-list-id]'));
- } else {
- e.preventDefault();
- let index = config.ui.inputs.indexOf(target);
- config.ui.inputs[index+1].focus();
- }
- }
-
- }
-
- checkForConditionalFields(form) {
- form.querySelectorAll(this.selectors.dependsOn).forEach( field => {
- const dependsOn = field.dataset.dependsOn;
- const requiredValue = field.dataset.dependsValue;
- const operator = field.dataset.dependsOperatior??'==';
-
- if (!this.dependencies.has(dependsOn)) {
- let element = document.querySelector(`[field="${dependsOn}"]`);
- if (element) {
- this.dependencies.set(dependsOn, {
- element: element,
- items: []
- });
- }
- }
- let dependency = this.dependencies.get(dependsOn);
- dependency.items.push({
- field: field,
- form: form.dataset.formId,
- requiredValue: requiredValue,
- operator: operator
- });
- this.dependencies.set(dependsOn, dependency);
- this.checkFieldDependency(dependency, dependsOn);
});
}
- checkFieldDependency(dependentField, controlFieldName) {
- const controlField = this.dependencies.get(controlFieldName);
- if (!controlField) return;
- const controlValue = this.getFieldCheckedValue(controlField.element);
- const shouldShow = this.evaluateCondition(
- controlValue,
- dependentField.requiredValue,
- dependentField.operator
- );
+ repeater.dataset.repeaterId = config.id;
+ this.addRepeaterListeners(repeater);
+ this.repeaters.set(config.id, config);
+ });
- this.toggleFieldVisibility(dependentField.field, shouldShow);
- }
- evaluateCondition(value, requiredValue, operator) {
- const fieldStr = String(value || '');
- const requiredStr = String(requiredValue || '');
+ }
+ addRepeaterListeners(el) {
+ el.addEventListener('click', this.repeaterClick);
+ }
+ removeRepeaterListeners(el) {
+ el.removeEventListener('click', this.repeaterClick);
+ }
+ handleRepeaterClick(e) {
+ if (e.target.matches(this.selectors.repeater.add)) {
+ this.addRepeaterRow(e.target.closest('[data-repeater-id]'));
+ } else if (e.target.matches(this.selectors.repeater.remove)) {
+ this.removeRepeaterRow(e.target.closest('[data-index]'));
+ }
+ }
+ addRepeaterRow(repeater) {
+ let data = {};
+ data.repeater = repeater;
+ let config = this.repeaters.get(repeater.dataset.repeaterId);
- switch (operator) {
- case '==': return fieldStr === requiredStr;
- case '!=': return fieldStr !== requiredStr;
- case '>': return parseFloat(fieldStr) > parseFloat(requiredStr);
- case '<': return parseFloat(fieldStr) < parseFloat(requiredStr);
- case '>=': return parseFloat(fieldStr) >= parseFloat(requiredStr);
- case '<=': return parseFloat(fieldStr) <= parseFloat(requiredStr);
- case 'contains': return fieldStr.includes(requiredStr);
- case 'empty': return fieldStr === '';
- case 'not_empty': return fieldStr !== '';
- default: return fieldStr === requiredStr;
- }
- }
- toggleFieldVisibility(field, show) {
- const wrapper = field.closest('.field, fieldset');
- if (!wrapper) return;
+ let row = this.templates.create(repeater.dataset.repeaterId, data);
+ config.rows.push({
+ element: row,
+ fields: Array.from(row.querySelectorAll('[data-field]'))
+ });
+ this.repeaters.set(config.id, config);
+ config.ui.items.append(row);
- wrapper.hidden = !show;
- wrapper.querySelectorAll('input, select, textarea').forEach(control => {
- control.disabled = !show;
- if (!show && control.hasAttribute('required')) {
- control.dataset.wasRequired = 'true';
- control.removeAttribute('required');
- } else if (show && control.dataset.wasRequired === 'true') {
- control.setAttribute('required', '');
- delete control.dataset.wasRequired;
+ let form = this.getForm(repeater);
+ this.initializeFields(repeater, form);
+ this.a11y.announce('Row added');
+ }
+ removeRepeaterRow(row) {
+ let repeater = row.closest('[data-repeater-id]');
+ row.remove();
+ this.reindexList(repeater);
+ this.a11y.announce('Row removed');
+ }
+ checkForTagLists(form) {
+ form.querySelectorAll(this.selectors.tagList.tagList)?.forEach(field=> {
+ let config = {
+ id: field.querySelector('template').className??window.generateID('tagList'),
+ ui: window.uiFromSelectors(this.selectors.tagList, field),
+ element: field,
+ form: form.dataset.formId,
+ format: field.dataset.tagFormat??'first_field'
+ };
+ if (!config.ui.input || !config.ui.add || !config.ui.items) return;
+
+ field.dataset.tagListId = config.id;
+ config.fieldName = field.dataset.field;
+
+ let template = field.querySelector('template');
+ this.templates.define(
+ template.className,
+ {
+ refs: {
+ label: this.selectors.tagList.label,
+ },
+ manyRefs: {
+ inputs: this.inputSelectors,
+ },
+ setup({el, refs, manyRefs, data}) {
+ let index = config.ui.items?.children?.length??0;
+ el.dataset.index = index;
+ manyRefs.inputs?.forEach(input => {
+ let wrapper = input.closest('.tag-item');
+ window.prefixInput(input, `${data.fieldName}:${index}:`, wrapper, false, true)
+ });
+
+ if (refs.label) {
+ refs.label.textContent = data.label;
}
- });
+ }
+ },
+ );
+ config.ui.inputs = Array.from(field.querySelectorAll(this.selectors.tagList.inputs));
+ config.ui.value = Array.from(field.querySelectorAll(this.selectors.tagList.value));
+ this.tagLists.set(config.id, config);
+ this.addTagListListeners(field);
+ });
+
+ }
+ addTagListListeners(el) {
+ el.addEventListener('click', this.tagListClick);
+ el.addEventListener('keypress', this.tagListInput);
+ }
+ removeTagListListeners(el) {
+ el.removeEventListener('click', this.tagListClick);
+ el.removeEventListener('keypress', this.tagListInput);
+ }
+
+ handleTagListClick(e) {
+ if (window.targetCheck(e,this.selectors.tagList.add)) {
+ this.addTagListItem(e.target.closest('[data-tag-list-id]'));
+ } else if (window.targetCheck(e, this.selectors.tagList.remove)) {
+ this.removeTagListItem(e.target.closest(this.selectors.tagList.item));
+ }
+ }
+ addTagListItem(tagList) {
+ let config = this.tagLists.get(tagList.dataset.tagListId);
+ if (!config) return;
+
+ let data = {};
+ let hasValue = false;
+ let isValid = true;
+
+ // First pass: validate all inputs
+ for (let input of config.ui.inputs) {
+ const isRequired = input.required || input.dataset.required === 'true';
+ const value = this.getFieldValue(input);
+
+ if (value) hasValue = true;
+
+ // Validate and check for errors
+ const valid = this.validateField(input);
+
+ if (isRequired && !value) {
+ this.showError(input, 'This field is required');
+ isValid = false;
+ } else if (!valid) {
+ isValid = false;
+ }
+
+ const fieldName = input.name.replace('new_','');
+ data[fieldName] = value;
+ }
+
+ // Stop if validation failed
+ if (!isValid) {
+ this.a11y.announce('Please correct the errors before adding');
+ const firstInvalid = config.ui.inputs.find(input => {
+ const isRequired = input.required || input.dataset.required === 'true';
+ return (isRequired && !this.getFieldValue(input));
+ });
+ if (firstInvalid) firstInvalid.focus();
+ return;
+ }
+
+ if (!hasValue) {
+ this.a11y.announce('Please fill in at least one field');
+ config.ui.inputs[0].focus();
+ return;
+ }
+
+ // Build label
+ let label;
+ switch (config.format) {
+ case 'first_field':
+ label = Object.values(data)[0];
+ break;
+ case 'all_fields':
+ label = Object.values(data).join(', ');
+ break;
+ default:
+ if (config.format.includes('{')) {
+ label = config.format;
+ for (const [key, value] of Object.entries(data)) {
+ label = label.replace(`{${key}}`, value);
+ }
+ } else {
+ label = data[config.format]??Object.values(data)[0];
}
+ break;
+ }
+
+ let newItem = this.templates.create(tagList.dataset.tagListId, {
+ label: label,
+ fieldName: config.fieldName
+ });
+
+ const index = config.ui.items?.children?.length ?? 0;
+ newItem?.querySelectorAll('input[type=hidden]')?.forEach(input => {
+ const fieldKey = input.dataset.field;
+ input.name = `${config.fieldName}:${index}:${fieldKey}`;
+ input.id = `${config.fieldName}:${index}:${fieldKey}`;
+ input.value = data[fieldKey] || '';
+ });
+
+ config.ui.items.append(newItem);
+
+ // Clear inputs AFTER success
+ for (let input of config.ui.inputs) {
+ if (['checkbox', 'radio'].includes(input.type)) {
+ input.checked = false;
+ } else {
+ input.value = '';
+ }
+ this.clearValidation(input);
+ }
+
+ config.ui.inputs[0]?.focus();
+ this.updateCollectionField(tagList);
+ this.a11y.announce('Item added');
+ }
+ removeTagListItem(item) {
+ let tagList = item.closest('[data-tag-list-id]');
+ if (!tagList) return;
+ item.remove();
+ this.reindexList(tagList);
+ this.updateCollectionField(tagList);
+ this.a11y.announce('Item removed');
+ }
+ handleTagListInput(e) {
+ let target = e.target;
+ let field = target.closest('[data-tag-list-id]');
+ if (!field) return;
+ let config = this.tagLists.get(field.dataset.tagListId);
+ if (!config) return;
+
+ if (e.key === 'Enter') {
+ if (target === config.ui.inputs[config.ui.inputs.length - 1]) {
+ e.preventDefault();
+ this.addTagListItem(target.closest('[data-tag-list-id]'));
+ } else {
+ e.preventDefault();
+ let index = config.ui.inputs.indexOf(target);
+ config.ui.inputs[index+1].focus();
+ }
+ }
+
+ }
+
+ checkForConditionalFields(form) {
+ form.querySelectorAll(this.selectors.dependsOn).forEach( field => {
+ const dependsOn = field.dataset.dependsOn;
+ const requiredValue = field.dataset.dependsValue;
+ const operator = field.dataset.dependsOperatior??'==';
+
+ let formData = this.forms.get(form.dataset.formId);
+
+ if (!this.dependencies.has(dependsOn)) {
+ if (Object.hasOwn(formData.ui.fields, dependsOn)) {
+ this.dependencies.set(dependsOn, []);
+ }
+ }
+ let dependency = this.dependencies.get(dependsOn);
+ if (dependency) {
+ dependency.push({
+ field: field,
+ form: form.dataset.formId,
+ requiredValue: requiredValue,
+ operator: operator
+ });
+ this.dependencies.set(dependsOn, dependency);
+ }
+
+ this.checkFieldDependency(field, dependsOn);
+ });
+ }
+ checkFieldDependency(dependentField, controlFieldName) {
+ const form = this.getForm(dependentField);
+ const controlField = this.dependencies.get(controlFieldName);
+ if (!controlField) return;
+
+
+ const controlValue = this.getFieldValue(form.ui.fields[controlFieldName]);
+ const shouldShow = this.evaluateCondition(
+ controlValue,
+ dependentField.dataset.dependsValue,
+ dependentField.dataset.dependsOperatior
+ );
+
+ this.toggleFieldVisibility(dependentField, shouldShow);
+ }
+ evaluateCondition(value, requiredValue, operator) {
+ const fieldStr = String(value || '');
+ const requiredStr = String(requiredValue || '');
+
+ switch (operator) {
+ case '==': return fieldStr === requiredStr;
+ case '!=': return fieldStr !== requiredStr;
+ case '>': return parseFloat(fieldStr) > parseFloat(requiredStr);
+ case '<': return parseFloat(fieldStr) < parseFloat(requiredStr);
+ case '>=': return parseFloat(fieldStr) >= parseFloat(requiredStr);
+ case '<=': return parseFloat(fieldStr) <= parseFloat(requiredStr);
+ case 'contains': return fieldStr.includes(requiredStr);
+ case 'empty': return fieldStr === '';
+ case 'not_empty': return fieldStr !== '';
+ default: return fieldStr === requiredStr;
+ }
+ }
+ toggleFieldVisibility(field, show) {
+ const wrapper = field.closest('.field, fieldset');
+ if (!wrapper) return;
+
+ wrapper.hidden = !show;
+ wrapper.querySelectorAll('input, select, textarea').forEach(control => {
+ control.disabled = !show;
+ if (!show && control.hasAttribute('required')) {
+ control.dataset.wasRequired = 'true';
+ control.removeAttribute('required');
+ } else if (show && control.dataset.wasRequired === 'true') {
+ control.setAttribute('required', '');
+ delete control.dataset.wasRequired;
+ }
+ });
+ }
checkForCharacterLimits(form) {
if (!form.querySelector(this.selectors.limits.hasLimit)) return;
this.countUpdaters = this.updateCount.bind(this);
@@ -1189,84 +1247,92 @@
this.addCharacterLimitListeners(input);
});
}
- addCharacterLimitListeners(input) {
- input.addEventListener('input', this.countUpdaters, {passive: true});
+ addCharacterLimitListeners(input) {
+ input.addEventListener('input', this.countUpdaters, {passive: true});
+ }
+ removeCharacterLimitListeners(input) {
+ input.removeEventListener('input', this.countUpdaters, {passive: true});
+ }
+ updateCount(e) {
+ let target = e.target;
+ let config = this.charLimits.get(target.dataset.charLimitId);
+ if (!config) return;
+ let length = target.value.length;
+ let limit = target.dataset.limit;
+ if (config.ui.current) {
+ config.ui.current.textContent = length;
+ config.ui.current.classList.toggle('exceeded', length >= limit);
+ }
+ if (length > limit) {
+ target.value = target.value.slice(0, limit);
+ }
+ }
+ checkForImageUploads(form, config) {
+ this.hasUploads = true;
+ window.jvbUploads.scanFields(form, config.options.autoUpload, config.options.imageMeta);
+ let uploads = form.querySelectorAll('[data-field-type="upload"]');
+ if (uploads) {
+ config.ui.uploads = {};
+ uploads.forEach(upload => {
+ config.ui.uploads[upload.dataset.field] = upload;
+ });
+ }
+ }
+
+ checkForTabs(form, config) {
+ if (window.jvbTabs && form.querySelector('nav.tabs')) {
+ config.tabs = window.jvbTabs.registerTab(form, {
+ preCheck: (section, tabConfig) => {
+ return this.validateStep(section, config);
}
- removeCharacterLimitListeners(input) {
- input.removeEventListener('input', this.countUpdaters, {passive: true});
- }
- updateCount(e) {
- let target = e.target;
- let config = this.charLimits.get(target.dataset.charLimitId);
- if (!config) return;
- let length = target.value.length;
- let limit = target.dataset.limit;
- if (config.ui.current) {
- config.ui.current.textContent = length;
- config.ui.current.classList.toggle('exceeded', length >= limit);
- }
- if (length > limit) {
- target.value = target.value.slice(0, limit);
+ });
+ 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.buttons = Array.from(form.querySelectorAll(this.selectors.tabs.buttons));
+
+ config.unsubscribeTabs = window.jvbTabs.subscribe((event, data) => {
+ if (event === 'tab-switched') {
+ 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;
+
+ window.showProgress(
+ config.ui.tabs.progress,
+ step,
+ total
+ );
}
}
- checkForImageUploads(form, config) {
- window.jvbUploads.scanFields(form, config.options.autoUpload, config.options.imageMeta);
- }
+ });
+ this.forms.set(config.id, config);
+ }
+ }
+ validateStep(section, config) {
+ const formId = section.closest('[data-form-id]')?.dataset.formId;
+ if (!formId) return true;
- checkForTabs(form, config) {
- if (window.jvbTabs && form.querySelector('nav.tabs')) {
- config.tabs = window.jvbTabs.registerTab(form, {
- preCheck: (section, tabConfig) => {
- return this.validateStep(section, config);
- }
- });
- 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.buttons = Array.from(form.querySelectorAll(this.selectors.tabs.buttons));
+ const form = this.forms.get(formId);
+ if (!form) return true;
- config.unsubscribeTabs = window.jvbTabs.subscribe((event, data) => {
- if (event === 'tab-switched') {
- 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 inputs = Array.from(this.inputs.values())
+ .filter(item =>
+ item &&
+ item.form === formId &&
+ item.section === section.dataset.tab &&
+ !item.element.closest('[hidden]')
+ );
- window.showProgress(
- config.ui.tabs.progress,
- step,
- total
- );
- }
- }
- });
- this.forms.set(config.id, config);
- }
- }
- validateStep(section, config) {
- const formId = section.closest('[data-form-id]')?.dataset.formId;
- if (!formId) return true;
-
- const form = this.forms.get(formId);
- if (!form) return true;
-
- const inputs = Array.from(this.inputs.values())
- .filter(item =>
- item &&
- item.form === formId &&
- item.section === section.dataset.tab &&
- !item.element.closest('[hidden]')
- );
-
- return inputs.every(item => this.validateField(item.element) === true);
- }
- checkForSelectors(form) {
- if (window.jvbSelector) window.jvbSelector.scanExistingFields(form);
- }
+ return inputs.every(item => this.validateField(item.element) === true);
+ }
+ checkForSelectors(form) {
+ if (window.jvbSelector) window.jvbSelector.scanExistingFields(form);
+ }
/**
* Mainly for repeaters or taglist
* @param {HTMLElement} container
@@ -1322,7 +1388,7 @@
}
/**********************************************************************
VALIDATION
- **********************************************************************/
+ **********************************************************************/
//text, email, url, tel, date, time, datetime, number
//select, checkbox, radio, true_false
//textarea
@@ -1357,8 +1423,6 @@
field.classList.remove('has-success');
field.classList.add('has-error');
- if (item.ui.success) item.ui.success.hidden = true;
- if (item.ui.error) item.ui.error.hidden = true;
if (item.ui.message) {
item.ui.message.hidden = false;
item.ui.message.textContent = message;
@@ -1374,9 +1438,6 @@
field.classList.remove('has-error');
field.classList.add('has-success');
- if (item.ui.success) item.ui.success.hidden = false;
- if (item.ui.error) item.ui.error.hidden = true;
-
if (item.ui.message) {
item.ui.message.hidden = message=== '';
item.ui.message.textContent = message;
@@ -1524,35 +1585,35 @@
form.ui.status.icon.className = 'icon icon-'+this.getDefaultIcon(status);
setTimeout(()=> form.ui.status.status.hidden = true, (status === 'submitted') ? 3000 : 10000);
}
- getDefaultMessage(status) {
- const messages = {
- 'saving': 'Saving changes...',
- 'autosaved': 'Changes saved locally. Submit form to send to server.',
- 'uploading': 'Uploading your form to server',
- 'submitted': 'Successfully sent to server',
- 'pending': 'Unsaved changes',
- 'restored': 'Welcome back! We\'ve restored your previous entry.',
- 'error': 'Failed to save changes. Refresh and try again?',
- 'offline': 'Changes will be saved when online'
- };
- return messages[status]??status;
+ getDefaultMessage(status) {
+ const messages = {
+ 'saving': 'Saving changes...',
+ 'autosaved': 'Changes saved locally. Submit form to send to server.',
+ 'uploading': 'Uploading your form to server',
+ 'submitted': 'Successfully sent to server',
+ 'pending': 'Unsaved changes',
+ 'restored': 'Welcome back! We\'ve restored your previous entry.',
+ 'error': 'Failed to save changes. Refresh and try again?',
+ 'offline': 'Changes will be saved when online'
+ };
+ return messages[status]??status;
+ }
+ getDefaultIcon(status) {
+ const icons = {
+ 'autosaved': 'check-circle',
+ 'submitted': 'check-circle',
+ 'restored': 'history',
+ 'error': 'close-circle',
+ 'offline': 'cloud-slash',
+ 'pending': 'exclamation-mark'
}
- getDefaultIcon(status) {
- const icons = {
- 'autosaved': 'check-circle',
- 'submitted': 'check-circle',
- 'restored': 'history',
- 'error': 'close-circle',
- 'offline': 'cloud-slash',
- 'pending': 'exclamation-mark'
- }
- return icons[status]??'';
- }
+ return icons[status]??'';
+ }
/**********************************************************************
SUMMARY
- **********************************************************************/
+ **********************************************************************/
showSummary(data) {
let summary = this.templates.create('formSummary', data);
data.config.element.after(summary);
@@ -1560,7 +1621,7 @@
}
/**********************************************************************
UTILITY
- **********************************************************************/
+ **********************************************************************/
getForm(element) {
let form = element.closest('[data-form-id]');
if (!form) return false;
@@ -1581,6 +1642,7 @@
getFieldValue(element) {
let type = this.getFieldType(element);
let conf = this.getItem(element);
+
let fieldName = conf.field?.dataset.field??false;
if (!fieldName) return false;
@@ -1592,8 +1654,8 @@
return this.getTagListValue(element, conf);
case 'group':
- //Do we actually need anything here? I think each subfield just
- break;
+ return null;
+ //Do we actually need anything here? I think each subfield just
case 'location':
return this.getLocationValue(element, conf);
@@ -1659,25 +1721,25 @@
if (Array.isArray(value) && value.length === 0) return true;
return typeof value === 'object' && Object.keys(value).length === 0;
}
- getRepeaterValue(element, conf) {
- const items = element.querySelector('.repeater-items');
- if (!items) return [];
- let ignore = ['image_data','image-title','image-caption','image-description','image-alt-text']
- let value = [];
- Array.from(items.children).forEach(row => {
- let rowData = {};
- row.querySelectorAll('[data-field]').forEach(field => {
- if (!ignore.includes(field.dataset.field)) {
- const input = this.getFieldInput(field);
- if (input) {
- rowData[field.dataset.field] = this.getFieldValue(input);
- }
+ getRepeaterValue(element, conf) {
+ const items = element.querySelector('.repeater-items');
+ if (!items) return [];
+ let ignore = ['image_data','image-title','image-caption','image-description','image-alt-text']
+ let value = [];
+ Array.from(items.children).forEach(row => {
+ let rowData = {};
+ row.querySelectorAll('[data-field]').forEach(field => {
+ if (!ignore.includes(field.dataset.field)) {
+ const input = this.getFieldInput(field);
+ if (input) {
+ rowData[field.dataset.field] = this.getFieldValue(input);
}
- });
- value.push(rowData);
+ }
});
- return value;
- }
+ value.push(rowData);
+ });
+ return value;
+ }
getFieldInput(field) {
// For quill fields, target the specific editor textarea
const quillTextarea = field.querySelector('textarea[data-editor]');
@@ -1685,40 +1747,46 @@
return field.querySelector(this.inputSelectors);
}
- getTagListValue(element, conf) {
- if (!conf.container) {
- conf.container = conf.field?.querySelector('.tag-items');
- this.saveItem(conf);
- }
- let value = [];
- Array.from(conf.container.children).forEach(item => {
- let inputs = item.querySelectorAll('input[type="hidden"]');
- let fieldData = {};
- inputs.forEach(input => {
- fieldData[input.dataset.field] = input.value;
- });
- value.push(fieldData);
- });
- return value;
- }
- getLocationValue(element, conf) {
- if(!conf.values){
- conf.values = Array.from(conf.field?.querySelectorAll('[data-location-field]'));
- this.saveItem(conf);
- }
- let value = {};
- conf.values.forEach(input => {
- value[input.dataset.locationField] = input.value;
- });
- return value;
- }
- getHiddenInputValue(element, conf, fieldName) {
- if (!conf.value) {
- conf.value = conf.field?.querySelector(`input[type=hidden][name="${fieldName}"]`)
- || conf.field?.querySelector(`input[type=hidden]`);
+ getTagListValue(element, conf) {
+ if (!conf.container) {
+ conf.container = conf.field?.querySelector('.tag-items');
this.saveItem(conf);
}
- return conf.value?.value ?? '';
+ let value = [];
+ Array.from(conf.container.children).forEach(item => {
+ let inputs = item.querySelectorAll('input[type="hidden"]');
+ let fieldData = {};
+ inputs.forEach(input => {
+ fieldData[input.dataset.field] = input.value;
+ });
+ value.push(fieldData);
+ });
+ return value;
+ }
+ getLocationValue(element, conf) {
+ if(!conf.values){
+ conf.values = Array.from(conf.field?.querySelectorAll('[data-location-field]'));
+ this.saveItem(conf);
+ }
+ let value = {};
+ conf.values.forEach(input => {
+ value[input.dataset.locationField] = input.value;
+ });
+ return value;
+ }
+ getHiddenInputValue(element, conf, fieldName) {
+ if (element.tagName !== 'INPUT' || element.type !== 'hidden'){
+ element = element.querySelector('input[type="hidden"][name="'+fieldName+'"]');
+ if (!element) {
+ return null;
+ }
+ }
+
+ if (conf.value === undefined || conf.value !== element.value) {
+ conf.value = element.value;
+ this.saveItem(conf);
+ }
+ return conf.value;
}
/**
@@ -1761,7 +1829,7 @@
case 'upload':
case 'image': //legacy, shouldn't be needed
case 'gallery': //legacy, shouldn't be needed
- // These might need special handling depending on your needs
+ // These might need special handling depending on your needs
return this.formatHiddenFieldForSummary(value, input, fieldType);
default:
@@ -1981,7 +2049,7 @@
}
/**********************************************************************
Subscription
- **********************************************************************/
+ **********************************************************************/
subscribe(callback) {
this.subscribers.add(callback);
return () => this.subscribers.delete(callback);
@@ -1998,7 +2066,7 @@
}
/**********************************************************************
Cleanup
- **********************************************************************/
+ **********************************************************************/
destroy() {
if (this.forms.size > 0) {
Array.from(this.forms.values()).forEach(form => {
--
Gitblit v1.10.0