From df6c00db050e188a6bd5707e72c4f1f331ced923 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 08 Feb 2026 20:46:43 +0000
Subject: [PATCH] =Port over to jakevan 2
---
assets/js/concise/FormController.js | 217 +++++++++++++++++++++++++++++------------------------
1 files changed, 118 insertions(+), 99 deletions(-)
diff --git a/assets/js/concise/FormController.js b/assets/js/concise/FormController.js
index 1286355..8996cbf 100644
--- a/assets/js/concise/FormController.js
+++ b/assets/js/concise/FormController.js
@@ -59,7 +59,7 @@
field: '.field', //querySelectorAll
label: 'label',
success: '.success',
- error: '.success',
+ error: '.error',
message: '.validation-message',
},
repeater: {
@@ -78,6 +78,7 @@
remove: '.remove-tag',
label: '.tag-label',
items: '.tag-items',
+ item: '.tag-item',
inputs: this.inputSelectors, //querySelectorAll
value: 'input[type="hidden"]' //querySelectorAll
},
@@ -334,7 +335,7 @@
});
}
- if (Object.hasOwn(field.dataset, 'repeater-id') || Object.hasOwn(field.dataset,'tag-list-id')) {
+ if (field.dataset.fieldType === 'repeater' || field.dataset.fieldType === 'tag-list') {
this.updateCollectionField(field);
return;
}
@@ -763,14 +764,14 @@
let conf = this.quantityFields.get(e.target.closest('[data-num-id]')?.dataset.numId);
if(!conf) return;
let change = 0;
- if (conf.increase.contains(e.target)) {
+ if (conf.ui.increase.contains(e.target)) {
change++;
- } else if (conf.decrease.contains(e.target)) {
+ } else if (conf.ui.decrease.contains(e.target)) {
change--;
}
if (change === 0) return;
let field = this.getField(e.target);
- let step = conf.input.step;
+ let step = conf.ui.input.step;
step = Math.max(step, 1);
if (e.ctrlKey && e.shiftKey) {
step = step * 50;
@@ -779,20 +780,20 @@
} else if (e.shiftKey) {
step = step * 10;
}
- let value = (conf.input.value === '') ? 0 : parseFloat(conf.input.value);
- conf.input.value = (value + (step * change));
+ let value = (conf.ui.input.value === '') ? 0 : parseFloat(conf.ui.input.value);
+ conf.ui.input.value = (value + (step * change));
- value = parseFloat(conf.input.value);
+ value = parseFloat(conf.ui.input.value);
- if (conf.input.min && value < conf.input.min) {
- conf.input.value = conf.input.min;
- conf.decrease.disabled = true;
- } else if (conf.input.max && value > conf.input.max) {
- conf.input.value = conf.input.max;
- conf.increase.disabled = true;
+ 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.decrease.disabled) conf.decrease.disabled = false;
- if (conf.increase.disabled) conf.increase.disabled = false;
+ if (conf.ui.decrease.disabled) conf.ui.decrease.disabled = false;
+ if (conf.ui.increase.disabled) conf.ui.increase.disabled = false;
}
}
checkForRepeaters(form) {
@@ -824,7 +825,7 @@
manyRefs.inputs?.forEach(input => {
- window.prefixInput(input, `${data.repeater.dataset.fieldName}:${index}:`, el);
+ window.prefixInput(input, `${data.repeater.dataset.field}:${index}:`, el);
});
}
},
@@ -853,10 +854,8 @@
}
handleRepeaterClick(e) {
if (e.target.matches(this.selectors.repeater.add)) {
- console.log('Add Repeater Row');
this.addRepeaterRow(e.target.closest('[data-repeater-id]'));
} else if (e.target.matches(this.selectors.repeater.remove)) {
- console.log('Remove Repeater Row');
this.removeRepeaterRow(e.target.closest('[data-index]'));
}
}
@@ -882,10 +881,10 @@
form: form.dataset.formId,
format: field.dataset.tagFormat??'first_field'
};
- console.log('Registering Tag List with config', config);
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(
@@ -902,7 +901,7 @@
el.dataset.index = index;
manyRefs.inputs?.forEach(input => {
let wrapper = input.closest('.tag-item');
- window.prefixInput(input, `${el.dataset.fieldName}:${index}:`, wrapper)
+ window.prefixInput(input, `${data.fieldName}:${index}:`, wrapper)
});
if (refs.label) {
@@ -914,7 +913,6 @@
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);
- console.log('Adding tag list listeners to ', field);
this.addTagListListeners(field);
});
@@ -931,82 +929,114 @@
handleTagListClick(e) {
if (window.targetCheck(e,this.selectors.tagList.add)) {
this.addTagListItem(e.target.closest('[data-tag-list-id]'));
- } else if (e.target.matches(this.selectors.tagList.remove)) {
- this.removeTagListItem(e.target.closest(this.selectors.tagList.remove));
+ } 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;
+ addTagListItem(tagList) {
+ let config = this.tagLists.get(tagList.dataset.tagListId);
+ if (!config) return;
- let data = {};
- let hasValue = false;
- for (let input of config.ui.inputs) {
- this.validateField(input);
- const fieldName = input.name.replace('new_','');
- const value = this.getFieldValue(input);
- if (value) hasValue = true;
- data[fieldName] = value;
+ let data = {};
+ let hasValue = false;
+ let isValid = true;
- //clear values and validation
- if (['checkbox', 'radio'].includes(input.type)) {
- input.checked = false;
- } else {
- input.value = '';
+ // 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);
}
- this.clearValidation(input);
+ } else {
+ label = data[config.format]??Object.values(data)[0];
}
+ break;
+ }
- if (!hasValue) {
- this.a11y.announce('Please fill in at least one field');
- config.ui.inputs[0].focus();
- return;
- }
+ let newItem = this.templates.create(tagList.dataset.tagListId, {
+ label: label,
+ fieldName: config.fieldName
+ });
- 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('{')) {
- let 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;
- }
+ 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] || '';
+ });
- let newItem = this.templates.create(tagList.dataset.tagListId, {
- label: label
- });
+ config.ui.items.append(newItem);
- const index = config.ui.items?.children?.length ?? 0;
- newItem?.querySelectorAll('input[type=hidden]')?.forEach(input => {
- const fieldKey = input.dataset.field;
- input.name = `${config.element.field}:${index}:${fieldKey}`;
- input.value = data[fieldKey] || '';
- });
-
- config.ui.items.append(newItem);
- config.ui.inputs[0]?.focus();
-
- this.updateCollectionField(tagList);
-
- this.a11y.announce('Item added');
+ // Clear inputs AFTER success
+ for (let input of config.ui.inputs) {
+ if (['checkbox', 'radio'].includes(input.type)) {
+ input.checked = false;
+ } else {
+ input.value = '';
}
- removeTagListItem(tag) {
- let tagList = tag.closest('[data-tag-list-id]');
- tag.remove();
- this.reindexList(tagList);
- this.a11y.announce('Item removed');
- }
+ 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]');
@@ -1397,28 +1427,20 @@
if (data.field) {
const fieldWrapper = form.querySelector(`[data-field="${data.field}"]`);
if (fieldWrapper) {
- // Use existing showError method for consistency
this.showError(fieldWrapper, data.message);
- // Mark as touched so validation persists
- this.touchedFields.add(data.field);
-
- // Scroll to error
fieldWrapper.scrollIntoView({ behavior: 'smooth', block: 'center' });
- // Focus the input for better UX
const input = fieldWrapper.querySelector('input, textarea, select');
if (input) {
input.focus();
}
}
} else {
- // General form error (not field-specific)
const error = document.createElement('div');
error.className = 'form-error error-message';
error.textContent = data.message;
- // Add icon for consistency
const icon = window.getIcon?.('close-circle');
if (icon) {
icon.classList.add('error-icon');
@@ -1426,12 +1448,9 @@
}
form.insertBefore(error, form.firstChild);
-
- // Scroll to top to show the error
form.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
- // Announce error for accessibility
if (window.jvbA11y) {
const announcement = data.field
? `Error in ${data.field}: ${data.message}`
@@ -1439,7 +1458,6 @@
window.jvbA11y.announce(announcement);
}
- // Trigger custom event
form.dispatchEvent(new CustomEvent('jvb-form-error', {
detail: data
}));
@@ -1502,6 +1520,7 @@
**********************************************************************/
getForm(element) {
let form = element.closest('[data-form-id]');
+ if (!form) return false;
let id = form.dataset.formId;
if (!id) return false;
let config = this.forms.get(id);
--
Gitblit v1.10.0