@usefillo/core 0.12.1 → 0.14.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 +118 -3
- package/dist/index.js +366 -6
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ interface JumpRule {
|
|
|
20
20
|
/** Target page id, or "end" to finish the form. */
|
|
21
21
|
to: string | "end";
|
|
22
22
|
}
|
|
23
|
-
type FieldKind = "short_text" | "long_text" | "email" | "url" | "phone" | "number" | "select" | "multi_select" | "dropdown" | "checkbox" | "rating" | "linear_scale" | "ranking" | "matrix" | "signature" | "date" | "file_upload" | "hidden" | "calculated" | "custom";
|
|
23
|
+
type FieldKind = "short_text" | "long_text" | "email" | "url" | "phone" | "number" | "select" | "multi_select" | "dropdown" | "checkbox" | "rating" | "linear_scale" | "ranking" | "matrix" | "signature" | "date" | "file_upload" | "hidden" | "calculated" | "repeating_group" | "custom";
|
|
24
24
|
type ContentKind = "heading" | "paragraph" | "divider";
|
|
25
25
|
type BlockKind = FieldKind | ContentKind;
|
|
26
26
|
interface SelectOption {
|
|
@@ -198,6 +198,53 @@ interface CalculatedField extends BaseField {
|
|
|
198
198
|
prefix?: string;
|
|
199
199
|
suffix?: string;
|
|
200
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* One repeated instance's answers: childId → that child's canonical value,
|
|
203
|
+
* the exact same shapes a top-level field would carry (trimmed strings,
|
|
204
|
+
* wire-format numerics coerced, empty answers absent — see canonical.ts). A
|
|
205
|
+
* key absent from the record means that child is unanswered in this instance,
|
|
206
|
+
* mirroring how a key absent from {@link ResponseData} means a top-level field
|
|
207
|
+
* is unanswered.
|
|
208
|
+
*/
|
|
209
|
+
type GroupInstanceValue = Record<string, FieldValue>;
|
|
210
|
+
/**
|
|
211
|
+
* A repeated block of fields (logic depth P3) — "one row per guest/item",
|
|
212
|
+
* respondent-controlled. Bounded by construction: `maxInstances` is required
|
|
213
|
+
* (1–20); `minInstances` clamps to 0..maxInstances (default 1). The template
|
|
214
|
+
* (`fields`) is capped at 12 children, drawn from a restricted v1 allowlist
|
|
215
|
+
* (see schema-validation's GROUP_CHILD_ALLOWED_KINDS) — notably NOT
|
|
216
|
+
* `repeating_group` itself (nesting is out of scope for v1) and NOT
|
|
217
|
+
* `calculated` (no per-instance calc plumbing yet). Child ids are unique only
|
|
218
|
+
* WITHIN the group (the select-option precedent, not the global block
|
|
219
|
+
* namespace), so the same child id can recur across two different groups.
|
|
220
|
+
*
|
|
221
|
+
* Value shape (locked, roadmap decision 4):
|
|
222
|
+
* `data[groupId]: GroupInstanceValue[]`, instances in respondent order. Key
|
|
223
|
+
* absent ⇔ zero instances (unanswered iff `minInstances` is 0).
|
|
224
|
+
*
|
|
225
|
+
* Scope walls (v1, strict): a child's `visibleIf` may reference ONLY
|
|
226
|
+
* same-group sibling ids, evaluated per instance over that instance's
|
|
227
|
+
* canonical values (see logic.ts's `visibleGroupChildren`). Nothing outside
|
|
228
|
+
* the group — top-level visibility, jumps, calc operands, piping — can
|
|
229
|
+
* reference a child id; both directions are schema hard errors.
|
|
230
|
+
*/
|
|
231
|
+
interface RepeatingGroupField extends BaseField {
|
|
232
|
+
kind: "repeating_group";
|
|
233
|
+
/** The repeated template. Child ids are unique WITHIN the group (the
|
|
234
|
+
* select-option precedent, not the global block namespace). */
|
|
235
|
+
fields: Field[];
|
|
236
|
+
/** Respondent-controlled instance count, clamped server-side. 0..maxInstances,
|
|
237
|
+
* default 1. */
|
|
238
|
+
minInstances?: number;
|
|
239
|
+
/** REQUIRED, 1..20 — bounded by design (also bounds the authoring-time
|
|
240
|
+
* worst-case payload-size estimate). */
|
|
241
|
+
maxInstances: number;
|
|
242
|
+
/** "Add another" default via strings. */
|
|
243
|
+
addLabel?: string;
|
|
244
|
+
/** Names one instance — "Guest" → headings "Guest 2 of 3", announcements,
|
|
245
|
+
* CSV column prefixes. Defaults to the field label. */
|
|
246
|
+
itemLabel?: string;
|
|
247
|
+
}
|
|
201
248
|
/**
|
|
202
249
|
* A field type Fillo doesn't ship. You define it in code and supply the
|
|
203
250
|
* renderer via the SDK's `customComponents` map, keyed by `component`. The
|
|
@@ -212,7 +259,7 @@ interface CustomField extends BaseField {
|
|
|
212
259
|
/** Arbitrary options handed to your component. */
|
|
213
260
|
config?: Record<string, unknown>;
|
|
214
261
|
}
|
|
215
|
-
type Field = TextField | PhoneField | NumberField | ChoiceField | CheckboxField | RatingField | LinearScaleField | RankingField | MatrixField | SignatureField | DateField | FileUploadField | HiddenField | CalculatedField | CustomField;
|
|
262
|
+
type Field = TextField | PhoneField | NumberField | ChoiceField | CheckboxField | RatingField | LinearScaleField | RankingField | MatrixField | SignatureField | DateField | FileUploadField | HiddenField | CalculatedField | RepeatingGroupField | CustomField;
|
|
216
263
|
interface HeadingBlock extends BaseBlock {
|
|
217
264
|
kind: "heading";
|
|
218
265
|
text: string;
|
|
@@ -389,6 +436,10 @@ type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
|
389
436
|
type FieldValue = string | number | boolean | string[] | FileValue[]
|
|
390
437
|
/** Matrix answers: rowId → columnId. */
|
|
391
438
|
| Record<string, string>
|
|
439
|
+
/** Repeating-group answers: one record per instance, in respondent order
|
|
440
|
+
* (roadmap decision 4). A NAMED member — never smuggled through
|
|
441
|
+
* JsonValue — so the discriminated union stays exhaustiveness-checkable. */
|
|
442
|
+
| GroupInstanceValue[]
|
|
392
443
|
/** Custom fields hold arbitrary JSON. */
|
|
393
444
|
| JsonValue | null | undefined;
|
|
394
445
|
type ResponseData = Record<string, FieldValue>;
|
|
@@ -491,6 +542,27 @@ declare function allFields(form: FormSchema): Field[];
|
|
|
491
542
|
declare function responseScopeValue(settings: FormSettings, data: ResponseData): string | null;
|
|
492
543
|
/** Fields currently visible given the response data. */
|
|
493
544
|
declare function visibleFields(form: FormSchema, data: ResponseData): Field[];
|
|
545
|
+
/**
|
|
546
|
+
* THE scoped per-instance visibility resolver for a repeating group's
|
|
547
|
+
* children (contract decisions 3 + 4): given ONE instance's values, the
|
|
548
|
+
* template children currently visible in that instance, in template order.
|
|
549
|
+
* Both renderers (which rows to draw in instance n) and validateResponse
|
|
550
|
+
* (which children to require/keep in instance n) consume THIS one helper —
|
|
551
|
+
* no second engine. It literally IS the shared engine: the children run
|
|
552
|
+
* through the same {@link resolveLogicState} fixpoint a page of top-level
|
|
553
|
+
* fields would, over the instance's values — so canonical-value discipline
|
|
554
|
+
* (trim/coerce/empty-drop before any condition reads a sibling) and
|
|
555
|
+
* hidden-sibling cascading (a hidden controller reads as unanswered) behave
|
|
556
|
+
* exactly like top level, one scope down. Scope walls are enforced at schema
|
|
557
|
+
* time: a child's visibleIf can only name same-group siblings, so the
|
|
558
|
+
* instance's own values are the complete condition universe — outer answers
|
|
559
|
+
* can never matter here, and child values never leak out (the group is one
|
|
560
|
+
* opaque field in the top-level fixpoint). No calc pass runs because
|
|
561
|
+
* `calculated` children are schema-rejected (decision 2). A logic-hidden
|
|
562
|
+
* child is unanswered for its siblings AND skipped by per-instance
|
|
563
|
+
* validation, mirroring the top-level discipline.
|
|
564
|
+
*/
|
|
565
|
+
declare function visibleGroupChildren(group: RepeatingGroupField, instanceValues: GroupInstanceValue): Field[];
|
|
494
566
|
/** Where navigation goes when leaving a page. */
|
|
495
567
|
type NextPage = {
|
|
496
568
|
to: string;
|
|
@@ -637,6 +709,16 @@ declare const FILLO_CHALLENGE_MIN_SDK_VERSION = "0.9.0";
|
|
|
637
709
|
* applicable floors).
|
|
638
710
|
*/
|
|
639
711
|
declare const FILLO_CALC_MIN_SDK_VERSION = "0.11.0";
|
|
712
|
+
/**
|
|
713
|
+
* The floor served INSTEAD of FILLO_MIN_SDK_VERSION for forms whose live
|
|
714
|
+
* schema contains a repeating_group field: the first release that ships the
|
|
715
|
+
* kind. Stricter than the calc case (recon risk 5): an old SDK's zod enum
|
|
716
|
+
* doesn't just strip one row, it drops the WHOLE templated section silently —
|
|
717
|
+
* worse than a blank row, because there is no visual trace a question ever
|
|
718
|
+
* existed. Non-negotiable; the form GET picks the max of the applicable
|
|
719
|
+
* floors (the Turnstile/calc precedent).
|
|
720
|
+
*/
|
|
721
|
+
declare const FILLO_GROUP_MIN_SDK_VERSION = "0.13.0";
|
|
640
722
|
declare function normalizeSettings(value: unknown): FormSettings;
|
|
641
723
|
interface SchemaValidationResult {
|
|
642
724
|
ok: boolean;
|
|
@@ -899,6 +981,14 @@ declare function createEmptyForm(title?: string): FormSchema;
|
|
|
899
981
|
* first two are code-only concerns and signature is too niche to draft well)
|
|
900
982
|
* and calculated (an LLM can't reliably wire a calc AST to stable field ids —
|
|
901
983
|
* owners add calculations deliberately, in the builder or in code).
|
|
984
|
+
* Also excludes repeating_group (contract decision 11): the flat FieldSpec →
|
|
985
|
+
* createBlock-defaults assembly has no nested-template channel, and a group
|
|
986
|
+
* needs structural choices an LLM gets wrong in ways the assembler can't
|
|
987
|
+
* repair — per-group-unique child ids, the v1 child-kind allowlist, and a
|
|
988
|
+
* deliberate maxInstances bound (required, 1–20). A malformed group is a
|
|
989
|
+
* schema HARD error, so a hallucinated one would brick the whole draft rather
|
|
990
|
+
* than degrade a field. Owners add groups deliberately, in the builder or in
|
|
991
|
+
* code — the calculated precedent.
|
|
902
992
|
*/
|
|
903
993
|
declare const DRAFT_KINDS: readonly BlockKind[];
|
|
904
994
|
/** The LLM-friendly intermediate shape. Everything optional but `kind`. */
|
|
@@ -1028,6 +1118,16 @@ interface SubmitMeta {
|
|
|
1028
1118
|
elapsedMs?: number;
|
|
1029
1119
|
/** Embedding surface, recorded per response for measurement — never gated. */
|
|
1030
1120
|
surface?: "default" | "headless";
|
|
1121
|
+
/**
|
|
1122
|
+
* Browser page context used by connected analytics/CRM destinations. Fillo
|
|
1123
|
+
* reads an existing HubSpot tracking cookie when present but never creates
|
|
1124
|
+
* one or loads tracking code.
|
|
1125
|
+
*/
|
|
1126
|
+
attribution?: {
|
|
1127
|
+
pageUri?: string;
|
|
1128
|
+
pageName?: string;
|
|
1129
|
+
hubspotutk?: string;
|
|
1130
|
+
};
|
|
1031
1131
|
/**
|
|
1032
1132
|
* Browser-scoped de-duplication key sent when a form opts into
|
|
1033
1133
|
* settings.responseLimit.by = "browser".
|
|
@@ -1814,6 +1914,16 @@ interface FilloFieldStrings {
|
|
|
1814
1914
|
errorSummaryTitle: string;
|
|
1815
1915
|
/** Live-region announcement after a ranking move: "«label», position n of m". */
|
|
1816
1916
|
rankingPosition: (label: string, position: number, count: number) => string;
|
|
1917
|
+
/** Repeating group: the Add button's default label. */
|
|
1918
|
+
groupAdd: string;
|
|
1919
|
+
/** Repeating group instance heading — "«item» n of m". */
|
|
1920
|
+
groupInstanceLabel: (item: string, position: number, count: number) => string;
|
|
1921
|
+
/** Repeating group per-instance Remove button accessible label. */
|
|
1922
|
+
groupRemoveLabel: (item: string, position: number) => string;
|
|
1923
|
+
/** Live-region announcement after adding an instance. */
|
|
1924
|
+
groupInstanceAdded: (item: string, position: number, count: number) => string;
|
|
1925
|
+
/** Live-region announcement after removing an instance. */
|
|
1926
|
+
groupInstanceRemoved: (item: string, count: number) => string;
|
|
1817
1927
|
/** Live-region announcement once a phone country-picker selection commits
|
|
1818
1928
|
* (focus moves straight to the national input, so nothing else announces it). */
|
|
1819
1929
|
phoneCountrySelected: (name: string) => string;
|
|
@@ -1954,6 +2064,11 @@ interface BlockSpec {
|
|
|
1954
2064
|
optionChildren?: boolean;
|
|
1955
2065
|
/** String children become the `text` prop (heading/paragraph). */
|
|
1956
2066
|
textChildren?: boolean;
|
|
2067
|
+
/** Accepts Fillo FIELD elements as children, compiled into the `fields`
|
|
2068
|
+
* template (repeating groups). The walk's flatten/classify/buildProps trio
|
|
2069
|
+
* is reentrant, so children build through the exact same path as top-level
|
|
2070
|
+
* blocks. Child ids are per-GROUP (checked here), not global. */
|
|
2071
|
+
blockChildren?: boolean;
|
|
1957
2072
|
/** Content blocks carry no label/required/etc. */
|
|
1958
2073
|
content?: boolean;
|
|
1959
2074
|
}
|
|
@@ -2074,4 +2189,4 @@ declare class Sha1 {
|
|
|
2074
2189
|
}
|
|
2075
2190
|
declare const sha1Base64: (bytes: Uint8Array) => string;
|
|
2076
2191
|
|
|
2077
|
-
export { type AutoSubmitContext, BLOCK_KIND_META, type BaseField, type Block, type BlockKind, CONTENT_KINDS, type CalcExpr, type CalculatedField, type ChallengeConfig, type CheckboxField, type ChoiceField, type CodeForm, type Condition, type ConditionOp, type ContentBlock, type ContentKind, type CreatedDraft, type CustomField, DEFAULT_FIELD_STRINGS, DEFAULT_STRINGS, DRAFT_KINDS, type DateField, type DividerBlock, FILLO_CALC_MIN_SDK_VERSION, FILLO_CHALLENGE_MIN_SDK_VERSION, FILLO_DATA_ATTRS, FILLO_MIN_SDK_VERSION, FILLO_SCHEMA_VERSION, FILLO_SDK_VERSION, FILLO_SLOTS, FILLO_THEME_VARS, type Field, type FieldKind, type FieldSpec, type FieldValue, type FileUploadField, type FileValue, type FilloAppearance, FilloClient, type FilloClientOptions, FilloError, type FilloFieldStrings, FilloJsxError, type FilloRendererStrings, type FilloRespondent, type FilloSlot, type FilloStrings, type FormBranding, type FormController, type FormControllerOptions, type FormControllerState, type FormDraftSpec, type FormPage, type FormSchema, type FormSettings, type FormStatus, type FormTheme, type HeadingBlock, type HiddenField, JSX_BLOCK_COMPONENTS, JSX_BLOCK_SPECS, type JsonValue, type JsxFormMeta, type JumpRule, type LinearScaleField, type MatrixField, type NextPage, type NumberField, PHONE_COUNTRIES, PHONE_PICKER_COUNTRIES, PHONE_POPOVER_VIEWPORT_GAP, type ParagraphBlock, type ParsePhoneOptions, type ParsedPhone, type PhoneCountry, type PhoneField, type PhonePopoverPlacement, type ProvisionWorkspaceResult, type PublishedForm, REQUIRED_FIELD_MESSAGE, type RankingField, type RatingField, type ResponseData, type ResponseDraft, type ResponseLimit, type SchemaValidationResult, type SelectOption, Sha1, type SignatureField, type SlotClass, type SlotState, type SubmitMeta, type SubmitResult, type SyncFormResult, type SyncedForm, type TextField, type TrustPolicy, type UploadFileOptions, type UploadProgress, type UploadSession, type UploadStatus, type UploadTransport, type ValidationResult, type WhenBuilder, allFields, assembleForm, codeFormFromJsx, computeCalculated, conditionsMet, contentHash, countryByDialCode, countryByIso, countryByTimeZone, createBlock, createClient, createEmptyForm, createFormController, createId, defineForm, digitsOnly, evaluateCalc, flagEmoji, formSchemasEqual, formatAnswer, formatGroupedNumber, formatNational, isAutoSubmitBlock, isBlockVisible, isBuildTimeDevEnv, isCodeForm, isField, isFilloError, isLikelyDevEnv, isPossiblePhone, isTerminalPage, isValidPartialNumberText, localeForNotation, needsExplicitSubmit, normalizeFormSchema, normalizeFormTheme, normalizeSettings, parseGroupedNumber, parsePhone, pipeBlock, positionPhonePopover, prefillFromParams, provisionWorkspace, radioGroupStep, reachableFieldIds, reachableFields, reachablePageIds, reachablePageSequence, requiredFieldMessage, resolveNextPage, resolveSlotClass, resolveStrings, resolveText, resolveThemeAppearance, responseScopeValue, schemaFromJsx, sha1Base64, shouldAutoSubmit, slotClass, syncCodeForm, toE164, validateField, validateFormSchema, validateResponse, visibleBlocks, visibleFields, visiblePageBlocks, when };
|
|
2192
|
+
export { type AutoSubmitContext, BLOCK_KIND_META, type BaseField, type Block, type BlockKind, CONTENT_KINDS, type CalcExpr, type CalculatedField, type ChallengeConfig, type CheckboxField, type ChoiceField, type CodeForm, type Condition, type ConditionOp, type ContentBlock, type ContentKind, type CreatedDraft, type CustomField, DEFAULT_FIELD_STRINGS, DEFAULT_STRINGS, DRAFT_KINDS, type DateField, type DividerBlock, FILLO_CALC_MIN_SDK_VERSION, FILLO_CHALLENGE_MIN_SDK_VERSION, FILLO_DATA_ATTRS, FILLO_GROUP_MIN_SDK_VERSION, FILLO_MIN_SDK_VERSION, FILLO_SCHEMA_VERSION, FILLO_SDK_VERSION, FILLO_SLOTS, FILLO_THEME_VARS, type Field, type FieldKind, type FieldSpec, type FieldValue, type FileUploadField, type FileValue, type FilloAppearance, FilloClient, type FilloClientOptions, FilloError, type FilloFieldStrings, FilloJsxError, type FilloRendererStrings, type FilloRespondent, type FilloSlot, type FilloStrings, type FormBranding, type FormController, type FormControllerOptions, type FormControllerState, type FormDraftSpec, type FormPage, type FormSchema, type FormSettings, type FormStatus, type FormTheme, type GroupInstanceValue, type HeadingBlock, type HiddenField, JSX_BLOCK_COMPONENTS, JSX_BLOCK_SPECS, type JsonValue, type JsxFormMeta, type JumpRule, type LinearScaleField, type MatrixField, type NextPage, type NumberField, PHONE_COUNTRIES, PHONE_PICKER_COUNTRIES, PHONE_POPOVER_VIEWPORT_GAP, type ParagraphBlock, type ParsePhoneOptions, type ParsedPhone, type PhoneCountry, type PhoneField, type PhonePopoverPlacement, type ProvisionWorkspaceResult, type PublishedForm, REQUIRED_FIELD_MESSAGE, type RankingField, type RatingField, type RepeatingGroupField, type ResponseData, type ResponseDraft, type ResponseLimit, type SchemaValidationResult, type SelectOption, Sha1, type SignatureField, type SlotClass, type SlotState, type SubmitMeta, type SubmitResult, type SyncFormResult, type SyncedForm, type TextField, type TrustPolicy, type UploadFileOptions, type UploadProgress, type UploadSession, type UploadStatus, type UploadTransport, type ValidationResult, type WhenBuilder, allFields, assembleForm, codeFormFromJsx, computeCalculated, conditionsMet, contentHash, countryByDialCode, countryByIso, countryByTimeZone, createBlock, createClient, createEmptyForm, createFormController, createId, defineForm, digitsOnly, evaluateCalc, flagEmoji, formSchemasEqual, formatAnswer, formatGroupedNumber, formatNational, isAutoSubmitBlock, isBlockVisible, isBuildTimeDevEnv, isCodeForm, isField, isFilloError, isLikelyDevEnv, isPossiblePhone, isTerminalPage, isValidPartialNumberText, localeForNotation, needsExplicitSubmit, normalizeFormSchema, normalizeFormTheme, normalizeSettings, parseGroupedNumber, parsePhone, pipeBlock, positionPhonePopover, prefillFromParams, provisionWorkspace, radioGroupStep, reachableFieldIds, reachableFields, reachablePageIds, reachablePageSequence, requiredFieldMessage, resolveNextPage, resolveSlotClass, resolveStrings, resolveText, resolveThemeAppearance, responseScopeValue, schemaFromJsx, sha1Base64, shouldAutoSubmit, slotClass, syncCodeForm, toE164, validateField, validateFormSchema, validateResponse, visibleBlocks, visibleFields, visibleGroupChildren, visiblePageBlocks, when };
|
package/dist/index.js
CHANGED
|
@@ -13,9 +13,38 @@ function isEmpty(value) {
|
|
|
13
13
|
if (typeof value === "object") return Object.keys(value).length === 0;
|
|
14
14
|
return false;
|
|
15
15
|
}
|
|
16
|
+
function normalizeGroupValue(field2, value) {
|
|
17
|
+
if (!Array.isArray(value)) return void 0;
|
|
18
|
+
const childById = new Map(field2.fields.map((child) => [child.id, child]));
|
|
19
|
+
let changed = false;
|
|
20
|
+
const instances = value.map((instance) => {
|
|
21
|
+
if (!instance || typeof instance !== "object" || Array.isArray(instance)) return instance;
|
|
22
|
+
let instanceChanged = false;
|
|
23
|
+
const out = {};
|
|
24
|
+
for (const [key, raw] of Object.entries(instance)) {
|
|
25
|
+
const child = childById.get(key);
|
|
26
|
+
if (!child) {
|
|
27
|
+
instanceChanged = true;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const canonical = canonicalValue(child, raw);
|
|
31
|
+
if (canonical === void 0) {
|
|
32
|
+
instanceChanged = true;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
out[key] = canonical;
|
|
36
|
+
if (canonical !== raw) instanceChanged = true;
|
|
37
|
+
}
|
|
38
|
+
if (!instanceChanged) return instance;
|
|
39
|
+
changed = true;
|
|
40
|
+
return out;
|
|
41
|
+
});
|
|
42
|
+
return changed ? instances : value;
|
|
43
|
+
}
|
|
16
44
|
function normalizeValue(field2, value) {
|
|
17
45
|
if (value === void 0 || value === null) return value;
|
|
18
46
|
if (field2.kind === "custom") return value;
|
|
47
|
+
if (field2.kind === "repeating_group") return normalizeGroupValue(field2, value);
|
|
19
48
|
if (field2.kind === "number" && typeof value === "string" && value.trim() !== "") {
|
|
20
49
|
const n = Number(value);
|
|
21
50
|
return Number.isNaN(n) ? value : n;
|
|
@@ -190,7 +219,12 @@ function resolveLogicState(fields, data) {
|
|
|
190
219
|
for (const f of fields) {
|
|
191
220
|
if (f.kind === "calculated") continue;
|
|
192
221
|
const value = canonicalValue(f, data[f.id]);
|
|
193
|
-
if (value
|
|
222
|
+
if (value === void 0) continue;
|
|
223
|
+
if (f.kind === "repeating_group") {
|
|
224
|
+
const count = Array.isArray(value) ? value.length : 0;
|
|
225
|
+
if (count < Math.max(1, f.minInstances ?? 1)) continue;
|
|
226
|
+
}
|
|
227
|
+
canonical[f.id] = value;
|
|
194
228
|
}
|
|
195
229
|
let visible = scoped;
|
|
196
230
|
let calc = {};
|
|
@@ -257,6 +291,11 @@ function visibleFields(form, data) {
|
|
|
257
291
|
const visible = visibleFieldIds(fields, data);
|
|
258
292
|
return fields.filter((f) => visible.has(f.id));
|
|
259
293
|
}
|
|
294
|
+
function visibleGroupChildren(group, instanceValues) {
|
|
295
|
+
const values = instanceValues && typeof instanceValues === "object" && !Array.isArray(instanceValues) ? instanceValues : {};
|
|
296
|
+
const state = resolveLogicState(group.fields, values);
|
|
297
|
+
return group.fields.filter((f) => state.visible.has(f.id));
|
|
298
|
+
}
|
|
260
299
|
function formResolver(form, data) {
|
|
261
300
|
return resolveLogicState(allFields(form), data).resolve;
|
|
262
301
|
}
|
|
@@ -754,6 +793,11 @@ var DEFAULT_FIELD_STRINGS = {
|
|
|
754
793
|
submittingAnnouncement: "Submitting your response\u2026",
|
|
755
794
|
errorSummaryTitle: "Check these fields",
|
|
756
795
|
rankingPosition: (label, position, count) => `${label}, position ${position} of ${count}`,
|
|
796
|
+
groupAdd: "Add another",
|
|
797
|
+
groupInstanceLabel: (item, position, count) => `${item} ${position} of ${count}`,
|
|
798
|
+
groupRemoveLabel: (item, position) => `Remove ${item} ${position}`,
|
|
799
|
+
groupInstanceAdded: (item, position, count) => `${item} ${position} added, ${position} of ${count}`,
|
|
800
|
+
groupInstanceRemoved: (item, count) => count === 0 ? `${item} removed, none left` : `${item} removed, ${count} remaining`,
|
|
757
801
|
phoneCountrySelected: (name) => `${name} selected`,
|
|
758
802
|
phoneResultsCount: (count) => `${count} ${count === 1 ? "result" : "results"}`,
|
|
759
803
|
signatureEmpty: "No signature yet",
|
|
@@ -778,9 +822,17 @@ var fileValueSchema = z.object({
|
|
|
778
822
|
mime: z.string(),
|
|
779
823
|
url: z.optional(z.string())
|
|
780
824
|
});
|
|
825
|
+
function groupCountError(field2, count) {
|
|
826
|
+
const min = Math.max(0, field2.minInstances ?? 1);
|
|
827
|
+
if (count < min) return `Add at least ${min} ${min === 1 ? "entry" : "entries"}`;
|
|
828
|
+
if (Number.isFinite(field2.maxInstances) && count > field2.maxInstances)
|
|
829
|
+
return `At most ${field2.maxInstances} ${field2.maxInstances === 1 ? "entry" : "entries"}`;
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
781
832
|
function validateField(field2, value) {
|
|
782
833
|
if (isEmpty(value)) {
|
|
783
834
|
if (field2.kind === "calculated") return null;
|
|
835
|
+
if (field2.kind === "repeating_group") return groupCountError(field2, 0);
|
|
784
836
|
return field2.required ? REQUIRED_FIELD_MESSAGE : null;
|
|
785
837
|
}
|
|
786
838
|
switch (field2.kind) {
|
|
@@ -900,6 +952,17 @@ function validateField(field2, value) {
|
|
|
900
952
|
case "calculated": {
|
|
901
953
|
return null;
|
|
902
954
|
}
|
|
955
|
+
case "repeating_group": {
|
|
956
|
+
if (!Array.isArray(value)) {
|
|
957
|
+
return groupCountError(field2, 0);
|
|
958
|
+
}
|
|
959
|
+
const instances = value;
|
|
960
|
+
for (const instance of instances) {
|
|
961
|
+
if (!instance || typeof instance !== "object" || Array.isArray(instance))
|
|
962
|
+
return "Invalid value";
|
|
963
|
+
}
|
|
964
|
+
return groupCountError(field2, instances.length);
|
|
965
|
+
}
|
|
903
966
|
case "custom": {
|
|
904
967
|
return null;
|
|
905
968
|
}
|
|
@@ -929,6 +992,10 @@ function validateResponse(form, data) {
|
|
|
929
992
|
for (const field2 of fields) {
|
|
930
993
|
if (field2.kind === "calculated") continue;
|
|
931
994
|
const value = normalizeValue(field2, data[field2.id]);
|
|
995
|
+
if (field2.kind === "repeating_group") {
|
|
996
|
+
validateGroupResponse(field2, value, errors, cleaned);
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
932
999
|
const error = validateField(field2, value);
|
|
933
1000
|
if (error) {
|
|
934
1001
|
errors[field2.id] = error;
|
|
@@ -938,6 +1005,29 @@ function validateResponse(form, data) {
|
|
|
938
1005
|
}
|
|
939
1006
|
return { ok: Object.keys(errors).length === 0, errors, data: cleaned };
|
|
940
1007
|
}
|
|
1008
|
+
function validateGroupResponse(field2, value, errors, cleaned) {
|
|
1009
|
+
const groupError = validateField(field2, value);
|
|
1010
|
+
if (groupError) errors[field2.id] = groupError;
|
|
1011
|
+
if (!Array.isArray(value)) return;
|
|
1012
|
+
const instances = value;
|
|
1013
|
+
const kept = [];
|
|
1014
|
+
for (let index = 0; index < instances.length; index++) {
|
|
1015
|
+
const instance = instances[index];
|
|
1016
|
+
if (!instance || typeof instance !== "object" || Array.isArray(instance)) continue;
|
|
1017
|
+
const keptInstance = {};
|
|
1018
|
+
for (const child of visibleGroupChildren(field2, instance)) {
|
|
1019
|
+
const childValue = instance[child.id];
|
|
1020
|
+
const childError = validateField(child, childValue);
|
|
1021
|
+
if (childError) {
|
|
1022
|
+
errors[`${field2.id}.${index}.${child.id}`] = childError;
|
|
1023
|
+
} else if (!isEmpty(childValue)) {
|
|
1024
|
+
keptInstance[child.id] = childValue;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
kept.push(keptInstance);
|
|
1028
|
+
}
|
|
1029
|
+
if (!groupError && kept.length > 0) cleaned[field2.id] = kept;
|
|
1030
|
+
}
|
|
941
1031
|
|
|
942
1032
|
// src/schema-validation.ts
|
|
943
1033
|
import * as z2 from "zod/mini";
|
|
@@ -961,6 +1051,7 @@ var FIELD_KINDS = [
|
|
|
961
1051
|
"file_upload",
|
|
962
1052
|
"hidden",
|
|
963
1053
|
"calculated",
|
|
1054
|
+
"repeating_group",
|
|
964
1055
|
"custom"
|
|
965
1056
|
];
|
|
966
1057
|
var CONTENT_KINDS2 = ["heading", "paragraph", "divider"];
|
|
@@ -997,10 +1088,11 @@ var schemaShape = z2.object({
|
|
|
997
1088
|
});
|
|
998
1089
|
var MAX_SCHEMA_VERSION = 1;
|
|
999
1090
|
var FILLO_SCHEMA_VERSION = 1;
|
|
1000
|
-
var FILLO_SDK_VERSION = true ? "0.
|
|
1091
|
+
var FILLO_SDK_VERSION = true ? "0.14.0" : "0.0.0-dev";
|
|
1001
1092
|
var FILLO_MIN_SDK_VERSION = "0.4.0";
|
|
1002
1093
|
var FILLO_CHALLENGE_MIN_SDK_VERSION = "0.9.0";
|
|
1003
1094
|
var FILLO_CALC_MIN_SDK_VERSION = "0.11.0";
|
|
1095
|
+
var FILLO_GROUP_MIN_SDK_VERSION = "0.13.0";
|
|
1004
1096
|
function str(value, max, fallback = "") {
|
|
1005
1097
|
return typeof value === "string" ? value.trim().slice(0, max) : fallback;
|
|
1006
1098
|
}
|
|
@@ -1170,7 +1262,7 @@ function collectCalcRefs(expr, valueRefs, whenRefs) {
|
|
|
1170
1262
|
return;
|
|
1171
1263
|
}
|
|
1172
1264
|
}
|
|
1173
|
-
function validateCalculatedFields(pages) {
|
|
1265
|
+
function validateCalculatedFields(pages, childToGroup) {
|
|
1174
1266
|
const kinds = /* @__PURE__ */ new Map();
|
|
1175
1267
|
for (const page of pages) {
|
|
1176
1268
|
for (const block of page.blocks) if (isField(block)) kinds.set(block.id, block.kind);
|
|
@@ -1190,6 +1282,10 @@ function validateCalculatedFields(pages) {
|
|
|
1190
1282
|
for (const ref of valueRefs) {
|
|
1191
1283
|
const kind = kinds.get(ref);
|
|
1192
1284
|
if (kind === void 0) {
|
|
1285
|
+
const owningGroup = childToGroup.get(ref);
|
|
1286
|
+
if (owningGroup !== void 0) {
|
|
1287
|
+
return `Calculated field ${block.id} references ${ref}, which is a child of repeating group ${owningGroup} \u2014 calculations can't reach inside a repeating group (calc operands must be top-level fields)`;
|
|
1288
|
+
}
|
|
1193
1289
|
return `Calculated field ${block.id} references a missing field: ${ref} \u2014 remove the reference or restore that field`;
|
|
1194
1290
|
}
|
|
1195
1291
|
if (!CALC_OPERAND_KINDS.includes(kind)) {
|
|
@@ -1263,6 +1359,151 @@ function calcComplexityError(pages) {
|
|
|
1263
1359
|
}
|
|
1264
1360
|
return null;
|
|
1265
1361
|
}
|
|
1362
|
+
var GROUP_CHILD_ALLOWED_KINDS = [
|
|
1363
|
+
"short_text",
|
|
1364
|
+
"long_text",
|
|
1365
|
+
"email",
|
|
1366
|
+
"url",
|
|
1367
|
+
"phone",
|
|
1368
|
+
"number",
|
|
1369
|
+
"select",
|
|
1370
|
+
"multi_select",
|
|
1371
|
+
"dropdown",
|
|
1372
|
+
"checkbox",
|
|
1373
|
+
"date",
|
|
1374
|
+
"rating",
|
|
1375
|
+
"linear_scale"
|
|
1376
|
+
];
|
|
1377
|
+
var MAX_GROUP_TEMPLATE_CHILDREN = 12;
|
|
1378
|
+
var MAX_GROUP_ESTIMATED_BYTES = 18e4;
|
|
1379
|
+
function worstCaseChildBytes(child) {
|
|
1380
|
+
const overhead = child.id.length + 20;
|
|
1381
|
+
switch (child.kind) {
|
|
1382
|
+
case "short_text":
|
|
1383
|
+
case "email":
|
|
1384
|
+
case "url":
|
|
1385
|
+
return overhead + (child.maxLength ?? 2e3);
|
|
1386
|
+
case "long_text":
|
|
1387
|
+
return overhead + (child.maxLength ?? 2e4);
|
|
1388
|
+
case "phone":
|
|
1389
|
+
return overhead + 32;
|
|
1390
|
+
case "number":
|
|
1391
|
+
return overhead + 32;
|
|
1392
|
+
case "select":
|
|
1393
|
+
case "dropdown": {
|
|
1394
|
+
const longestOption = child.options.reduce((max, o) => Math.max(max, o.id.length), 0);
|
|
1395
|
+
return overhead + Math.max(longestOption, child.allowOther ? 500 : 0);
|
|
1396
|
+
}
|
|
1397
|
+
case "multi_select": {
|
|
1398
|
+
const everyOption = child.options.reduce((sum, o) => sum + o.id.length + 3, 0);
|
|
1399
|
+
return overhead + everyOption + (child.allowOther ? 500 : 0);
|
|
1400
|
+
}
|
|
1401
|
+
case "checkbox":
|
|
1402
|
+
return overhead + 8;
|
|
1403
|
+
case "date":
|
|
1404
|
+
return overhead + 16;
|
|
1405
|
+
case "rating":
|
|
1406
|
+
case "linear_scale":
|
|
1407
|
+
return overhead + 8;
|
|
1408
|
+
default:
|
|
1409
|
+
return overhead + 64;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
function groupSizeEstimate(field2) {
|
|
1413
|
+
const perInstance = field2.fields.reduce((sum, child) => sum + worstCaseChildBytes(child), 24);
|
|
1414
|
+
return perInstance * (Number.isFinite(field2.maxInstances) ? field2.maxInstances : 0);
|
|
1415
|
+
}
|
|
1416
|
+
function validateRepeatingGroups(pages) {
|
|
1417
|
+
for (const page of pages) {
|
|
1418
|
+
for (const block of page.blocks) {
|
|
1419
|
+
if (block.kind !== "repeating_group") continue;
|
|
1420
|
+
if (!Number.isInteger(block.maxInstances) || block.maxInstances < 1 || block.maxInstances > 20) {
|
|
1421
|
+
return `Repeating group ${block.id} needs maxInstances to be a whole number between 1 and 20`;
|
|
1422
|
+
}
|
|
1423
|
+
const minInstances = block.minInstances ?? 1;
|
|
1424
|
+
if (!Number.isInteger(minInstances) || minInstances < 0 || minInstances > block.maxInstances) {
|
|
1425
|
+
return `Repeating group ${block.id} needs minInstances to be a whole number between 0 and maxInstances (${block.maxInstances})`;
|
|
1426
|
+
}
|
|
1427
|
+
block.minInstances = minInstances;
|
|
1428
|
+
if (block.fields.length === 0) {
|
|
1429
|
+
return `Repeating group ${block.id} has no template fields \u2014 add at least one field to repeat`;
|
|
1430
|
+
}
|
|
1431
|
+
if (block.fields.length > MAX_GROUP_TEMPLATE_CHILDREN) {
|
|
1432
|
+
return `Repeating group ${block.id} has ${block.fields.length} template fields \u2014 the maximum is ${MAX_GROUP_TEMPLATE_CHILDREN}; split it into fewer fields`;
|
|
1433
|
+
}
|
|
1434
|
+
for (const child of block.fields) {
|
|
1435
|
+
if (!GROUP_CHILD_ALLOWED_KINDS.includes(child.kind)) {
|
|
1436
|
+
return `Repeating group ${block.id} has a "${child.kind}" child (${child.id}) \u2014 allowed child kinds are ${GROUP_CHILD_ALLOWED_KINDS.join(", ")}`;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
const duplicateChild = duplicateOptionId(block.fields);
|
|
1440
|
+
if (duplicateChild) {
|
|
1441
|
+
return `Duplicate child id in repeating group ${block.id}: ${duplicateChild}`;
|
|
1442
|
+
}
|
|
1443
|
+
const siblingIds = new Set(block.fields.map((f) => f.id));
|
|
1444
|
+
for (const child of block.fields) {
|
|
1445
|
+
for (const cond of child.visibleIf ?? []) {
|
|
1446
|
+
if (!siblingIds.has(cond.fieldId)) {
|
|
1447
|
+
return `Field ${child.id} in repeating group ${block.id} has a visibleIf referencing "${cond.fieldId}" \u2014 a child's visibleIf can only reference another field in the SAME group`;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
const estimate = groupSizeEstimate(block);
|
|
1452
|
+
if (estimate > MAX_GROUP_ESTIMATED_BYTES) {
|
|
1453
|
+
return `Repeating group ${block.id} could reach ~${Math.ceil(estimate / 1e3)}KB at ${block.maxInstances} instances \u2014 the maximum is ~${Math.round(MAX_GROUP_ESTIMATED_BYTES / 1e3)}KB; lower maxInstances, shorten text limits, or trim the template`;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
return null;
|
|
1458
|
+
}
|
|
1459
|
+
function collectGroupChildIds(pages) {
|
|
1460
|
+
const topLevelIds = /* @__PURE__ */ new Set();
|
|
1461
|
+
for (const page of pages) {
|
|
1462
|
+
for (const block of page.blocks) if (isField(block)) topLevelIds.add(block.id);
|
|
1463
|
+
}
|
|
1464
|
+
const childToGroup = /* @__PURE__ */ new Map();
|
|
1465
|
+
for (const page of pages) {
|
|
1466
|
+
for (const block of page.blocks) {
|
|
1467
|
+
if (block.kind !== "repeating_group") continue;
|
|
1468
|
+
for (const child of block.fields) {
|
|
1469
|
+
if (!topLevelIds.has(child.id) && !childToGroup.has(child.id)) {
|
|
1470
|
+
childToGroup.set(child.id, block.id);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
return childToGroup;
|
|
1476
|
+
}
|
|
1477
|
+
function groupScopeWallError(pages, rawPages, childToGroup) {
|
|
1478
|
+
for (const page of pages) {
|
|
1479
|
+
for (const block of page.blocks) {
|
|
1480
|
+
for (const cond of block.visibleIf ?? []) {
|
|
1481
|
+
const owner = childToGroup.get(cond.fieldId);
|
|
1482
|
+
if (owner !== void 0) {
|
|
1483
|
+
return `Field ${block.id}'s visibleIf references "${cond.fieldId}", which is a child of repeating group ${owner} \u2014 fields outside a repeating group can't reference its children`;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
for (const page of rawPages) {
|
|
1489
|
+
if (!Array.isArray(page.next)) continue;
|
|
1490
|
+
for (const rule of page.next) {
|
|
1491
|
+
if (!rule || typeof rule !== "object") continue;
|
|
1492
|
+
const when2 = rule.when;
|
|
1493
|
+
if (!Array.isArray(when2)) continue;
|
|
1494
|
+
for (const cond of when2) {
|
|
1495
|
+
if (!cond || typeof cond !== "object") continue;
|
|
1496
|
+
const fieldId = cond.fieldId;
|
|
1497
|
+
if (typeof fieldId !== "string") continue;
|
|
1498
|
+
const owner = childToGroup.get(fieldId);
|
|
1499
|
+
if (owner !== void 0) {
|
|
1500
|
+
return `A jump rule on page ${page.id} references "${fieldId}", which is a child of repeating group ${owner} \u2014 jumps can't reference a repeating group's children`;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
return null;
|
|
1506
|
+
}
|
|
1266
1507
|
function baseBlock(rec) {
|
|
1267
1508
|
return {
|
|
1268
1509
|
id: str(rec.id, 128),
|
|
@@ -1476,6 +1717,33 @@ function normalizeBlock(input) {
|
|
|
1476
1717
|
return item ? [item] : [];
|
|
1477
1718
|
}).slice(0, 50) : void 0
|
|
1478
1719
|
});
|
|
1720
|
+
case "repeating_group": {
|
|
1721
|
+
const rawFields = Array.isArray(rec.fields) ? rec.fields : [];
|
|
1722
|
+
const fields = rawFields.slice(0, 50).flatMap((raw) => {
|
|
1723
|
+
const child = normalizeBlock(raw);
|
|
1724
|
+
return child && isField(child) ? [child] : [];
|
|
1725
|
+
});
|
|
1726
|
+
return cleanObject({
|
|
1727
|
+
...baseField(rec, kind),
|
|
1728
|
+
// A container's OWN completeness is governed by minInstances (0 is a
|
|
1729
|
+
// legitimate "this group is optional"), not the generic required
|
|
1730
|
+
// flag — a separately-true `required` would be redundant at best and
|
|
1731
|
+
// self-contradictory at worst (the hidden/calculated precedent).
|
|
1732
|
+
required: false,
|
|
1733
|
+
fields,
|
|
1734
|
+
// Bounds become authoritative in validateRepeatingGroups as HARD
|
|
1735
|
+
// ERRORS, not silent clamps (unlike rating.max/linear_scale.min-max)
|
|
1736
|
+
// — instances are respondent-controlled wire shape, not cosmetic
|
|
1737
|
+
// display config. maxInstances is schema-REQUIRED, so a missing/
|
|
1738
|
+
// non-finite raw value becomes NaN here (fails every bounds check on
|
|
1739
|
+
// its own) rather than a fabricated fallback the post-pass would
|
|
1740
|
+
// have to override anyway.
|
|
1741
|
+
maxInstances: finite(rec.maxInstances) ?? Number.NaN,
|
|
1742
|
+
minInstances: finite(rec.minInstances),
|
|
1743
|
+
addLabel: optionalStr(rec.addLabel, 100),
|
|
1744
|
+
itemLabel: optionalStr(rec.itemLabel, 100)
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1479
1747
|
case "custom":
|
|
1480
1748
|
return cleanObject({
|
|
1481
1749
|
...baseField(rec, kind),
|
|
@@ -1564,7 +1832,12 @@ function normalizeFormSchema(input) {
|
|
|
1564
1832
|
}
|
|
1565
1833
|
}
|
|
1566
1834
|
}
|
|
1567
|
-
const
|
|
1835
|
+
const groupError = validateRepeatingGroups(pages);
|
|
1836
|
+
if (groupError) return { ok: false, error: groupError };
|
|
1837
|
+
const childToGroup = collectGroupChildIds(pages);
|
|
1838
|
+
const wallError = groupScopeWallError(pages, rebuilt, childToGroup);
|
|
1839
|
+
if (wallError) return { ok: false, error: wallError };
|
|
1840
|
+
const calcError = validateCalculatedFields(pages, childToGroup);
|
|
1568
1841
|
if (calcError) return { ok: false, error: calcError };
|
|
1569
1842
|
const calcComplexity = calcComplexityError(pages);
|
|
1570
1843
|
if (calcComplexity) return { ok: false, error: calcComplexity };
|
|
@@ -1653,6 +1926,19 @@ function formatAnswer(field2, value) {
|
|
|
1653
1926
|
const text = field2.decimals === void 0 ? String(n) : n.toFixed(Math.max(0, Math.min(6, field2.decimals)));
|
|
1654
1927
|
return `${field2.prefix ?? ""}${text}${field2.suffix ?? ""}`;
|
|
1655
1928
|
}
|
|
1929
|
+
case "repeating_group": {
|
|
1930
|
+
if (!Array.isArray(value)) return String(value);
|
|
1931
|
+
const instances = value;
|
|
1932
|
+
if (instances.length === 0) return "";
|
|
1933
|
+
const noun = field2.itemLabel ?? field2.label;
|
|
1934
|
+
const summary = `${instances.length} \xD7 ${noun}`;
|
|
1935
|
+
const firstChild = field2.fields[0];
|
|
1936
|
+
if (!firstChild) return summary;
|
|
1937
|
+
const previews = instances.map(
|
|
1938
|
+
(instance) => instance && typeof instance === "object" && !Array.isArray(instance) ? formatAnswer(firstChild, instance[firstChild.id]) : ""
|
|
1939
|
+
).filter((text) => text !== "");
|
|
1940
|
+
return previews.length > 0 ? `${summary}: ${previews.join(", ")}` : summary;
|
|
1941
|
+
}
|
|
1656
1942
|
case "phone": {
|
|
1657
1943
|
const { country, national, e164 } = parsePhone(String(value));
|
|
1658
1944
|
if (!e164) return String(value);
|
|
@@ -1815,6 +2101,7 @@ var BLOCK_KIND_META = {
|
|
|
1815
2101
|
file_upload: { label: "File upload", hint: "Direct to connected storage" },
|
|
1816
2102
|
hidden: { label: "Hidden field", hint: "Filled from the URL" },
|
|
1817
2103
|
calculated: { label: "Calculated value", hint: "Computed from other answers" },
|
|
2104
|
+
repeating_group: { label: "Repeating group", hint: "A set of fields respondents can repeat" },
|
|
1818
2105
|
custom: { label: "Custom", hint: "Your own component (code only)" },
|
|
1819
2106
|
heading: { label: "Heading", hint: "Section title" },
|
|
1820
2107
|
paragraph: { label: "Paragraph", hint: "Explanatory text" },
|
|
@@ -1886,6 +2173,14 @@ function createBlock(kind) {
|
|
|
1886
2173
|
return { id, kind, label: "Hidden field" };
|
|
1887
2174
|
case "calculated":
|
|
1888
2175
|
return { id, kind, label: "Calculated value", calc: { op: "const", value: 0 } };
|
|
2176
|
+
case "repeating_group":
|
|
2177
|
+
return {
|
|
2178
|
+
id,
|
|
2179
|
+
kind,
|
|
2180
|
+
label: "Repeating group",
|
|
2181
|
+
maxInstances: 5,
|
|
2182
|
+
fields: [{ id: createId(6), kind: "short_text", label: "Short text" }]
|
|
2183
|
+
};
|
|
1889
2184
|
case "custom":
|
|
1890
2185
|
return { id, kind, label: "Custom field", component: "" };
|
|
1891
2186
|
case "file_upload":
|
|
@@ -2919,6 +3214,8 @@ function prefillFromParams(form, params) {
|
|
|
2919
3214
|
case "calculated":
|
|
2920
3215
|
break;
|
|
2921
3216
|
// engine-computed, never prefillable — a crafted URL can't seed it
|
|
3217
|
+
case "repeating_group":
|
|
3218
|
+
break;
|
|
2922
3219
|
default:
|
|
2923
3220
|
data[field2.id] = raw;
|
|
2924
3221
|
}
|
|
@@ -2927,6 +3224,22 @@ function prefillFromParams(form, params) {
|
|
|
2927
3224
|
}
|
|
2928
3225
|
|
|
2929
3226
|
// src/controller.ts
|
|
3227
|
+
function browserAttribution() {
|
|
3228
|
+
if (typeof document === "undefined" || typeof window === "undefined") return void 0;
|
|
3229
|
+
try {
|
|
3230
|
+
const page = new URL(window.location.href);
|
|
3231
|
+
page.hash = "";
|
|
3232
|
+
const hubspotutk = document.cookie.split(";").map((part) => part.trim()).find((part) => part.startsWith("hubspotutk="))?.slice("hubspotutk=".length);
|
|
3233
|
+
const attribution = {
|
|
3234
|
+
pageUri: page.href.slice(0, 2048),
|
|
3235
|
+
pageName: document.title.trim().slice(0, 200) || void 0,
|
|
3236
|
+
hubspotutk: hubspotutk && /^[a-f0-9]{32}$/i.test(hubspotutk) ? hubspotutk : void 0
|
|
3237
|
+
};
|
|
3238
|
+
return Object.values(attribution).some(Boolean) ? attribution : void 0;
|
|
3239
|
+
} catch {
|
|
3240
|
+
return void 0;
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
2930
3243
|
var EMPTY_PAGE = { id: "empty", blocks: [] };
|
|
2931
3244
|
var warnedNoClient = false;
|
|
2932
3245
|
var warnedNoFormId = false;
|
|
@@ -3347,8 +3660,11 @@ function createFormController(options2) {
|
|
|
3347
3660
|
data = computeCalculated(form, { ...data, [fieldId]: value });
|
|
3348
3661
|
onChange?.(data);
|
|
3349
3662
|
submitError = void 0;
|
|
3350
|
-
|
|
3351
|
-
|
|
3663
|
+
const prefix = `${fieldId}.`;
|
|
3664
|
+
if (fieldId in errors || Object.keys(errors).some((k) => k.startsWith(prefix))) {
|
|
3665
|
+
errors = Object.fromEntries(
|
|
3666
|
+
Object.entries(errors).filter(([k]) => k !== fieldId && !k.startsWith(prefix))
|
|
3667
|
+
);
|
|
3352
3668
|
}
|
|
3353
3669
|
notify();
|
|
3354
3670
|
scheduleDraftSave();
|
|
@@ -3482,6 +3798,7 @@ function createFormController(options2) {
|
|
|
3482
3798
|
hp: options2.getHoneypot?.() ?? "",
|
|
3483
3799
|
elapsedMs: Date.now() - mountedAt,
|
|
3484
3800
|
surface: options2.surface ?? "headless",
|
|
3801
|
+
attribution: browserAttribution(),
|
|
3485
3802
|
submissionKey,
|
|
3486
3803
|
draft: submitDraft ? { id: submitDraft.id, token: submitDraft.token } : void 0,
|
|
3487
3804
|
respondent,
|
|
@@ -3952,6 +4269,9 @@ var JSX_BLOCK_SPECS = [
|
|
|
3952
4269
|
field("FileUpload", "file_upload", ["maxFiles", "maxFileSizeMb", "accept"]),
|
|
3953
4270
|
field("Hidden", "hidden", ["paramName", "defaultValue"]),
|
|
3954
4271
|
field("Calculated", "calculated", ["calc", "decimals", "prefix", "suffix"]),
|
|
4272
|
+
field("RepeatingGroup", "repeating_group", ["minInstances", "maxInstances", "addLabel", "itemLabel"], {
|
|
4273
|
+
blockChildren: true
|
|
4274
|
+
}),
|
|
3955
4275
|
field("Custom", "custom", ["component", "config"]),
|
|
3956
4276
|
content("Heading", "heading", true),
|
|
3957
4277
|
content("Paragraph", "paragraph", true),
|
|
@@ -4055,6 +4375,29 @@ function buildProps(spec, el) {
|
|
|
4055
4375
|
);
|
|
4056
4376
|
}
|
|
4057
4377
|
out.__childText = parts.join("");
|
|
4378
|
+
} else if (spec.blockChildren && props.children !== void 0) {
|
|
4379
|
+
const childBlocks = flatten(props.children).map((child) => {
|
|
4380
|
+
const childSpec = classify(child);
|
|
4381
|
+
if (childSpec.kind === "__page" || childSpec.kind === "__option" || childSpec.content) {
|
|
4382
|
+
throw new FilloJsxError(
|
|
4383
|
+
"NON_FILLO_CHILD",
|
|
4384
|
+
`Fillo.${spec.name} "${id}" children must be field elements, found Fillo.${childSpec.name} \u2014 a repeating group's template holds answerable fields only`
|
|
4385
|
+
);
|
|
4386
|
+
}
|
|
4387
|
+
return buildProps(childSpec, child);
|
|
4388
|
+
});
|
|
4389
|
+
const childIds = /* @__PURE__ */ new Set();
|
|
4390
|
+
for (const child of childBlocks) {
|
|
4391
|
+
const childId = child.id;
|
|
4392
|
+
if (childIds.has(childId)) {
|
|
4393
|
+
throw new FilloJsxError(
|
|
4394
|
+
"DUPLICATE_ID",
|
|
4395
|
+
`Two fields in repeating group "${id}" share the id "${childId}" \u2014 child ids key instance answers and must be unique within the group`
|
|
4396
|
+
);
|
|
4397
|
+
}
|
|
4398
|
+
childIds.add(childId);
|
|
4399
|
+
}
|
|
4400
|
+
out.__childBlocks = childBlocks;
|
|
4058
4401
|
} else if (props.children !== void 0) {
|
|
4059
4402
|
throw new FilloJsxError(
|
|
4060
4403
|
"NON_FILLO_CHILD",
|
|
@@ -4077,6 +4420,16 @@ function buildProps(spec, el) {
|
|
|
4077
4420
|
if (spec.textChildren && out.text === void 0) {
|
|
4078
4421
|
throw new FilloJsxError("TEXT_CHILD_REQUIRED", `Fillo.${spec.name} "${id}" has no text`);
|
|
4079
4422
|
}
|
|
4423
|
+
if (spec.blockChildren) {
|
|
4424
|
+
if (out.__childBlocks === void 0) {
|
|
4425
|
+
throw new FilloJsxError(
|
|
4426
|
+
"TEXT_CHILD_REQUIRED",
|
|
4427
|
+
`Fillo.${spec.name} "${id}" has no fields \u2014 a repeating group needs at least one field element as a child (empty templates are invalid)`
|
|
4428
|
+
);
|
|
4429
|
+
}
|
|
4430
|
+
out.fields = out.__childBlocks;
|
|
4431
|
+
delete out.__childBlocks;
|
|
4432
|
+
}
|
|
4080
4433
|
if (props.visibleIf !== void 0) {
|
|
4081
4434
|
const conditions2 = Array.isArray(props.visibleIf) ? props.visibleIf : [props.visibleIf];
|
|
4082
4435
|
out.visibleIf = conditions2;
|
|
@@ -4184,6 +4537,11 @@ function schemaFromJsx(children, meta) {
|
|
|
4184
4537
|
assertUniqueOptionIds(block.id, block.rows, "matrix rows");
|
|
4185
4538
|
assertUniqueOptionIds(block.id, block.columns, "matrix columns");
|
|
4186
4539
|
}
|
|
4540
|
+
if (block.kind === "repeating_group") {
|
|
4541
|
+
for (const child of block.fields) {
|
|
4542
|
+
if ("options" in child) assertUniqueOptionIds(child.id, child.options, "options");
|
|
4543
|
+
}
|
|
4544
|
+
}
|
|
4187
4545
|
}
|
|
4188
4546
|
}
|
|
4189
4547
|
return {
|
|
@@ -4281,6 +4639,7 @@ export {
|
|
|
4281
4639
|
FILLO_CALC_MIN_SDK_VERSION,
|
|
4282
4640
|
FILLO_CHALLENGE_MIN_SDK_VERSION,
|
|
4283
4641
|
FILLO_DATA_ATTRS,
|
|
4642
|
+
FILLO_GROUP_MIN_SDK_VERSION,
|
|
4284
4643
|
FILLO_MIN_SDK_VERSION,
|
|
4285
4644
|
FILLO_SCHEMA_VERSION,
|
|
4286
4645
|
FILLO_SDK_VERSION,
|
|
@@ -4362,6 +4721,7 @@ export {
|
|
|
4362
4721
|
validateResponse,
|
|
4363
4722
|
visibleBlocks,
|
|
4364
4723
|
visibleFields,
|
|
4724
|
+
visibleGroupChildren,
|
|
4365
4725
|
visiblePageBlocks,
|
|
4366
4726
|
when
|
|
4367
4727
|
};
|