@usefillo/core 0.5.1 → 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.ts +239 -14
- package/dist/index.js +659 -111
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -381,18 +381,18 @@ function isEmpty(value) {
|
|
|
381
381
|
if (typeof value === "object") return Object.keys(value).length === 0;
|
|
382
382
|
return false;
|
|
383
383
|
}
|
|
384
|
-
function validateField(
|
|
384
|
+
function validateField(field2, value) {
|
|
385
385
|
if (isEmpty(value)) {
|
|
386
|
-
return
|
|
386
|
+
return field2.required ? "This field is required" : null;
|
|
387
387
|
}
|
|
388
|
-
switch (
|
|
388
|
+
switch (field2.kind) {
|
|
389
389
|
case "short_text":
|
|
390
390
|
case "long_text": {
|
|
391
391
|
if (typeof value !== "string") return "Expected text";
|
|
392
|
-
if (
|
|
393
|
-
return `Must be at most ${
|
|
394
|
-
const hardCap =
|
|
395
|
-
if (!
|
|
392
|
+
if (field2.maxLength && value.length > field2.maxLength)
|
|
393
|
+
return `Must be at most ${field2.maxLength} characters`;
|
|
394
|
+
const hardCap = field2.kind === "long_text" ? 2e4 : 2e3;
|
|
395
|
+
if (!field2.maxLength && value.length > hardCap) return "Answer is too long";
|
|
396
396
|
return null;
|
|
397
397
|
}
|
|
398
398
|
case "email": {
|
|
@@ -419,25 +419,25 @@ function validateField(field, value) {
|
|
|
419
419
|
case "number": {
|
|
420
420
|
const n = typeof value === "string" ? Number(value) : value;
|
|
421
421
|
if (typeof n !== "number" || !Number.isFinite(n)) return "Enter a number";
|
|
422
|
-
if (
|
|
423
|
-
if (
|
|
422
|
+
if (field2.min !== void 0 && n < field2.min) return `Must be at least ${field2.min}`;
|
|
423
|
+
if (field2.max !== void 0 && n > field2.max) return `Must be at most ${field2.max}`;
|
|
424
424
|
return null;
|
|
425
425
|
}
|
|
426
426
|
case "select":
|
|
427
427
|
case "dropdown": {
|
|
428
428
|
if (typeof value !== "string" || value === "") return "Pick an option";
|
|
429
|
-
if (
|
|
430
|
-
if (
|
|
429
|
+
if (field2.options.some((o) => o.id === value)) return null;
|
|
430
|
+
if (field2.allowOther && value.length <= 500) return null;
|
|
431
431
|
return "Pick a valid option";
|
|
432
432
|
}
|
|
433
433
|
case "multi_select": {
|
|
434
434
|
if (!Array.isArray(value)) return "Pick at least one option";
|
|
435
|
-
const ids = new Set(
|
|
435
|
+
const ids = new Set(field2.options.map((o) => o.id));
|
|
436
436
|
const arr = value;
|
|
437
437
|
let others = 0;
|
|
438
438
|
for (const v of arr) {
|
|
439
439
|
if (ids.has(v)) continue;
|
|
440
|
-
if (
|
|
440
|
+
if (field2.allowOther !== true || typeof v !== "string" || v.length === 0 || v.length > 500)
|
|
441
441
|
return "Pick valid options";
|
|
442
442
|
if (++others > 1) return "Only one 'Other' answer is allowed";
|
|
443
443
|
}
|
|
@@ -447,15 +447,15 @@ function validateField(field, value) {
|
|
|
447
447
|
return typeof value === "boolean" ? null : "Invalid value";
|
|
448
448
|
}
|
|
449
449
|
case "rating": {
|
|
450
|
-
const max =
|
|
450
|
+
const max = field2.max ?? 5;
|
|
451
451
|
const n = typeof value === "string" ? Number(value) : value;
|
|
452
452
|
if (typeof n !== "number" || !Number.isInteger(n) || n < 1 || n > max)
|
|
453
453
|
return `Pick a rating between 1 and ${max}`;
|
|
454
454
|
return null;
|
|
455
455
|
}
|
|
456
456
|
case "linear_scale": {
|
|
457
|
-
const min =
|
|
458
|
-
const max =
|
|
457
|
+
const min = field2.min ?? 1;
|
|
458
|
+
const max = field2.max ?? 10;
|
|
459
459
|
const n = typeof value === "string" ? Number(value) : value;
|
|
460
460
|
if (typeof n !== "number" || !Number.isInteger(n) || n < min || n > max)
|
|
461
461
|
return `Pick a value between ${min} and ${max}`;
|
|
@@ -463,7 +463,7 @@ function validateField(field, value) {
|
|
|
463
463
|
}
|
|
464
464
|
case "ranking": {
|
|
465
465
|
if (!Array.isArray(value)) return "Drag the options into order";
|
|
466
|
-
const ids =
|
|
466
|
+
const ids = field2.options.map((o) => o.id).sort();
|
|
467
467
|
const got = [...value].sort();
|
|
468
468
|
if (ids.length !== got.length || ids.some((id, i) => id !== got[i]))
|
|
469
469
|
return "Rank every option";
|
|
@@ -473,12 +473,12 @@ function validateField(field, value) {
|
|
|
473
473
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
474
474
|
return "Answer the grid";
|
|
475
475
|
const answers = value;
|
|
476
|
-
const columnIds = new Set(
|
|
477
|
-
const rowIds = new Set(
|
|
476
|
+
const columnIds = new Set(field2.columns.map((c) => c.id));
|
|
477
|
+
const rowIds = new Set(field2.rows.map((r) => r.id));
|
|
478
478
|
for (const [rowId, columnId] of Object.entries(answers)) {
|
|
479
479
|
if (!rowIds.has(rowId) || !columnIds.has(columnId)) return "Invalid grid answer";
|
|
480
480
|
}
|
|
481
|
-
if (
|
|
481
|
+
if (field2.required && Object.keys(answers).length < field2.rows.length)
|
|
482
482
|
return "Answer every row";
|
|
483
483
|
return null;
|
|
484
484
|
}
|
|
@@ -505,7 +505,7 @@ function validateField(field, value) {
|
|
|
505
505
|
case "file_upload": {
|
|
506
506
|
const r = z.array(fileValueSchema).safeParse(value);
|
|
507
507
|
if (!r.success) return "Upload did not complete";
|
|
508
|
-
const maxFiles =
|
|
508
|
+
const maxFiles = field2.maxFiles ?? 1;
|
|
509
509
|
if (r.data.length > maxFiles)
|
|
510
510
|
return `At most ${maxFiles} file${maxFiles === 1 ? "" : "s"}`;
|
|
511
511
|
return null;
|
|
@@ -516,24 +516,24 @@ function validateResponse(form, data) {
|
|
|
516
516
|
const fields = visibleFields(form, data);
|
|
517
517
|
const errors = {};
|
|
518
518
|
const cleaned = {};
|
|
519
|
-
for (const
|
|
520
|
-
const value = normalizeValue(
|
|
521
|
-
const error = validateField(
|
|
519
|
+
for (const field2 of fields) {
|
|
520
|
+
const value = normalizeValue(field2, data[field2.id]);
|
|
521
|
+
const error = validateField(field2, value);
|
|
522
522
|
if (error) {
|
|
523
|
-
errors[
|
|
523
|
+
errors[field2.id] = error;
|
|
524
524
|
} else if (!isEmpty(value)) {
|
|
525
|
-
cleaned[
|
|
525
|
+
cleaned[field2.id] = value;
|
|
526
526
|
}
|
|
527
527
|
}
|
|
528
528
|
return { ok: Object.keys(errors).length === 0, errors, data: cleaned };
|
|
529
529
|
}
|
|
530
|
-
function normalizeValue(
|
|
530
|
+
function normalizeValue(field2, value) {
|
|
531
531
|
if (value === void 0 || value === null) return value;
|
|
532
|
-
if (
|
|
532
|
+
if (field2.kind === "number" && typeof value === "string" && value.trim() !== "") {
|
|
533
533
|
const n = Number(value);
|
|
534
534
|
return Number.isNaN(n) ? value : n;
|
|
535
535
|
}
|
|
536
|
-
if ((
|
|
536
|
+
if ((field2.kind === "rating" || field2.kind === "linear_scale") && typeof value === "string") {
|
|
537
537
|
const n = Number(value);
|
|
538
538
|
return Number.isNaN(n) ? value : n;
|
|
539
539
|
}
|
|
@@ -869,26 +869,26 @@ function normalizeFormTheme(input) {
|
|
|
869
869
|
function assertNever(x) {
|
|
870
870
|
throw new Error(`Unhandled field kind: ${JSON.stringify(x)}`);
|
|
871
871
|
}
|
|
872
|
-
function formatAnswer(
|
|
872
|
+
function formatAnswer(field2, value) {
|
|
873
873
|
if (value === null || value === void 0 || value === "") return "";
|
|
874
|
-
switch (
|
|
874
|
+
switch (field2.kind) {
|
|
875
875
|
case "file_upload":
|
|
876
876
|
if (!Array.isArray(value)) return String(value);
|
|
877
877
|
return value.map((f) => f.name).join(", ");
|
|
878
878
|
case "select":
|
|
879
879
|
case "dropdown":
|
|
880
|
-
return
|
|
880
|
+
return field2.options.find((o) => o.id === value)?.label ?? String(value);
|
|
881
881
|
case "multi_select":
|
|
882
882
|
if (!Array.isArray(value)) return String(value);
|
|
883
|
-
return value.map((id) =>
|
|
883
|
+
return value.map((id) => field2.options.find((o) => o.id === id)?.label ?? id).join(", ");
|
|
884
884
|
case "ranking":
|
|
885
885
|
if (!Array.isArray(value)) return String(value);
|
|
886
|
-
return value.map((id, i) => `${i + 1}. ${
|
|
886
|
+
return value.map((id, i) => `${i + 1}. ${field2.options.find((o) => o.id === id)?.label ?? id}`).join(", ");
|
|
887
887
|
case "matrix": {
|
|
888
888
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
889
889
|
return String(value);
|
|
890
890
|
const answers = value;
|
|
891
|
-
return
|
|
891
|
+
return field2.rows.filter((row) => answers[row.id]).map((row) => `${row.label}: ${field2.columns.find((c) => c.id === answers[row.id])?.label ?? ""}`).join(", ");
|
|
892
892
|
}
|
|
893
893
|
case "signature":
|
|
894
894
|
return typeof value === "string" && value.startsWith("data:image/") ? "\u270D signed" : "";
|
|
@@ -915,7 +915,7 @@ function formatAnswer(field, value) {
|
|
|
915
915
|
case "custom":
|
|
916
916
|
return typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
917
917
|
default:
|
|
918
|
-
return assertNever(
|
|
918
|
+
return assertNever(field2);
|
|
919
919
|
}
|
|
920
920
|
}
|
|
921
921
|
|
|
@@ -1255,13 +1255,30 @@ var sha1Base64 = (bytes) => bytesToBase64(new Sha1().update(bytes).digest());
|
|
|
1255
1255
|
|
|
1256
1256
|
// src/client.ts
|
|
1257
1257
|
var FilloError = class extends Error {
|
|
1258
|
-
constructor(message, status) {
|
|
1258
|
+
constructor(message, status, retryAfterSec2) {
|
|
1259
1259
|
super(message);
|
|
1260
1260
|
this.status = status;
|
|
1261
|
+
this.retryAfterSec = retryAfterSec2;
|
|
1261
1262
|
this.name = "FilloError";
|
|
1262
1263
|
}
|
|
1263
1264
|
status;
|
|
1265
|
+
retryAfterSec;
|
|
1264
1266
|
};
|
|
1267
|
+
function isFilloError(err) {
|
|
1268
|
+
return err instanceof Error && err.name === "FilloError";
|
|
1269
|
+
}
|
|
1270
|
+
async function errorMessage(res, fallback) {
|
|
1271
|
+
try {
|
|
1272
|
+
const body = await res.json();
|
|
1273
|
+
if (typeof body.error === "string" && body.error) return body.error;
|
|
1274
|
+
} catch {
|
|
1275
|
+
}
|
|
1276
|
+
return fallback;
|
|
1277
|
+
}
|
|
1278
|
+
function retryAfterSec(res) {
|
|
1279
|
+
const raw = Number(res.headers.get("retry-after"));
|
|
1280
|
+
return Number.isFinite(raw) && raw > 0 ? raw : void 0;
|
|
1281
|
+
}
|
|
1265
1282
|
var MAX_CHUNK_RETRIES = 3;
|
|
1266
1283
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
1267
1284
|
function isRetryableStatus(status) {
|
|
@@ -1311,6 +1328,7 @@ async function digestPartBase64(bytes) {
|
|
|
1311
1328
|
return sha1Base64(bytes);
|
|
1312
1329
|
}
|
|
1313
1330
|
var DEFAULT_BASE_URL = "https://fillo.so";
|
|
1331
|
+
var CLIENT_HEADER = { "X-Fillo-Client": `@usefillo/core@${FILLO_SDK_VERSION}` };
|
|
1314
1332
|
var FilloClient = class {
|
|
1315
1333
|
/** Server origin this client targets, normalized (no trailing slash). */
|
|
1316
1334
|
baseUrl;
|
|
@@ -1318,7 +1336,7 @@ var FilloClient = class {
|
|
|
1318
1336
|
/** Publishable workspace key, when configured. */
|
|
1319
1337
|
key;
|
|
1320
1338
|
constructor(options2 = {}) {
|
|
1321
|
-
this.baseUrl = options2.sameOrigin ? "" : DEFAULT_BASE_URL;
|
|
1339
|
+
this.baseUrl = options2.baseUrl ? options2.baseUrl.replace(/\/$/, "") : options2.sameOrigin ? "" : DEFAULT_BASE_URL;
|
|
1322
1340
|
this.key = options2.key;
|
|
1323
1341
|
this.fetch = options2.fetch ?? ((...args) => globalThis.fetch(...args));
|
|
1324
1342
|
}
|
|
@@ -1326,7 +1344,7 @@ var FilloClient = class {
|
|
|
1326
1344
|
return `${this.baseUrl}/api/v1${path}`;
|
|
1327
1345
|
}
|
|
1328
1346
|
async json(path, init) {
|
|
1329
|
-
const headers = init?.body != null ? { "Content-Type": "application/json", ...init?.headers } : init?.headers;
|
|
1347
|
+
const headers = init?.body != null ? { "Content-Type": "application/json", ...CLIENT_HEADER, ...init?.headers } : init?.headers;
|
|
1330
1348
|
let res;
|
|
1331
1349
|
try {
|
|
1332
1350
|
res = await this.fetch(this.url(path), {
|
|
@@ -1338,8 +1356,8 @@ var FilloClient = class {
|
|
|
1338
1356
|
throw asRequestError(err);
|
|
1339
1357
|
}
|
|
1340
1358
|
if (!res.ok) {
|
|
1341
|
-
const
|
|
1342
|
-
throw new FilloError(
|
|
1359
|
+
const message = await errorMessage(res, `Request failed: ${res.status}`);
|
|
1360
|
+
throw new FilloError(message, res.status, retryAfterSec(res));
|
|
1343
1361
|
}
|
|
1344
1362
|
return await res.json();
|
|
1345
1363
|
}
|
|
@@ -1379,7 +1397,7 @@ var FilloClient = class {
|
|
|
1379
1397
|
try {
|
|
1380
1398
|
res = await this.fetch(this.url(`/forms/${encodeURIComponent(formId)}/responses`), {
|
|
1381
1399
|
method: "POST",
|
|
1382
|
-
headers: { "Content-Type": "application/json" },
|
|
1400
|
+
headers: { "Content-Type": "application/json", ...CLIENT_HEADER },
|
|
1383
1401
|
body: JSON.stringify({ data, meta }),
|
|
1384
1402
|
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
|
|
1385
1403
|
});
|
|
@@ -1391,7 +1409,8 @@ var FilloClient = class {
|
|
|
1391
1409
|
return { ok: false, errors: body2.errors };
|
|
1392
1410
|
}
|
|
1393
1411
|
if (!res.ok) {
|
|
1394
|
-
|
|
1412
|
+
const message = await errorMessage(res, `Submit failed: ${res.status}`);
|
|
1413
|
+
throw new FilloError(message, res.status, retryAfterSec(res));
|
|
1395
1414
|
}
|
|
1396
1415
|
const body = await res.json();
|
|
1397
1416
|
return { ok: true, responseId: body.id, duplicate: body.duplicate };
|
|
@@ -1722,6 +1741,68 @@ async function provisionWorkspace(options2) {
|
|
|
1722
1741
|
return await res.json();
|
|
1723
1742
|
}
|
|
1724
1743
|
|
|
1744
|
+
// src/prefill.ts
|
|
1745
|
+
function parseDecimal(raw) {
|
|
1746
|
+
const s = raw.trim();
|
|
1747
|
+
if (!/^-?\d*\.?\d+$/.test(s)) return null;
|
|
1748
|
+
const n = Number(s);
|
|
1749
|
+
return Number.isFinite(n) ? n : null;
|
|
1750
|
+
}
|
|
1751
|
+
function prefillFromParams(form, params) {
|
|
1752
|
+
const data = {};
|
|
1753
|
+
for (const field2 of allFields(form)) {
|
|
1754
|
+
const raw = field2.kind === "hidden" ? params[field2.paramName ?? field2.id] ?? field2.defaultValue : params[field2.id];
|
|
1755
|
+
if (raw === void 0 || raw === "") continue;
|
|
1756
|
+
switch (field2.kind) {
|
|
1757
|
+
case "number": {
|
|
1758
|
+
const n = parseDecimal(raw);
|
|
1759
|
+
if (n === null) break;
|
|
1760
|
+
if (field2.min !== void 0 && n < field2.min) break;
|
|
1761
|
+
if (field2.max !== void 0 && n > field2.max) break;
|
|
1762
|
+
data[field2.id] = n;
|
|
1763
|
+
break;
|
|
1764
|
+
}
|
|
1765
|
+
case "rating": {
|
|
1766
|
+
const n = parseDecimal(raw);
|
|
1767
|
+
if (n !== null && Number.isInteger(n) && n >= 1 && n <= (field2.max ?? 5))
|
|
1768
|
+
data[field2.id] = n;
|
|
1769
|
+
break;
|
|
1770
|
+
}
|
|
1771
|
+
case "linear_scale": {
|
|
1772
|
+
const n = parseDecimal(raw);
|
|
1773
|
+
const min = field2.min ?? 1;
|
|
1774
|
+
const max = field2.max ?? 10;
|
|
1775
|
+
if (n !== null && Number.isInteger(n) && n >= min && n <= max) data[field2.id] = n;
|
|
1776
|
+
break;
|
|
1777
|
+
}
|
|
1778
|
+
case "checkbox":
|
|
1779
|
+
data[field2.id] = raw === "true" || raw === "1" || raw === "yes";
|
|
1780
|
+
break;
|
|
1781
|
+
case "select":
|
|
1782
|
+
case "dropdown": {
|
|
1783
|
+
if (field2.options.some((o) => o.id === raw)) data[field2.id] = raw;
|
|
1784
|
+
break;
|
|
1785
|
+
}
|
|
1786
|
+
case "multi_select": {
|
|
1787
|
+
const ids = new Set(field2.options.map((o) => o.id));
|
|
1788
|
+
const picked = raw.split(",").filter((v) => ids.has(v));
|
|
1789
|
+
if (picked.length > 0) data[field2.id] = picked;
|
|
1790
|
+
break;
|
|
1791
|
+
}
|
|
1792
|
+
case "file_upload":
|
|
1793
|
+
case "ranking":
|
|
1794
|
+
case "matrix":
|
|
1795
|
+
case "signature":
|
|
1796
|
+
case "custom":
|
|
1797
|
+
break;
|
|
1798
|
+
// not prefillable from a URL
|
|
1799
|
+
default:
|
|
1800
|
+
data[field2.id] = raw;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
return data;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1725
1806
|
// src/controller.ts
|
|
1726
1807
|
var EMPTY_PAGE = { id: "empty", blocks: [] };
|
|
1727
1808
|
var warnedNoClient = false;
|
|
@@ -1791,6 +1872,10 @@ function hasSubmittedOnce(form, formId) {
|
|
|
1791
1872
|
if (form.settings.submissionLimit !== "once_per_visitor" || !formId) return false;
|
|
1792
1873
|
return Boolean(readStoredSubmission(formId)?.submittedAt);
|
|
1793
1874
|
}
|
|
1875
|
+
function submitFailureMessage(err) {
|
|
1876
|
+
if (isFilloError(err) && err.status && err.status > 0 && err.message) return err.message;
|
|
1877
|
+
return "Couldn't reach the server \u2014 check your connection and try again. If this keeps happening, a browser extension, firewall, or the site's Content-Security-Policy may be blocking the request.";
|
|
1878
|
+
}
|
|
1794
1879
|
function createFormController(options2) {
|
|
1795
1880
|
const { onChange, onSubmitted } = options2;
|
|
1796
1881
|
const initial = normalizeFormSchema(options2.form);
|
|
@@ -1801,7 +1886,28 @@ function createFormController(options2) {
|
|
|
1801
1886
|
let data = { ...options2.initialData ?? {} };
|
|
1802
1887
|
let errors = {};
|
|
1803
1888
|
let pageIndex = 0;
|
|
1804
|
-
let status =
|
|
1889
|
+
let status = "idle";
|
|
1890
|
+
queueMicrotask(() => {
|
|
1891
|
+
if (status === "idle" && hasSubmittedOnce(form, formId)) {
|
|
1892
|
+
status = "submitted";
|
|
1893
|
+
notify();
|
|
1894
|
+
}
|
|
1895
|
+
if (typeof location !== "undefined" && location.search) {
|
|
1896
|
+
const params = Object.fromEntries(new URLSearchParams(location.search));
|
|
1897
|
+
let merged = null;
|
|
1898
|
+
for (const [key, value] of Object.entries(prefillFromParams(form, params))) {
|
|
1899
|
+
if (data[key] === void 0) {
|
|
1900
|
+
merged = merged ?? { ...data };
|
|
1901
|
+
merged[key] = value;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
if (merged && status === "idle") {
|
|
1905
|
+
data = merged;
|
|
1906
|
+
notify();
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
});
|
|
1910
|
+
let submitError;
|
|
1805
1911
|
const uploadingFields = /* @__PURE__ */ new Set();
|
|
1806
1912
|
const listeners = /* @__PURE__ */ new Set();
|
|
1807
1913
|
const mountedAt = Date.now();
|
|
@@ -1822,7 +1928,8 @@ function createFormController(options2) {
|
|
|
1822
1928
|
blocks: visibleBlocks(page, data),
|
|
1823
1929
|
isFirstPage: clamped === 0,
|
|
1824
1930
|
isLastPage: clamped === pageCount - 1,
|
|
1825
|
-
uploading: uploadingFields.size > 0
|
|
1931
|
+
uploading: uploadingFields.size > 0,
|
|
1932
|
+
submitError
|
|
1826
1933
|
};
|
|
1827
1934
|
}
|
|
1828
1935
|
let snapshot = computeSnapshot();
|
|
@@ -1855,6 +1962,7 @@ function createFormController(options2) {
|
|
|
1855
1962
|
}
|
|
1856
1963
|
data = { ...data, [fieldId]: value };
|
|
1857
1964
|
onChange?.(data);
|
|
1965
|
+
submitError = void 0;
|
|
1858
1966
|
if (fieldId in errors) {
|
|
1859
1967
|
errors = Object.fromEntries(Object.entries(errors).filter(([k]) => k !== fieldId));
|
|
1860
1968
|
}
|
|
@@ -1897,10 +2005,24 @@ function createFormController(options2) {
|
|
|
1897
2005
|
notify();
|
|
1898
2006
|
return;
|
|
1899
2007
|
}
|
|
2008
|
+
if (client && !formId && options2.resolveFormId) {
|
|
2009
|
+
status = "submitting";
|
|
2010
|
+
submitError = void 0;
|
|
2011
|
+
notify();
|
|
2012
|
+
try {
|
|
2013
|
+
formId = await options2.resolveFormId();
|
|
2014
|
+
} catch (err) {
|
|
2015
|
+
status = "idle";
|
|
2016
|
+
submitError = submitFailureMessage(err);
|
|
2017
|
+
notify();
|
|
2018
|
+
throw err;
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
1900
2021
|
if (client && formId) {
|
|
1901
2022
|
const visitorKey = form.settings.submissionLimit === "once_per_visitor" ? ensureSubmissionKey(formId) : void 0;
|
|
1902
2023
|
const submissionKey = visitorKey ?? idempotencyKey;
|
|
1903
2024
|
status = "submitting";
|
|
2025
|
+
submitError = void 0;
|
|
1904
2026
|
notify();
|
|
1905
2027
|
let res;
|
|
1906
2028
|
try {
|
|
@@ -1912,6 +2034,7 @@ function createFormController(options2) {
|
|
|
1912
2034
|
});
|
|
1913
2035
|
} catch (err) {
|
|
1914
2036
|
status = "idle";
|
|
2037
|
+
submitError = submitFailureMessage(err);
|
|
1915
2038
|
notify();
|
|
1916
2039
|
throw err;
|
|
1917
2040
|
}
|
|
@@ -1968,6 +2091,122 @@ function createFormController(options2) {
|
|
|
1968
2091
|
};
|
|
1969
2092
|
}
|
|
1970
2093
|
|
|
2094
|
+
// src/auto-submit.ts
|
|
2095
|
+
function isAutoSubmitBlock(block) {
|
|
2096
|
+
return isField(block) && (block.kind === "select" || block.kind === "dropdown" || block.kind === "checkbox" || block.kind === "rating" || block.kind === "linear_scale");
|
|
2097
|
+
}
|
|
2098
|
+
function needsExplicitSubmit(visible) {
|
|
2099
|
+
const interactive = visible.filter((b) => !(isField(b) && b.kind === "hidden"));
|
|
2100
|
+
if (interactive.length === 0) return false;
|
|
2101
|
+
if (interactive.length > 1) return true;
|
|
2102
|
+
return !isAutoSubmitBlock(interactive[0]);
|
|
2103
|
+
}
|
|
2104
|
+
function shouldAutoSubmit(field2, value, ctx) {
|
|
2105
|
+
if (ctx.form.settings.submitMode !== "auto") return false;
|
|
2106
|
+
if (!ctx.isLastPage || ctx.status !== "idle" || ctx.uploading) return false;
|
|
2107
|
+
const hasDiscreteAnswer = (() => {
|
|
2108
|
+
switch (field2.kind) {
|
|
2109
|
+
case "select":
|
|
2110
|
+
case "dropdown":
|
|
2111
|
+
return typeof value === "string" && field2.options.some((option) => option.id === value);
|
|
2112
|
+
case "checkbox":
|
|
2113
|
+
return value === true;
|
|
2114
|
+
case "rating":
|
|
2115
|
+
case "linear_scale":
|
|
2116
|
+
return typeof value === "number";
|
|
2117
|
+
default:
|
|
2118
|
+
return false;
|
|
2119
|
+
}
|
|
2120
|
+
})();
|
|
2121
|
+
if (!hasDiscreteAnswer) return false;
|
|
2122
|
+
if (value === ctx.data[field2.id]) return false;
|
|
2123
|
+
const nextData = { ...ctx.data, [field2.id]: value };
|
|
2124
|
+
const interactive = visibleFields(ctx.form, nextData).filter((f) => f.kind !== "hidden");
|
|
2125
|
+
if (interactive.length !== 1 || interactive[0]?.id !== field2.id) return false;
|
|
2126
|
+
return validateResponse(ctx.form, nextData).ok;
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
// src/appearance.ts
|
|
2130
|
+
var FILLO_SLOTS = [
|
|
2131
|
+
"root",
|
|
2132
|
+
"header",
|
|
2133
|
+
"title",
|
|
2134
|
+
"description",
|
|
2135
|
+
"pageTitle",
|
|
2136
|
+
"progress",
|
|
2137
|
+
"progressFill",
|
|
2138
|
+
"blocks",
|
|
2139
|
+
"field",
|
|
2140
|
+
"label",
|
|
2141
|
+
"fieldDescription",
|
|
2142
|
+
"control",
|
|
2143
|
+
"options",
|
|
2144
|
+
"option",
|
|
2145
|
+
"optionLabel",
|
|
2146
|
+
"error",
|
|
2147
|
+
"footer",
|
|
2148
|
+
"button",
|
|
2149
|
+
"success"
|
|
2150
|
+
];
|
|
2151
|
+
var FILLO_DATA_ATTRS = {
|
|
2152
|
+
/** Slot name, on every slot element: `data-fillo="control"`. */
|
|
2153
|
+
slot: "data-fillo",
|
|
2154
|
+
/** Field kind, on the field wrapper: `data-kind="email"`. */
|
|
2155
|
+
kind: "data-kind",
|
|
2156
|
+
/** Field id, on the field wrapper (pre-existing). */
|
|
2157
|
+
field: "data-field",
|
|
2158
|
+
invalid: "data-invalid",
|
|
2159
|
+
required: "data-required",
|
|
2160
|
+
/** Selected option row / active scale step / active star. */
|
|
2161
|
+
selected: "data-selected",
|
|
2162
|
+
/** Checkbox / toggle checked state. */
|
|
2163
|
+
checked: "data-checked",
|
|
2164
|
+
dragOver: "data-drag-over",
|
|
2165
|
+
/** Engine status on the root: idle | submitting | submitted | error. */
|
|
2166
|
+
state: "data-state",
|
|
2167
|
+
page: "data-page",
|
|
2168
|
+
lastPage: "data-last-page"
|
|
2169
|
+
};
|
|
2170
|
+
function resolveSlotClass(appearance, state) {
|
|
2171
|
+
if (!appearance?.classNames && !appearance?.fields) return "";
|
|
2172
|
+
const parts = [];
|
|
2173
|
+
const general = appearance.classNames?.[state.slot];
|
|
2174
|
+
if (general) parts.push(typeof general === "function" ? general(state) : general);
|
|
2175
|
+
const field2 = state.fieldId ? appearance.fields?.[state.fieldId]?.[state.slot] : void 0;
|
|
2176
|
+
if (field2) parts.push(typeof field2 === "function" ? field2(state) : field2);
|
|
2177
|
+
return parts.filter(Boolean).join(" ");
|
|
2178
|
+
}
|
|
2179
|
+
function slotClass(base, appearance, state) {
|
|
2180
|
+
const extra = resolveSlotClass(appearance, state);
|
|
2181
|
+
return extra ? `${base} ${extra}` : base;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
// src/strings.ts
|
|
2185
|
+
var DEFAULT_STRINGS = {
|
|
2186
|
+
back: "Back",
|
|
2187
|
+
next: "Next",
|
|
2188
|
+
submit: "Submit",
|
|
2189
|
+
submitting: "Submitting\u2026",
|
|
2190
|
+
uploading: "Uploading\u2026",
|
|
2191
|
+
optional: " (optional)",
|
|
2192
|
+
other: "Other",
|
|
2193
|
+
otherPrompt: "Other \u2014 please specify",
|
|
2194
|
+
otherPlaceholder: "Your answer",
|
|
2195
|
+
choosePlaceholder: "Choose\u2026",
|
|
2196
|
+
successTitle: "Thanks!",
|
|
2197
|
+
successMessage: "Your response has been recorded.",
|
|
2198
|
+
closed: "This form is no longer accepting responses.",
|
|
2199
|
+
notLive: "This form isn't live yet.",
|
|
2200
|
+
submitFailed: "This form can't submit right now. Please try again in a moment.",
|
|
2201
|
+
loadFailedNotFound: "Form not found \u2014 check the form id and that it's published.",
|
|
2202
|
+
loadFailedNetwork: "Couldn't reach the server \u2014 check your connection or CORS.",
|
|
2203
|
+
loadFailed: "This form could not be loaded.",
|
|
2204
|
+
renderFailed: "This form could not be rendered."
|
|
2205
|
+
};
|
|
2206
|
+
function resolveStrings(overrides) {
|
|
2207
|
+
return overrides ? { ...DEFAULT_STRINGS, ...overrides } : DEFAULT_STRINGS;
|
|
2208
|
+
}
|
|
2209
|
+
|
|
1971
2210
|
// src/define.ts
|
|
1972
2211
|
function defineForm(def) {
|
|
1973
2212
|
return {
|
|
@@ -1987,83 +2226,376 @@ function isCodeForm(form) {
|
|
|
1987
2226
|
return typeof form === "object" && form !== null && "__filloCodeForm" in form;
|
|
1988
2227
|
}
|
|
1989
2228
|
var syncCache = /* @__PURE__ */ new Map();
|
|
2229
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2230
|
+
async function syncWithRetry(client, form) {
|
|
2231
|
+
let delay = 800;
|
|
2232
|
+
for (let attempt = 0; ; attempt++) {
|
|
2233
|
+
try {
|
|
2234
|
+
return await client.syncForm(form.id, form.schema, form.theme ?? null);
|
|
2235
|
+
} catch (err) {
|
|
2236
|
+
const status = isFilloError(err) ? err.status ?? 0 : 0;
|
|
2237
|
+
const retryable = !isFilloError(err) || status === 0 || status === 408 || status === 429 || status >= 500;
|
|
2238
|
+
if (!retryable || attempt >= 2) throw err;
|
|
2239
|
+
const hinted = isFilloError(err) ? err.retryAfterSec : void 0;
|
|
2240
|
+
await sleep(Math.min(hinted ? hinted * 1e3 : delay * (1 + Math.random() * 0.5), 15e3));
|
|
2241
|
+
delay *= 2;
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
1990
2245
|
function contentHash(input) {
|
|
1991
2246
|
let h = 5381;
|
|
1992
2247
|
for (let i = 0; i < input.length; i++) h = (h << 5) + h + input.charCodeAt(i) | 0;
|
|
1993
2248
|
return (h >>> 0).toString(36);
|
|
1994
2249
|
}
|
|
1995
|
-
function
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2250
|
+
function storageKey(client, handle) {
|
|
2251
|
+
return `fillo:sync:${client.baseUrl}|${client.key}|${handle}`;
|
|
2252
|
+
}
|
|
2253
|
+
function readStoredSync(client, handle, hash) {
|
|
2254
|
+
try {
|
|
2255
|
+
const raw = globalThis.localStorage?.getItem(storageKey(client, handle));
|
|
2256
|
+
if (!raw) return null;
|
|
2257
|
+
const parsed = JSON.parse(raw);
|
|
2258
|
+
if (typeof parsed.formId !== "string" || parsed.hash !== hash || typeof parsed.ts !== "number")
|
|
2259
|
+
return null;
|
|
2260
|
+
const ttl = parsed.status === "published" ? 36e5 : 6e4;
|
|
2261
|
+
if (Date.now() - parsed.ts > ttl) return null;
|
|
2262
|
+
return parsed;
|
|
2263
|
+
} catch {
|
|
2264
|
+
return null;
|
|
2003
2265
|
}
|
|
2266
|
+
}
|
|
2267
|
+
function writeStoredSync(client, handle, hash, r) {
|
|
2268
|
+
try {
|
|
2269
|
+
globalThis.localStorage?.setItem(
|
|
2270
|
+
storageKey(client, handle),
|
|
2271
|
+
JSON.stringify({
|
|
2272
|
+
formId: r.formId,
|
|
2273
|
+
slug: r.slug,
|
|
2274
|
+
status: r.status,
|
|
2275
|
+
hash,
|
|
2276
|
+
ts: Date.now()
|
|
2277
|
+
})
|
|
2278
|
+
);
|
|
2279
|
+
} catch {
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
function syncCodeForm(client, form, opts) {
|
|
2283
|
+
const content2 = JSON.stringify(form.schema) + JSON.stringify(form.theme ?? null);
|
|
2284
|
+
const hash = contentHash(content2);
|
|
2285
|
+
const key = `${client.baseUrl}:${client.key}:${form.id}:${hash}`;
|
|
2286
|
+
let pending = syncCache.get(key);
|
|
2287
|
+
if (pending && !opts?.bypassCache) return pending;
|
|
2288
|
+
if (!opts?.bypassCache) {
|
|
2289
|
+
const stored = readStoredSync(client, form.id, hash);
|
|
2290
|
+
if (stored) {
|
|
2291
|
+
pending = Promise.resolve({ formId: stored.formId, slug: stored.slug, status: stored.status });
|
|
2292
|
+
syncCache.set(key, pending);
|
|
2293
|
+
return pending;
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
pending = syncWithRetry(client, form).then((r) => {
|
|
2297
|
+
writeStoredSync(client, form.id, hash, r);
|
|
2298
|
+
return r;
|
|
2299
|
+
});
|
|
2300
|
+
syncCache.set(key, pending);
|
|
2301
|
+
pending.catch(() => syncCache.delete(key));
|
|
2004
2302
|
return pending;
|
|
2005
2303
|
}
|
|
2006
2304
|
|
|
2007
|
-
// src/
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2305
|
+
// src/jsx.ts
|
|
2306
|
+
var FilloJsxError = class extends Error {
|
|
2307
|
+
constructor(code, message) {
|
|
2308
|
+
super(`[fillo] ${message} \u2014 https://fillo.so/docs/troubleshooting#jsx-${code.toLowerCase()}`);
|
|
2309
|
+
this.code = code;
|
|
2310
|
+
this.name = "FilloJsxError";
|
|
2311
|
+
}
|
|
2312
|
+
code;
|
|
2313
|
+
};
|
|
2314
|
+
var BRAND = /* @__PURE__ */ Symbol.for("fillo.block");
|
|
2315
|
+
var REACT_FRAGMENT = /* @__PURE__ */ Symbol.for("react.fragment");
|
|
2316
|
+
var FIELD_BASE = ["label", "description", "required", "placeholder"];
|
|
2317
|
+
var field = (name, kind, extra, opts = {}) => ({
|
|
2318
|
+
name,
|
|
2319
|
+
kind,
|
|
2320
|
+
props: [...FIELD_BASE, ...extra],
|
|
2321
|
+
...opts
|
|
2322
|
+
});
|
|
2323
|
+
var content = (name, kind, textChildren) => ({
|
|
2324
|
+
name,
|
|
2325
|
+
kind,
|
|
2326
|
+
props: textChildren ? ["text"] : [],
|
|
2327
|
+
textChildren,
|
|
2328
|
+
content: true
|
|
2329
|
+
});
|
|
2330
|
+
var CHOICE_EXTRA = ["options", "allowOther", "shuffleOptions"];
|
|
2331
|
+
var JSX_BLOCK_SPECS = [
|
|
2332
|
+
field("Text", "short_text", ["maxLength"]),
|
|
2333
|
+
field("LongText", "long_text", ["maxLength"]),
|
|
2334
|
+
field("Email", "email", ["maxLength"]),
|
|
2335
|
+
field("Url", "url", ["maxLength"]),
|
|
2336
|
+
field("Phone", "phone", ["defaultCountry"]),
|
|
2337
|
+
field("Number", "number", ["min", "max"]),
|
|
2338
|
+
field("Select", "select", CHOICE_EXTRA, { optionChildren: true }),
|
|
2339
|
+
field("MultiSelect", "multi_select", CHOICE_EXTRA, { optionChildren: true }),
|
|
2340
|
+
field("Dropdown", "dropdown", CHOICE_EXTRA, { optionChildren: true }),
|
|
2341
|
+
field("Checkbox", "checkbox", ["appearance"]),
|
|
2342
|
+
field("Rating", "rating", ["max"]),
|
|
2343
|
+
field("Scale", "linear_scale", ["min", "max", "minLabel", "maxLabel"]),
|
|
2344
|
+
field("Ranking", "ranking", ["options"], { optionChildren: true }),
|
|
2345
|
+
field("Matrix", "matrix", ["rows", "columns"]),
|
|
2346
|
+
field("Signature", "signature", []),
|
|
2347
|
+
field("Date", "date", []),
|
|
2348
|
+
field("FileUpload", "file_upload", ["maxFiles", "maxFileSizeMb", "accept"]),
|
|
2349
|
+
field("Hidden", "hidden", ["paramName", "defaultValue"]),
|
|
2350
|
+
field("Custom", "custom", ["component", "config"]),
|
|
2351
|
+
content("Heading", "heading", true),
|
|
2352
|
+
content("Paragraph", "paragraph", true),
|
|
2353
|
+
{ name: "Divider", kind: "divider", props: [], content: true }
|
|
2354
|
+
];
|
|
2355
|
+
var PAGE_SPEC = { name: "Page", kind: "__page", props: ["title"] };
|
|
2356
|
+
var OPTION_SPEC = { name: "Option", kind: "__option", props: ["label", "icon"] };
|
|
2357
|
+
function makeInert(spec) {
|
|
2358
|
+
const component = () => {
|
|
2359
|
+
throw new FilloJsxError(
|
|
2360
|
+
"RENDERED_INERT",
|
|
2361
|
+
`<Fillo.${spec.name}> was rendered by React. Fillo fields are schema declarations, valid only inside <Fillo.Form> or Fillo.defineForm(). If this form lives in a server component, move it to a client module ("use client")`
|
|
2362
|
+
);
|
|
2363
|
+
};
|
|
2364
|
+
component[BRAND] = spec;
|
|
2365
|
+
return component;
|
|
2366
|
+
}
|
|
2367
|
+
var JSX_BLOCK_COMPONENTS = Object.fromEntries([
|
|
2368
|
+
...JSX_BLOCK_SPECS.map((spec) => [spec.name, makeInert(spec)]),
|
|
2369
|
+
["Page", makeInert(PAGE_SPEC)],
|
|
2370
|
+
["Option", makeInert(OPTION_SPEC)]
|
|
2371
|
+
]);
|
|
2372
|
+
var isElement = (v) => typeof v === "object" && v !== null && "type" in v && "props" in v;
|
|
2373
|
+
var brandOf = (type) => typeof type === "function" ? type[BRAND] : void 0;
|
|
2374
|
+
function flatten(children, out = []) {
|
|
2375
|
+
if (children === null || children === void 0 || typeof children === "boolean") return out;
|
|
2376
|
+
if (Array.isArray(children)) {
|
|
2377
|
+
for (const child of children) flatten(child, out);
|
|
2378
|
+
return out;
|
|
2379
|
+
}
|
|
2380
|
+
if (isElement(children)) {
|
|
2381
|
+
if (children.type === REACT_FRAGMENT) return flatten(children.props.children, out);
|
|
2382
|
+
out.push(children);
|
|
2383
|
+
return out;
|
|
2384
|
+
}
|
|
2385
|
+
throw new FilloJsxError(
|
|
2386
|
+
"NON_FILLO_CHILD",
|
|
2387
|
+
`Unexpected ${typeof children} child inside <Fillo.Form> \u2014 children define the schema; put text in <Fillo.Paragraph> and layout outside the form`
|
|
2388
|
+
);
|
|
2013
2389
|
}
|
|
2014
|
-
function
|
|
2015
|
-
const
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2390
|
+
function classify(el) {
|
|
2391
|
+
const spec = brandOf(el.type);
|
|
2392
|
+
if (spec) return spec;
|
|
2393
|
+
const t = el.type;
|
|
2394
|
+
if (typeof t === "string") {
|
|
2395
|
+
throw new FilloJsxError(
|
|
2396
|
+
"NON_FILLO_CHILD",
|
|
2397
|
+
`<${t}> is layout, and <Fillo.Form> children define the schema only. Put markup outside the form, or compose your own layout with FilloProvider (headless)`
|
|
2398
|
+
);
|
|
2399
|
+
}
|
|
2400
|
+
if (typeof t === "object" && t !== null) {
|
|
2401
|
+
throw new FilloJsxError(
|
|
2402
|
+
"OPAQUE_TYPE",
|
|
2403
|
+
`A child of <Fillo.Form> has an opaque component type (lazy/memo or a server-component boundary). Define the form in a client module ("use client") and pass the form VALUE across boundaries instead`
|
|
2404
|
+
);
|
|
2405
|
+
}
|
|
2406
|
+
const name = typeof t === "function" && t.name ? ` (${t.name})` : "";
|
|
2407
|
+
throw new FilloJsxError(
|
|
2408
|
+
"WRAPPER_COMPONENT",
|
|
2409
|
+
`A plain component${name} is not a Fillo field \u2014 <Fillo.Form> never renders children, so wrappers are invisible to it. For reuse, call a plain function that RETURNS Fillo elements: {contactFields()}`
|
|
2410
|
+
);
|
|
2411
|
+
}
|
|
2412
|
+
function buildProps(spec, el) {
|
|
2413
|
+
const props = el.props;
|
|
2414
|
+
const allowed = /* @__PURE__ */ new Set([...spec.props, "id", "visibleIf", "children"]);
|
|
2415
|
+
for (const key of Object.keys(props)) {
|
|
2416
|
+
if (props[key] === void 0) continue;
|
|
2417
|
+
if (!allowed.has(key)) {
|
|
2418
|
+
const hint = key === "className" || key === "style" ? `styling lives on <Fillo.Form appearance={\u2026}>, never in the schema` : `schema props for Fillo.${spec.name}: ${[...spec.props].join(", ")}`;
|
|
2419
|
+
throw new FilloJsxError("UNKNOWN_PROP", `Fillo.${spec.name} has no prop "${key}" \u2014 ${hint}`);
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
const id = props.id;
|
|
2423
|
+
if (typeof id !== "string" || id.length === 0 || id.length > 128) {
|
|
2424
|
+
throw new FilloJsxError(
|
|
2425
|
+
"MISSING_ID",
|
|
2426
|
+
`Every Fillo.${spec.name} needs a stable string id (1-128 chars) \u2014 ids key responses, logic, piping, and sync; never derive them from position`
|
|
2427
|
+
);
|
|
2428
|
+
}
|
|
2429
|
+
const out = { id, kind: spec.kind };
|
|
2430
|
+
if (spec.optionChildren && props.children !== void 0) {
|
|
2431
|
+
if (props.options !== void 0) {
|
|
2432
|
+
throw new FilloJsxError(
|
|
2433
|
+
"OPTION_PROP_CONFLICT",
|
|
2434
|
+
`Fillo.${spec.name} "${id}" has both an options prop and <Fillo.Option> children \u2014 pick one`
|
|
2435
|
+
);
|
|
2436
|
+
}
|
|
2437
|
+
out.__childOptions = flatten(props.children).map((child) => buildOption(child, spec, id));
|
|
2438
|
+
} else if (spec.textChildren && props.children !== void 0) {
|
|
2439
|
+
const parts = Array.isArray(props.children) ? props.children : [props.children];
|
|
2440
|
+
if (!parts.every((p) => typeof p === "string" || typeof p === "number")) {
|
|
2441
|
+
throw new FilloJsxError(
|
|
2442
|
+
"TEXT_CHILD_REQUIRED",
|
|
2443
|
+
`Fillo.${spec.name} children must be plain text (piping tokens like {{name}} included) \u2014 expressions and elements can't serialize into the schema`
|
|
2444
|
+
);
|
|
2445
|
+
}
|
|
2446
|
+
if (props.text !== void 0) {
|
|
2447
|
+
throw new FilloJsxError(
|
|
2448
|
+
"OPTION_PROP_CONFLICT",
|
|
2449
|
+
`Fillo.${spec.name} "${id}" has both a text prop and children \u2014 pick one`
|
|
2450
|
+
);
|
|
2451
|
+
}
|
|
2452
|
+
out.__childText = parts.join("");
|
|
2453
|
+
} else if (props.children !== void 0) {
|
|
2454
|
+
throw new FilloJsxError(
|
|
2455
|
+
"NON_FILLO_CHILD",
|
|
2456
|
+
`Fillo.${spec.name} takes no children \u2014 its configuration is props-only`
|
|
2457
|
+
);
|
|
2458
|
+
}
|
|
2459
|
+
for (const key of spec.props) {
|
|
2460
|
+
if (key === "options" && out.__childOptions !== void 0) {
|
|
2461
|
+
out.options = out.__childOptions;
|
|
2462
|
+
continue;
|
|
2463
|
+
}
|
|
2464
|
+
if (key === "text" && out.__childText !== void 0) {
|
|
2465
|
+
out.text = out.__childText;
|
|
2466
|
+
continue;
|
|
2467
|
+
}
|
|
2468
|
+
if (props[key] !== void 0) out[key] = props[key];
|
|
2469
|
+
}
|
|
2470
|
+
delete out.__childOptions;
|
|
2471
|
+
delete out.__childText;
|
|
2472
|
+
if (spec.textChildren && out.text === void 0) {
|
|
2473
|
+
throw new FilloJsxError("TEXT_CHILD_REQUIRED", `Fillo.${spec.name} "${id}" has no text`);
|
|
2474
|
+
}
|
|
2475
|
+
if (props.visibleIf !== void 0) {
|
|
2476
|
+
const conditions2 = Array.isArray(props.visibleIf) ? props.visibleIf : [props.visibleIf];
|
|
2477
|
+
out.visibleIf = conditions2;
|
|
2478
|
+
}
|
|
2479
|
+
return out;
|
|
2480
|
+
}
|
|
2481
|
+
function buildOption(el, parent, parentId) {
|
|
2482
|
+
const spec = classify(el);
|
|
2483
|
+
if (spec.kind !== "__option") {
|
|
2484
|
+
throw new FilloJsxError(
|
|
2485
|
+
"NON_FILLO_CHILD",
|
|
2486
|
+
`Fillo.${parent.name} "${parentId}" children must be <Fillo.Option> elements, found Fillo.${spec.name}`
|
|
2487
|
+
);
|
|
2488
|
+
}
|
|
2489
|
+
const props = el.props;
|
|
2490
|
+
for (const key of Object.keys(props)) {
|
|
2491
|
+
if (props[key] !== void 0 && key !== "id" && key !== "label" && key !== "icon") {
|
|
2492
|
+
throw new FilloJsxError("UNKNOWN_PROP", `Fillo.Option has no prop "${key}" \u2014 it takes id, label, icon`);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
if (typeof props.id !== "string" || props.id.length === 0) {
|
|
2496
|
+
throw new FilloJsxError(
|
|
2497
|
+
"MISSING_ID",
|
|
2498
|
+
`Every <Fillo.Option> in "${parentId}" needs a stable string id \u2014 it's the stored answer value`
|
|
2499
|
+
);
|
|
2500
|
+
}
|
|
2501
|
+
if (typeof props.label !== "string") {
|
|
2502
|
+
throw new FilloJsxError("MISSING_ID", `<Fillo.Option id="${props.id}"> needs a label`);
|
|
2503
|
+
}
|
|
2504
|
+
const option = { id: props.id, label: props.label };
|
|
2505
|
+
if (props.icon !== void 0) option.icon = props.icon;
|
|
2506
|
+
return option;
|
|
2507
|
+
}
|
|
2508
|
+
function schemaFromJsx(children, meta) {
|
|
2509
|
+
const elements = flatten(children);
|
|
2510
|
+
const pageSpecs = elements.map((el) => ({ el, spec: classify(el) }));
|
|
2511
|
+
const pages = [];
|
|
2512
|
+
const isPage = (s) => s.kind === "__page";
|
|
2513
|
+
if (pageSpecs.some(({ spec }) => isPage(spec))) {
|
|
2514
|
+
if (!pageSpecs.every(({ spec }) => isPage(spec))) {
|
|
2515
|
+
throw new FilloJsxError(
|
|
2516
|
+
"PAGE_MIX",
|
|
2517
|
+
`<Fillo.Form> children must be EITHER all <Fillo.Page> elements or all blocks \u2014 wrap the loose blocks in a page`
|
|
2518
|
+
);
|
|
2519
|
+
}
|
|
2520
|
+
for (const { el } of pageSpecs) {
|
|
2521
|
+
const built = buildProps(PAGE_SPEC, { type: el.type, props: { ...el.props, children: void 0 } });
|
|
2522
|
+
const blocks = flatten(el.props.children).map((child) => {
|
|
2523
|
+
const spec = classify(child);
|
|
2524
|
+
if (isPage(spec) || spec.kind === "__option") {
|
|
2525
|
+
throw new FilloJsxError("PAGE_MIX", `<Fillo.${spec.name}> can't appear inside <Fillo.Page>`);
|
|
2526
|
+
}
|
|
2527
|
+
return buildProps(spec, child);
|
|
2528
|
+
});
|
|
2529
|
+
pages.push({
|
|
2530
|
+
id: built.id,
|
|
2531
|
+
...built.title !== void 0 ? { title: built.title } : {},
|
|
2532
|
+
blocks
|
|
2533
|
+
});
|
|
2534
|
+
}
|
|
2535
|
+
} else {
|
|
2536
|
+
const blocks = pageSpecs.map(({ el, spec }) => {
|
|
2537
|
+
if (spec.kind === "__option") {
|
|
2538
|
+
throw new FilloJsxError(
|
|
2539
|
+
"NON_FILLO_CHILD",
|
|
2540
|
+
`<Fillo.Option> only makes sense inside a choice field (Select/MultiSelect/Dropdown/Ranking)`
|
|
2541
|
+
);
|
|
2048
2542
|
}
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2543
|
+
return buildProps(spec, el);
|
|
2544
|
+
});
|
|
2545
|
+
pages.push({ id: "main", blocks });
|
|
2546
|
+
}
|
|
2547
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2548
|
+
for (const page of pages) {
|
|
2549
|
+
for (const block of page.blocks) {
|
|
2550
|
+
if (seen.has(block.id)) {
|
|
2551
|
+
throw new FilloJsxError(
|
|
2552
|
+
"DUPLICATE_ID",
|
|
2553
|
+
`Two blocks share the id "${block.id}" \u2014 ids key responses and must be unique across the whole form`
|
|
2554
|
+
);
|
|
2054
2555
|
}
|
|
2055
|
-
|
|
2056
|
-
case "ranking":
|
|
2057
|
-
case "matrix":
|
|
2058
|
-
case "signature":
|
|
2059
|
-
case "custom":
|
|
2060
|
-
break;
|
|
2061
|
-
// not prefillable from a URL
|
|
2062
|
-
default:
|
|
2063
|
-
data[field.id] = raw;
|
|
2556
|
+
seen.add(block.id);
|
|
2064
2557
|
}
|
|
2065
2558
|
}
|
|
2066
|
-
return
|
|
2559
|
+
return {
|
|
2560
|
+
version: 1,
|
|
2561
|
+
title: meta.title ?? "",
|
|
2562
|
+
...meta.description !== void 0 ? { description: meta.description } : {},
|
|
2563
|
+
pages,
|
|
2564
|
+
settings: meta.settings ?? {}
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
function codeFormFromJsx(meta, children) {
|
|
2568
|
+
if (typeof meta.id !== "string" || meta.id.length === 0) {
|
|
2569
|
+
throw new FilloJsxError("MISSING_ID", `<Fillo.Form> needs an id \u2014 it names the form in your workspace`);
|
|
2570
|
+
}
|
|
2571
|
+
const schema = schemaFromJsx(children, {
|
|
2572
|
+
id: meta.id,
|
|
2573
|
+
title: meta.title,
|
|
2574
|
+
description: meta.description,
|
|
2575
|
+
settings: meta.settings
|
|
2576
|
+
});
|
|
2577
|
+
return defineForm({
|
|
2578
|
+
id: meta.id,
|
|
2579
|
+
title: schema.title,
|
|
2580
|
+
description: schema.description,
|
|
2581
|
+
pages: schema.pages,
|
|
2582
|
+
settings: schema.settings,
|
|
2583
|
+
theme: meta.theme
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
// src/when.ts
|
|
2588
|
+
function when(fieldId) {
|
|
2589
|
+
const cond = (op, value) => Object.freeze(value === void 0 ? { fieldId, op } : { fieldId, op, value });
|
|
2590
|
+
return {
|
|
2591
|
+
eq: (value) => cond("eq", value),
|
|
2592
|
+
neq: (value) => cond("neq", value),
|
|
2593
|
+
contains: (value) => cond("contains", value),
|
|
2594
|
+
gt: (value) => cond("gt", value),
|
|
2595
|
+
lt: (value) => cond("lt", value),
|
|
2596
|
+
answered: () => cond("answered"),
|
|
2597
|
+
notAnswered: () => cond("not_answered")
|
|
2598
|
+
};
|
|
2067
2599
|
}
|
|
2068
2600
|
|
|
2069
2601
|
// src/piping.ts
|
|
@@ -2072,10 +2604,10 @@ function resolveText(text, data, form) {
|
|
|
2072
2604
|
if (!text.includes("{{")) return text;
|
|
2073
2605
|
const fields = allFields(form);
|
|
2074
2606
|
return text.replace(TOKEN, (_, id) => {
|
|
2075
|
-
const
|
|
2607
|
+
const field2 = fields.find((f) => f.id === id);
|
|
2076
2608
|
const value = data[id];
|
|
2077
2609
|
if (value === null || value === void 0 || value === "") return "";
|
|
2078
|
-
const label = (optionId) =>
|
|
2610
|
+
const label = (optionId) => field2 && "options" in field2 ? field2.options.find((o) => o.id === optionId)?.label ?? optionId : optionId;
|
|
2079
2611
|
if (Array.isArray(value)) return value.map((v) => label(String(v))).join(", ");
|
|
2080
2612
|
if (typeof value === "string") return label(value);
|
|
2081
2613
|
return String(value);
|
|
@@ -2096,15 +2628,22 @@ function pipeBlock(block, data, form) {
|
|
|
2096
2628
|
export {
|
|
2097
2629
|
BLOCK_KIND_META,
|
|
2098
2630
|
CONTENT_KINDS,
|
|
2631
|
+
DEFAULT_STRINGS,
|
|
2099
2632
|
DRAFT_KINDS,
|
|
2633
|
+
FILLO_DATA_ATTRS,
|
|
2100
2634
|
FILLO_SCHEMA_VERSION,
|
|
2101
2635
|
FILLO_SDK_VERSION,
|
|
2636
|
+
FILLO_SLOTS,
|
|
2102
2637
|
FilloClient,
|
|
2103
2638
|
FilloError,
|
|
2639
|
+
FilloJsxError,
|
|
2640
|
+
JSX_BLOCK_COMPONENTS,
|
|
2641
|
+
JSX_BLOCK_SPECS,
|
|
2104
2642
|
PHONE_COUNTRIES,
|
|
2105
2643
|
Sha1,
|
|
2106
2644
|
allFields,
|
|
2107
2645
|
assembleForm,
|
|
2646
|
+
codeFormFromJsx,
|
|
2108
2647
|
contentHash,
|
|
2109
2648
|
countryByDialCode,
|
|
2110
2649
|
countryByIso,
|
|
@@ -2119,23 +2658,32 @@ export {
|
|
|
2119
2658
|
flagEmoji,
|
|
2120
2659
|
formatAnswer,
|
|
2121
2660
|
formatNational,
|
|
2661
|
+
isAutoSubmitBlock,
|
|
2122
2662
|
isBlockVisible,
|
|
2123
2663
|
isCodeForm,
|
|
2124
2664
|
isField,
|
|
2665
|
+
isFilloError,
|
|
2125
2666
|
isPossiblePhone,
|
|
2667
|
+
needsExplicitSubmit,
|
|
2126
2668
|
normalizeFormSchema,
|
|
2127
2669
|
normalizeFormTheme,
|
|
2128
2670
|
parsePhone,
|
|
2129
2671
|
pipeBlock,
|
|
2130
2672
|
prefillFromParams,
|
|
2131
2673
|
provisionWorkspace,
|
|
2674
|
+
resolveSlotClass,
|
|
2675
|
+
resolveStrings,
|
|
2132
2676
|
resolveText,
|
|
2677
|
+
schemaFromJsx,
|
|
2133
2678
|
sha1Base64,
|
|
2679
|
+
shouldAutoSubmit,
|
|
2680
|
+
slotClass,
|
|
2134
2681
|
syncCodeForm,
|
|
2135
2682
|
toE164,
|
|
2136
2683
|
validateField,
|
|
2137
2684
|
validateFormSchema,
|
|
2138
2685
|
validateResponse,
|
|
2139
2686
|
visibleBlocks,
|
|
2140
|
-
visibleFields
|
|
2687
|
+
visibleFields,
|
|
2688
|
+
when
|
|
2141
2689
|
};
|