@verbb/formie-browser 1.0.2 → 1.0.4
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/CHANGELOG.md +16 -0
- package/dist/chunks/{address-finder-CQW81kY0.js → address-finder-BU4roxT6.js} +2 -2
- package/dist/chunks/{bpoint-D8OjLmsn.js → bpoint-uNg5j6rt.js} +1 -1
- package/dist/chunks/{calculations-BqZsENCq.js → calculations-C9t6Zop9.js} +2 -2
- package/dist/chunks/{captcha-eu-DIa6aUWz.js → captcha-eu-BAPzHNts.js} +2 -2
- package/dist/chunks/{checkbox-radio-BI-UjgMF.js → checkbox-radio-CHeLNVIK.js} +39 -34
- package/dist/chunks/{conditions-BVl6jTYo.js → conditions-DKxVCsjD.js} +189 -163
- package/dist/chunks/{date-picker-69TgvErA.js → date-picker-B3iQ797Z.js} +2 -2
- package/dist/chunks/{eway-1GPxH6DU.js → eway-DCielIML.js} +2 -2
- package/dist/chunks/file-upload-DP2TOUYV.js +316 -0
- package/dist/chunks/{friendly-captcha-v1-DA2QPKA3.js → friendly-captcha-v1-DdlcGiFY.js} +1 -1
- package/dist/chunks/{friendly-captcha-v2-BorOWiz_.js → friendly-captcha-v2-76w-YnFm.js} +1 -1
- package/dist/chunks/{go-cardless-DwBgERcA.js → go-cardless-rwCkzbNK.js} +1 -1
- package/dist/chunks/{google-address-BcTBncyS.js → google-address-BD8IZ86V.js} +77 -58
- package/dist/chunks/{hcaptcha-BchdVCqq.js → hcaptcha-BNNTL6uf.js} +2 -2
- package/dist/chunks/{hidden-D8RqSPt7.js → hidden-CW6Rcf1N.js} +1 -1
- package/dist/chunks/{index-C1ZOKiAi.js → index-D6npYxvr.js} +1372 -1178
- package/dist/chunks/{loqate-nXuTX7In.js → loqate-B-ZNS65B.js} +2 -2
- package/dist/chunks/{mollie-qdCynQCd.js → mollie-BDAZ-1X4.js} +1 -1
- package/dist/chunks/{moneris-BNEA-laO.js → moneris-B1b0uEtY.js} +1 -1
- package/dist/chunks/{opayo-BJtXOkuK.js → opayo-CC5hraHo.js} +2 -2
- package/dist/chunks/{paddle-D72Pa8kr.js → paddle-B4GmrDsq.js} +2 -2
- package/dist/chunks/{paypal-Cqt2bned.js → paypal-BFS6XsIS.js} +2 -2
- package/dist/chunks/{payway-BjCqsc0H.js → payway-DekoJead.js} +2 -2
- package/dist/chunks/{phone-country-K9pYAlzM.js → phone-country-Cm5YUeXI.js} +2 -2
- package/dist/chunks/{place-kit-Dg6f_UoM.js → place-kit-DizZWV_e.js} +1 -1
- package/dist/chunks/{recaptcha-enterprise--VYAeeqz.js → recaptcha-enterprise-BbOvO7JZ.js} +2 -2
- package/dist/chunks/{recaptcha-shared-PSIEEEe1.js → recaptcha-shared-Dwg1eVFS.js} +2 -2
- package/dist/chunks/{recaptcha-v2-checkbox-atBNe5ml.js → recaptcha-v2-checkbox-SY3ti5iQ.js} +2 -2
- package/dist/chunks/{recaptcha-v2-invisible-DnBufNPT.js → recaptcha-v2-invisible-DSmQnn6c.js} +2 -2
- package/dist/chunks/{recaptcha-v3-CF6O8rDj.js → recaptcha-v3-DNh7YCnR.js} +2 -2
- package/dist/chunks/{repeater-CB7Yojvx.js → repeater-DM0vuFNQ.js} +2 -2
- package/dist/chunks/{rich-text-nAV_SQZo.js → rich-text-C8aGYp70.js} +1 -1
- package/dist/chunks/{scripts-BkAAux0i.js → scripts-CKTFtxPO.js} +1 -1
- package/dist/chunks/{shared-DGqeMvcJ.js → shared--BU5CFoL.js} +4 -4
- package/dist/chunks/{signature-BT6CsQ6n.js → signature-pFhZfH7d.js} +1 -1
- package/dist/chunks/{snaptcha-C7qQo02f.js → snaptcha-VuOUOaLj.js} +1 -1
- package/dist/chunks/{square-C5AH3trb.js → square-BR9pvToy.js} +4 -4
- package/dist/chunks/{stripe-EJK0RyUv.js → stripe-OHUwoV9g.js} +2 -2
- package/dist/chunks/{summary-DC_ipXCU.js → summary-Da7Ktg3N.js} +2 -2
- package/dist/chunks/{table-BEAI99qo.js → table-C0PmR9bI.js} +2 -2
- package/dist/chunks/text-limit-mLEwZDIa.js +144 -0
- package/dist/chunks/{turnstile-_uCRwnU-.js → turnstile-9tM7q_LK.js} +2 -2
- package/dist/css/theme/forms/_form.css +28 -0
- package/dist/index.js +9 -9
- package/dist/js/core/create-formie-client.d.ts.map +1 -1
- package/dist/js/core/field-error-container.d.ts +4 -0
- package/dist/js/core/field-error-container.d.ts.map +1 -0
- package/dist/js/core/submit-result-state.d.ts.map +1 -1
- package/dist/js/core/submit-result-ui.d.ts.map +1 -1
- package/dist/js/modules/address/google-address.d.ts +7 -0
- package/dist/js/modules/address/google-address.d.ts.map +1 -1
- package/dist/js/modules/fields/checkbox-radio.d.ts.map +1 -1
- package/dist/js/modules/fields/conditions/effects.d.ts +3 -1
- package/dist/js/modules/fields/conditions/effects.d.ts.map +1 -1
- package/dist/js/modules/fields/conditions.d.ts.map +1 -1
- package/dist/js/modules/fields/file-upload.d.ts.map +1 -1
- package/dist/js/modules/fields/text-limit.d.ts.map +1 -1
- package/dist/js/submit/pipeline.d.ts.map +1 -1
- package/dist/js/utils/i18n.d.ts.map +1 -1
- package/dist/js/validation/rules/email.d.ts.map +1 -1
- package/dist/js/validation/rules/number.d.ts.map +1 -1
- package/dist/js/validation/scope.d.ts +6 -0
- package/dist/js/validation/scope.d.ts.map +1 -0
- package/dist/js/validation/submit-readiness.d.ts +4 -0
- package/dist/js/validation/submit-readiness.d.ts.map +1 -0
- package/dist/js/validation/validator.d.ts +1 -0
- package/dist/js/validation/validator.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/css/theme/forms/_form.css +28 -0
- package/src/js/core/create-formie-client.ts +6 -0
- package/src/js/core/field-error-container.ts +39 -0
- package/src/js/core/submit-result-state.ts +29 -2
- package/src/js/core/submit-result-ui.ts +3 -9
- package/src/js/modules/address/google-address.ts +33 -1
- package/src/js/modules/fields/checkbox-radio.ts +21 -15
- package/src/js/modules/fields/conditions/effects.ts +79 -2
- package/src/js/modules/fields/conditions.ts +53 -11
- package/src/js/modules/fields/file-upload.ts +200 -105
- package/src/js/modules/fields/text-limit.ts +87 -22
- package/src/js/submit/pipeline.ts +3 -28
- package/src/js/utils/i18n.ts +184 -5
- package/src/js/validation/rules/email.ts +3 -2
- package/src/js/validation/rules/match.ts +2 -2
- package/src/js/validation/rules/minmax.ts +4 -4
- package/src/js/validation/rules/number.ts +9 -5
- package/src/js/validation/rules/pattern.ts +4 -4
- package/src/js/validation/rules/required.ts +1 -1
- package/src/js/validation/rules/url.ts +1 -1
- package/src/js/validation/scope.ts +25 -0
- package/src/js/validation/submit-readiness.ts +157 -0
- package/src/js/validation/validator.ts +11 -7
- package/dist/chunks/file-upload-BDZZftgL.js +0 -281
- package/dist/chunks/text-limit-Blgq5G-G.js +0 -135
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import fileUploadCss from '#theme-css/fields/_file.css?inline';
|
|
2
2
|
|
|
3
3
|
import type { FormieModuleDefinition } from '#contracts/modules';
|
|
4
|
-
import { dispatchFieldEvent,
|
|
4
|
+
import { dispatchFieldEvent, releaseFormValidators, retainFormValidators } from '#modules/fields/shared';
|
|
5
5
|
import { ensureModuleStyles } from '#modules/styles';
|
|
6
|
-
import { getFileUploadEventName, getFormStateEventName } from '#utils/event-names';
|
|
6
|
+
import { getFieldModuleEventName, getFileUploadEventName, getFormStateEventName } from '#utils/event-names';
|
|
7
7
|
import { requestJson } from '#utils/http';
|
|
8
8
|
|
|
9
9
|
const INPUT_SELECTOR = 'input[type="file"][data-formie-file-input]';
|
|
@@ -20,6 +20,8 @@ const FILE_VALIDATORS = [
|
|
|
20
20
|
] as const;
|
|
21
21
|
const VALIDATOR_SCOPE = 'file-upload';
|
|
22
22
|
const MODULE_ID = 'file-upload';
|
|
23
|
+
const REPEATER_INIT_ROW_EVENT = getFieldModuleEventName('repeater', 'init-row');
|
|
24
|
+
const FIELD_HANDLE_SELECTOR = '[data-formie-field-handle]';
|
|
23
25
|
|
|
24
26
|
ensureModuleStyles(MODULE_ID, [fileUploadCss]);
|
|
25
27
|
|
|
@@ -237,9 +239,10 @@ function registerValidators(form: HTMLFormElement | null): void {
|
|
|
237
239
|
|
|
238
240
|
return input.files.length <= limit;
|
|
239
241
|
}, ({ input, t }) => {
|
|
240
|
-
return
|
|
241
|
-
|
|
242
|
-
|
|
242
|
+
return input.getAttribute('data-formie-validation-max-files-message')
|
|
243
|
+
?? t('Choose up to {files} files.', {
|
|
244
|
+
files: input.getAttribute('data-formie-file-limit') || '',
|
|
245
|
+
});
|
|
243
246
|
});
|
|
244
247
|
|
|
245
248
|
validator.addValidator('fileSizeMinLimit', ({ input }) => {
|
|
@@ -254,9 +257,10 @@ function registerValidators(form: HTMLFormElement | null): void {
|
|
|
254
257
|
return file.size >= sizeBytes;
|
|
255
258
|
});
|
|
256
259
|
}, ({ input, t }) => {
|
|
257
|
-
return
|
|
258
|
-
|
|
259
|
-
|
|
260
|
+
return input.getAttribute('data-formie-validation-min-file-size-message')
|
|
261
|
+
?? t('File must be larger than {filesize} MB.', {
|
|
262
|
+
filesize: input.getAttribute('data-formie-size-min-limit') || '',
|
|
263
|
+
});
|
|
260
264
|
});
|
|
261
265
|
|
|
262
266
|
validator.addValidator('fileSizeMaxLimit', ({ input }) => {
|
|
@@ -271,9 +275,10 @@ function registerValidators(form: HTMLFormElement | null): void {
|
|
|
271
275
|
return file.size <= sizeBytes;
|
|
272
276
|
});
|
|
273
277
|
}, ({ input, t }) => {
|
|
274
|
-
return
|
|
275
|
-
|
|
276
|
-
|
|
278
|
+
return input.getAttribute('data-formie-validation-max-file-size-message')
|
|
279
|
+
?? t('File must be smaller than {filesize} MB.', {
|
|
280
|
+
filesize: input.getAttribute('data-formie-size-max-limit') || '',
|
|
281
|
+
});
|
|
277
282
|
});
|
|
278
283
|
});
|
|
279
284
|
}
|
|
@@ -366,6 +371,47 @@ function updateUploadedAssetInputs(field: HTMLElement, input: HTMLInputElement,
|
|
|
366
371
|
input.value = '';
|
|
367
372
|
}
|
|
368
373
|
|
|
374
|
+
function getFormHandle(form: HTMLFormElement | null): string {
|
|
375
|
+
if (!form) {
|
|
376
|
+
return '';
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const handleInput = form.querySelector('input[name="handle"]');
|
|
380
|
+
|
|
381
|
+
if (handleInput instanceof HTMLInputElement && handleInput.value.trim()) {
|
|
382
|
+
return handleInput.value.trim();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return form.getAttribute('data-formie-handle')?.trim() || '';
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function buildHydrateFormData(field: HTMLElement, input: HTMLInputElement, assetIds: number[]): FormData {
|
|
389
|
+
const form = input.form;
|
|
390
|
+
const body = new FormData();
|
|
391
|
+
const handle = getFormHandle(form);
|
|
392
|
+
const fieldHandle = getFieldHandle(field);
|
|
393
|
+
|
|
394
|
+
if (handle) {
|
|
395
|
+
body.append('handle', handle);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (fieldHandle) {
|
|
399
|
+
body.append('fieldHandle', fieldHandle);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const submissionUidInput = form?.querySelector('input[name="submissionUid"]');
|
|
403
|
+
|
|
404
|
+
if (submissionUidInput instanceof HTMLInputElement && submissionUidInput.value.trim()) {
|
|
405
|
+
body.append('submissionUid', submissionUidInput.value.trim());
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
assetIds.forEach((assetId) => {
|
|
409
|
+
body.append('assetIds[]', String(assetId));
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
return body;
|
|
413
|
+
}
|
|
414
|
+
|
|
369
415
|
async function hydrateUploadedAssets(field: HTMLElement, input: HTMLInputElement, uploadedAssets: UploadedAsset[]): Promise<UploadedAsset[]> {
|
|
370
416
|
const assetIds = uploadedAssets.map((asset) => {
|
|
371
417
|
return asset.assetId;
|
|
@@ -378,14 +424,9 @@ async function hydrateUploadedAssets(field: HTMLElement, input: HTMLInputElement
|
|
|
378
424
|
}
|
|
379
425
|
|
|
380
426
|
const hydrateEndpoint = input.getAttribute(HYDRATE_ENDPOINT_ATTR)?.trim() || '/actions/formie/file-upload/hydrate';
|
|
381
|
-
const body = new FormData();
|
|
382
|
-
assetIds.forEach((assetId) => {
|
|
383
|
-
body.append('assetIds[]', String(assetId));
|
|
384
|
-
});
|
|
385
|
-
|
|
386
427
|
const response = await requestJson<FileUploadHydrateResponse>(hydrateEndpoint, {
|
|
387
428
|
method: 'POST',
|
|
388
|
-
body,
|
|
429
|
+
body: buildHydrateFormData(field, input, assetIds),
|
|
389
430
|
});
|
|
390
431
|
const hydratedAssets = normalizeUploadedAssets(response.assets);
|
|
391
432
|
|
|
@@ -504,6 +545,118 @@ function getUploadedAssetsFromEvent(event: Event, matcher: FileUploadEventMatche
|
|
|
504
545
|
return [];
|
|
505
546
|
}
|
|
506
547
|
|
|
548
|
+
function getFieldContainer(input: HTMLInputElement): HTMLElement | null {
|
|
549
|
+
const field = input.closest(FIELD_HANDLE_SELECTOR);
|
|
550
|
+
|
|
551
|
+
return field instanceof HTMLElement ? field : null;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function collectFileUploadInputs(root: ParentNode): HTMLInputElement[] {
|
|
555
|
+
return Array.from(root.querySelectorAll(INPUT_SELECTOR)).filter((input): input is HTMLInputElement => {
|
|
556
|
+
return input instanceof HTMLInputElement;
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function bindFileUploadInput(field: HTMLElement, input: HTMLInputElement, form: HTMLFormElement | null): () => void {
|
|
561
|
+
const state: FileUploadState = {
|
|
562
|
+
field,
|
|
563
|
+
input,
|
|
564
|
+
summaryRoot: ensureSummaryRoot(field, input),
|
|
565
|
+
uploadedAssets: mergeUploadedAssets(
|
|
566
|
+
readUploadedAssetsFromHiddenInputs(field, input),
|
|
567
|
+
readSummaryItems(field.querySelector('[data-formie-file-summary]') as HTMLElement | null),
|
|
568
|
+
),
|
|
569
|
+
pendingFiles: [],
|
|
570
|
+
hydrationToken: 0,
|
|
571
|
+
};
|
|
572
|
+
const syncPendingFiles = () => {
|
|
573
|
+
state.pendingFiles = Array.from(input.files || []).map((file) => {
|
|
574
|
+
return file.name;
|
|
575
|
+
});
|
|
576
|
+
renderSummary(state);
|
|
577
|
+
};
|
|
578
|
+
const syncUploadedAssets = async(uploadedAssets: UploadedAsset[]) => {
|
|
579
|
+
state.hydrationToken += 1;
|
|
580
|
+
const hydrationToken = state.hydrationToken;
|
|
581
|
+
state.pendingFiles = [];
|
|
582
|
+
state.uploadedAssets = uploadedAssets;
|
|
583
|
+
updateUploadedAssetInputs(field, input, uploadedAssets);
|
|
584
|
+
renderSummary(state);
|
|
585
|
+
|
|
586
|
+
try {
|
|
587
|
+
const hydratedAssets = await hydrateUploadedAssets(field, input, uploadedAssets);
|
|
588
|
+
|
|
589
|
+
if (hydrationToken !== state.hydrationToken) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
state.uploadedAssets = hydratedAssets;
|
|
594
|
+
renderSummary(state);
|
|
595
|
+
dispatchFieldEvent(field, MODULE_ID, 'uploaded-assets-sync', {
|
|
596
|
+
fileUpload: field,
|
|
597
|
+
assets: hydratedAssets,
|
|
598
|
+
});
|
|
599
|
+
} catch (error) {
|
|
600
|
+
console.error('[formie] Failed to hydrate uploaded file details.', error);
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
const resetUploadedAssets = () => {
|
|
604
|
+
state.hydrationToken += 1;
|
|
605
|
+
state.pendingFiles = [];
|
|
606
|
+
state.uploadedAssets = [];
|
|
607
|
+
updateUploadedAssetInputs(field, input, []);
|
|
608
|
+
renderSummary(state);
|
|
609
|
+
};
|
|
610
|
+
const uploadedAssetHandler = (event: Event) => {
|
|
611
|
+
const fieldHandle = getFieldHandle(field);
|
|
612
|
+
|
|
613
|
+
if (!fieldHandle) {
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const uploadedAssets = getUploadedAssetsFromEvent(event, {
|
|
618
|
+
fieldHandle,
|
|
619
|
+
inputKey: getFileUploadKey(input),
|
|
620
|
+
inputName: input.name,
|
|
621
|
+
pendingFiles: state.pendingFiles,
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
if (!uploadedAssets.length) {
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
void syncUploadedAssets(uploadedAssets);
|
|
629
|
+
};
|
|
630
|
+
const resetHandler = () => {
|
|
631
|
+
resetUploadedAssets();
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
input.addEventListener('change', syncPendingFiles);
|
|
635
|
+
form?.addEventListener(FILE_UPLOAD_EVENT, uploadedAssetHandler as EventListener);
|
|
636
|
+
form?.addEventListener(FORM_RESET_EVENT, resetHandler as EventListener);
|
|
637
|
+
|
|
638
|
+
const syncInitialState = () => {
|
|
639
|
+
const assetIdsFromDom = readUploadedAssetsFromHiddenInputs(field, input);
|
|
640
|
+
const summaryItems = readSummaryItems(state.summaryRoot);
|
|
641
|
+
state.uploadedAssets = mergeUploadedAssets(assetIdsFromDom, summaryItems);
|
|
642
|
+
|
|
643
|
+
if (state.uploadedAssets.some((asset) => asset.assetId && !asset.filename)) {
|
|
644
|
+
void syncUploadedAssets(state.uploadedAssets);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
renderSummary(state);
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
syncInitialState();
|
|
652
|
+
|
|
653
|
+
return () => {
|
|
654
|
+
input.removeEventListener('change', syncPendingFiles);
|
|
655
|
+
form?.removeEventListener(FILE_UPLOAD_EVENT, uploadedAssetHandler as EventListener);
|
|
656
|
+
form?.removeEventListener(FORM_RESET_EVENT, resetHandler as EventListener);
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
507
660
|
export const fileUploadModule: FormieModuleDefinition = {
|
|
508
661
|
id: MODULE_ID,
|
|
509
662
|
kind: 'field',
|
|
@@ -511,111 +664,53 @@ export const fileUploadModule: FormieModuleDefinition = {
|
|
|
511
664
|
return !!ctx.target.querySelector(INPUT_SELECTOR);
|
|
512
665
|
},
|
|
513
666
|
setup: async(ctx) => {
|
|
514
|
-
const field = getModuleFieldTarget(ctx);
|
|
515
|
-
const scope = field || ctx.target;
|
|
516
|
-
const inputs = Array.from(scope.querySelectorAll(INPUT_SELECTOR)).filter((input): input is HTMLInputElement => {
|
|
517
|
-
return input instanceof HTMLInputElement;
|
|
518
|
-
});
|
|
519
667
|
const form = ctx.form;
|
|
668
|
+
const boundInputs = new WeakSet<HTMLInputElement>();
|
|
669
|
+
const unbinds: Array<() => void> = [];
|
|
520
670
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
if (!(field instanceof HTMLElement)) {
|
|
525
|
-
return () => {};
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
const state: FileUploadState = {
|
|
529
|
-
field,
|
|
530
|
-
input,
|
|
531
|
-
summaryRoot: ensureSummaryRoot(field, input),
|
|
532
|
-
uploadedAssets: mergeUploadedAssets(
|
|
533
|
-
readUploadedAssetsFromHiddenInputs(field, input),
|
|
534
|
-
readSummaryItems(field.querySelector('[data-formie-file-summary]') as HTMLElement | null)
|
|
535
|
-
),
|
|
536
|
-
pendingFiles: [],
|
|
537
|
-
hydrationToken: 0,
|
|
538
|
-
};
|
|
539
|
-
const syncPendingFiles = () => {
|
|
540
|
-
state.pendingFiles = Array.from(input.files || []).map((file) => {
|
|
541
|
-
return file.name;
|
|
542
|
-
});
|
|
543
|
-
renderSummary(state);
|
|
544
|
-
};
|
|
545
|
-
const syncUploadedAssets = async(uploadedAssets: UploadedAsset[]) => {
|
|
546
|
-
state.hydrationToken += 1;
|
|
547
|
-
const hydrationToken = state.hydrationToken;
|
|
548
|
-
state.pendingFiles = [];
|
|
549
|
-
state.uploadedAssets = uploadedAssets;
|
|
550
|
-
updateUploadedAssetInputs(field, input, uploadedAssets);
|
|
551
|
-
renderSummary(state);
|
|
552
|
-
|
|
553
|
-
try {
|
|
554
|
-
const hydratedAssets = await hydrateUploadedAssets(field, input, uploadedAssets);
|
|
555
|
-
|
|
556
|
-
if (hydrationToken !== state.hydrationToken) {
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
state.uploadedAssets = hydratedAssets;
|
|
561
|
-
renderSummary(state);
|
|
562
|
-
dispatchFieldEvent(field, MODULE_ID, 'uploaded-assets-sync', {
|
|
563
|
-
fileUpload: field,
|
|
564
|
-
assets: hydratedAssets,
|
|
565
|
-
});
|
|
566
|
-
} catch (error) {
|
|
567
|
-
console.error('[formie] Failed to hydrate uploaded file details.', error);
|
|
568
|
-
}
|
|
569
|
-
};
|
|
570
|
-
const resetUploadedAssets = () => {
|
|
571
|
-
state.hydrationToken += 1;
|
|
572
|
-
state.pendingFiles = [];
|
|
573
|
-
state.uploadedAssets = [];
|
|
574
|
-
updateUploadedAssetInputs(field, input, []);
|
|
575
|
-
renderSummary(state);
|
|
576
|
-
};
|
|
577
|
-
const uploadedAssetHandler = (event: Event) => {
|
|
578
|
-
const fieldHandle = getFieldHandle(field);
|
|
579
|
-
|
|
580
|
-
if (!fieldHandle) {
|
|
671
|
+
const bindInputsInRoot = (root: ParentNode) => {
|
|
672
|
+
collectFileUploadInputs(root).forEach((input) => {
|
|
673
|
+
if (boundInputs.has(input)) {
|
|
581
674
|
return;
|
|
582
675
|
}
|
|
583
676
|
|
|
584
|
-
const
|
|
585
|
-
fieldHandle,
|
|
586
|
-
inputKey: getFileUploadKey(input),
|
|
587
|
-
inputName: input.name,
|
|
588
|
-
pendingFiles: state.pendingFiles,
|
|
589
|
-
});
|
|
677
|
+
const field = getFieldContainer(input);
|
|
590
678
|
|
|
591
|
-
if (!
|
|
679
|
+
if (!field) {
|
|
592
680
|
return;
|
|
593
681
|
}
|
|
594
682
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
};
|
|
683
|
+
boundInputs.add(input);
|
|
684
|
+
unbinds.push(bindFileUploadInput(field, input, form));
|
|
685
|
+
});
|
|
686
|
+
};
|
|
600
687
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
form?.addEventListener(FORM_RESET_EVENT, resetHandler as EventListener);
|
|
604
|
-
renderSummary(state);
|
|
688
|
+
registerValidators(form);
|
|
689
|
+
bindInputsInRoot(ctx.target);
|
|
605
690
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
691
|
+
const repeaterInitRowHandler = (event: Event) => {
|
|
692
|
+
const detail = (event as CustomEvent<unknown>).detail;
|
|
693
|
+
|
|
694
|
+
if (!isRecord(detail)) {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const row = detail.row;
|
|
699
|
+
|
|
700
|
+
if (row instanceof HTMLElement) {
|
|
701
|
+
bindInputsInRoot(row);
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
form?.addEventListener(REPEATER_INIT_ROW_EVENT, repeaterInitRowHandler as EventListener);
|
|
612
706
|
|
|
613
707
|
await ctx.emit('formie:module:file-upload:init', {
|
|
614
|
-
count:
|
|
708
|
+
count: unbinds.length,
|
|
615
709
|
});
|
|
616
710
|
|
|
617
711
|
return {
|
|
618
712
|
destroy: () => {
|
|
713
|
+
form?.removeEventListener(REPEATER_INIT_ROW_EVENT, repeaterInitRowHandler as EventListener);
|
|
619
714
|
unbinds.forEach((unbind) => {
|
|
620
715
|
unbind();
|
|
621
716
|
});
|
|
@@ -5,6 +5,7 @@ import { getTextLimitMetrics } from '@verbb/formie-core';
|
|
|
5
5
|
import type { FormieModuleDefinition } from '#contracts/modules';
|
|
6
6
|
import { getModuleFieldTarget, releaseFormValidators, retainFormValidators } from '#modules/fields/shared';
|
|
7
7
|
import { ensureModuleStyles } from '#modules/styles';
|
|
8
|
+
import { t } from '#utils/i18n';
|
|
8
9
|
|
|
9
10
|
const INPUT_SELECTOR = 'input[data-formie-single-line-text-input], textarea[data-formie-multi-line-text-input]';
|
|
10
11
|
const TEXT_LIMIT_VALIDATORS = [
|
|
@@ -15,8 +16,17 @@ const TEXT_LIMIT_VALIDATORS = [
|
|
|
15
16
|
] as const;
|
|
16
17
|
const VALIDATOR_SCOPE = 'text-limit';
|
|
17
18
|
const ALLOW_OVERTYPE_ATTR = 'data-formie-text-limit-allow-overtype';
|
|
19
|
+
const TEXT_LIMIT_CHARACTERS_ALLOWED = '{count, plural, one{character allowed} other{characters allowed}}';
|
|
20
|
+
const TEXT_LIMIT_CHARACTERS_LEFT = '{count, plural, one{character left} other{characters left}}';
|
|
21
|
+
const TEXT_LIMIT_CHARACTERS_OVER = '{count, plural, one{character over limit} other{characters over limit}}';
|
|
22
|
+
const TEXT_LIMIT_WORDS_ALLOWED = '{count, plural, one{word allowed} other{words allowed}}';
|
|
23
|
+
const TEXT_LIMIT_WORDS_LEFT = '{count, plural, one{word left} other{words left}}';
|
|
24
|
+
const TEXT_LIMIT_WORDS_OVER = '{count, plural, one{word over limit} other{words over limit}}';
|
|
18
25
|
const limitTargetCache = new WeakMap<HTMLInputElement | HTMLTextAreaElement, HTMLElement | null>();
|
|
19
26
|
|
|
27
|
+
type TextLimitUnit = 'character' | 'word';
|
|
28
|
+
type TextLimitCounterState = 'allowed' | 'left' | 'over';
|
|
29
|
+
|
|
20
30
|
ensureModuleStyles('text-limit', [textLimitCss]);
|
|
21
31
|
|
|
22
32
|
type TextLimitOptions = {
|
|
@@ -69,10 +79,11 @@ function registerValidators(form: HTMLFormElement | null): void {
|
|
|
69
79
|
|
|
70
80
|
return getTextLimitMetrics(input.value).graphemeCount >= limit;
|
|
71
81
|
}, ({ label, input, t }) => {
|
|
72
|
-
return
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
82
|
+
return input.getAttribute('data-formie-validation-min-characters-message')
|
|
83
|
+
|| t('{label} must be no less than {min} characters.', {
|
|
84
|
+
label,
|
|
85
|
+
min: input.getAttribute('data-formie-min-chars') || '',
|
|
86
|
+
});
|
|
76
87
|
});
|
|
77
88
|
|
|
78
89
|
validator.addValidator('textMaxCharacterLimit', ({ input }) => {
|
|
@@ -92,10 +103,11 @@ function registerValidators(form: HTMLFormElement | null): void {
|
|
|
92
103
|
|
|
93
104
|
return getTextLimitMetrics(input.value).graphemeCount <= limit;
|
|
94
105
|
}, ({ label, input, t }) => {
|
|
95
|
-
return
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
106
|
+
return input.getAttribute('data-formie-validation-max-characters-message')
|
|
107
|
+
|| t('{label} must be no greater than {max} characters.', {
|
|
108
|
+
label,
|
|
109
|
+
max: input.getAttribute('data-formie-max-chars') || '',
|
|
110
|
+
});
|
|
99
111
|
});
|
|
100
112
|
|
|
101
113
|
validator.addValidator('textMinWordLimit', ({ input }) => {
|
|
@@ -111,10 +123,11 @@ function registerValidators(form: HTMLFormElement | null): void {
|
|
|
111
123
|
|
|
112
124
|
return getTextLimitMetrics(input.value).wordCount >= limit;
|
|
113
125
|
}, ({ label, input, t }) => {
|
|
114
|
-
return
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
126
|
+
return input.getAttribute('data-formie-validation-min-words-message')
|
|
127
|
+
|| t('{label} must be no less than {min} words.', {
|
|
128
|
+
label,
|
|
129
|
+
min: input.getAttribute('data-formie-min-words') || '',
|
|
130
|
+
});
|
|
118
131
|
});
|
|
119
132
|
|
|
120
133
|
validator.addValidator('textMaxWordLimit', ({ input }) => {
|
|
@@ -134,10 +147,11 @@ function registerValidators(form: HTMLFormElement | null): void {
|
|
|
134
147
|
|
|
135
148
|
return getTextLimitMetrics(input.value).wordCount <= limit;
|
|
136
149
|
}, ({ label, input, t }) => {
|
|
137
|
-
return
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
150
|
+
return input.getAttribute('data-formie-validation-max-words-message')
|
|
151
|
+
|| t('{label} must be no greater than {max} words.', {
|
|
152
|
+
label,
|
|
153
|
+
max: input.getAttribute('data-formie-max-words') || '',
|
|
154
|
+
});
|
|
141
155
|
});
|
|
142
156
|
});
|
|
143
157
|
}
|
|
@@ -182,14 +196,65 @@ function getLimitTarget(input: HTMLInputElement | HTMLTextAreaElement): HTMLElem
|
|
|
182
196
|
return target;
|
|
183
197
|
}
|
|
184
198
|
|
|
185
|
-
function
|
|
199
|
+
function getTextLimitCounterState(
|
|
200
|
+
input: HTMLInputElement | HTMLTextAreaElement,
|
|
201
|
+
remaining: number,
|
|
202
|
+
unit: TextLimitUnit,
|
|
203
|
+
): TextLimitCounterState {
|
|
204
|
+
const isEmpty = unit === 'character' ? input.value === '' : input.value.trim() === '';
|
|
205
|
+
|
|
206
|
+
if (isEmpty) {
|
|
207
|
+
return 'allowed';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (remaining < 0) {
|
|
211
|
+
return 'over';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return 'left';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function getTextLimitCounterMessageKey(unit: TextLimitUnit, state: TextLimitCounterState): string {
|
|
218
|
+
if (unit === 'character') {
|
|
219
|
+
if (state === 'allowed') {
|
|
220
|
+
return TEXT_LIMIT_CHARACTERS_ALLOWED;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (state === 'over') {
|
|
224
|
+
return TEXT_LIMIT_CHARACTERS_OVER;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return TEXT_LIMIT_CHARACTERS_LEFT;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (state === 'allowed') {
|
|
231
|
+
return TEXT_LIMIT_WORDS_ALLOWED;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (state === 'over') {
|
|
235
|
+
return TEXT_LIMIT_WORDS_OVER;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return TEXT_LIMIT_WORDS_LEFT;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function renderCounter(
|
|
242
|
+
target: HTMLElement,
|
|
243
|
+
input: HTMLInputElement | HTMLTextAreaElement,
|
|
244
|
+
remaining: number,
|
|
245
|
+
limit: number,
|
|
246
|
+
unit: TextLimitUnit,
|
|
247
|
+
): void {
|
|
248
|
+
const state = getTextLimitCounterState(input, remaining, unit);
|
|
249
|
+
const displayCount = state === 'allowed' ? limit : Math.abs(remaining);
|
|
186
250
|
const number = document.createElement('span');
|
|
187
|
-
number.className =
|
|
188
|
-
number.textContent = String(
|
|
251
|
+
number.className = state === 'over' ? 'formie-limit-number formie-limit-number-error' : 'formie-limit-number';
|
|
252
|
+
number.textContent = String(displayCount);
|
|
253
|
+
const messageKey = getTextLimitCounterMessageKey(unit, state);
|
|
189
254
|
|
|
190
255
|
target.replaceChildren(
|
|
191
256
|
number,
|
|
192
|
-
document.createTextNode(` ${
|
|
257
|
+
document.createTextNode(` ${t(messageKey, { count: displayCount })}`)
|
|
193
258
|
);
|
|
194
259
|
}
|
|
195
260
|
|
|
@@ -206,14 +271,14 @@ function updateCounter(input: HTMLInputElement | HTMLTextAreaElement): void {
|
|
|
206
271
|
|
|
207
272
|
if (maxChars > 0) {
|
|
208
273
|
const remaining = maxChars - metrics.graphemeCount;
|
|
209
|
-
renderCounter(target, remaining, 'character');
|
|
274
|
+
renderCounter(target, input, remaining, maxChars, 'character');
|
|
210
275
|
|
|
211
276
|
return;
|
|
212
277
|
}
|
|
213
278
|
|
|
214
279
|
if (maxWords > 0) {
|
|
215
280
|
const remaining = maxWords - metrics.wordCount;
|
|
216
|
-
renderCounter(target, remaining, 'word');
|
|
281
|
+
renderCounter(target, input, remaining, maxWords, 'word');
|
|
217
282
|
}
|
|
218
283
|
}
|
|
219
284
|
|
|
@@ -3,6 +3,7 @@ import type { FormSubmitResult } from '#contracts/schema';
|
|
|
3
3
|
import { EventBus } from '#events/event-bus';
|
|
4
4
|
import { submitForm } from '#transport/forms-api';
|
|
5
5
|
import { createDebug } from '#utils/debug';
|
|
6
|
+
import { getFormPages, getValidationScope } from '#validation/scope';
|
|
6
7
|
import type { FormieValidator } from '#validation/validator';
|
|
7
8
|
|
|
8
9
|
export type SubmitPipelineContext = {
|
|
@@ -30,32 +31,6 @@ function getAbortedResult(stage: SubmitStage, reason?: string): FormSubmitResult
|
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
function getPages(form: HTMLFormElement): HTMLElement[] {
|
|
34
|
-
return Array.from(form.querySelectorAll('[data-formie-page]')) as HTMLElement[];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function getValidationScope(form: HTMLFormElement): { scope: Element; final: boolean } {
|
|
38
|
-
const pages = getPages(form);
|
|
39
|
-
|
|
40
|
-
if (!pages.length) {
|
|
41
|
-
return {
|
|
42
|
-
scope: form,
|
|
43
|
-
final: true,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Multi-page submits validate the visible page until the user reaches the
|
|
48
|
-
// final step, where hidden earlier pages are included again.
|
|
49
|
-
const currentPage = pages.find((page) => {
|
|
50
|
-
return !page.hasAttribute('data-formie-page-hidden');
|
|
51
|
-
}) || pages[pages.length - 1];
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
scope: currentPage,
|
|
55
|
-
final: currentPage === pages[pages.length - 1],
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
34
|
function isSubmittableControl(element: Element): element is HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement {
|
|
60
35
|
return element instanceof HTMLInputElement
|
|
61
36
|
|| element instanceof HTMLSelectElement
|
|
@@ -172,7 +147,7 @@ function appendMissingFieldClears(formData: FormData, fieldNames: Set<string>):
|
|
|
172
147
|
}
|
|
173
148
|
|
|
174
149
|
function buildSubmitFormData(form: HTMLFormElement, action: FormAction): FormData {
|
|
175
|
-
const pages =
|
|
150
|
+
const pages = getFormPages(form);
|
|
176
151
|
const currentPage = pages.find((page) => {
|
|
177
152
|
return !page.hasAttribute('data-formie-page-hidden');
|
|
178
153
|
}) || null;
|
|
@@ -200,7 +175,7 @@ function isFinalSubmitAttempt(form: HTMLFormElement, action: FormAction): boolea
|
|
|
200
175
|
return false;
|
|
201
176
|
}
|
|
202
177
|
|
|
203
|
-
const pages =
|
|
178
|
+
const pages = getFormPages(form);
|
|
204
179
|
|
|
205
180
|
if (!pages.length) {
|
|
206
181
|
return true;
|