@usefillo/dom 0.11.0 → 0.12.1
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/index.d.ts +29 -4
- package/dist/index.js +485 -194
- package/dist/standalone.global.js +1 -1
- package/dist/styles.css +161 -36
- package/dist/styles.unlayered.css +161 -36
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
isFilloError,
|
|
24
24
|
isLikelyDevEnv,
|
|
25
25
|
isValidPartialNumberText,
|
|
26
|
+
localeForNotation,
|
|
26
27
|
normalizeFormSchema,
|
|
27
28
|
normalizeFormTheme,
|
|
28
29
|
parseGroupedNumber,
|
|
@@ -33,6 +34,8 @@ import {
|
|
|
33
34
|
needsExplicitSubmit,
|
|
34
35
|
radioGroupStep,
|
|
35
36
|
reachablePageSequence,
|
|
37
|
+
REQUIRED_FIELD_MESSAGE,
|
|
38
|
+
requiredFieldMessage,
|
|
36
39
|
resolveThemeAppearance,
|
|
37
40
|
shouldAutoSubmit,
|
|
38
41
|
syncCodeForm,
|
|
@@ -93,11 +96,11 @@ function badgeUrl(href) {
|
|
|
93
96
|
function inputValue(value) {
|
|
94
97
|
return typeof value === "string" || typeof value === "number" ? String(value) : "";
|
|
95
98
|
}
|
|
96
|
-
function groupedInputValue(value, decimals) {
|
|
99
|
+
function groupedInputValue(value, decimals, locale) {
|
|
97
100
|
const raw = inputValue(value);
|
|
98
101
|
if (raw === "") return raw;
|
|
99
102
|
const n = Number(raw);
|
|
100
|
-
return Number.isFinite(n) ? formatGroupedNumber(n, { decimals }) : raw;
|
|
103
|
+
return Number.isFinite(n) ? formatGroupedNumber(n, { locale, decimals }) : raw;
|
|
101
104
|
}
|
|
102
105
|
function appendChildren(parent, children) {
|
|
103
106
|
for (const child of children) {
|
|
@@ -126,13 +129,11 @@ function optionIcon(icon) {
|
|
|
126
129
|
svg.appendChild(path);
|
|
127
130
|
return svg;
|
|
128
131
|
}
|
|
129
|
-
function applyFieldAria(node, field, error, ids) {
|
|
132
|
+
function applyFieldAria(node, field, error, ids, opts = {}) {
|
|
130
133
|
if (error) node.setAttribute("aria-invalid", "true");
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
error ? ids.errorId : null
|
|
135
|
-
].filter(Boolean).join(" ");
|
|
134
|
+
if ((opts.requiredSupported ?? true) && field.required)
|
|
135
|
+
node.setAttribute("aria-required", "true");
|
|
136
|
+
const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ");
|
|
136
137
|
if (describedBy) node.setAttribute("aria-describedby", describedBy);
|
|
137
138
|
}
|
|
138
139
|
function bindRadioKeys(group, values, current, select) {
|
|
@@ -185,7 +186,8 @@ function shell(field, error, child, ids) {
|
|
|
185
186
|
function textInput(type, context) {
|
|
186
187
|
const field = context.field;
|
|
187
188
|
const numberField = field.kind === "number" ? field : void 0;
|
|
188
|
-
const grouped = numberField?.notation
|
|
189
|
+
const grouped = numberField?.notation !== void 0;
|
|
190
|
+
const locale = localeForNotation(numberField?.notation);
|
|
189
191
|
const formatted = grouped || Boolean(numberField?.prefix) || Boolean(numberField?.suffix);
|
|
190
192
|
const input = el("input", {
|
|
191
193
|
className: "fillo-input",
|
|
@@ -194,7 +196,7 @@ function textInput(type, context) {
|
|
|
194
196
|
if (formatted) input.setAttribute("inputmode", "decimal");
|
|
195
197
|
if (type === "email") input.setAttribute("autocomplete", "email");
|
|
196
198
|
else if (type === "url") input.setAttribute("autocomplete", "url");
|
|
197
|
-
input.value = grouped ? groupedInputValue(context.value, numberField?.decimals) : inputValue(context.value);
|
|
199
|
+
input.value = grouped ? groupedInputValue(context.value, numberField?.decimals, locale) : inputValue(context.value);
|
|
198
200
|
if (field.placeholder) input.placeholder = field.placeholder;
|
|
199
201
|
if ("maxLength" in field && field.maxLength !== void 0) {
|
|
200
202
|
input.maxLength = field.maxLength;
|
|
@@ -212,31 +214,31 @@ function textInput(type, context) {
|
|
|
212
214
|
};
|
|
213
215
|
if (grouped) {
|
|
214
216
|
input.addEventListener("input", () => {
|
|
215
|
-
if (!isValidPartialNumberText(input.value)) return rejectEdit();
|
|
217
|
+
if (!isValidPartialNumberText(input.value, locale)) return rejectEdit();
|
|
216
218
|
lastValid = input.value;
|
|
217
219
|
lastCaret = input.selectionStart ?? input.value.length;
|
|
218
|
-
context.setValue(parseGroupedNumber(input.value), { render: false });
|
|
220
|
+
context.setValue(parseGroupedNumber(input.value, locale), { render: false });
|
|
219
221
|
});
|
|
220
222
|
input.addEventListener("change", () => {
|
|
221
|
-
if (!isValidPartialNumberText(input.value)) return rejectEdit();
|
|
223
|
+
if (!isValidPartialNumberText(input.value, locale)) return rejectEdit();
|
|
222
224
|
lastValid = input.value;
|
|
223
|
-
context.setValue(parseGroupedNumber(input.value));
|
|
225
|
+
context.setValue(parseGroupedNumber(input.value, locale));
|
|
224
226
|
});
|
|
225
227
|
input.addEventListener("blur", () => {
|
|
226
228
|
const live = context.getValue?.() ?? context.value;
|
|
227
|
-
input.value = groupedInputValue(live, numberField?.decimals);
|
|
229
|
+
input.value = groupedInputValue(live, numberField?.decimals, locale);
|
|
228
230
|
lastValid = input.value;
|
|
229
231
|
lastCaret = input.value.length;
|
|
230
232
|
});
|
|
231
233
|
} else {
|
|
232
234
|
input.addEventListener("input", () => {
|
|
233
|
-
if (!isValidPartialNumberText(input.value)) return rejectEdit();
|
|
235
|
+
if (!isValidPartialNumberText(input.value, locale)) return rejectEdit();
|
|
234
236
|
lastValid = input.value;
|
|
235
237
|
lastCaret = input.selectionStart ?? input.value.length;
|
|
236
238
|
context.setValue(input.value, { render: false });
|
|
237
239
|
});
|
|
238
240
|
input.addEventListener("change", () => {
|
|
239
|
-
if (!isValidPartialNumberText(input.value)) return rejectEdit();
|
|
241
|
+
if (!isValidPartialNumberText(input.value, locale)) return rejectEdit();
|
|
240
242
|
lastValid = input.value;
|
|
241
243
|
context.setValue(input.value);
|
|
242
244
|
});
|
|
@@ -293,7 +295,10 @@ function phoneInput(context) {
|
|
|
293
295
|
className: "fillo-phone-flag",
|
|
294
296
|
attrs: { type: "button", "aria-haspopup": "listbox", "aria-expanded": "false" }
|
|
295
297
|
});
|
|
296
|
-
const flagSpan = el("span", {
|
|
298
|
+
const flagSpan = el("span", {
|
|
299
|
+
className: "fillo-phone-flag-emoji",
|
|
300
|
+
attrs: { "aria-hidden": "true" }
|
|
301
|
+
});
|
|
297
302
|
const dialSpan = el("span", { className: "fillo-phone-dial" });
|
|
298
303
|
appendChildren(flagBtn, [
|
|
299
304
|
flagSpan,
|
|
@@ -449,10 +454,18 @@ function phoneInput(context) {
|
|
|
449
454
|
matches.forEach((c, i) => {
|
|
450
455
|
const li = el("li", {
|
|
451
456
|
className: `fillo-phone-option${i === active ? " fillo-phone-option--active" : ""}`,
|
|
452
|
-
attrs: {
|
|
457
|
+
attrs: {
|
|
458
|
+
id: `${listId}-opt-${i}`,
|
|
459
|
+
role: "option",
|
|
460
|
+
"aria-selected": String(c.iso2 === country.iso2)
|
|
461
|
+
}
|
|
453
462
|
});
|
|
454
463
|
appendChildren(li, [
|
|
455
|
-
el("span", {
|
|
464
|
+
el("span", {
|
|
465
|
+
className: "fillo-phone-flag-emoji",
|
|
466
|
+
text: flagEmoji(c.iso2),
|
|
467
|
+
attrs: { "aria-hidden": "true" }
|
|
468
|
+
}),
|
|
456
469
|
el("span", { className: "fillo-phone-option-name", text: c.name }),
|
|
457
470
|
el("span", { className: "fillo-phone-option-dial", text: `+${c.dialCode}` })
|
|
458
471
|
]);
|
|
@@ -587,7 +600,10 @@ function singleChoice(context) {
|
|
|
587
600
|
attrs: { "data-option": option.id, "data-fillo": "option" }
|
|
588
601
|
});
|
|
589
602
|
label.toggleAttribute("data-selected", value === option.id);
|
|
590
|
-
const input = el("input", {
|
|
603
|
+
const input = el("input", {
|
|
604
|
+
className: "fillo-option-input",
|
|
605
|
+
attrs: { type: "radio", name: context.ids.name }
|
|
606
|
+
});
|
|
591
607
|
input.checked = value === option.id;
|
|
592
608
|
input.addEventListener("change", () => {
|
|
593
609
|
activeOtherFields?.delete(field.id);
|
|
@@ -596,12 +612,20 @@ function singleChoice(context) {
|
|
|
596
612
|
appendChildren(label, [
|
|
597
613
|
input,
|
|
598
614
|
optionIcon(option.icon),
|
|
599
|
-
el("span", {
|
|
615
|
+
el("span", {
|
|
616
|
+
className: "fillo-option-label",
|
|
617
|
+
text: option.label,
|
|
618
|
+
attrs: { "data-fillo": "optionLabel" }
|
|
619
|
+
}),
|
|
600
620
|
// Icon-mode hides the native input visually (CSS), so an SVG recolor
|
|
601
621
|
// was the only selected-state signal — add a decorative shape signal
|
|
602
622
|
// that survives forced-colors (audit P1.2; state still lives on the
|
|
603
623
|
// input, this marker is non-authoritative).
|
|
604
|
-
option.icon && value === option.id ? el("span", {
|
|
624
|
+
option.icon && value === option.id ? el("span", {
|
|
625
|
+
className: "fillo-option-check",
|
|
626
|
+
text: "\u2713",
|
|
627
|
+
attrs: { "aria-hidden": "true" }
|
|
628
|
+
}) : null
|
|
605
629
|
]);
|
|
606
630
|
wrap.appendChild(label);
|
|
607
631
|
}
|
|
@@ -611,7 +635,10 @@ function singleChoice(context) {
|
|
|
611
635
|
attrs: { "data-fillo": "option" }
|
|
612
636
|
});
|
|
613
637
|
label.toggleAttribute("data-selected", otherActive);
|
|
614
|
-
const input = el("input", {
|
|
638
|
+
const input = el("input", {
|
|
639
|
+
className: "fillo-option-input",
|
|
640
|
+
attrs: { type: "radio", name: context.ids.name }
|
|
641
|
+
});
|
|
615
642
|
input.checked = otherActive;
|
|
616
643
|
input.addEventListener("change", () => {
|
|
617
644
|
activeOtherFields?.add(field.id);
|
|
@@ -620,7 +647,11 @@ function singleChoice(context) {
|
|
|
620
647
|
appendChildren(label, [
|
|
621
648
|
appendChildren(el("span", { className: "fillo-option-main" }), [
|
|
622
649
|
input,
|
|
623
|
-
el("span", {
|
|
650
|
+
el("span", {
|
|
651
|
+
className: "fillo-option-label",
|
|
652
|
+
text: DEFAULT_STRINGS.other,
|
|
653
|
+
attrs: { "data-fillo": "optionLabel" }
|
|
654
|
+
})
|
|
624
655
|
])
|
|
625
656
|
]);
|
|
626
657
|
if (otherActive) {
|
|
@@ -660,7 +691,7 @@ function multiChoice(context) {
|
|
|
660
691
|
className: "fillo-options",
|
|
661
692
|
attrs: { role: "group", "aria-labelledby": context.ids.labelId, "data-fillo": "options" }
|
|
662
693
|
});
|
|
663
|
-
applyFieldAria(wrap, field, context.error, context.ids);
|
|
694
|
+
applyFieldAria(wrap, field, context.error, context.ids, { requiredSupported: false });
|
|
664
695
|
const setSelected = (next) => context.setValue(next);
|
|
665
696
|
for (const option of options) {
|
|
666
697
|
const label = el("label", {
|
|
@@ -672,13 +703,23 @@ function multiChoice(context) {
|
|
|
672
703
|
input.checked = selected.includes(option.id);
|
|
673
704
|
input.addEventListener("change", () => {
|
|
674
705
|
const current = getSelected();
|
|
675
|
-
setSelected(
|
|
706
|
+
setSelected(
|
|
707
|
+
input.checked ? [...current, option.id] : current.filter((id) => id !== option.id)
|
|
708
|
+
);
|
|
676
709
|
});
|
|
677
710
|
appendChildren(label, [
|
|
678
711
|
input,
|
|
679
712
|
optionIcon(option.icon),
|
|
680
|
-
el("span", {
|
|
681
|
-
|
|
713
|
+
el("span", {
|
|
714
|
+
className: "fillo-option-label",
|
|
715
|
+
text: option.label,
|
|
716
|
+
attrs: { "data-fillo": "optionLabel" }
|
|
717
|
+
}),
|
|
718
|
+
option.icon && selected.includes(option.id) ? el("span", {
|
|
719
|
+
className: "fillo-option-check",
|
|
720
|
+
text: "\u2713",
|
|
721
|
+
attrs: { "aria-hidden": "true" }
|
|
722
|
+
}) : null
|
|
682
723
|
]);
|
|
683
724
|
wrap.appendChild(label);
|
|
684
725
|
}
|
|
@@ -704,7 +745,11 @@ function multiChoice(context) {
|
|
|
704
745
|
appendChildren(label, [
|
|
705
746
|
appendChildren(el("span", { className: "fillo-option-main" }), [
|
|
706
747
|
input,
|
|
707
|
-
el("span", {
|
|
748
|
+
el("span", {
|
|
749
|
+
className: "fillo-option-label",
|
|
750
|
+
text: DEFAULT_STRINGS.other,
|
|
751
|
+
attrs: { "data-fillo": "optionLabel" }
|
|
752
|
+
})
|
|
708
753
|
])
|
|
709
754
|
]);
|
|
710
755
|
if (active) {
|
|
@@ -750,11 +795,16 @@ function dropdown(context) {
|
|
|
750
795
|
className: "fillo-input fillo-select",
|
|
751
796
|
attrs: { id: context.ids.inputId, "data-fillo": "control" }
|
|
752
797
|
});
|
|
753
|
-
select.appendChild(
|
|
798
|
+
select.appendChild(
|
|
799
|
+
el("option", { text: field.placeholder ?? "Choose...", attrs: { value: "" } })
|
|
800
|
+
);
|
|
754
801
|
for (const option of displayOptions(field)) {
|
|
755
802
|
select.appendChild(el("option", { text: option.label, attrs: { value: option.id } }));
|
|
756
803
|
}
|
|
757
|
-
if (field.allowOther)
|
|
804
|
+
if (field.allowOther)
|
|
805
|
+
select.appendChild(
|
|
806
|
+
el("option", { text: `${DEFAULT_STRINGS.other}\u2026`, attrs: { value: other } })
|
|
807
|
+
);
|
|
758
808
|
select.value = otherActive ? other : value;
|
|
759
809
|
select.addEventListener("change", () => {
|
|
760
810
|
if (select.value === other) {
|
|
@@ -766,7 +816,10 @@ function dropdown(context) {
|
|
|
766
816
|
}
|
|
767
817
|
});
|
|
768
818
|
applyFieldAria(select, field, context.error, context.ids);
|
|
769
|
-
appendChildren(selectWrap, [
|
|
819
|
+
appendChildren(selectWrap, [
|
|
820
|
+
select,
|
|
821
|
+
el("span", { className: "fillo-select-icon", attrs: { "aria-hidden": "true" } })
|
|
822
|
+
]);
|
|
770
823
|
wrap.appendChild(selectWrap);
|
|
771
824
|
if (otherActive) {
|
|
772
825
|
const other2 = el("input", {
|
|
@@ -826,9 +879,10 @@ function checkbox(context) {
|
|
|
826
879
|
appendChildren(label2, [
|
|
827
880
|
copy,
|
|
828
881
|
input2,
|
|
829
|
-
appendChildren(
|
|
830
|
-
el("span", { className: "fillo-toggle-
|
|
831
|
-
|
|
882
|
+
appendChildren(
|
|
883
|
+
el("span", { className: "fillo-toggle-track", attrs: { "aria-hidden": "true" } }),
|
|
884
|
+
[el("span", { className: "fillo-toggle-thumb" })]
|
|
885
|
+
)
|
|
832
886
|
]);
|
|
833
887
|
wrap2.appendChild(label2);
|
|
834
888
|
if (context.field.description)
|
|
@@ -890,7 +944,7 @@ function checkbox(context) {
|
|
|
890
944
|
el("p", {
|
|
891
945
|
className: "fillo-error",
|
|
892
946
|
text: context.error,
|
|
893
|
-
attrs: {
|
|
947
|
+
attrs: { id: context.ids.errorId, "data-fillo": "error" }
|
|
894
948
|
})
|
|
895
949
|
);
|
|
896
950
|
return wrap;
|
|
@@ -1013,7 +1067,7 @@ function ranking(context) {
|
|
|
1013
1067
|
"data-fillo": "control"
|
|
1014
1068
|
}
|
|
1015
1069
|
});
|
|
1016
|
-
applyFieldAria(group, context.field, context.error, context.ids);
|
|
1070
|
+
applyFieldAria(group, context.field, context.error, context.ids, { requiredSupported: false });
|
|
1017
1071
|
const list = el("ol", { className: "fillo-ranking" });
|
|
1018
1072
|
for (const [index, id] of order.entries()) {
|
|
1019
1073
|
const option = field.options.find((item2) => item2.id === id);
|
|
@@ -1072,8 +1126,9 @@ function matrix(context) {
|
|
|
1072
1126
|
const table = el("table", { className: "fillo-matrix" });
|
|
1073
1127
|
const thead = document.createElement("thead");
|
|
1074
1128
|
const headRow = document.createElement("tr");
|
|
1075
|
-
headRow.appendChild(document.createElement("
|
|
1076
|
-
for (const col of field.columns)
|
|
1129
|
+
headRow.appendChild(document.createElement("td"));
|
|
1130
|
+
for (const col of field.columns)
|
|
1131
|
+
headRow.appendChild(el("th", { text: col.label, attrs: { scope: "col" } }));
|
|
1077
1132
|
thead.appendChild(headRow);
|
|
1078
1133
|
table.appendChild(thead);
|
|
1079
1134
|
const tbody = document.createElement("tbody");
|
|
@@ -1126,7 +1181,7 @@ function matrix(context) {
|
|
|
1126
1181
|
"data-fillo": "control"
|
|
1127
1182
|
}
|
|
1128
1183
|
});
|
|
1129
|
-
applyFieldAria(group, context.field, context.error, context.ids);
|
|
1184
|
+
applyFieldAria(group, context.field, context.error, context.ids, { requiredSupported: false });
|
|
1130
1185
|
group.appendChild(table);
|
|
1131
1186
|
return shell(context.field, context.error, group, context.ids);
|
|
1132
1187
|
}
|
|
@@ -1142,7 +1197,11 @@ function signature(context) {
|
|
|
1142
1197
|
text: "Clear",
|
|
1143
1198
|
attrs: { type: "button" }
|
|
1144
1199
|
});
|
|
1145
|
-
const hint = el("div", {
|
|
1200
|
+
const hint = el("div", {
|
|
1201
|
+
className: "fillo-signature-hint",
|
|
1202
|
+
text: "Sign here",
|
|
1203
|
+
attrs: { "aria-hidden": "true" }
|
|
1204
|
+
});
|
|
1146
1205
|
canvas.width = 720;
|
|
1147
1206
|
canvas.height = 220;
|
|
1148
1207
|
const ctx = canvas.getContext("2d");
|
|
@@ -1249,8 +1308,47 @@ function formatBytes(bytes) {
|
|
|
1249
1308
|
if (bytes < 1024 ** 3) return `${(bytes / 1024 ** 2).toFixed(1)} MB`;
|
|
1250
1309
|
return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
|
|
1251
1310
|
}
|
|
1252
|
-
function
|
|
1253
|
-
const
|
|
1311
|
+
function uploadSvg(paths, size = 22) {
|
|
1312
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
1313
|
+
svg.setAttribute("class", "fillo-file-state-icon");
|
|
1314
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
1315
|
+
svg.setAttribute("width", String(size));
|
|
1316
|
+
svg.setAttribute("height", String(size));
|
|
1317
|
+
svg.setAttribute("fill", "none");
|
|
1318
|
+
svg.setAttribute("aria-hidden", "true");
|
|
1319
|
+
svg.setAttribute("focusable", "false");
|
|
1320
|
+
for (const d of paths) {
|
|
1321
|
+
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
1322
|
+
path.setAttribute("d", d);
|
|
1323
|
+
svg.appendChild(path);
|
|
1324
|
+
}
|
|
1325
|
+
return svg;
|
|
1326
|
+
}
|
|
1327
|
+
function fileStateIcon(state) {
|
|
1328
|
+
const mark = el("span", {
|
|
1329
|
+
className: `fillo-file-state fillo-file-state--${state}`,
|
|
1330
|
+
attrs: { "aria-hidden": "true" }
|
|
1331
|
+
});
|
|
1332
|
+
mark.appendChild(
|
|
1333
|
+
state === "done" ? uploadSvg([
|
|
1334
|
+
"M3.75 12a8.25 8.25 0 1 0 16.5 0 8.25 8.25 0 1 0-16.5 0",
|
|
1335
|
+
"m8.5 12 2.25 2.25 4.75-5"
|
|
1336
|
+
]) : state === "failed" ? uploadSvg([
|
|
1337
|
+
"M3.75 12a8.25 8.25 0 1 0 16.5 0 8.25 8.25 0 1 0-16.5 0",
|
|
1338
|
+
"M12 7.75v5.5",
|
|
1339
|
+
"M12 16.5h.01"
|
|
1340
|
+
]) : uploadSvg(["M7.25 3.75h6.5l3 3v13.5h-9.5z", "M13.75 3.75v3h3"])
|
|
1341
|
+
);
|
|
1342
|
+
return mark;
|
|
1343
|
+
}
|
|
1344
|
+
function closeIcon() {
|
|
1345
|
+
const icon = uploadSvg(["m7 7 10 10M17 7 7 17"], 16);
|
|
1346
|
+
icon.setAttribute("class", "fillo-file-action-icon");
|
|
1347
|
+
return icon;
|
|
1348
|
+
}
|
|
1349
|
+
function fileActionBtn(className, label, content, onClick) {
|
|
1350
|
+
const btn = el("button", { className, attrs: { type: "button", "aria-label": label } });
|
|
1351
|
+
btn.append(content);
|
|
1254
1352
|
btn.addEventListener("click", onClick);
|
|
1255
1353
|
return btn;
|
|
1256
1354
|
}
|
|
@@ -1258,29 +1356,87 @@ function fileUpload(context) {
|
|
|
1258
1356
|
const field = context.field;
|
|
1259
1357
|
if (field.kind !== "file_upload") return null;
|
|
1260
1358
|
const internal = context;
|
|
1261
|
-
const
|
|
1262
|
-
const
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
input.disabled = storageBlocked;
|
|
1269
|
-
applyFieldAria(input, field, context.error, context.ids);
|
|
1270
|
-
input.addEventListener("change", () => {
|
|
1271
|
-
if (storageBlocked) return;
|
|
1272
|
-
const files = Array.from(input.files ?? []);
|
|
1273
|
-
input.value = "";
|
|
1274
|
-
void context.uploadFiles?.(field, files);
|
|
1275
|
-
});
|
|
1359
|
+
const uploadDisabledReason = internal.uploadDisabledReason;
|
|
1360
|
+
const canUpload = uploadDisabledReason === void 0;
|
|
1361
|
+
const maxFiles = field.maxFiles ?? 1;
|
|
1362
|
+
const maxMb = field.maxFileSizeMb ?? 500;
|
|
1363
|
+
const done = Array.isArray(context.value) ? context.value : [];
|
|
1364
|
+
const rows = internal.uploadRows ?? [];
|
|
1365
|
+
const remaining = maxFiles - done.length - rows.filter((row) => row.error === void 0).length;
|
|
1276
1366
|
const wrap = el("div");
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1367
|
+
if (remaining > 0) {
|
|
1368
|
+
const input = el("input", {
|
|
1369
|
+
className: "fillo-input",
|
|
1370
|
+
attrs: { id: context.ids.inputId, type: "file", hidden: "" }
|
|
1371
|
+
});
|
|
1372
|
+
if (maxFiles > 1) input.multiple = true;
|
|
1373
|
+
if (field.accept?.length) input.accept = field.accept.join(",");
|
|
1374
|
+
input.disabled = !canUpload;
|
|
1375
|
+
input.addEventListener("change", () => {
|
|
1376
|
+
if (!canUpload) return;
|
|
1377
|
+
const files = Array.from(input.files ?? []);
|
|
1378
|
+
input.value = "";
|
|
1379
|
+
void context.uploadFiles?.(field, files);
|
|
1380
|
+
});
|
|
1381
|
+
const dropzone = el("div", {
|
|
1382
|
+
className: `fillo-dropzone${canUpload ? "" : " fillo-dropzone--disabled"}`,
|
|
1383
|
+
attrs: {
|
|
1384
|
+
role: "button",
|
|
1385
|
+
tabindex: "0",
|
|
1386
|
+
"aria-labelledby": context.ids.labelId,
|
|
1387
|
+
"data-fillo": "control"
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
applyFieldAria(dropzone, field, context.error, context.ids, { requiredSupported: false });
|
|
1391
|
+
if (!canUpload) dropzone.setAttribute("aria-disabled", "true");
|
|
1392
|
+
dropzone.addEventListener("dragover", (e) => {
|
|
1393
|
+
e.preventDefault();
|
|
1394
|
+
dropzone.classList.add("fillo-dropzone--over");
|
|
1395
|
+
dropzone.setAttribute("data-drag-over", "");
|
|
1396
|
+
});
|
|
1397
|
+
dropzone.addEventListener("dragleave", () => {
|
|
1398
|
+
dropzone.classList.remove("fillo-dropzone--over");
|
|
1399
|
+
dropzone.removeAttribute("data-drag-over");
|
|
1400
|
+
});
|
|
1401
|
+
dropzone.addEventListener("drop", (e) => {
|
|
1402
|
+
e.preventDefault();
|
|
1403
|
+
dropzone.classList.remove("fillo-dropzone--over");
|
|
1404
|
+
dropzone.removeAttribute("data-drag-over");
|
|
1405
|
+
if (canUpload) void context.uploadFiles?.(field, Array.from(e.dataTransfer?.files ?? []));
|
|
1406
|
+
});
|
|
1407
|
+
dropzone.addEventListener("click", () => {
|
|
1408
|
+
if (canUpload) input.click();
|
|
1409
|
+
});
|
|
1410
|
+
dropzone.addEventListener("keydown", (e) => {
|
|
1411
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
1412
|
+
e.preventDefault();
|
|
1413
|
+
if (canUpload) input.click();
|
|
1414
|
+
}
|
|
1282
1415
|
});
|
|
1283
|
-
|
|
1416
|
+
dropzone.appendChild(input);
|
|
1417
|
+
if (canUpload) {
|
|
1418
|
+
appendChildren(dropzone, [
|
|
1419
|
+
el("span", {
|
|
1420
|
+
className: "fillo-dropzone-title",
|
|
1421
|
+
text: DEFAULT_FIELD_STRINGS.dropzoneTitle(maxFiles > 1)
|
|
1422
|
+
}),
|
|
1423
|
+
el("span", {
|
|
1424
|
+
className: "fillo-dropzone-hint",
|
|
1425
|
+
text: DEFAULT_FIELD_STRINGS.dropzoneHint(maxMb)
|
|
1426
|
+
})
|
|
1427
|
+
]);
|
|
1428
|
+
} else if (uploadDisabledReason === "render_only") {
|
|
1429
|
+
dropzone.appendChild(
|
|
1430
|
+
el("span", {
|
|
1431
|
+
className: "fillo-dropzone-hint",
|
|
1432
|
+
text: DEFAULT_FIELD_STRINGS.uploadsRenderOnly
|
|
1433
|
+
})
|
|
1434
|
+
);
|
|
1435
|
+
} else if (internal.storageFixUrl) {
|
|
1436
|
+
const hint = el("span", {
|
|
1437
|
+
className: "fillo-dropzone-hint",
|
|
1438
|
+
text: "Connect file storage to enable uploads"
|
|
1439
|
+
});
|
|
1284
1440
|
hint.append(" \u2014 ");
|
|
1285
1441
|
hint.appendChild(
|
|
1286
1442
|
el("a", {
|
|
@@ -1288,47 +1444,55 @@ function fileUpload(context) {
|
|
|
1288
1444
|
attrs: { href: internal.storageFixUrl, target: "_blank", rel: "noopener noreferrer" }
|
|
1289
1445
|
})
|
|
1290
1446
|
);
|
|
1447
|
+
dropzone.appendChild(hint);
|
|
1448
|
+
} else {
|
|
1449
|
+
dropzone.appendChild(
|
|
1450
|
+
el("span", {
|
|
1451
|
+
className: "fillo-dropzone-hint",
|
|
1452
|
+
text: DEFAULT_FIELD_STRINGS.uploadsUnavailable
|
|
1453
|
+
})
|
|
1454
|
+
);
|
|
1291
1455
|
}
|
|
1292
|
-
wrap.appendChild(
|
|
1456
|
+
wrap.appendChild(dropzone);
|
|
1293
1457
|
}
|
|
1294
|
-
const done = Array.isArray(context.value) ? context.value : [];
|
|
1295
|
-
const rows = internal.uploadRows ?? [];
|
|
1296
|
-
const maxFiles = field.maxFiles ?? 1;
|
|
1297
|
-
const remaining = maxFiles - done.length - rows.filter((row) => row.error === void 0).length;
|
|
1298
1458
|
if (done.length > 0 || rows.length > 0) {
|
|
1299
1459
|
const list = el("ul", { className: "fillo-files" });
|
|
1300
1460
|
for (const file of done) {
|
|
1301
1461
|
const li = el("li", { className: "fillo-file fillo-file--done" });
|
|
1302
|
-
|
|
1462
|
+
const content = el("span", { className: "fillo-file-content" });
|
|
1463
|
+
appendChildren(content, [
|
|
1303
1464
|
el("span", { className: "fillo-file-name", text: file.name }),
|
|
1304
|
-
el("span", {
|
|
1305
|
-
|
|
1465
|
+
el("span", {
|
|
1466
|
+
className: "fillo-file-meta",
|
|
1467
|
+
text: DEFAULT_FIELD_STRINGS.uploadedFile(formatBytes(file.size))
|
|
1468
|
+
})
|
|
1469
|
+
]);
|
|
1470
|
+
const actions = el("span", { className: "fillo-file-actions" });
|
|
1471
|
+
actions.appendChild(
|
|
1306
1472
|
fileActionBtn(
|
|
1307
1473
|
"fillo-file-remove",
|
|
1308
|
-
|
|
1309
|
-
|
|
1474
|
+
`${DEFAULT_FIELD_STRINGS.uploadRemove} ${file.name}`,
|
|
1475
|
+
closeIcon(),
|
|
1310
1476
|
() => context.setValue(done.filter((f) => f.fileId !== file.fileId))
|
|
1311
1477
|
)
|
|
1312
|
-
|
|
1478
|
+
);
|
|
1479
|
+
appendChildren(li, [fileStateIcon("done"), content, actions]);
|
|
1313
1480
|
list.appendChild(li);
|
|
1314
1481
|
}
|
|
1315
1482
|
for (const row of rows) {
|
|
1316
1483
|
const failed = row.error !== void 0;
|
|
1317
1484
|
const li = el("li", {
|
|
1318
|
-
className: `fillo-file${failed ? " fillo-file--failed" : ""}
|
|
1319
|
-
attrs: failed ? {} : {
|
|
1320
|
-
role: "progressbar",
|
|
1321
|
-
"aria-label": `Uploading ${row.name}`,
|
|
1322
|
-
"aria-valuemin": "0",
|
|
1323
|
-
"aria-valuemax": "100",
|
|
1324
|
-
"aria-valuenow": String(Math.round(row.fraction * 100))
|
|
1325
|
-
}
|
|
1485
|
+
className: `fillo-file${failed ? " fillo-file--failed" : ""}`
|
|
1326
1486
|
});
|
|
1327
|
-
li.appendChild(
|
|
1487
|
+
li.appendChild(fileStateIcon(failed ? "failed" : "uploading"));
|
|
1488
|
+
const content = el("span", { className: "fillo-file-content" });
|
|
1489
|
+
content.appendChild(el("span", { className: "fillo-file-name", text: row.name }));
|
|
1328
1490
|
if (failed) {
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1491
|
+
const actions = el("span", { className: "fillo-file-actions" });
|
|
1492
|
+
content.appendChild(el("span", { className: "fillo-file-error", text: row.error ?? "" }));
|
|
1493
|
+
li.appendChild(content);
|
|
1494
|
+
if (!row.tooLarge && remaining > 0) {
|
|
1495
|
+
actions.appendChild(
|
|
1332
1496
|
fileActionBtn(
|
|
1333
1497
|
"fillo-file-retry",
|
|
1334
1498
|
`${DEFAULT_FIELD_STRINGS.uploadRetry} ${row.name}`,
|
|
@@ -1337,14 +1501,52 @@ function fileUpload(context) {
|
|
|
1337
1501
|
)
|
|
1338
1502
|
);
|
|
1339
1503
|
}
|
|
1340
|
-
|
|
1504
|
+
actions.appendChild(
|
|
1505
|
+
fileActionBtn(
|
|
1506
|
+
"fillo-file-remove",
|
|
1507
|
+
`${DEFAULT_FIELD_STRINGS.uploadDismiss} ${row.name}`,
|
|
1508
|
+
closeIcon(),
|
|
1509
|
+
() => internal.uploadAction?.(row.key, "dismiss")
|
|
1510
|
+
)
|
|
1511
|
+
);
|
|
1512
|
+
li.appendChild(actions);
|
|
1341
1513
|
} else {
|
|
1342
1514
|
const bar = el("span", { className: "fillo-progress-bar" });
|
|
1343
1515
|
bar.style.width = `${row.fraction * 100}%`;
|
|
1516
|
+
content.appendChild(
|
|
1517
|
+
el("span", {
|
|
1518
|
+
className: "fillo-file-meta",
|
|
1519
|
+
text: DEFAULT_FIELD_STRINGS.uploadingFile(
|
|
1520
|
+
Math.round(row.fraction * 100),
|
|
1521
|
+
formatBytes(row.size)
|
|
1522
|
+
)
|
|
1523
|
+
})
|
|
1524
|
+
);
|
|
1525
|
+
const actions = el("span", { className: "fillo-file-actions" });
|
|
1526
|
+
actions.appendChild(
|
|
1527
|
+
fileActionBtn(
|
|
1528
|
+
"fillo-file-remove",
|
|
1529
|
+
`${DEFAULT_FIELD_STRINGS.uploadCancel} ${row.name}`,
|
|
1530
|
+
closeIcon(),
|
|
1531
|
+
() => internal.uploadAction?.(row.key, "cancel")
|
|
1532
|
+
)
|
|
1533
|
+
);
|
|
1344
1534
|
appendChildren(li, [
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1535
|
+
content,
|
|
1536
|
+
actions,
|
|
1537
|
+
appendChildren(
|
|
1538
|
+
el("span", {
|
|
1539
|
+
className: "fillo-progress",
|
|
1540
|
+
attrs: {
|
|
1541
|
+
role: "progressbar",
|
|
1542
|
+
"aria-label": `Uploading ${row.name}`,
|
|
1543
|
+
"aria-valuemin": "0",
|
|
1544
|
+
"aria-valuemax": "100",
|
|
1545
|
+
"aria-valuenow": String(Math.round(row.fraction * 100))
|
|
1546
|
+
}
|
|
1547
|
+
}),
|
|
1548
|
+
[bar]
|
|
1549
|
+
)
|
|
1348
1550
|
]);
|
|
1349
1551
|
}
|
|
1350
1552
|
list.appendChild(li);
|
|
@@ -1596,7 +1798,7 @@ var DomFormController = class {
|
|
|
1596
1798
|
return {
|
|
1597
1799
|
form,
|
|
1598
1800
|
formId: this.formId,
|
|
1599
|
-
client: this.options.client,
|
|
1801
|
+
client: this.options.renderOnly ? void 0 : this.options.client,
|
|
1600
1802
|
data: state?.data ?? this.mutableData,
|
|
1601
1803
|
errors: state?.errors ?? {},
|
|
1602
1804
|
status: this.status,
|
|
@@ -1640,9 +1842,14 @@ var DomFormController = class {
|
|
|
1640
1842
|
resolutionFailureCode = null;
|
|
1641
1843
|
async resolve() {
|
|
1642
1844
|
try {
|
|
1643
|
-
this.challenge = this.options.challenge;
|
|
1845
|
+
this.challenge = this.options.renderOnly ? void 0 : this.options.challenge;
|
|
1644
1846
|
const codeForm = isCodeForm(this.options.form) ? this.options.form : null;
|
|
1645
1847
|
if (codeForm) {
|
|
1848
|
+
if (this.options.renderOnly) {
|
|
1849
|
+
this.setSchema(codeForm.schema, this.options.theme ?? codeForm.theme, void 0);
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
if (!this.options.client && this.options.formId) this.options.client = createClient();
|
|
1646
1853
|
const client = this.options.client;
|
|
1647
1854
|
if (client?.key) {
|
|
1648
1855
|
this.resolveFormIdFn = async () => {
|
|
@@ -1687,12 +1894,7 @@ var DomFormController = class {
|
|
|
1687
1894
|
}
|
|
1688
1895
|
if (result.syncError) {
|
|
1689
1896
|
this.reportSyncNotice(
|
|
1690
|
-
new FilloError(
|
|
1691
|
-
result.syncError.message,
|
|
1692
|
-
403,
|
|
1693
|
-
void 0,
|
|
1694
|
-
result.syncError.code
|
|
1695
|
-
)
|
|
1897
|
+
new FilloError(result.syncError.message, 403, void 0, result.syncError.code)
|
|
1696
1898
|
);
|
|
1697
1899
|
}
|
|
1698
1900
|
this.resolutionFailureCode = null;
|
|
@@ -1705,8 +1907,16 @@ var DomFormController = class {
|
|
|
1705
1907
|
}
|
|
1706
1908
|
};
|
|
1707
1909
|
}
|
|
1708
|
-
if (!client?.key) {
|
|
1709
|
-
this.setSchema(
|
|
1910
|
+
if (!client?.key && this.options.formId) {
|
|
1911
|
+
this.setSchema(
|
|
1912
|
+
codeForm.schema,
|
|
1913
|
+
this.options.theme ?? codeForm.theme,
|
|
1914
|
+
this.options.formId
|
|
1915
|
+
);
|
|
1916
|
+
}
|
|
1917
|
+
if (!client && !this.options.formId) {
|
|
1918
|
+
this.genericUnavailable = !this.devChrome();
|
|
1919
|
+
throw formTargetRequiredError();
|
|
1710
1920
|
}
|
|
1711
1921
|
if (client && !client.key && !this.options.formId) {
|
|
1712
1922
|
this.reportSyncFailure(
|
|
@@ -1721,7 +1931,11 @@ var DomFormController = class {
|
|
|
1721
1931
|
}
|
|
1722
1932
|
if (!client?.key) return;
|
|
1723
1933
|
if (this.devChrome()) {
|
|
1724
|
-
this.setSchema(
|
|
1934
|
+
this.setSchema(
|
|
1935
|
+
codeForm.schema,
|
|
1936
|
+
this.options.theme ?? codeForm.theme,
|
|
1937
|
+
this.options.formId
|
|
1938
|
+
);
|
|
1725
1939
|
} else {
|
|
1726
1940
|
this.theme = this.options.theme ?? codeForm.theme;
|
|
1727
1941
|
this.render();
|
|
@@ -1763,19 +1977,18 @@ var DomFormController = class {
|
|
|
1763
1977
|
if (result.warning) console.warn(`[fillo] ${result.warning}`);
|
|
1764
1978
|
if (result.syncError) {
|
|
1765
1979
|
this.reportSyncNotice(
|
|
1766
|
-
new FilloError(
|
|
1767
|
-
result.syncError.message,
|
|
1768
|
-
403,
|
|
1769
|
-
void 0,
|
|
1770
|
-
result.syncError.code
|
|
1771
|
-
)
|
|
1980
|
+
new FilloError(result.syncError.message, 403, void 0, result.syncError.code)
|
|
1772
1981
|
);
|
|
1773
1982
|
}
|
|
1774
1983
|
const connectStorage = result.warningUrl ? ` \u2014 connect storage to publish: ${result.warningUrl}` : "";
|
|
1775
1984
|
if (result.staged) {
|
|
1776
|
-
console.info(
|
|
1985
|
+
console.info(
|
|
1986
|
+
`[fillo] "${codeForm.id}" changes staged as a draft \u2014 rendering the live version until you publish them in your Fillo dashboard${connectStorage}`
|
|
1987
|
+
);
|
|
1777
1988
|
} else if (result.status === "draft") {
|
|
1778
|
-
console.info(
|
|
1989
|
+
console.info(
|
|
1990
|
+
`[fillo] "${codeForm.id}" is a draft \u2014 it renders here but won't accept responses until published${connectStorage}`
|
|
1991
|
+
);
|
|
1779
1992
|
}
|
|
1780
1993
|
} catch (error) {
|
|
1781
1994
|
if (this.destroyed) return;
|
|
@@ -1785,6 +1998,15 @@ var DomFormController = class {
|
|
|
1785
1998
|
}
|
|
1786
1999
|
const schema = this.options.form && !isCodeForm(this.options.form) ? this.options.form : null;
|
|
1787
2000
|
if (schema) {
|
|
2001
|
+
if (this.options.renderOnly) {
|
|
2002
|
+
this.setSchema(schema, this.options.theme, void 0);
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2005
|
+
if (!this.options.formId) {
|
|
2006
|
+
this.genericUnavailable = !this.devChrome();
|
|
2007
|
+
throw formTargetRequiredError();
|
|
2008
|
+
}
|
|
2009
|
+
this.options.client ??= createClient();
|
|
1788
2010
|
this.setSchema(schema, this.options.theme, this.options.formId);
|
|
1789
2011
|
return;
|
|
1790
2012
|
}
|
|
@@ -1813,7 +2035,11 @@ var DomFormController = class {
|
|
|
1813
2035
|
this.render(this.fatalError);
|
|
1814
2036
|
return;
|
|
1815
2037
|
}
|
|
1816
|
-
this.setSchema(
|
|
2038
|
+
this.setSchema(
|
|
2039
|
+
published.schema,
|
|
2040
|
+
this.options.theme ?? published.theme ?? void 0,
|
|
2041
|
+
published.id
|
|
2042
|
+
);
|
|
1817
2043
|
} catch (error) {
|
|
1818
2044
|
if (this.destroyed) return;
|
|
1819
2045
|
this.handleError(toFilloError(error));
|
|
@@ -1823,31 +2049,35 @@ var DomFormController = class {
|
|
|
1823
2049
|
setSchema(schema, theme, formId) {
|
|
1824
2050
|
if (this.destroyed) return;
|
|
1825
2051
|
const normalized = normalizeFormSchema(schema);
|
|
1826
|
-
if (!normalized.ok)
|
|
2052
|
+
if (!normalized.ok)
|
|
2053
|
+
throw new FilloError(`This form could not be rendered: ${normalized.error}`, 422);
|
|
1827
2054
|
const normalizedSchema = normalized.schema;
|
|
1828
2055
|
this.schema = normalizedSchema;
|
|
1829
2056
|
this.theme = theme;
|
|
1830
|
-
this.
|
|
2057
|
+
const renderOnly = this.options.renderOnly === true;
|
|
2058
|
+
this.formId = renderOnly ? void 0 : formId;
|
|
1831
2059
|
const displayOnly = this.notOpenVariant !== null;
|
|
2060
|
+
const transportDisabled = displayOnly || renderOnly;
|
|
1832
2061
|
this.engine = coreCreateFormController({
|
|
1833
2062
|
form: normalizedSchema,
|
|
1834
|
-
formId:
|
|
1835
|
-
client:
|
|
2063
|
+
formId: transportDisabled ? void 0 : formId,
|
|
2064
|
+
client: transportDisabled ? void 0 : this.options.client,
|
|
1836
2065
|
initialData: this.options.initialData,
|
|
1837
2066
|
onChange: displayOnly ? void 0 : this.options.onChange,
|
|
1838
|
-
onSubmitted:
|
|
2067
|
+
onSubmitted: transportDisabled ? void 0 : this.options.onSubmitted,
|
|
1839
2068
|
getHoneypot: () => this.element.querySelector('input[name="fillo_hp_field"]')?.value ?? this.hpValue,
|
|
1840
2069
|
surface: "default",
|
|
1841
|
-
resolveFormId:
|
|
2070
|
+
resolveFormId: transportDisabled ? void 0 : this.resolveFormIdFn,
|
|
2071
|
+
skipValidation: renderOnly,
|
|
1842
2072
|
// Dev chrome gets the developer-grade resolution failure (message +
|
|
1843
2073
|
// machine code) instead of the respondent-safe "unavailable" fallback.
|
|
1844
2074
|
verboseResolutionErrors: this.devChrome(),
|
|
1845
|
-
respondent:
|
|
2075
|
+
respondent: transportDisabled ? void 0 : this.options.respondent,
|
|
1846
2076
|
// Challenge (Turnstile) — wired as react's useFilloController wires the
|
|
1847
2077
|
// core controller: challengeRequired captured once here (forced off for
|
|
1848
2078
|
// a not-open preview, which never submits regardless);
|
|
1849
2079
|
// getChallengeToken/onChallengeFailed stay live via `this`.
|
|
1850
|
-
challengeRequired:
|
|
2080
|
+
challengeRequired: transportDisabled ? false : Boolean(this.challenge),
|
|
1851
2081
|
getChallengeToken: () => this.challengeToken,
|
|
1852
2082
|
onChallengeFailed: () => this.resetChallengeWidget()
|
|
1853
2083
|
});
|
|
@@ -1866,7 +2096,7 @@ var DomFormController = class {
|
|
|
1866
2096
|
this.queueRender();
|
|
1867
2097
|
}
|
|
1868
2098
|
});
|
|
1869
|
-
if (!
|
|
2099
|
+
if (!transportDisabled) this.configureDraftFlush(normalizedSchema);
|
|
1870
2100
|
this.phase = this.closed ? "closed" : "ready";
|
|
1871
2101
|
this.render();
|
|
1872
2102
|
}
|
|
@@ -1982,13 +2212,22 @@ var DomFormController = class {
|
|
|
1982
2212
|
* (it must not strand the field at permanently-full). */
|
|
1983
2213
|
fieldActive(fieldId) {
|
|
1984
2214
|
let n = 0;
|
|
1985
|
-
for (const row of this.inFlight.values())
|
|
2215
|
+
for (const row of this.inFlight.values())
|
|
2216
|
+
if (row.fieldId === fieldId && row.error === void 0) n++;
|
|
1986
2217
|
return n;
|
|
1987
2218
|
}
|
|
1988
2219
|
uploadRowsFor(fieldId) {
|
|
1989
2220
|
const rows = [];
|
|
1990
2221
|
for (const [key, row] of this.inFlight) {
|
|
1991
|
-
if (row.fieldId === fieldId)
|
|
2222
|
+
if (row.fieldId === fieldId)
|
|
2223
|
+
rows.push({
|
|
2224
|
+
key,
|
|
2225
|
+
name: row.name,
|
|
2226
|
+
size: row.size,
|
|
2227
|
+
fraction: row.fraction,
|
|
2228
|
+
error: row.error,
|
|
2229
|
+
tooLarge: row.tooLarge
|
|
2230
|
+
});
|
|
1992
2231
|
}
|
|
1993
2232
|
return rows;
|
|
1994
2233
|
}
|
|
@@ -2004,6 +2243,7 @@ var DomFormController = class {
|
|
|
2004
2243
|
} else if (action === "dismiss") {
|
|
2005
2244
|
this.inFlight.delete(key);
|
|
2006
2245
|
} else {
|
|
2246
|
+
if (row.tooLarge) return;
|
|
2007
2247
|
this.inFlight.delete(key);
|
|
2008
2248
|
void this.startUpload(row.fieldId, row.file, row.maxFiles);
|
|
2009
2249
|
}
|
|
@@ -2013,7 +2253,15 @@ var DomFormController = class {
|
|
|
2013
2253
|
async startUpload(fieldId, file, maxFiles) {
|
|
2014
2254
|
const key = `${file.name}-${file.size}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
2015
2255
|
const controller = new AbortController();
|
|
2016
|
-
this.inFlight.set(key, {
|
|
2256
|
+
this.inFlight.set(key, {
|
|
2257
|
+
fieldId,
|
|
2258
|
+
name: file.name,
|
|
2259
|
+
size: file.size,
|
|
2260
|
+
fraction: 0,
|
|
2261
|
+
file,
|
|
2262
|
+
controller,
|
|
2263
|
+
maxFiles
|
|
2264
|
+
});
|
|
2017
2265
|
this.engine?.setUploading(fieldId, true);
|
|
2018
2266
|
this.queueRender();
|
|
2019
2267
|
try {
|
|
@@ -2059,6 +2307,7 @@ var DomFormController = class {
|
|
|
2059
2307
|
const maxFileSizeMb = this.uploadFileSizeLimitMb ? Math.min(configuredMaxMb, this.uploadFileSizeLimitMb) : configuredMaxMb;
|
|
2060
2308
|
const maxBytes = maxFileSizeMb * 1024 * 1024;
|
|
2061
2309
|
const selected = withinLimit.filter((file) => file.size <= maxBytes);
|
|
2310
|
+
const oversizedFiles = withinLimit.filter((file) => file.size > maxBytes);
|
|
2062
2311
|
const dropped = files.length - withinLimit.length;
|
|
2063
2312
|
const oversized = withinLimit.length - selected.length;
|
|
2064
2313
|
if (dropped > 0 || oversized > 0) {
|
|
@@ -2068,17 +2317,29 @@ var DomFormController = class {
|
|
|
2068
2317
|
`You can attach up to ${maxFiles} file${maxFiles === 1 ? "" : "s"}. ${dropped} ${dropped === 1 ? "file was" : "files were"} not added.`
|
|
2069
2318
|
);
|
|
2070
2319
|
}
|
|
2071
|
-
if (oversized > 0) {
|
|
2072
|
-
notices.push(
|
|
2073
|
-
`${oversized} ${oversized === 1 ? "file exceeds" : "files exceed"} the ${maxFileSizeMb} MB limit.`
|
|
2074
|
-
);
|
|
2075
|
-
}
|
|
2076
2320
|
const text = notices.join(" ");
|
|
2077
|
-
this.uploadNotices.set(field.id, text);
|
|
2078
|
-
this.
|
|
2321
|
+
if (text) this.uploadNotices.set(field.id, text);
|
|
2322
|
+
else this.uploadNotices.delete(field.id);
|
|
2323
|
+
if (text) this.announce(text);
|
|
2079
2324
|
} else {
|
|
2080
2325
|
this.uploadNotices.delete(field.id);
|
|
2081
2326
|
}
|
|
2327
|
+
for (const file of oversizedFiles) {
|
|
2328
|
+
const message = DEFAULT_FIELD_STRINGS.fileTooLarge(maxFileSizeMb);
|
|
2329
|
+
const key = `${file.name}-${file.size}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
2330
|
+
this.inFlight.set(key, {
|
|
2331
|
+
fieldId: field.id,
|
|
2332
|
+
name: file.name,
|
|
2333
|
+
size: file.size,
|
|
2334
|
+
fraction: 0,
|
|
2335
|
+
error: message,
|
|
2336
|
+
tooLarge: true,
|
|
2337
|
+
file,
|
|
2338
|
+
controller: new AbortController(),
|
|
2339
|
+
maxFiles
|
|
2340
|
+
});
|
|
2341
|
+
this.announceAlert(message);
|
|
2342
|
+
}
|
|
2082
2343
|
if (selected.length === 0) {
|
|
2083
2344
|
this.queueRender();
|
|
2084
2345
|
return;
|
|
@@ -2124,7 +2385,8 @@ var DomFormController = class {
|
|
|
2124
2385
|
this.teardownTurnstileWidget();
|
|
2125
2386
|
this.turnstileSiteKey = siteKey;
|
|
2126
2387
|
}
|
|
2127
|
-
if (this.turnstileWidgetId !== null || this.turnstileLoadInFlight || this.challengeError)
|
|
2388
|
+
if (this.turnstileWidgetId !== null || this.turnstileLoadInFlight || this.challengeError)
|
|
2389
|
+
return;
|
|
2128
2390
|
this.turnstileLoadInFlight = true;
|
|
2129
2391
|
const attempt = ++this.turnstileAttempt;
|
|
2130
2392
|
loadTurnstileScript().then(() => {
|
|
@@ -2227,7 +2489,9 @@ var DomFormController = class {
|
|
|
2227
2489
|
const buttons = Array.from(
|
|
2228
2490
|
this.element.querySelectorAll("[data-fillo-rank-opt]")
|
|
2229
2491
|
).filter((b) => b.getAttribute("data-fillo-rank-opt") === desc.rankOpt);
|
|
2230
|
-
target = buttons.find(
|
|
2492
|
+
target = buttons.find(
|
|
2493
|
+
(b) => b.getAttribute("data-fillo-rank-dir") === desc.rankDir && !b.disabled
|
|
2494
|
+
) ?? buttons.find((b) => !b.disabled) ?? null;
|
|
2231
2495
|
}
|
|
2232
2496
|
if (!target && desc.id) target = document.getElementById(desc.id);
|
|
2233
2497
|
if (!target && desc.fieldId != null && desc.controlIndex != null && desc.controlIndex >= 0) {
|
|
@@ -2250,11 +2514,6 @@ var DomFormController = class {
|
|
|
2250
2514
|
applyFocus(intent, captured) {
|
|
2251
2515
|
if (typeof document === "undefined") return;
|
|
2252
2516
|
if (intent === "error") {
|
|
2253
|
-
const summary = this.element.querySelector(".fillo-error-summary");
|
|
2254
|
-
if (summary) {
|
|
2255
|
-
summary.focus();
|
|
2256
|
-
return;
|
|
2257
|
-
}
|
|
2258
2517
|
const invalid = this.element.querySelector('[aria-invalid="true"]');
|
|
2259
2518
|
invalid && focusableWithin(invalid)?.focus();
|
|
2260
2519
|
return;
|
|
@@ -2311,13 +2570,26 @@ var DomFormController = class {
|
|
|
2311
2570
|
);
|
|
2312
2571
|
this.syncErrorNotice = error;
|
|
2313
2572
|
}
|
|
2573
|
+
// A macrotask, not a microtask (ledger #4, docs/decisions/input-quality.md):
|
|
2574
|
+
// a text field's blur fires "change" -> setValue -> this full-tree rebuild,
|
|
2575
|
+
// and if the respondent's next action is a pointer click elsewhere (e.g. a
|
|
2576
|
+
// rating star), that click is a SEPARATE, later task from the blur's — but
|
|
2577
|
+
// a microtask always drains before any later task runs, including that
|
|
2578
|
+
// click. A microtask-deferred rebuild still replaces the click's target
|
|
2579
|
+
// node before the click is dispatched, so Chromium/WebKit never fire it at
|
|
2580
|
+
// all (the click is silently swallowed — click doesn't fire when the
|
|
2581
|
+
// mousedown target was detached before mouseup: w3c/uievents#141).
|
|
2582
|
+
// setTimeout is the standard fix for exactly this "blur beats click" race:
|
|
2583
|
+
// scheduled during the blur's own task, it reliably runs BEHIND whatever
|
|
2584
|
+
// pointer/click tasks were already in flight for the same gesture, so an
|
|
2585
|
+
// in-flight click lands on the pre-rebuild node first.
|
|
2314
2586
|
queueRender() {
|
|
2315
2587
|
if (this.renderQueued || this.destroyed) return;
|
|
2316
2588
|
this.renderQueued = true;
|
|
2317
|
-
|
|
2589
|
+
setTimeout(() => {
|
|
2318
2590
|
this.renderQueued = false;
|
|
2319
2591
|
this.render();
|
|
2320
|
-
});
|
|
2592
|
+
}, 0);
|
|
2321
2593
|
}
|
|
2322
2594
|
render(error) {
|
|
2323
2595
|
if (this.destroyed) return;
|
|
@@ -2344,12 +2616,16 @@ var DomFormController = class {
|
|
|
2344
2616
|
}
|
|
2345
2617
|
const activeError = error ?? this.fatalError;
|
|
2346
2618
|
if (this.phase === "error" && activeError) {
|
|
2619
|
+
const targetMessage = activeError.code === "form_target_required" && this.devChrome() ? `${activeError.message} (${activeError.code})` : null;
|
|
2347
2620
|
const errorView = this.options.renderError?.(activeError) ?? el("div", {
|
|
2348
2621
|
className: "fillo-form fillo-form--error",
|
|
2349
|
-
text: this.genericUnavailable ? "This form is unavailable." : loadFailureMessage(activeError)
|
|
2622
|
+
text: targetMessage ?? (this.genericUnavailable ? "This form is unavailable." : loadFailureMessage(activeError))
|
|
2350
2623
|
});
|
|
2351
2624
|
if (!errorView.hasAttribute("data-fillo")) errorView.setAttribute("data-fillo", "root");
|
|
2352
2625
|
if (!errorView.hasAttribute("data-state")) errorView.setAttribute("data-state", "error");
|
|
2626
|
+
if (activeError.code && !errorView.hasAttribute("data-error-code")) {
|
|
2627
|
+
errorView.setAttribute("data-error-code", activeError.code);
|
|
2628
|
+
}
|
|
2353
2629
|
if (!errorView.hasAttribute("role")) errorView.setAttribute("role", "alert");
|
|
2354
2630
|
themeStyle(errorView, this.theme);
|
|
2355
2631
|
this.element.appendChild(errorView);
|
|
@@ -2361,13 +2637,16 @@ var DomFormController = class {
|
|
|
2361
2637
|
attrs: { "data-fillo": "root", "data-state": "closed" }
|
|
2362
2638
|
});
|
|
2363
2639
|
themeStyle(closed, this.theme);
|
|
2364
|
-
closed.appendChild(
|
|
2640
|
+
closed.appendChild(
|
|
2641
|
+
el("p", { className: "fillo-closed", text: "This form is no longer accepting responses." })
|
|
2642
|
+
);
|
|
2365
2643
|
this.element.appendChild(closed);
|
|
2366
2644
|
return;
|
|
2367
2645
|
}
|
|
2368
2646
|
if (this.notOpenVariant && this.schema) {
|
|
2369
2647
|
this.element.appendChild(this.renderNotOpen(this.notOpenVariant));
|
|
2370
|
-
if (this.brandingVisible())
|
|
2648
|
+
if (this.brandingVisible())
|
|
2649
|
+
this.element.appendChild(poweredByEl(this.fetchedBrandLabel, this.fetchedBrandHref));
|
|
2371
2650
|
return;
|
|
2372
2651
|
}
|
|
2373
2652
|
if (!this.schema) return;
|
|
@@ -2376,11 +2655,15 @@ var DomFormController = class {
|
|
|
2376
2655
|
const state = this.engine.getState();
|
|
2377
2656
|
const success = this.options.renderSuccess?.(api) ?? defaultSuccess(this.schema);
|
|
2378
2657
|
success.setAttribute("data-fillo", success.getAttribute("data-fillo") ?? "root");
|
|
2658
|
+
if (this.formId && !success.hasAttribute("data-fillo-form-id")) {
|
|
2659
|
+
success.setAttribute("data-fillo-form-id", this.formId);
|
|
2660
|
+
}
|
|
2379
2661
|
success.setAttribute("data-state", success.getAttribute("data-state") ?? "submitted");
|
|
2380
2662
|
if (!success.hasAttribute("tabindex")) success.setAttribute("tabindex", "-1");
|
|
2381
2663
|
themeStyle(success, this.theme);
|
|
2382
2664
|
this.element.appendChild(success);
|
|
2383
|
-
if (this.brandingVisible())
|
|
2665
|
+
if (this.brandingVisible())
|
|
2666
|
+
this.element.appendChild(poweredByEl(this.fetchedBrandLabel, this.fetchedBrandHref));
|
|
2384
2667
|
if (!state.restoredSubmission && !this.liveSubmissionHandled) {
|
|
2385
2668
|
this.liveSubmissionHandled = true;
|
|
2386
2669
|
const redirect = safeHttpUrl(this.schema.settings.redirectUrl);
|
|
@@ -2402,12 +2685,13 @@ var DomFormController = class {
|
|
|
2402
2685
|
staged: this.stagedNotice,
|
|
2403
2686
|
draft: this.draftNotice,
|
|
2404
2687
|
warningUrl: this.syncWarningUrl,
|
|
2405
|
-
noClient: !this.options.client
|
|
2688
|
+
noClient: !this.options.client && !this.options.renderOnly
|
|
2406
2689
|
})
|
|
2407
2690
|
);
|
|
2408
2691
|
}
|
|
2409
2692
|
this.element.appendChild(this.renderForm());
|
|
2410
|
-
if (this.brandingVisible())
|
|
2693
|
+
if (this.brandingVisible())
|
|
2694
|
+
this.element.appendChild(poweredByEl(this.fetchedBrandLabel, this.fetchedBrandHref));
|
|
2411
2695
|
this.applyFocus(intent, captured);
|
|
2412
2696
|
}
|
|
2413
2697
|
/** Server-driven and not client-removable; defaults on until the server says. */
|
|
@@ -2465,6 +2749,7 @@ var DomFormController = class {
|
|
|
2465
2749
|
"data-page": String(api.pageIndex + 1)
|
|
2466
2750
|
}
|
|
2467
2751
|
});
|
|
2752
|
+
if (!displayOnly && this.formId) form.setAttribute("data-fillo-form-id", this.formId);
|
|
2468
2753
|
form.toggleAttribute("data-last-page", api.isLastPage);
|
|
2469
2754
|
themeStyle(form, this.theme);
|
|
2470
2755
|
const advance = () => {
|
|
@@ -2532,7 +2817,11 @@ var DomFormController = class {
|
|
|
2532
2817
|
const header = el("header", { className: "fillo-header", attrs: { "data-fillo": "header" } });
|
|
2533
2818
|
if (api.form.title) {
|
|
2534
2819
|
header.appendChild(
|
|
2535
|
-
el("h1", {
|
|
2820
|
+
el("h1", {
|
|
2821
|
+
className: "fillo-title",
|
|
2822
|
+
text: api.form.title,
|
|
2823
|
+
attrs: { "data-fillo": "title" }
|
|
2824
|
+
})
|
|
2536
2825
|
);
|
|
2537
2826
|
}
|
|
2538
2827
|
if (api.form.description) {
|
|
@@ -2554,8 +2843,6 @@ var DomFormController = class {
|
|
|
2554
2843
|
})
|
|
2555
2844
|
);
|
|
2556
2845
|
}
|
|
2557
|
-
const summary = this.renderErrorSummary(api);
|
|
2558
|
-
if (summary) content.appendChild(summary);
|
|
2559
2846
|
const blocks = el("div", { className: "fillo-blocks", attrs: { "data-fillo": "blocks" } });
|
|
2560
2847
|
for (const block of api.blocks) {
|
|
2561
2848
|
const rendered = this.renderBlock(pipeBlock(block, this.mutableData, api.form), api);
|
|
@@ -2563,7 +2850,16 @@ var DomFormController = class {
|
|
|
2563
2850
|
}
|
|
2564
2851
|
content.appendChild(blocks);
|
|
2565
2852
|
if (!displayOnly) {
|
|
2566
|
-
const hp = el("input", {
|
|
2853
|
+
const hp = el("input", {
|
|
2854
|
+
className: "fillo-hp",
|
|
2855
|
+
attrs: {
|
|
2856
|
+
type: "text",
|
|
2857
|
+
name: "fillo_hp_field",
|
|
2858
|
+
tabindex: "-1",
|
|
2859
|
+
autocomplete: "off",
|
|
2860
|
+
"aria-hidden": "true"
|
|
2861
|
+
}
|
|
2862
|
+
});
|
|
2567
2863
|
hp.style.position = "absolute";
|
|
2568
2864
|
hp.style.left = "-9999px";
|
|
2569
2865
|
hp.style.top = "auto";
|
|
@@ -2599,12 +2895,19 @@ var DomFormController = class {
|
|
|
2599
2895
|
}
|
|
2600
2896
|
const submitError = api.status === "error" ? "This form can't submit right now. Please try again in a moment." : this.engine?.getState().submitError;
|
|
2601
2897
|
if (submitError) {
|
|
2602
|
-
const alert = el("p", {
|
|
2898
|
+
const alert = el("p", {
|
|
2899
|
+
className: "fillo-submit-error",
|
|
2900
|
+
attrs: { role: "alert" },
|
|
2901
|
+
text: submitError
|
|
2902
|
+
});
|
|
2603
2903
|
const fixUrl = this.devChrome() && this.resolutionFailureCode === "form_not_published" && this.syncWarningCode === "storage_required" ? safeHttpUrl(this.syncWarningUrl) : null;
|
|
2604
2904
|
if (fixUrl) {
|
|
2605
2905
|
alert.append(" Connect storage: ");
|
|
2606
2906
|
alert.appendChild(
|
|
2607
|
-
el("a", {
|
|
2907
|
+
el("a", {
|
|
2908
|
+
text: fixUrl,
|
|
2909
|
+
attrs: { href: fixUrl, target: "_blank", rel: "noopener noreferrer" }
|
|
2910
|
+
})
|
|
2608
2911
|
);
|
|
2609
2912
|
}
|
|
2610
2913
|
content.appendChild(alert);
|
|
@@ -2647,42 +2950,6 @@ var DomFormController = class {
|
|
|
2647
2950
|
}
|
|
2648
2951
|
return form;
|
|
2649
2952
|
}
|
|
2650
|
-
/** GOV.UK error summary (audit P2.8): scoped to the CURRENTLY RENDERED
|
|
2651
|
-
* page — a failed submit already jumps to the first page with a visible
|
|
2652
|
-
* error, so every link resolves to something in the live tree. Per-field
|
|
2653
|
-
* messages lose their own role="alert"; this is the one alert. */
|
|
2654
|
-
renderErrorSummary(api) {
|
|
2655
|
-
const fields = api.blocks.filter(isField).filter((field) => api.errors[field.id]);
|
|
2656
|
-
if (fields.length === 0) return null;
|
|
2657
|
-
const box = el("div", {
|
|
2658
|
-
className: "fillo-error-summary",
|
|
2659
|
-
attrs: { role: "alert", tabindex: "-1", "data-fillo": "error-summary" }
|
|
2660
|
-
});
|
|
2661
|
-
box.appendChild(
|
|
2662
|
-
el("p", { className: "fillo-error-summary-title", text: DEFAULT_FIELD_STRINGS.errorSummaryTitle })
|
|
2663
|
-
);
|
|
2664
|
-
const list = el("ul");
|
|
2665
|
-
for (const field of fields) {
|
|
2666
|
-
const link = el("a", {
|
|
2667
|
-
text: `${field.label}: ${api.errors[field.id]}`,
|
|
2668
|
-
attrs: { href: `#${fieldIds(this.instanceId, field.id).inputId}` }
|
|
2669
|
-
});
|
|
2670
|
-
link.addEventListener("click", (event) => {
|
|
2671
|
-
event.preventDefault();
|
|
2672
|
-
const fieldEl = Array.from(this.element.querySelectorAll("[data-field]")).find(
|
|
2673
|
-
(node) => node.getAttribute("data-field") === field.id
|
|
2674
|
-
);
|
|
2675
|
-
if (!fieldEl) return;
|
|
2676
|
-
const invalid = fieldEl.querySelector('[aria-invalid="true"]') ?? fieldEl;
|
|
2677
|
-
focusableWithin(invalid)?.focus();
|
|
2678
|
-
});
|
|
2679
|
-
const item = el("li");
|
|
2680
|
-
item.appendChild(link);
|
|
2681
|
-
list.appendChild(item);
|
|
2682
|
-
}
|
|
2683
|
-
box.appendChild(list);
|
|
2684
|
-
return box;
|
|
2685
|
-
}
|
|
2686
2953
|
renderBlock(block, api) {
|
|
2687
2954
|
if (!isField(block)) return renderContent(block);
|
|
2688
2955
|
const setValue = (value, options) => {
|
|
@@ -2701,7 +2968,7 @@ var DomFormController = class {
|
|
|
2701
2968
|
const context = {
|
|
2702
2969
|
field: block,
|
|
2703
2970
|
value: api.data[block.id],
|
|
2704
|
-
error: api.errors[block.id],
|
|
2971
|
+
error: api.errors[block.id] === REQUIRED_FIELD_MESSAGE ? requiredFieldMessage(block, DEFAULT_FIELD_STRINGS) : api.errors[block.id],
|
|
2705
2972
|
ids: fieldIds(this.instanceId, block.id),
|
|
2706
2973
|
api,
|
|
2707
2974
|
setValue,
|
|
@@ -2712,7 +2979,7 @@ var DomFormController = class {
|
|
|
2712
2979
|
getValue: () => this.engine?.getState().data[block.id],
|
|
2713
2980
|
registerOverlay: this.registerOverlay,
|
|
2714
2981
|
uploadNotice: this.uploadNotices.get(block.id),
|
|
2715
|
-
|
|
2982
|
+
uploadDisabledReason: this.options.renderOnly ? "render_only" : this.storageBlocked() ? "storage_unavailable" : void 0,
|
|
2716
2983
|
storageFixUrl: this.storageBlocked() && this.devChrome() ? safeHttpUrl(this.syncWarningUrl) : null,
|
|
2717
2984
|
announce: this.announce,
|
|
2718
2985
|
announceAlert: this.announceAlert
|
|
@@ -2738,7 +3005,10 @@ function defaultSuccess(schema) {
|
|
|
2738
3005
|
appendChildren(inner, [
|
|
2739
3006
|
el("div", { className: "fillo-success-mark", attrs: { "aria-hidden": "true" } }),
|
|
2740
3007
|
el("h2", { className: "fillo-success-title", text: schema.settings.successTitle ?? "Thanks!" }),
|
|
2741
|
-
el("p", {
|
|
3008
|
+
el("p", {
|
|
3009
|
+
className: "fillo-success-message",
|
|
3010
|
+
text: schema.settings.successMessage ?? "Your response has been recorded."
|
|
3011
|
+
})
|
|
2742
3012
|
]);
|
|
2743
3013
|
success.appendChild(inner);
|
|
2744
3014
|
return success;
|
|
@@ -2786,7 +3056,10 @@ function devDraftEl(warningUrl) {
|
|
|
2786
3056
|
if (safeUrl) {
|
|
2787
3057
|
node.append("Connect storage to publish: ");
|
|
2788
3058
|
node.appendChild(
|
|
2789
|
-
el("a", {
|
|
3059
|
+
el("a", {
|
|
3060
|
+
text: safeUrl,
|
|
3061
|
+
attrs: { href: safeUrl, target: "_blank", rel: "noopener noreferrer" }
|
|
3062
|
+
})
|
|
2790
3063
|
);
|
|
2791
3064
|
node.append(". ");
|
|
2792
3065
|
}
|
|
@@ -2824,6 +3097,14 @@ function loadFailureMessage(error) {
|
|
|
2824
3097
|
if (error.status === 422) return DEFAULT_STRINGS.renderFailed;
|
|
2825
3098
|
return DEFAULT_STRINGS.loadFailed;
|
|
2826
3099
|
}
|
|
3100
|
+
function formTargetRequiredError() {
|
|
3101
|
+
return new FilloError(
|
|
3102
|
+
"This form has no Fillo target. Pass formId with the schema, use a defineForm() value with client, or set renderOnly for a non-submitting preview.",
|
|
3103
|
+
422,
|
|
3104
|
+
void 0,
|
|
3105
|
+
"form_target_required"
|
|
3106
|
+
);
|
|
3107
|
+
}
|
|
2827
3108
|
function toFilloError(error) {
|
|
2828
3109
|
if (isFilloError(error)) return error;
|
|
2829
3110
|
return new FilloError(error instanceof Error ? error.message : String(error), 0);
|
|
@@ -2831,7 +3112,11 @@ function toFilloError(error) {
|
|
|
2831
3112
|
function renderForm(target, options) {
|
|
2832
3113
|
const element = typeof target === "string" ? document.querySelector(target) : target;
|
|
2833
3114
|
if (!element) throw new FilloError(`Fillo mount target not found: ${String(target)}`, 0);
|
|
2834
|
-
|
|
3115
|
+
const internalOptions = {
|
|
3116
|
+
...options,
|
|
3117
|
+
initialData: options.initialData ?? {}
|
|
3118
|
+
};
|
|
3119
|
+
return new DomFormController(element, internalOptions);
|
|
2835
3120
|
}
|
|
2836
3121
|
function createFormElement(options) {
|
|
2837
3122
|
const host = el("div");
|
|
@@ -2849,7 +3134,7 @@ var FilloFormElement = class extends HTMLElementBase {
|
|
|
2849
3134
|
mounted = false;
|
|
2850
3135
|
mountScheduled = false;
|
|
2851
3136
|
static get observedAttributes() {
|
|
2852
|
-
return ["publishable-key", "form-id", "data-preview"];
|
|
3137
|
+
return ["publishable-key", "form-id", "data-preview", "data-render-only"];
|
|
2853
3138
|
}
|
|
2854
3139
|
connectedCallback() {
|
|
2855
3140
|
this.scheduleMount();
|
|
@@ -2906,6 +3191,11 @@ var FilloFormElement = class extends HTMLElementBase {
|
|
|
2906
3191
|
const value = this.getAttribute("data-preview");
|
|
2907
3192
|
return value !== null && value !== "false" && value !== "0";
|
|
2908
3193
|
}
|
|
3194
|
+
/** Explicit local-preview mode for custom-element integrations. */
|
|
3195
|
+
renderOnlyAttrEnabled() {
|
|
3196
|
+
const value = this.getAttribute("data-render-only");
|
|
3197
|
+
return value !== null && value !== "false" && value !== "0";
|
|
3198
|
+
}
|
|
2909
3199
|
mount() {
|
|
2910
3200
|
this.instance?.destroy();
|
|
2911
3201
|
const key = this.getAttribute("publishable-key") ?? void 0;
|
|
@@ -2923,6 +3213,7 @@ var FilloFormElement = class extends HTMLElementBase {
|
|
|
2923
3213
|
// Value-aware: frameworks stringify booleans onto data-* attributes, so
|
|
2924
3214
|
// data-preview="false"/"0" must mean OFF, not presence-implies-on.
|
|
2925
3215
|
preview: this.previewAttrEnabled() ? true : void 0,
|
|
3216
|
+
renderOnly: this.renderOnlyAttrEnabled() ? true : void 0,
|
|
2926
3217
|
onChange: (data) => this.dispatchEvent(new CustomEvent("fillo-change", { detail: { data } })),
|
|
2927
3218
|
onSubmitted: (responseId, data) => this.dispatchEvent(new CustomEvent("fillo-submit", { detail: { responseId, data } })),
|
|
2928
3219
|
onError: (error) => this.dispatchEvent(new CustomEvent("fillo-error", { detail: { error } }))
|