From e9967fa22781d922ba4eb8fb44fe72d200ac4b14 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Mon, 10 Nov 2025 21:04:10 +0000
Subject: [PATCH] =IconsManager.php update
---
assets/js/concise/FormController.js | 278 +++++++++++++++++++++++++++++++++++++++++++++++--------
1 files changed, 237 insertions(+), 41 deletions(-)
diff --git a/assets/js/concise/FormController.js b/assets/js/concise/FormController.js
index cdae988..73a2756 100644
--- a/assets/js/concise/FormController.js
+++ b/assets/js/concise/FormController.js
@@ -188,6 +188,7 @@
* Register a standalone form (for front-end forms)
*/
registerForm(formElement, options = {}) {
+ if (!formElement) return;
const formId = formElement.dataset.formId || `form_${Date.now()}`;
formElement.dataset.formId = formId;
@@ -196,16 +197,17 @@
const formConfig = {
element: formElement,
id: formId,
+ status: '',
options: {
- autoSave: true,
+ autosave: 'autosave' in formElement.dataset,
saveDelay: this.autoSaveDefaults.delay,
- endpoint: formElement.dataset.save,
+ endpoint: formElement.dataset.save??'',
+ formStatus: true,
cache: true,
...options
},
dependencies: new Map(),
data: this.collectFormData(formElement),
- isDirty: false
};
// Initialize special fields
@@ -255,7 +257,7 @@
// Scan for existing selector fields
if (window.jvbSelector) {
- window.jvbSelector.scanExistingFields();
+ window.jvbSelector.scanExistingFields(form);
}
}
@@ -444,8 +446,7 @@
container.appendChild(row);
- // Schedule save if auto-save enabled
- if (formConfig && formConfig.options.autoSave) {
+ if (formConfig) {
this.scheduleSave(formConfig, {
type: 'repeater',
action: 'add',
@@ -472,7 +473,7 @@
this.updateRepeaterOrder(repeater, formConfig);
// Schedule save
- if (formConfig && formConfig.options.autoSave) {
+ if (formConfig) {
this.scheduleSave(formConfig, {
type: 'repeater',
action: 'remove',
@@ -515,7 +516,7 @@
});
// Schedule save
- if (formConfig && formConfig.options.autoSave) {
+ if (formConfig) {
this.scheduleSave(formConfig, {
type: 'repeater',
action: 'reorder',
@@ -650,16 +651,15 @@
/* ========== Event Handlers ========== */
- handleSubmit(event) {
- //TODO: submit data, if successful, delete from store
- if (this.subscribers.size > 0 ){
- const form = event.target;
- if (!form.dataset.formId) return;
+ async handleSubmit(event) {
+ const form = event.target;
+ if (!form.dataset.formId) return;
+
+ const formConfig = this.forms.get(form.dataset.formId);
+
+ // Handle subscriber-based forms
+ if (this.subscribers.size > 0) {
event.preventDefault();
-
- const formConfig = this.forms.get(form.dataset.formId);
- if (!formConfig) return;
-
const formData = this.collectFormData(form);
this.notify('form-submit', {
formId: formConfig.id,
@@ -669,6 +669,133 @@
}
}
+ handleFormSuccess(form, data) {
+ // Clear previous errors
+ form.querySelectorAll('.error-message').forEach(el => el.remove());
+ form.querySelectorAll('.field-error').forEach(el =>
+ el.classList.remove('field-error')
+ );
+
+ // Add success class to form
+ form.classList.add('form-success');
+
+ // Show success message if provided
+ if (data.message) {
+ const success = document.createElement('div');
+ success.className = 'form-success-message success-message';
+ success.textContent = data.message;
+ form.insertBefore(success, form.firstChild);
+
+ // Optionally add icon
+ const icon = window.getIcon?.('check-circle');
+ if (icon) {
+ icon.classList.add('success-icon');
+ success.prepend(icon);
+ }
+ }
+
+ // If there's a title/description (for registration success)
+ if (data.title || data.description) {
+ const successBox = document.createElement('div');
+ successBox.className = 'success-box';
+
+ if (data.title) {
+ const title = document.createElement('h3');
+ title.textContent = data.title;
+ successBox.appendChild(title);
+ }
+
+ if (data.description) {
+ // Handle both string and array descriptions
+ const descriptions = Array.isArray(data.description)
+ ? data.description
+ : [data.description];
+
+ descriptions.forEach(desc => {
+ const p = document.createElement('p');
+ p.textContent = desc;
+ successBox.appendChild(p);
+ });
+ }
+
+ form.insertBefore(successBox, form.firstChild);
+ }
+
+ // Announce success for accessibility
+ if (window.jvbA11y) {
+ window.jvbA11y.announce(data.message || 'Form submitted successfully');
+ }
+
+ // Trigger custom event
+ form.dispatchEvent(new CustomEvent('jvb-form-success', {
+ detail: data
+ }));
+ }
+
+ handleFormError(form, data) {
+ // Clear all previous errors
+ form.querySelectorAll('.error-message').forEach(el => el.remove());
+ form.querySelectorAll('.field-error, .has-error').forEach(el => {
+ el.classList.remove('field-error', 'has-error');
+ });
+
+ // Clear validation states using existing method
+ form.querySelectorAll('.field').forEach(fieldWrapper => {
+ this.clearValidation(fieldWrapper);
+ });
+
+ // Handle field-specific errors
+ 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');
+ error.prepend(icon);
+ }
+
+ 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}`
+ : `Form error: ${data.message}`;
+ window.jvbA11y.announce(announcement);
+ }
+
+ // Trigger custom event
+ form.dispatchEvent(new CustomEvent('jvb-form-error', {
+ detail: data
+ }));
+ }
+
handleClick(e) {
if (window.targetCheck(e, 'div.quantity')) {
let container = window.targetCheck(e, 'div.quantity');
@@ -746,15 +873,16 @@
}
handleChange(event) {
- if (this.subscribers.size > 0) {
- const target = event.target;
- const form = target.form || target.closest('form');
+ if (event.target.closest('[data-ignore]')) {
+ return;
+ }
+ const target = event.target;
+ const form = target.form || target.closest('form');if (!form) return;
- if (!form) return;
-
- const formConfig = this.forms?.get(form.dataset.formId);
- if (!formConfig) return;
-
+ const formConfig = this.forms?.get(form.dataset.formId);
+ if (!formConfig) return;
+ console.log(formConfig.options);
+ if (formConfig.options.autosave || this.subscribers.size > 0) {
// Check conditional fields
const dependencies = formConfig.dependencies.get(target.name);
if (dependencies) {
@@ -764,10 +892,8 @@
}
// Schedule auto-save if enabled
- if (formConfig.options.autoSave && !form.dataset.noautosave) {
- const delay = this.getDelayForField(target);
- this.scheduleSave(formConfig, delay);
- }
+ const delay = this.getDelayForField(target);
+ this.scheduleSave(formConfig, delay);
}
}
@@ -780,6 +906,9 @@
}
handleBlur(e) {
+ if (e.target.closest('[data-ignore]')) {
+ return;
+ }
const target = e.target;
const form = target.form || target.closest('form');
@@ -801,7 +930,7 @@
this.validateField(input, fieldWrapper);
}
const formConfig = this.forms?.get(form.dataset.formId);
- if (formConfig && formConfig.options.autoSave && !form.dataset.noautosave) {
+ if (formConfig) {
// Shorter delay on blur
this.scheduleSave(formConfig, {
type: 'blur',
@@ -813,6 +942,9 @@
}
handleInput(e) {
+ if (e.target.closest('[data-ignore]') || ! e.target.closest('form')) {
+ return;
+ }
const input = e.target.closest('input, textarea, select');
if (!input) return;
@@ -974,6 +1106,7 @@
// All validations passed
this.showSuccess(fieldWrapper);
+ this.notify('field-validated', input);
return true;
}
@@ -998,7 +1131,7 @@
/**
* Show success state (green checkmark)
*/
- showSuccess(fieldWrapper) {
+ showSuccess(fieldWrapper, textMessage = '') {
if (!fieldWrapper) return;
// Find validation elements (they might be in field-input-wrapper or field-content)
@@ -1024,8 +1157,13 @@
// Hide error message
if (message) {
- message.hidden = true;
- message.textContent = '';
+ if (textMessage === '') {
+ message.hidden = true;
+ message.textContent = '';
+ } else {
+ message.hidden = false;
+ message.textContent = textMessage;
+ }
}
}
@@ -1244,6 +1382,9 @@
return this.autoSaveDefaults.delay;
}
scheduleSave(formConfig, delay = this.autoSaveDefaults.delay) {
+ if (!formConfig.options.autosave) {
+ return;
+ }
document.addEventListener('input', this.saveCheck, {passive: true});
const saveKey = `autosave_${formConfig.id}`;
@@ -1279,7 +1420,9 @@
// Get only changed fields
const changes = this.getChangedFields(formConfig.data, formData);
+ console.log('Changes:', changes);
if (Object.keys(changes).length === 0) return;
+ console.log('Continuing on:');
// Update stored data
formConfig.data = formData;
@@ -1313,14 +1456,23 @@
// Check if current data differs from snapshot
const currentData = this.collectFormData(formConfig.element);
- const changes = this.getChangedFields(formConfig.lastSnapshot, currentData);
+ const changes = this.getChangedFields(formConfig.data, currentData);
return Object.keys(changes).length > 0;
}
- showFormStatus(formID, status) {
+ showFormStatus(formID, status, message='') {
// Remove existing status
let form = this.forms.get(formID);
+ if (!form.options.formStatus) {
+ return;
+ }
+
+ if (form.status === status){
+ return;
+ }
+
+ form.status = status;
console.log('Setting status: ', status);
@@ -1341,9 +1493,9 @@
'offline': 'Changes will be saved when online'
};
const icons = {
- 'autosaved': 'check',
- 'submitted': 'check',
- 'error': 'close',
+ 'autosaved': 'check-circle',
+ 'submitted': 'check-circle',
+ 'error': 'close-circle',
'offline': 'cloud-slash',
'pending': 'exclamation-mark'
}
@@ -1352,9 +1504,10 @@
if (icon) {
statusWrap.prepend(icon);
}
- console.log(status, messages[status]);
- console.log(status, icons[status]);
- statusElement.textContent = messages[status] || status;
+ if (message === '') {
+ message = messages[status] || status;
+ }
+ statusElement.textContent = message;
statusWrap.classList.toggle('loading', ['uploading', 'saving'].includes(status));
// Auto-hide success messages
@@ -1382,6 +1535,9 @@
/* ========== Form Data Methods ========== */
collectFormData(form) {
+ if (Object.hasOwn(form.dataset, 'timeline')) {
+ return this.collectTimeline(form);
+ }
const formData = new FormData(form);
let data = {};
const repeaterData = {};
@@ -1400,6 +1556,46 @@
return this.mergeRepeaterData(data, repeaterData);
}
+ collectTimeline(form) {
+ console.log('Collecting Timeline data:');
+ let data = {};
+ let posts = {}; // Temporary object keyed by post ID
+ let postOrder = []; // Track order as encountered (preserves DOM/drag order)
+ let formData = new FormData(form);
+
+ for (const [key, value] of formData.entries()) {
+ if (this.ignore.includes(key) || key.endsWith('_temp')) {
+ continue;
+ }
+ const match = key.match(/^\[(\d+)\](.+)$/);
+ if (match) {
+ // Timeline-specific field: [postId]fieldName
+ const [, postId, fieldName] = match;
+ if (!posts[postId]) {
+ posts[postId] = { id: parseInt(postId) };
+ postOrder.push(postId); // Track first occurrence
+ }
+ const processor = this.getFieldProcessor(fieldName);
+ processor(fieldName, value, posts[postId], {}, {}, form);
+ } else {
+ // Shared field (post_title, taxonomies, etc.)
+ const processor = this.getFieldProcessor(key);
+ processor(key, value, data, {}, {}, form);
+ }
+ }
+
+ // Convert to array in DOM order (matches menu_order)
+ data.timeline = postOrder.map(id => posts[id]);
+
+ delete data['form-id'];
+ delete data['sendAll'];
+ delete data['timeline_temp'];
+ delete data['']; // Empty key
+
+ console.log('Data: ', data);
+ return data;
+ }
+
getFieldProcessor(key) {
if (key.includes('|')) return this.processTableField;
if (key.includes('::')) return this.processGroupField;
--
Gitblit v1.10.0