@verbb/formie-browser 1.0.14 → 1.0.16
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 +66 -0
- package/dist/chunks/{address-country-Dn71jroa.js → address-country-DLJF-OkC.js} +5 -9
- package/dist/chunks/{address-finder-O49uDRoR.js → address-finder-QK1m1eyv.js} +2 -2
- package/dist/chunks/{address-state-ByYatUur.js → address-state-CHKit8AW.js} +113 -112
- package/dist/chunks/{api-DYYsVIub.js → api-BPtH5RBA.js} +162 -130
- package/dist/chunks/{api-D6kJ7kXZ.js → api-BR2Gqy37.js} +313 -303
- package/dist/chunks/{api-ghNr2uxX.js → api-CmuOZ0i3.js} +1 -1
- package/dist/chunks/{async-nPFRNQ06.js → async-2GdXNFCE.js} +18 -1
- package/dist/chunks/{bpoint-D0RKFM4F.js → bpoint-GvYfyPjD.js} +1 -1
- package/dist/chunks/{calculations-YEoVCG2q.js → calculations-DeEmdUDq.js} +5 -7
- package/dist/chunks/{captcha-eu-DERJbXnH.js → captcha-eu-eU_M6snp.js} +2 -2
- package/dist/chunks/{checkbox-radio-gsm8QPJg.js → checkbox-radio-CSXo6svm.js} +4 -12
- package/dist/chunks/{combobox-CWNdx4Fk.js → combobox-CJh2DoSO.js} +323 -363
- package/dist/chunks/{conditions-DqNHLTbN.js → conditions-Dz3068K0.js} +140 -153
- package/dist/chunks/{country-from-ip-DDj_mFki.js → country-from-ip-BqQ-BqLH.js} +1 -1
- package/dist/chunks/{custom-google-maps-C4l2S3GO.js → custom-google-maps-rNm5VFbb.js} +2 -6
- package/dist/chunks/{custom-link-CLazgKYH.js → custom-link-CLqFCssQ.js} +1 -1
- package/dist/chunks/{custom-maps-DqcAlD0v.js → custom-maps-DgjYQDLj.js} +4 -8
- package/dist/chunks/{date-picker-RUnevF7l.js → date-picker-BVJxJVYT.js} +42 -58
- package/dist/chunks/{debug-BV0DvdHx.js → debug-rhzqP1j2.js} +2 -14
- package/dist/chunks/{dist-ZLeW0zZ3.js → dist-CJReDlDK.js} +13 -8
- package/dist/chunks/{eway-or6dtjoT.js → eway-BZuTBAUW.js} +3 -3
- package/dist/chunks/{field-references.resolver-Ca8MguFv.js → field-references.resolver-uSHf5-NP.js} +1 -1
- package/dist/chunks/{file-upload-Bm-TXyNB.js → file-upload-vEUr_EuP.js} +131 -113
- package/dist/chunks/{friendly-captcha-v1-GFqCt1Mh.js → friendly-captcha-v1-C_cF1WD2.js} +2 -2
- package/dist/chunks/{friendly-captcha-v2-WoW246JG.js → friendly-captcha-v2-BenESH-n.js} +2 -2
- package/dist/chunks/{friendly-challenge-DPG9MJb1.js → friendly-challenge-BgPBEMXx.js} +21 -35
- package/dist/chunks/{go-cardless-DzJ-0FKg.js → go-cardless-DWZeB0Bq.js} +2 -6
- package/dist/chunks/{google-address-C1P1nTDM.js → google-address-DQR6rsbh.js} +15 -10
- package/dist/chunks/{hcaptcha-D0T8aIHz.js → hcaptcha-COilLmRT.js} +2 -2
- package/dist/chunks/{hidden-BZhXnNPe.js → hidden-Bc2TpcgF.js} +1 -1
- package/dist/chunks/{http-C2QdHuWO.js → http-Dj_skqaa.js} +1 -1
- package/dist/chunks/{leaflet-src-DE6rv6M-.js → leaflet-src-Cg-LUOD8.js} +74 -122
- package/dist/chunks/{loqate-DD1S7PY9.js → loqate-ZZClcacr.js} +2 -2
- package/dist/chunks/{mollie-DG8Ju83x.js → mollie-8OMXIYa8.js} +2 -6
- package/dist/chunks/{moneris-C8m3Fvqu.js → moneris-qYHJ9EFy.js} +1 -1
- package/dist/chunks/opayo-DlWPEnla.js +159 -0
- package/dist/chunks/{paddle-BIYR9Um3.js → paddle-CArfWih0.js} +2 -2
- package/dist/chunks/{password-validation-C8_uO_c_.js → password-validation-HGOU9kf3.js} +2 -2
- package/dist/chunks/{paypal-B_yxIo2d.js → paypal-BcaMCMGY.js} +2 -2
- package/dist/chunks/{payway-CeOzS0sL.js → payway-vxC7_bfR.js} +4 -12
- package/dist/chunks/{phone-country-bAqje17P.js → phone-country-Bx9EG50h.js} +124 -141
- package/dist/chunks/{place-kit-BE5gi3vo.js → place-kit-BKk8xDXf.js} +10 -10
- package/dist/chunks/{placekit-autocomplete.esm-CZZERYur.js → placekit-autocomplete.esm-D2A_0iVU.js} +2 -5
- package/dist/chunks/{recaptcha-enterprise-CruXddZq.js → recaptcha-enterprise-Cx6HUpMa.js} +6 -12
- package/dist/chunks/recaptcha-shared-CcMI-r8t.js +33 -0
- package/dist/chunks/{recaptcha-v2-checkbox-CCnJYCnG.js → recaptcha-v2-checkbox-DQ9BJzcv.js} +2 -2
- package/dist/chunks/{recaptcha-v2-invisible-BuVqRRYn.js → recaptcha-v2-invisible-BP0T8Pan.js} +2 -2
- package/dist/chunks/recaptcha-v3-DKMGQ27K.js +38 -0
- package/dist/chunks/{repeater-DwE27jI1.js → repeater-BR0RUvTs.js} +50 -40
- package/dist/chunks/{rich-text-uV4o_pGk.js → rich-text-H2A6cm1t.js} +33 -11
- package/dist/chunks/{rolldown-runtime-DtPi1Y-2.js → rolldown-runtime-VHNaYcLd.js} +1 -1
- package/dist/chunks/{scripts-CbQ7agX3.js → scripts-DrccjrFv.js} +1 -1
- package/dist/chunks/{sdk-lkwSvlpM.js → sdk-_cmrRYgp.js} +4 -16
- package/dist/chunks/{shared-OKhSay59.js → shared-CJ1LIEh3.js} +1 -3
- package/dist/chunks/{signature-7cldFbqj.js → signature-CfKA_poI.js} +9 -41
- package/dist/chunks/skip-CEW89ExY.js +7 -0
- package/dist/chunks/{snaptcha-C04kNGYQ.js → snaptcha-P3EoSQud.js} +1 -1
- package/dist/chunks/{square-DEzxv9D9.js → square-Dyz0p_GU.js} +2 -2
- package/dist/chunks/{stripe-CVPQGDz3.js → stripe-Bfd_jp_k.js} +4 -7
- package/dist/chunks/{summary-BF_8mrXI.js → summary-B3CAebLV.js} +45 -37
- package/dist/chunks/{survey-likert-fk1hUPff.js → survey-likert-CEtUceag.js} +1 -1
- package/dist/chunks/{survey-rank-BsLGl2LX.js → survey-rank-Dtt97y6s.js} +3 -7
- package/dist/chunks/{survey-rating-DVS6e6fM.js → survey-rating-mlDMI0uq.js} +9 -23
- package/dist/chunks/{table-DnIBdeKF.js → table-0k6Md0IO.js} +2 -2
- package/dist/chunks/{text-limit-LGMPhFuC.js → text-limit-CTVML6s7.js} +10 -10
- package/dist/chunks/{turnstile-n9DNyPxi.js → turnstile-Sp4ubowK.js} +2 -2
- package/dist/chunks/{upload-manager-BbiUosZq.js → upload-manager-Ccg3d_4I.js} +258 -260
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +743 -734
- package/dist/js/core/create-formie-client.d.ts.map +1 -1
- package/dist/js/core/dom-events.d.ts +5 -0
- package/dist/js/core/dom-events.d.ts.map +1 -1
- package/dist/js/core/page-client-event.d.ts.map +1 -1
- package/dist/js/core/submit-result-state.d.ts.map +1 -1
- package/dist/js/core/submit-result-ui.d.ts +3 -1
- package/dist/js/core/submit-result-ui.d.ts.map +1 -1
- package/dist/js/core/validation-focus.d.ts.map +1 -1
- package/dist/js/modules/address/google-address.d.ts +10 -0
- package/dist/js/modules/address/google-address.d.ts.map +1 -1
- package/dist/js/modules/address/place-kit.d.ts.map +1 -1
- package/dist/js/modules/captchas/factories.d.ts.map +1 -1
- package/dist/js/modules/captchas/host.d.ts +4 -1
- package/dist/js/modules/captchas/host.d.ts.map +1 -1
- package/dist/js/modules/captchas/recaptcha-shared.d.ts +1 -1
- package/dist/js/modules/captchas/recaptcha-shared.d.ts.map +1 -1
- package/dist/js/modules/captchas/recaptcha-v3.d.ts.map +1 -1
- package/dist/js/modules/fields/address-state.d.ts.map +1 -1
- package/dist/js/modules/fields/combobox.d.ts +2 -0
- package/dist/js/modules/fields/combobox.d.ts.map +1 -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/date-picker.d.ts.map +1 -1
- package/dist/js/modules/fields/file-upload.d.ts.map +1 -1
- package/dist/js/modules/fields/phone-country.d.ts.map +1 -1
- package/dist/js/modules/fields/repeater.d.ts.map +1 -1
- package/dist/js/modules/fields/rich-text.d.ts.map +1 -1
- package/dist/js/modules/fields/summary.d.ts.map +1 -1
- package/dist/js/modules/fields/text-limit.d.ts.map +1 -1
- package/dist/js/modules/fields/upload-manager.d.ts.map +1 -1
- package/dist/js/modules/payments/factories.d.ts.map +1 -1
- package/dist/js/modules/payments/opayo.d.ts.map +1 -1
- package/dist/js/submit/pipeline.d.ts +2 -0
- package/dist/js/submit/pipeline.d.ts.map +1 -1
- package/dist/js/utils/async.d.ts +1 -0
- package/dist/js/utils/async.d.ts.map +1 -1
- package/dist/js/utils/form-started-at.d.ts +1 -0
- package/dist/js/utils/form-started-at.d.ts.map +1 -1
- package/dist/js/utils/unload-warning.d.ts +4 -0
- package/dist/js/utils/unload-warning.d.ts.map +1 -1
- package/dist/js/validation/rules/required.d.ts.map +1 -1
- package/dist/js/validation/submit-readiness.d.ts.map +1 -1
- package/dist/js/validation/validator.d.ts +1 -0
- package/dist/js/validation/validator.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +1 -0
- package/src/js/core/create-formie-client.ts +37 -28
- package/src/js/core/dom-events.ts +30 -1
- package/src/js/core/page-client-event.ts +5 -7
- package/src/js/core/submit-result-state.ts +30 -2
- package/src/js/core/submit-result-ui.ts +25 -3
- package/src/js/core/validation-focus.ts +17 -3
- package/src/js/modules/address/google-address.ts +95 -37
- package/src/js/modules/address/place-kit.ts +13 -7
- package/src/js/modules/captchas/factories.ts +241 -101
- package/src/js/modules/captchas/host.ts +16 -0
- package/src/js/modules/captchas/recaptcha-enterprise.ts +1 -1
- package/src/js/modules/captchas/recaptcha-shared.ts +23 -5
- package/src/js/modules/captchas/recaptcha-v3.ts +23 -14
- package/src/js/modules/fields/address-country.ts +5 -5
- package/src/js/modules/fields/address-state.ts +80 -27
- package/src/js/modules/fields/combobox.ts +22 -1
- package/src/js/modules/fields/conditions/effects.ts +4 -5
- package/src/js/modules/fields/conditions.ts +9 -0
- package/src/js/modules/fields/date-picker.ts +6 -3
- package/src/js/modules/fields/file-upload.ts +55 -18
- package/src/js/modules/fields/phone-country.ts +25 -2
- package/src/js/modules/fields/repeater.ts +55 -4
- package/src/js/modules/fields/rich-text.ts +33 -0
- package/src/js/modules/fields/summary.ts +63 -10
- package/src/js/modules/fields/text-limit.ts +11 -0
- package/src/js/modules/fields/upload-manager.ts +54 -18
- package/src/js/modules/payments/factories.ts +87 -54
- package/src/js/modules/payments/opayo.ts +7 -0
- package/src/js/submit/pipeline.ts +16 -9
- package/src/js/utils/async.ts +34 -0
- package/src/js/utils/form-started-at.ts +19 -4
- package/src/js/utils/unload-warning.ts +63 -7
- package/src/js/validation/rules/required.ts +18 -0
- package/src/js/validation/submit-readiness.ts +2 -0
- package/src/js/validation/validator.ts +29 -6
- package/src/vendor.d.ts +2 -0
- package/dist/chunks/opayo-B3EvrVxw.js +0 -188
- package/dist/chunks/recaptcha-shared-AQRMDMme.js +0 -27
- package/dist/chunks/recaptcha-v3-CloqhO2A.js +0 -28
|
@@ -18,7 +18,7 @@ ensureModuleStyles(MODULE_ID, [intlTelInputCss]);
|
|
|
18
18
|
|
|
19
19
|
type PhoneCountryOptions = {
|
|
20
20
|
countryDefaultValue?: string;
|
|
21
|
-
|
|
21
|
+
allowedCountryCodes?: string[];
|
|
22
22
|
countryPreselectFromIp?: boolean;
|
|
23
23
|
countryFromIpAction?: string;
|
|
24
24
|
language?: string;
|
|
@@ -93,7 +93,7 @@ function buildOptions(options: PhoneCountryOptions, form: HTMLFormElement | null
|
|
|
93
93
|
loadUtils: () => import('intl-tel-input/utils'),
|
|
94
94
|
};
|
|
95
95
|
|
|
96
|
-
const allowedCountries = (options.
|
|
96
|
+
const allowedCountries = (options.allowedCountryCodes || [])
|
|
97
97
|
.map((item) => {
|
|
98
98
|
return item.toLowerCase();
|
|
99
99
|
})
|
|
@@ -164,6 +164,27 @@ function initPhoneField(phoneInput: PhoneInput, countryInput: HTMLInputElement,
|
|
|
164
164
|
phoneInput.addEventListener('blur', syncCountry);
|
|
165
165
|
syncCountry();
|
|
166
166
|
|
|
167
|
+
// Hidden input values also change their native reset default, so retain the initial country.
|
|
168
|
+
let defaultCountry = countryInput.value;
|
|
169
|
+
let destroyed = false;
|
|
170
|
+
void validator.promise.then(() => {
|
|
171
|
+
if (!destroyed) {
|
|
172
|
+
defaultCountry ||= validator.getSelectedCountryData()?.iso2?.toUpperCase() || '';
|
|
173
|
+
}
|
|
174
|
+
}, () => {});
|
|
175
|
+
|
|
176
|
+
const form = phoneInput.form;
|
|
177
|
+
const onReset = (event: Event) => {
|
|
178
|
+
queueMicrotask(() => {
|
|
179
|
+
if (!destroyed && !event.defaultPrevented && defaultCountry) {
|
|
180
|
+
validator.setCountry(defaultCountry.toLowerCase() as IntlCountryCode);
|
|
181
|
+
validator.setNumber(phoneInput.defaultValue);
|
|
182
|
+
syncCountry();
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
form?.addEventListener('reset', onReset);
|
|
187
|
+
|
|
167
188
|
// Keep the country dropdown button in sync when the tel input is enabled/disabled after
|
|
168
189
|
// init (multi-page page-hidden, or conditions). intl-tel-input disables the button to match
|
|
169
190
|
// a disabled input at init, but won't re-enable it when `disabled` is later removed.
|
|
@@ -199,6 +220,8 @@ function initPhoneField(phoneInput: PhoneInput, countryInput: HTMLInputElement,
|
|
|
199
220
|
});
|
|
200
221
|
|
|
201
222
|
return () => {
|
|
223
|
+
destroyed = true;
|
|
224
|
+
form?.removeEventListener('reset', onReset);
|
|
202
225
|
phoneInput.removeEventListener('countrychange', syncCountry);
|
|
203
226
|
phoneInput.removeEventListener('blur', syncCountry);
|
|
204
227
|
countryButtonObserver.disconnect();
|
|
@@ -32,6 +32,18 @@ function getRowCount(field: HTMLElement): number {
|
|
|
32
32
|
return field.querySelectorAll(ROW_SELECTOR).length;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function focusRow(row: Element | null): boolean {
|
|
36
|
+
const control = Array.from(row?.querySelectorAll<HTMLElement>(
|
|
37
|
+
'input:not([type="hidden"]), select, textarea, button, a[href], [role="button"], [role="combobox"], [role="textbox"], [contenteditable="true"]',
|
|
38
|
+
) || []).find((element) => {
|
|
39
|
+
return element.tabIndex >= 0 && !element.matches(':disabled')
|
|
40
|
+
&& !element.closest('[hidden], [inert]') && element.getClientRects().length > 0;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
control?.focus();
|
|
44
|
+
return !!control;
|
|
45
|
+
}
|
|
46
|
+
|
|
35
47
|
function syncAddButton(addButton: HTMLButtonElement | null, rowCount: number): void {
|
|
36
48
|
if (!addButton) {
|
|
37
49
|
return;
|
|
@@ -46,7 +58,7 @@ function syncAddButton(addButton: HTMLButtonElement | null, rowCount: number): v
|
|
|
46
58
|
addButton.disabled = false;
|
|
47
59
|
}
|
|
48
60
|
|
|
49
|
-
function bindRepeaterField(field: HTMLElement): () => void {
|
|
61
|
+
function bindRepeaterField(field: HTMLElement, isCpEdit: boolean): () => void {
|
|
50
62
|
const container = field.matches(CONTAINER_SELECTOR)
|
|
51
63
|
? field
|
|
52
64
|
: field.querySelector(CONTAINER_SELECTOR);
|
|
@@ -57,6 +69,9 @@ function bindRepeaterField(field: HTMLElement): () => void {
|
|
|
57
69
|
return () => {};
|
|
58
70
|
}
|
|
59
71
|
|
|
72
|
+
const form = field.closest('form');
|
|
73
|
+
let disposed = false;
|
|
74
|
+
let generation = 0;
|
|
60
75
|
const removeHandlers = new Map<HTMLElement, EventListener>();
|
|
61
76
|
// Start after the highest existing row id so SSR rows and client-added rows
|
|
62
77
|
// keep producing unique `__ROW__` replacements across one repeater instance.
|
|
@@ -84,8 +99,17 @@ function bindRepeaterField(field: HTMLElement): () => void {
|
|
|
84
99
|
return;
|
|
85
100
|
}
|
|
86
101
|
|
|
102
|
+
const restoreFocus = row.contains(document.activeElement)
|
|
103
|
+
&& (!isCpEdit || (event instanceof MouseEvent && event.detail === 0));
|
|
104
|
+
const nextRow = row.nextElementSibling;
|
|
105
|
+
const previousRow = row.previousElementSibling;
|
|
87
106
|
row.remove();
|
|
88
107
|
syncAddButton(addButton instanceof HTMLButtonElement ? addButton : null, getRowCount(field));
|
|
108
|
+
// Removing the focused control otherwise sends keyboard users back
|
|
109
|
+
// to the document body. Keep them in the nearest surviving row.
|
|
110
|
+
if (restoreFocus && !focusRow(nextRow) && !focusRow(previousRow)) {
|
|
111
|
+
if (addButton instanceof HTMLButtonElement) addButton.focus();
|
|
112
|
+
}
|
|
89
113
|
debug.log('Row removed.', {
|
|
90
114
|
rowCount: getRowCount(field),
|
|
91
115
|
});
|
|
@@ -100,7 +124,7 @@ function bindRepeaterField(field: HTMLElement): () => void {
|
|
|
100
124
|
});
|
|
101
125
|
};
|
|
102
126
|
|
|
103
|
-
const addRow = async() => {
|
|
127
|
+
const addRow = async(focusNewRow = false) => {
|
|
104
128
|
if (!(addButton instanceof HTMLButtonElement)) {
|
|
105
129
|
return;
|
|
106
130
|
}
|
|
@@ -130,11 +154,16 @@ function bindRepeaterField(field: HTMLElement): () => void {
|
|
|
130
154
|
return;
|
|
131
155
|
}
|
|
132
156
|
|
|
157
|
+
const addedGeneration = generation;
|
|
133
158
|
container.appendChild(row);
|
|
134
159
|
// Give downstream field modules a tick to see the new row before we emit
|
|
135
160
|
// the public append/init-row lifecycle events used for nested enhancements.
|
|
136
161
|
await sleep(50);
|
|
162
|
+
if (disposed || addedGeneration !== generation || !row.isConnected) return;
|
|
137
163
|
bindRemoveButtons();
|
|
164
|
+
// Rows are inserted before Add, so leaving focus there skips the new
|
|
165
|
+
// inputs on Tab. Do not steal focus if the user moved on while mounting.
|
|
166
|
+
if (focusNewRow && document.activeElement === addButton) focusRow(row);
|
|
138
167
|
syncAddButton(addButton, getRowCount(field));
|
|
139
168
|
debug.log('Row appended.', {
|
|
140
169
|
rowCount: getRowCount(field),
|
|
@@ -151,7 +180,10 @@ function bindRepeaterField(field: HTMLElement): () => void {
|
|
|
151
180
|
|
|
152
181
|
const addHandler = (event: Event) => {
|
|
153
182
|
event.preventDefault();
|
|
154
|
-
|
|
183
|
+
// Pointer additions in the CP should not focus Craft's field wrappers
|
|
184
|
+
// and show a focus ring around the newly inserted field.
|
|
185
|
+
const focusNewRow = !isCpEdit || (event instanceof MouseEvent && event.detail === 0);
|
|
186
|
+
void addRow(focusNewRow);
|
|
155
187
|
};
|
|
156
188
|
|
|
157
189
|
if (addButton instanceof HTMLButtonElement) {
|
|
@@ -170,6 +202,23 @@ function bindRepeaterField(field: HTMLElement): () => void {
|
|
|
170
202
|
}
|
|
171
203
|
}
|
|
172
204
|
|
|
205
|
+
const initialRows = Array.from(container.children).map((row) => row.cloneNode(true));
|
|
206
|
+
|
|
207
|
+
const onReset = (event: Event) => {
|
|
208
|
+
queueMicrotask(() => {
|
|
209
|
+
if (event.defaultPrevented || disposed) return;
|
|
210
|
+
generation++;
|
|
211
|
+
removeHandlers.forEach((handler, button) => button.removeEventListener('click', handler));
|
|
212
|
+
removeHandlers.clear();
|
|
213
|
+
// Replace rows so removed initial rows return and added rows disappear.
|
|
214
|
+
// The module loader disposes old controls and hydrates the new rows.
|
|
215
|
+
container.replaceChildren(...initialRows.map((row) => row.cloneNode(true)));
|
|
216
|
+
bindRemoveButtons();
|
|
217
|
+
syncAddButton(addButton instanceof HTMLButtonElement ? addButton : null, getRowCount(field));
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
form?.addEventListener('reset', onReset);
|
|
221
|
+
|
|
173
222
|
dispatchFieldEvent(field, MODULE_ID, 'init', {
|
|
174
223
|
repeater: field,
|
|
175
224
|
});
|
|
@@ -178,6 +227,8 @@ function bindRepeaterField(field: HTMLElement): () => void {
|
|
|
178
227
|
});
|
|
179
228
|
|
|
180
229
|
return () => {
|
|
230
|
+
disposed = true;
|
|
231
|
+
form?.removeEventListener('reset', onReset);
|
|
181
232
|
if (addButton instanceof HTMLButtonElement) {
|
|
182
233
|
addButton.removeEventListener('click', addHandler);
|
|
183
234
|
}
|
|
@@ -211,7 +262,7 @@ export const repeaterModule: BrowserModuleDefinition = {
|
|
|
211
262
|
});
|
|
212
263
|
|
|
213
264
|
const destroyBindings = fields.map((field) => {
|
|
214
|
-
return bindRepeaterField(field);
|
|
265
|
+
return bindRepeaterField(field, ctx.surface === 'cp-edit');
|
|
215
266
|
});
|
|
216
267
|
debug.log('Module setup.', { fieldCount: fields.length });
|
|
217
268
|
|
|
@@ -168,6 +168,37 @@ function initRichTextField(container: HTMLElement, input: RichTextInput, options
|
|
|
168
168
|
input.richText = editor;
|
|
169
169
|
editor.content.innerHTML = input.value || '';
|
|
170
170
|
|
|
171
|
+
editor.content.setAttribute('role', 'textbox');
|
|
172
|
+
editor.content.setAttribute('aria-multiline', 'true');
|
|
173
|
+
|
|
174
|
+
const labels = Array.from(input.labels || []);
|
|
175
|
+
const ariaAttributes = ['aria-label', 'aria-labelledby', 'aria-invalid', 'aria-describedby', 'aria-errormessage'];
|
|
176
|
+
const syncAccessibility = () => {
|
|
177
|
+
ariaAttributes.forEach((attribute) => {
|
|
178
|
+
const value = input.getAttribute(attribute);
|
|
179
|
+
|
|
180
|
+
if (value !== null) {
|
|
181
|
+
editor.content.setAttribute(attribute, value);
|
|
182
|
+
} else {
|
|
183
|
+
editor.content.removeAttribute(attribute);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (!input.hasAttribute('aria-label') && !input.hasAttribute('aria-labelledby') && labels.length) {
|
|
188
|
+
editor.content.setAttribute('aria-label', labels.map((label) => label.textContent?.trim() || '').join(' '));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
editor.content.setAttribute('aria-required', input.getAttribute('aria-required') || String(input.required));
|
|
192
|
+
};
|
|
193
|
+
syncAccessibility();
|
|
194
|
+
|
|
195
|
+
// Validation updates the submitted textarea; expose the same state on the editor.
|
|
196
|
+
const accessibilityObserver = new MutationObserver(syncAccessibility);
|
|
197
|
+
accessibilityObserver.observe(input, { attributes: true, attributeFilter: [...ariaAttributes, 'aria-required', 'required'] });
|
|
198
|
+
|
|
199
|
+
const onLabelClick = () => editor.content.focus();
|
|
200
|
+
labels.forEach((label) => label.addEventListener('click', onLabelClick));
|
|
201
|
+
|
|
171
202
|
const form = input.form;
|
|
172
203
|
const onReset = (event: Event) => {
|
|
173
204
|
// Wait for the textarea to regain its default value before updating Pell.
|
|
@@ -188,6 +219,8 @@ function initRichTextField(container: HTMLElement, input: RichTextInput, options
|
|
|
188
219
|
});
|
|
189
220
|
|
|
190
221
|
return () => {
|
|
222
|
+
accessibilityObserver.disconnect();
|
|
223
|
+
labels.forEach((label) => label.removeEventListener('click', onLabelClick));
|
|
191
224
|
form?.removeEventListener('reset', onReset);
|
|
192
225
|
container.innerHTML = '';
|
|
193
226
|
delete input.richText;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import summaryCss from '#theme-css/fields/_summary.css?inline';
|
|
2
2
|
|
|
3
3
|
import type { BrowserModuleDefinition } from '#contracts/modules';
|
|
4
|
+
import type { FormSubmitResult } from '#contracts/schema';
|
|
4
5
|
import { dispatchFieldEvent, getModuleFieldContainers } from '#modules/fields/shared';
|
|
5
6
|
import { ensureModuleStyles } from '#modules/styles';
|
|
6
7
|
import { toggleThemeClasses } from '#theme/theme-classes';
|
|
@@ -28,8 +29,10 @@ function getSummaryRequestUrl(): string {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
function getSummaryRequestState(field: HTMLElement): SummaryRequestState {
|
|
31
|
-
const
|
|
32
|
-
const accessToken =
|
|
32
|
+
const tokenElement = field.querySelector('[data-formie-summary-token]');
|
|
33
|
+
const accessToken = (tokenElement instanceof HTMLInputElement
|
|
34
|
+
? tokenElement.value
|
|
35
|
+
: tokenElement?.getAttribute('data-formie-summary-token'))?.trim() || null;
|
|
33
36
|
|
|
34
37
|
return {
|
|
35
38
|
accessToken,
|
|
@@ -71,6 +74,9 @@ function initSummaryField(field: HTMLElement, root: Element): () => void {
|
|
|
71
74
|
let dirtyVersion = 0;
|
|
72
75
|
let requestVersion = 0;
|
|
73
76
|
let activeRequest: AbortController | null = null;
|
|
77
|
+
let destroyed = false;
|
|
78
|
+
const initialState = getSummaryRequestState(field);
|
|
79
|
+
let accessToken = initialState.accessToken;
|
|
74
80
|
|
|
75
81
|
const getBlocks = (): HTMLElement | null => {
|
|
76
82
|
const blocks = field.querySelector(BLOCKS_SELECTOR);
|
|
@@ -101,11 +107,10 @@ function initSummaryField(field: HTMLElement, root: Element): () => void {
|
|
|
101
107
|
toggleThemeClasses(blocks, form, 'loading', false);
|
|
102
108
|
};
|
|
103
109
|
|
|
104
|
-
const initialState = getSummaryRequestState(field);
|
|
105
110
|
setLoadingState(!!initialState.accessToken);
|
|
106
111
|
|
|
107
112
|
const queueFetch = (): void => {
|
|
108
|
-
if (!isVisible || (hasFetched && !isDirty)) {
|
|
113
|
+
if (destroyed || !isVisible || (hasFetched && !isDirty)) {
|
|
109
114
|
return;
|
|
110
115
|
}
|
|
111
116
|
|
|
@@ -114,7 +119,11 @@ function initSummaryField(field: HTMLElement, root: Element): () => void {
|
|
|
114
119
|
};
|
|
115
120
|
|
|
116
121
|
const fetchSummary = debounce(async() => {
|
|
117
|
-
|
|
122
|
+
if (destroyed) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const state = { accessToken };
|
|
118
127
|
|
|
119
128
|
if (!getBlocks() || !state.accessToken) {
|
|
120
129
|
debug.warn('Missing state for fetch.', state);
|
|
@@ -170,7 +179,9 @@ function initSummaryField(field: HTMLElement, root: Element): () => void {
|
|
|
170
179
|
setLoadingState(false);
|
|
171
180
|
activeRequest = null;
|
|
172
181
|
|
|
173
|
-
|
|
182
|
+
// Retry only when newer input arrived during this request. A
|
|
183
|
+
// failed request alone must not create an endless retry loop.
|
|
184
|
+
if (isDirty && dirtyVersion !== requestDirtyVersion) {
|
|
174
185
|
queueFetch();
|
|
175
186
|
}
|
|
176
187
|
}
|
|
@@ -194,18 +205,56 @@ function initSummaryField(field: HTMLElement, root: Element): () => void {
|
|
|
194
205
|
queueFetch();
|
|
195
206
|
};
|
|
196
207
|
|
|
197
|
-
const handleSubmitResult = (): void => {
|
|
198
|
-
|
|
199
|
-
|
|
208
|
+
const handleSubmitResult = (event: Event): void => {
|
|
209
|
+
const result = (event as CustomEvent<FormSubmitResult>).detail;
|
|
210
|
+
const tokens = result?.meta?.summaryTokens;
|
|
211
|
+
const uid = field.dataset.formieFieldUid;
|
|
212
|
+
|
|
213
|
+
// AJAX navigation reuses the initial markup, which had no submission
|
|
214
|
+
// token. The accepted save now grants access to this summary field.
|
|
215
|
+
if (uid && tokens && typeof tokens === 'object') {
|
|
216
|
+
const token = (tokens as Record<string, unknown>)[uid];
|
|
217
|
+
|
|
218
|
+
if (typeof token === 'string' && token) {
|
|
219
|
+
accessToken = token;
|
|
220
|
+
// Page navigation can remount modules with updated configuration.
|
|
221
|
+
getContainer()?.setAttribute('data-formie-summary-token', token);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
markDirty();
|
|
226
|
+
debug.log('Submit result received; refreshing.', { uid, hasToken: !!accessToken });
|
|
200
227
|
queueFetch();
|
|
201
228
|
};
|
|
202
229
|
|
|
203
230
|
const handlePageNavigate = (): void => {
|
|
204
|
-
|
|
231
|
+
markDirty();
|
|
205
232
|
debug.log('Page navigation received; refreshing.');
|
|
206
233
|
queueFetch();
|
|
207
234
|
};
|
|
208
235
|
|
|
236
|
+
const handleReset = (event: Event): void => {
|
|
237
|
+
queueMicrotask(() => {
|
|
238
|
+
if (event.defaultPrevented || destroyed) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// A new submission must not display or fetch the previous visitor's
|
|
243
|
+
// completed values while its initial markup is reused.
|
|
244
|
+
requestVersion += 1;
|
|
245
|
+
activeRequest?.abort();
|
|
246
|
+
activeRequest = null;
|
|
247
|
+
accessToken = null;
|
|
248
|
+
getContainer()?.removeAttribute('data-formie-summary-token');
|
|
249
|
+
field.querySelectorAll('input[data-formie-summary-token]').forEach((input) => input.remove());
|
|
250
|
+
hasFetched = false;
|
|
251
|
+
getBlocks()?.replaceChildren();
|
|
252
|
+
setLoadingState(false);
|
|
253
|
+
markDirty();
|
|
254
|
+
queueFetch();
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
|
|
209
258
|
// Defer the summary fetch until the field is near view. This avoids extra
|
|
210
259
|
// requests for hidden pages or summaries the user never reaches.
|
|
211
260
|
const observer = new IntersectionObserver((entries) => {
|
|
@@ -228,14 +277,18 @@ function initSummaryField(field: HTMLElement, root: Element): () => void {
|
|
|
228
277
|
observer.observe(field);
|
|
229
278
|
form.addEventListener('input', handleFieldMutation);
|
|
230
279
|
form.addEventListener('change', handleFieldMutation);
|
|
280
|
+
form.addEventListener('reset', handleReset);
|
|
231
281
|
root.addEventListener('formie:page:navigate:after', handlePageNavigate as EventListener);
|
|
232
282
|
root.addEventListener('formie:submit:result', handleSubmitResult as EventListener);
|
|
233
283
|
|
|
234
284
|
return () => {
|
|
285
|
+
destroyed = true;
|
|
286
|
+
requestVersion += 1;
|
|
235
287
|
activeRequest?.abort();
|
|
236
288
|
observer.disconnect();
|
|
237
289
|
form.removeEventListener('input', handleFieldMutation);
|
|
238
290
|
form.removeEventListener('change', handleFieldMutation);
|
|
291
|
+
form.removeEventListener('reset', handleReset);
|
|
239
292
|
root.removeEventListener('formie:page:navigate:after', handlePageNavigate as EventListener);
|
|
240
293
|
root.removeEventListener('formie:submit:result', handleSubmitResult as EventListener);
|
|
241
294
|
debug.log('Field destroyed.');
|
|
@@ -323,12 +323,23 @@ export const textLimitModule: BrowserModuleDefinition = {
|
|
|
323
323
|
};
|
|
324
324
|
});
|
|
325
325
|
|
|
326
|
+
const onReset = (event: Event) => {
|
|
327
|
+
// Native reset restores input values after the reset event.
|
|
328
|
+
queueMicrotask(() => {
|
|
329
|
+
if (!event.defaultPrevented) {
|
|
330
|
+
counterInputs.forEach(updateCounter);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
};
|
|
334
|
+
ctx.form?.addEventListener('reset', onReset);
|
|
335
|
+
|
|
326
336
|
await ctx.emit('formie:module:text-limit:init', {
|
|
327
337
|
count: inputs.length,
|
|
328
338
|
});
|
|
329
339
|
|
|
330
340
|
return {
|
|
331
341
|
destroy: () => {
|
|
342
|
+
ctx.form?.removeEventListener('reset', onReset);
|
|
332
343
|
unbinds.forEach((unbind) => {
|
|
333
344
|
unbind();
|
|
334
345
|
});
|
|
@@ -42,9 +42,9 @@ type UploadManagerOptions = {
|
|
|
42
42
|
uploadEndpoint?: string;
|
|
43
43
|
deleteEndpoint?: string;
|
|
44
44
|
hydrateEndpoint?: string;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
maxFiles?: number | null;
|
|
46
|
+
minFileSize?: number | null;
|
|
47
|
+
maxFileSize?: number | null;
|
|
48
48
|
accept?: string | null;
|
|
49
49
|
restrictFiles?: boolean;
|
|
50
50
|
};
|
|
@@ -508,6 +508,19 @@ function markUploadComplete(listItem: HTMLElement): void {
|
|
|
508
508
|
}, UPLOAD_COMPLETE_DISPLAY_MS);
|
|
509
509
|
}
|
|
510
510
|
|
|
511
|
+
function markUploadFailed(listItem: HTMLElement): void {
|
|
512
|
+
const { track, bar, label } = getProgressElements(listItem);
|
|
513
|
+
|
|
514
|
+
listItem.classList.add('is-error');
|
|
515
|
+
listItem.classList.remove('is-uploading', 'is-upload-complete', 'is-processing', 'is-preparing');
|
|
516
|
+
track?.setAttribute('data-indeterminate', 'false');
|
|
517
|
+
bar?.setAttribute('data-progress', 'error');
|
|
518
|
+
|
|
519
|
+
if (label) {
|
|
520
|
+
label.textContent = 'Upload failed';
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
511
524
|
function createSortButton(direction: 'up' | 'down'): HTMLButtonElement {
|
|
512
525
|
const label = direction === 'up' ? 'Move file up' : 'Move file down';
|
|
513
526
|
const button = document.createElement('button');
|
|
@@ -763,9 +776,9 @@ function bindUploadManagerField(field: HTMLElement, form: HTMLFormElement | null
|
|
|
763
776
|
const uploadEndpoint = getEndpoint(dropzone, options.uploadEndpoint, 'data-formie-file-upload-upload-endpoint', '/actions/formie/file-upload/upload');
|
|
764
777
|
const deleteEndpoint = getEndpoint(dropzone, options.deleteEndpoint, 'data-formie-file-upload-delete-endpoint', '/actions/formie/file-upload/delete');
|
|
765
778
|
const hydrateEndpoint = getEndpoint(dropzone, options.hydrateEndpoint, 'data-formie-file-upload-hydrate-endpoint', '/actions/formie/file-upload/hydrate');
|
|
766
|
-
const
|
|
767
|
-
const
|
|
768
|
-
const
|
|
779
|
+
const maxFiles = options.maxFiles ?? toPositiveInt(dropzone.getAttribute('data-formie-file-limit'));
|
|
780
|
+
const maxFileSize = options.maxFileSize ?? parseFloat(dropzone.getAttribute('data-formie-size-max-limit') || '');
|
|
781
|
+
const minFileSize = options.minFileSize ?? parseFloat(dropzone.getAttribute('data-formie-size-min-limit') || '');
|
|
769
782
|
const accept = options.accept ?? dropzone.getAttribute('accept') ?? browseInput.accept;
|
|
770
783
|
const allowedFileTypes = parseAcceptTypes(accept);
|
|
771
784
|
|
|
@@ -776,16 +789,16 @@ function bindUploadManagerField(field: HTMLElement, form: HTMLFormElement | null
|
|
|
776
789
|
allowedFileTypes?: string[] | null;
|
|
777
790
|
} = {};
|
|
778
791
|
|
|
779
|
-
if (
|
|
780
|
-
restrictions.maxNumberOfFiles =
|
|
792
|
+
if (maxFiles) {
|
|
793
|
+
restrictions.maxNumberOfFiles = maxFiles;
|
|
781
794
|
}
|
|
782
795
|
|
|
783
|
-
if (
|
|
784
|
-
restrictions.maxFileSize =
|
|
796
|
+
if (maxFileSize) {
|
|
797
|
+
restrictions.maxFileSize = maxFileSize * 1000 * 1000;
|
|
785
798
|
}
|
|
786
799
|
|
|
787
|
-
if (
|
|
788
|
-
restrictions.minFileSize =
|
|
800
|
+
if (minFileSize) {
|
|
801
|
+
restrictions.minFileSize = minFileSize * 1000 * 1000;
|
|
789
802
|
}
|
|
790
803
|
|
|
791
804
|
if (allowedFileTypes) {
|
|
@@ -797,6 +810,27 @@ function bindUploadManagerField(field: HTMLElement, form: HTMLFormElement | null
|
|
|
797
810
|
restrictions,
|
|
798
811
|
});
|
|
799
812
|
|
|
813
|
+
const selectionError = document.createElement('div');
|
|
814
|
+
selectionError.className = 'formie-upload-manager-error';
|
|
815
|
+
selectionError.setAttribute('role', 'alert');
|
|
816
|
+
selectionError.hidden = true;
|
|
817
|
+
root.append(selectionError);
|
|
818
|
+
|
|
819
|
+
// Uppy stores restriction messages for UI plugins; this custom UI must render them.
|
|
820
|
+
uppy.on('info-visible', () => {
|
|
821
|
+
const messages = uppy.getState().info.filter((info) => info.type === 'error');
|
|
822
|
+
|
|
823
|
+
if (messages.length) {
|
|
824
|
+
selectionError.textContent = [...new Set(messages.map((info) => info.message))].join(' ');
|
|
825
|
+
selectionError.hidden = false;
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
const clearSelectionError = () => {
|
|
830
|
+
selectionError.textContent = '';
|
|
831
|
+
selectionError.hidden = true;
|
|
832
|
+
};
|
|
833
|
+
|
|
800
834
|
// Resolve once for Uppy's allow-list; per-request meta/header refresh
|
|
801
835
|
// still re-reads the live form token (including custom csrfTokenName).
|
|
802
836
|
const initialCsrf = getFormCsrfToken(resolvedForm);
|
|
@@ -886,11 +920,11 @@ function bindUploadManagerField(field: HTMLElement, form: HTMLFormElement | null
|
|
|
886
920
|
};
|
|
887
921
|
|
|
888
922
|
const remainingUploadCapacity = (): number | null => {
|
|
889
|
-
if (!
|
|
923
|
+
if (!maxFiles) {
|
|
890
924
|
return null;
|
|
891
925
|
}
|
|
892
926
|
|
|
893
|
-
return Math.max(0,
|
|
927
|
+
return Math.max(0, maxFiles - countManagedAssets(state));
|
|
894
928
|
};
|
|
895
929
|
|
|
896
930
|
const setDropzoneActive = (active: boolean) => {
|
|
@@ -1147,8 +1181,7 @@ function bindUploadManagerField(field: HTMLElement, form: HTMLFormElement | null
|
|
|
1147
1181
|
const assetId = toPositiveInt(body.assetId);
|
|
1148
1182
|
|
|
1149
1183
|
if (!body.success || !assetId) {
|
|
1150
|
-
managedFile.listItem
|
|
1151
|
-
managedFile.listItem.classList.remove('is-uploading', 'is-upload-complete', 'is-processing', 'is-preparing');
|
|
1184
|
+
markUploadFailed(managedFile.listItem);
|
|
1152
1185
|
|
|
1153
1186
|
if (errorEl instanceof HTMLElement) {
|
|
1154
1187
|
errorEl.hidden = false;
|
|
@@ -1184,8 +1217,7 @@ function bindUploadManagerField(field: HTMLElement, form: HTMLFormElement | null
|
|
|
1184
1217
|
}
|
|
1185
1218
|
|
|
1186
1219
|
const errorEl = managedFile.listItem.querySelector('[data-formie-upload-manager-error]');
|
|
1187
|
-
managedFile.listItem
|
|
1188
|
-
managedFile.listItem.classList.remove('is-uploading', 'is-upload-complete', 'is-processing', 'is-preparing');
|
|
1220
|
+
markUploadFailed(managedFile.listItem);
|
|
1189
1221
|
|
|
1190
1222
|
if (errorEl instanceof HTMLElement) {
|
|
1191
1223
|
errorEl.hidden = false;
|
|
@@ -1254,6 +1286,7 @@ function bindUploadManagerField(field: HTMLElement, form: HTMLFormElement | null
|
|
|
1254
1286
|
|
|
1255
1287
|
const files = Array.from(browseInput.files);
|
|
1256
1288
|
browseInput.value = '';
|
|
1289
|
+
clearSelectionError();
|
|
1257
1290
|
|
|
1258
1291
|
try {
|
|
1259
1292
|
uppy.addFiles(files.map((file) => {
|
|
@@ -1294,6 +1327,7 @@ function bindUploadManagerField(field: HTMLElement, form: HTMLFormElement | null
|
|
|
1294
1327
|
}
|
|
1295
1328
|
|
|
1296
1329
|
const files = Array.from(transfer.files);
|
|
1330
|
+
clearSelectionError();
|
|
1297
1331
|
|
|
1298
1332
|
try {
|
|
1299
1333
|
uppy.addFiles(files.map((file) => {
|
|
@@ -1311,6 +1345,7 @@ function bindUploadManagerField(field: HTMLElement, form: HTMLFormElement | null
|
|
|
1311
1345
|
});
|
|
1312
1346
|
|
|
1313
1347
|
const resetHandler = () => {
|
|
1348
|
+
clearSelectionError();
|
|
1314
1349
|
state.files.forEach((managedFile) => {
|
|
1315
1350
|
managedFile.listItem.remove();
|
|
1316
1351
|
});
|
|
@@ -1326,6 +1361,7 @@ function bindUploadManagerField(field: HTMLElement, form: HTMLFormElement | null
|
|
|
1326
1361
|
return () => {
|
|
1327
1362
|
resolvedForm?.removeEventListener(FORM_RESET_EVENT, resetHandler);
|
|
1328
1363
|
uppy.destroy();
|
|
1364
|
+
selectionError.remove();
|
|
1329
1365
|
state.files = [];
|
|
1330
1366
|
};
|
|
1331
1367
|
}
|