@formique/semantq 1.0.7 → 1.0.9

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': {
@@ -99,8 +125,7 @@ class Formique extends FormBuilder {
99
125
  ];
100
126
  this.formiqueEndpoint = "https://formiqueapi.onrender.com/api/send-email";
101
127
 
102
- // DISABLE DOM LISTENER
103
-
128
+ // DISABLE DOM LISTENER
104
129
  // document.addEventListener('DOMContentLoaded', () => {
105
130
  // 1. Build the form's HTML in memory
106
131
  this.formMarkUp += this.renderFormElement(); // Adds opening <form> tag and any hidden inputs
@@ -149,46 +174,67 @@ class Formique extends FormBuilder {
149
174
 
150
175
 
151
176
  // 2. Inject the complete form HTML into the DOM
152
- this.renderFormHTML(); // This puts the form element into the document!
153
-
154
- // 3. Now that the form is in the DOM, attach event listeners
155
- const formElement = document.getElementById(`${this.formId}`);
156
- if (formElement) { // Add a check here just in case, although it should now exist
157
- formElement.addEventListener('submit', function(event) {
158
- if (this.formSettings.submitMode === 'email') {
159
- event.preventDefault();
160
- document.getElementById("formiqueSpinner").style.display = "block";
161
- this.handleEmailSubmission(this.formId);
162
- }
163
-
164
- if (this.formSettings.submitOnPage) {
165
- event.preventDefault();
166
- document.getElementById("formiqueSpinner").style.display = "block";
167
- this.handleOnPageFormSubmission(this.formId);
168
- }
169
- }.bind(this));
170
- } else {
171
- console.error(`Form with ID ${this.formId} not found after rendering. Event listener could not be attached.`);
177
+ // A conceptual snippet from your form initialization method
178
+ this.renderFormHTML(); // This puts the form element into the document!
179
+
180
+ // 3. Now that the form is in the DOM, get the element and attach a single event listener
181
+ const formElement = document.getElementById(`${this.formId}`);
182
+ if (formElement) {
183
+ // Attach a single, unified submit event listener
184
+ formElement.addEventListener('submit', (event) => {
185
+ // Prevent default submission behavior immediately
186
+ event.preventDefault();
187
+
188
+ // Check if reCAPTCHA is present in the form schema
189
+ const recaptchaField = this.formSchema.find(field => field[0] === 'recaptcha');
190
+
191
+ // If reCAPTCHA is required, validate it first
192
+ if (recaptchaField) {
193
+ const recaptchaToken = grecaptcha.getResponse();
194
+
195
+ if (!recaptchaToken) {
196
+ // If reCAPTCHA is not checked, display an error and stop
197
+ document.getElementById("formiqueSpinner").style.display = "none";
198
+ alert('Please verify that you are not a robot.');
199
+ return; // Stop execution of the handler
172
200
  }
201
+ }
202
+
203
+ // If reCAPTCHA is not required or is validated, proceed with submission logic
204
+ document.getElementById("formiqueSpinner").style.display = "block";
205
+
206
+ if (this.formSettings.submitMode === 'email' || this.formSettings.submitMode === 'rsvp') {
207
+ this.handleEmailSubmission(this.formId);
208
+ }
209
+
210
+ if (this.formSettings.submitOnPage) {
211
+ this.handleOnPageFormSubmission(this.formId);
212
+ }
213
+ });
214
+
215
+ } else {
216
+ console.error(`Form with ID ${this.formId} not found after rendering. Event listener could not be attached.`);
217
+ }
218
+
219
+ // Initialize dependency graph and observers after the form is rendered
220
+ this.initDependencyGraph();
221
+ this.registerObservers();
222
+ this.attachDynamicSelectListeners();
223
+
224
+ // Apply theme
225
+ if (this.themeColor) {
226
+ this.applyCustomTheme(this.themeColor, this.formContainerId);
227
+ } else if (this.formSettings.theme && this.themes.includes(this.formSettings.theme)) {
228
+ let theme = this.formSettings.theme;
229
+ this.applyTheme(theme, this.formContainerId);
230
+ } else {
231
+ this.applyTheme('dark', this.formContainerId); // Default to 'dark'
232
+ }
233
+
173
234
 
174
- // Initialize dependency graph and observers after the form is rendered
175
- this.initDependencyGraph();
176
- this.registerObservers();
177
- this.attachDynamicSelectListeners();
178
-
179
- // Apply theme
180
- if (this.themeColor) {
181
- this.applyCustomTheme(this.themeColor, this.formContainerId); // <--- NEW: Apply custom theme
182
- } else if (this.formSettings.theme && this.themes.includes(this.formSettings.theme)) {
183
- let theme = this.formSettings.theme;
184
- this.applyTheme(theme, this.formContainerId);
185
- } else {
186
- // Fallback if no themeColor and no valid theme specified
187
- this.applyTheme('dark', this.formContainerId); // Default to 'dark'
188
- }
189
235
 
190
- // DISABLE DOM LISTENER
191
- //}); // DOM LISTENER WRAPPER
236
+ // DISABLE DOM LISTENER
237
+ // }); // DOM LISTENER WRAPPER
192
238
 
193
239
  // CONSTRUCTOR WRAPPER FOR FORMIQUE CLASS
194
240
  }
@@ -328,6 +374,7 @@ handleParentFieldChange(parentFieldId, value) {
328
374
  }
329
375
  }
