@formique/semantq 1.0.8 → 1.0.10

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.
@@ -1,4 +1,6 @@
1
1
  'use strict';
2
+ import LowCodeParser from './LowCodeParser.js';
3
+ import astToFormique from './astToFormique.js';
2
4
  /**
3
5
  * Formique Semantq Class Library
4
6
  *
@@ -50,18 +52,42 @@ class FormBuilder
50
52
 
51
53
  // Extended class for specific form rendering methods
52
54
  class Formique extends FormBuilder {
53
- constructor(formSchema, formSettings = {}, formParams = {}) {
55
+ constructor(formDefinition, formSettings = {}, formParams = {}) {
54
56
  super();
57
+ let formSchema;
58
+ let finalSettings = formSettings;
59
+ let finalParams = formParams;
60
+
61
+ if (typeof formDefinition === 'string') {
62
+ const ast = LowCodeParser.parse(formDefinition.trim());
63
+ // console.log("AST", JSON.stringify(ast, null,2));
64
+
65
+ const formObjects = new astToFormique(ast);
66
+
67
+ //console.log("formSchema", JSON.stringify(formObjects.formSchema,null,2));
68
+ // Assign from formObjects if a string is passed
69
+ formSchema = formObjects.formSchema;
70
+ finalSettings = { ...formSettings, ...formObjects.formSettings };
71
+ finalParams = { ...formParams, ...formObjects.formParams };
72
+ } else {
73
+ // Assign from the parameters if formDefinition is not a string
74
+ formSchema = formDefinition;
75
+ }
76
+
55
77
  this.formSchema = formSchema;
56
- this.formParams = formParams;
78
+ this.formParams = finalParams;
57
79
  this.formSettings = {
58
80
  requiredFieldIndicator: true,
59
81
  placeholders: true,
60
82
  asteriskHtml: '<span aria-hidden="true" style="color: red;">*</span>',
61
- ...formSettings
83
+ ...finalSettings
62
84
  };
63
85
 
64
- this.themeColor = formSettings.themeColor || null;
86
+ //console.log("constructor",this.formSettings);
87
+
88
+ this.themeColor = this.formSettings.themeColor || null;
89
+
90
+ //console.log("color set?", this.themeColor);
65
91
 
66
92
  this.themeColorMap = {
67
93
  'primary': {
@@ -100,17 +126,21 @@ class Formique extends FormBuilder {
100
126
  this.formiqueEndpoint = "https://formiqueapi.onrender.com/api/send-email";
101
127
 
102
128
  // DISABLE DOM LISTENER
103
- //document.addEventListener('DOMContentLoaded', () => {
129
+ // document.addEventListener('DOMContentLoaded', () => {
104
130
  // 1. Build the form's HTML in memory
105
131
  this.formMarkUp += this.renderFormElement(); // Adds opening <form> tag and any hidden inputs
106
132
 
107
133
  // Filter out 'submit' field for rendering, and render all other fields
108
134
  const nonSubmitFieldsHtml = this.formSchema
109
135
  .filter(field => field[0] !== 'submit')
110
- .map(field => {
111
- const [type, name, label, validate, attributes = {}, options] = field;
112
- return this.renderField(type, name, label, validate, attributes, options);
113
- }).join('');
136
+ .map(field => {
137
+ // FIX 1: Add 'subOptions' to capture the 7th element (Index 6)
138
+ const [type, name, label, validate, attributes = {}, options, subOptions] = field;
139
+
140
+ // FIX 2: Pass 'subOptions' through to renderField
141
+ return this.renderField(type, name, label, validate, attributes, options, subOptions);
142
+ }).join('');
143
+
114
144
  this.formMarkUp += nonSubmitFieldsHtml;
115
145
 
116
146
  // Find and render the submit button separately, at the very end of the form content
@@ -148,46 +178,67 @@ class Formique extends FormBuilder {
148
178
 
149
179
 
150
180
  // 2. Inject the complete form HTML into the DOM
151
- this.renderFormHTML(); // This puts the form element into the document!
152
-
153
- // 3. Now that the form is in the DOM, attach event listeners
154
- const formElement = document.getElementById(`${this.formId}`);
155
- if (formElement) { // Add a check here just in case, although it should now exist
156
- formElement.addEventListener('submit', function(event) {
157
- if (this.formSettings.submitMode === 'email' || this.formSettings.submitMode === 'rsvp') {
158
- event.preventDefault();
159
- document.getElementById("formiqueSpinner").style.display = "block";
160
- this.handleEmailSubmission(this.formId);
161
- }
162
-
163
- if (this.formSettings.submitOnPage) {
164
- event.preventDefault();
165
- document.getElementById("formiqueSpinner").style.display = "block";
166
- this.handleOnPageFormSubmission(this.formId);
167
- }
168
- }.bind(this));
169
- } else {
170
- console.error(`Form with ID ${this.formId} not found after rendering. Event listener could not be attached.`);
181
+ // A conceptual snippet from your form initialization method
182
+ this.renderFormHTML(); // This puts the form element into the document!
183
+
184
+ // 3. Now that the form is in the DOM, get the element and attach a single event listener
185
+ const formElement = document.getElementById(`${this.formId}`);
186
+ if (formElement) {
187
+ // Attach a single, unified submit event listener
188
+ formElement.addEventListener('submit', (event) => {
189
+ // Prevent default submission behavior immediately
190
+ event.preventDefault();
191
+
192
+ // Check if reCAPTCHA is present in the form schema
193
+ const recaptchaField = this.formSchema.find(field => field[0] === 'recaptcha');
194
+
195
+ // If reCAPTCHA is required, validate it first
196
+ if (recaptchaField) {
197
+ const recaptchaToken = grecaptcha.getResponse();
198
+
199
+ if (!recaptchaToken) {
200
+ // If reCAPTCHA is not checked, display an error and stop
201
+ document.getElementById("formiqueSpinner").style.display = "none";
202
+ alert('Please verify that you are not a robot.');
203
+ return; // Stop execution of the handler
171
204
  }
205
+ }
206
+
207
+ // If reCAPTCHA is not required or is validated, proceed with submission logic
208
+ document.getElementById("formiqueSpinner").style.display = "block";
209
+
210
+ if (this.formSettings.submitMode === 'email' || this.formSettings.submitMode === 'rsvp') {
211
+ this.handleEmailSubmission(this.formId);
212
+ }
213
+
214
+ if (this.formSettings.submitOnPage) {
215
+ this.handleOnPageFormSubmission(this.formId);
216
+ }
217
+ });
218
+
219
+ } else {
220
+ console.error(`Form with ID ${this.formId} not found after rendering. Event listener could not be attached.`);
221
+ }
222
+
223
+ // Initialize dependency graph and observers after the form is rendered
224
+ this.initDependencyGraph();
225
+ this.registerObservers();
226
+ this.attachDynamicSelectListeners();
227
+
228
+ // Apply theme
229
+ if (this.themeColor) {
230
+ this.applyCustomTheme(this.themeColor, this.formContainerId);
231
+ } else if (this.formSettings.theme && this.themes.includes(this.formSettings.theme)) {
232
+ let theme = this.formSettings.theme;
233
+ this.applyTheme(theme, this.formContainerId);
234
+ } else {
235
+ this.applyTheme('dark', this.formContainerId); // Default to 'dark'
236
+ }
237
+
172
238
 
173
- // Initialize dependency graph and observers after the form is rendered
174
- this.initDependencyGraph();
175
- this.registerObservers();
176
- this.attachDynamicSelectListeners();
177
-
178
- // Apply theme
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)) {
182
- let theme = this.formSettings.theme;
183
- this.applyTheme(theme, this.formContainerId);
184
- } else {
185
- // Fallback if no themeColor and no valid theme specified
186
- this.applyTheme('dark', this.formContainerId); // Default to 'dark'
187
- }
188
239
 
189
240
  // DISABLE DOM LISTENER
190
- //}); // DOM LISTENER WRAPPER
241
+ // }); // DOM LISTENER WRAPPER
191
242
 
192
243
  // CONSTRUCTOR WRAPPER FOR FORMIQUE CLASS
193
244
  }
@@ -264,16 +315,48 @@ initDependencyGraph() {
264
315
  }
265
316
 
266
317
  // Attach Event Listeners
318
+ // Corrected Attach Event Listeners
267
319
  attachInputChangeListener(parentField) {
268
- const fieldElement = document.getElementById(parentField);
269
- //alert(parentField);
270
-
271
- if (fieldElement) {
272
- fieldElement.addEventListener('input', (event) => {
273
- const value = event.target.value;
274
- this.handleParentFieldChange(parentField, value);
275
- });
320
+ // Use querySelectorAll to get all elements with the name attribute matching the fieldId.
321
+ // This correctly targets all radio/checkbox inputs in a group.
322
+ const fieldElements = document.querySelectorAll(`[name="${parentField}"]`);
323
+
324
+ // If no elements found by name, fall back to getting the single element by ID
325
+ if (fieldElements.length === 0) {
326
+ const singleElement = document.getElementById(parentField);
327
+ if (singleElement) {
328
+ fieldElements = [singleElement]; // Treat it as a single element array
329
+ } else {
330
+ console.warn(`Parent field element(s) not found for field: ${parentField}`);
331
+ return;
332
+ }
276
333
  }
334
+
335
+ fieldElements.forEach(fieldElement => {
336
+ // Radio/checkbox groups should use 'change', not 'input'
337
+ const eventType = (fieldElement.type === 'radio' || fieldElement.type === 'checkbox') ? 'change' : 'input';
338
+
339
+ fieldElement.addEventListener(eventType, (event) => {
340
+ let value;
341
+ if (fieldElement.type === 'radio' && !event.target.checked) {
342
+ // Only process the change if the radio button is now checked
343
+ return;
344
+ }
345
+
346
+ if (fieldElement.type === 'checkbox') {
347
+ // For checkboxes, you might need special logic to return an array of checked values
348
+ // For now, let's stick to the change event on a single checkbox
349
+ value = event.target.checked ? event.target.value : '';
350
+ } else {
351
+ value = event.target.value;
352
+ }
353
+
354
+ // Convert value to lowercase for consistent comparison with 'yes' condition
355
+ //this.handleParentFieldChange(parentField, value.toLowerCase());
356
+
357
+ this.handleParentFieldChange(parentField, value.toLowerCase());
358
+ });
359
+ });
277
360
  }
278
361
 
279
362
 
@@ -327,6 +410,7 @@ handleParentFieldChange(parentFieldId, value) {
327
410
  }
328
411
  }
329
412
 
413
+
330
414
  // Register observers for each dependent field
331
415
  registerObservers() {
332
416
  this.formSchema.forEach((field) => {
@@ -378,7 +462,7 @@ attachDynamicSelectListeners() {
378
462
  const selectedCategory = event.target.value; // e.g., 'frontend', 'backend', 'server'
379
463
 
380
464
  // Find all sub-category fieldsets related to this main select
381
- const subCategoryFieldsets = document.querySelectorAll(`.${mainSelectId}-subcategory-group`);
465
+ const subCategoryFieldsets = document.querySelectorAll(`.${mainSelectId}`);
382
466
 
383
467
  subCategoryFieldsets.forEach(fieldset => {
384
468
  const subSelect = fieldset.querySelector('select'); // Get the actual select element
@@ -391,7 +475,7 @@ attachDynamicSelectListeners() {
391
475
  });
392
476
 
393
477
  // Show the selected sub-category fieldset and manage its required state
394
- const selectedFieldsetId = selectedCategory + '-options'; // Matches the ID format in renderSingleSelectField
478
+ const selectedFieldsetId = selectedCategory; // Matches the ID format in renderSingleSelectField
395
479
  const selectedFieldset = document.getElementById(selectedFieldsetId);
396
480
 
397
481
  if (selectedFieldset) {
@@ -646,51 +730,10 @@ renderForm() {
646
730
  }
647
731
 
648
732
 
649
- /*
650
- renderField(type, name, label, validate, attributes, options) {
651
- const fieldRenderMap = {
652
- 'text': this.renderTextField,
653
- 'email': this.renderEmailField,
654
- 'number': this.renderNumberField,
655
- 'password': this.renderPasswordField,
656
- 'textarea': this.renderTextAreaField,
657
- 'tel': this.renderTelField,
658
- 'date': this.renderDateField,
659
- 'time': this.renderTimeField,
660
- 'datetime-local': this.renderDateTimeField,
661
- 'month': this.renderMonthField,
662
- 'week': this.renderWeekField,
663
- 'url': this.renderUrlField,
664
- 'search': this.renderSearchField,
665
- 'color': this.renderColorField,
666
- 'checkbox': this.renderCheckboxField,
667
- 'radio': this.renderRadioField,
668
- 'file': this.renderFileField,
669
- 'hidden': this.renderHiddenField,
670
- 'image': this.renderImageField,
671
- 'textarea': this.renderTextAreaField,
672
- 'singleSelect': this.renderSingleSelectField,
673
- 'multipleSelect': this.renderMultipleSelectField,
674
- 'dynamicSingleSelect': this.renderDynamicSingleSelectField,
675
- 'range': this.renderRangeField,
676
- 'submit': this.renderSubmitButton,
677
- };
678
-
679
- const renderMethod = fieldRenderMap[type];
680
-
681
- if (renderMethod) {
682
- return renderMethod.call(this, type, name, label, validate, attributes, options);
683
- } else {
684
- console.warn(`Unsupported field type '${type}' encountered.`);
685
- return ''; // or handle gracefully
686
- }
687
- }
688
-
689
- */
690
733
 
