n8n-nodes-extended-forms 0.2.5 → 0.3.0

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 (29) hide show
  1. package/dist/credentials/GithubIssuesApi.credentials.js +1 -36
  2. package/dist/credentials/GithubIssuesOAuth2Api.credentials.js +1 -53
  3. package/dist/nodes/Form123/Form.node.js +1 -388
  4. package/dist/nodes/Form123/Form.node.js.map +1 -1
  5. package/dist/nodes/Form123/FormTrigger.node.js +1 -27
  6. package/dist/nodes/Form123/common.descriptions.js +1 -630
  7. package/dist/nodes/Form123/common.descriptions.js.map +1 -1
  8. package/dist/nodes/Form123/cssVariables.js +1 -73
  9. package/dist/nodes/Form123/interfaces.d.ts +5 -0
  10. package/dist/nodes/Form123/interfaces.js +1 -4
  11. package/dist/nodes/Form123/interfaces.js.map +1 -1
  12. package/dist/nodes/Form123/templates/form-trigger-completion.handlebars +2 -1
  13. package/dist/nodes/Form123/templates/form-trigger.handlebars +774 -73
  14. package/dist/nodes/Form123/utils/descriptions.js +1 -10
  15. package/dist/nodes/Form123/utils/formCompletionUtils.js +1 -106
  16. package/dist/nodes/Form123/utils/formNodeUtils.js +3 -75
  17. package/dist/nodes/Form123/utils/formNodeUtils.js.map +1 -1
  18. package/dist/nodes/Form123/utils/templateRenderer.js +1 -71
  19. package/dist/nodes/Form123/utils/utilities.js +3 -47
  20. package/dist/nodes/Form123/utils/utils.d.ts +11 -3
  21. package/dist/nodes/Form123/utils/utils.js +3 -592
  22. package/dist/nodes/Form123/utils/utils.js.map +1 -1
  23. package/dist/nodes/Form123/utils/waitUtils.js +1 -67
  24. package/dist/nodes/Form123/v1/FormTriggerV1.node.js +1 -82
  25. package/dist/nodes/Form123/v2/FormTriggerV2.node.js +1 -193
  26. package/dist/nodes/Form123/v2/FormTriggerV2.node.js.map +1 -1
  27. package/dist/package.json +3 -1
  28. package/dist/tsconfig.tsbuildinfo +1 -1
  29. package/package.json +3 -1
@@ -281,6 +281,16 @@
281
281
  visibility: visible;
282
282
  }
283
283
 
284
+ .error {
285
+ display: block;
286
+ color: var(--color-error);
287
+ text-align: left;
288
+ font-size: var(--font-size-error);
289
+ font-weight: 400;
290
+ padding-top: 6px;
291
+ min-height: 18px;
292
+ }
293
+
284
294
  /* multiselect ----------------------------------- */
285
295
  .multiselect {
286
296
  padding-left: 6px;
@@ -304,48 +314,48 @@
304
314
  min-width: var(--checkbox-size);
305
315
  }
306
316
 
307
- input[type='checkbox'] {
308
- appearance: none;
309
- width: var(--checkbox-size);
310
- height: var(--checkbox-size);
311
- border: 1px solid var(--color-input-border);
312
- border-radius: 3px;
313
- cursor: pointer;
314
- position: relative;
315
- }
316
- .multiselect-checkbox:checked {
317
- background: var(--color-focus-border);
318
- border-color: var(--color-focus-border);
319
- }
320
- .multiselect-checkbox:checked::after {
321
- content: '✔';
322
- position: absolute;
323
- top: 50%;
324
- left: 50%;
325
- transform: translate(-50%, -50%);
326
- color: white;
327
- font-size: var(--font-size-label);
328
- font-weight: bold;
329
- }
330
-
331
- .multiselect[data-radio-select] .multiselect-checkbox {
332
- border-radius: 50%;
333
- width: var(--checkbox-size);
334
- height: var(--checkbox-size);
335
- display: flex;
336
- align-items: center;
337
- justify-content: center;
338
- }
339
-
340
- .multiselect[data-radio-select] .multiselect-checkbox:checked {
341
- background: white;
342
- border-color: var(--color-focus-border);
343
- border-width: 4px;
344
- border-radius: 50%;
345
- }
346
- .multiselect[data-radio-select] .multiselect-checkbox:checked::after {
347
- content: '';
348
- }
317
+ input[type='checkbox'],
318
+ input[type='radio'] {
319
+ appearance: none;
320
+ -webkit-appearance: none;
321
+ -moz-appearance: none;
322
+ width: var(--checkbox-size);
323
+ height: var(--checkbox-size);
324
+ border: 1px solid var(--color-input-border);
325
+ border-radius: 3px;
326
+ cursor: pointer;
327
+ position: relative;
328
+ }
329
+
330
+ input[type='checkbox']:checked {
331
+ background: var(--color-focus-border);
332
+ border-color: var(--color-focus-border);
333
+ }
334
+ input[type='checkbox']:checked::after {
335
+ content: '✔';
336
+ position: absolute;
337
+ top: 50%;
338
+ left: 50%;
339
+ transform: translate(-50%, -50%);
340
+ color: white;
341
+ font-size: var(--font-size-label);
342
+ font-weight: bold;
343
+ }
344
+
345
+ /* Radio button specific styles */
346
+ input[type='radio'] {
347
+ border-radius: 50%;
348
+ }
349
+
350
+ input[type='radio']:checked {
351
+ background: white;
352
+ border-color: var(--color-focus-border);
353
+ border-width: 4px;
354
+ }
355
+
356
+ input[type='radio']:checked::after {
357
+ content: '';
358
+ }
349
359
 
