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.
- package/dist/credentials/GithubIssuesApi.credentials.js +1 -36
- package/dist/credentials/GithubIssuesOAuth2Api.credentials.js +1 -53
- package/dist/nodes/Form123/Form.node.js +1 -388
- package/dist/nodes/Form123/Form.node.js.map +1 -1
- package/dist/nodes/Form123/FormTrigger.node.js +1 -27
- package/dist/nodes/Form123/common.descriptions.js +1 -630
- package/dist/nodes/Form123/common.descriptions.js.map +1 -1
- package/dist/nodes/Form123/cssVariables.js +1 -73
- package/dist/nodes/Form123/interfaces.d.ts +5 -0
- package/dist/nodes/Form123/interfaces.js +1 -4
- package/dist/nodes/Form123/interfaces.js.map +1 -1
- package/dist/nodes/Form123/templates/form-trigger-completion.handlebars +2 -1
- package/dist/nodes/Form123/templates/form-trigger.handlebars +774 -73
- package/dist/nodes/Form123/utils/descriptions.js +1 -10
- package/dist/nodes/Form123/utils/formCompletionUtils.js +1 -106
- package/dist/nodes/Form123/utils/formNodeUtils.js +3 -75
- package/dist/nodes/Form123/utils/formNodeUtils.js.map +1 -1
- package/dist/nodes/Form123/utils/templateRenderer.js +1 -71
- package/dist/nodes/Form123/utils/utilities.js +3 -47
- package/dist/nodes/Form123/utils/utils.d.ts +11 -3
- package/dist/nodes/Form123/utils/utils.js +3 -592
- package/dist/nodes/Form123/utils/utils.js.map +1 -1
- package/dist/nodes/Form123/utils/waitUtils.js +1 -67
- package/dist/nodes/Form123/v1/FormTriggerV1.node.js +1 -82
- package/dist/nodes/Form123/v2/FormTriggerV2.node.js +1 -193
- package/dist/nodes/Form123/v2/FormTriggerV2.node.js.map +1 -1
- package/dist/package.json +3 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- 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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
|
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">×</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
|
|
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
|
-
|
|
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
|
|
1128
|
-
if (isSingleSelect
|
|
1129
|
-
|
|
1130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
1217
|
-
selectedValue
|
|
1218
|
-
|
|
1219
|
-
|
|
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
|
|
1324
|
-
menu
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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">×</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>
|