n8n-nodes-extended-forms 0.2.6 → 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 (28) 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 -647
  7. package/dist/nodes/Form123/cssVariables.js +1 -73
  8. package/dist/nodes/Form123/interfaces.d.ts +4 -0
  9. package/dist/nodes/Form123/interfaces.js +1 -4
  10. package/dist/nodes/Form123/interfaces.js.map +1 -1
  11. package/dist/nodes/Form123/templates/form-trigger-completion.handlebars +1 -1
  12. package/dist/nodes/Form123/templates/form-trigger.handlebars +694 -71
  13. package/dist/nodes/Form123/utils/descriptions.js +1 -10
  14. package/dist/nodes/Form123/utils/formCompletionUtils.js +1 -106
  15. package/dist/nodes/Form123/utils/formNodeUtils.js +3 -75
  16. package/dist/nodes/Form123/utils/formNodeUtils.js.map +1 -1
  17. package/dist/nodes/Form123/utils/templateRenderer.js +1 -71
  18. package/dist/nodes/Form123/utils/utilities.js +3 -47
  19. package/dist/nodes/Form123/utils/utils.d.ts +11 -3
  20. package/dist/nodes/Form123/utils/utils.js +3 -593
  21. package/dist/nodes/Form123/utils/utils.js.map +1 -1
  22. package/dist/nodes/Form123/utils/waitUtils.js +1 -67
  23. package/dist/nodes/Form123/v1/FormTriggerV1.node.js +1 -82
  24. package/dist/nodes/Form123/v2/FormTriggerV2.node.js +1 -193
  25. package/dist/nodes/Form123/v2/FormTriggerV2.node.js.map +1 -1
  26. package/dist/package.json +3 -1
  27. package/dist/tsconfig.tsbuildinfo +1 -1
  28. package/package.json +3 -1
@@ -314,48 +314,48 @@
314
314
  min-width: var(--checkbox-size);
315
315
  }
316
316
 
317
- input[type='checkbox'] {
318
- appearance: none;
319
- width: var(--checkbox-size);
320
- height: var(--checkbox-size);
321
- border: 1px solid var(--color-input-border);
322
- border-radius: 3px;
323
- cursor: pointer;
324
- position: relative;
325
- }
326
- .multiselect-checkbox:checked {
327
- background: var(--color-focus-border);
328
- border-color: var(--color-focus-border);
329
- }
330
- .multiselect-checkbox:checked::after {
331
- content: '✔';
332
- position: absolute;
333
- top: 50%;
334
- left: 50%;
335
- transform: translate(-50%, -50%);
336
- color: white;
337
- font-size: var(--font-size-label);
338
- font-weight: bold;
339
- }
340
-
341
- .multiselect[data-radio-select] .multiselect-checkbox {
342
- border-radius: 50%;
343
- width: var(--checkbox-size);
344
- height: var(--checkbox-size);
345
- display: flex;
346
- align-items: center;
347
- justify-content: center;
348
- }
349
-
350
- .multiselect[data-radio-select] .multiselect-checkbox:checked {
351
- background: white;
352
- border-color: var(--color-focus-border);
353
- border-width: 4px;
354
- border-radius: 50%;
355
- }
356
- .multiselect[data-radio-select] .multiselect-checkbox:checked::after {
357
- content: '';
358
- }
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
+ }
359
359
 
360
360
  /* searchable dropdown ----------------------------- */