330
376
 
377
+
331
378
  // Register observers for each dependent field
332
379
  registerObservers() {
333
380
  this.formSchema.forEach((field) => {
@@ -716,6 +763,7 @@ renderField(type, name, label, validate, attributes, options) {
716
763
  'multipleSelect': this.renderMultipleSelectField,
717
764
  'dynamicSingleSelect': this.renderDynamicSingleSelectField,
718
765
  'range': this.renderRangeField,
766
+ 'recaptcha': this.renderRecaptchaField,
719
767
  'submit': this.renderSubmitButton, // Keep this for completeness, but renderSubmitButtonElement will now handle it
720
768
  };
721
769
 
@@ -798,120 +846,191 @@ hasFileInputs(form) {
798
846
 
799
847
 
800
848
  async handleEmailSubmission(formId) {
801
- console.log(`Starting email submission for form ID: ${formId}`);
802
-
803
- const form = document.getElementById(formId);
804
- if (!form) {
805
- console.error(`Form with ID ${formId} not found`);
806
- throw new Error(`Form with ID ${formId} not found`);
807
- }
849
+ console.log(`Starting email submission for form ID: ${formId}`);
808
850
 
809
- // Validate required settings - now checks if sendTo is array with at least one item
810
- if (!Array.isArray(this.formSettings?.sendTo) || this.formSettings.sendTo.length === 0) {
811
- console.error('formSettings.sendTo must be an array with at least one recipient email');
812
- throw new Error('formSettings.sendTo must be an array with at least one recipient email');
813
- }
851
+ const form = document.getElementById(formId);
852
+ if (!form) {
853
+ console.error(`Form with ID ${formId} not found`);
854
+ throw new Error(`Form with ID ${formId} not found`);
855
+ }
814
856
 
815
- // Serialize form data
816
- const payload = {
817
- formData: {},
818
- metadata: {
819
- recipients: this.formSettings.sendTo, // Now sending array
820
- timestamp: new Date().toISOString()
857
+ // Validate required settings for 'sendTo'
858
+ if (!Array.isArray(this.formSettings?.sendTo) || this.formSettings.sendTo.length === 0) {
859
+ console.error('formSettings.sendTo must be an array with at least one recipient email');
860
+ throw new Error('formSettings.sendTo must be an array with at least one recipient email');
821
861
  }
822
- };
823
862
 
824
- let senderName = '';
825
- let senderEmail = '';
826
- let formSubject = '';
863
+ // Serialize form data
864
+ const payload = {
865
+ formData: {},
866
+ metadata: {
867
+ recipients: this.formSettings.sendTo,
868
+ timestamp: new Date().toISOString(),
869
+ },
870
+ };
827
871
 
828
- console.log('Initial payload structure:', JSON.parse(JSON.stringify(payload)));
872
+ let senderName = '';
873
+ let senderEmail = '';
874
+ let formSubject = '';
875
+ let registrantEmail = '';
829
876
 
830
- // Process form fields (unchanged)
831
- new FormData(form).forEach((value, key) => {
832
- console.log(`Processing form field - Key: ${key}, Value: ${value}`);
833
- payload.formData[key] = value;
834
-
835
- const lowerKey = key.toLowerCase();
836
- if ((lowerKey === 'email' || lowerKey.includes('email'))) {
837
- senderEmail = value;
838
- }
839
- if ((lowerKey === 'name' || lowerKey.includes('name'))) {
840
- senderName = value;
841
- }
842
- if ((lowerKey === 'subject' || lowerKey.includes('subject'))) {
843
- formSubject = value;
844
- }
845
- });
877
+ console.log('Initial payload structure:', JSON.parse(JSON.stringify(payload)));
846
878
 
847
- // Determine the email subject with fallback logic
848
- payload.metadata.subject = formSubject ||
849
- this.formSettings.subject ||
850
- 'Message From Contact Form';
851
-
852
- console.log('Determined email subject:', payload.metadata.subject);
853
-
854
- // Add sender information to metadata
855
- if (senderEmail) {
856
- payload.metadata.sender = senderEmail;
857
- payload.metadata.replyTo = senderName
858
- ? `${senderName} <${senderEmail}>`
859
- : senderEmail;
860
- }
879
+ // Process form fields and find registrant's email
880
+ const formData = new FormData(form);
881
+ formData.forEach((value, key) => {
882
+ console.log(`Processing form field - Key: ${key}, Value: ${value}`);
883
+ payload.formData[key] = value;
861
884
 
862
- console.log('Payload after form processing:', JSON.parse(JSON.stringify(payload)));
885
+ const lowerKey = key.toLowerCase();
886
+ if (lowerKey.includes('email')) {
887
+ senderEmail = value;
888
+ }
889
+ if (lowerKey.includes('name')) {
890
+ senderName = value;
891
+ }
892
+ if (lowerKey.includes('subject')) {
893
+ formSubject = value;
894
+ }
863
895
 
864
- try {
865
- const endpoint = this.formiqueEndpoint || this.formAction;
866
- const method = this.method || 'POST';
867
-
868
- console.log(`Preparing to send request to: ${endpoint}`);
869
- console.log(`Request method: ${method}`);
870
- console.log('Final payload being sent:', payload);
871
-
872
- const response = await fetch(endpoint, {
873
- method: method,
874
- headers: {
875
- 'Content-Type': 'application/json',
876
- 'X-Formique-Version': '1.0'
877
- },
878
- body: JSON.stringify(payload)
896
+ // Check if the current field is the registrant's email
897
+ if (this.formSettings.emailField && key === this.formSettings.emailField) {
898
+ registrantEmail = value;
899
+ }
879
900
  });
880
901
 
881
- console.log(`Received response with status: ${response.status}`);
902
+ // Determine the email subject with fallback logic
903
+ payload.metadata.subject = formSubject ||
904
+ this.formSettings.subject ||
905
+ 'Message From Contact Form';
882
906
 
883
- if (!response.ok) {
884
- const errorData = await response.json().catch(() => ({}));
885
- console.error('API Error Response:', errorData);
886
- throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
887
- document.getElementById("formiqueSpinner").style.display = "none";
907
+ console.log('Determined email subject:', payload.metadata.subject);
888
908
 
909
+ // Add sender information to metadata
910
+ if (senderEmail) {
911
+ payload.metadata.sender = senderEmail;
912
+ payload.metadata.replyTo = senderName
913
+ ? `${senderName} <${senderEmail}>`
914
+ : senderEmail;
889
915
  }
890
916
 
891
- const data = await response.json();
892
- console.log('API Success Response:', data);
893
-
894
- const successMessage = this.formSettings.successMessage ||
895
- data.message ||
896
- 'Your message has been sent successfully!';
897
- console.log(`Showing success message: ${successMessage}`);
898
-
899
- this.showSuccessMessage(successMessage);
900
-
901
- } catch (error) {
902
- console.error('Email submission failed:', error);
903
- const errorMessage = this.formSettings.errorMessage ||
904
- error.message ||
905
- 'Failed to send message. Please try again later.';
906
- console.log(`Showing error message: ${errorMessage}`);
907
- this.showErrorMessage(errorMessage);
908
- document.getElementById("formiqueSpinner").style.display = "none";
917
+ // **NEW:** Add reCAPTCHA secret key to the metadata object
918
+ if (this.formSettings.recaptchaSecretKey) {
919
+ payload.metadata.recaptchaSecretKey = this.formSettings.recaptchaSecretKey;
920
+ }
921
+
922
+ console.log('Payload after form processing:', JSON.parse(JSON.stringify(payload)));
923
+
924
+ // ... (The rest of your code remains the same)
925
+ try {
926
+ const endpoint = this.formiqueEndpoint || this.formAction;
927
+ const method = this.method || 'POST';
928
+
929
+ console.log(`Preparing to send primary request to: ${endpoint}`);
930
+ console.log(`Request method: ${method}`);
931
+ console.log('Final payload being sent to recipients:', payload);
932
+
933
+ // Send the first email to the 'sendTo' recipients
934
+ const response = await fetch(endpoint, {
935
+ method: method,
936
+ headers: {
937
+ 'Content-Type': 'application/json',
938
+ 'X-Formique-Version': '1.0',
939
+ },
940
+ body: JSON.stringify(payload),
941
+ });
942
+
943
+ console.log(`Received response for primary email with status: ${response.status}`);
944
+
945
+ if (!response.ok) {
946
+ const errorData = await response.json().catch(() => ({}));
947
+ console.error('API Error Response:', errorData);
948
+ throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
949
+ }
909
950
 
951
+ const data = await response.json();
952
+ console.log('Primary API Success Response:', data);
953
+
954
+ // ------------------- NEW RSVP LOGIC -------------------
955
+ if (this.formSettings.submitMode === 'rsvp' && registrantEmail && this.formSettings.registrantMessage) {
956
+ console.log('RSVP mode detected. Sending confirmation email to registrant.');
957
+
958
+ // Create a new payload for the registrant
959
+ const rsvpPayload = {
960
+ formData: payload.formData,
961
+ metadata: {
962
+ recipients: [registrantEmail], // Send only to the registrant
963
+ timestamp: new Date().toISOString(),
964
+ subject: this.formSettings.registrantSubject || 'RSVP Confirmation',
965
+ body: this.processDynamicMessage(this.formSettings.registrantMessage, payload.formData),
966
+ sender: this.formSettings.sendFrom || 'noreply@yourdomain.com',
967
+ replyTo: this.formSettings.sendFrom || 'noreply@yourdomain.com',
968
+ },
969
+ };
970
+
971
+ try {
972
+ console.log('Preparing to send RSVP email. Final payload:', rsvpPayload);
973
+ const rsvpResponse = await fetch(endpoint, {
974
+ method: method,
975
+ headers: {
976
+ 'Content-Type': 'application/json',
977
+ 'X-Formique-Version': '1.0',
978
+ },
979
+ body: JSON.stringify(rsvpPayload),
980
+ });
981
+
982
+ if (!rsvpResponse.ok) {
983
+ const rsvpErrorData = await rsvpResponse.json().catch(() => ({}));
984
+ console.error('RSVP API Error Response:', rsvpErrorData);
985
+ // Log the error but don't fail the entire submission since the primary email was sent
986
+ console.warn('Failed to send RSVP email to registrant, but primary submission was successful.');
987
+ } else {
988
+ console.log('RSVP email sent successfully to registrant.');
989
+ }
990
+ } catch (rsvpError) {
991
+ console.error('RSVP email submission failed:', rsvpError);
992
+ console.warn('Failed to send RSVP email to registrant, but primary submission was successful.');
993
+ }
994
+ }
995
+ // ------------------- END NEW RSVP LOGIC -------------------
996
+
997
+ const successMessage = this.formSettings.successMessage ||
998
+ data.message ||
999
+ 'Your message has been sent successfully!';
1000
+ console.log(`Showing success message: ${successMessage}`);
1001
+
1002
+ this.showSuccessMessage(successMessage);
1003
+
1004
+ } catch (error) {
1005
+ console.error('Email submission failed:', error);
1006
+ const errorMessage = this.formSettings.errorMessage ||
1007
+ error.message ||
1008
+ 'Failed to send message. Please try again later.';
1009
+ console.log(`Showing error message: ${errorMessage}`);
1010
+ this.showErrorMessage(errorMessage);
1011
+ } finally {
1012
+ document.getElementById("formiqueSpinner").style.display = "none";
1013
+ }
1014
+ }
1015
+
1016
+ // Add this method to your Formique class
1017
+ processDynamicMessage(message, formData) {
1018
+ let processedMessage = message;
1019
+ // Iterate over each key-value pair in the form data
1020
+ for (const key in formData) {
1021
+ if (Object.prototype.hasOwnProperty.call(formData, key)) {
1022
+ const placeholder = `{${key}}`;
1023
+ // Replace all occurrences of the placeholder with the corresponding form data value
1024
+ processedMessage = processedMessage.split(placeholder).join(formData[key]);
1025
+ }
910
1026
  }
1027
+ return processedMessage;
911
1028
  }
912
1029
 
913
1030
 
914
1031
 
1032
+
1033
+
915
1034
  // Email validation helper
916
1035
  validateEmail(email) {
917
1036
  const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
@@ -920,84 +1039,145 @@ validateEmail(email) {
920
1039
  }
921
1040
 
922
1041
 
1042
+ attachSubmitListener() {
1043
+ this.formElement.addEventListener('submit', (e) => {
1044
+ // Find the reCAPTCHA field in the form schema.
1045
+ const recaptchaField = this.formSchema.find(field => field[0] === 'recaptcha');
1046
+
1047
+ // If a reCAPTCHA field is present, check its state.
1048
+ if (recaptchaField) {
1049
+ const recaptchaToken = grecaptcha.getResponse();
923
1050
 
924
- // Method to handle on-page form submissions
925
- handleOnPageFormSubmission(formId) {
926
- const formElement = document.getElementById(formId);
927
- //console.warn("handler fired also",formId,this.method,this.formAction);
928
-
929
- if (formElement) {
930
- // Gather form data
931
- const formData = new FormData(formElement);
932
-
933
- // Submit form data using fetch to a test endpoint
934
- fetch(this.formAction, {
935
- method: this.method,
936
- body: formData
937
- })
938
- .then(response => response.json())
939
- .then(data => {
940
- console.log('Success:', data);
941
- // Handle the response data here, e.g., show a success message
942
-
943
- // Get the form container element
944
- const formContainer = document.getElementById(this.formContainerId);
1051
+ if (!recaptchaToken) {
1052
+ // Prevent the default form submission.
1053
+ e.preventDefault();
1054
+
1055
+ // Display the alert and handle UI.
1056
+ alert('Please verify that you are not a robot.');
1057
+ document.getElementById("formiqueSpinner").style.display = "none";
1058
+ return;
1059
+ }
1060
+ }
945
1061
 
946
- if (this.redirect && this.redirectURL) {
947
- window.location.href = this.redirectURL;
948
- }
1062
+ // If reCAPTCHA is valid or not present, proceed with submission logic.
1063
+ this.handleOnPageFormSubmission(e);
1064
+ });
1065
+ }
949
1066
 
950
1067
 
951
- if (formContainer) {
952
- // Create a new div element for the success message
953
- const successMessageDiv = document.createElement('div');
954
1068
 
955
- // Add custom classes for styling the success message
956
- successMessageDiv.classList.add('success-message', 'message-container');
1069
+ // Method to handle on-page form submissions
1070
+ handleOnPageFormSubmission(formId) {
1071
+ const formElement = document.getElementById(formId);
1072
+
1073
+ if (formElement) {
1074
+ // Intercept the form's native submit event
1075
+ formElement.addEventListener('submit', (e) => {
1076
+ // Find the reCAPTCHA field in the form schema.
1077
+ const recaptchaField = this.formSchema.find(field => field[0] === 'recaptcha');
1078
+
1079
+ // If a reCAPTCHA field exists, perform client-side validation.
1080
+ if (recaptchaField) {
1081
+ const recaptchaToken = grecaptcha.getResponse();
1082
+
1083
+ // If the token is empty, the reCAPTCHA challenge has not been completed.
1084
+ if (!recaptchaToken) {
1085
+ e.preventDefault(); // <-- The crucial line to stop default form submission
1086
+
1087
+ // Hide the spinner to indicate the submission was halted.
1088
+ document.getElementById("formiqueSpinner").style.display = "none";
1089
+
1090
+ // Display a user-friendly error message.
1091
+ alert('Please verify that you are not a robot.');
1092
+
1093
+ // Stop the function's execution to prevent form submission.
1094
+ return;
1095
+ }
1096
+ }
957
1097
 
958
- // Set the success message text
959
- successMessageDiv.innerHTML = this.formSettings.successMessage || 'Your details have been successfully submitted!';
1098
+ // At this point, reCAPTCHA is validated (or not present), so we can proceed with the fetch request.
1099
+ // Show the spinner as submission is now beginning.
1100
+ document.getElementById("formiqueSpinner").style.display = "block";
960
1101
 
961
- // Replace the content of the form container with the success message div
962
- formContainer.innerHTML = ''; // Clear existing content
963
- formContainer.appendChild(successMessageDiv); // Append the new success message div
964
- }
1102
+ // Gather form data.
1103
+ const formData = {};
1104
+ new FormData(formElement).forEach((value, key) => {
1105
+ formData[key] = value;
1106
+ });
965
1107
 
1108
+ console.log("Setting Object",this.formSettings);
966
1109
 
967
- })
968
- .catch(error => {
969
- console.error('Error:', error);
1110
+ // Create the full payload with formData and metadata, including the secret key.
1111
+ const payload = {
1112
+ formData: formData,
1113
+ metadata: {
1114
+ ...this.formSettings, // Include all formSettings
1115
+ // Other metadata like recipients and sender will be included from this.formSettings
1116
+ }
1117
+ };
1118
+
1119
+ // Submit form data using fetch to the endpoint.
1120
+ fetch(this.formAction, {
1121
+ method: this.method,
1122
+ headers: {
1123
+ 'Content-Type': 'application/json' // Important: set the content type
1124
+ },
1125
+ body: JSON.stringify(payload) // Send the combined payload as JSON
1126
+ })
1127
+ .then(response => {
1128
+ // Check if the response status is OK (200-299).
1129
+ if (!response.ok) {
1130
+ return response.json().then(errorData => {
1131
+ throw new Error(errorData.error || `HTTP error! Status: ${response.status}`);
1132
+ });
1133
+ }
1134
+ return response.json();
1135
+ })
1136
+ .then(data => {
1137
+ console.log('Success:', data);
1138
+
1139
+ // Hide the spinner on success.
1140
+ document.getElementById("formiqueSpinner").style.display = "none";
1141
+
1142
+ const formContainer = document.getElementById(this.formContainerId);
1143
+ if (this.redirect && this.redirectURL) {
1144
+ window.location.href = this.redirectURL;
1145
+ }
1146
+ if (formContainer) {
1147
+ const successMessageDiv = document.createElement('div');
1148
+ successMessageDiv.classList.add('success-message', 'message-container');
1149
+ successMessageDiv.innerHTML = this.formSettings.successMessage || 'Your details have been successfully submitted!';
1150
+ formContainer.innerHTML = '';
1151
+ formContainer.appendChild(successMessageDiv);
1152
+ }
1153
+ })
1154
+ .catch(error => {
1155
+ console.error('Error:', error);
1156
+
1157
+ // Hide the spinner on error.
1158
+ document.getElementById("formiqueSpinner").style.display = "none";
1159
+
1160
+ const formContainer = document.getElementById(this.formContainerId);
1161
+ if (formContainer) {
1162
+ let existingErrorDiv = formContainer.querySelector('.error-message');
1163
+ if (existingErrorDiv) {
1164
+ existingErrorDiv.remove();
1165
+ }
1166
+ const errorMessageDiv = document.createElement('div');
1167
+ errorMessageDiv.classList.add('error-message', 'message-container');
1168
+ let err = this.formSettings.errorMessage || 'An error occurred while submitting the form. Please try again.';
1169
+ err = `${err}<br/>Details: ${error.message}`;
1170
+ errorMessageDiv.innerHTML = err;
1171
+ formContainer.appendChild(errorMessageDiv);
1172
+ }
1173
+ });
970
1174
 
971
- const formContainer = document.getElementById(this.formContainerId);
972
- if (formContainer) {
973
- // Check if an error message div already exists and remove it
974
- let existingErrorDiv = formContainer.querySelector('.error-message');
975
- if (existingErrorDiv) {
976
- existingErrorDiv.remove();
1175
+ // Return false to ensure no other default action is taken, especially for legacy browsers.
1176
+ return false;
1177
+ });
977
1178
  }
978
-
979
- // Create a new div element for the error message
980
- const errorMessageDiv = document.createElement('div');
981
-
982
- // Add custom classes for styling the error message
983
- errorMessageDiv.classList.add('error-message', 'message-container');
984
-
985
- // Set the error message text
986
- let err = this.formSettings.errorMessage || 'An error occurred while submitting the form. Please try again.';
987
- err = `${err}<br/>Details: ${error.message}`;
988
- errorMessageDiv.innerHTML = err;
989
-
990
- // Append the new error message div to the form container
991
- formContainer.appendChild(errorMessageDiv);
992
- }
993
- });
994
-
995
- }
996
1179
  }
997
1180
 
998
-
999
-
1000
-
1001
1181
  // text field rendering
1002
1182
  renderTextField(type, name, label, validate, attributes) {
1003
1183
  const textInputValidationAttributes = [
@@ -3280,6 +3460,8 @@ renderTextAreaField(type, name, label, validate, attributes) {
3280
3460
 
3281
3461
  renderRadioField(type, name, label, validate, attributes, options) {
3282
3462
  // Define valid validation attributes for radio fields
3463
+ console.log("RADIO DEBUG - options:", JSON.stringify(options, null, 2));
3464
+
3283
3465
  const radioValidationAttributes = ['required'];
3284
3466
 
3285
3467
  // Construct validation attributes
@@ -3311,15 +3493,15 @@ renderRadioField(type, name, label, validate, attributes, options) {
3311
3493
  // Handle the binding syntax
3312
3494
  let bindingDirective = '';
3313
3495
  if (attributes.binding) {
3314
- if (attributes.binding === 'bind:value' && name) {
3315
- bindingDirective = ` bind:value="${name}"\n`;
3316
- } else if (attributes.binding.startsWith('::') && name) {
3317
- bindingDirective = ` bind:value="${name}"\n`;
3318
- } else if (attributes.binding && !name) {
3319
- console.log(`\x1b[31m%s\x1b[0m`, `You cannot set binding value when there is no name attribute defined in ${name} ${type} field.`);
3320
- return;
3496
+ if (attributes.binding === 'bind:value' && name) {
3497
+ bindingDirective = ` bind:value="${name}"\n`;
3498
+ } else if (attributes.binding.startsWith('::') && name) {
3499
+ bindingDirective = ` bind:value="${name}"\n`;
3500
+ } else if (attributes.binding && !name) {
3501
+ console.log(`\x1b[31m%s\x1b[0m`, `You cannot set binding value when there is no name attribute defined in ${name} ${type} field.`);
3502
+ return;
3503
+ }
3321
3504
  }
3322
- }
3323
3505
 
3324
3506
  // Define attributes for the radio inputs
3325
3507
  let id = attributes.id || name;
@@ -3327,7 +3509,8 @@ renderRadioField(type, name, label, validate, attributes, options) {
3327
3509
  // Construct additional attributes dynamically
3328
3510
  let additionalAttrs = '';
3329
3511
  for (const [key, value] of Object.entries(attributes)) {
3330
- if (key !== 'id' && key !== 'class' && key !== 'dependsOn' && key !== 'dependents' && value !== undefined) { if (key.startsWith('on')) {
3512
+ if (key !== 'id' && key !== 'class' && key !== 'dependsOn' && key !== 'dependents' && value !== undefined) {
3513
+ if (key.startsWith('on')) {
3331
3514
  // Handle event attributes
3332
3515
  const eventValue = value.endsWith('()') ? value.slice(0, -2) : value;
3333
3516
  additionalAttrs += ` @${key.replace(/^on/, '')}={${eventValue}}\n`;
@@ -3345,10 +3528,28 @@ renderRadioField(type, name, label, validate, attributes, options) {
3345
3528
 
3346
3529
  let inputClass = attributes.class || this.inputClass;
3347
3530
 
3531
+ // Determine which option should be selected
3532
+ let selectedValue = null;
3533
+
3534
+ // Check options array for selected: true
3535
+ if (options && options.length) {
3536
+ const selectedOption = options.find(opt => opt.selected === true);
3537
+ console.log("RADIO DEBUG - selectedOption:", selectedOption);
3538
+ if (selectedOption) {
3539
+ selectedValue = selectedOption.value;
3540
+ console.log("RADIO DEBUG - selectedValue:", selectedValue);
3541
+ }
3542
+ }
3543
+
3348
3544
  // Construct radio button HTML based on options
3349
3545
  let optionsHTML = '';
3350
3546
  if (options && options.length) {
3351
3547
  optionsHTML = options.map((option) => {
3548
+ // Check if this option should be selected
3549
+ const isSelected = (option.value === selectedValue);
3550
+ console.log("RADIO DEBUG - option:", option.value, "isSelected:", isSelected);
3551
+ const checkedAttr = isSelected ? ' checked' : '';
3552
+
3352
3553
  return `
3353
3554
  <div>
3354
3555
  <input
@@ -3360,6 +3561,7 @@ renderRadioField(type, name, label, validate, attributes, options) {
3360
3561
  ${attributes.id ? `id="${id}-${option.value}"` : `id="${id}-${option.value}"`}
3361
3562
  class="${inputClass}"
3362
3563
  ${validationAttrs}
3564
+ ${checkedAttr}
3363
3565
  />
3364
3566
  <label
3365
3567
  for="${attributes.id ? `${id}-${option.value}` : `${id}-${option.value}`}">
@@ -3394,13 +3596,13 @@ renderRadioField(type, name, label, validate, attributes, options) {
3394
3596
  return `\n${match}\n`;
3395
3597
  }).replace(/\n\s*\n/g, '\n'); // Remove extra blank lines
3396
3598
 
3397
- //return formattedHtml;
3398
- this.formMarkUp +=formattedHtml;
3599
+ this.formMarkUp += formattedHtml;
3399
3600
  }
3400
3601
 
3401
-
3402
3602
  renderCheckboxField(type, name, label, validate, attributes, options) {
3403
3603
  // Define valid validation attributes for checkbox fields
3604
+ console.log("CHECKBOX DEBUG - options:", JSON.stringify(options, null, 2));
3605
+
3404
3606
  const checkboxValidationAttributes = ['required'];
3405
3607
 
3406
3608
  // Construct validation attributes
@@ -3420,12 +3622,12 @@ renderCheckboxField(type, name, label, validate, attributes, options) {
3420
3622
  // Handle the binding syntax
3421
3623
  let bindingDirective = '';
3422
3624
  if (attributes.binding) {
3423
- if (attributes.binding === 'bind:checked') {
3424
- bindingDirective = ` bind:checked="${name}"\n`;
3425
- } else if (attributes.binding.startsWith('::')) {
3426
- bindingDirective = ` bind:checked="${name}"\n`;
3625
+ if (attributes.binding === 'bind:checked') {
3626
+ bindingDirective = ` bind:checked="${name}"\n`;
3627
+ } else if (attributes.binding.startsWith('::')) {
3628
+ bindingDirective = ` bind:checked="${name}"\n`;
3629
+ }
3427
3630
  }
3428
- }
3429
3631
 
3430
3632
  // Define attributes for the checkbox inputs
3431
3633
  let id = attributes.id || name;
@@ -3433,7 +3635,8 @@ renderCheckboxField(type, name, label, validate, attributes, options) {
3433
3635
  // Handle additional attributes
3434
3636
  let additionalAttrs = '';
3435
3637
  for (const [key, value] of Object.entries(attributes)) {
3436
- if (key !== 'id' && key !== 'class' && key !== 'dependsOn' && key !== 'dependents' && value !== undefined) { if (key.startsWith('on')) {
3638
+ if (key !== 'id' && key !== 'class' && key !== 'dependsOn' && key !== 'dependents' && value !== undefined) {
3639
+ if (key.startsWith('on')) {
3437
3640
  // Handle event attributes
3438
3641
  const eventValue = value.endsWith('()') ? value.slice(0, -2) : value;
3439
3642
  additionalAttrs += ` @${key.replace(/^on/, '')}={${eventValue}}\n`;
@@ -3449,30 +3652,45 @@ renderCheckboxField(type, name, label, validate, attributes, options) {
3449
3652
  }
3450
3653
  }
3451
3654
 
3452
-
3453
3655
  let inputClass;
3454
3656
  if ('class' in attributes) {
3455
3657
  inputClass = attributes.class;
3456
3658
  } else {
3457
- inputClass = this.inputClass;
3659
+ inputClass = this.inputClass;
3458
3660
  }
3459
3661
 
3662
+ // Determine which options should be checked
3663
+ const checkedValues = [];
3664
+ if (options && options.length) {
3665
+ options.forEach(option => {
3666
+ if (option.checked === true || option.selected === true) {
3667
+ checkedValues.push(option.value);
3668
+ }
3669
+ });
3670
+ }
3671
+ console.log("CHECKBOX DEBUG - checkedValues:", checkedValues);
3672
+
3460
3673
  // Construct checkbox HTML based on options
3461
3674
  let optionsHTML = '';
3462
3675
  if (Array.isArray(options)) {
3463
3676
  optionsHTML = options.map((option) => {
3464
3677
  const optionId = `${id}-${option.value}`;
3678
+ const isChecked = checkedValues.includes(option.value);
3679
+ console.log("CHECKBOX DEBUG - option:", option.value, "isChecked:", isChecked);
3680
+ const checkedAttr = isChecked ? ' checked' : '';
3681
+
3465
3682
  return `
3466
3683
  <div>
3467
3684
  <input
3468
- type="checkbox"
3469
- name="${name}"
3470
- value="${option.value}"${bindingDirective} ${additionalAttrs}
3685
+ type="checkbox"
3686
+ name="${name}"
3687
+ value="${option.value}"${bindingDirective} ${additionalAttrs}
3471
3688
  ${attributes.id ? `id="${optionId}"` : `id="${optionId}"`}
3472
3689
  class="${inputClass}"
3690
+ ${checkedAttr}
3473
3691
  />
3474
3692
  <label
3475
- for="${optionId}">
3693
+ for="${optionId}">
3476
3694
  ${option.label}
3477
3695
  </label>
3478
3696
  </div>
@@ -3505,8 +3723,7 @@ renderCheckboxField(type, name, label, validate, attributes, options) {
3505
3723
  return `\n${match}\n`;
3506
3724
  }).replace(/\n\s*\n/g, '\n'); // Remove extra blank lines
3507
3725
 
3508
- //return formattedHtml;
3509
- this.formMarkUp +=formattedHtml;
3726
+ this.formMarkUp += formattedHtml;
3510
3727
  }
3511
3728
 
3512
3729
 
@@ -3946,6 +4163,23 @@ renderRangeField(type, name, label, validate, attributes) {
3946
4163
  }
3947
4164
 
3948
4165
 
4166
+ renderRecaptchaField(type, name, label, validate, attributes = {}) {
4167
+ const fieldId = attributes.id || name;
4168
+ const siteKey = attributes.siteKey;
4169
+ // Check for the presence of a siteKey
4170
+ if (!siteKey) {
4171
+ console.error('reCAPTCHA siteKey is missing from the field attributes.');
4172
+ return ''; // Do not render if the key is missing
4173
+ }
4174
+
4175
+ return `
4176
+ <div class="${this.divClass}" id="${fieldId}-block">
4177
+ <label for="${fieldId}">${label}</label>
4178
+ <div class="g-recaptcha" id="${fieldId}" data-sitekey="${siteKey}"></div>
4179
+ </div>
4180
+ `;
4181
+ }
4182
+
3949
4183
 
3950
4184
  /*
3951
4185
  renderRangeField(type, name, label, validate, attributes) {
@@ -4127,5 +4361,3 @@ const spinner = `<div id="formiqueSpinner" style="display: flex; align-items: ce
4127
4361
 
4128
4362
 
4129
4363
  export default Formique;
4130
-
4131
-