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.
- 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 -647
- package/dist/nodes/Form123/cssVariables.js +1 -73
- package/dist/nodes/Form123/interfaces.d.ts +4 -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 +1 -1
- package/dist/nodes/Form123/templates/form-trigger.handlebars +694 -71
- 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 -593
- 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
|
@@ -314,48 +314,48 @@
|
|
|
314
314
|
min-width: var(--checkbox-size);
|
|
315
315
|
}
|
|
316
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1190
|
-
if (isSingleSelect
|
|
1191
|
-
|
|
1192
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
1279
|
-
selectedValue
|
|
1280
|
-
|
|
1281
|
-
|
|
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
|
|
1386
|
-
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
|
+
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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">×</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>
|