@formique/semantq 1.0.6 → 1.0.8

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