@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.
- package/LowCodeParser.js +1576 -0
- package/astToFormique.js +1054 -0
- package/formique-semantq.js +461 -229
- package/package.json +1 -1
package/formique-semantq.js
CHANGED
|
@@ -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(
|
|
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 =
|
|
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
|
-
...
|
|
83
|
+
...finalSettings
|
|
62
84
|
};
|
|
63
85
|
|
|
64
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
-
|
|
872
|
+
let senderName = '';
|
|
873
|
+
let senderEmail = '';
|
|
874
|
+
let formSubject = '';
|
|
875
|
+
let registrantEmail = '';
|
|
829
876
|
|
|
830
|
-
|
|
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
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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
|
-
|
|
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
|
-
|
|
865
|
-
|
|
866
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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
|
-
|
|
947
|
-
|
|
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
|
-
|
|
956
|
-
|
|
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
|
-
|
|
959
|
-
|
|
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
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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
|
-
|
|
969
|
-
|
|
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
|
-
|
|
972
|
-
|
|
973
|
-
|
|
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
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|