@formique/semantq 1.0.6 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/formique-semantq.js +398 -217
- package/package.json +1 -1
package/formique-semantq.js
CHANGED
|
@@ -60,6 +60,23 @@ class Formique extends FormBuilder {
|
|
|
60
60
|
asteriskHtml: '<span aria-hidden="true" style="color: red;">*</span>',
|
|
61
61
|
...formSettings
|
|
62
62
|
};
|
|
63
|
+
|
|
64
|
+
this.themeColor = formSettings.themeColor || null;
|
|
65
|
+
|
|
66
|
+
this.themeColorMap = {
|
|
67
|
+
'primary': {
|
|
68
|
+
'--formique-base-bg': '#ffffff',
|
|
69
|
+
'--formique-base-text': '#333333',
|
|
70
|
+
'--formique-base-shadow': '0 10px 30px rgba(0, 0, 0, 0.1)',
|
|
71
|
+
'--formique-base-label': '#555555',
|
|
72
|
+
'--formique-input-border': '#dddddd',
|
|
73
|
+
'--formique-focus-color': null, // Will be set to themeColor
|
|
74
|
+
'--formique-btn-bg': null, // Will be set to themeColor
|
|
75
|
+
'--formique-btn-text': '#ffffff',
|
|
76
|
+
'--formique-btn-shadow': null // Will be calculated from themeColor
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
63
80
|
this.divClass = 'input-block';
|
|
64
81
|
this.inputClass = 'form-input';
|
|
65
82
|
this.radioGroupClass = 'radio-group';
|
|
@@ -82,7 +99,7 @@ class Formique extends FormBuilder {
|
|
|
82
99
|
];
|
|
83
100
|
this.formiqueEndpoint = "https://formiqueapi.onrender.com/api/send-email";
|
|
84
101
|
|
|
85
|
-
// DISABLE DOM LISTENER
|
|
102
|
+
// DISABLE DOM LISTENER
|
|
86
103
|
//document.addEventListener('DOMContentLoaded', () => {
|
|
87
104
|
// 1. Build the form's HTML in memory
|
|
88
105
|
this.formMarkUp += this.renderFormElement(); // Adds opening <form> tag and any hidden inputs
|
|
@@ -137,7 +154,7 @@ class Formique extends FormBuilder {
|
|
|
137
154
|
const formElement = document.getElementById(`${this.formId}`);
|
|
138
155
|
if (formElement) { // Add a check here just in case, although it should now exist
|
|
139
156
|
formElement.addEventListener('submit', function(event) {
|
|
140
|
-
if (this.formSettings.submitMode === 'email') {
|
|
157
|
+
if (this.formSettings.submitMode === 'email' || this.formSettings.submitMode === 'rsvp') {
|
|
141
158
|
event.preventDefault();
|
|
142
159
|
document.getElementById("formiqueSpinner").style.display = "block";
|
|
143
160
|
this.handleEmailSubmission(this.formId);
|
|
@@ -156,17 +173,20 @@ class Formique extends FormBuilder {
|
|
|
156
173
|
// Initialize dependency graph and observers after the form is rendered
|
|
157
174
|
this.initDependencyGraph();
|
|
158
175
|
this.registerObservers();
|
|
176
|
+
this.attachDynamicSelectListeners();
|
|
159
177
|
|
|
160
178
|
// Apply theme
|
|
161
|
-
if (this.
|
|
179
|
+
if (this.themeColor) {
|
|
180
|
+
this.applyCustomTheme(this.themeColor, this.formContainerId); // <--- NEW: Apply custom theme
|
|
181
|
+
} else if (this.formSettings.theme && this.themes.includes(this.formSettings.theme)) {
|
|
162
182
|
let theme = this.formSettings.theme;
|
|
163
183
|
this.applyTheme(theme, this.formContainerId);
|
|
164
184
|
} else {
|
|
165
|
-
|
|
185
|
+
// Fallback if no themeColor and no valid theme specified
|
|
186
|
+
this.applyTheme('dark', this.formContainerId); // Default to 'dark'
|
|
166
187
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
//DISABLE DOM LISTNER
|
|
188
|
+
|
|
189
|
+
// DISABLE DOM LISTENER
|
|
170
190
|
//}); // DOM LISTENER WRAPPER
|
|
171
191
|
|
|
172
192
|
// CONSTRUCTOR WRAPPER FOR FORMIQUE CLASS
|
|
@@ -180,69 +200,69 @@ generateFormId() {
|
|
|
180
200
|
|
|
181
201
|
|
|
182
202
|
initDependencyGraph() {
|
|
183
|
-
|
|
203
|
+
this.dependencyGraph = {};
|
|
204
|
+
|
|
205
|
+
this.formSchema.forEach((field) => {
|
|
206
|
+
const [type, name, label, validate, attributes = {}] = field;
|
|
207
|
+
const fieldId = attributes.id || name;
|
|
208
|
+
|
|
209
|
+
if (attributes.dependents) {
|
|
210
|
+
// Initialize dependency array for the parent field
|
|
211
|
+
this.dependencyGraph[fieldId] = attributes.dependents.map((dependentName) => {
|
|
212
|
+
const dependentField = this.formSchema.find(
|
|
213
|
+
([, depName]) => depName === dependentName
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
if (dependentField) {
|
|
217
|
+
const dependentAttributes = dependentField[4] || {};
|
|
218
|
+
const dependentFieldId = dependentAttributes.id || dependentName; // Get dependent field ID
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
dependent: dependentFieldId,
|
|
222
|
+
condition: dependentAttributes.condition || null,
|
|
223
|
+
};
|
|
224
|
+
} else {
|
|
225
|
+
console.warn(`Dependent field "${dependentName}" not found in schema.`);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
184
228
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const fieldId = attributes.id || name;
|
|
229
|
+
// Add state tracking for the parent field
|
|
230
|
+
this.dependencyGraph[fieldId].push({ state: null });
|
|
188
231
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
this.dependencyGraph[fieldId] = attributes.dependents.map((dependentName) => {
|
|
192
|
-
const dependentField = this.formSchema.find(
|
|
193
|
-
([, depName]) => depName === dependentName
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
if (dependentField) {
|
|
197
|
-
const dependentAttributes = dependentField[4] || {};
|
|
198
|
-
const dependentFieldId = dependentAttributes.id || dependentName; // Get dependent field ID
|
|
199
|
-
|
|
200
|
-
return {
|
|
201
|
-
dependent: dependentFieldId,
|
|
202
|
-
condition: dependentAttributes.condition || null,
|
|
203
|
-
};
|
|
204
|
-
} else {
|
|
205
|
-
console.warn(`Dependent field "${dependentName}" not found in schema.`);
|
|
232
|
+
// Attach the input change event listener to the parent field
|
|
233
|
+
this.attachInputChangeListener(fieldId);
|
|
206
234
|
}
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// Add state tracking for the parent field
|
|
210
|
-
this.dependencyGraph[fieldId].push({ state: null });
|
|
211
|
-
|
|
212
|
-
// console.log("Graph", this.dependencyGraph[fieldId]);
|
|
213
|
-
|
|
214
|
-
// Attach the input change event listener to the parent field
|
|
215
|
-
this.attachInputChangeListener(fieldId);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Hide dependent fields initially
|
|
219
|
-
if (attributes.dependents) {
|
|
220
235
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
236
|
+
// Hide dependent fields initially and set their required state
|
|
237
|
+
if (attributes.dependents) {
|
|
238
|
+
attributes.dependents.forEach((dependentName) => {
|
|
239
|
+
const dependentField = this.formSchema.find(
|
|
240
|
+
([, depName]) => depName === dependentName
|
|
241
|
+
);
|
|
242
|
+
const dependentAttributes = dependentField ? dependentField[4] || {} : {};
|
|
243
|
+
const dependentFieldId = dependentAttributes.id || dependentName;
|
|
244
|
+
|
|
245
|
+
const inputBlock = document.querySelector(`#${dependentFieldId}-block`);
|
|
246
|
+
|
|
247
|
+
if (inputBlock) {
|
|
248
|
+
inputBlock.style.display = 'none'; // Hide dependent field by default
|
|
249
|
+
// Save original required state and set to false
|
|
250
|
+
const inputs = inputBlock.querySelectorAll('input, select, textarea');
|
|
251
|
+
inputs.forEach((input) => {
|
|
252
|
+
// Check if the input was originally required in the schema
|
|
253
|
+
if (input.hasAttribute('required') && input.required === true) {
|
|
254
|
+
input.setAttribute('data-original-required', 'true'); // Save original required state
|
|
255
|
+
input.required = false; // Remove required attribute when hiding
|
|
256
|
+
} else {
|
|
257
|
+
input.setAttribute('data-original-required', 'false'); // Explicitly mark as not originally required
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
});
|
|
237
262
|
}
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
// console.log("Dependency Graph:", this.dependencyGraph);
|
|
263
|
+
});
|
|
243
264
|
}
|
|
244
265
|
|
|
245
|
-
|
|
246
266
|
// Attach Event Listeners
|
|
247
267
|
attachInputChangeListener(parentField) {
|
|
248
268
|
const fieldElement = document.getElementById(parentField);
|
|
@@ -344,6 +364,60 @@ registerObservers() {
|
|
|
344
364
|
}
|
|
345
365
|
|
|
346
366
|
|
|
367
|
+
// --- NEW METHOD FOR DYNAMIC SELECT LISTENERS ---
|
|
368
|
+
attachDynamicSelectListeners() {
|
|
369
|
+
this.formSchema.forEach(field => {
|
|
370
|
+
const [type, name, label, validate, attributes = {}] = field;
|
|
371
|
+
|
|
372
|
+
if (type === 'dynamicSingleSelect') {
|
|
373
|
+
const mainSelectId = attributes.id || name;
|
|
374
|
+
const mainSelectElement = document.getElementById(mainSelectId);
|
|
375
|
+
|
|
376
|
+
if (mainSelectElement) {
|
|
377
|
+
mainSelectElement.addEventListener('change', (event) => {
|
|
378
|
+
const selectedCategory = event.target.value; // e.g., 'frontend', 'backend', 'server'
|
|
379
|
+
|
|
380
|
+
// Find all sub-category fieldsets related to this main select
|
|
381
|
+
const subCategoryFieldsets = document.querySelectorAll(`.${mainSelectId}-subcategory-group`);
|
|
382
|
+
|
|
383
|
+
subCategoryFieldsets.forEach(fieldset => {
|
|
384
|
+
const subSelect = fieldset.querySelector('select'); // Get the actual select element
|
|
385
|
+
if (subSelect) {
|
|
386
|
+
// Save original required state (if it was true) then set to false if hidden
|
|
387
|
+
subSelect.setAttribute('data-original-required', subSelect.required.toString());
|
|
388
|
+
subSelect.required = false; // Always set to false when hiding
|
|
389
|
+
}
|
|
390
|
+
fieldset.style.display = 'none'; // Hide all sub-category fieldsets initially
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// Show the selected sub-category fieldset and manage its required state
|
|
394
|
+
const selectedFieldsetId = selectedCategory + '-options'; // Matches the ID format in renderSingleSelectField
|
|
395
|
+
const selectedFieldset = document.getElementById(selectedFieldsetId);
|
|
396
|
+
|
|
397
|
+
if (selectedFieldset) {
|
|
398
|
+
selectedFieldset.style.display = 'block'; // Show the selected one
|
|
399
|
+
const selectedSubSelect = selectedFieldset.querySelector('select');
|
|
400
|
+
if (selectedSubSelect) {
|
|
401
|
+
// Restore original required state for the visible select
|
|
402
|
+
selectedSubSelect.required = selectedSubSelect.getAttribute('data-original-required') === 'true';
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// IMPORTANT: Trigger the change listener once on load if a default option is selected
|
|
408
|
+
// This ensures correct initial visibility and required states if there's a pre-selected main category.
|
|
409
|
+
// We do this by dispatching a 'change' event programmatically if the select has a value.
|
|
410
|
+
if (mainSelectElement.value) {
|
|
411
|
+
const event = new Event('change');
|
|
412
|
+
mainSelectElement.dispatchEvent(event);
|
|
413
|
+
}
|
|
414
|
+
} else {
|
|
415
|
+
console.warn(`Main dynamic select element with ID ${mainSelectId} not found.`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
347
421
|
applyTheme(theme, formContainerId) {
|
|
348
422
|
//const stylesheet = document.querySelector('link[formique-style]');
|
|
349
423
|
|
|
@@ -398,6 +472,67 @@ applyTheme(theme, formContainerId) {
|
|
|
398
472
|
}
|
|
399
473
|
|
|
400
474
|
|
|
475
|
+
|
|
476
|
+
// New method to apply a custom theme based on a color
|
|
477
|
+
applyCustomTheme(color, formContainerId) {
|
|
478
|
+
const formContainer = document.getElementById(formContainerId);
|
|
479
|
+
|
|
480
|
+
if (!formContainer) {
|
|
481
|
+
console.error(`Form container with ID "${formContainerId}" not found. Cannot apply custom theme.`);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// You can add 'formique' class here as well if not already added
|
|
486
|
+
formContainer.classList.add('formique');
|
|
487
|
+
|
|
488
|
+
// Generate a slightly darker shade for the button shadow if needed
|
|
489
|
+
// This is a simplified example; for robust color manipulation, consider a library
|
|
490
|
+
const darkenColor = (hex, percent) => {
|
|
491
|
+
const f = parseInt(hex.slice(1), 16);
|
|
492
|
+
const t = percent < 0 ? 0 : 255;
|
|
493
|
+
const p = percent < 0 ? percent * -1 : percent;
|
|
494
|
+
const R = f >> 16;
|
|
495
|
+
const G = (f >> 8) & 0x00FF;
|
|
496
|
+
const B = f & 0x0000FF;
|
|
497
|
+
return "#" + (0x1000000 + (Math.round((t - R) * p) + R) * 0x10000 + (Math.round((t - G) * p) + G) * 0x100 + (Math.round((t - B) * p) + B)).toString(16).slice(1);
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const shadowColor = darkenColor(color, 0.2); // Darken the theme color by 20% for shadow
|
|
501
|
+
|
|
502
|
+
// Define the custom CSS variables, prioritizing the provided color
|
|
503
|
+
const customCssVars = {
|
|
504
|
+
'--formique-base-bg': '#ffffff', // Light theme base background
|
|
505
|
+
'--formique-base-text': '#333333', // Light theme base text
|
|
506
|
+
'--formique-base-shadow': '0 10px 30px rgba(0, 0, 0, 0.1)', // Light theme shadow
|
|
507
|
+
'--formique-base-label': '#555555', // Light theme label
|
|
508
|
+
'--formique-input-border': '#dddddd', // Light theme input border
|
|
509
|
+
'--formique-focus-color': color, // Set to the provided custom color
|
|
510
|
+
'--formique-btn-bg': color, // Set to the provided custom color
|
|
511
|
+
'--formique-btn-text': '#ffffff', // White text for buttons
|
|
512
|
+
'--formique-btn-shadow': `0 2px 10px ${shadowColor || 'rgba(0, 0, 0, 0.1)'}` // Dynamic button shadow
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
let styleContent = '';
|
|
516
|
+
for (const [prop, val] of Object.entries(customCssVars)) {
|
|
517
|
+
styleContent += ` ${prop}: ${val};\n`;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Create a <style> tag for the custom theme
|
|
521
|
+
const styleElement = document.createElement('style');
|
|
522
|
+
styleElement.textContent = `
|
|
523
|
+
#${formContainerId}.formique {
|
|
524
|
+
${styleContent}
|
|
525
|
+
}
|
|
526
|
+
`;
|
|
527
|
+
|
|
528
|
+
// Insert the style element into the head or before the form container
|
|
529
|
+
formContainer.parentNode.insertBefore(styleElement, formContainer);
|
|
530
|
+
|
|
531
|
+
console.log(`Applied custom theme with color: ${color} to form container: ${formContainerId}`);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
|
|
401
536
|
// renderFormElement method
|
|
402
537
|
renderFormElement() {
|
|
403
538
|
let formHTML = '<form';
|
|
@@ -663,14 +798,14 @@ hasFileInputs(form) {
|
|
|
663
798
|
|
|
664
799
|
async handleEmailSubmission(formId) {
|
|
665
800
|
console.log(`Starting email submission for form ID: ${formId}`);
|
|
666
|
-
|
|
801
|
+
|
|
667
802
|
const form = document.getElementById(formId);
|
|
668
803
|
if (!form) {
|
|
669
804
|
console.error(`Form with ID ${formId} not found`);
|
|
670
805
|
throw new Error(`Form with ID ${formId} not found`);
|
|
671
806
|
}
|
|
672
807
|
|
|
673
|
-
// Validate required settings
|
|
808
|
+
// Validate required settings for 'sendTo'
|
|
674
809
|
if (!Array.isArray(this.formSettings?.sendTo) || this.formSettings.sendTo.length === 0) {
|
|
675
810
|
console.error('formSettings.sendTo must be an array with at least one recipient email');
|
|
676
811
|
throw new Error('formSettings.sendTo must be an array with at least one recipient email');
|
|
@@ -680,46 +815,53 @@ async handleEmailSubmission(formId) {
|
|
|
680
815
|
const payload = {
|
|
681
816
|
formData: {},
|
|
682
817
|
metadata: {
|
|
683
|
-
recipients: this.formSettings.sendTo,
|
|
684
|
-
timestamp: new Date().toISOString()
|
|
685
|
-
}
|
|
818
|
+
recipients: this.formSettings.sendTo,
|
|
819
|
+
timestamp: new Date().toISOString(),
|
|
820
|
+
},
|
|
686
821
|
};
|
|
687
822
|
|
|
688
823
|
let senderName = '';
|
|
689
824
|
let senderEmail = '';
|
|
690
825
|
let formSubject = '';
|
|
826
|
+
let registrantEmail = ''; // Variable to store the registrant's email
|
|
691
827
|
|
|
692
828
|
console.log('Initial payload structure:', JSON.parse(JSON.stringify(payload)));
|
|
693
829
|
|
|
694
|
-
// Process form fields
|
|
695
|
-
new FormData(form)
|
|
830
|
+
// Process form fields and find registrant's email
|
|
831
|
+
const formData = new FormData(form);
|
|
832
|
+
formData.forEach((value, key) => {
|
|
696
833
|
console.log(`Processing form field - Key: ${key}, Value: ${value}`);
|
|
697
834
|
payload.formData[key] = value;
|
|
698
|
-
|
|
835
|
+
|
|
699
836
|
const lowerKey = key.toLowerCase();
|
|
700
|
-
if (
|
|
837
|
+
if (lowerKey.includes('email')) {
|
|
701
838
|
senderEmail = value;
|
|
702
839
|
}
|
|
703
|
-
if (
|
|
840
|
+
if (lowerKey.includes('name')) {
|
|
704
841
|
senderName = value;
|
|
705
842
|
}
|
|
706
|
-
if (
|
|
843
|
+
if (lowerKey.includes('subject')) {
|
|
707
844
|
formSubject = value;
|
|
708
845
|
}
|
|
846
|
+
|
|
847
|
+
// NEW: Check if the current field is the registrant's email
|
|
848
|
+
if (this.formSettings.emailField && key === this.formSettings.emailField) {
|
|
849
|
+
registrantEmail = value;
|
|
850
|
+
}
|
|
709
851
|
});
|
|
710
852
|
|
|
711
853
|
// Determine the email subject with fallback logic
|
|
712
|
-
payload.metadata.subject = formSubject ||
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
854
|
+
payload.metadata.subject = formSubject ||
|
|
855
|
+
this.formSettings.subject ||
|
|
856
|
+
'Message From Contact Form';
|
|
857
|
+
|
|
716
858
|
console.log('Determined email subject:', payload.metadata.subject);
|
|
717
859
|
|
|
718
860
|
// Add sender information to metadata
|
|
719
861
|
if (senderEmail) {
|
|
720
862
|
payload.metadata.sender = senderEmail;
|
|
721
|
-
payload.metadata.replyTo = senderName
|
|
722
|
-
? `${senderName} <${senderEmail}>`
|
|
863
|
+
payload.metadata.replyTo = senderName
|
|
864
|
+
? `${senderName} <${senderEmail}>`
|
|
723
865
|
: senderEmail;
|
|
724
866
|
}
|
|
725
867
|
|
|
@@ -728,54 +870,113 @@ async handleEmailSubmission(formId) {
|
|
|
728
870
|
try {
|
|
729
871
|
const endpoint = this.formiqueEndpoint || this.formAction;
|
|
730
872
|
const method = this.method || 'POST';
|
|
731
|
-
|
|
732
|
-
console.log(`Preparing to send request to: ${endpoint}`);
|
|
873
|
+
|
|
874
|
+
console.log(`Preparing to send primary request to: ${endpoint}`);
|
|
733
875
|
console.log(`Request method: ${method}`);
|
|
734
|
-
console.log('Final payload being sent:', payload);
|
|
876
|
+
console.log('Final payload being sent to recipients:', payload);
|
|
735
877
|
|
|
878
|
+
// Send the first email to the 'sendTo' recipients
|
|
736
879
|
const response = await fetch(endpoint, {
|
|
737
880
|
method: method,
|
|
738
|
-
headers: {
|
|
881
|
+
headers: {
|
|
739
882
|
'Content-Type': 'application/json',
|
|
740
|
-
'X-Formique-Version': '1.0'
|
|
883
|
+
'X-Formique-Version': '1.0',
|
|
741
884
|
},
|
|
742
|
-
body: JSON.stringify(payload)
|
|
885
|
+
body: JSON.stringify(payload),
|
|
743
886
|
});
|
|
744
887
|
|
|
745
|
-
console.log(`Received response with status: ${response.status}`);
|
|
888
|
+
console.log(`Received response for primary email with status: ${response.status}`);
|
|
746
889
|
|
|
747
890
|
if (!response.ok) {
|
|
748
891
|
const errorData = await response.json().catch(() => ({}));
|
|
749
892
|
console.error('API Error Response:', errorData);
|
|
750
893
|
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
|
751
|
-
document.getElementById("formiqueSpinner").style.display = "none";
|
|
752
|
-
|
|
753
894
|
}
|
|
754
895
|
|
|
755
896
|
const data = await response.json();
|
|
756
|
-
console.log('API Success Response:', data);
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
897
|
+
console.log('Primary API Success Response:', data);
|
|
898
|
+
|
|
899
|
+
// ------------------- NEW RSVP LOGIC -------------------
|
|
900
|
+
if (this.formSettings.submitMode === 'rsvp' && registrantEmail && this.formSettings.registrantMessage) {
|
|
901
|
+
console.log('RSVP mode detected. Sending confirmation email to registrant.');
|
|
902
|
+
|
|
903
|
+
// Create a new payload for the registrant
|
|
904
|
+
const rsvpPayload = {
|
|
905
|
+
formData: payload.formData,
|
|
906
|
+
metadata: {
|
|
907
|
+
recipients: [registrantEmail], // Send only to the registrant
|
|
908
|
+
timestamp: new Date().toISOString(),
|
|
909
|
+
subject: this.formSettings.registrantSubject || 'RSVP Confirmation',
|
|
910
|
+
body: this.processDynamicMessage(this.formSettings.registrantMessage, payload.formData),
|
|
911
|
+
sender: this.formSettings.sendFrom || 'noreply@yourdomain.com',
|
|
912
|
+
replyTo: this.formSettings.sendFrom || 'noreply@yourdomain.com',
|
|
913
|
+
},
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
try {
|
|
917
|
+
console.log('Preparing to send RSVP email. Final payload:', rsvpPayload);
|
|
918
|
+
const rsvpResponse = await fetch(endpoint, {
|
|
919
|
+
method: method,
|
|
920
|
+
headers: {
|
|
921
|
+
'Content-Type': 'application/json',
|
|
922
|
+
'X-Formique-Version': '1.0',
|
|
923
|
+
},
|
|
924
|
+
body: JSON.stringify(rsvpPayload),
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
if (!rsvpResponse.ok) {
|
|
928
|
+
const rsvpErrorData = await rsvpResponse.json().catch(() => ({}));
|
|
929
|
+
console.error('RSVP API Error Response:', rsvpErrorData);
|
|
930
|
+
// Log the error but don't fail the entire submission since the primary email was sent
|
|
931
|
+
console.warn('Failed to send RSVP email to registrant, but primary submission was successful.');
|
|
932
|
+
} else {
|
|
933
|
+
console.log('RSVP email sent successfully to registrant.');
|
|
934
|
+
}
|
|
935
|
+
} catch (rsvpError) {
|
|
936
|
+
console.error('RSVP email submission failed:', rsvpError);
|
|
937
|
+
console.warn('Failed to send RSVP email to registrant, but primary submission was successful.');
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
// ------------------- END NEW RSVP LOGIC -------------------
|
|
941
|
+
|
|
942
|
+
const successMessage = this.formSettings.successMessage ||
|
|
943
|
+
data.message ||
|
|
944
|
+
'Your message has been sent successfully!';
|
|
761
945
|
console.log(`Showing success message: ${successMessage}`);
|
|
762
946
|
|
|
763
947
|
this.showSuccessMessage(successMessage);
|
|
764
948
|
|
|
765
949
|
} catch (error) {
|
|
766
950
|
console.error('Email submission failed:', error);
|
|
767
|
-
const errorMessage = this.formSettings.errorMessage ||
|
|
768
|
-
|
|
769
|
-
|
|
951
|
+
const errorMessage = this.formSettings.errorMessage ||
|
|
952
|
+
error.message ||
|
|
953
|
+
'Failed to send message. Please try again later.';
|
|
770
954
|
console.log(`Showing error message: ${errorMessage}`);
|
|
771
955
|
this.showErrorMessage(errorMessage);
|
|
956
|
+
} finally {
|
|
772
957
|
document.getElementById("formiqueSpinner").style.display = "none";
|
|
958
|
+
}
|
|
959
|
+
}
|
|
773
960
|
|
|
961
|
+
|
|
962
|
+
// Add this method to your Formique class
|
|
963
|
+
processDynamicMessage(message, formData) {
|
|
964
|
+
let processedMessage = message;
|
|
965
|
+
// Iterate over each key-value pair in the form data
|
|
966
|
+
for (const key in formData) {
|
|
967
|
+
if (Object.prototype.hasOwnProperty.call(formData, key)) {
|
|
968
|
+
const placeholder = `{${key}}`;
|
|
969
|
+
// Replace all occurrences of the placeholder with the corresponding form data value
|
|
970
|
+
processedMessage = processedMessage.split(placeholder).join(formData[key]);
|
|
971
|
+
}
|
|
774
972
|
}
|
|
973
|
+
return processedMessage;
|
|
775
974
|
}
|
|
776
975
|
|
|
777
976
|
|
|
778
977
|
|
|
978
|
+
|
|
979
|
+
|
|
779
980
|
// Email validation helper
|
|
780
981
|
validateEmail(email) {
|
|
781
982
|
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
@@ -3402,17 +3603,20 @@ this.renderSingleSelectField(type, name, label, validate, attributes, mainCatego
|
|
|
3402
3603
|
|
|
3403
3604
|
renderSingleSelectField(type, name, label, validate, attributes, options, subCategoriesOptions, mode) {
|
|
3404
3605
|
|
|
3405
|
-
console.log("Within");
|
|
3606
|
+
console.log("Within renderSingleSelectField");
|
|
3406
3607
|
// Define valid validation attributes for select fields
|
|
3407
3608
|
const selectValidationAttributes = ['required'];
|
|
3408
3609
|
|
|
3409
3610
|
// Construct validation attributes
|
|
3410
3611
|
let validationAttrs = '';
|
|
3612
|
+
// Store original required state for the main select
|
|
3613
|
+
let originalRequired = false; // <--- This variable tracks if the main select was originally required
|
|
3411
3614
|
if (validate) {
|
|
3412
3615
|
Object.entries(validate).forEach(([key, value]) => {
|
|
3413
3616
|
if (selectValidationAttributes.includes(key)) {
|
|
3414
3617
|
if (key === 'required') {
|
|
3415
3618
|
validationAttrs += `${key} `;
|
|
3619
|
+
originalRequired = true; // Mark that it was originally required
|
|
3416
3620
|
}
|
|
3417
3621
|
} else {
|
|
3418
3622
|
console.warn(`\x1b[31mUnsupported validation attribute '${key}' for field '${name}' of type '${type}'.\x1b[0m`);
|
|
@@ -3423,10 +3627,10 @@ console.log("Within");
|
|
|
3423
3627
|
// Handle the binding syntax
|
|
3424
3628
|
let bindingDirective = '';
|
|
3425
3629
|
if (attributes.binding) {
|
|
3426
|
-
|
|
3427
|
-
|
|
3630
|
+
if (typeof attributes.binding === 'string' && attributes.binding.startsWith('::')) {
|
|
3631
|
+
bindingDirective = ` bind:value="${name}" `;
|
|
3632
|
+
}
|
|
3428
3633
|
}
|
|
3429
|
-
}
|
|
3430
3634
|
|
|
3431
3635
|
// Define attributes for the select field
|
|
3432
3636
|
let id = attributes.id || name;
|
|
@@ -3435,7 +3639,8 @@ console.log("Within");
|
|
|
3435
3639
|
// Handle additional attributes
|
|
3436
3640
|
let additionalAttrs = '';
|
|
3437
3641
|
for (const [key, value] of Object.entries(attributes)) {
|
|
3438
|
-
|
|
3642
|
+
if (key !== 'id' && key !== 'class' && key !== 'dependsOn' && key !== 'dependents' && value !== undefined) {
|
|
3643
|
+
if (key.startsWith('on')) {
|
|
3439
3644
|
// Handle event attributes
|
|
3440
3645
|
const eventValue = value.endsWith('()') ? value.slice(0, -2) : value;
|
|
3441
3646
|
additionalAttrs += ` @${key.replace(/^on/, '')}={${eventValue}}\n`;
|
|
@@ -3470,32 +3675,33 @@ console.log("Within");
|
|
|
3470
3675
|
|
|
3471
3676
|
let inputClass = attributes.class || this.inputClass;
|
|
3472
3677
|
|
|
3473
|
-
|
|
3474
|
-
|
|
3678
|
+
// Remove `onchange` from HTML; it will be handled by JavaScript event listeners
|
|
3679
|
+
const onchangeAttr = ''; // <--- Ensure this is an empty string
|
|
3680
|
+
|
|
3475
3681
|
let labelDisplay;
|
|
3476
|
-
let rawLabel;
|
|
3682
|
+
let rawLabel;
|
|
3477
3683
|
|
|
3478
3684
|
if (mode === 'dynamicSingleSelect' && subCategoriesOptions) {
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3685
|
+
if (label.includes('-')) {
|
|
3686
|
+
const [mainCategoryLabel] = label.split('-');
|
|
3687
|
+
labelDisplay = mainCategoryLabel;
|
|
3688
|
+
rawLabel = label;
|
|
3689
|
+
} else {
|
|
3690
|
+
labelDisplay = label;
|
|
3691
|
+
rawLabel = label;
|
|
3692
|
+
}
|
|
3487
3693
|
} else {
|
|
3488
|
-
|
|
3694
|
+
labelDisplay = label;
|
|
3489
3695
|
}
|
|
3490
3696
|
|
|
3491
3697
|
|
|
3492
|
-
// Construct the final HTML string
|
|
3698
|
+
// Construct the final HTML string for the main select
|
|
3493
3699
|
let formHTML = `
|
|
3494
3700
|
<fieldset class="${this.selectGroupClass}" id="${id + '-block'}">
|
|
3495
|
-
<legend>${labelDisplay}
|
|
3701
|
+
<legend>${labelDisplay}
|
|
3496
3702
|
${validationAttrs.includes('required') && this.formSettings.requiredFieldIndicator ? this.formSettings.asteriskHtml : ''}
|
|
3497
3703
|
</legend>
|
|
3498
|
-
<label for="${id}"> Select ${labelDisplay}
|
|
3704
|
+
<label for="${id}"> Select ${labelDisplay}
|
|
3499
3705
|
<select name="${name}"
|
|
3500
3706
|
${bindingDirective}
|
|
3501
3707
|
${dimensionAttrs}
|
|
@@ -3503,8 +3709,7 @@ console.log("Within");
|
|
|
3503
3709
|
class="${inputClass}"
|
|
3504
3710
|
${additionalAttrs}
|
|
3505
3711
|
${validationAttrs}
|
|
3506
|
-
${
|
|
3507
|
-
>
|
|
3712
|
+
data-original-required="${originalRequired}" >
|
|
3508
3713
|
${selectHTML}
|
|
3509
3714
|
</select>
|
|
3510
3715
|
</fieldset>
|
|
@@ -3524,122 +3729,89 @@ console.log("Within");
|
|
|
3524
3729
|
return `\n${match}\n`;
|
|
3525
3730
|
}).replace(/\n\s*\n/g, '\n'); // Remove extra blank lines
|
|
3526
3731
|
|
|
3527
|
-
//console.log(formattedHtml);
|
|
3528
3732
|
this.formMarkUp+=formattedHtml;
|
|
3529
|
-
//return formattedHtml;
|
|
3530
|
-
|
|
3531
3733
|
|
|
3532
|
-
/* dynamicSingleSelect */
|
|
3533
3734
|
|
|
3534
|
-
|
|
3735
|
+
/* dynamicSingleSelect - Sub-Category Generation Block */
|
|
3535
3736
|
|
|
3737
|
+
if (mode && mode ==='dynamicSingleSelect' && subCategoriesOptions) {
|
|
3536
3738
|
|
|
3537
|
-
//
|
|
3538
|
-
const targetDiv = document.getElementById(this.formContainerId);
|
|
3739
|
+
const categoryId = attributes.id || name; // This is the ID of the main dynamic select ('languages')
|
|
3539
3740
|
|
|
3540
|
-
|
|
3741
|
+
subCategoriesOptions.forEach(subCategory => {
|
|
3742
|
+
const { id, label, options: subOptions } = subCategory; // Renamed 'options' to 'subOptions' to avoid conflict
|
|
3541
3743
|
|
|
3744
|
+
// IMPORTANT: Sub-category selects are *initially hidden*
|
|
3745
|
+
// Therefore, by default, they are NOT required until they are revealed.
|
|
3746
|
+
// If your schema later allows specific sub-categories to be inherently required
|
|
3747
|
+
// when shown, you'd need to extract that validation from your schema here.
|
|
3748
|
+
// For now, they are considered non-required until JavaScript makes them required.
|
|
3749
|
+
let isSubCategoryRequired = false; // Default to false as they are hidden
|
|
3750
|
+
const subCategoryValidationAttrs = ''; // No direct 'required' in HTML initially
|
|
3542
3751
|
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
// Hide all subcategory fields
|
|
3551
|
-
document.querySelectorAll(\`[class*="\${fieldsetid}"]\`).forEach(div => {
|
|
3552
|
-
div.style.display = "none";
|
|
3553
|
-
});
|
|
3752
|
+
// Build the select options HTML for sub-category
|
|
3753
|
+
const subSelectHTML = subOptions.map(option => {
|
|
3754
|
+
const isSelected = option.selected ? ' selected' : '';
|
|
3755
|
+
return `
|
|
3756
|
+
<option value="${option.value}"${isSelected}>${option.label}</option>
|
|
3757
|
+
`;
|
|
3758
|
+
}).join('');
|
|
3554
3759
|
|
|
3555
|
-
// Show the selected category
|
|
3556
|
-
const selectedCategoryFieldset = document.getElementById(category + '-options');
|
|
3557
|
-
if (selectedCategoryFieldset) {
|
|
3558
|
-
selectedCategoryFieldset.style.display = "block";
|
|
3559
|
-
}
|
|
3560
|
-
}
|
|
3561
|
-
`;
|
|
3562
3760
|
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
} else {
|
|
3566
|
-
console.error(`Target div with id "${this.formContainerId}" not found.`);
|
|
3567
|
-
}
|
|
3761
|
+
let subCategoryLabel;
|
|
3762
|
+
console.log('Label (rawLabel for sub-category):', rawLabel); // Debug log
|
|
3568
3763
|
|
|
3569
|
-
|
|
3570
|
-
|
|
3764
|
+
if (rawLabel.includes('-')) {
|
|
3765
|
+
subCategoryLabel = rawLabel.split('-')?.[1] + ' Options';
|
|
3766
|
+
} else {
|
|
3767
|
+
subCategoryLabel = 'options';
|
|
3768
|
+
}
|
|
3571
3769
|
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
}).join('');
|
|
3770
|
+
let optionsLabel;
|
|
3771
|
+
if (subCategoryLabel !== 'options') {
|
|
3772
|
+
optionsLabel = rawLabel.split('-')?.[1] + ' Option';
|
|
3773
|
+
} else {
|
|
3774
|
+
optionsLabel = subCategoryLabel;
|
|
3775
|
+
}
|
|
3579
3776
|
|
|
3580
3777
|
|
|
3581
|
-
|
|
3582
|
-
|
|
3778
|
+
// Create the HTML for the sub-category fieldset and select elements
|
|
3779
|
+
// Added a class based on the main select's ID for easy grouping/selection
|
|
3780
|
+
let subFormHTML = `
|
|
3781
|
+
<fieldset class="${this.selectGroupClass} ${categoryId}-subcategory-group" id="${id}-options" style="display: none;"> <legend>${label} ${subCategoryLabel} ${isSubCategoryRequired && this.formSettings.requiredFieldIndicator ? this.formSettings.asteriskHtml : ''}
|
|
3782
|
+
</legend>
|
|
3783
|
+
<label for="${id}"> Select ${label} ${optionsLabel}
|
|
3784
|
+
</label>
|
|
3785
|
+
<select name="${id}"
|
|
3786
|
+
${bindingDirective}
|
|
3787
|
+
${dimensionAttrs}
|
|
3788
|
+
id="${id}"
|
|
3789
|
+
class="${inputClass}"
|
|
3790
|
+
${additionalAttrs}
|
|
3791
|
+
${subCategoryValidationAttrs}
|
|
3792
|
+
data-original-required="${isSubCategoryRequired}" >
|
|
3793
|
+
<option value="">Choose an option</option>
|
|
3794
|
+
${subSelectHTML}
|
|
3795
|
+
</select>
|
|
3796
|
+
</fieldset>
|
|
3797
|
+
`.replace(/^\s*\n/gm, '').trim();
|
|
3798
|
+
|
|
3799
|
+
// Apply vertical layout to the <select> element and its children
|
|
3800
|
+
subFormHTML = subFormHTML.replace(/<select\s+([^>]*)>([\s\S]*?)<\/select>/g, (match, p1, p2) => {
|
|
3801
|
+
const attributes = p1.trim().split(/\s+/).map(attr => ` ${attr}`).join('\n');
|
|
3802
|
+
return `<select\n${attributes}\n>\n${p2.trim()}\n</select>`;
|
|
3803
|
+
});
|
|
3583
3804
|
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
}
|
|
3805
|
+
// Ensure the <fieldset> block starts on a new line and remove extra blank lines
|
|
3806
|
+
subFormHTML = subFormHTML.replace(/(<fieldset\s+[^>]*>)/g, (match) => {
|
|
3807
|
+
return `\n${match}\n`;
|
|
3808
|
+
}).replace(/\n\s*\n/g, '\n');
|
|
3589
3809
|
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
} else {
|
|
3594
|
-
optionsLabel = subCategoryLabel;
|
|
3810
|
+
// Append the generated HTML to formMarkUp
|
|
3811
|
+
this.formMarkUp += subFormHTML;
|
|
3812
|
+
});
|
|
3595
3813
|
}
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
// Create the HTML for the fieldset and select elements
|
|
3599
|
-
let formHTML = `
|
|
3600
|
-
<fieldset class="${this.selectGroupClass} ${categoryId}" id="${id}-options" style="display: none;">
|
|
3601
|
-
<legend> ${label} ${subCategoryLabel} ${this.formSettings.requiredFieldIndicator ? this.formSettings.asteriskHtml : ''}
|
|
3602
|
-
</legend>
|
|
3603
|
-
<label for="${id}"> Select ${label} ${optionsLabel}
|
|
3604
|
-
</label>
|
|
3605
|
-
<select name="${id}"
|
|
3606
|
-
${bindingDirective}
|
|
3607
|
-
${dimensionAttrs}
|
|
3608
|
-
id="${id + '-block'}"
|
|
3609
|
-
class="${inputClass}"
|
|
3610
|
-
${additionalAttrs}
|
|
3611
|
-
${validationAttrs}
|
|
3612
|
-
>
|
|
3613
|
-
<option value="">Choose an option</option>
|
|
3614
|
-
${selectHTML}
|
|
3615
|
-
</select>
|
|
3616
|
-
</fieldset>
|
|
3617
|
-
`.replace(/^\s*\n/gm, '').trim();
|
|
3618
|
-
|
|
3619
|
-
// Apply vertical layout to the <select> element and its children
|
|
3620
|
-
formHTML = formHTML.replace(/<select\s+([^>]*)>([\s\S]*?)<\/select>/g, (match, p1, p2) => {
|
|
3621
|
-
// Reformat attributes into a vertical layout
|
|
3622
|
-
const attributes = p1.trim().split(/\s+/).map(attr => ` ${attr}`).join('\n');
|
|
3623
|
-
return `<select\n${attributes}\n>\n${p2.trim()}\n</select>`;
|
|
3624
|
-
});
|
|
3625
|
-
|
|
3626
|
-
// Ensure the <fieldset> block starts on a new line and remove extra blank lines
|
|
3627
|
-
formHTML = formHTML.replace(/(<fieldset\s+[^>]*>)/g, (match) => {
|
|
3628
|
-
// Ensure <fieldset> starts on a new line
|
|
3629
|
-
return `\n${match}\n`;
|
|
3630
|
-
}).replace(/\n\s*\n/g, '\n'); // Remove extra blank lines
|
|
3631
|
-
|
|
3632
|
-
// Append the generated HTML to formMarkUp
|
|
3633
|
-
this.formMarkUp += formHTML;
|
|
3634
|
-
|
|
3635
|
-
//return formHTML;
|
|
3636
|
-
});
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
3814
|
}
|
|
3640
|
-
}
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
3815
|
|
|
3644
3816
|
renderMultipleSelectField(type, name, label, validate, attributes, options) {
|
|
3645
3817
|
// Define valid validation attributes for multiple select fields
|
|
@@ -4022,3 +4194,12 @@ const spinner = `<div id="formiqueSpinner" style="display: flex; align-items: ce
|
|
|
4022
4194
|
export default Formique;
|
|
4023
4195
|
|
|
4024
4196
|
|
|
4197
|
+
|
|
4198
|
+
|
|
4199
|
+
|
|
4200
|
+
|
|
4201
|
+
|
|
4202
|
+
|
|
4203
|
+
|
|
4204
|
+
|
|
4205
|
+
|