691
734
 
692
735
  // renderField method - No change needed here for this issue, but ensure it handles 'submit' type correctly if called directly
693
- renderField(type, name, label, validate, attributes, options) {
736
+ renderField(type, name, label, validate, attributes, options, subOptions = undefined) {
694
737
  const fieldRenderMap = {
695
738
  'text': this.renderTextField,
696
739
  'email': this.renderEmailField,
@@ -715,6 +758,7 @@ renderField(type, name, label, validate, attributes, options) {
715
758
  'multipleSelect': this.renderMultipleSelectField,
716
759
  'dynamicSingleSelect': this.renderDynamicSingleSelectField,
717
760
  'range': this.renderRangeField,
761
+ 'recaptcha': this.renderRecaptchaField,
718
762
  'submit': this.renderSubmitButton, // Keep this for completeness, but renderSubmitButtonElement will now handle it
719
763
  };
720
764
 
@@ -724,7 +768,7 @@ renderField(type, name, label, validate, attributes, options) {
724
768
  // If the type is 'submit', ensure we use the specific renderSubmitButtonElement
725
769
  // Although, with the filter in renderForm(), this branch for 'submit' type
726
770
  // might not be hit in the primary rendering flow, it's good practice.
727
- return renderMethod.call(this, type, name, label, validate, attributes, options);
771
+ return renderMethod.call(this, type, name, label, validate, attributes, options,subOptions);
728
772
 
729
773
  if (type === 'submit') {
730
774
  return this.renderSubmitButton(type, name, label, validate, attributes, options);
@@ -797,168 +841,173 @@ hasFileInputs(form) {
797
841
 
798
842
 
799
843
  async handleEmailSubmission(formId) {
800
- console.log(`Starting email submission for form ID: ${formId}`);
844
+ console.log(`Starting email submission for form ID: ${formId}`);
801
845
 
802
- const form = document.getElementById(formId);
803
- if (!form) {
804
- console.error(`Form with ID ${formId} not found`);
805
- throw new Error(`Form with ID ${formId} not found`);
806
- }
807
-
808
- // Validate required settings for 'sendTo'
809
- if (!Array.isArray(this.formSettings?.sendTo) || this.formSettings.sendTo.length === 0) {
810
- console.error('formSettings.sendTo must be an array with at least one recipient email');
811
- throw new Error('formSettings.sendTo must be an array with at least one recipient email');
812
- }
846
+ const form = document.getElementById(formId);
847
+ if (!form) {
848
+ console.error(`Form with ID ${formId} not found`);
849
+ throw new Error(`Form with ID ${formId} not found`);
850
+ }
813
851
 
814
- // Serialize form data
815
- const payload = {
816
- formData: {},
817
- metadata: {
818
- recipients: this.formSettings.sendTo,
819
- timestamp: new Date().toISOString(),
820
- },
821
- };
852
+ // Validate required settings for 'sendTo'
853
+ if (!Array.isArray(this.formSettings?.sendTo) || this.formSettings.sendTo.length === 0) {
854
+ console.error('formSettings.sendTo must be an array with at least one recipient email');
855
+ throw new Error('formSettings.sendTo must be an array with at least one recipient email');
856
+ }
822
857
 
823
- let senderName = '';
824
- let senderEmail = '';
825
- let formSubject = '';
826
- let registrantEmail = ''; // Variable to store the registrant's email
858
+ // Serialize form data
859
+ const payload = {
860
+ formData: {},
861
+ metadata: {
862
+ recipients: this.formSettings.sendTo,
863
+ timestamp: new Date().toISOString(),
864
+ },
865
+ };
827
866
 
828
- console.log('Initial payload structure:', JSON.parse(JSON.stringify(payload)));
867
+ let senderName = '';
868
+ let senderEmail = '';
869
+ let formSubject = '';
870
+ let registrantEmail = '';
829
871
 
830
- // Process form fields and find registrant's email
831
- const formData = new FormData(form);
832
- formData.forEach((value, key) => {
833
- console.log(`Processing form field - Key: ${key}, Value: ${value}`);
834
- payload.formData[key] = value;
872
+ console.log('Initial payload structure:', JSON.parse(JSON.stringify(payload)));
835
873
 
836
- const lowerKey = key.toLowerCase();
837
- if (lowerKey.includes('email')) {
838
- senderEmail = value;
839
- }
840
- if (lowerKey.includes('name')) {
841
- senderName = value;
842
- }
843
- if (lowerKey.includes('subject')) {
844
- formSubject = value;
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
- }
851
- });
874
+ // Process form fields and find registrant's email
875
+ const formData = new FormData(form);
876
+ formData.forEach((value, key) => {
877
+ console.log(`Processing form field - Key: ${key}, Value: ${value}`);
878
+ payload.formData[key] = value;
852
879
 
853
- // Determine the email subject with fallback logic
854
- payload.metadata.subject = formSubject ||
855
- this.formSettings.subject ||
856
- 'Message From Contact Form';
880
+ const lowerKey = key.toLowerCase();
881
+ if (lowerKey.includes('email')) {
882
+ senderEmail = value;
883
+ }
884
+ if (lowerKey.includes('name')) {
885
+ senderName = value;
886
+ }
887
+ if (lowerKey.includes('subject')) {
888
+ formSubject = value;
889
+ }
857
890
 
858
- console.log('Determined email subject:', payload.metadata.subject);
891
+ // Check if the current field is the registrant's email
892
+ if (this.formSettings.emailField && key === this.formSettings.emailField) {
893
+ registrantEmail = value;
894
+ }
895
+ });
859
896
 
860
- // Add sender information to metadata
861
- if (senderEmail) {
862
- payload.metadata.sender = senderEmail;
863
- payload.metadata.replyTo = senderName
864
- ? `${senderName} <${senderEmail}>`
865
- : senderEmail;
866
- }
897
+ // Determine the email subject with fallback logic
898
+ payload.metadata.subject = formSubject ||
899
+ this.formSettings.subject ||
900
+ 'Message From Contact Form';
867
901
 
868
- console.log('Payload after form processing:', JSON.parse(JSON.stringify(payload)));
902
+ console.log('Determined email subject:', payload.metadata.subject);
869
903
 
870
- try {
871
- const endpoint = this.formiqueEndpoint || this.formAction;
872
- const method = this.method || 'POST';
904
+ // Add sender information to metadata
905
+ if (senderEmail) {
906
+ payload.metadata.sender = senderEmail;
907
+ payload.metadata.replyTo = senderName
908
+ ? `${senderName} <${senderEmail}>`
909
+ : senderEmail;
910
+ }
873
911
 
874
- console.log(`Preparing to send primary request to: ${endpoint}`);
875
- console.log(`Request method: ${method}`);
876
- console.log('Final payload being sent to recipients:', payload);
912
+ // **NEW:** Add reCAPTCHA secret key to the metadata object
913
+ if (this.formSettings.recaptchaSecretKey) {
914
+ payload.metadata.recaptchaSecretKey = this.formSettings.recaptchaSecretKey;
915
+ }
877
916
 
878
- // Send the first email to the 'sendTo' recipients
879
- const response = await fetch(endpoint, {
880
- method: method,
881
- headers: {
882
- 'Content-Type': 'application/json',
883
- 'X-Formique-Version': '1.0',
884
- },
885
- body: JSON.stringify(payload),
886
- });
917
+ console.log('Payload after form processing:', JSON.parse(JSON.stringify(payload)));
887
918
 
888
- console.log(`Received response for primary email with status: ${response.status}`);
919
+ // ... (The rest of your code remains the same)
920
+ try {
921
+ const endpoint = this.formiqueEndpoint || this.formAction;
922
+ const method = this.method || 'POST';
889
923
 
890
- if (!response.ok) {
891
- const errorData = await response.json().catch(() => ({}));
892
- console.error('API Error Response:', errorData);
893
- throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
894
- }
924
+ console.log(`Preparing to send primary request to: ${endpoint}`);
925
+ console.log(`Request method: ${method}`);
926
+ console.log('Final payload being sent to recipients:', payload);
895
927
 
896
- const data = await response.json();
897
- console.log('Primary API Success Response:', data);
928
+ // Send the first email to the 'sendTo' recipients
929
+ const response = await fetch(endpoint, {
930
+ method: method,
931
+ headers: {
932
+ 'Content-Type': 'application/json',
933
+ 'X-Formique-Version': '1.0',
934
+ },
935
+ body: JSON.stringify(payload),
936
+ });
898
937
 
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.');
938
+ console.log(`Received response for primary email with status: ${response.status}`);
902
939
 
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
- };
940
+ if (!response.ok) {
941
+ const errorData = await response.json().catch(() => ({}));
942
+ console.error('API Error Response:', errorData);
943
+ throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
944
+ }
915
945
 
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',
946
+ const data = await response.json();
947
+ console.log('Primary API Success Response:', data);
948
+
949
+ // ------------------- NEW RSVP LOGIC -------------------
950
+ if (this.formSettings.submitMode === 'rsvp' && registrantEmail && this.formSettings.registrantMessage) {
951
+ console.log('RSVP mode detected. Sending confirmation email to registrant.');
952
+
953
+ // Create a new payload for the registrant
954
+ const rsvpPayload = {
955
+ formData: payload.formData,
956
+ metadata: {
957
+ recipients: [registrantEmail], // Send only to the registrant
958
+ timestamp: new Date().toISOString(),
959
+ subject: this.formSettings.registrantSubject || 'RSVP Confirmation',
960
+ body: this.processDynamicMessage(this.formSettings.registrantMessage, payload.formData),
961
+ sender: this.formSettings.sendFrom || 'noreply@yourdomain.com',
962
+ replyTo: this.formSettings.sendFrom || 'noreply@yourdomain.com',
923
963
  },
924
- body: JSON.stringify(rsvpPayload),
925
- });
964
+ };
965
+
966
+ try {
967
+ console.log('Preparing to send RSVP email. Final payload:', rsvpPayload);
968
+ const rsvpResponse = await fetch(endpoint, {
969
+ method: method,
970
+ headers: {
971
+ 'Content-Type': 'application/json',
972
+ 'X-Formique-Version': '1.0',
973
+ },
974
+ body: JSON.stringify(rsvpPayload),
975
+ });
926
976
 
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
977
+ if (!rsvpResponse.ok) {
978
+ const rsvpErrorData = await rsvpResponse.json().catch(() => ({}));
979
+ console.error('RSVP API Error Response:', rsvpErrorData);
980
+ // Log the error but don't fail the entire submission since the primary email was sent
981
+ console.warn('Failed to send RSVP email to registrant, but primary submission was successful.');
982
+ } else {
983
+ console.log('RSVP email sent successfully to registrant.');
984
+ }
985
+ } catch (rsvpError) {
986
+ console.error('RSVP email submission failed:', rsvpError);
931
987
  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
988
  }
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
989
  }
990
+ // ------------------- END NEW RSVP LOGIC -------------------
991
+
992
+ const successMessage = this.formSettings.successMessage ||
993
+ data.message ||
994
+ 'Your message has been sent successfully!';
995
+ console.log(`Showing success message: ${successMessage}`);
996
+
997
+ this.showSuccessMessage(successMessage);
998
+
999
+ } catch (error) {
1000
+ console.error('Email submission failed:', error);
1001
+ const errorMessage = this.formSettings.errorMessage ||
1002
+ error.message ||
1003
+ 'Failed to send message. Please try again later.';
1004
+ console.log(`Showing error message: ${errorMessage}`);
1005
+ this.showErrorMessage(errorMessage);
1006
+ } finally {
1007
+ document.getElementById("formiqueSpinner").style.display = "none";
939
1008
  }
940
- // ------------------- END NEW RSVP LOGIC -------------------
941
-
942
- const successMessage = this.formSettings.successMessage ||
943
- data.message ||
944
- 'Your message has been sent successfully!';
945
- console.log(`Showing success message: ${successMessage}`);
946
-
947
- this.showSuccessMessage(successMessage);
948
-
949
- } catch (error) {
950
- console.error('Email submission failed:', error);
951
- const errorMessage = this.formSettings.errorMessage ||
952
- error.message ||
953
- 'Failed to send message. Please try again later.';
954
- console.log(`Showing error message: ${errorMessage}`);
955
- this.showErrorMessage(errorMessage);
956
- } finally {
957
- document.getElementById("formiqueSpinner").style.display = "none";
958
- }
959
1009
  }
960
1010
 
961
-
962
1011
  // Add this method to your Formique class
963
1012
  processDynamicMessage(message, formData) {
964
1013
  let processedMessage = message;
@@ -985,84 +1034,145 @@ validateEmail(email) {
985
1034
  }
986
1035
 
987
1036
 
1037
+ attachSubmitListener() {
1038
+ this.formElement.addEventListener('submit', (e) => {
1039
+ // Find the reCAPTCHA field in the form schema.
1040
+ const recaptchaField = this.formSchema.find(field => field[0] === 'recaptcha');
1041
+
1042
+ // If a reCAPTCHA field is present, check its state.
1043
+ if (recaptchaField) {
1044
+ const recaptchaToken = grecaptcha.getResponse();
988
1045
 
989
- // Method to handle on-page form submissions
990
- handleOnPageFormSubmission(formId) {
991
- const formElement = document.getElementById(formId);
992
- //console.warn("handler fired also",formId,this.method,this.formAction);
993
-
994
- if (formElement) {
995
- // Gather form data
996
- const formData = new FormData(formElement);
997
-
998
- // Submit form data using fetch to a test endpoint
999
- fetch(this.formAction, {
1000
- method: this.method,
1001
- body: formData
1002
- })
1003
- .then(response => response.json())
1004
- .then(data => {
1005
- console.log('Success:', data);
1006
- // Handle the response data here, e.g., show a success message
1007
-
1008
- // Get the form container element
1009
- const formContainer = document.getElementById(this.formContainerId);
1046
+ if (!recaptchaToken) {
1047
+ // Prevent the default form submission.
1048
+ e.preventDefault();
1049
+
1050
+ // Display the alert and handle UI.
1051
+ alert('Please verify that you are not a robot.');
1052
+ document.getElementById("formiqueSpinner").style.display = "none";
1053
+ return;
1054
+ }
1055
+ }
1010
1056
 
1011
- if (this.redirect && this.redirectURL) {
1012
- window.location.href = this.redirectURL;
1013
- }
1057
+ // If reCAPTCHA is valid or not present, proceed with submission logic.
1058
+ this.handleOnPageFormSubmission(e);
1059
+ });
1060
+ }
1014
1061
 
1015
1062
 
1016
- if (formContainer) {
1017
- // Create a new div element for the success message
1018
- const successMessageDiv = document.createElement('div');
1019
1063
 
1020
- // Add custom classes for styling the success message
1021
- successMessageDiv.classList.add('success-message', 'message-container');
1064
+ // Method to handle on-page form submissions
1065
+ handleOnPageFormSubmission(formId) {
1066
+ const formElement = document.getElementById(formId);
1067
+
1068
+ if (formElement) {
1069
+ // Intercept the form's native submit event
1070
+ formElement.addEventListener('submit', (e) => {
1071
+ // Find the reCAPTCHA field in the form schema.
1072
+ const recaptchaField = this.formSchema.find(field => field[0] === 'recaptcha');
1073
+
1074
+ // If a reCAPTCHA field exists, perform client-side validation.
1075
+ if (recaptchaField) {
1076
+ const recaptchaToken = grecaptcha.getResponse();
1077
+
1078
+ // If the token is empty, the reCAPTCHA challenge has not been completed.
1079
+ if (!recaptchaToken) {
1080
+ e.preventDefault(); // <-- The crucial line to stop default form submission
1081
+
1082
+ // Hide the spinner to indicate the submission was halted.
1083
+ document.getElementById("formiqueSpinner").style.display = "none";
1084
+
1085
+ // Display a user-friendly error message.
1086
+ alert('Please verify that you are not a robot.');
1087
+
1088
+ // Stop the function's execution to prevent form submission.
1089
+ return;
1090
+ }
1091
+ }
1022
1092
 
1023
- // Set the success message text
1024
- successMessageDiv.innerHTML = this.formSettings.successMessage || 'Your details have been successfully submitted!';
1093
+ // At this point, reCAPTCHA is validated (or not present), so we can proceed with the fetch request.
1094
+ // Show the spinner as submission is now beginning.
1095
+ document.getElementById("formiqueSpinner").style.display = "block";
1025
1096
 
1026
- // Replace the content of the form container with the success message div
1027
- formContainer.innerHTML = ''; // Clear existing content
1028
- formContainer.appendChild(successMessageDiv); // Append the new success message div
1029
- }
1097
+ // Gather form data.
1098
+ const formData = {};
1099
+ new FormData(formElement).forEach((value, key) => {
1100
+ formData[key] = value;
1101
+ });
1030
1102
 
1103
+ console.log("Setting Object",this.formSettings);
1031
1104
 
1032
- })
1033
- .catch(error => {
1034
- console.error('Error:', error);
1105
+ // Create the full payload with formData and metadata, including the secret key.
1106
+ const payload = {
1107
+ formData: formData,
1108
+ metadata: {
1109
+ ...this.formSettings, // Include all formSettings
1110
+ // Other metadata like recipients and sender will be included from this.formSettings
1111
+ }
1112
+ };
1113
+
1114
+ // Submit form data using fetch to the endpoint.
1115
+ fetch(this.formAction, {
1116
+ method: this.method,
1117
+ headers: {
1118
+ 'Content-Type': 'application/json' // Important: set the content type
1119
+ },
1120
+ body: JSON.stringify(payload) // Send the combined payload as JSON
1121
+ })
1122
+ .then(response => {
1123
+ // Check if the response status is OK (200-299).
1124
+ if (!response.ok) {
1125
+ return response.json().then(errorData => {
1126
+ throw new Error(errorData.error || `HTTP error! Status: ${response.status}`);
1127
+ });
1128
+ }
1129
+ return response.json();
1130
+ })
1131
+ .then(data => {
1132
+ console.log('Success:', data);
1133
+
1134
+ // Hide the spinner on success.
1135
+ document.getElementById("formiqueSpinner").style.display = "none";
1136
+
1137
+ const formContainer = document.getElementById(this.formContainerId);
1138
+ if (this.redirect && this.redirectURL) {
1139
+ window.location.href = this.redirectURL;
1140
+ }
1141
+ if (formContainer) {
1142
+ const successMessageDiv = document.createElement('div');
1143
+ successMessageDiv.classList.add('success-message', 'message-container');
1144
+ successMessageDiv.innerHTML = this.formSettings.successMessage || 'Your details have been successfully submitted!';
1145
+ formContainer.innerHTML = '';
1146
+ formContainer.appendChild(successMessageDiv);
1147
+ }
1148
+ })
1149
+ .catch(error => {
1150
+ console.error('Error:', error);
1151
+
1152
+ // Hide the spinner on error.
1153
+ document.getElementById("formiqueSpinner").style.display = "none";
1154
+
1155
+ const formContainer = document.getElementById(this.formContainerId);
1156
+ if (formContainer) {
1157
+ let existingErrorDiv = formContainer.querySelector('.error-message');
1158
+ if (existingErrorDiv) {
1159
+ existingErrorDiv.remove();
1160
+ }
1161
+ const errorMessageDiv = document.createElement('div');
1162
+ errorMessageDiv.classList.add('error-message', 'message-container');
1163
+ let err = this.formSettings.errorMessage || 'An error occurred while submitting the form. Please try again.';
1164
+ err = `${err}<br/>Details: ${error.message}`;
1165
+ errorMessageDiv.innerHTML = err;
1166
+ formContainer.appendChild(errorMessageDiv);
1167
+ }
1168
+ });
1035
1169
 
1036
- const formContainer = document.getElementById(this.formContainerId);
1037
- if (formContainer) {
1038
- // Check if an error message div already exists and remove it
1039
- let existingErrorDiv = formContainer.querySelector('.error-message');
1040
- if (existingErrorDiv) {
1041
- existingErrorDiv.remove();
1170
+ // Return false to ensure no other default action is taken, especially for legacy browsers.
1171
+ return false;
1172
+ });
1042
1173
  }
1043
-
1044
- // Create a new div element for the error message
1045
- const errorMessageDiv = document.createElement('div');
1046
-
1047
- // Add custom classes for styling the error message
1048
- errorMessageDiv.classList.add('error-message', 'message-container');
1049
-
1050
- // Set the error message text
1051
- let err = this.formSettings.errorMessage || 'An error occurred while submitting the form. Please try again.';
1052
- err = `${err}<br/>Details: ${error.message}`;
1053
- errorMessageDiv.innerHTML = err;
1054
-
1055
- // Append the new error message div to the form container
1056
- formContainer.appendChild(errorMessageDiv);
1057
- }
1058
- });
1059
-
1060
- }
1061
1174
  }
1062
1175
 
1063
-
1064
-
1065
-
1066
1176
  // text field rendering
1067
1177
  renderTextField(type, name, label, validate, attributes) {
1068
1178
  const textInputValidationAttributes = [
@@ -3345,6 +3455,8 @@ renderTextAreaField(type, name, label, validate, attributes) {
3345
3455
 
3346
3456
  renderRadioField(type, name, label, validate, attributes, options) {
3347
3457
  // Define valid validation attributes for radio fields
3458
+ console.log("RADIO DEBUG - options:", JSON.stringify(options, null, 2));
3459
+
3348
3460
  const radioValidationAttributes = ['required'];
3349
3461
 
3350
3462
  // Construct validation attributes
@@ -3376,15 +3488,15 @@ renderRadioField(type, name, label, validate, attributes, options) {
3376
3488
  // Handle the binding syntax
3377
3489
  let bindingDirective = '';
3378
3490
  if (attributes.binding) {
3379
- if (attributes.binding === 'bind:value' && name) {
3380
- bindingDirective = ` bind:value="${name}"\n`;
3381
- } else if (attributes.binding.startsWith('::') && name) {
3382
- bindingDirective = ` bind:value="${name}"\n`;
3383
- } else if (attributes.binding && !name) {
3384
- console.log(`\x1b[31m%s\x1b[0m`, `You cannot set binding value when there is no name attribute defined in ${name} ${type} field.`);
3385
- return;
3491
+ if (attributes.binding === 'bind:value' && name) {
3492
+ bindingDirective = ` bind:value="${name}"\n`;
3493
+ } else if (attributes.binding.startsWith('::') && name) {
3494
+ bindingDirective = ` bind:value="${name}"\n`;
3495
+ } else if (attributes.binding && !name) {
3496
+ console.log(`\x1b[31m%s\x1b[0m`, `You cannot set binding value when there is no name attribute defined in ${name} ${type} field.`);
3497
+ return;
3498
+ }
3386
3499
  }
3387
- }
3388
3500
 
3389
3501
  // Define attributes for the radio inputs
3390
3502
  let id = attributes.id || name;
@@ -3392,7 +3504,8 @@ renderRadioField(type, name, label, validate, attributes, options) {
3392
3504
  // Construct additional attributes dynamically
3393
3505
  let additionalAttrs = '';
3394
3506
  for (const [key, value] of Object.entries(attributes)) {
3395
- if (key !== 'id' && key !== 'class' && key !== 'dependsOn' && key !== 'dependents' && value !== undefined) { if (key.startsWith('on')) {
3507
+ if (key !== 'id' && key !== 'class' && key !== 'dependsOn' && key !== 'dependents' && value !== undefined) {
3508
+ if (key.startsWith('on')) {
3396
3509
  // Handle event attributes
3397
3510
  const eventValue = value.endsWith('()') ? value.slice(0, -2) : value;
3398
3511
  additionalAttrs += ` @${key.replace(/^on/, '')}={${eventValue}}\n`;
@@ -3410,10 +3523,28 @@ renderRadioField(type, name, label, validate, attributes, options) {
3410
3523
 
3411
3524
  let inputClass = attributes.class || this.inputClass;
3412
3525
 
3526
+ // Determine which option should be selected
3527
+ let selectedValue = null;
3528
+
3529
+ // Check options array for selected: true
3530
+ if (options && options.length) {
3531
+ const selectedOption = options.find(opt => opt.selected === true);
3532
+ console.log("RADIO DEBUG - selectedOption:", selectedOption);
3533
+ if (selectedOption) {
3534
+ selectedValue = selectedOption.value;
3535
+ console.log("RADIO DEBUG - selectedValue:", selectedValue);
3536
+ }
3537
+ }
3538
+
3413
3539
  // Construct radio button HTML based on options
3414
3540
  let optionsHTML = '';
3415
3541
  if (options && options.length) {
3416
3542
  optionsHTML = options.map((option) => {
3543
+ // Check if this option should be selected
3544
+ const isSelected = (option.value === selectedValue);
3545
+ console.log("RADIO DEBUG - option:", option.value, "isSelected:", isSelected);
3546
+ const checkedAttr = isSelected ? ' checked' : '';
3547
+
3417
3548
  return `
3418
3549
  <div>
3419
3550
  <input
@@ -3425,6 +3556,7 @@ renderRadioField(type, name, label, validate, attributes, options) {
3425
3556
  ${attributes.id ? `id="${id}-${option.value}"` : `id="${id}-${option.value}"`}
3426
3557
  class="${inputClass}"
3427
3558
  ${validationAttrs}
3559
+ ${checkedAttr}
3428
3560
  />
3429
3561
  <label
3430
3562
  for="${attributes.id ? `${id}-${option.value}` : `${id}-${option.value}`}">
@@ -3459,13 +3591,13 @@ renderRadioField(type, name, label, validate, attributes, options) {
3459
3591
  return `\n${match}\n`;
3460
3592
  }).replace(/\n\s*\n/g, '\n'); // Remove extra blank lines
3461
3593
 
3462
- //return formattedHtml;
3463
- this.formMarkUp +=formattedHtml;
3594
+ this.formMarkUp += formattedHtml;
3464
3595
  }
3465
3596
 
3466
-
3467
3597
  renderCheckboxField(type, name, label, validate, attributes, options) {
3468
3598
  // Define valid validation attributes for checkbox fields
3599
+ console.log("CHECKBOX DEBUG - options:", JSON.stringify(options, null, 2));
3600
+
3469
3601
  const checkboxValidationAttributes = ['required'];
3470
3602
 
3471
3603
  // Construct validation attributes
@@ -3485,12 +3617,12 @@ renderCheckboxField(type, name, label, validate, attributes, options) {
3485
3617
  // Handle the binding syntax
3486
3618
  let bindingDirective = '';
3487
3619
  if (attributes.binding) {
3488
- if (attributes.binding === 'bind:checked') {
3489
- bindingDirective = ` bind:checked="${name}"\n`;
3490
- } else if (attributes.binding.startsWith('::')) {
3491
- bindingDirective = ` bind:checked="${name}"\n`;
3620
+ if (attributes.binding === 'bind:checked') {
3621
+ bindingDirective = ` bind:checked="${name}"\n`;
3622
+ } else if (attributes.binding.startsWith('::')) {
3623
+ bindingDirective = ` bind:checked="${name}"\n`;
3624
+ }
3492
3625
  }
3493
- }
3494
3626
 
3495
3627
  // Define attributes for the checkbox inputs
3496
3628
  let id = attributes.id || name;
@@ -3498,7 +3630,8 @@ renderCheckboxField(type, name, label, validate, attributes, options) {
3498
3630
  // Handle additional attributes
3499
3631
  let additionalAttrs = '';
3500
3632
  for (const [key, value] of Object.entries(attributes)) {
3501
- if (key !== 'id' && key !== 'class' && key !== 'dependsOn' && key !== 'dependents' && value !== undefined) { if (key.startsWith('on')) {
3633
+ if (key !== 'id' && key !== 'class' && key !== 'dependsOn' && key !== 'dependents' && value !== undefined) {
3634
+ if (key.startsWith('on')) {
3502
3635
  // Handle event attributes
3503
3636
  const eventValue = value.endsWith('()') ? value.slice(0, -2) : value;
3504
3637
  additionalAttrs += ` @${key.replace(/^on/, '')}={${eventValue}}\n`;
@@ -3514,30 +3647,45 @@ renderCheckboxField(type, name, label, validate, attributes, options) {
3514
3647
  }
3515
3648
  }
3516
3649
 
3517
-
3518
3650
  let inputClass;
3519
3651
  if ('class' in attributes) {
3520
3652
  inputClass = attributes.class;
3521
3653
  } else {
3522
- inputClass = this.inputClass;
3654
+ inputClass = this.inputClass;
3655
+ }
3656
+
3657
+ // Determine which options should be checked
3658
+ const checkedValues = [];
3659
+ if (options && options.length) {
3660
+ options.forEach(option => {
3661
+ if (option.checked === true || option.selected === true) {
3662
+ checkedValues.push(option.value);
3663
+ }
3664
+ });
3523
3665
  }
3666
+ console.log("CHECKBOX DEBUG - checkedValues:", checkedValues);
3524
3667
 
3525
3668
  // Construct checkbox HTML based on options
3526
3669
  let optionsHTML = '';
3527
3670
  if (Array.isArray(options)) {
3528
3671
  optionsHTML = options.map((option) => {
3529
3672
  const optionId = `${id}-${option.value}`;
3673
+ const isChecked = checkedValues.includes(option.value);
3674
+ console.log("CHECKBOX DEBUG - option:", option.value, "isChecked:", isChecked);
3675
+ const checkedAttr = isChecked ? ' checked' : '';
3676
+
3530
3677
  return `
3531
3678
  <div>
3532
3679
  <input
3533
- type="checkbox"
3534
- name="${name}"
3535
- value="${option.value}"${bindingDirective} ${additionalAttrs}
3680
+ type="checkbox"
3681
+ name="${name}"
3682
+ value="${option.value}"${bindingDirective} ${additionalAttrs}
3536
3683
  ${attributes.id ? `id="${optionId}"` : `id="${optionId}"`}
3537
3684
  class="${inputClass}"
3685
+ ${checkedAttr}
3538
3686
  />
3539
3687
  <label
3540
- for="${optionId}">
3688
+ for="${optionId}">
3541
3689
  ${option.label}
3542
3690
  </label>
3543
3691
  </div>
@@ -3570,8 +3718,7 @@ renderCheckboxField(type, name, label, validate, attributes, options) {
3570
3718
  return `\n${match}\n`;
3571
3719
  }).replace(/\n\s*\n/g, '\n'); // Remove extra blank lines
3572
3720
 
3573
- //return formattedHtml;
3574
- this.formMarkUp +=formattedHtml;
3721
+ this.formMarkUp += formattedHtml;
3575
3722
  }
3576
3723
 
3577
3724
 
@@ -3579,239 +3726,240 @@ renderCheckboxField(type, name, label, validate, attributes, options) {
3579
3726
  /* DYNAMIC SINGLE SELECT BLOCK */
3580
3727
 
3581
3728
  // Function to render the dynamic select field and update based on user selection
3582
- renderDynamicSingleSelectField(type, name, label, validate, attributes, options) {
3583
-
3584
- // Step 1: Transform the data into an array of objects
3585
- const mainCategoryOptions = options.flat().map(item => {
3586
- // Check if any option has selected: true
3587
- const selected = item.options.some(option => option.selected === true);
3588
-
3589
- // Create a transformed object
3590
- return {
3591
- value: item.id,
3592
- label: item.label,
3593
- ...(selected && { selected: true }) // Conditionally add selected: true
3594
- };
3595
- });
3596
-
3597
- const subCategoriesOptions=options;
3598
- const mode='dynamicSingleSelect';
3599
- this.renderSingleSelectField(type, name, label, validate, attributes, mainCategoryOptions, subCategoriesOptions, mode);
3729
+ renderDynamicSingleSelectField(type, name, label, validate, attributes, options, subCategoriesOptions) {
3730
+
3731
+ // Step 1: Transform the data into an array of objects
3732
+ const mainCategoryOptions = options.map(item => {
3733
+ // CRITICAL GUARD FIX: Check for item.options existence to prevent crash
3734
+ const selected = item.options
3735
+ ? item.options.some(option => option.selected === true)
3736
+ : item.selected === true;
3737
+
3738
+ // Create a transformed object
3739
+ return {
3740
+ value: item.value,
3741
+ label: item.label,
3742
+ ...(selected && { selected: true })
3743
+ };
3744
+ });
3600
3745
 
3601
- }
3746
+ const mode = 'dynamicSingleSelect';
3747
+
3748
+ // Pass the main options and the nested sub categories options to the single select renderer
3749
+ this.renderSingleSelectField(type, name, label, validate, attributes, mainCategoryOptions, subCategoriesOptions, mode);
3750
+ }
3751
+
3602
3752
 
3603
3753
 
3604
3754
  renderSingleSelectField(type, name, label, validate, attributes, options, subCategoriesOptions, mode) {
3605
3755
 
3606
- console.log("Within renderSingleSelectField");
3607
- // Define valid validation attributes for select fields
3608
- const selectValidationAttributes = ['required'];
3756
+ // Define valid validation attributes for select fields
3757
+ const selectValidationAttributes = ['required'];
3609
3758
 
3610
- // Construct validation attributes
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
3614
- if (validate) {
3615
- Object.entries(validate).forEach(([key, value]) => {
3616
- if (selectValidationAttributes.includes(key)) {
3617
- if (key === 'required') {
3618
- validationAttrs += `${key} `;
3619
- originalRequired = true; // Mark that it was originally required
3620
- }
3621
- } else {
3622
- console.warn(`\x1b[31mUnsupported validation attribute '${key}' for field '${name}' of type '${type}'.\x1b[0m`);
3623
- }
3624
- });
3625
- }
3626
-
3627
- // Handle the binding syntax
3628
- let bindingDirective = '';
3629
- if (attributes.binding) {
3630
- if (typeof attributes.binding === 'string' && attributes.binding.startsWith('::')) {
3631
- bindingDirective = ` bind:value="${name}" `;
3759
+ // Construct validation attributes
3760
+ let validationAttrs = '';
3761
+ // Store original required state for the main select
3762
+ let originalRequired = false; // <--- This variable tracks if the main select was originally required
3763
+ if (validate) {
3764
+ Object.entries(validate).forEach(([key, value]) => {
3765
+ if (selectValidationAttributes.includes(key)) {
3766
+ if (key === 'required') {
3767
+ validationAttrs += `${key} `;
3768
+ originalRequired = true; // Mark that it was originally required
3632
3769
  }
3770
+ } else {
3771
+ // Removed console.warn
3772
+ }
3773
+ });
3774
+ }
3775
+
3776
+ // Handle the binding syntax
3777
+ let bindingDirective = '';
3778
+ if (attributes.binding) {
3779
+ if (typeof attributes.binding === 'string' && attributes.binding.startsWith('::')) {
3780
+ bindingDirective = ` bind:value="${name}" `;
3633
3781
  }
3782
+ }
3634
3783
 
3635
- // Define attributes for the select field
3636
- let id = attributes.id || name;
3637
- let dimensionAttrs = ''; // No dimension attributes applicable for select fields
3784
+ // Define attributes for the select field
3785
+ let id = attributes.id || name;
3786
+ let dimensionAttrs = ''; // No dimension attributes applicable for select fields
3638
3787
 
3639
- // Handle additional attributes
3640
- let additionalAttrs = '';
3641
- for (const [key, value] of Object.entries(attributes)) {
3642
- if (key !== 'id' && key !== 'class' && key !== 'dependsOn' && key !== 'dependents' && value !== undefined) {
3643
- if (key.startsWith('on')) {
3644
- // Handle event attributes
3645
- const eventValue = value.endsWith('()') ? value.slice(0, -2) : value;
3646
- additionalAttrs += ` @${key.replace(/^on/, '')}={${eventValue}}\n`;
3647
- } else {
3648
- // Handle boolean attributes
3649
- if (value === true) {
3650
- additionalAttrs += ` ${key.replace(/_/g, '-')}\n`;
3651
- } else if (value !== false) {
3652
- // Convert underscores to hyphens and set the attribute
3653
- additionalAttrs += ` ${key.replace(/_/g, '-')}="${value}"\n`;
3654
- }
3655
- }
3788
+ // Handle additional attributes
3789
+ let additionalAttrs = '';
3790
+ for (const [key, value] of Object.entries(attributes)) {
3791
+ if (key !== 'id' && key !== 'class' && key !== 'dependsOn' && key !== 'dependents' && value !== undefined) {
3792
+ if (key.startsWith('on')) {
3793
+ // Handle event attributes
3794
+ const eventValue = value.endsWith('()') ? value.slice(0, -2) : value;
3795
+ additionalAttrs += ` @${key.replace(/^on/, '')}={${eventValue}}\n`;
3796
+ } else {
3797
+ // Handle boolean attributes
3798
+ if (value === true) {
3799
+ additionalAttrs += ` ${key.replace(/_/g, '-')}\n`;
3800
+ } else if (value !== false) {
3801
+ // Convert underscores to hyphens and set the attribute
3802
+ additionalAttrs += ` ${key.replace(/_/g, '-')}="${value}"\n`;
3656
3803
  }
3804
+ }
3657
3805
  }
3806
+ }
3658
3807
 
3659
- // Construct select options HTML based on options
3660
- let selectHTML = '';
3661
- if (Array.isArray(options)) {
3662
- // Add a default option
3663
- selectHTML += `
3664
- <option value="">Choose an option</option>
3665
- `;
3808
+ // Construct select options HTML based on options
3809
+ let selectHTML = '';
3810
+ if (Array.isArray(options)) {
3811
+ // Add a default option
3812
+ selectHTML += `
3813
+ <option value="">Choose an option</option>
3814
+ `;
3666
3815
 
3667
- // Add the provided options
3668
- selectHTML += options.map((option) => {
3669
- const isSelected = option.selected ? ' selected' : '';
3670
- return `
3671
- <option value="${option.value}"${isSelected}>${option.label}</option>
3672
- `;
3673
- }).join('');
3674
- }
3816
+ // Add the provided options
3817
+ selectHTML += options.map((option) => {
3818
+ const isSelected = option.selected ? ' selected' : '';
3819
+ return `
3820
+ <option value="${option.value}"${isSelected}>${option.label}</option>
3821
+ `;
3822
+ }).join('');
3823
+ }
3675
3824
 
3676
- let inputClass = attributes.class || this.inputClass;
3825
+ let inputClass = attributes.class || this.inputClass;
3677
3826
 
3678
- // Remove `onchange` from HTML; it will be handled by JavaScript event listeners
3679
- const onchangeAttr = ''; // <--- Ensure this is an empty string
3827
+ // Remove `onchange` from HTML; it will be handled by JavaScript event listeners
3828
+ const onchangeAttr = ''; // <--- Ensure this is an empty string
3680
3829
 
3681
- let labelDisplay;
3682
- let rawLabel;
3830
+ let labelDisplay;
3831
+ let rawLabel;
3683
3832
 
3684
- if (mode === 'dynamicSingleSelect' && subCategoriesOptions) {
3685
- if (label.includes('-')) {
3686
- const [mainCategoryLabel] = label.split('-');
3687
- labelDisplay = mainCategoryLabel;
3688
- rawLabel = label;
3689
- } else {
3690
- labelDisplay = label;
3691
- rawLabel = label;
3692
- }
3833
+ if (mode === 'dynamicSingleSelect' && subCategoriesOptions) {
3834
+ if (label.includes('-')) {
3835
+ const [mainCategoryLabel] = label.split('-');
3836
+ labelDisplay = mainCategoryLabel;
3837
+ rawLabel = label;
3693
3838
  } else {
3694
- labelDisplay = label;
3839
+ labelDisplay = label;
3840
+ rawLabel = label;
3695
3841
  }
3842
+ } else {
3843
+ labelDisplay = label;
3844
+ }
3696
3845
 
3697
-
3698
- // Construct the final HTML string for the main select
3699
- let formHTML = `
3700
- <fieldset class="${this.selectGroupClass}" id="${id + '-block'}">
3701
- <legend>${labelDisplay}
3702
- ${validationAttrs.includes('required') && this.formSettings.requiredFieldIndicator ? this.formSettings.asteriskHtml : ''}
3703
- </legend>
3704
- <label for="${id}"> Select ${labelDisplay}
3705
- <select name="${name}"
3706
- ${bindingDirective}
3707
- ${dimensionAttrs}
3708
- id="${id}"
3709
- class="${inputClass}"
3710
- ${additionalAttrs}
3711
- ${validationAttrs}
3712
- data-original-required="${originalRequired}" >
3713
- ${selectHTML}
3714
- </select>
3715
- </fieldset>
3846
+ // Construct the final HTML string for the main select
3847
+ let formHTML = `
3848
+ <fieldset class="${this.selectGroupClass}" id="${id + '-block'}">
3849
+ <legend>${labelDisplay}
3850
+ ${validationAttrs.includes('required') && this.formSettings.requiredFieldIndicator ? this.formSettings.asteriskHtml : ''}
3851
+ </legend>
3852
+ <label for="${id}"> Select ${labelDisplay}
3853
+ <select name="${name}"
3854
+ ${bindingDirective}
3855
+ ${dimensionAttrs}
3856
+ id="${id}"
3857
+ class="${inputClass}"
3858
+ ${additionalAttrs}
3859
+ ${validationAttrs}
3860
+ data-original-required="${originalRequired}" >
3861
+ ${selectHTML}
3862
+ </select>
3863
+ </fieldset>
3716
3864
  `.replace(/^\s*\n/gm, '').trim();
3717
3865
 
3866
+ // FIXED: Apply vertical layout to the <select> element and its children
3867
+ // Only split on actual attribute boundaries, not within attribute values
3868
+ let formattedHtml = formHTML.replace(/<select\s+([^>]*)>([\s\S]*?)<\/select>/g, (match, p1, p2) => {
3869
+ // Use regex to match complete attribute="value" pairs
3870
+ const attributes = p1.match(/(\w+(?:-\w+)*=("[^"]*"|'[^']*'|\w+)|[^=\s]+(?!\s*=))/g) || [];
3871
+ const formattedAttributes = attributes.map(attr => ` ${attr}`).join('\n');
3872
+ return `<select\n${formattedAttributes}\n>\n${p2.trim()}\n</select>`;
3873
+ });
3718
3874
 
3719
- // Apply vertical layout to the <select> element and its children
3720
- let formattedHtml = formHTML.replace(/<select\s+([^>]*)>([\s\S]*?)<\/select>/g, (match, p1, p2) => {
3721
- // Reformat attributes into a vertical layout
3722
- const attributes = p1.trim().split(/\s+/).map(attr => ` ${attr}`).join('\n');
3723
- return `<select\n${attributes}\n>\n${p2.trim()}\n</select>`;
3724
- });
3875
+ // Ensure the <fieldset> block starts on a new line and remove extra blank lines
3876
+ formattedHtml = formattedHtml.replace(/(<fieldset\s+[^>]*>)/g, (match) => {
3877
+ // Ensure <fieldset> starts on a new line
3878
+ return `\n${match}\n`;
3879
+ }).replace(/\n\s*\n/g, '\n'); // Remove extra blank lines
3725
3880
 
3726
- // Ensure the <fieldset> block starts on a new line and remove extra blank lines
3727
- formattedHtml = formattedHtml.replace(/(<fieldset\s+[^>]*>)/g, (match) => {
3728
- // Ensure <fieldset> starts on a new line
3729
- return `\n${match}\n`;
3730
- }).replace(/\n\s*\n/g, '\n'); // Remove extra blank lines
3881
+ this.formMarkUp+=formattedHtml;
3731
3882
 
3732
- this.formMarkUp+=formattedHtml;
3733
3883
 
3884
+ /* dynamicSingleSelect - Sub-Category Generation Block */
3734
3885
 
3735
- /* dynamicSingleSelect - Sub-Category Generation Block */
3886
+ if (mode && mode ==='dynamicSingleSelect' && subCategoriesOptions) {
3736
3887
 
3737
- if (mode && mode ==='dynamicSingleSelect' && subCategoriesOptions) {
3888
+ const categoryId = attributes.id || name; // This is the ID of the main dynamic select ('languages')
3738
3889
 
3739
- const categoryId = attributes.id || name; // This is the ID of the main dynamic select ('languages')
3890
+ subCategoriesOptions.forEach((subCategory) => {
3891
+ const { id, label, options: subOptions } = subCategory; // Renamed 'options' to 'subOptions' to avoid conflict
3740
3892
 
3741
- subCategoriesOptions.forEach(subCategory => {
3742
- const { id, label, options: subOptions } = subCategory; // Renamed 'options' to 'subOptions' to avoid conflict
3893
+ // IMPORTANT: Sub-category selects are *initially hidden*
3894
+ // Therefore, by default, they are NOT required until they are revealed.
3895
+ let isSubCategoryRequired = false; // Default to false as they are hidden
3896
+ const subCategoryValidationAttrs = ''; // No direct 'required' in HTML initially
3743
3897
 
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
3898
+ // Build the select options HTML for sub-category
3899
+ const subSelectHTML = subOptions.map(option => {
3900
+ const isSelected = option.selected ? ' selected' : '';
3901
+ return `
3902
+ <option value="${option.value}"${isSelected}>${option.label}</option>
3903
+ `;
3904
+ }).join('');
3751
3905
 
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('');
3759
3906
 
3907
+ let subCategoryLabel;
3908
+
3909
+ if (rawLabel.includes('-')) {
3910
+ subCategoryLabel = rawLabel.split('-')?.[1] + ' Options';
3911
+ } else {
3912
+ subCategoryLabel = 'options';
3913
+ }
3760
3914
 
3761
- let subCategoryLabel;
3762
- console.log('Label (rawLabel for sub-category):', rawLabel); // Debug log
3915
+ let optionsLabel;
3916
+ if (subCategoryLabel !== 'options') {
3917
+ optionsLabel = rawLabel.split('-')?.[1] + ' Option';
3918
+ } else {
3919
+ optionsLabel = subCategoryLabel;
3920
+ }
3763
3921
 
3764
- if (rawLabel.includes('-')) {
3765
- subCategoryLabel = rawLabel.split('-')?.[1] + ' Options';
3766
- } else {
3767
- subCategoryLabel = 'options';
3768
- }
3769
3922
 
3770
- let optionsLabel;
3771
- if (subCategoryLabel !== 'options') {
3772
- optionsLabel = rawLabel.split('-')?.[1] + ' Option';
3773
- } else {
3774
- optionsLabel = subCategoryLabel;
3775
- }
3923
+ // Create the HTML for the sub-category fieldset and select elements
3924
+ // Added a class based on the main select's ID for easy grouping/selection
3925
+ let subFormHTML = `
3926
+ <fieldset class="${this.selectGroupClass} ${categoryId}" id="${id}" style="display: none;"> <legend>${label} ${subCategoryLabel} ${isSubCategoryRequired && this.formSettings.requiredFieldIndicator ? this.formSettings.asteriskHtml : ''}
3927
+ </legend>
3928
+ <label for="${id}"> Select ${label} ${optionsLabel}
3929
+ </label>
3930
+ <select name="${id}"
3931
+ ${bindingDirective}
3932
+ ${dimensionAttrs}
3933
+ id="${id}"
3934
+ class="${inputClass}"
3935
+ ${additionalAttrs}
3936
+ ${subCategoryValidationAttrs}
3937
+ data-original-required="${isSubCategoryRequired}" >
3938
+ <option value="">Choose an option</option>
3939
+ ${subSelectHTML}
3940
+ </select>
3941
+ </fieldset>
3942
+ `.replace(/^\s*\n/gm, '').trim();
3943
+
3944
+ // FIXED: Apply the same corrected formatting to sub-category selects
3945
+ subFormHTML = subFormHTML.replace(/<select\s+([^>]*)>([\s\S]*?)<\/select>/g, (match, p1, p2) => {
3946
+ const attributes = p1.match(/(\w+(?:-\w+)*=("[^"]*"|'[^']*'|\w+)|[^=\s]+(?!\s*=))/g) || [];
3947
+ const formattedAttributes = attributes.map(attr => ` ${attr}`).join('\n');
3948
+ return `<select\n${formattedAttributes}\n>\n${p2.trim()}\n</select>`;
3949
+ });
3776
3950
 
3951
+ // Ensure the <fieldset> block starts on a new line and remove extra blank lines
3952
+ subFormHTML = subFormHTML.replace(/(<fieldset\s+[^>]*>)/g, (match) => {
3953
+ return `\n${match}\n`;
3954
+ }).replace(/\n\s*\n/g, '\n');
3777
3955
 
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
- });
3956
+ // Append the generated HTML to formMarkUp
3957
+ this.formMarkUp += subFormHTML;
3958
+ });
3959
+ }
3960
+ }
3804
3961
 
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');
3809
3962
 
3810
- // Append the generated HTML to formMarkUp
3811
- this.formMarkUp += subFormHTML;
3812
- });
3813
- }
3814
- }
3815
3963
 
3816
3964
  renderMultipleSelectField(type, name, label, validate, attributes, options) {
3817
3965
  // Define valid validation attributes for multiple select fields
@@ -4011,6 +4159,24 @@ renderRangeField(type, name, label, validate, attributes) {
4011
4159
  }
4012
4160
 
4013
4161
 
4162
+ renderRecaptchaField(type, name, label, validate, attributes = {}) {
4163
+ const fieldId = attributes.id || name;
4164
+ const siteKey = attributes.siteKey;
4165
+ // Check for the presence of a siteKey
4166
+ if (!siteKey) {
4167
+ console.error('reCAPTCHA siteKey is missing from the field attributes.');
4168
+ return ''; // Do not render if the key is missing
4169
+ }
4170
+
4171
+ return `
4172
+ <div class="${this.divClass}" id="${fieldId}-block">
4173
+ <label for="${fieldId}">${label}</label>
4174
+ <div class="g-recaptcha" id="${fieldId}" data-sitekey="${siteKey}"></div>
4175
+ </div>
4176
+ `;
4177
+ }
4178
+
4179
+
4014
4180
 
4015
4181
  /*
4016
4182
  renderRangeField(type, name, label, validate, attributes) {
@@ -4192,14 +4358,3 @@ const spinner = `<div id="formiqueSpinner" style="display: flex; align-items: ce
4192
4358
 
4193
4359
 
4194
4360
  export default Formique;
4195
-
4196
-
4197
-
4198
-
4199
-
4200
-
4201
-
4202
-
4203
-
4204
-
4205
-