@formancy/react 0.4.0 → 0.6.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.mts +52 -6
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +304 -127
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Fragment, createContext, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
2
|
-
import { cancelUploads, fieldUploads, formatPath, parsePath, thumbnailSize } from "@formancy/core";
|
|
2
|
+
import { cancelUploads, fieldUploads, formatPath, parsePath, placedGroup, placedPage, thumbnailSize } from "@formancy/core";
|
|
3
|
+
import { createFormText, optionsStatus } from "@formancy/core/words";
|
|
4
|
+
import { LAYOUT_LEAF_KINDS, acceptRemoteAnswer, answerFromText, applyRichCommand, capRemoteAnswer, datagridColumns, editMasked, formatMasked, isImageSource, layoutChildren, layoutNodeShows, maskIsNumeric, maskPlaceholder, narrowOptionsByLabel, parseRichText, resolveText, resolvedLocale } from "@formancy/spec";
|
|
3
5
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
-
import { LAYOUT_LEAF_KINDS, acceptRemoteOptions, answerFromText, applyRichCommand, capRemoteOptions, datagridColumns, editMasked, formatMasked, isImageSource, layoutChildren, layoutNodeShows, maskIsNumeric, maskPlaceholder, narrowOptionsByLabel, parseRichText, resolveText } from "@formancy/spec";
|
|
5
6
|
import { flushSync } from "react-dom";
|
|
6
7
|
import { encode } from "uqr";
|
|
7
8
|
//#region src/context.tsx
|
|
@@ -12,11 +13,23 @@ import { encode } from "uqr";
|
|
|
12
13
|
* use prop getters.
|
|
13
14
|
*/
|
|
14
15
|
const FormancyContext = createContext(null);
|
|
15
|
-
|
|
16
|
+
/**
|
|
17
|
+
* The form's own words, in the language its questions are read in (0171). Beside the engine
|
|
18
|
+
* rather than on it, because the engine has no buttons — the server runs the same one.
|
|
19
|
+
*/
|
|
20
|
+
const FormancyTextContext = createContext(null);
|
|
21
|
+
function FormancyProvider({ engine, words, children }) {
|
|
16
22
|
useEffect(() => () => cancelUploads(engine), [engine]);
|
|
23
|
+
const text = useMemo(() => createFormText({
|
|
24
|
+
locale: resolvedLocale(engine.schema(), engine.locale()),
|
|
25
|
+
...words === void 0 ? {} : { words }
|
|
26
|
+
}), [engine, words]);
|
|
17
27
|
return /* @__PURE__ */ jsx(FormancyContext.Provider, {
|
|
18
28
|
value: engine,
|
|
19
|
-
children
|
|
29
|
+
children: /* @__PURE__ */ jsx(FormancyTextContext.Provider, {
|
|
30
|
+
value: text,
|
|
31
|
+
children
|
|
32
|
+
})
|
|
20
33
|
});
|
|
21
34
|
}
|
|
22
35
|
function useFormEngine() {
|
|
@@ -24,6 +37,21 @@ function useFormEngine() {
|
|
|
24
37
|
if (!engine) throw new Error("formancy hooks need a <FormancyProvider engine={...}> above them in the tree");
|
|
25
38
|
return engine;
|
|
26
39
|
}
|
|
40
|
+
let english;
|
|
41
|
+
/**
|
|
42
|
+
* The form's words, in the language the reader chose for it.
|
|
43
|
+
*
|
|
44
|
+
* English outside a `FormancyProvider`, which has no engine to take a language from — a
|
|
45
|
+
* resume notice a host draws beside the form rather than inside its provider. Never the
|
|
46
|
+
* browser's language: the form's is the one the reader chose, and the browser's is the
|
|
47
|
+
* one their machine came with.
|
|
48
|
+
*/
|
|
49
|
+
function useFormText() {
|
|
50
|
+
const text = useContext(FormancyTextContext);
|
|
51
|
+
if (text !== null) return text;
|
|
52
|
+
english ??= createFormText({ locale: "" });
|
|
53
|
+
return english;
|
|
54
|
+
}
|
|
27
55
|
//#endregion
|
|
28
56
|
//#region src/use-field.ts
|
|
29
57
|
/**
|
|
@@ -75,6 +103,10 @@ function useRepeater(path) {
|
|
|
75
103
|
const subscribe = useCallback((onChange) => engine.subscribeField(parsed, onChange), [engine, parsed]);
|
|
76
104
|
const getRowCount = useCallback(() => engine.rowCount(parsed), [engine, parsed]);
|
|
77
105
|
const rowCount = useSyncExternalStore(subscribe, getRowCount, getRowCount);
|
|
106
|
+
const getCanAdd = useCallback(() => engine.canAddRow(parsed), [engine, parsed]);
|
|
107
|
+
const canAdd = useSyncExternalStore(subscribe, getCanAdd, getCanAdd);
|
|
108
|
+
const getCanRemove = useCallback(() => engine.canRemoveRow(parsed), [engine, parsed]);
|
|
109
|
+
const canRemove = useSyncExternalStore(subscribe, getCanRemove, getCanRemove);
|
|
78
110
|
const addRow = useCallback(() => engine.addRow(parsed), [engine, parsed]);
|
|
79
111
|
const removeRow = useCallback((index) => engine.removeRow(parsed, index), [engine, parsed]);
|
|
80
112
|
const moveRow = useCallback((from, to) => engine.moveRow(parsed, from, to), [engine, parsed]);
|
|
@@ -84,12 +116,16 @@ function useRepeater(path) {
|
|
|
84
116
|
return useMemo(() => ({
|
|
85
117
|
rowCount,
|
|
86
118
|
rowIds,
|
|
119
|
+
canAdd,
|
|
120
|
+
canRemove,
|
|
87
121
|
addRow,
|
|
88
122
|
removeRow,
|
|
89
123
|
moveRow
|
|
90
124
|
}), [
|
|
91
125
|
rowCount,
|
|
92
126
|
rowIds,
|
|
127
|
+
canAdd,
|
|
128
|
+
canRemove,
|
|
93
129
|
addRow,
|
|
94
130
|
removeRow,
|
|
95
131
|
moveRow
|
|
@@ -267,6 +303,7 @@ function LayoutNodeView({ schema, node, locale, renderField, at, shows }) {
|
|
|
267
303
|
*/
|
|
268
304
|
function Tabs({ schema, node, locale, renderField, at, here, shows }) {
|
|
269
305
|
const base = useId();
|
|
306
|
+
const text = useFormText();
|
|
270
307
|
const shown = node.children.flatMap((child, index) => shows === void 0 || layoutNodeShows(child, shows) ? [{
|
|
271
308
|
child,
|
|
272
309
|
index
|
|
@@ -325,7 +362,7 @@ function Tabs({ schema, node, locale, renderField, at, here, shows }) {
|
|
|
325
362
|
tabIndex: index === open ? 0 : -1,
|
|
326
363
|
"data-formancy-part": "tab",
|
|
327
364
|
onClick: () => setOpen(index),
|
|
328
|
-
children: names[index] ??
|
|
365
|
+
children: names[index] ?? text("tabs.unnamed", { position: index + 1 })
|
|
329
366
|
}, position))
|
|
330
367
|
}), shown.map(({ child, index: position }, index) => /* @__PURE__ */ jsx("div", {
|
|
331
368
|
ref: (element) => {
|
|
@@ -518,6 +555,11 @@ function useOptionsSources() {
|
|
|
518
555
|
const DEBOUNCE_MS = 250;
|
|
519
556
|
const MIN_QUERY = 0;
|
|
520
557
|
const MAX_ROWS = 50;
|
|
558
|
+
/**
|
|
559
|
+
* Nothing left out, as one value: a reset that stores it again is the same state, so
|
|
560
|
+
* React bails out of the render rather than repeating one.
|
|
561
|
+
*/
|
|
562
|
+
const COMPLETE = { kind: "complete" };
|
|
521
563
|
function useSourcedOptions(field, query, enabled = true) {
|
|
522
564
|
const engine = useFormEngine();
|
|
523
565
|
const sources = useOptionsSources();
|
|
@@ -527,19 +569,28 @@ function useSourcedOptions(field, query, enabled = true) {
|
|
|
527
569
|
const [named, setNamed] = useState(/* @__PURE__ */ new Map());
|
|
528
570
|
const [busy, setBusy] = useState(false);
|
|
529
571
|
const [failed, setFailed] = useState(false);
|
|
530
|
-
const [
|
|
572
|
+
const [completeness, setCompleteness] = useState(COMPLETE);
|
|
573
|
+
const [attempt, setAttempt] = useState(0);
|
|
574
|
+
const retry = useCallback(() => setAttempt((was) => was + 1), []);
|
|
531
575
|
const debounceMs = source?.debounceMs ?? DEBOUNCE_MS;
|
|
532
576
|
const minQueryLength = source?.minQueryLength ?? MIN_QUERY;
|
|
533
577
|
const maxRows = source?.maxRows ?? MAX_ROWS;
|
|
534
578
|
const stored = typeof field.value === "string" && field.value !== "" ? field.value : void 0;
|
|
535
579
|
const path = field.path;
|
|
580
|
+
const disabled = field.disabled;
|
|
536
581
|
const asked = useRef(0);
|
|
537
582
|
useEffect(() => {
|
|
538
|
-
if (!enabled || source === void 0) return;
|
|
539
583
|
const generation = asked.current += 1;
|
|
584
|
+
if (!enabled || source === void 0) return;
|
|
585
|
+
if (disabled) {
|
|
586
|
+
setBusy(false);
|
|
587
|
+
setFailed(false);
|
|
588
|
+
setCompleteness(COMPLETE);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
540
591
|
if (query.trim().length < minQueryLength) {
|
|
541
592
|
setRows([]);
|
|
542
|
-
|
|
593
|
+
setCompleteness(COMPLETE);
|
|
543
594
|
setBusy(false);
|
|
544
595
|
return;
|
|
545
596
|
}
|
|
@@ -558,23 +609,21 @@ function useSourcedOptions(field, query, enabled = true) {
|
|
|
558
609
|
signal: controller.signal
|
|
559
610
|
}).then((answer) => {
|
|
560
611
|
if (generation !== asked.current) return;
|
|
561
|
-
const accepted =
|
|
612
|
+
const accepted = acceptRemoteAnswer(answer);
|
|
562
613
|
if (accepted === void 0) {
|
|
563
614
|
setFailed(true);
|
|
564
615
|
setRows([]);
|
|
565
|
-
|
|
616
|
+
setCompleteness(COMPLETE);
|
|
566
617
|
return;
|
|
567
618
|
}
|
|
568
|
-
const
|
|
569
|
-
setRows(shown);
|
|
570
|
-
|
|
571
|
-
shown: shown.length,
|
|
572
|
-
total
|
|
573
|
-
} : null);
|
|
619
|
+
const settled = capRemoteAnswer(accepted, maxRows);
|
|
620
|
+
setRows(settled.shown);
|
|
621
|
+
setCompleteness(settled.completeness);
|
|
574
622
|
}).catch(() => {
|
|
575
623
|
if (generation !== asked.current) return;
|
|
576
624
|
setFailed(true);
|
|
577
625
|
setRows([]);
|
|
626
|
+
setCompleteness(COMPLETE);
|
|
578
627
|
}).finally(() => {
|
|
579
628
|
if (generation === asked.current) setBusy(false);
|
|
580
629
|
});
|
|
@@ -585,6 +634,7 @@ function useSourcedOptions(field, query, enabled = true) {
|
|
|
585
634
|
};
|
|
586
635
|
}, [
|
|
587
636
|
enabled,
|
|
637
|
+
disabled,
|
|
588
638
|
source,
|
|
589
639
|
name,
|
|
590
640
|
path,
|
|
@@ -592,7 +642,8 @@ function useSourcedOptions(field, query, enabled = true) {
|
|
|
592
642
|
minQueryLength,
|
|
593
643
|
maxRows,
|
|
594
644
|
debounceMs,
|
|
595
|
-
engine
|
|
645
|
+
engine,
|
|
646
|
+
attempt
|
|
596
647
|
]);
|
|
597
648
|
const askedFor = useRef(/* @__PURE__ */ new Set());
|
|
598
649
|
useEffect(() => {
|
|
@@ -610,11 +661,11 @@ function useSourcedOptions(field, query, enabled = true) {
|
|
|
610
661
|
limit: 1,
|
|
611
662
|
signal: controller.signal
|
|
612
663
|
}).then((answer) => {
|
|
613
|
-
const accepted =
|
|
664
|
+
const accepted = acceptRemoteAnswer(answer);
|
|
614
665
|
if (accepted === void 0) return;
|
|
615
666
|
setNamed((was) => {
|
|
616
667
|
const next = new Map(was);
|
|
617
|
-
for (const row of accepted) next.set(row.value, row.label);
|
|
668
|
+
for (const row of accepted.options) next.set(row.value, row.label);
|
|
618
669
|
return next;
|
|
619
670
|
});
|
|
620
671
|
}).catch(() => void 0);
|
|
@@ -645,31 +696,13 @@ function useSourcedOptions(field, query, enabled = true) {
|
|
|
645
696
|
remote: {
|
|
646
697
|
unavailable: source === void 0,
|
|
647
698
|
busy,
|
|
699
|
+
failed,
|
|
648
700
|
needs: minQueryLength,
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
busy,
|
|
652
|
-
needs: minQueryLength,
|
|
653
|
-
query,
|
|
654
|
-
capped
|
|
655
|
-
})
|
|
701
|
+
completeness,
|
|
702
|
+
retry
|
|
656
703
|
}
|
|
657
704
|
};
|
|
658
705
|
}
|
|
659
|
-
/**
|
|
660
|
-
* The one thing the field says out loud, and never more than one.
|
|
661
|
-
*
|
|
662
|
-
* Success announces nothing: the listbox and `aria-activedescendant` are the
|
|
663
|
-
* feedback, and a region that spoke on every successful keystroke would talk over
|
|
664
|
-
* them. A failure is a sentence somebody can act on rather than a code.
|
|
665
|
-
*/
|
|
666
|
-
function statusFor({ failed, busy, needs, query, capped }) {
|
|
667
|
-
if (failed) return "The options could not be loaded. Type to try again.";
|
|
668
|
-
if (query.trim().length < needs) return `Type at least ${String(needs)} characters to search.`;
|
|
669
|
-
if (busy) return "Searching…";
|
|
670
|
-
if (capped !== null) return `Showing the first ${String(capped.shown)} of ${String(capped.total)} — keep typing to narrow.`;
|
|
671
|
-
return "";
|
|
672
|
-
}
|
|
673
706
|
//#endregion
|
|
674
707
|
//#region src/uploads.ts
|
|
675
708
|
const UploaderContext = createContext(void 0);
|
|
@@ -750,11 +783,11 @@ function FieldShell({ path, field, label, children }) {
|
|
|
750
783
|
* Not the text: a theme that keyed off English prose would break in every other
|
|
751
784
|
* language the form is offered in.
|
|
752
785
|
*/
|
|
753
|
-
function statusState({ sourced, open, matches }) {
|
|
786
|
+
function statusState({ sourced, said, open, matches }) {
|
|
754
787
|
if (sourced.remote === null) return open && matches === 0 ? "empty" : void 0;
|
|
755
788
|
if (sourced.remote.busy) return "busy";
|
|
756
|
-
if (sourced.remote.
|
|
757
|
-
if (
|
|
789
|
+
if (sourced.remote.failed) return "failed";
|
|
790
|
+
if (said !== "") return "hint";
|
|
758
791
|
return open && matches === 0 ? "empty" : void 0;
|
|
759
792
|
}
|
|
760
793
|
/**
|
|
@@ -769,11 +802,12 @@ function statusState({ sourced, open, matches }) {
|
|
|
769
802
|
* a red asterisk is a requirement some people cannot perceive.
|
|
770
803
|
*/
|
|
771
804
|
function RequiredHint({ field }) {
|
|
805
|
+
const text = useFormText();
|
|
772
806
|
if (!field.required) return null;
|
|
773
807
|
return /* @__PURE__ */ jsx("span", {
|
|
774
808
|
"data-formancy-part": "required-hint",
|
|
775
809
|
id: field.props.hint.id,
|
|
776
|
-
children: "required"
|
|
810
|
+
children: text("form.required")
|
|
777
811
|
});
|
|
778
812
|
}
|
|
779
813
|
/** One option's element id, in the shape the radio group already uses, so a
|
|
@@ -822,6 +856,7 @@ function OptionPicture({ option }) {
|
|
|
822
856
|
* These were one 2,154-line file — the largest in the repository.
|
|
823
857
|
*/
|
|
824
858
|
function TypeaheadSelectField({ path, label, field }) {
|
|
859
|
+
const text = useFormText();
|
|
825
860
|
/**
|
|
826
861
|
* What is in the box while somebody types, or null when the box is simply
|
|
827
862
|
* showing the answer.
|
|
@@ -831,7 +866,11 @@ function TypeaheadSelectField({ path, label, field }) {
|
|
|
831
866
|
* first clears the answer on the way out.
|
|
832
867
|
*/
|
|
833
868
|
const [query, setQuery] = useState(null);
|
|
834
|
-
const [
|
|
869
|
+
const [opened, setOpen] = useState(false);
|
|
870
|
+
/** A field disabled by a rule while its list was showing has no list: nobody can
|
|
871
|
+
* choose from it, and its region would say "no options match" under a read-only
|
|
872
|
+
* answer. */
|
|
873
|
+
const open = opened && !field.disabled;
|
|
835
874
|
/** The arrowed-over option by VALUE, not by index: the filtered list changes on
|
|
836
875
|
* every keystroke and an index would point at a different row after one. */
|
|
837
876
|
const [activeValue, setActiveValue] = useState(null);
|
|
@@ -840,6 +879,16 @@ function TypeaheadSelectField({ path, label, field }) {
|
|
|
840
879
|
path
|
|
841
880
|
}, query ?? "");
|
|
842
881
|
const options = sourced.options;
|
|
882
|
+
const said = sourced.remote === null ? "" : optionsStatus(text, {
|
|
883
|
+
control: "typeahead",
|
|
884
|
+
open,
|
|
885
|
+
disabled: field.disabled,
|
|
886
|
+
failed: sourced.remote.failed,
|
|
887
|
+
busy: sourced.remote.busy,
|
|
888
|
+
needs: sourced.remote.needs,
|
|
889
|
+
query: query ?? "",
|
|
890
|
+
completeness: sourced.remote.completeness
|
|
891
|
+
});
|
|
843
892
|
const chosen = options.find((option) => option.value === field.value);
|
|
844
893
|
const matches = sourced.remote === null ? narrowOptionsByLabel(options, query ?? "") : options;
|
|
845
894
|
/** Collapsed whenever there is nothing on the screen, so `aria-expanded` never
|
|
@@ -933,7 +982,7 @@ function TypeaheadSelectField({ path, label, field }) {
|
|
|
933
982
|
}), /* @__PURE__ */ jsx("ul", {
|
|
934
983
|
id: listboxId,
|
|
935
984
|
role: "listbox",
|
|
936
|
-
"aria-label":
|
|
985
|
+
"aria-label": text("options.suggestions", { label }),
|
|
937
986
|
"data-formancy-part": "typeahead-listbox",
|
|
938
987
|
hidden: !expanded,
|
|
939
988
|
children: matches.map((option) => /* @__PURE__ */ jsx("li", {
|
|
@@ -952,14 +1001,16 @@ function TypeaheadSelectField({ path, label, field }) {
|
|
|
952
1001
|
"data-formancy-part": "typeahead-status",
|
|
953
1002
|
...statusState({
|
|
954
1003
|
sourced,
|
|
1004
|
+
said,
|
|
955
1005
|
open,
|
|
956
1006
|
matches: matches.length
|
|
957
1007
|
}) === void 0 ? {} : { "data-state": statusState({
|
|
958
1008
|
sourced,
|
|
1009
|
+
said,
|
|
959
1010
|
open,
|
|
960
1011
|
matches: matches.length
|
|
961
1012
|
}) },
|
|
962
|
-
children:
|
|
1013
|
+
children: said !== "" ? said : open && matches.length === 0 && sourced.remote?.busy !== true ? text("options.noMatch") : ""
|
|
963
1014
|
})]
|
|
964
1015
|
});
|
|
965
1016
|
}
|
|
@@ -989,18 +1040,30 @@ function CheckboxField({ path, label }) {
|
|
|
989
1040
|
}
|
|
990
1041
|
function SelectField({ path, label }) {
|
|
991
1042
|
const field = useField(path);
|
|
1043
|
+
const text = useFormText();
|
|
992
1044
|
const sourced = useSourcedOptions({
|
|
993
1045
|
...field,
|
|
994
1046
|
path
|
|
995
1047
|
}, "", field.def.widget !== "typeahead");
|
|
996
1048
|
const options = sourced.options;
|
|
1049
|
+
const remote = sourced.remote;
|
|
1050
|
+
const said = remote === null ? "" : optionsStatus(text, {
|
|
1051
|
+
control: "select",
|
|
1052
|
+
open: true,
|
|
1053
|
+
disabled: field.disabled,
|
|
1054
|
+
failed: remote.failed,
|
|
1055
|
+
busy: remote.busy,
|
|
1056
|
+
needs: remote.needs,
|
|
1057
|
+
query: "",
|
|
1058
|
+
completeness: remote.completeness
|
|
1059
|
+
});
|
|
997
1060
|
if (sourced.remote?.unavailable === true) return /* @__PURE__ */ jsx(FieldShell, {
|
|
998
1061
|
path,
|
|
999
1062
|
field,
|
|
1000
1063
|
label,
|
|
1001
1064
|
children: /* @__PURE__ */ jsx("p", {
|
|
1002
1065
|
"data-formancy-part": "options-unavailable",
|
|
1003
|
-
children:
|
|
1066
|
+
children: text("options.unavailable", { source: field.def.optionsSource ?? "" })
|
|
1004
1067
|
})
|
|
1005
1068
|
});
|
|
1006
1069
|
if (field.def.widget === "typeahead") return /* @__PURE__ */ jsx(TypeaheadSelectField, {
|
|
@@ -1016,15 +1079,18 @@ function SelectField({ path, label }) {
|
|
|
1016
1079
|
...field.controlProps,
|
|
1017
1080
|
value: typeof field.value === "string" ? field.value : "",
|
|
1018
1081
|
onChange: (event) => field.setValue(event.target.value === "" ? null : event.target.value),
|
|
1082
|
+
onFocus: () => {
|
|
1083
|
+
if (remote?.failed === true) remote.retry();
|
|
1084
|
+
},
|
|
1019
1085
|
onBlur: () => field.touch(),
|
|
1020
1086
|
children: [/* @__PURE__ */ jsx("option", { value: "" }), options.map((option) => /* @__PURE__ */ jsx("option", {
|
|
1021
1087
|
value: option.value,
|
|
1022
1088
|
children: option.label
|
|
1023
1089
|
}, option.value))]
|
|
1024
|
-
}),
|
|
1090
|
+
}), remote === null ? null : /* @__PURE__ */ jsx("p", {
|
|
1025
1091
|
role: "status",
|
|
1026
1092
|
"data-formancy-part": "select-status",
|
|
1027
|
-
children:
|
|
1093
|
+
children: said
|
|
1028
1094
|
})]
|
|
1029
1095
|
});
|
|
1030
1096
|
}
|
|
@@ -1096,6 +1162,7 @@ function RadioGroupField({ path, label }) {
|
|
|
1096
1162
|
*/
|
|
1097
1163
|
function FileField({ path, label }) {
|
|
1098
1164
|
const field = useField(path);
|
|
1165
|
+
const text = useFormText();
|
|
1099
1166
|
const upload = useUploader();
|
|
1100
1167
|
const queue = useUploadQueue(path, upload);
|
|
1101
1168
|
const pending = useSyncExternalStore(queue.subscribe, queue.pending, queue.pending);
|
|
@@ -1122,7 +1189,7 @@ function FileField({ path, label }) {
|
|
|
1122
1189
|
children: [
|
|
1123
1190
|
upload === void 0 ? /* @__PURE__ */ jsx("p", {
|
|
1124
1191
|
"data-formancy-part": "file-unavailable",
|
|
1125
|
-
children: "
|
|
1192
|
+
children: text("file.unavailable")
|
|
1126
1193
|
}) : /* @__PURE__ */ jsx("div", {
|
|
1127
1194
|
"data-formancy-part": "file-dropzone",
|
|
1128
1195
|
...over ? { "data-state": "over" } : {},
|
|
@@ -1166,16 +1233,24 @@ function FileField({ path, label }) {
|
|
|
1166
1233
|
"data-formancy-part": "file-up",
|
|
1167
1234
|
disabled: field.disabled,
|
|
1168
1235
|
onClick: () => field.setValue(moved(files, index, index - 1)),
|
|
1169
|
-
children:
|
|
1236
|
+
children: text("file.up", {
|
|
1237
|
+
name: file.name,
|
|
1238
|
+
position: index + 1,
|
|
1239
|
+
count: files.length
|
|
1240
|
+
})
|
|
1170
1241
|
}) : null,
|
|
1171
1242
|
index < files.length - 1 ? /* @__PURE__ */ jsx("button", {
|
|
1172
1243
|
type: "button",
|
|
1173
1244
|
"data-formancy-part": "file-down",
|
|
1174
1245
|
disabled: field.disabled,
|
|
1175
1246
|
onClick: () => field.setValue(moved(files, index, index + 1)),
|
|
1176
|
-
children:
|
|
1247
|
+
children: text("file.down", {
|
|
1248
|
+
name: file.name,
|
|
1249
|
+
position: index + 1,
|
|
1250
|
+
count: files.length
|
|
1251
|
+
})
|
|
1177
1252
|
}) : null,
|
|
1178
|
-
/* @__PURE__ */
|
|
1253
|
+
/* @__PURE__ */ jsx("button", {
|
|
1179
1254
|
type: "button",
|
|
1180
1255
|
disabled: field.disabled,
|
|
1181
1256
|
onClick: () => {
|
|
@@ -1185,14 +1260,15 @@ function FileField({ path, label }) {
|
|
|
1185
1260
|
}]);
|
|
1186
1261
|
field.setValue(files.filter((other) => other.id !== file.id));
|
|
1187
1262
|
},
|
|
1188
|
-
children:
|
|
1263
|
+
children: text("file.remove", { name: file.name })
|
|
1189
1264
|
})
|
|
1190
1265
|
]
|
|
1191
1266
|
}, file.id)),
|
|
1192
1267
|
pending.map((entry) => /* @__PURE__ */ jsx(PendingRow, {
|
|
1193
1268
|
entry,
|
|
1194
1269
|
queue,
|
|
1195
|
-
disabled: field.disabled
|
|
1270
|
+
disabled: field.disabled,
|
|
1271
|
+
text
|
|
1196
1272
|
}, entry.key)),
|
|
1197
1273
|
removed.map(({ at, file }) => /* @__PURE__ */ jsxs("li", {
|
|
1198
1274
|
"data-formancy-part": "file-item",
|
|
@@ -1200,7 +1276,7 @@ function FileField({ path, label }) {
|
|
|
1200
1276
|
children: [/* @__PURE__ */ jsx("span", {
|
|
1201
1277
|
"data-formancy-part": "file-name",
|
|
1202
1278
|
children: file.name
|
|
1203
|
-
}), /* @__PURE__ */
|
|
1279
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
1204
1280
|
type: "button",
|
|
1205
1281
|
disabled: field.disabled,
|
|
1206
1282
|
onClick: () => {
|
|
@@ -1209,7 +1285,7 @@ function FileField({ path, label }) {
|
|
|
1209
1285
|
next.splice(Math.min(at, next.length), 0, file);
|
|
1210
1286
|
field.setValue(next);
|
|
1211
1287
|
},
|
|
1212
|
-
children:
|
|
1288
|
+
children: text("file.undo", { name: file.name })
|
|
1213
1289
|
})]
|
|
1214
1290
|
}, file.id))
|
|
1215
1291
|
]
|
|
@@ -1217,7 +1293,7 @@ function FileField({ path, label }) {
|
|
|
1217
1293
|
/* @__PURE__ */ jsx("p", {
|
|
1218
1294
|
role: "status",
|
|
1219
1295
|
"data-formancy-part": "file-status",
|
|
1220
|
-
children: statusOf(pending)
|
|
1296
|
+
children: statusOf(pending, text)
|
|
1221
1297
|
})
|
|
1222
1298
|
]
|
|
1223
1299
|
});
|
|
@@ -1229,7 +1305,7 @@ function moved(files, from, to) {
|
|
|
1229
1305
|
return next;
|
|
1230
1306
|
}
|
|
1231
1307
|
/** A file not yet in the answer: waiting, being sent, or refused. */
|
|
1232
|
-
function PendingRow({ entry, queue, disabled }) {
|
|
1308
|
+
function PendingRow({ entry, queue, disabled, text }) {
|
|
1233
1309
|
const failed = entry.state === "failed";
|
|
1234
1310
|
return /* @__PURE__ */ jsxs("li", {
|
|
1235
1311
|
"data-formancy-part": "file-item",
|
|
@@ -1241,46 +1317,51 @@ function PendingRow({ entry, queue, disabled }) {
|
|
|
1241
1317
|
}),
|
|
1242
1318
|
entry.state === "waiting" ? /* @__PURE__ */ jsx("span", {
|
|
1243
1319
|
"data-formancy-part": "file-waiting",
|
|
1244
|
-
children: "
|
|
1320
|
+
children: text("file.waiting")
|
|
1245
1321
|
}) : null,
|
|
1246
1322
|
entry.state === "uploading" ? /* @__PURE__ */ jsx("progress", {
|
|
1247
1323
|
"data-formancy-part": "file-progress",
|
|
1248
|
-
"aria-label":
|
|
1324
|
+
"aria-label": text("file.uploading", { name: entry.name }),
|
|
1249
1325
|
...entry.total === void 0 ? {} : {
|
|
1250
1326
|
value: entry.sent,
|
|
1251
1327
|
max: entry.total
|
|
1252
1328
|
}
|
|
1253
1329
|
}) : null,
|
|
1254
|
-
failed ? /* @__PURE__ */
|
|
1330
|
+
failed ? /* @__PURE__ */ jsx("span", {
|
|
1255
1331
|
"data-formancy-part": "file-error",
|
|
1256
|
-
children:
|
|
1332
|
+
children: text("file.notAttached", { reason: entry.reason ?? "" })
|
|
1257
1333
|
}) : null,
|
|
1258
|
-
failed ? /* @__PURE__ */
|
|
1334
|
+
failed ? /* @__PURE__ */ jsx("button", {
|
|
1259
1335
|
type: "button",
|
|
1260
1336
|
disabled,
|
|
1261
1337
|
onClick: () => queue.retry(entry.key),
|
|
1262
|
-
children:
|
|
1263
|
-
"Try ",
|
|
1264
|
-
entry.name,
|
|
1265
|
-
" again"
|
|
1266
|
-
]
|
|
1338
|
+
children: text("file.retry", { name: entry.name })
|
|
1267
1339
|
}) : null,
|
|
1268
1340
|
/* @__PURE__ */ jsx("button", {
|
|
1269
1341
|
type: "button",
|
|
1270
1342
|
disabled,
|
|
1271
1343
|
onClick: () => queue.cancel(entry.key),
|
|
1272
|
-
children: failed ?
|
|
1344
|
+
children: failed ? text("file.dismiss", { name: entry.name }) : text("file.cancel", { name: entry.name })
|
|
1273
1345
|
})
|
|
1274
1346
|
]
|
|
1275
1347
|
});
|
|
1276
1348
|
}
|
|
1277
1349
|
/** What the status region says: the file being sent, or the files refused, by name. */
|
|
1278
|
-
function statusOf(pending) {
|
|
1350
|
+
function statusOf(pending, text) {
|
|
1279
1351
|
const sending = pending.find((entry) => entry.state === "uploading");
|
|
1280
|
-
if (sending !== void 0) return
|
|
1352
|
+
if (sending !== void 0) return text("file.status.uploading", { name: sending.name });
|
|
1281
1353
|
const failed = pending.filter((entry) => entry.state === "failed");
|
|
1282
|
-
if (failed.length === 1) return
|
|
1283
|
-
|
|
1354
|
+
if (failed.length === 1) return text("file.status.failed", {
|
|
1355
|
+
name: failed[0].name,
|
|
1356
|
+
reason: failed[0].reason ?? ""
|
|
1357
|
+
});
|
|
1358
|
+
if (failed.length > 1) {
|
|
1359
|
+
const names = failed.map((entry) => entry.name);
|
|
1360
|
+
return text("file.status.failedSeveral", {
|
|
1361
|
+
count: failed.length,
|
|
1362
|
+
names
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1284
1365
|
return "";
|
|
1285
1366
|
}
|
|
1286
1367
|
/**
|
|
@@ -1384,30 +1465,26 @@ function FileThumbnail({ source }) {
|
|
|
1384
1465
|
function RichTextToolbar({ disabled, label, onCommand }) {
|
|
1385
1466
|
const buttons = useRef([]);
|
|
1386
1467
|
const [active, setActive] = useState(0);
|
|
1468
|
+
const text = useFormText();
|
|
1387
1469
|
const commands = [
|
|
1388
1470
|
{
|
|
1389
1471
|
command: "strong",
|
|
1390
|
-
name: "Bold",
|
|
1391
1472
|
glyph: "B"
|
|
1392
1473
|
},
|
|
1393
1474
|
{
|
|
1394
1475
|
command: "emphasis",
|
|
1395
|
-
name: "Italic",
|
|
1396
1476
|
glyph: "I"
|
|
1397
1477
|
},
|
|
1398
1478
|
{
|
|
1399
1479
|
command: "link",
|
|
1400
|
-
name: "Link",
|
|
1401
1480
|
glyph: "↗"
|
|
1402
1481
|
},
|
|
1403
1482
|
{
|
|
1404
1483
|
command: "bulletList",
|
|
1405
|
-
name: "Bulleted list",
|
|
1406
1484
|
glyph: "•"
|
|
1407
1485
|
},
|
|
1408
1486
|
{
|
|
1409
1487
|
command: "orderedList",
|
|
1410
|
-
name: "Numbered list",
|
|
1411
1488
|
glyph: "1."
|
|
1412
1489
|
}
|
|
1413
1490
|
];
|
|
@@ -1418,7 +1495,7 @@ function RichTextToolbar({ disabled, label, onCommand }) {
|
|
|
1418
1495
|
};
|
|
1419
1496
|
return /* @__PURE__ */ jsx("div", {
|
|
1420
1497
|
role: "toolbar",
|
|
1421
|
-
"aria-label":
|
|
1498
|
+
"aria-label": text("richtext.toolbar", { label }),
|
|
1422
1499
|
"data-formancy-part": "richtext-toolbar",
|
|
1423
1500
|
onKeyDown: (event) => {
|
|
1424
1501
|
if (event.key === "ArrowRight") {
|
|
@@ -1446,7 +1523,7 @@ function RichTextToolbar({ disabled, label, onCommand }) {
|
|
|
1446
1523
|
onFocus: () => setActive(index),
|
|
1447
1524
|
onClick: () => {
|
|
1448
1525
|
if (entry.command === "link") {
|
|
1449
|
-
const href = window.prompt("
|
|
1526
|
+
const href = window.prompt(text("richtext.linkAddress"));
|
|
1450
1527
|
if (href === null || href === "") return;
|
|
1451
1528
|
onCommand("link", href);
|
|
1452
1529
|
return;
|
|
@@ -1458,7 +1535,7 @@ function RichTextToolbar({ disabled, label, onCommand }) {
|
|
|
1458
1535
|
children: entry.glyph
|
|
1459
1536
|
}), /* @__PURE__ */ jsx("span", {
|
|
1460
1537
|
"data-formancy-part": "visually-hidden",
|
|
1461
|
-
children: entry.
|
|
1538
|
+
children: text(`richtext.${entry.command}`)
|
|
1462
1539
|
})]
|
|
1463
1540
|
}, entry.command))
|
|
1464
1541
|
});
|
|
@@ -1644,6 +1721,7 @@ function SelectBoxesSwitch(props) {
|
|
|
1644
1721
|
*/
|
|
1645
1722
|
function TagPickerField({ path, label }) {
|
|
1646
1723
|
const field = useField(path);
|
|
1724
|
+
const text = useFormText();
|
|
1647
1725
|
const options = useResolvedOptions(field);
|
|
1648
1726
|
const chosen = Array.isArray(field.value) ? field.value.map(String) : [];
|
|
1649
1727
|
const [query, setQuery] = useState("");
|
|
@@ -1651,7 +1729,7 @@ function TagPickerField({ path, label }) {
|
|
|
1651
1729
|
const listboxId = `${field.ids.control}:listbox`;
|
|
1652
1730
|
const remaining = options.filter((option) => !chosen.includes(option.value));
|
|
1653
1731
|
const matches = narrowOptionsByLabel(remaining, query);
|
|
1654
|
-
const expanded = open && matches.length > 0;
|
|
1732
|
+
const expanded = open && !field.disabled && matches.length > 0;
|
|
1655
1733
|
const add = (value) => {
|
|
1656
1734
|
const next = options.map((option) => option.value).filter((candidate) => candidate === value || chosen.includes(candidate));
|
|
1657
1735
|
field.setValue(next);
|
|
@@ -1670,13 +1748,14 @@ function TagPickerField({ path, label }) {
|
|
|
1670
1748
|
"data-formancy-part": "tagpicker",
|
|
1671
1749
|
children: [chosen.length === 0 ? null : /* @__PURE__ */ jsx("ul", {
|
|
1672
1750
|
"data-formancy-part": "tagpicker-chips",
|
|
1673
|
-
"aria-label":
|
|
1751
|
+
"aria-label": text("tagpicker.chosen", { label }),
|
|
1674
1752
|
children: chosen.map((value) => /* @__PURE__ */ jsxs("li", {
|
|
1675
1753
|
"data-formancy-part": "tagpicker-chip",
|
|
1676
1754
|
children: [labelFor(value), /* @__PURE__ */ jsx("button", {
|
|
1677
1755
|
type: "button",
|
|
1678
1756
|
"data-formancy-part": "tagpicker-remove",
|
|
1679
|
-
"aria-label":
|
|
1757
|
+
"aria-label": text("tagpicker.remove", { option: labelFor(value) }),
|
|
1758
|
+
disabled: field.disabled,
|
|
1680
1759
|
onClick: () => remove(value),
|
|
1681
1760
|
children: "×"
|
|
1682
1761
|
})]
|
|
@@ -1717,7 +1796,7 @@ function TagPickerField({ path, label }) {
|
|
|
1717
1796
|
}), /* @__PURE__ */ jsx("ul", {
|
|
1718
1797
|
id: listboxId,
|
|
1719
1798
|
role: "listbox",
|
|
1720
|
-
"aria-label":
|
|
1799
|
+
"aria-label": text("options.suggestions", { label }),
|
|
1721
1800
|
"data-formancy-part": "tagpicker-listbox",
|
|
1722
1801
|
hidden: !expanded,
|
|
1723
1802
|
children: matches.map((option) => /* @__PURE__ */ jsx("li", {
|
|
@@ -1803,6 +1882,7 @@ function SelectBoxesField({ path, label }) {
|
|
|
1803
1882
|
*/
|
|
1804
1883
|
function RankingField({ path, label }) {
|
|
1805
1884
|
const field = useField(path);
|
|
1885
|
+
const text = useFormText();
|
|
1806
1886
|
const options = useResolvedOptions(field);
|
|
1807
1887
|
const showError = field.touched && field.errors.length > 0;
|
|
1808
1888
|
const order = Array.isArray(field.value) ? field.value.map(String) : [];
|
|
@@ -1849,7 +1929,7 @@ function RankingField({ path, label }) {
|
|
|
1849
1929
|
/* @__PURE__ */ jsx(RequiredHint, { field }),
|
|
1850
1930
|
ranked.length === 0 ? null : /* @__PURE__ */ jsx("ol", {
|
|
1851
1931
|
"data-formancy-part": "ranking-order",
|
|
1852
|
-
"aria-label":
|
|
1932
|
+
"aria-label": text("ranking.order", { label }),
|
|
1853
1933
|
children: ranked.map((option, index) => /* @__PURE__ */ jsxs("li", {
|
|
1854
1934
|
"data-formancy-part": "ranking-item",
|
|
1855
1935
|
children: [
|
|
@@ -1861,7 +1941,7 @@ function RankingField({ path, label }) {
|
|
|
1861
1941
|
type: "button",
|
|
1862
1942
|
id: buttonId("up", option.value),
|
|
1863
1943
|
"data-formancy-part": "ranking-up",
|
|
1864
|
-
"aria-label":
|
|
1944
|
+
"aria-label": text("ranking.up", { option: option.label }),
|
|
1865
1945
|
"aria-disabled": index === 0 ? true : void 0,
|
|
1866
1946
|
onClick: () => move(option.value, -1),
|
|
1867
1947
|
onBlur: () => field.touch(),
|
|
@@ -1871,7 +1951,7 @@ function RankingField({ path, label }) {
|
|
|
1871
1951
|
type: "button",
|
|
1872
1952
|
id: buttonId("down", option.value),
|
|
1873
1953
|
"data-formancy-part": "ranking-down",
|
|
1874
|
-
"aria-label":
|
|
1954
|
+
"aria-label": text("ranking.down", { option: option.label }),
|
|
1875
1955
|
"aria-disabled": index === ranked.length - 1 ? true : void 0,
|
|
1876
1956
|
onClick: () => move(option.value, 1),
|
|
1877
1957
|
onBlur: () => field.touch(),
|
|
@@ -1881,7 +1961,7 @@ function RankingField({ path, label }) {
|
|
|
1881
1961
|
type: "button",
|
|
1882
1962
|
id: buttonId("remove", option.value),
|
|
1883
1963
|
"data-formancy-part": "ranking-remove",
|
|
1884
|
-
"aria-label":
|
|
1964
|
+
"aria-label": text("ranking.remove", { option: option.label }),
|
|
1885
1965
|
onClick: () => reorder(order.filter((value) => value !== option.value), buttonId("rank", option.value)),
|
|
1886
1966
|
onBlur: () => field.touch(),
|
|
1887
1967
|
children: "×"
|
|
@@ -1891,14 +1971,14 @@ function RankingField({ path, label }) {
|
|
|
1891
1971
|
}),
|
|
1892
1972
|
unranked.length === 0 ? null : /* @__PURE__ */ jsx("ul", {
|
|
1893
1973
|
"data-formancy-part": "ranking-pool",
|
|
1894
|
-
"aria-label":
|
|
1974
|
+
"aria-label": text("ranking.pool", { label }),
|
|
1895
1975
|
children: unranked.map((option) => /* @__PURE__ */ jsx("li", {
|
|
1896
1976
|
"data-formancy-part": "ranking-candidate",
|
|
1897
1977
|
children: /* @__PURE__ */ jsx("button", {
|
|
1898
1978
|
type: "button",
|
|
1899
1979
|
id: buttonId("rank", option.value),
|
|
1900
1980
|
"data-formancy-part": "ranking-add",
|
|
1901
|
-
"aria-label":
|
|
1981
|
+
"aria-label": text("ranking.rank", { option: option.label }),
|
|
1902
1982
|
onClick: () => rank(option.value),
|
|
1903
1983
|
onBlur: () => field.touch(),
|
|
1904
1984
|
children: option.label
|
|
@@ -2129,6 +2209,7 @@ function NumberFallback({ path, label }) {
|
|
|
2129
2209
|
children: /* @__PURE__ */ jsx("input", {
|
|
2130
2210
|
type: "number",
|
|
2131
2211
|
...field.controlProps,
|
|
2212
|
+
readOnly: field.readOnly,
|
|
2132
2213
|
value: typeof field.value === "number" ? field.value : "",
|
|
2133
2214
|
onChange: (event) => field.setValue(event.target.value === "" ? null : event.target.valueAsNumber),
|
|
2134
2215
|
onBlur: () => field.touch()
|
|
@@ -2166,6 +2247,7 @@ function NumberFallback({ path, label }) {
|
|
|
2166
2247
|
*/
|
|
2167
2248
|
function SignatureField({ path, label }) {
|
|
2168
2249
|
const field = useField(path);
|
|
2250
|
+
const text = useFormText();
|
|
2169
2251
|
const answer = typeof field.value === "object" && field.value !== null ? field.value : {};
|
|
2170
2252
|
const drawn = answer.drawn ?? [];
|
|
2171
2253
|
const [width, height] = field.def?.box ?? [600, 200];
|
|
@@ -2259,7 +2341,7 @@ function SignatureField({ path, label }) {
|
|
|
2259
2341
|
"data-formancy-part": "signature-typed",
|
|
2260
2342
|
type: "text",
|
|
2261
2343
|
value: answer.typed ?? "",
|
|
2262
|
-
"aria-label": "
|
|
2344
|
+
"aria-label": text("signature.typed"),
|
|
2263
2345
|
onChange: (event) => {
|
|
2264
2346
|
const typed = event.target.value;
|
|
2265
2347
|
field.setValue(typed === "" ? null : { typed });
|
|
@@ -2274,7 +2356,7 @@ function SignatureField({ path, label }) {
|
|
|
2274
2356
|
before.current = [];
|
|
2275
2357
|
field.setValue(null);
|
|
2276
2358
|
},
|
|
2277
|
-
children: "
|
|
2359
|
+
children: text("signature.clear")
|
|
2278
2360
|
})
|
|
2279
2361
|
]
|
|
2280
2362
|
})
|
|
@@ -2297,9 +2379,10 @@ function SignatureField({ path, label }) {
|
|
|
2297
2379
|
* heading element either: the spec does not say what level it would be, and
|
|
2298
2380
|
* guessing produces a document outline that skips levels.
|
|
2299
2381
|
*/
|
|
2300
|
-
function StaticField({ label }) {
|
|
2382
|
+
function StaticField({ path, label }) {
|
|
2301
2383
|
return /* @__PURE__ */ jsx("p", {
|
|
2302
2384
|
"data-formancy-part": "static",
|
|
2385
|
+
"data-formancy-field-path": path,
|
|
2303
2386
|
children: label
|
|
2304
2387
|
});
|
|
2305
2388
|
}
|
|
@@ -2320,6 +2403,7 @@ function DateField({ path, label }) {
|
|
|
2320
2403
|
children: /* @__PURE__ */ jsx("input", {
|
|
2321
2404
|
type: "date",
|
|
2322
2405
|
...field.controlProps,
|
|
2406
|
+
readOnly: field.readOnly,
|
|
2323
2407
|
value: typeof field.value === "string" ? field.value : "",
|
|
2324
2408
|
onChange: (event) => field.setValue(event.target.value === "" ? null : event.target.value),
|
|
2325
2409
|
onBlur: () => field.touch()
|
|
@@ -2348,6 +2432,7 @@ function TimeField({ path, label }) {
|
|
|
2348
2432
|
children: /* @__PURE__ */ jsx("input", {
|
|
2349
2433
|
type: "time",
|
|
2350
2434
|
...field.controlProps,
|
|
2435
|
+
readOnly: field.readOnly,
|
|
2351
2436
|
...typeof field.def.earliest === "string" ? { min: field.def.earliest } : {},
|
|
2352
2437
|
...typeof field.def.latest === "string" ? { max: field.def.latest } : {},
|
|
2353
2438
|
value: typeof field.value === "string" ? field.value : "",
|
|
@@ -2394,6 +2479,7 @@ function DateTimeField({ path, label }) {
|
|
|
2394
2479
|
children: /* @__PURE__ */ jsx("input", {
|
|
2395
2480
|
type: "datetime-local",
|
|
2396
2481
|
...field.controlProps,
|
|
2482
|
+
readOnly: field.readOnly,
|
|
2397
2483
|
value: asLocal(),
|
|
2398
2484
|
onChange: (event) => {
|
|
2399
2485
|
const local = event.target.value;
|
|
@@ -2429,8 +2515,12 @@ function DateTimeField({ path, label }) {
|
|
|
2429
2515
|
function TextField({ path, label }) {
|
|
2430
2516
|
const field = useField(path);
|
|
2431
2517
|
const scan = useScanner();
|
|
2518
|
+
const words = useFormText();
|
|
2432
2519
|
const [scanning, setScanning] = useState(false);
|
|
2433
|
-
/**
|
|
2520
|
+
/**
|
|
2521
|
+
* A device failure, held here rather than in the field's errors. See below. What went
|
|
2522
|
+
* wrong rather than a sentence about it, so the sentence is the form's language's.
|
|
2523
|
+
*/
|
|
2434
2524
|
const [trouble, setTrouble] = useState(void 0);
|
|
2435
2525
|
/**
|
|
2436
2526
|
* The ONE place a text field's answer is written, typed or scanned.
|
|
@@ -2455,13 +2545,13 @@ function TextField({ path, label }) {
|
|
|
2455
2545
|
});
|
|
2456
2546
|
if (text === null) return;
|
|
2457
2547
|
if (typeof text !== "string") {
|
|
2458
|
-
setTrouble(
|
|
2548
|
+
setTrouble({});
|
|
2459
2549
|
return;
|
|
2460
2550
|
}
|
|
2461
2551
|
commit(mask === void 0 ? text : answerFromText(mask, text));
|
|
2462
2552
|
field.touch();
|
|
2463
2553
|
} catch (error) {
|
|
2464
|
-
setTrouble(
|
|
2554
|
+
setTrouble({ reason: error instanceof Error ? error.message : String(error) });
|
|
2465
2555
|
} finally {
|
|
2466
2556
|
setScanning(false);
|
|
2467
2557
|
}
|
|
@@ -2473,6 +2563,7 @@ function TextField({ path, label }) {
|
|
|
2473
2563
|
children: [/* @__PURE__ */ jsx("input", {
|
|
2474
2564
|
type: "text",
|
|
2475
2565
|
...field.controlProps,
|
|
2566
|
+
readOnly: field.readOnly,
|
|
2476
2567
|
value: shown,
|
|
2477
2568
|
placeholder: mask === void 0 ? void 0 : maskPlaceholder(mask),
|
|
2478
2569
|
inputMode: mask !== void 0 && maskIsNumeric(mask) ? "numeric" : void 0,
|
|
@@ -2500,14 +2591,18 @@ function TextField({ path, label }) {
|
|
|
2500
2591
|
onClick: () => {
|
|
2501
2592
|
read();
|
|
2502
2593
|
},
|
|
2503
|
-
children: [
|
|
2504
|
-
"
|
|
2505
|
-
|
|
2506
|
-
|
|
2594
|
+
children: [
|
|
2595
|
+
words("scanner.scan"),
|
|
2596
|
+
" ",
|
|
2597
|
+
/* @__PURE__ */ jsx("span", {
|
|
2598
|
+
"data-formancy-part": "visually-hidden",
|
|
2599
|
+
children: label
|
|
2600
|
+
})
|
|
2601
|
+
]
|
|
2507
2602
|
}), /* @__PURE__ */ jsx("p", {
|
|
2508
2603
|
role: "status",
|
|
2509
2604
|
"data-formancy-part": "scanner-status",
|
|
2510
|
-
children: scanning ? "
|
|
2605
|
+
children: scanning ? words("scanner.scanning") : trouble === void 0 ? "" : trouble.reason === void 0 ? words("scanner.noText") : words("scanner.failed", { reason: trouble.reason })
|
|
2511
2606
|
})] }) : null]
|
|
2512
2607
|
});
|
|
2513
2608
|
}
|
|
@@ -2519,6 +2614,7 @@ function TextareaField({ path, label }) {
|
|
|
2519
2614
|
label,
|
|
2520
2615
|
children: /* @__PURE__ */ jsx("textarea", {
|
|
2521
2616
|
...field.controlProps,
|
|
2617
|
+
readOnly: field.readOnly,
|
|
2522
2618
|
value: typeof field.value === "string" ? field.value : "",
|
|
2523
2619
|
onChange: (event) => field.setValue(event.target.value),
|
|
2524
2620
|
onBlur: () => field.touch()
|
|
@@ -2553,6 +2649,7 @@ function FormancyForm(props) {
|
|
|
2553
2649
|
}
|
|
2554
2650
|
function SubmitButton({ submitLabel, onSubmit, onFailedNavigate }) {
|
|
2555
2651
|
const engine = useFormEngine();
|
|
2652
|
+
const text = useFormText();
|
|
2556
2653
|
const submit = useSubmit();
|
|
2557
2654
|
return /* @__PURE__ */ jsx("button", {
|
|
2558
2655
|
type: "button",
|
|
@@ -2572,7 +2669,7 @@ function SubmitButton({ submitLabel, onSubmit, onFailedNavigate }) {
|
|
|
2572
2669
|
errors: outcome.errors
|
|
2573
2670
|
});
|
|
2574
2671
|
},
|
|
2575
|
-
children: submitLabel ?? "
|
|
2672
|
+
children: submitLabel ?? text("form.submit")
|
|
2576
2673
|
});
|
|
2577
2674
|
}
|
|
2578
2675
|
function FlatForm(props) {
|
|
@@ -2580,6 +2677,7 @@ function FlatForm(props) {
|
|
|
2580
2677
|
}
|
|
2581
2678
|
function PagedForm(props) {
|
|
2582
2679
|
const engine = useFormEngine();
|
|
2680
|
+
const text = useFormText();
|
|
2583
2681
|
const wizard = useWizard();
|
|
2584
2682
|
const pages = engine.pages().map((page, index) => ({
|
|
2585
2683
|
...page,
|
|
@@ -2588,7 +2686,7 @@ function PagedForm(props) {
|
|
|
2588
2686
|
return /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
2589
2687
|
/* @__PURE__ */ jsx("nav", {
|
|
2590
2688
|
"data-formancy-part": "stepper",
|
|
2591
|
-
"aria-label": "
|
|
2689
|
+
"aria-label": text("form.progress"),
|
|
2592
2690
|
children: /* @__PURE__ */ jsx("ol", { children: pages.map((page) => /* @__PURE__ */ jsx("li", {
|
|
2593
2691
|
"aria-current": page.index === wizard.page ? "step" : void 0,
|
|
2594
2692
|
children: engine.text(page.def.label) ?? page.key
|
|
@@ -2603,11 +2701,11 @@ function PagedForm(props) {
|
|
|
2603
2701
|
children: [wizard.canGoBack ? /* @__PURE__ */ jsx("button", {
|
|
2604
2702
|
type: "button",
|
|
2605
2703
|
onClick: () => wizard.back(),
|
|
2606
|
-
children: "
|
|
2704
|
+
children: text("form.back")
|
|
2607
2705
|
}) : null, wizard.canGoNext ? /* @__PURE__ */ jsx("button", {
|
|
2608
2706
|
type: "button",
|
|
2609
2707
|
onClick: () => void wizard.next(),
|
|
2610
|
-
children: "
|
|
2708
|
+
children: text("form.next")
|
|
2611
2709
|
}) : /* @__PURE__ */ jsx(SubmitButton, {
|
|
2612
2710
|
...props,
|
|
2613
2711
|
onFailedNavigate: (page) => wizard.goTo(page)
|
|
@@ -2626,13 +2724,20 @@ function FieldList({ labels, registry, page, layout }) {
|
|
|
2626
2724
|
schema,
|
|
2627
2725
|
nodes: arrangement.nodes,
|
|
2628
2726
|
locale: engine.locale(),
|
|
2629
|
-
shows: page === void 0 ? void 0 : (path) => engine
|
|
2727
|
+
shows: page === void 0 ? void 0 : (path) => placedPage(engine, path) === page,
|
|
2630
2728
|
renderField: (path) => {
|
|
2631
2729
|
if (repeaterWires.includes(path)) return /* @__PURE__ */ jsx(RepeaterSection, {
|
|
2632
2730
|
wire: path,
|
|
2633
2731
|
labels,
|
|
2634
2732
|
registry
|
|
2635
2733
|
}, path);
|
|
2734
|
+
const group = placedGroup(engine, path);
|
|
2735
|
+
if (group !== void 0) return /* @__PURE__ */ jsx(GroupSection, {
|
|
2736
|
+
path,
|
|
2737
|
+
group,
|
|
2738
|
+
labels,
|
|
2739
|
+
registry
|
|
2740
|
+
}, path);
|
|
2636
2741
|
return /* @__PURE__ */ jsx(FieldSlot, {
|
|
2637
2742
|
path,
|
|
2638
2743
|
fallbackLabel: labels?.[path],
|
|
@@ -2664,13 +2769,50 @@ function FieldSlot({ path, fallbackLabel, registry }) {
|
|
|
2664
2769
|
label
|
|
2665
2770
|
});
|
|
2666
2771
|
}
|
|
2772
|
+
/**
|
|
2773
|
+
* A group an arrangement places whole: its fields, under its label, the way a labelled
|
|
2774
|
+
* section is drawn — a real group when it has a name, a box when it has none. What it holds
|
|
2775
|
+
* and in which order is `@formancy/core`'s, shared with the Angular renderer (0151).
|
|
2776
|
+
*/
|
|
2777
|
+
function GroupSection({ path, group, labels, registry }) {
|
|
2778
|
+
const engine = useFormEngine();
|
|
2779
|
+
const headingId = useId();
|
|
2780
|
+
const label = resolveText(engine.schema(), group.def.label, engine.locale());
|
|
2781
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2782
|
+
"data-formancy-part": "group",
|
|
2783
|
+
"data-formancy-field-path": path,
|
|
2784
|
+
...label === void 0 ? {} : {
|
|
2785
|
+
role: "group",
|
|
2786
|
+
"aria-labelledby": headingId
|
|
2787
|
+
},
|
|
2788
|
+
children: [
|
|
2789
|
+
label === void 0 ? null : /* @__PURE__ */ jsx("p", {
|
|
2790
|
+
id: headingId,
|
|
2791
|
+
"data-formancy-part": "group-heading",
|
|
2792
|
+
children: label
|
|
2793
|
+
}),
|
|
2794
|
+
group.fields.map((wire) => /* @__PURE__ */ jsx(FieldSlot, {
|
|
2795
|
+
path: wire,
|
|
2796
|
+
fallbackLabel: labels?.[wire],
|
|
2797
|
+
registry
|
|
2798
|
+
}, wire)),
|
|
2799
|
+
group.repeaters.map((wire) => /* @__PURE__ */ jsx(RepeaterSection, {
|
|
2800
|
+
wire,
|
|
2801
|
+
labels,
|
|
2802
|
+
registry
|
|
2803
|
+
}, wire))
|
|
2804
|
+
]
|
|
2805
|
+
});
|
|
2806
|
+
}
|
|
2667
2807
|
function RepeaterSection({ wire, labels, registry }) {
|
|
2668
2808
|
const engine = useFormEngine();
|
|
2809
|
+
const text = useFormText();
|
|
2669
2810
|
const repeater = useRepeater(wire);
|
|
2811
|
+
const field = useField(wire);
|
|
2670
2812
|
const def = engine.repeaters().find((candidate) => candidate.wire === wire)?.def;
|
|
2671
2813
|
const label = engine.text(def?.label) ?? labels?.[wire] ?? wire;
|
|
2672
|
-
const addLabel = def?.addLabel ??
|
|
2673
|
-
const removeLabel = def?.removeLabel ??
|
|
2814
|
+
const addLabel = def?.addLabel ?? text("repeater.add", { label });
|
|
2815
|
+
const removeLabel = def?.removeLabel ?? text("repeater.remove", { label });
|
|
2674
2816
|
const fallbackFor = (instanceWire) => {
|
|
2675
2817
|
const template = instanceWire.replace(/\[\d+\]/, "[]");
|
|
2676
2818
|
return labels?.[template] ?? labels?.[instanceWire];
|
|
@@ -2718,31 +2860,46 @@ function RepeaterSection({ wire, labels, registry }) {
|
|
|
2718
2860
|
* the consumer ([0004](../../../docs/decisions/0004-headless-core.md)), and a renderer
|
|
2719
2861
|
* that shipped a glyph would be choosing one for every design system at once. */
|
|
2720
2862
|
const rowButtons = (index) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
2721
|
-
/* @__PURE__ */ jsx("button", {
|
|
2863
|
+
repeater.canRemove ? /* @__PURE__ */ jsx("button", {
|
|
2722
2864
|
type: "button",
|
|
2723
2865
|
"data-formancy-part": "row-remove",
|
|
2866
|
+
disabled: field.disabled,
|
|
2724
2867
|
onClick: () => repeater.removeRow(index),
|
|
2725
2868
|
children: /* @__PURE__ */ jsx("span", {
|
|
2726
2869
|
"data-formancy-part": "row-action-text",
|
|
2727
|
-
children:
|
|
2870
|
+
children: text("repeater.removeRow", {
|
|
2871
|
+
remove: removeLabel,
|
|
2872
|
+
position: index + 1,
|
|
2873
|
+
count: repeater.rowCount
|
|
2874
|
+
})
|
|
2728
2875
|
})
|
|
2729
|
-
}),
|
|
2876
|
+
}) : null,
|
|
2730
2877
|
index > 0 ? /* @__PURE__ */ jsx("button", {
|
|
2731
2878
|
type: "button",
|
|
2732
2879
|
"data-formancy-part": "row-up",
|
|
2880
|
+
disabled: field.disabled,
|
|
2733
2881
|
onClick: () => repeater.moveRow(index, index - 1),
|
|
2734
2882
|
children: /* @__PURE__ */ jsx("span", {
|
|
2735
2883
|
"data-formancy-part": "row-action-text",
|
|
2736
|
-
children:
|
|
2884
|
+
children: text("repeater.moveUp", {
|
|
2885
|
+
label,
|
|
2886
|
+
position: index + 1,
|
|
2887
|
+
count: repeater.rowCount
|
|
2888
|
+
})
|
|
2737
2889
|
})
|
|
2738
2890
|
}) : null,
|
|
2739
2891
|
index < repeater.rowCount - 1 ? /* @__PURE__ */ jsx("button", {
|
|
2740
2892
|
type: "button",
|
|
2741
2893
|
"data-formancy-part": "row-down",
|
|
2894
|
+
disabled: field.disabled,
|
|
2742
2895
|
onClick: () => repeater.moveRow(index, index + 1),
|
|
2743
2896
|
children: /* @__PURE__ */ jsx("span", {
|
|
2744
2897
|
"data-formancy-part": "row-action-text",
|
|
2745
|
-
children:
|
|
2898
|
+
children: text("repeater.moveDown", {
|
|
2899
|
+
label,
|
|
2900
|
+
position: index + 1,
|
|
2901
|
+
count: repeater.rowCount
|
|
2902
|
+
})
|
|
2746
2903
|
})
|
|
2747
2904
|
}) : null
|
|
2748
2905
|
] });
|
|
@@ -2775,13 +2932,20 @@ function RepeaterSection({ wire, labels, registry }) {
|
|
|
2775
2932
|
"data-formancy-part": "row",
|
|
2776
2933
|
children: [engine.fieldPaths().filter((candidate) => candidate.startsWith(`${wire}[${String(index)}]`)).map(slot), rowButtons(index)]
|
|
2777
2934
|
}, rowId));
|
|
2935
|
+
if (!field.visible) return null;
|
|
2936
|
+
const showError = field.touched && field.errors.length > 0;
|
|
2778
2937
|
return /* @__PURE__ */ jsxs("fieldset", {
|
|
2779
2938
|
"data-formancy-part": "repeater",
|
|
2939
|
+
"data-formancy-field-path": wire,
|
|
2940
|
+
"data-state": showError ? "invalid" : "valid",
|
|
2941
|
+
id: field.controlProps.id,
|
|
2942
|
+
"aria-describedby": field.controlProps["aria-describedby"],
|
|
2780
2943
|
children: [
|
|
2781
2944
|
/* @__PURE__ */ jsx("legend", {
|
|
2782
2945
|
"data-formancy-part": "repeater-legend",
|
|
2783
2946
|
children: label
|
|
2784
2947
|
}),
|
|
2948
|
+
/* @__PURE__ */ jsx(RequiredHint, { field }),
|
|
2785
2949
|
grid ? /* @__PURE__ */ jsxs("div", {
|
|
2786
2950
|
"data-formancy-part": "datagrid",
|
|
2787
2951
|
"data-columns": String(plan.length),
|
|
@@ -2795,11 +2959,17 @@ function RepeaterSection({ wire, labels, registry }) {
|
|
|
2795
2959
|
}, entry.key))
|
|
2796
2960
|
}) : null, rows]
|
|
2797
2961
|
}) : rows,
|
|
2798
|
-
/* @__PURE__ */ jsx("
|
|
2962
|
+
showError ? /* @__PURE__ */ jsx("p", {
|
|
2963
|
+
"data-formancy-part": "error",
|
|
2964
|
+
...field.errorProps,
|
|
2965
|
+
children: field.errors.join(", ")
|
|
2966
|
+
}) : null,
|
|
2967
|
+
repeater.canAdd ? /* @__PURE__ */ jsx("button", {
|
|
2799
2968
|
type: "button",
|
|
2969
|
+
disabled: field.disabled,
|
|
2800
2970
|
onClick: () => repeater.addRow(),
|
|
2801
2971
|
children: addLabel
|
|
2802
|
-
})
|
|
2972
|
+
}) : null
|
|
2803
2973
|
]
|
|
2804
2974
|
});
|
|
2805
2975
|
}
|
|
@@ -2844,6 +3014,7 @@ const DEFAULT_COMPONENTS = {
|
|
|
2844
3014
|
*/
|
|
2845
3015
|
function ErrorSummary({ labels }) {
|
|
2846
3016
|
const engine = useFormEngine();
|
|
3017
|
+
const text = useFormText();
|
|
2847
3018
|
const subscribe = useCallback((onChange) => engine.subscribe(onChange), [engine]);
|
|
2848
3019
|
const getErrors = useCallback(() => engine.visibleErrors(), [engine]);
|
|
2849
3020
|
const errors = useSyncExternalStore(subscribe, getErrors, getErrors);
|
|
@@ -2861,7 +3032,7 @@ function ErrorSummary({ labels }) {
|
|
|
2861
3032
|
ref: region,
|
|
2862
3033
|
children: [/* @__PURE__ */ jsx("h2", {
|
|
2863
3034
|
"data-formancy-part": "error-summary-heading",
|
|
2864
|
-
children: errors.
|
|
3035
|
+
children: text("errors.heading", { count: errors.length })
|
|
2865
3036
|
}), /* @__PURE__ */ jsx("ul", { children: errors.map(({ path, codes }) => {
|
|
2866
3037
|
const controlId = engine.getFieldSnapshot(parsePath(path)).ids.control;
|
|
2867
3038
|
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("a", {
|
|
@@ -2870,7 +3041,10 @@ function ErrorSummary({ labels }) {
|
|
|
2870
3041
|
event.preventDefault();
|
|
2871
3042
|
focusControl(document.getElementById(controlId));
|
|
2872
3043
|
},
|
|
2873
|
-
children:
|
|
3044
|
+
children: text("errors.entry", {
|
|
3045
|
+
label: labelFor(path),
|
|
3046
|
+
codes: codes.join(", ")
|
|
3047
|
+
})
|
|
2874
3048
|
}) }, path);
|
|
2875
3049
|
}) })]
|
|
2876
3050
|
});
|
|
@@ -2879,6 +3053,7 @@ function ErrorSummary({ labels }) {
|
|
|
2879
3053
|
//#region src/resume-notice.tsx
|
|
2880
3054
|
function ResumeNotice({ migration, labels }) {
|
|
2881
3055
|
const region = useRef(null);
|
|
3056
|
+
const text = useFormText();
|
|
2882
3057
|
useEffect(() => {
|
|
2883
3058
|
if (migration !== void 0) region.current?.focus();
|
|
2884
3059
|
}, [migration]);
|
|
@@ -2886,25 +3061,27 @@ function ResumeNotice({ migration, labels }) {
|
|
|
2886
3061
|
const setAside = migration.changes.map((change) => change.path).filter((path) => path !== void 0);
|
|
2887
3062
|
return /* @__PURE__ */ jsxs("div", {
|
|
2888
3063
|
role: "region",
|
|
2889
|
-
"aria-label": "
|
|
3064
|
+
"aria-label": text("resume.heading"),
|
|
2890
3065
|
"data-formancy-part": "resume-notice",
|
|
2891
3066
|
"data-state": migration.severity,
|
|
2892
3067
|
tabIndex: -1,
|
|
2893
3068
|
ref: region,
|
|
2894
3069
|
children: [/* @__PURE__ */ jsx("h2", {
|
|
2895
3070
|
"data-formancy-part": "resume-notice-heading",
|
|
2896
|
-
children: "
|
|
3071
|
+
children: text("resume.heading")
|
|
2897
3072
|
}), migration.severity === "breaking" ? /* @__PURE__ */ jsxs("p", { children: [
|
|
2898
|
-
"
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
3073
|
+
text("resume.breaking.kept"),
|
|
3074
|
+
" ",
|
|
3075
|
+
/* @__PURE__ */ jsx("strong", { children: text("resume.breaking.cannotSubmit") }),
|
|
3076
|
+
" ",
|
|
3077
|
+
text("resume.breaking.restart")
|
|
3078
|
+
] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("p", { children: text("resume.setAside", { count: setAside.length }) }), setAside.length === 0 ? null : /* @__PURE__ */ jsx("ul", {
|
|
2902
3079
|
"data-formancy-part": "resume-notice-list",
|
|
2903
3080
|
children: setAside.map((path) => /* @__PURE__ */ jsx("li", { children: labels?.[path] ?? path }, path))
|
|
2904
3081
|
})] })]
|
|
2905
3082
|
});
|
|
2906
3083
|
}
|
|
2907
3084
|
//#endregion
|
|
2908
|
-
export { ErrorSummary, FormancyForm, FormancyProvider, OptionsSourcesProvider, ResumeNotice, RichText, RichTextEditorProvider, ScannerProvider, UploaderProvider, useField, useFormEngine, useOptionsSources, useRepeater, useRichTextEditorFactory, useScanner, useSubmit, useUploader, useWizard };
|
|
3085
|
+
export { ErrorSummary, FormancyForm, FormancyProvider, OptionsSourcesProvider, ResumeNotice, RichText, RichTextEditorProvider, ScannerProvider, UploaderProvider, useField, useFormEngine, useFormText, useOptionsSources, useRepeater, useRichTextEditorFactory, useScanner, useSubmit, useUploader, useWizard };
|
|
2909
3086
|
|
|
2910
3087
|
//# sourceMappingURL=index.mjs.map
|