From 3d25ad7de4fdbb3eba2cde2a62422e6be5d79a0a Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 25 Aug 2026 22:57:22 +0000
Subject: [PATCH] =It's been a minute. I went through the Routes and associated Managers and ensured they played together well. Reworked the Cache.php, and I think I caught all the references. Moving away from JVBase() root class, to individuall classes that are Instanced. Double checked for the ABSPATH check in every file, and added index.php to every directory that didn't already have it. Also still working on the MenuBlock to work with WooCommerce for now for Northeh
---
assets/js/concise/FormController.js | 395 ++++++++++++++++++++++++++++++++++++++++++++++++--------
1 files changed, 336 insertions(+), 59 deletions(-)
diff --git a/assets/js/concise/FormController.js b/assets/js/concise/FormController.js
index 19e4a81..e5f3868 100644
--- a/assets/js/concise/FormController.js
+++ b/assets/js/concise/FormController.js
@@ -51,7 +51,7 @@
this.selectors = {
tabs: {
nav: 'nav.tabs',
- sections: '.tab.content', //querySelectorAll
+ sections: '.tab-content', //querySelectorAll
progress: {
progress: '.progress',
fill: '.progress .fill',
@@ -84,6 +84,8 @@
},
repeater: {
repeater: '.repeater', //querySelectorAll
+ row: '.repeater-row',
+ handle: '.drag-handle',
header: '.repeater-row-header',
remove: '.remove-row',
add: '.add-repeater-row',
@@ -210,7 +212,10 @@
let field = key.replace('_tempUpload', '');
if (Object.hasOwn(form.ui.uploads, field)) {
- uploads.push(value);
+ uploads = [
+ ... uploads,
+ ... value
+ ];
}
}
}
@@ -353,6 +358,28 @@
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;
}
}
}
@@ -368,11 +395,12 @@
// 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);
});
}
const collectionName = collectionField.dataset.field;
+ this.maybeUpdateCollectionDisplay(field);
window.debouncer.schedule(
`collection:${collectionName}`,
() => this.updateCollectionField(collectionField),
@@ -384,7 +412,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);
});
}
@@ -454,9 +482,14 @@
} else {
this.notify('form-submit', {
config: form,
- data: this.changes.get(form.id)?.changes??{},
+ data: this.changes.get(form.id)?.changes ?? {},
});
}
+ }
+ if (form.options.endpoint && !form.options.handled) {
+ e.preventDefault();
+ console.log('Submitting form...', this.changes.get(form.id)?.changes ?? {});
+ await this.handleServerSave(form, this.changes.get(form.id)?.changes ?? {});
}
@@ -483,12 +516,88 @@
});
}
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;
+ }
+
+ if (form.tabs) {
+ this.updateStepProgress(form);
+ }
+
this.changes.set(form.id, changes);
if (form.options.cache) {
this.scheduleBackup();
}
+
+ 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(
@@ -543,6 +652,89 @@
this.changes.delete(formId);
}
+ scheduleServerSave(form, changes){
+
+ window.debouncer.schedule(
+ `form_${form.id}_server_save`,
+ async () => {
+ await this.handleServerSave(form, changes);
+ },
+ 1500
+ );
+ }
+ cancelServerSave(formId) {
+ window.debouncer.cancel(`form_${formId}_server_save`);
+ }
+ async handleServerSave(form, changes) {
+ this.cancelServerSave(form.id);
+
+ if (form.useQueue && window.jvbQueue) {
+ // changes.user = window.auth.getUser();
+ window.jvbQueue.addToQueue({
+ endpoint: form.options.endpoint,
+ data: changes,
+ headers: form.options.headers??{},
+ });
+ } else {
+ try {
+ let response = await window.auth.fetch(jvbSettings.api+form.options.endpoint, {
+ body: changes,
+ headers: form.options.headers??{},
+ method: 'POST',
+ });
+
+ if (!response.ok) {
+ let errorDetails;
+ try {
+ // Attempt to parse the server's error payload as JSON
+ errorDetails = await response.json();
+ } catch {
+ // Fallback if the error response isn't JSON (e.g., HTML error page)
+ errorDetails = { message: response.statusText || 'Unknown HTTP error' };
+ }
+ this.notify('form-error', {
+ config: form,
+ data: errorDetails
+ });
+ return;
+ }
+ let result = await response.json();
+ this.notify('form-success', {
+ config: form,
+ result: result
+ });
+ } catch (error) {
+ if (error.status) {
+ this.notify('form-error', {
+ config: form,
+ data: error
+ });
+ // This is an HTTP error thrown manually (e.g., 404, 500)
+ } else if (error instanceof SyntaxError) {
+ this.notify('form-error', {
+ config: form,
+ data: {
+ status: 'error',
+ success: false,
+ message: 'Something went wrong'
+ }
+ });
+ // This happens if response.json() fails on a successful 200 OK response
+ console.error('Data Error: Server returned invalid JSON format.');
+ } else {
+ this.notify('form-error', {
+ config: form,
+ data: {
+ status: 'network_error',
+ message: 'Network error: could not connect to the server'
+ }
+ });
+ }
+ }
+
+ }
+ }
+
/**
* Register a form for handling
@@ -550,12 +742,29 @@
* @param {object} options
*/
registerForm(form, options) {
+ options = {
+ autoUpload: false,
+ imageMeta: true,
+ delay: 1500,
+ useQueue: true,
+ endpoint: Object.hasOwn(form.dataset, 'save') ? form.dataset.save: '',
+ showStatus: true,
+ showSummary: false,
+ handled: Object.hasOwn(form.dataset, 'handled'),
+ cache: true,
+ ignore: [],
+ ... options
+ };
+
//Bail if form already registered
if (Object.hasOwn(form.dataset, 'formId') && this.forms.has(form.dataset.formId)) return;
if (!Object.hasOwn(form.dataset, 'formId')) {
form.dataset.formId = window.generateID('form_');
}
+ if (Object.hasOwn(form.dataset, 'action')) {
+ options.headers = window.auth.getHeader(form.dataset.action);
+ }
const formId = form.dataset.formId;
this.addFormListeners(form);
@@ -564,22 +773,17 @@
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) {
@@ -603,10 +807,10 @@
}
// Clean up dependencies for this form
this.dependencies.forEach((dependency, fieldName) => {
- dependency.items = dependency.items.filter(item => item.form !== formId);
+ dependency = dependency.filter(item => item.form !== formId);
// Remove the dependency entry entirely if no items left
- if (dependency.items.length === 0) {
+ if (dependency.length === 0) {
this.dependencies.delete(fieldName);
}
});
@@ -696,7 +900,7 @@
p: 'p',
},
setup({ el, refs, manyRefs, data }) {
- const skipFields = ['sendAll', ...data.config.options.ignore??[]];
+ const skipFields = ['sendAll', ...data?.config?.options?.ignore??[]];
for (let [key, value] of Object.entries(data.changes)) {
if (skipFields.includes(key) || form.isEmptyValue(value)) continue;
@@ -750,8 +954,6 @@
}
refs.result?.remove();
- data.config.element.after(el);
- window.fade(data.config.element, false);
}
}
);
@@ -856,9 +1058,7 @@
}
}
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'),
@@ -891,11 +1091,15 @@
);
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);
}
});
}
@@ -934,12 +1138,17 @@
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) {
@@ -1133,38 +1342,41 @@
const requiredValue = field.dataset.dependsValue;
const operator = field.dataset.dependsOperatior??'==';
+ let formData = this.forms.get(form.dataset.formId);
+
if (!this.dependencies.has(dependsOn)) {
- let element = document.querySelector(`[field="${dependsOn}"]`);
- if (element) {
- this.dependencies.set(dependsOn, {
- element: element,
- items: []
- });
+ if (Object.hasOwn(formData.ui.fields, dependsOn)) {
+ this.dependencies.set(dependsOn, []);
}
}
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);
+ 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.getFieldCheckedValue(controlField.element);
+
+ const controlValue = this.getFieldValue(form.ui.fields[controlFieldName]);
const shouldShow = this.evaluateCondition(
controlValue,
- dependentField.requiredValue,
- dependentField.operator
+ dependentField.dataset.dependsValue,
+ dependentField.dataset.dependsOperatior
);
- this.toggleFieldVisibility(dependentField.field, shouldShow);
+ this.toggleFieldVisibility(dependentField, shouldShow);
}
evaluateCondition(value, requiredValue, operator) {
const fieldStr = String(value || '');
@@ -1259,16 +1471,17 @@
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));
@@ -1277,13 +1490,14 @@
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}`
);
}
}
@@ -1291,8 +1505,24 @@
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);
@@ -1302,7 +1532,7 @@
.filter(item =>
item &&
item.form === formId &&
- item.section === section.dataset.tab &&
+ item.section === currentSection.dataset.tab &&
!item.element.closest('[hidden]')
);
@@ -1316,11 +1546,21 @@
* @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');
@@ -1329,14 +1569,17 @@
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
);
});
@@ -1364,6 +1607,27 @@
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
**********************************************************************/
@@ -1400,6 +1664,12 @@
field.classList.remove('has-success');
field.classList.add('has-error');
+ field.scrollIntoView({
+ behavior: "smooth",
+ block: "center",
+ inline: "nearest"
+ });
+ field.querySelector(this.inputSelectors)?.focus();
if (item.ui.message) {
item.ui.message.hidden = false;
@@ -1706,6 +1976,10 @@
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);
@@ -2086,6 +2360,9 @@
window.auth.subscribe(event => {
if (event === 'auth-loaded') {
window.jvbForm = new FormController();
+ document.querySelectorAll('form[data-auto]').forEach(form => {
+ window.jvbForm.registerForm(form, {});
+ });
}
});
});
--
Gitblit v1.10.0