361
361
  .searchable-dropdown {
@@ -665,6 +665,7 @@
665
665
  <div
666
666
  class='multiselect {{inputRequired}}'
667
667
  id='{{id}}'
668
+ data-field-name='{{label}}'
668
669
  {{#if radioSelect}}
669
670
  data-radio-select='{{radioSelect}}'
670
671
  {{/if}}
@@ -680,7 +681,14 @@
680
681
  >
681
682
  {{#each multiSelectOptions}}
682
683
  <div class='multiselect-option'>
683
- <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
+ />
684
692
  <label for='{{id}}'>{{label}}</label>
685
693
  </div>
686
694
  {{/each}}
@@ -695,7 +703,7 @@
695
703
  <div class='form-group {{#if showWhen}}input--hide{{/if}}' {{#if showWhen}}data-trigger-field='{{showWhen.fieldId}}' data-trigger-values='{{json showWhen.values}}'{{/if}}>
696
704
  <label class='form-label {{inputRequired}}' for='{{id}}'>{{label}}</label>
697
705
  <div class='select-input'>
698
- <select id='{{id}}' name='{{id}}' class='{{inputRequired}}'>
706
+ <select id='{{id}}' name='{{id}}' class='{{inputRequired}}' data-field-name='{{label}}'>
699
707
  <option value='' disabled selected>Select an option ...</option>
700
708
  {{#each selectOptions}}
701
709
  <option value='{{this}}'>{{this}}</option>
@@ -711,7 +719,7 @@
711
719
  {{#if isSearchableDropdown}}
712
720
  <div class='form-group {{#if showWhen}}input--hide{{/if}}' {{#if showWhen}}data-trigger-field='{{showWhen.fieldId}}' data-trigger-values='{{json showWhen.values}}'{{/if}}>
713
721
  <label class='form-label {{inputRequired}}' for='{{id}}'>{{label}}</label>
714
- <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'>
715
723
  <div class='searchable-dropdown-input-wrapper'>
716
724
  <input
717
725
  type='text'
@@ -741,7 +749,7 @@
741
749
  {{#if isSearchableMultiselect}}
742
750
  <div class='form-group {{#if showWhen}}input--hide{{/if}}' {{#if showWhen}}data-trigger-field='{{showWhen.fieldId}}' data-trigger-values='{{json showWhen.values}}'{{/if}}>
743
751
  <label class='form-label {{inputRequired}}' for='{{id}}'>{{label}}</label>
744
- <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'>
745
753
  <div class='searchable-dropdown-input-wrapper'>
746
754
  <input
747
755
  type='text'
@@ -790,6 +798,7 @@
790
798
  class='form-input {{inputRequired}}'
791
799
  id='{{id}}'
792
800
  name='{{id}}'
801
+ data-field-name='{{label}}'
793
802
  placeholder="{{placeholder}}"
794
803
  >{{defaultValue}}</textarea>
795
804
  <p class='{{errorId}} error-hidden'>
@@ -826,6 +835,7 @@
826
835
  type='{{type}}'
827
836
  id='{{id}}'
828
837
  name='{{id}}'
838
+ data-field-name='{{label}}'
829
839
  value="{{defaultValue}}"
830
840
  placeholder="{{placeholder}}"
831
841
  />
@@ -1182,15 +1192,43 @@
1182
1192
  const errorElement = document.querySelector(`.error-${fieldId}`);
1183
1193
  const isSingleSelect = dropdown.dataset.singleSelect === 'true';
1184
1194
 
1185
- let selectedValues = isSingleSelect ? null : [];
1195
+ // Initialize selectedValues from hiddenInput (includes prefilled data)
1196
+ let selectedValues = null;
1186
1197
  let selectedValue = null;
1187
1198
  let isClicking = false; // Flag to prevent focus handler from interfering
1188
1199
 
1189
- // Initialize with default value for single-select
1190
- if (isSingleSelect && hiddenInput.value) {
1191
- selectedValue = hiddenInput.value;
1192
- 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
+ }
1193
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
+ });
1194
1232
 
1195
1233
  // Toggle dropdown menu
1196
1234
  inputWrapper.addEventListener('mousedown', (e) => {
@@ -1208,10 +1246,30 @@
1208
1246
  // User can type to search, then select an option
1209
1247
  if (isSingleSelect) {
1210
1248
  searchInput.value = '';
1211
- // Show all options when opening
1249
+ let selectedOption = null;
1250
+ // Show all options and re-highlight the selected one
1212
1251
  options.forEach((option) => {
1213
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
+ }
1214
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
+ }
1215
1273
  } else {
1216
1274
  searchInput.value = '';
1217
1275
  }
@@ -1256,10 +1314,28 @@
1256
1314
  if (selectedValue && searchInput.value === selectedValue) {
1257
1315
  searchInput.value = '';
1258
1316
  }
1259
- // Show all options
1317
+ let selectedOption = null;
1318
+ // Show all options and re-highlight the selected one
1260
1319
  options.forEach((option) => {
1261
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
+ }
1262
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
+ }
1263
1339
  }
1264
1340
  });
1265
1341
  }
@@ -1275,21 +1351,32 @@
1275
1351
 
1276
1352
  option.addEventListener('click', (e) => {
1277
1353
  if (isSingleSelect) {
1278
- // Single-select: select one value, show in input, close menu
1279
- selectedValue = value;
1280
- searchInput.value = value;
1281
- 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
+
1282
1378
  menu.style.display = 'none';
1283
1379
  searchInput.blur(); // Remove focus
1284
-
1285
- // Highlight selected option
1286
- options.forEach((opt) => {
1287
- opt.classList.remove('selected');
1288
- if (opt.dataset.value === value) {
1289
- opt.classList.add('selected');
1290
- }
1291
- });
1292
-
1293
1380
  validateSearchableDropdown();
1294
1381
  } else {
1295
1382
  // Multiselect: toggle checkbox, show tags
@@ -1298,12 +1385,19 @@
1298
1385
  checkbox.checked = !checkbox.checked;
1299
1386
  }
1300
1387
 
1388
+
1389
+
1390
+
1301
1391
  if (checkbox.checked) {
1302
1392
  if (!selectedValues.includes(value)) {
1303
1393
  selectedValues.push(value);
1394
+
1304
1395
  addTag(value);
1396
+ } else {
1397
+
1305
1398
  }
1306
1399
  } else {
1400
+
1307
1401
  removeValue(value);
1308
1402
  }
1309
1403
 
@@ -1350,7 +1444,9 @@
1350
1444
  }
1351
1445
 
1352
1446
  function updateHiddenInput() {
1447
+
1353
1448
  hiddenInput.value = JSON.stringify(selectedValues);
1449
+
1354
1450
  }
1355
1451
 
1356
1452
  function validateSearchableDropdown() {
@@ -1382,8 +1478,21 @@
1382
1478
 
1383
1479
  // Close all dropdowns when clicking outside
1384
1480
  document.addEventListener('click', () => {
1385
- document.querySelectorAll('.searchable-dropdown-menu').forEach((menu) => {
1386
- 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
+ }
1387
1496
  });
1388
1497
  });
1389
1498
 
@@ -1476,11 +1585,26 @@
1476
1585
  interval = Math.round(interval * 1.1);
1477
1586
  timeoutId = setTimeout(checkExecutionStatus, interval);
1478
1587
  } catch (error) {
1479
- console.error("Error fetching data:", error);
1588
+
1480
1589
  }
1481
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
+ });
1482
1605
 
1483
1606
  form.addEventListener('submit', (e) => {
1607
+
1484
1608
  const valid = [];
1485
1609
  e.preventDefault();
1486
1610
 
@@ -1560,6 +1684,11 @@
1560
1684
  var formData = new FormData();
1561
1685
 
1562
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
+
1563
1692
  if(filed.type !== 'file') {
1564
1693
  formData.append(filed.name, filed.value);
1565
1694
  } else {
@@ -1571,7 +1700,18 @@
1571
1700
  }
1572
1701
  }
1573
1702
  }
1574
- 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) => {
1575
1715
  const selectedValues = getSelectedValues(multiselect);
1576
1716
  formData.append(multiselect.id, JSON.stringify(selectedValues));
1577
1717
  });
@@ -1655,7 +1795,7 @@
1655
1795
  return;
1656
1796
  })
1657
1797
  .catch(function (error) {
1658
- console.error('Error:', error);
1798
+
1659
1799
  })
1660
1800
  .finally(() => {
1661
1801
  if (window.location.href.includes('form-waiting')) {
@@ -1665,6 +1805,489 @@
1665
1805
  });
1666
1806
  }
1667
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 ==========
1668
2291
  </script>
1669
2292
  </body>
1670
2293
  </html>