@usefillo/dom 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +29 -4
- package/dist/index.js +384 -125
- package/dist/standalone.global.js +1 -1
- package/dist/styles.css +53 -17
- package/dist/styles.unlayered.css +53 -17
- 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,
|
|
@@ -93,11 +94,11 @@ function badgeUrl(href) {
|
|
|
93
94
|
function inputValue(value) {
|
|
94
95
|
return typeof value === "string" || typeof value === "number" ? String(value) : "";
|
|
95
96
|
}
|
|
96
|
-
function groupedInputValue(value, decimals) {
|
|
97
|
+
function groupedInputValue(value, decimals, locale) {
|
|
97
98
|
const raw = inputValue(value);
|
|
98
99
|
if (raw === "") return raw;
|
|
99
100
|
const n = Number(raw);
|
|
100
|
-
return Number.isFinite(n) ? formatGroupedNumber(n, { decimals }) : raw;
|
|
101
|
+
return Number.isFinite(n) ? formatGroupedNumber(n, { locale, decimals }) : raw;
|
|
101
102
|
}
|
|
102
103
|
function appendChildren(parent, children) {
|
|
103
104
|
for (const child of children) {
|
|
@@ -126,13 +127,11 @@ function optionIcon(icon) {
|
|
|
126
127
|
svg.appendChild(path);
|
|
127
128
|
return svg;
|
|
128
129
|
}
|
|
129
|
-
function applyFieldAria(node, field, error, ids) {
|
|
130
|
+
function applyFieldAria(node, field, error, ids, opts = {}) {
|
|
130
131
|
if (error) node.setAttribute("aria-invalid", "true");
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
error ? ids.errorId : null
|
|
135
|
-
].filter(Boolean).join(" ");
|
|
132
|
+
if ((opts.requiredSupported ?? true) && field.required)
|
|
133
|
+
node.setAttribute("aria-required", "true");
|
|
134
|
+
const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ");
|
|
136
135
|
if (describedBy) node.setAttribute("aria-describedby", describedBy);
|
|
137
136
|
}
|
|
138
137
|
function bindRadioKeys(group, values, current, select) {
|
|
@@ -185,7 +184,8 @@ function shell(field, error, child, ids) {
|
|
|
185
184
|
function textInput(type, context) {
|
|
186
185
|
const field = context.field;
|
|
187
186
|
const numberField = field.kind === "number" ? field : void 0;
|
|
188
|
-
const grouped = numberField?.notation
|
|
187
|
+
const grouped = numberField?.notation !== void 0;
|
|
188
|
+
const locale = localeForNotation(numberField?.notation);
|
|
189
189
|
const formatted = grouped || Boolean(numberField?.prefix) || Boolean(numberField?.suffix);
|
|
190
190
|
const input = el("input", {
|
|
191
191
|
className: "fillo-input",
|
|
@@ -194,7 +194,7 @@ function textInput(type, context) {
|
|
|
194
194
|
if (formatted) input.setAttribute("inputmode", "decimal");
|
|
195
195
|
if (type === "email") input.setAttribute("autocomplete", "email");
|
|
196
196
|
else if (type === "url") input.setAttribute("autocomplete", "url");
|
|
197
|
-
input.value = grouped ? groupedInputValue(context.value, numberField?.decimals) : inputValue(context.value);
|
|
197
|
+
input.value = grouped ? groupedInputValue(context.value, numberField?.decimals, locale) : inputValue(context.value);
|
|
198
198
|
if (field.placeholder) input.placeholder = field.placeholder;
|
|
199
199
|
if ("maxLength" in field && field.maxLength !== void 0) {
|
|
200
200
|
input.maxLength = field.maxLength;
|
|
@@ -212,31 +212,31 @@ function textInput(type, context) {
|
|
|
212
212
|
};
|
|
213
213
|
if (grouped) {
|
|
214
214
|
input.addEventListener("input", () => {
|
|
215
|
-
if (!isValidPartialNumberText(input.value)) return rejectEdit();
|
|
215
|
+
if (!isValidPartialNumberText(input.value, locale)) return rejectEdit();
|
|
216
216
|
lastValid = input.value;
|
|
217
217
|
lastCaret = input.selectionStart ?? input.value.length;
|
|
218
|
-
context.setValue(parseGroupedNumber(input.value), { render: false });
|
|
218
|
+
context.setValue(parseGroupedNumber(input.value, locale), { render: false });
|
|
219
219
|
});
|
|
220
220
|
input.addEventListener("change", () => {
|
|
221
|
-
if (!isValidPartialNumberText(input.value)) return rejectEdit();
|
|
221
|
+
if (!isValidPartialNumberText(input.value, locale)) return rejectEdit();
|
|
222
222
|
lastValid = input.value;
|
|
223
|
-
context.setValue(parseGroupedNumber(input.value));
|
|
223
|
+
context.setValue(parseGroupedNumber(input.value, locale));
|
|
224
224
|
});
|
|
225
225
|
input.addEventListener("blur", () => {
|
|
226
226
|
const live = context.getValue?.() ?? context.value;
|
|
227
|
-
input.value = groupedInputValue(live, numberField?.decimals);
|
|
227
|
+
input.value = groupedInputValue(live, numberField?.decimals, locale);
|
|
228
228
|
lastValid = input.value;
|
|
229
229
|
lastCaret = input.value.length;
|
|
230
230
|
});
|
|
231
231
|
} else {
|
|
232
232
|
input.addEventListener("input", () => {
|
|
233
|
-
if (!isValidPartialNumberText(input.value)) return rejectEdit();
|
|
233
|
+
if (!isValidPartialNumberText(input.value, locale)) return rejectEdit();
|
|
234
234
|
lastValid = input.value;
|
|
235
235
|
lastCaret = input.selectionStart ?? input.value.length;
|
|
236
236
|
context.setValue(input.value, { render: false });
|
|
237
237
|
});
|
|
238
238
|
input.addEventListener("change", () => {
|
|
239
|
-
if (!isValidPartialNumberText(input.value)) return rejectEdit();
|
|
239
|
+
if (!isValidPartialNumberText(input.value, locale)) return rejectEdit();
|
|
240
240
|
lastValid = input.value;
|
|
241
241
|
context.setValue(input.value);
|
|
242
242
|
});
|
|
@@ -293,7 +293,10 @@ function phoneInput(context) {
|
|
|
293
293
|
className: "fillo-phone-flag",
|
|
294
294
|
attrs: { type: "button", "aria-haspopup": "listbox", "aria-expanded": "false" }
|
|
295
295
|
});
|
|
296
|
-
const flagSpan = el("span", {
|
|
296
|
+
const flagSpan = el("span", {
|
|
297
|
+
className: "fillo-phone-flag-emoji",
|
|
298
|
+
attrs: { "aria-hidden": "true" }
|
|
299
|
+
});
|
|
297
300
|
const dialSpan = el("span", { className: "fillo-phone-dial" });
|
|
298
301
|
appendChildren(flagBtn, [
|
|
299
302
|
flagSpan,
|
|
@@ -449,10 +452,18 @@ function phoneInput(context) {
|
|
|
449
452
|
matches.forEach((c, i) => {
|
|
450
453
|
const li = el("li", {
|
|
451
454
|
className: `fillo-phone-option${i === active ? " fillo-phone-option--active" : ""}`,
|
|
452
|
-
attrs: {
|
|
455
|
+
attrs: {
|
|
456
|
+
id: `${listId}-opt-${i}`,
|
|
457
|
+
role: "option",
|
|
458
|
+
"aria-selected": String(c.iso2 === country.iso2)
|
|
459
|
+
}
|
|
453
460
|
});
|
|
454
461
|
appendChildren(li, [
|
|
455
|
-
el("span", {
|
|
462
|
+
el("span", {
|
|
463
|
+
className: "fillo-phone-flag-emoji",
|
|
464
|
+
text: flagEmoji(c.iso2),
|
|
465
|
+
attrs: { "aria-hidden": "true" }
|
|
466
|
+
}),
|
|
456
467
|
el("span", { className: "fillo-phone-option-name", text: c.name }),
|
|
457
468
|
el("span", { className: "fillo-phone-option-dial", text: `+${c.dialCode}` })
|
|
458
469
|
]);
|
|
@@ -587,7 +598,10 @@ function singleChoice(context) {
|
|
|
587
598
|
attrs: { "data-option": option.id, "data-fillo": "option" }
|
|
588
599
|
});
|
|
589
600
|
label.toggleAttribute("data-selected", value === option.id);
|
|
590
|
-
const input = el("input", {
|
|
601
|
+
const input = el("input", {
|
|
602
|
+
className: "fillo-option-input",
|
|
603
|
+
attrs: { type: "radio", name: context.ids.name }
|
|
604
|
+
});
|
|
591
605
|
input.checked = value === option.id;
|
|
592
606
|
input.addEventListener("change", () => {
|
|
593
607
|
activeOtherFields?.delete(field.id);
|
|
@@ -596,12 +610,20 @@ function singleChoice(context) {
|
|
|
596
610
|
appendChildren(label, [
|
|
597
611
|
input,
|
|
598
612
|
optionIcon(option.icon),
|
|
599
|
-
el("span", {
|
|
613
|
+
el("span", {
|
|
614
|
+
className: "fillo-option-label",
|
|
615
|
+
text: option.label,
|
|
616
|
+
attrs: { "data-fillo": "optionLabel" }
|
|
617
|
+
}),
|
|
600
618
|
// Icon-mode hides the native input visually (CSS), so an SVG recolor
|
|
601
619
|
// was the only selected-state signal — add a decorative shape signal
|
|
602
620
|
// that survives forced-colors (audit P1.2; state still lives on the
|
|
603
621
|
// input, this marker is non-authoritative).
|
|
604
|
-
option.icon && value === option.id ? el("span", {
|
|
622
|
+
option.icon && value === option.id ? el("span", {
|
|
623
|
+
className: "fillo-option-check",
|
|
624
|
+
text: "\u2713",
|
|
625
|
+
attrs: { "aria-hidden": "true" }
|
|
626
|
+
}) : null
|
|
605
627
|
]);
|
|
606
628
|
wrap.appendChild(label);
|
|
607
629
|
}
|
|
@@ -611,7 +633,10 @@ function singleChoice(context) {
|
|
|
611
633
|
attrs: { "data-fillo": "option" }
|
|
612
634
|
});
|
|
613
635
|
label.toggleAttribute("data-selected", otherActive);
|
|
614
|
-
const input = el("input", {
|
|
636
|
+
const input = el("input", {
|
|
637
|
+
className: "fillo-option-input",
|
|
638
|
+
attrs: { type: "radio", name: context.ids.name }
|
|
639
|
+
});
|
|
615
640
|
input.checked = otherActive;
|
|
616
641
|
input.addEventListener("change", () => {
|
|
617
642
|
activeOtherFields?.add(field.id);
|
|
@@ -620,7 +645,11 @@ function singleChoice(context) {
|
|
|
620
645
|
appendChildren(label, [
|
|
621
646
|
appendChildren(el("span", { className: "fillo-option-main" }), [
|
|
622
647
|
input,
|
|
623
|
-
el("span", {
|
|
648
|
+
el("span", {
|
|
649
|
+
className: "fillo-option-label",
|
|
650
|
+
text: DEFAULT_STRINGS.other,
|
|
651
|
+
attrs: { "data-fillo": "optionLabel" }
|
|
652
|
+
})
|
|
624
653
|
])
|
|
625
654
|
]);
|
|
626
655
|
if (otherActive) {
|
|
@@ -660,7 +689,7 @@ function multiChoice(context) {
|
|
|
660
689
|
className: "fillo-options",
|
|
661
690
|
attrs: { role: "group", "aria-labelledby": context.ids.labelId, "data-fillo": "options" }
|
|
662
691
|
});
|
|
663
|
-
applyFieldAria(wrap, field, context.error, context.ids);
|
|
692
|
+
applyFieldAria(wrap, field, context.error, context.ids, { requiredSupported: false });
|
|
664
693
|
const setSelected = (next) => context.setValue(next);
|
|
665
694
|
for (const option of options) {
|
|
666
695
|
const label = el("label", {
|
|
@@ -672,13 +701,23 @@ function multiChoice(context) {
|
|
|
672
701
|
input.checked = selected.includes(option.id);
|
|
673
702
|
input.addEventListener("change", () => {
|
|
674
703
|
const current = getSelected();
|
|
675
|
-
setSelected(
|
|
704
|
+
setSelected(
|
|
705
|
+
input.checked ? [...current, option.id] : current.filter((id) => id !== option.id)
|
|
706
|
+
);
|
|
676
707
|
});
|
|
677
708
|
appendChildren(label, [
|
|
678
709
|
input,
|
|
679
710
|
optionIcon(option.icon),
|
|
680
|
-
el("span", {
|
|
681
|
-
|
|
711
|
+
el("span", {
|
|
712
|
+
className: "fillo-option-label",
|
|
713
|
+
text: option.label,
|
|
714
|
+
attrs: { "data-fillo": "optionLabel" }
|
|
715
|
+
}),
|
|
716
|
+
option.icon && selected.includes(option.id) ? el("span", {
|
|
717
|
+
className: "fillo-option-check",
|
|
718
|
+
text: "\u2713",
|
|
719
|
+
attrs: { "aria-hidden": "true" }
|
|
720
|
+
}) : null
|
|
682
721
|
]);
|
|
683
722
|
wrap.appendChild(label);
|
|
684
723
|
}
|
|
@@ -704,7 +743,11 @@ function multiChoice(context) {
|
|
|
704
743
|
appendChildren(label, [
|
|
705
744
|
appendChildren(el("span", { className: "fillo-option-main" }), [
|
|
706
745
|
input,
|
|
707
|
-
el("span", {
|
|
746
|
+
el("span", {
|
|
747
|
+
className: "fillo-option-label",
|
|
748
|
+
text: DEFAULT_STRINGS.other,
|
|
749
|
+
attrs: { "data-fillo": "optionLabel" }
|
|
750
|
+
})
|
|
708
751
|
])
|
|
709
752
|
]);
|
|
710
753
|
if (active) {
|
|
@@ -750,11 +793,16 @@ function dropdown(context) {
|
|
|
750
793
|
className: "fillo-input fillo-select",
|
|
751
794
|
attrs: { id: context.ids.inputId, "data-fillo": "control" }
|
|
752
795
|
});
|
|
753
|
-
select.appendChild(
|
|
796
|
+
select.appendChild(
|
|
797
|
+
el("option", { text: field.placeholder ?? "Choose...", attrs: { value: "" } })
|
|
798
|
+
);
|
|
754
799
|
for (const option of displayOptions(field)) {
|
|
755
800
|
select.appendChild(el("option", { text: option.label, attrs: { value: option.id } }));
|
|
756
801
|
}
|
|
757
|
-
if (field.allowOther)
|
|
802
|
+
if (field.allowOther)
|
|
803
|
+
select.appendChild(
|
|
804
|
+
el("option", { text: `${DEFAULT_STRINGS.other}\u2026`, attrs: { value: other } })
|
|
805
|
+
);
|
|
758
806
|
select.value = otherActive ? other : value;
|
|
759
807
|
select.addEventListener("change", () => {
|
|
760
808
|
if (select.value === other) {
|
|
@@ -766,7 +814,10 @@ function dropdown(context) {
|
|
|
766
814
|
}
|
|
767
815
|
});
|
|
768
816
|
applyFieldAria(select, field, context.error, context.ids);
|
|
769
|
-
appendChildren(selectWrap, [
|
|
817
|
+
appendChildren(selectWrap, [
|
|
818
|
+
select,
|
|
819
|
+
el("span", { className: "fillo-select-icon", attrs: { "aria-hidden": "true" } })
|
|
820
|
+
]);
|
|
770
821
|
wrap.appendChild(selectWrap);
|
|
771
822
|
if (otherActive) {
|
|
772
823
|
const other2 = el("input", {
|
|
@@ -826,9 +877,10 @@ function checkbox(context) {
|
|
|
826
877
|
appendChildren(label2, [
|
|
827
878
|
copy,
|
|
828
879
|
input2,
|
|
829
|
-
appendChildren(
|
|
830
|
-
el("span", { className: "fillo-toggle-
|
|
831
|
-
|
|
880
|
+
appendChildren(
|
|
881
|
+
el("span", { className: "fillo-toggle-track", attrs: { "aria-hidden": "true" } }),
|
|
882
|
+
[el("span", { className: "fillo-toggle-thumb" })]
|
|
883
|
+
)
|
|
832
884
|
]);
|
|
833
885
|
wrap2.appendChild(label2);
|
|
834
886
|
if (context.field.description)
|
|
@@ -1013,7 +1065,7 @@ function ranking(context) {
|
|
|
1013
1065
|
"data-fillo": "control"
|
|
1014
1066
|
}
|
|
1015
1067
|
});
|
|
1016
|
-
applyFieldAria(group, context.field, context.error, context.ids);
|
|
1068
|
+
applyFieldAria(group, context.field, context.error, context.ids, { requiredSupported: false });
|
|
1017
1069
|
const list = el("ol", { className: "fillo-ranking" });
|
|
1018
1070
|
for (const [index, id] of order.entries()) {
|
|
1019
1071
|
const option = field.options.find((item2) => item2.id === id);
|
|
@@ -1072,8 +1124,9 @@ function matrix(context) {
|
|
|
1072
1124
|
const table = el("table", { className: "fillo-matrix" });
|
|
1073
1125
|
const thead = document.createElement("thead");
|
|
1074
1126
|
const headRow = document.createElement("tr");
|
|
1075
|
-
headRow.appendChild(document.createElement("
|
|
1076
|
-
for (const col of field.columns)
|
|
1127
|
+
headRow.appendChild(document.createElement("td"));
|
|
1128
|
+
for (const col of field.columns)
|
|
1129
|
+
headRow.appendChild(el("th", { text: col.label, attrs: { scope: "col" } }));
|
|
1077
1130
|
thead.appendChild(headRow);
|
|
1078
1131
|
table.appendChild(thead);
|
|
1079
1132
|
const tbody = document.createElement("tbody");
|
|
@@ -1126,7 +1179,7 @@ function matrix(context) {
|
|
|
1126
1179
|
"data-fillo": "control"
|
|
1127
1180
|
}
|
|
1128
1181
|
});
|
|
1129
|
-
applyFieldAria(group, context.field, context.error, context.ids);
|
|
1182
|
+
applyFieldAria(group, context.field, context.error, context.ids, { requiredSupported: false });
|
|
1130
1183
|
group.appendChild(table);
|
|
1131
1184
|
return shell(context.field, context.error, group, context.ids);
|
|
1132
1185
|
}
|
|
@@ -1142,7 +1195,11 @@ function signature(context) {
|
|
|
1142
1195
|
text: "Clear",
|
|
1143
1196
|
attrs: { type: "button" }
|
|
1144
1197
|
});
|
|
1145
|
-
const hint = el("div", {
|
|
1198
|
+
const hint = el("div", {
|
|
1199
|
+
className: "fillo-signature-hint",
|
|
1200
|
+
text: "Sign here",
|
|
1201
|
+
attrs: { "aria-hidden": "true" }
|
|
1202
|
+
});
|
|
1146
1203
|
canvas.width = 720;
|
|
1147
1204
|
canvas.height = 220;
|
|
1148
1205
|
const ctx = canvas.getContext("2d");
|
|
@@ -1258,29 +1315,87 @@ function fileUpload(context) {
|
|
|
1258
1315
|
const field = context.field;
|
|
1259
1316
|
if (field.kind !== "file_upload") return null;
|
|
1260
1317
|
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
|
-
});
|
|
1318
|
+
const uploadDisabledReason = internal.uploadDisabledReason;
|
|
1319
|
+
const canUpload = uploadDisabledReason === void 0;
|
|
1320
|
+
const maxFiles = field.maxFiles ?? 1;
|
|
1321
|
+
const maxMb = field.maxFileSizeMb ?? 500;
|
|
1322
|
+
const done = Array.isArray(context.value) ? context.value : [];
|
|
1323
|
+
const rows = internal.uploadRows ?? [];
|
|
1324
|
+
const remaining = maxFiles - done.length - rows.filter((row) => row.error === void 0).length;
|
|
1276
1325
|
const wrap = el("div");
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1326
|
+
if (remaining > 0) {
|
|
1327
|
+
const input = el("input", {
|
|
1328
|
+
className: "fillo-input",
|
|
1329
|
+
attrs: { id: context.ids.inputId, type: "file", hidden: "" }
|
|
1330
|
+
});
|
|
1331
|
+
if (maxFiles > 1) input.multiple = true;
|
|
1332
|
+
if (field.accept?.length) input.accept = field.accept.join(",");
|
|
1333
|
+
input.disabled = !canUpload;
|
|
1334
|
+
input.addEventListener("change", () => {
|
|
1335
|
+
if (!canUpload) return;
|
|
1336
|
+
const files = Array.from(input.files ?? []);
|
|
1337
|
+
input.value = "";
|
|
1338
|
+
void context.uploadFiles?.(field, files);
|
|
1339
|
+
});
|
|
1340
|
+
const dropzone = el("div", {
|
|
1341
|
+
className: `fillo-dropzone${canUpload ? "" : " fillo-dropzone--disabled"}`,
|
|
1342
|
+
attrs: {
|
|
1343
|
+
role: "button",
|
|
1344
|
+
tabindex: "0",
|
|
1345
|
+
"aria-labelledby": context.ids.labelId,
|
|
1346
|
+
"data-fillo": "control"
|
|
1347
|
+
}
|
|
1348
|
+
});
|
|
1349
|
+
applyFieldAria(dropzone, field, context.error, context.ids, { requiredSupported: false });
|
|
1350
|
+
if (!canUpload) dropzone.setAttribute("aria-disabled", "true");
|
|
1351
|
+
dropzone.addEventListener("dragover", (e) => {
|
|
1352
|
+
e.preventDefault();
|
|
1353
|
+
dropzone.classList.add("fillo-dropzone--over");
|
|
1354
|
+
dropzone.setAttribute("data-drag-over", "");
|
|
1355
|
+
});
|
|
1356
|
+
dropzone.addEventListener("dragleave", () => {
|
|
1357
|
+
dropzone.classList.remove("fillo-dropzone--over");
|
|
1358
|
+
dropzone.removeAttribute("data-drag-over");
|
|
1359
|
+
});
|
|
1360
|
+
dropzone.addEventListener("drop", (e) => {
|
|
1361
|
+
e.preventDefault();
|
|
1362
|
+
dropzone.classList.remove("fillo-dropzone--over");
|
|
1363
|
+
dropzone.removeAttribute("data-drag-over");
|
|
1364
|
+
if (canUpload) void context.uploadFiles?.(field, Array.from(e.dataTransfer?.files ?? []));
|
|
1282
1365
|
});
|
|
1283
|
-
|
|
1366
|
+
dropzone.addEventListener("click", () => {
|
|
1367
|
+
if (canUpload) input.click();
|
|
1368
|
+
});
|
|
1369
|
+
dropzone.addEventListener("keydown", (e) => {
|
|
1370
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
1371
|
+
e.preventDefault();
|
|
1372
|
+
if (canUpload) input.click();
|
|
1373
|
+
}
|
|
1374
|
+
});
|
|
1375
|
+
dropzone.appendChild(input);
|
|
1376
|
+
if (canUpload) {
|
|
1377
|
+
appendChildren(dropzone, [
|
|
1378
|
+
el("span", {
|
|
1379
|
+
className: "fillo-dropzone-title",
|
|
1380
|
+
text: DEFAULT_FIELD_STRINGS.dropzoneTitle(maxFiles > 1)
|
|
1381
|
+
}),
|
|
1382
|
+
el("span", {
|
|
1383
|
+
className: "fillo-dropzone-hint",
|
|
1384
|
+
text: DEFAULT_FIELD_STRINGS.dropzoneHint(maxMb)
|
|
1385
|
+
})
|
|
1386
|
+
]);
|
|
1387
|
+
} else if (uploadDisabledReason === "render_only") {
|
|
1388
|
+
dropzone.appendChild(
|
|
1389
|
+
el("span", {
|
|
1390
|
+
className: "fillo-dropzone-hint",
|
|
1391
|
+
text: DEFAULT_FIELD_STRINGS.uploadsRenderOnly
|
|
1392
|
+
})
|
|
1393
|
+
);
|
|
1394
|
+
} else if (internal.storageFixUrl) {
|
|
1395
|
+
const hint = el("span", {
|
|
1396
|
+
className: "fillo-dropzone-hint",
|
|
1397
|
+
text: "Connect file storage to enable uploads"
|
|
1398
|
+
});
|
|
1284
1399
|
hint.append(" \u2014 ");
|
|
1285
1400
|
hint.appendChild(
|
|
1286
1401
|
el("a", {
|
|
@@ -1288,13 +1403,17 @@ function fileUpload(context) {
|
|
|
1288
1403
|
attrs: { href: internal.storageFixUrl, target: "_blank", rel: "noopener noreferrer" }
|
|
1289
1404
|
})
|
|
1290
1405
|
);
|
|
1406
|
+
dropzone.appendChild(hint);
|
|
1407
|
+
} else {
|
|
1408
|
+
dropzone.appendChild(
|
|
1409
|
+
el("span", {
|
|
1410
|
+
className: "fillo-dropzone-hint",
|
|
1411
|
+
text: DEFAULT_FIELD_STRINGS.uploadsUnavailable
|
|
1412
|
+
})
|
|
1413
|
+
);
|
|
1291
1414
|
}
|
|
1292
|
-
wrap.appendChild(
|
|
1415
|
+
wrap.appendChild(dropzone);
|
|
1293
1416
|
}
|
|
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
1417
|
if (done.length > 0 || rows.length > 0) {
|
|
1299
1418
|
const list = el("ul", { className: "fillo-files" });
|
|
1300
1419
|
for (const file of done) {
|
|
@@ -1326,9 +1445,9 @@ function fileUpload(context) {
|
|
|
1326
1445
|
});
|
|
1327
1446
|
li.appendChild(el("span", { className: "fillo-file-name", text: row.name }));
|
|
1328
1447
|
if (failed) {
|
|
1329
|
-
|
|
1448
|
+
const actions = el("span", { className: "fillo-file-actions" });
|
|
1330
1449
|
if (remaining > 0) {
|
|
1331
|
-
|
|
1450
|
+
actions.appendChild(
|
|
1332
1451
|
fileActionBtn(
|
|
1333
1452
|
"fillo-file-retry",
|
|
1334
1453
|
`${DEFAULT_FIELD_STRINGS.uploadRetry} ${row.name}`,
|
|
@@ -1337,14 +1456,34 @@ function fileUpload(context) {
|
|
|
1337
1456
|
)
|
|
1338
1457
|
);
|
|
1339
1458
|
}
|
|
1340
|
-
|
|
1459
|
+
actions.appendChild(
|
|
1460
|
+
fileActionBtn(
|
|
1461
|
+
"fillo-file-remove",
|
|
1462
|
+
"Dismiss",
|
|
1463
|
+
"\xD7",
|
|
1464
|
+
() => internal.uploadAction?.(row.key, "dismiss")
|
|
1465
|
+
)
|
|
1466
|
+
);
|
|
1467
|
+
li.appendChild(actions);
|
|
1468
|
+
li.appendChild(el("span", { className: "fillo-file-error", text: row.error ?? "" }));
|
|
1341
1469
|
} else {
|
|
1342
1470
|
const bar = el("span", { className: "fillo-progress-bar" });
|
|
1343
1471
|
bar.style.width = `${row.fraction * 100}%`;
|
|
1344
1472
|
appendChildren(li, [
|
|
1345
|
-
el("span", {
|
|
1346
|
-
|
|
1347
|
-
|
|
1473
|
+
el("span", {
|
|
1474
|
+
className: "fillo-file-meta",
|
|
1475
|
+
text: `${Math.round(row.fraction * 100)}% of ${formatBytes(row.size)}`
|
|
1476
|
+
}),
|
|
1477
|
+
fileActionBtn(
|
|
1478
|
+
"fillo-file-remove",
|
|
1479
|
+
`Cancel ${row.name}`,
|
|
1480
|
+
"\xD7",
|
|
1481
|
+
() => internal.uploadAction?.(row.key, "cancel")
|
|
1482
|
+
),
|
|
1483
|
+
appendChildren(
|
|
1484
|
+
el("span", { className: "fillo-progress", attrs: { "aria-hidden": "true" } }),
|
|
1485
|
+
[bar]
|
|
1486
|
+
)
|
|
1348
1487
|
]);
|
|
1349
1488
|
}
|
|
1350
1489
|
list.appendChild(li);
|
|
@@ -1596,7 +1735,7 @@ var DomFormController = class {
|
|
|
1596
1735
|
return {
|
|
1597
1736
|
form,
|
|
1598
1737
|
formId: this.formId,
|
|
1599
|
-
client: this.options.client,
|
|
1738
|
+
client: this.options.renderOnly ? void 0 : this.options.client,
|
|
1600
1739
|
data: state?.data ?? this.mutableData,
|
|
1601
1740
|
errors: state?.errors ?? {},
|
|
1602
1741
|
status: this.status,
|
|
@@ -1640,9 +1779,14 @@ var DomFormController = class {
|
|
|
1640
1779
|
resolutionFailureCode = null;
|
|
1641
1780
|
async resolve() {
|
|
1642
1781
|
try {
|
|
1643
|
-
this.challenge = this.options.challenge;
|
|
1782
|
+
this.challenge = this.options.renderOnly ? void 0 : this.options.challenge;
|
|
1644
1783
|
const codeForm = isCodeForm(this.options.form) ? this.options.form : null;
|
|
1645
1784
|
if (codeForm) {
|
|
1785
|
+
if (this.options.renderOnly) {
|
|
1786
|
+
this.setSchema(codeForm.schema, this.options.theme ?? codeForm.theme, void 0);
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
if (!this.options.client && this.options.formId) this.options.client = createClient();
|
|
1646
1790
|
const client = this.options.client;
|
|
1647
1791
|
if (client?.key) {
|
|
1648
1792
|
this.resolveFormIdFn = async () => {
|
|
@@ -1687,12 +1831,7 @@ var DomFormController = class {
|
|
|
1687
1831
|
}
|
|
1688
1832
|
if (result.syncError) {
|
|
1689
1833
|
this.reportSyncNotice(
|
|
1690
|
-
new FilloError(
|
|
1691
|
-
result.syncError.message,
|
|
1692
|
-
403,
|
|
1693
|
-
void 0,
|
|
1694
|
-
result.syncError.code
|
|
1695
|
-
)
|
|
1834
|
+
new FilloError(result.syncError.message, 403, void 0, result.syncError.code)
|
|
1696
1835
|
);
|
|
1697
1836
|
}
|
|
1698
1837
|
this.resolutionFailureCode = null;
|
|
@@ -1705,8 +1844,16 @@ var DomFormController = class {
|
|
|
1705
1844
|
}
|
|
1706
1845
|
};
|
|
1707
1846
|
}
|
|
1708
|
-
if (!client?.key) {
|
|
1709
|
-
this.setSchema(
|
|
1847
|
+
if (!client?.key && this.options.formId) {
|
|
1848
|
+
this.setSchema(
|
|
1849
|
+
codeForm.schema,
|
|
1850
|
+
this.options.theme ?? codeForm.theme,
|
|
1851
|
+
this.options.formId
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
if (!client && !this.options.formId) {
|
|
1855
|
+
this.genericUnavailable = !this.devChrome();
|
|
1856
|
+
throw formTargetRequiredError();
|
|
1710
1857
|
}
|
|
1711
1858
|
if (client && !client.key && !this.options.formId) {
|
|
1712
1859
|
this.reportSyncFailure(
|
|
@@ -1721,7 +1868,11 @@ var DomFormController = class {
|
|
|
1721
1868
|
}
|
|
1722
1869
|
if (!client?.key) return;
|
|
1723
1870
|
if (this.devChrome()) {
|
|
1724
|
-
this.setSchema(
|
|
1871
|
+
this.setSchema(
|
|
1872
|
+
codeForm.schema,
|
|
1873
|
+
this.options.theme ?? codeForm.theme,
|
|
1874
|
+
this.options.formId
|
|
1875
|
+
);
|
|
1725
1876
|
} else {
|
|
1726
1877
|
this.theme = this.options.theme ?? codeForm.theme;
|
|
1727
1878
|
this.render();
|
|
@@ -1763,19 +1914,18 @@ var DomFormController = class {
|
|
|
1763
1914
|
if (result.warning) console.warn(`[fillo] ${result.warning}`);
|
|
1764
1915
|
if (result.syncError) {
|
|
1765
1916
|
this.reportSyncNotice(
|
|
1766
|
-
new FilloError(
|
|
1767
|
-
result.syncError.message,
|
|
1768
|
-
403,
|
|
1769
|
-
void 0,
|
|
1770
|
-
result.syncError.code
|
|
1771
|
-
)
|
|
1917
|
+
new FilloError(result.syncError.message, 403, void 0, result.syncError.code)
|
|
1772
1918
|
);
|
|
1773
1919
|
}
|
|
1774
1920
|
const connectStorage = result.warningUrl ? ` \u2014 connect storage to publish: ${result.warningUrl}` : "";
|
|
1775
1921
|
if (result.staged) {
|
|
1776
|
-
console.info(
|
|
1922
|
+
console.info(
|
|
1923
|
+
`[fillo] "${codeForm.id}" changes staged as a draft \u2014 rendering the live version until you publish them in your Fillo dashboard${connectStorage}`
|
|
1924
|
+
);
|
|
1777
1925
|
} else if (result.status === "draft") {
|
|
1778
|
-
console.info(
|
|
1926
|
+
console.info(
|
|
1927
|
+
`[fillo] "${codeForm.id}" is a draft \u2014 it renders here but won't accept responses until published${connectStorage}`
|
|
1928
|
+
);
|
|
1779
1929
|
}
|
|
1780
1930
|
} catch (error) {
|
|
1781
1931
|
if (this.destroyed) return;
|
|
@@ -1785,6 +1935,15 @@ var DomFormController = class {
|
|
|
1785
1935
|
}
|
|
1786
1936
|
const schema = this.options.form && !isCodeForm(this.options.form) ? this.options.form : null;
|
|
1787
1937
|
if (schema) {
|
|
1938
|
+
if (this.options.renderOnly) {
|
|
1939
|
+
this.setSchema(schema, this.options.theme, void 0);
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
if (!this.options.formId) {
|
|
1943
|
+
this.genericUnavailable = !this.devChrome();
|
|
1944
|
+
throw formTargetRequiredError();
|
|
1945
|
+
}
|
|
1946
|
+
this.options.client ??= createClient();
|
|
1788
1947
|
this.setSchema(schema, this.options.theme, this.options.formId);
|
|
1789
1948
|
return;
|
|
1790
1949
|
}
|
|
@@ -1813,7 +1972,11 @@ var DomFormController = class {
|
|
|
1813
1972
|
this.render(this.fatalError);
|
|
1814
1973
|
return;
|
|
1815
1974
|
}
|
|
1816
|
-
this.setSchema(
|
|
1975
|
+
this.setSchema(
|
|
1976
|
+
published.schema,
|
|
1977
|
+
this.options.theme ?? published.theme ?? void 0,
|
|
1978
|
+
published.id
|
|
1979
|
+
);
|
|
1817
1980
|
} catch (error) {
|
|
1818
1981
|
if (this.destroyed) return;
|
|
1819
1982
|
this.handleError(toFilloError(error));
|
|
@@ -1823,31 +1986,35 @@ var DomFormController = class {
|
|
|
1823
1986
|
setSchema(schema, theme, formId) {
|
|
1824
1987
|
if (this.destroyed) return;
|
|
1825
1988
|
const normalized = normalizeFormSchema(schema);
|
|
1826
|
-
if (!normalized.ok)
|
|
1989
|
+
if (!normalized.ok)
|
|
1990
|
+
throw new FilloError(`This form could not be rendered: ${normalized.error}`, 422);
|
|
1827
1991
|
const normalizedSchema = normalized.schema;
|
|
1828
1992
|
this.schema = normalizedSchema;
|
|
1829
1993
|
this.theme = theme;
|
|
1830
|
-
this.
|
|
1994
|
+
const renderOnly = this.options.renderOnly === true;
|
|
1995
|
+
this.formId = renderOnly ? void 0 : formId;
|
|
1831
1996
|
const displayOnly = this.notOpenVariant !== null;
|
|
1997
|
+
const transportDisabled = displayOnly || renderOnly;
|
|
1832
1998
|
this.engine = coreCreateFormController({
|
|
1833
1999
|
form: normalizedSchema,
|
|
1834
|
-
formId:
|
|
1835
|
-
client:
|
|
2000
|
+
formId: transportDisabled ? void 0 : formId,
|
|
2001
|
+
client: transportDisabled ? void 0 : this.options.client,
|
|
1836
2002
|
initialData: this.options.initialData,
|
|
1837
2003
|
onChange: displayOnly ? void 0 : this.options.onChange,
|
|
1838
|
-
onSubmitted:
|
|
2004
|
+
onSubmitted: transportDisabled ? void 0 : this.options.onSubmitted,
|
|
1839
2005
|
getHoneypot: () => this.element.querySelector('input[name="fillo_hp_field"]')?.value ?? this.hpValue,
|
|
1840
2006
|
surface: "default",
|
|
1841
|
-
resolveFormId:
|
|
2007
|
+
resolveFormId: transportDisabled ? void 0 : this.resolveFormIdFn,
|
|
2008
|
+
skipValidation: renderOnly,
|
|
1842
2009
|
// Dev chrome gets the developer-grade resolution failure (message +
|
|
1843
2010
|
// machine code) instead of the respondent-safe "unavailable" fallback.
|
|
1844
2011
|
verboseResolutionErrors: this.devChrome(),
|
|
1845
|
-
respondent:
|
|
2012
|
+
respondent: transportDisabled ? void 0 : this.options.respondent,
|
|
1846
2013
|
// Challenge (Turnstile) — wired as react's useFilloController wires the
|
|
1847
2014
|
// core controller: challengeRequired captured once here (forced off for
|
|
1848
2015
|
// a not-open preview, which never submits regardless);
|
|
1849
2016
|
// getChallengeToken/onChallengeFailed stay live via `this`.
|
|
1850
|
-
challengeRequired:
|
|
2017
|
+
challengeRequired: transportDisabled ? false : Boolean(this.challenge),
|
|
1851
2018
|
getChallengeToken: () => this.challengeToken,
|
|
1852
2019
|
onChallengeFailed: () => this.resetChallengeWidget()
|
|
1853
2020
|
});
|
|
@@ -1866,7 +2033,7 @@ var DomFormController = class {
|
|
|
1866
2033
|
this.queueRender();
|
|
1867
2034
|
}
|
|
1868
2035
|
});
|
|
1869
|
-
if (!
|
|
2036
|
+
if (!transportDisabled) this.configureDraftFlush(normalizedSchema);
|
|
1870
2037
|
this.phase = this.closed ? "closed" : "ready";
|
|
1871
2038
|
this.render();
|
|
1872
2039
|
}
|
|
@@ -1982,13 +2149,21 @@ var DomFormController = class {
|
|
|
1982
2149
|
* (it must not strand the field at permanently-full). */
|
|
1983
2150
|
fieldActive(fieldId) {
|
|
1984
2151
|
let n = 0;
|
|
1985
|
-
for (const row of this.inFlight.values())
|
|
2152
|
+
for (const row of this.inFlight.values())
|
|
2153
|
+
if (row.fieldId === fieldId && row.error === void 0) n++;
|
|
1986
2154
|
return n;
|
|
1987
2155
|
}
|
|
1988
2156
|
uploadRowsFor(fieldId) {
|
|
1989
2157
|
const rows = [];
|
|
1990
2158
|
for (const [key, row] of this.inFlight) {
|
|
1991
|
-
if (row.fieldId === fieldId)
|
|
2159
|
+
if (row.fieldId === fieldId)
|
|
2160
|
+
rows.push({
|
|
2161
|
+
key,
|
|
2162
|
+
name: row.name,
|
|
2163
|
+
size: row.size,
|
|
2164
|
+
fraction: row.fraction,
|
|
2165
|
+
error: row.error
|
|
2166
|
+
});
|
|
1992
2167
|
}
|
|
1993
2168
|
return rows;
|
|
1994
2169
|
}
|
|
@@ -2013,7 +2188,15 @@ var DomFormController = class {
|
|
|
2013
2188
|
async startUpload(fieldId, file, maxFiles) {
|
|
2014
2189
|
const key = `${file.name}-${file.size}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
2015
2190
|
const controller = new AbortController();
|
|
2016
|
-
this.inFlight.set(key, {
|
|
2191
|
+
this.inFlight.set(key, {
|
|
2192
|
+
fieldId,
|
|
2193
|
+
name: file.name,
|
|
2194
|
+
size: file.size,
|
|
2195
|
+
fraction: 0,
|
|
2196
|
+
file,
|
|
2197
|
+
controller,
|
|
2198
|
+
maxFiles
|
|
2199
|
+
});
|
|
2017
2200
|
this.engine?.setUploading(fieldId, true);
|
|
2018
2201
|
this.queueRender();
|
|
2019
2202
|
try {
|
|
@@ -2124,7 +2307,8 @@ var DomFormController = class {
|
|
|
2124
2307
|
this.teardownTurnstileWidget();
|
|
2125
2308
|
this.turnstileSiteKey = siteKey;
|
|
2126
2309
|
}
|
|
2127
|
-
if (this.turnstileWidgetId !== null || this.turnstileLoadInFlight || this.challengeError)
|
|
2310
|
+
if (this.turnstileWidgetId !== null || this.turnstileLoadInFlight || this.challengeError)
|
|
2311
|
+
return;
|
|
2128
2312
|
this.turnstileLoadInFlight = true;
|
|
2129
2313
|
const attempt = ++this.turnstileAttempt;
|
|
2130
2314
|
loadTurnstileScript().then(() => {
|
|
@@ -2227,7 +2411,9 @@ var DomFormController = class {
|
|
|
2227
2411
|
const buttons = Array.from(
|
|
2228
2412
|
this.element.querySelectorAll("[data-fillo-rank-opt]")
|
|
2229
2413
|
).filter((b) => b.getAttribute("data-fillo-rank-opt") === desc.rankOpt);
|
|
2230
|
-
target = buttons.find(
|
|
2414
|
+
target = buttons.find(
|
|
2415
|
+
(b) => b.getAttribute("data-fillo-rank-dir") === desc.rankDir && !b.disabled
|
|
2416
|
+
) ?? buttons.find((b) => !b.disabled) ?? null;
|
|
2231
2417
|
}
|
|
2232
2418
|
if (!target && desc.id) target = document.getElementById(desc.id);
|
|
2233
2419
|
if (!target && desc.fieldId != null && desc.controlIndex != null && desc.controlIndex >= 0) {
|
|
@@ -2311,13 +2497,26 @@ var DomFormController = class {
|
|
|
2311
2497
|
);
|
|
2312
2498
|
this.syncErrorNotice = error;
|
|
2313
2499
|
}
|
|
2500
|
+
// A macrotask, not a microtask (ledger #4, docs/decisions/input-quality.md):
|
|
2501
|
+
// a text field's blur fires "change" -> setValue -> this full-tree rebuild,
|
|
2502
|
+
// and if the respondent's next action is a pointer click elsewhere (e.g. a
|
|
2503
|
+
// rating star), that click is a SEPARATE, later task from the blur's — but
|
|
2504
|
+
// a microtask always drains before any later task runs, including that
|
|
2505
|
+
// click. A microtask-deferred rebuild still replaces the click's target
|
|
2506
|
+
// node before the click is dispatched, so Chromium/WebKit never fire it at
|
|
2507
|
+
// all (the click is silently swallowed — click doesn't fire when the
|
|
2508
|
+
// mousedown target was detached before mouseup: w3c/uievents#141).
|
|
2509
|
+
// setTimeout is the standard fix for exactly this "blur beats click" race:
|
|
2510
|
+
// scheduled during the blur's own task, it reliably runs BEHIND whatever
|
|
2511
|
+
// pointer/click tasks were already in flight for the same gesture, so an
|
|
2512
|
+
// in-flight click lands on the pre-rebuild node first.
|
|
2314
2513
|
queueRender() {
|
|
2315
2514
|
if (this.renderQueued || this.destroyed) return;
|
|
2316
2515
|
this.renderQueued = true;
|
|
2317
|
-
|
|
2516
|
+
setTimeout(() => {
|
|
2318
2517
|
this.renderQueued = false;
|
|
2319
2518
|
this.render();
|
|
2320
|
-
});
|
|
2519
|
+
}, 0);
|
|
2321
2520
|
}
|
|
2322
2521
|
render(error) {
|
|
2323
2522
|
if (this.destroyed) return;
|
|
@@ -2344,12 +2543,16 @@ var DomFormController = class {
|
|
|
2344
2543
|
}
|
|
2345
2544
|
const activeError = error ?? this.fatalError;
|
|
2346
2545
|
if (this.phase === "error" && activeError) {
|
|
2546
|
+
const targetMessage = activeError.code === "form_target_required" && this.devChrome() ? `${activeError.message} (${activeError.code})` : null;
|
|
2347
2547
|
const errorView = this.options.renderError?.(activeError) ?? el("div", {
|
|
2348
2548
|
className: "fillo-form fillo-form--error",
|
|
2349
|
-
text: this.genericUnavailable ? "This form is unavailable." : loadFailureMessage(activeError)
|
|
2549
|
+
text: targetMessage ?? (this.genericUnavailable ? "This form is unavailable." : loadFailureMessage(activeError))
|
|
2350
2550
|
});
|
|
2351
2551
|
if (!errorView.hasAttribute("data-fillo")) errorView.setAttribute("data-fillo", "root");
|
|
2352
2552
|
if (!errorView.hasAttribute("data-state")) errorView.setAttribute("data-state", "error");
|
|
2553
|
+
if (activeError.code && !errorView.hasAttribute("data-error-code")) {
|
|
2554
|
+
errorView.setAttribute("data-error-code", activeError.code);
|
|
2555
|
+
}
|
|
2353
2556
|
if (!errorView.hasAttribute("role")) errorView.setAttribute("role", "alert");
|
|
2354
2557
|
themeStyle(errorView, this.theme);
|
|
2355
2558
|
this.element.appendChild(errorView);
|
|
@@ -2361,13 +2564,16 @@ var DomFormController = class {
|
|
|
2361
2564
|
attrs: { "data-fillo": "root", "data-state": "closed" }
|
|
2362
2565
|
});
|
|
2363
2566
|
themeStyle(closed, this.theme);
|
|
2364
|
-
closed.appendChild(
|
|
2567
|
+
closed.appendChild(
|
|
2568
|
+
el("p", { className: "fillo-closed", text: "This form is no longer accepting responses." })
|
|
2569
|
+
);
|
|
2365
2570
|
this.element.appendChild(closed);
|
|
2366
2571
|
return;
|
|
2367
2572
|
}
|
|
2368
2573
|
if (this.notOpenVariant && this.schema) {
|
|
2369
2574
|
this.element.appendChild(this.renderNotOpen(this.notOpenVariant));
|
|
2370
|
-
if (this.brandingVisible())
|
|
2575
|
+
if (this.brandingVisible())
|
|
2576
|
+
this.element.appendChild(poweredByEl(this.fetchedBrandLabel, this.fetchedBrandHref));
|
|
2371
2577
|
return;
|
|
2372
2578
|
}
|
|
2373
2579
|
if (!this.schema) return;
|
|
@@ -2376,11 +2582,15 @@ var DomFormController = class {
|
|
|
2376
2582
|
const state = this.engine.getState();
|
|
2377
2583
|
const success = this.options.renderSuccess?.(api) ?? defaultSuccess(this.schema);
|
|
2378
2584
|
success.setAttribute("data-fillo", success.getAttribute("data-fillo") ?? "root");
|
|
2585
|
+
if (this.formId && !success.hasAttribute("data-fillo-form-id")) {
|
|
2586
|
+
success.setAttribute("data-fillo-form-id", this.formId);
|
|
2587
|
+
}
|
|
2379
2588
|
success.setAttribute("data-state", success.getAttribute("data-state") ?? "submitted");
|
|
2380
2589
|
if (!success.hasAttribute("tabindex")) success.setAttribute("tabindex", "-1");
|
|
2381
2590
|
themeStyle(success, this.theme);
|
|
2382
2591
|
this.element.appendChild(success);
|
|
2383
|
-
if (this.brandingVisible())
|
|
2592
|
+
if (this.brandingVisible())
|
|
2593
|
+
this.element.appendChild(poweredByEl(this.fetchedBrandLabel, this.fetchedBrandHref));
|
|
2384
2594
|
if (!state.restoredSubmission && !this.liveSubmissionHandled) {
|
|
2385
2595
|
this.liveSubmissionHandled = true;
|
|
2386
2596
|
const redirect = safeHttpUrl(this.schema.settings.redirectUrl);
|
|
@@ -2402,12 +2612,13 @@ var DomFormController = class {
|
|
|
2402
2612
|
staged: this.stagedNotice,
|
|
2403
2613
|
draft: this.draftNotice,
|
|
2404
2614
|
warningUrl: this.syncWarningUrl,
|
|
2405
|
-
noClient: !this.options.client
|
|
2615
|
+
noClient: !this.options.client && !this.options.renderOnly
|
|
2406
2616
|
})
|
|
2407
2617
|
);
|
|
2408
2618
|
}
|
|
2409
2619
|
this.element.appendChild(this.renderForm());
|
|
2410
|
-
if (this.brandingVisible())
|
|
2620
|
+
if (this.brandingVisible())
|
|
2621
|
+
this.element.appendChild(poweredByEl(this.fetchedBrandLabel, this.fetchedBrandHref));
|
|
2411
2622
|
this.applyFocus(intent, captured);
|
|
2412
2623
|
}
|
|
2413
2624
|
/** Server-driven and not client-removable; defaults on until the server says. */
|
|
@@ -2465,6 +2676,7 @@ var DomFormController = class {
|
|
|
2465
2676
|
"data-page": String(api.pageIndex + 1)
|
|
2466
2677
|
}
|
|
2467
2678
|
});
|
|
2679
|
+
if (!displayOnly && this.formId) form.setAttribute("data-fillo-form-id", this.formId);
|
|
2468
2680
|
form.toggleAttribute("data-last-page", api.isLastPage);
|
|
2469
2681
|
themeStyle(form, this.theme);
|
|
2470
2682
|
const advance = () => {
|
|
@@ -2532,7 +2744,11 @@ var DomFormController = class {
|
|
|
2532
2744
|
const header = el("header", { className: "fillo-header", attrs: { "data-fillo": "header" } });
|
|
2533
2745
|
if (api.form.title) {
|
|
2534
2746
|
header.appendChild(
|
|
2535
|
-
el("h1", {
|
|
2747
|
+
el("h1", {
|
|
2748
|
+
className: "fillo-title",
|
|
2749
|
+
text: api.form.title,
|
|
2750
|
+
attrs: { "data-fillo": "title" }
|
|
2751
|
+
})
|
|
2536
2752
|
);
|
|
2537
2753
|
}
|
|
2538
2754
|
if (api.form.description) {
|
|
@@ -2563,7 +2779,16 @@ var DomFormController = class {
|
|
|
2563
2779
|
}
|
|
2564
2780
|
content.appendChild(blocks);
|
|
2565
2781
|
if (!displayOnly) {
|
|
2566
|
-
const hp = el("input", {
|
|
2782
|
+
const hp = el("input", {
|
|
2783
|
+
className: "fillo-hp",
|
|
2784
|
+
attrs: {
|
|
2785
|
+
type: "text",
|
|
2786
|
+
name: "fillo_hp_field",
|
|
2787
|
+
tabindex: "-1",
|
|
2788
|
+
autocomplete: "off",
|
|
2789
|
+
"aria-hidden": "true"
|
|
2790
|
+
}
|
|
2791
|
+
});
|
|
2567
2792
|
hp.style.position = "absolute";
|
|
2568
2793
|
hp.style.left = "-9999px";
|
|
2569
2794
|
hp.style.top = "auto";
|
|
@@ -2599,12 +2824,19 @@ var DomFormController = class {
|
|
|
2599
2824
|
}
|
|
2600
2825
|
const submitError = api.status === "error" ? "This form can't submit right now. Please try again in a moment." : this.engine?.getState().submitError;
|
|
2601
2826
|
if (submitError) {
|
|
2602
|
-
const alert = el("p", {
|
|
2827
|
+
const alert = el("p", {
|
|
2828
|
+
className: "fillo-submit-error",
|
|
2829
|
+
attrs: { role: "alert" },
|
|
2830
|
+
text: submitError
|
|
2831
|
+
});
|
|
2603
2832
|
const fixUrl = this.devChrome() && this.resolutionFailureCode === "form_not_published" && this.syncWarningCode === "storage_required" ? safeHttpUrl(this.syncWarningUrl) : null;
|
|
2604
2833
|
if (fixUrl) {
|
|
2605
2834
|
alert.append(" Connect storage: ");
|
|
2606
2835
|
alert.appendChild(
|
|
2607
|
-
el("a", {
|
|
2836
|
+
el("a", {
|
|
2837
|
+
text: fixUrl,
|
|
2838
|
+
attrs: { href: fixUrl, target: "_blank", rel: "noopener noreferrer" }
|
|
2839
|
+
})
|
|
2608
2840
|
);
|
|
2609
2841
|
}
|
|
2610
2842
|
content.appendChild(alert);
|
|
@@ -2659,7 +2891,10 @@ var DomFormController = class {
|
|
|
2659
2891
|
attrs: { role: "alert", tabindex: "-1", "data-fillo": "error-summary" }
|
|
2660
2892
|
});
|
|
2661
2893
|
box.appendChild(
|
|
2662
|
-
el("p", {
|
|
2894
|
+
el("p", {
|
|
2895
|
+
className: "fillo-error-summary-title",
|
|
2896
|
+
text: DEFAULT_FIELD_STRINGS.errorSummaryTitle
|
|
2897
|
+
})
|
|
2663
2898
|
);
|
|
2664
2899
|
const list = el("ul");
|
|
2665
2900
|
for (const field of fields) {
|
|
@@ -2712,7 +2947,7 @@ var DomFormController = class {
|
|
|
2712
2947
|
getValue: () => this.engine?.getState().data[block.id],
|
|
2713
2948
|
registerOverlay: this.registerOverlay,
|
|
2714
2949
|
uploadNotice: this.uploadNotices.get(block.id),
|
|
2715
|
-
|
|
2950
|
+
uploadDisabledReason: this.options.renderOnly ? "render_only" : this.storageBlocked() ? "storage_unavailable" : void 0,
|
|
2716
2951
|
storageFixUrl: this.storageBlocked() && this.devChrome() ? safeHttpUrl(this.syncWarningUrl) : null,
|
|
2717
2952
|
announce: this.announce,
|
|
2718
2953
|
announceAlert: this.announceAlert
|
|
@@ -2738,7 +2973,10 @@ function defaultSuccess(schema) {
|
|
|
2738
2973
|
appendChildren(inner, [
|
|
2739
2974
|
el("div", { className: "fillo-success-mark", attrs: { "aria-hidden": "true" } }),
|
|
2740
2975
|
el("h2", { className: "fillo-success-title", text: schema.settings.successTitle ?? "Thanks!" }),
|
|
2741
|
-
el("p", {
|
|
2976
|
+
el("p", {
|
|
2977
|
+
className: "fillo-success-message",
|
|
2978
|
+
text: schema.settings.successMessage ?? "Your response has been recorded."
|
|
2979
|
+
})
|
|
2742
2980
|
]);
|
|
2743
2981
|
success.appendChild(inner);
|
|
2744
2982
|
return success;
|
|
@@ -2786,7 +3024,10 @@ function devDraftEl(warningUrl) {
|
|
|
2786
3024
|
if (safeUrl) {
|
|
2787
3025
|
node.append("Connect storage to publish: ");
|
|
2788
3026
|
node.appendChild(
|
|
2789
|
-
el("a", {
|
|
3027
|
+
el("a", {
|
|
3028
|
+
text: safeUrl,
|
|
3029
|
+
attrs: { href: safeUrl, target: "_blank", rel: "noopener noreferrer" }
|
|
3030
|
+
})
|
|
2790
3031
|
);
|
|
2791
3032
|
node.append(". ");
|
|
2792
3033
|
}
|
|
@@ -2824,6 +3065,14 @@ function loadFailureMessage(error) {
|
|
|
2824
3065
|
if (error.status === 422) return DEFAULT_STRINGS.renderFailed;
|
|
2825
3066
|
return DEFAULT_STRINGS.loadFailed;
|
|
2826
3067
|
}
|
|
3068
|
+
function formTargetRequiredError() {
|
|
3069
|
+
return new FilloError(
|
|
3070
|
+
"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.",
|
|
3071
|
+
422,
|
|
3072
|
+
void 0,
|
|
3073
|
+
"form_target_required"
|
|
3074
|
+
);
|
|
3075
|
+
}
|
|
2827
3076
|
function toFilloError(error) {
|
|
2828
3077
|
if (isFilloError(error)) return error;
|
|
2829
3078
|
return new FilloError(error instanceof Error ? error.message : String(error), 0);
|
|
@@ -2831,7 +3080,11 @@ function toFilloError(error) {
|
|
|
2831
3080
|
function renderForm(target, options) {
|
|
2832
3081
|
const element = typeof target === "string" ? document.querySelector(target) : target;
|
|
2833
3082
|
if (!element) throw new FilloError(`Fillo mount target not found: ${String(target)}`, 0);
|
|
2834
|
-
|
|
3083
|
+
const internalOptions = {
|
|
3084
|
+
...options,
|
|
3085
|
+
initialData: options.initialData ?? {}
|
|
3086
|
+
};
|
|
3087
|
+
return new DomFormController(element, internalOptions);
|
|
2835
3088
|
}
|
|
2836
3089
|
function createFormElement(options) {
|
|
2837
3090
|
const host = el("div");
|
|
@@ -2849,7 +3102,7 @@ var FilloFormElement = class extends HTMLElementBase {
|
|
|
2849
3102
|
mounted = false;
|
|
2850
3103
|
mountScheduled = false;
|
|
2851
3104
|
static get observedAttributes() {
|
|
2852
|
-
return ["publishable-key", "form-id", "data-preview"];
|
|
3105
|
+
return ["publishable-key", "form-id", "data-preview", "data-render-only"];
|
|
2853
3106
|
}
|
|
2854
3107
|
connectedCallback() {
|
|
2855
3108
|
this.scheduleMount();
|
|
@@ -2906,6 +3159,11 @@ var FilloFormElement = class extends HTMLElementBase {
|
|
|
2906
3159
|
const value = this.getAttribute("data-preview");
|
|
2907
3160
|
return value !== null && value !== "false" && value !== "0";
|
|
2908
3161
|
}
|
|
3162
|
+
/** Explicit local-preview mode for custom-element integrations. */
|
|
3163
|
+
renderOnlyAttrEnabled() {
|
|
3164
|
+
const value = this.getAttribute("data-render-only");
|
|
3165
|
+
return value !== null && value !== "false" && value !== "0";
|
|
3166
|
+
}
|
|
2909
3167
|
mount() {
|
|
2910
3168
|
this.instance?.destroy();
|
|
2911
3169
|
const key = this.getAttribute("publishable-key") ?? void 0;
|
|
@@ -2923,6 +3181,7 @@ var FilloFormElement = class extends HTMLElementBase {
|
|
|
2923
3181
|
// Value-aware: frameworks stringify booleans onto data-* attributes, so
|
|
2924
3182
|
// data-preview="false"/"0" must mean OFF, not presence-implies-on.
|
|
2925
3183
|
preview: this.previewAttrEnabled() ? true : void 0,
|
|
3184
|
+
renderOnly: this.renderOnlyAttrEnabled() ? true : void 0,
|
|
2926
3185
|
onChange: (data) => this.dispatchEvent(new CustomEvent("fillo-change", { detail: { data } })),
|
|
2927
3186
|
onSubmitted: (responseId, data) => this.dispatchEvent(new CustomEvent("fillo-submit", { detail: { responseId, data } })),
|
|
2928
3187
|
onError: (error) => this.dispatchEvent(new CustomEvent("fillo-error", { detail: { error } }))
|