350
360
  /* searchable dropdown ----------------------------- */
351
361
  .searchable-dropdown {
@@ -655,6 +665,7 @@
655
665
  <div
656
666
  class='multiselect {{inputRequired}}'
657
667
  id='{{id}}'
668
+ data-field-name='{{label}}'
658
669
  {{#if radioSelect}}
659
670
  data-radio-select='{{radioSelect}}'
660
671
  {{/if}}
@@ -670,7 +681,14 @@
670
681
  >
671
682
  {{#each multiSelectOptions}}
672
683
  <div class='multiselect-option'>
673
- <input type='checkbox' class='multiselect-checkbox' id='{{id}}' value='{{label}}' />
684
+ <input
685
+ type='{{#if ../radioSelect}}radio{{else}}checkbox{{/if}}'
686
+ class='multiselect-checkbox'
687
+ id='{{id}}'
688
+ name='{{#if ../radioSelect}}{{../id}}{{else}}{{id}}{{/if}}'
689
+ value='{{label}}'
690
+ data-field-name='{{../label}}'
691
+ />
674
692
  <label for='{{id}}'>{{label}}</label>
675
693
  </div>
676
694
  {{/each}}
@@ -685,7 +703,7 @@
685
703
  <div class='form-group {{#if showWhen}}input--hide{{/if}}' {{#if showWhen}}data-trigger-field='{{showWhen.fieldId}}' data-trigger-values='{{json showWhen.values}}'{{/if}}>
686
704
  <label class='form-label {{inputRequired}}' for='{{id}}'>{{label}}</label>
687
705
  <div class='select-input'>
688
- <select id='{{id}}' name='{{id}}' class='{{inputRequired}}'>
706
+ <select id='{{id}}' name='{{id}}' class='{{inputRequired}}' data-field-name='{{label}}'>
689
707
  <option value='' disabled selected>Select an option ...</option>
690
708
  {{#each selectOptions}}
691
709
  <option value='{{this}}'>{{this}}</option>
@@ -701,7 +719,7 @@
701
719
  {{#if isSearchableDropdown}}
702
720
  <div class='form-group {{#if showWhen}}input--hide{{/if}}' {{#if showWhen}}data-trigger-field='{{showWhen.fieldId}}' data-trigger-values='{{json showWhen.values}}'{{/if}}>
703
721
  <label class='form-label {{inputRequired}}' for='{{id}}'>{{label}}</label>
704
- <div class='searchable-dropdown searchable-dropdown-single {{inputRequired}}' id='{{id}}' data-field-id='{{id}}' data-single-select='true'>
722
+ <div class='searchable-dropdown searchable-dropdown-single {{inputRequired}}' id='{{id}}' data-field-id='{{id}}' data-field-name='{{label}}' data-single-select='true'>
705
723
  <div class='searchable-dropdown-input-wrapper'>
706
724
  <input
707
725
  type='text'
@@ -731,7 +749,7 @@
731
749
  {{#if isSearchableMultiselect}}
732
750
  <div class='form-group {{#if showWhen}}input--hide{{/if}}' {{#if showWhen}}data-trigger-field='{{showWhen.fieldId}}' data-trigger-values='{{json showWhen.values}}'{{/if}}>
733
751
  <label class='form-label {{inputRequired}}' for='{{id}}'>{{label}}</label>
734
- <div class='searchable-dropdown searchable-dropdown-multi {{inputRequired}}' id='{{id}}' data-field-id='{{id}}' data-single-select='false'>
752
+ <div class='searchable-dropdown searchable-dropdown-multi {{inputRequired}}' id='{{id}}' data-field-id='{{id}}' data-field-name='{{label}}' data-single-select='false'>
735
753
  <div class='searchable-dropdown-input-wrapper'>
736
754
  <input
737
755
  type='text'
@@ -780,6 +798,7 @@
780
798
  class='form-input {{inputRequired}}'
781
799
  id='{{id}}'
782
800
  name='{{id}}'
801
+ data-field-name='{{label}}'
783
802
  placeholder="{{placeholder}}"
784
803
  >{{defaultValue}}</textarea>
785
804
  <p class='{{errorId}} error-hidden'>
@@ -789,7 +808,7 @@
789
808
  {{/if}}
790
809
 
791
810
  {{#if isFileInput}}
792
- <div class='form-group file-input-wrapper {{#if showWhen}}input--hide{{/if}}' {{#if showWhen}}data-trigger-field='{{showWhen.fieldId}}' data-trigger-values='{{json showWhen.values}}'{{/if}}>
811
+ <div class='form-group file-input-wrapper {{#if showWhen}}input--hide{{/if}}' {{#if showWhen}}data-trigger-field='{{showWhen.fieldId}}' data-trigger-values='{{json showWhen.values}}'{{/if}} {{#if maxFileSize}}data-max-file-size='{{maxFileSize}}'{{/if}}>
793
812
  <label class='form-label {{inputRequired}}' for='{{id}}'>{{label}}</label>
794
813
  <input
795
814
  class='form-input {{inputRequired}}'
@@ -800,6 +819,7 @@
800
819
  {{multipleFiles}}
801
820
  placeholder="{{placeholder}}"
802
821
  />
822
+ <span class="error error-{{id}}"></span>
803
823
  <button class="clear-button">&times;</button>
804
824
  <p class='{{errorId}} error-hidden'>
805
825
  This field is required
@@ -815,6 +835,7 @@
815
835
  type='{{type}}'
816
836
  id='{{id}}'
817
837
  name='{{id}}'
838
+ data-field-name='{{label}}'
818
839
  value="{{defaultValue}}"
819
840
  placeholder="{{placeholder}}"
820
841
  />
@@ -955,6 +976,36 @@
955
976
  }
956
977
  }
957
978
 
979
+ function validateFileSize(fileInput, maxSizeMB) {
980
+ const errorElement = fileInput.parentElement.querySelector('.error-' + fileInput.id);
981
+ const files = fileInput.files;
982
+
983
+ if (!maxSizeMB || maxSizeMB <= 0 || files.length === 0) {
984
+ // Clear error message
985
+ if (errorElement) {
986
+ errorElement.textContent = '';
987
+ }
988
+ return true; // No limit set or no files
989
+ }
990
+
991
+ const maxSizeBytes = maxSizeMB * 1024 * 1024;
992
+
993
+ for (let i = 0; i < files.length; i++) {
994
+ if (files[i].size > maxSizeBytes) {
995
+ if (errorElement) {
996
+ errorElement.textContent = 'max file size ' + maxSizeMB + ' MB';
997
+ }
998
+ return false;
999
+ }
1000
+ }
1001
+
1002
+ // Clear error if validation passes
1003
+ if (errorElement) {
1004
+ errorElement.textContent = '';
1005
+ }
1006
+ return true;
1007
+ }
1008
+
958
1009
  function getSelectedValues(input) {
959
1010
  const selectedValues = [];
960
1011
  const checkboxes = input.querySelectorAll('.multiselect-checkbox');
@@ -1020,11 +1071,27 @@
1020
1071
  });
1021
1072
 
1022
1073
  document.querySelectorAll('input[type="file"]').forEach(fileInput => {
1023
- const clearButton = fileInput.nextElementSibling;
1074
+ const errorSpan = fileInput.nextElementSibling;
1075
+ const clearButton = fileInput.nextElementSibling.nextElementSibling;
1076
+ const wrapper = fileInput.closest('.file-input-wrapper');
1077
+ const maxFileSize = wrapper ? parseFloat(wrapper.dataset.maxFileSize) : 0;
1024
1078
  let previousFiles = [];
1025
1079
 
1026
1080
  fileInput.addEventListener('change', () => {
1027
1081
  const files = fileInput.files;
1082
+
1083
+ // Validate file size
1084
+ const isValid = validateFileSize(fileInput, maxFileSize);
1085
+
1086
+ if (!isValid && files.length > 0) {
1087
+ // File is too large - clear it after a brief moment
1088
+ setTimeout(() => {
1089
+ fileInput.value = '';
1090
+ clearButton.style.display = 'none';
1091
+ previousFiles = [];
1092
+ }, 100);
1093
+ return;
1094
+ }
1028
1095
 
1029
1096
  if (files.length > 0) {
1030
1097
  previousFiles = Array.from(files);
@@ -1044,6 +1111,11 @@
1044
1111
  fileInput.value = '';
1045
1112
  previousFiles = [];
1046
1113
  clearButton.style.display = 'none';
1114
+ // Clear any error message
1115
+ const errorElement = fileInput.parentElement.querySelector('.error-' + fileInput.id);
1116
+ if (errorElement) {
1117
+ errorElement.textContent = '';
1118
+ }
1047
1119
  });
1048
1120
  });
1049
1121
 
@@ -1120,15 +1192,43 @@
1120
1192
  const errorElement = document.querySelector(`.error-${fieldId}`);
1121
1193
  const isSingleSelect = dropdown.dataset.singleSelect === 'true';
1122
1194
 
1123
- let selectedValues = isSingleSelect ? null : [];
1195
+ // Initialize selectedValues from hiddenInput (includes prefilled data)
1196
+ let selectedValues = null;
1124
1197
  let selectedValue = null;
1125
1198
  let isClicking = false; // Flag to prevent focus handler from interfering
1126
1199
 
1127
- // Initialize with default value for single-select
1128
- if (isSingleSelect && hiddenInput.value) {
1129
- selectedValue = hiddenInput.value;
1130
- searchInput.value = selectedValue;
1200
+ // Initialize with default value
1201
+ if (isSingleSelect) {
1202
+ if (hiddenInput.value) {
1203
+ selectedValue = hiddenInput.value;
1204
+ searchInput.value = selectedValue;
1205
+
1206
+ }
1207
+ } else {
1208
+ // For multiselect, initialize from hidden input (preserves prefilled values)
1209
+ try {
1210
+ selectedValues = JSON.parse(hiddenInput.value || '[]');
1211
+
1212
+ } catch (e) {
1213
+ selectedValues = [];
1214
+
1215
+ }
1131
1216
  }
1217
+
1218
+ // Listen for prefill events to update internal variables after initialization
1219
+ dropdown.addEventListener('prefill-values', (e) => {
1220
+ if (!isSingleSelect) {
1221
+ selectedValues = e.detail.values || [];
1222
+
1223
+ }
1224
+ });
1225
+
1226
+ dropdown.addEventListener('prefill-single-value', (e) => {
1227
+ if (isSingleSelect) {
1228
+ selectedValue = e.detail.value || '';
1229
+
1230
+ }
1231
+ });
1132
1232
 
1133
1233
  // Toggle dropdown menu
1134
1234
  inputWrapper.addEventListener('mousedown', (e) => {
@@ -1146,10 +1246,30 @@
1146
1246
  // User can type to search, then select an option
1147
1247
  if (isSingleSelect) {
1148
1248
  searchInput.value = '';
1149
- // Show all options when opening
1249
+ let selectedOption = null;
1250
+ // Show all options and re-highlight the selected one
1150
1251
  options.forEach((option) => {
1151
1252
  option.classList.remove('hidden');
1253
+ // Re-highlight the currently selected option
1254
+ if (selectedValue && option.dataset.value === selectedValue) {
1255
+ option.classList.add('selected');
1256
+ selectedOption = option;
1257
+
1258
+ } else {
1259
+ option.classList.remove('selected');
1260
+ }
1152
1261
  });
1262
+
1263
+ // Scroll to the selected option
1264
+ if (selectedOption) {
1265
+ setTimeout(() => {
1266
+ selectedOption.scrollIntoView({
1267
+ block: 'nearest',
1268
+ behavior: 'smooth'
1269
+ });
1270
+
1271
+ }, 20);
1272
+ }
1153
1273
  } else {
1154
1274
  searchInput.value = '';
1155
1275
  }
@@ -1194,10 +1314,28 @@
1194
1314
  if (selectedValue && searchInput.value === selectedValue) {
1195
1315
  searchInput.value = '';
1196
1316
  }
1197
- // Show all options
1317
+ let selectedOption = null;
1318
+ // Show all options and re-highlight the selected one
1198
1319
  options.forEach((option) => {
1199
1320
  option.classList.remove('hidden');
1321
+ // Re-highlight the currently selected option
1322
+ if (selectedValue && option.dataset.value === selectedValue) {
1323
+ option.classList.add('selected');
1324
+ selectedOption = option;
1325
+ } else {
1326
+ option.classList.remove('selected');
1327
+ }
1200
1328
  });
1329
+
1330
+ // Scroll to the selected option
1331
+ if (selectedOption) {
1332
+ setTimeout(() => {
1333
+ selectedOption.scrollIntoView({
1334
+ block: 'nearest',
1335
+ behavior: 'smooth'
1336
+ });
1337
+ }, 20);
1338
+ }
1201
1339
  }
1202
1340
  });
1203
1341
  }
@@ -1213,21 +1351,32 @@
1213
1351
 
1214
1352
  option.addEventListener('click', (e) => {
1215
1353
  if (isSingleSelect) {
1216
- // Single-select: select one value, show in input, close menu
1217
- selectedValue = value;
1218
- searchInput.value = value;
1219
- hiddenInput.value = value;
1354
+ // Single-select: if clicking already selected option, clear it; otherwise select it
1355
+ if (selectedValue === value) {
1356
+ // Unselect: clear everything
1357
+
1358
+ selectedValue = '';
1359
+ searchInput.value = '';
1360
+ hiddenInput.value = '';
1361
+ options.forEach((opt) => opt.classList.remove('selected'));
1362
+ } else {
1363
+ // Select new value
1364
+
1365
+ selectedValue = value;
1366
+ searchInput.value = value;
1367
+ hiddenInput.value = value;
1368
+
1369
+ // Highlight selected option
1370
+ options.forEach((opt) => {
1371
+ opt.classList.remove('selected');
1372
+ if (opt.dataset.value === value) {
1373
+ opt.classList.add('selected');
1374
+ }
1375
+ });
1376
+ }
1377
+
1220
1378
  menu.style.display = 'none';
1221
1379
  searchInput.blur(); // Remove focus
1222
-
1223
- // Highlight selected option
1224
- options.forEach((opt) => {
1225
- opt.classList.remove('selected');
1226
- if (opt.dataset.value === value) {
1227
- opt.classList.add('selected');
1228
- }
1229
- });
1230
-
1231
1380
  validateSearchableDropdown();
1232
1381
  } else {
1233
1382
  // Multiselect: toggle checkbox, show tags
@@ -1236,12 +1385,19 @@
1236
1385
  checkbox.checked = !checkbox.checked;
1237
1386
  }
1238
1387
 
1388
+
1389
+
1390
+
1239
1391
  if (checkbox.checked) {
1240
1392
  if (!selectedValues.includes(value)) {
1241
1393
  selectedValues.push(value);
1394
+
1242
1395
  addTag(value);
1396
+ } else {
1397
+
1243
1398
  }
1244
1399
  } else {
1400
+
1245
1401
  removeValue(value);
1246
1402
  }
1247
1403
 
@@ -1288,7 +1444,9 @@
1288
1444
  }
1289
1445
 
1290
1446
  function updateHiddenInput() {
1447
+
1291
1448
  hiddenInput.value = JSON.stringify(selectedValues);
1449
+
1292
1450
  }
1293
1451
 
1294
1452
  function validateSearchableDropdown() {
@@ -1320,8 +1478,21 @@
1320
1478
 
1321
1479
  // Close all dropdowns when clicking outside
1322
1480
  document.addEventListener('click', () => {
1323
- document.querySelectorAll('.searchable-dropdown-menu').forEach((menu) => {
1324
- menu.style.display = 'none';
1481
+ document.querySelectorAll('.searchable-dropdown').forEach((dropdown) => {
1482
+ const menu = dropdown.querySelector('.searchable-dropdown-menu');
1483
+ const searchInput = dropdown.querySelector('.searchable-dropdown-search');
1484
+ const hiddenInput = dropdown.querySelector('.searchable-dropdown-value');
1485
+ const isSingleSelect = dropdown.dataset.singleSelect === 'true';
1486
+
1487
+ if (menu.style.display === 'block') {
1488
+ menu.style.display = 'none';
1489
+
1490
+ // For single-select: restore the selected value display when closing
1491
+ if (isSingleSelect && hiddenInput.value) {
1492
+ searchInput.value = hiddenInput.value;
1493
+
1494
+ }
1495
+ }
1325
1496
  });
1326
1497
  });
1327
1498
 
@@ -1414,11 +1585,26 @@
1414
1585
  interval = Math.round(interval * 1.1);
1415
1586
  timeoutId = setTimeout(checkExecutionStatus, interval);
1416
1587
  } catch (error) {
1417
- console.error("Error fetching data:", error);
1588
+
1418
1589
  }
1419
1590
  };
1591
+ form.addEventListener('focusout', (e) => {
1592
+ // Validate field when user leaves it
1593
+ e.preventDefault();
1594
+
1595
+ const progressQueryParam = '{{progressQueryParam}}' || 'token';
1596
+ const progressId = new URLSearchParams(window.location.search).get(progressQueryParam);
1597
+ const saveProgressWebhookUrl = '{{saveProgressWebhookUrl}}';
1598
+ const loadProgressWebhookUrl = '{{loadProgressWebhookUrl}}';
1599
+
1600
+
1601
+
1602
+
1603
+
1604
+ });
1420
1605
 
1421
1606
  form.addEventListener('submit', (e) => {
1607
+
1422
1608
  const valid = [];
1423
1609
  e.preventDefault();
1424
1610
 
@@ -1478,10 +1664,31 @@
1478
1664
  }
1479
1665
  });
1480
1666
 
1667
+ // Validate file sizes before submission
1668
+ const fileInputs = form.querySelectorAll('input[type="file"]');
1669
+ let fileSizeValid = true;
1670
+
1671
+ fileInputs.forEach(fileInput => {
1672
+ const wrapper = fileInput.closest('.file-input-wrapper');
1673
+ const maxFileSize = wrapper ? parseFloat(wrapper.dataset.maxFileSize) : 0;
1674
+ if (!validateFileSize(fileInput, maxFileSize)) {
1675
+ fileSizeValid = false;
1676
+ }
1677
+ });
1678
+
1679
+ if (!fileSizeValid) {
1680
+ return;
1681
+ }
1682
+
1481
1683
  if (valid.every((v) => v)) {
1482
1684
  var formData = new FormData();
1483
1685
 
1484
1686
  for (const filed of form.elements) {
1687
+ // Skip radio buttons and checkboxes inside .multiselect (they're handled separately)
1688
+ if (filed.type === 'radio' || (filed.type === 'checkbox' && filed.closest('.multiselect'))) {
1689
+ continue; // Will be handled below
1690
+ }
1691
+
1485
1692
  if(filed.type !== 'file') {
1486
1693
  formData.append(filed.name, filed.value);
1487
1694
  } else {
@@ -1493,7 +1700,18 @@
1493
1700
  }
1494
1701
  }
1495
1702
  }
1496
- document.querySelectorAll('.multiselect').forEach((multiselect) => {
1703
+
1704
+ // Handle radio buttons: wrap in array and stringify (backend expects JSON)
1705
+ document.querySelectorAll('.multiselect[data-radio-select="radio"]').forEach((radioGroup) => {
1706
+ const checkedRadio = radioGroup.querySelector('input[type="radio"]:checked');
1707
+ if (checkedRadio) {
1708
+ // Backend expects JSON array, extracts first element
1709
+ formData.append(radioGroup.id, JSON.stringify([checkedRadio.value]));
1710
+ }
1711
+ });
1712
+
1713
+ // Handle checkboxes: append as JSON array
1714
+ document.querySelectorAll('.multiselect:not([data-radio-select])').forEach((multiselect) => {
1497
1715
  const selectedValues = getSelectedValues(multiselect);
1498
1716
  formData.append(multiselect.id, JSON.stringify(selectedValues));
1499
1717
  });
@@ -1577,7 +1795,7 @@
1577
1795
  return;
1578
1796
  })
1579
1797
  .catch(function (error) {
1580
- console.error('Error:', error);
1798
+
1581
1799
  })
1582
1800
  .finally(() => {
1583
1801
  if (window.location.href.includes('form-waiting')) {
@@ -1587,6 +1805,489 @@
1587
1805
  });
1588
1806
  }
1589
1807
  });
1808
+
1809
+ // ========== AUTO-SAVE FUNCTIONALITY ==========
1810
+ {{#if enableProgressSaving}}
1811
+ (function() {
1812
+ const progressQueryParam = '{{progressQueryParam}}' || 'token';
1813
+ const progressId = new URLSearchParams(window.location.search).get(progressQueryParam);
1814
+ const saveProgressWebhookUrl = '{{saveProgressWebhookUrl}}';
1815
+ const loadProgressWebhookUrl = '{{loadProgressWebhookUrl}}';
1816
+
1817
+ if (!progressId || !saveProgressWebhookUrl || !loadProgressWebhookUrl) {
1818
+ return; // Exit if no progress ID or webhook URLs
1819
+ }
1820
+
1821
+ let savingFields = new Set();
1822
+
1823
+ /**
1824
+ * Get field value by field element
1825
+ */
1826
+ function getFieldValue(field) {
1827
+ if (field.type === 'checkbox' || field.type === 'radio') {
1828
+ return field.checked ? field.value : null;
1829
+ }
1830
+ if (field.tagName === 'SELECT') {
1831
+ return field.value;
1832
+ }
1833
+ if (field.tagName === 'TEXTAREA' || field.tagName === 'INPUT') {
1834
+ return field.value;
1835
+ }
1836
+ return null;
1837
+ }
1838
+
1839
+ /**
1840
+ * Get field name from element
1841
+ */
1842
+ function getFieldName(field) {
1843
+ // First try to get from data attribute
1844
+ if (field.dataset.fieldName) {
1845
+ return field.dataset.fieldName;
1846
+ }
1847
+ // For multiselect and searchable dropdowns, check parent
1848
+ if (field.closest) {
1849
+ const parent = field.closest('.multiselect, .searchable-dropdown');
1850
+ if (parent && parent.dataset.fieldName) {
1851
+ return parent.dataset.fieldName;
1852
+ }
1853
+ }
1854
+ // Fallback to label text or field name
1855
+ const formGroup = field.closest('.form-group');
1856
+ if (formGroup) {
1857
+ const label = formGroup.querySelector('label');
1858
+ if (label) {
1859
+ return label.textContent.trim().replace(' *', '');
1860
+ }
1861
+ }
1862
+ return field.name || field.id;
1863
+ }
1864
+
1865
+ /**
1866
+ * Save individual field update
1867
+ */
1868
+ function saveFieldUpdate(fieldName, value) {
1869
+ if (savingFields.has(fieldName)) {
1870
+ return; // Already saving this field
1871
+ }
1872
+
1873
+ savingFields.add(fieldName);
1874
+
1875
+ // If value is already a JSON string, parse it back to avoid double encoding
1876
+ let finalValue = value;
1877
+ if (typeof value === 'string' && (value.startsWith('[') || value.startsWith('{'))) {
1878
+ try {
1879
+ finalValue = JSON.parse(value);
1880
+ } catch (e) {
1881
+ // If parsing fails, use the original value
1882
+ finalValue = value;
1883
+ }
1884
+ }
1885
+
1886
+ const payload = {
1887
+ [progressQueryParam]: progressId,
1888
+ fieldName: fieldName,
1889
+ value: finalValue
1890
+ };
1891
+
1892
+
1893
+
1894
+ fetch(saveProgressWebhookUrl, {
1895
+ method: 'POST',
1896
+ headers: {
1897
+ 'Content-Type': 'application/json',
1898
+ },
1899
+ body: JSON.stringify(payload),
1900
+ })
1901
+ .then(response => {
1902
+ if (!response.ok) {
1903
+ throw new Error('Save failed');
1904
+ }
1905
+ return response.json();
1906
+ })
1907
+ .catch(error => {
1908
+
1909
+ // Silently fail - don't interrupt user
1910
+ })
1911
+ .finally(() => {
1912
+ savingFields.delete(fieldName);
1913
+ });
1914
+ }
1915
+
1916
+ /**
1917
+ * Handle field change - save individual field update
1918
+ */
1919
+ function handleFieldChange(event) {
1920
+
1921
+ const field = event.target;
1922
+ const fieldName = getFieldName(field);
1923
+
1924
+ if (!fieldName) return;
1925
+
1926
+ // Skip file inputs
1927
+ if (field.type === 'file') return;
1928
+
1929
+ let value = getFieldValue(field);
1930
+
1931
+ // Handle multiselect fields
1932
+ if (field.closest('.multiselect')) {
1933
+ const multiselect = field.closest('.multiselect');
1934
+ value = getSelectedValues(multiselect);
1935
+ }
1936
+
1937
+ // Handle searchable dropdowns
1938
+ if (field.closest('.searchable-dropdown')) {
1939
+ const dropdown = field.closest('.searchable-dropdown');
1940
+ const hiddenInput = dropdown.querySelector('.searchable-dropdown-value');
1941
+ if (hiddenInput) {
1942
+ const isSingleSelect = dropdown.dataset.singleSelect === 'true';
1943
+ if (isSingleSelect) {
1944
+ value = hiddenInput.value;
1945
+ } else {
1946
+ try {
1947
+ value = JSON.parse(hiddenInput.value || '[]');
1948
+ } catch (e) {
1949
+ value = [];
1950
+ }
1951
+ }
1952
+ }
1953
+ }
1954
+
1955
+ // Save the field update
1956
+ saveFieldUpdate(fieldName, value);
1957
+ }
1958
+
1959
+ /**
1960
+ * Load saved progress on page load
1961
+ */
1962
+ async function loadSavedProgress() {
1963
+
1964
+ try {
1965
+ const response = await fetch(`${loadProgressWebhookUrl}?${progressQueryParam}=${encodeURIComponent(progressId)}`);
1966
+
1967
+ // Handle 404: No saved data found (this is normal for new forms)
1968
+ if (response.status === 404) {
1969
+
1970
+ return;
1971
+ }
1972
+
1973
+ // Handle other errors
1974
+ if (!response.ok) {
1975
+
1976
+ return;
1977
+ }
1978
+
1979
+ const result = await response.json();
1980
+ // Expect response format: { fieldId: value, ... } or { formData: { fieldId: value, ... } }
1981
+ const savedData = result.formData || result;
1982
+
1983
+ if (!savedData || Object.keys(savedData).length === 0) {
1984
+
1985
+ return;
1986
+ }
1987
+
1988
+
1989
+
1990
+ // Prefill form fields
1991
+ const form = document.querySelector('#n8n-form');
1992
+ if (!form) return;
1993
+
1994
+
1995
+
1996
+ Object.entries(savedData).forEach(([fieldName, value]) => {
1997
+
1998
+ // Skip non-field keys like token, id, createdAt, etc
1999
+ if (['token', 'id', 'createdAt', 'updatedAt'].includes(fieldName)) {
2000
+ return;
2001
+ }
2002
+
2003
+ // Parse stringified JSON values
2004
+ let parsedValue = value;
2005
+ if (typeof value === 'string' && (value.startsWith('[') || value.startsWith('{'))) {
2006
+ try {
2007
+ parsedValue = JSON.parse(value);
2008
+
2009
+ } catch (e) {
2010
+ parsedValue = value;
2011
+ }
2012
+ }
2013
+
2014
+ // Handle radio buttons - query ALL radios with this field name
2015
+ const radioButtons = form.querySelectorAll(`input[type="radio"][data-field-name="${fieldName}"]`);
2016
+ if (radioButtons.length > 0) {
2017
+ // Radio values should be simple strings, but handle arrays for backward compatibility
2018
+ let radioValue = Array.isArray(parsedValue) ? parsedValue[0] : parsedValue;
2019
+
2020
+ radioButtons.forEach(radio => {
2021
+ radio.checked = radio.value === radioValue;
2022
+
2023
+ });
2024
+ return; // Done with this field
2025
+ }
2026
+
2027
+ // Handle checkboxes - query ALL checkboxes with this field name
2028
+ const checkboxes = form.querySelectorAll(`input[type="checkbox"][data-field-name="${fieldName}"]`);
2029
+ if (checkboxes.length > 0) {
2030
+ // Checkboxes can have multiple values - filter out null/undefined
2031
+ const checkValues = Array.isArray(parsedValue)
2032
+ ? parsedValue.filter(v => v != null)
2033
+ : (parsedValue != null ? [parsedValue] : []);
2034
+
2035
+ checkboxes.forEach(checkbox => {
2036
+ checkbox.checked = checkValues.includes(checkbox.value);
2037
+
2038
+ });
2039
+ return; // Done with this field
2040
+ }
2041
+
2042
+ // Handle regular inputs and textareas by data-field-name
2043
+ const field = form.querySelector(`[data-field-name="${fieldName}"]`);
2044
+ if (field) {
2045
+ if (field.tagName === 'SELECT') {
2046
+ field.value = parsedValue;
2047
+ } else if (field.tagName === 'TEXTAREA' || field.tagName === 'INPUT') {
2048
+ field.value = parsedValue;
2049
+ }
2050
+ }
2051
+
2052
+ // Handle multiselect fields
2053
+ const multiselect = form.querySelector(`.multiselect[data-field-name="${fieldName}"]`);
2054
+ if (multiselect) {
2055
+ const checkboxes = multiselect.querySelectorAll('.multiselect-checkbox');
2056
+ // Filter out null/undefined values
2057
+ const valueArray = Array.isArray(value)
2058
+ ? value.filter(v => v != null)
2059
+ : (value != null ? [value] : []);
2060
+ checkboxes.forEach(checkbox => {
2061
+ checkbox.checked = valueArray.includes(checkbox.value);
2062
+ });
2063
+ }
2064
+
2065
+ // Handle searchable dropdowns
2066
+ const dropdown = form.querySelector(`.searchable-dropdown[data-field-name="${fieldName}"]`);
2067
+ if (dropdown) {
2068
+
2069
+ const hiddenInput = dropdown.querySelector('.searchable-dropdown-value');
2070
+ if (hiddenInput) {
2071
+ const isSingleSelect = dropdown.dataset.singleSelect === 'true';
2072
+ if (isSingleSelect) {
2073
+ hiddenInput.value = String(value);
2074
+
2075
+ // Update visible input
2076
+ const searchInput = dropdown.querySelector('.searchable-dropdown-search');
2077
+ if (searchInput) {
2078
+ searchInput.value = String(value);
2079
+
2080
+ }
2081
+
2082
+ // CRITICAL: Update the dropdown's internal selectedValue variable
2083
+ // Trigger a custom event that the dropdown can listen to
2084
+ dropdown.dispatchEvent(new CustomEvent('prefill-single-value', {
2085
+ detail: { value: String(value) }
2086
+ }));
2087
+ } else {
2088
+ // Multiselect searchable dropdown
2089
+ let valueArray = Array.isArray(value) ? value : (typeof value === 'string' && value.startsWith('[') ? JSON.parse(value) : [value]);
2090
+ // Filter out null/undefined values
2091
+ valueArray = valueArray.filter(v => v != null);
2092
+
2093
+ hiddenInput.value = JSON.stringify(valueArray);
2094
+
2095
+ // CRITICAL: Update the dropdown's internal selectedValues array
2096
+ // We need to access the closure variable that was created during dropdown initialization
2097
+ // Trigger a custom event that the dropdown can listen to
2098
+ dropdown.dispatchEvent(new CustomEvent('prefill-values', {
2099
+ detail: { values: valueArray }
2100
+ }));
2101
+
2102
+ // Clear existing selected tags - CORRECT CLASS NAME
2103
+ const selectedContainer = dropdown.querySelector('.searchable-dropdown-selected');
2104
+ if (selectedContainer) {
2105
+ selectedContainer.innerHTML = '';
2106
+
2107
+ } else {
2108
+
2109
+ }
2110
+
2111
+ // Check the checkboxes and add tags for selected values
2112
+ const checkboxes = dropdown.querySelectorAll('.searchable-dropdown-checkbox');
2113
+ const options = dropdown.querySelectorAll('.searchable-dropdown-option');
2114
+
2115
+
2116
+ options.forEach((option, index) => {
2117
+ const optionValue = option.dataset.value;
2118
+ const checkbox = checkboxes[index];
2119
+ if (checkbox && valueArray.includes(optionValue)) {
2120
+ checkbox.checked = true;
2121
+
2122
+
2123
+ // Add tag to selected container
2124
+ if (selectedContainer) {
2125
+ const tag = document.createElement('div');
2126
+ tag.className = 'searchable-dropdown-tag';
2127
+ tag.dataset.value = optionValue;
2128
+ tag.innerHTML = `
2129
+ <span>${optionValue}</span>
2130
+ <span class="searchable-dropdown-tag-remove">&times;</span>
2131
+ `;
2132
+
2133
+ // Attach remove handler
2134
+ const removeBtn = tag.querySelector('.searchable-dropdown-tag-remove');
2135
+ if (removeBtn) {
2136
+ removeBtn.addEventListener('click', (e) => {
2137
+ e.stopPropagation();
2138
+ // Remove from valueArray
2139
+ const currentValue = JSON.parse(hiddenInput.value || '[]');
2140
+ const newArray = currentValue.filter(v => v !== optionValue);
2141
+ // Update hidden input
2142
+ hiddenInput.value = JSON.stringify(newArray);
2143
+ // Uncheck checkbox
2144
+ checkbox.checked = false;
2145
+ // Remove tag
2146
+ tag.remove();
2147
+
2148
+ });
2149
+ }
2150
+
2151
+ selectedContainer.appendChild(tag);
2152
+
2153
+ }
2154
+ }
2155
+ });
2156
+
2157
+ }
2158
+ }
2159
+ }
2160
+ });
2161
+ } catch (error) {
2162
+
2163
+
2164
+ // Don't show error to user - just proceed with empty form
2165
+ }
2166
+ }
2167
+
2168
+ // Initialize when DOM is ready
2169
+ document.addEventListener('DOMContentLoaded', () => {
2170
+
2171
+ const form = document.querySelector('#n8n-form');
2172
+ if (!form) return;
2173
+
2174
+ // Wait for searchable dropdowns and multiselects to initialize, then load saved data
2175
+ setTimeout(() => {
2176
+ loadSavedProgress();
2177
+ }, 100);
2178
+
2179
+ // Attach focusout listener only for regular inputs/textareas
2180
+ form.addEventListener('focusout', (e) => {
2181
+ const field = e.target;
2182
+ // Only handle text inputs, textareas, email, number, date - not dropdowns
2183
+ if (field.tagName === 'INPUT' && ['text', 'email', 'number', 'date'].includes(field.type)) {
2184
+ handleFieldChange(e);
2185
+ } else if (field.tagName === 'TEXTAREA') {
2186
+ handleFieldChange(e);
2187
+ }
2188
+ });
2189
+
2190
+ // Handle radio button changes
2191
+ document.querySelectorAll('input[type="radio"]').forEach(radio => {
2192
+ radio.addEventListener('change', (e) => {
2193
+ if (e.target.checked) {
2194
+ const fieldName = getFieldName(e.target);
2195
+ const value = e.target.value;
2196
+
2197
+
2198
+
2199
+ if (fieldName) {
2200
+
2201
+ saveFieldUpdate(fieldName, value);
2202
+ }
2203
+ }
2204
+ });
2205
+ });
2206
+
2207
+ // Handle checkbox changes
2208
+ document.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
2209
+ // Skip checkboxes inside multiselect/searchable-dropdown (they're handled separately)
2210
+ if (!checkbox.closest('.multiselect') && !checkbox.closest('.searchable-dropdown')) {
2211
+ checkbox.addEventListener('change', (e) => {
2212
+ const fieldName = getFieldName(e.target);
2213
+ const value = e.target.checked;
2214
+ if (fieldName) {
2215
+
2216
+ saveFieldUpdate(fieldName, value);
2217
+ }
2218
+ });
2219
+ }
2220
+ });
2221
+
2222
+ // Handle multiselect changes
2223
+ document.querySelectorAll('.multiselect').forEach(multiselect => {
2224
+ multiselect.addEventListener('change', (e) => {
2225
+ const fieldName = getFieldName(multiselect);
2226
+ const value = getSelectedValues(multiselect);
2227
+ if (fieldName) {
2228
+ saveFieldUpdate(fieldName, value);
2229
+ }
2230
+ });
2231
+ });
2232
+
2233
+ // Handle searchable dropdown changes - poll for value changes
2234
+ const dropdownStates = new Map();
2235
+ document.querySelectorAll('.searchable-dropdown').forEach(dropdown => {
2236
+ const fieldName = getFieldName(dropdown);
2237
+ const hiddenInput = dropdown.querySelector('.searchable-dropdown-value');
2238
+ const isSingleSelect = dropdown.dataset.singleSelect === 'true';
2239
+
2240
+ if (hiddenInput && fieldName) {
2241
+ // Store initial value
2242
+ dropdownStates.set(fieldName, hiddenInput.value);
2243
+
2244
+ // Poll for changes every 300ms
2245
+ setInterval(() => {
2246
+ const currentValue = hiddenInput.value;
2247
+ const previousValue = dropdownStates.get(fieldName);
2248
+
2249
+ if (currentValue !== previousValue) {
2250
+
2251
+
2252
+
2253
+ dropdownStates.set(fieldName, currentValue);
2254
+
2255
+ let value;
2256
+ if (isSingleSelect) {
2257
+ value = currentValue;
2258
+ } else {
2259
+ try {
2260
+ value = JSON.parse(currentValue || '[]');
2261
+ // Filter out null/undefined values from multiselect arrays
2262
+ value = value.filter(v => v != null);
2263
+
2264
+ } catch (e) {
2265
+ value = [];
2266
+
2267
+ }
2268
+ }
2269
+
2270
+ // ALWAYS save, even empty values (they represent cleared fields)
2271
+
2272
+ saveFieldUpdate(fieldName, value);
2273
+ }
2274
+ }, 300);
2275
+ }
2276
+ });
2277
+
2278
+ // Handle regular select dropdowns
2279
+ document.querySelectorAll('select').forEach(select => {
2280
+ select.addEventListener('change', (e) => {
2281
+ const fieldName = getFieldName(select);
2282
+ if (fieldName) {
2283
+ saveFieldUpdate(fieldName, select.value);
2284
+ }
2285
+ });
2286
+ });
2287
+ });
2288
+ })();
2289
+ {{/if}}
2290
+ // ========== END AUTO-SAVE FUNCTIONALITY ==========
1590
2291
  </script>
1591
2292
  </body>
1592
2293
  </html>