@usefillo/dom 0.9.0 → 0.10.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/README.md +18 -14
- package/dist/index.d.ts +20 -0
- package/dist/index.js +280 -70
- package/dist/standalone.global.js +1 -1
- package/dist/styles.css +56 -0
- package/dist/styles.unlayered.css +56 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import {
|
|
3
|
+
DEFAULT_STRINGS,
|
|
4
|
+
DEFAULT_FIELD_STRINGS,
|
|
3
5
|
FilloClient,
|
|
4
6
|
FilloError,
|
|
5
7
|
PHONE_COUNTRIES,
|
|
@@ -12,9 +14,11 @@ import {
|
|
|
12
14
|
flagEmoji,
|
|
13
15
|
formSchemasEqual,
|
|
14
16
|
formatNational,
|
|
17
|
+
isBuildTimeDevEnv,
|
|
15
18
|
isCodeForm,
|
|
16
19
|
isField,
|
|
17
20
|
isFilloError,
|
|
21
|
+
isLikelyDevEnv,
|
|
18
22
|
normalizeFormSchema,
|
|
19
23
|
normalizeFormTheme,
|
|
20
24
|
parsePhone,
|
|
@@ -1040,6 +1044,8 @@ function signature(context) {
|
|
|
1040
1044
|
function fileUpload(context) {
|
|
1041
1045
|
const field = context.field;
|
|
1042
1046
|
if (field.kind !== "file_upload") return null;
|
|
1047
|
+
const internal = context;
|
|
1048
|
+
const storageBlocked = internal.storageBlocked === true;
|
|
1043
1049
|
const input = el("input", {
|
|
1044
1050
|
className: "fillo-input",
|
|
1045
1051
|
attrs: { id: context.ids.inputId, type: "file", "data-fillo": "control" }
|
|
@@ -1047,14 +1053,32 @@ function fileUpload(context) {
|
|
|
1047
1053
|
if ((field.maxFiles ?? 1) > 1) input.multiple = true;
|
|
1048
1054
|
if (field.accept?.length) input.accept = field.accept.join(",");
|
|
1049
1055
|
const progress = context.uploadProgress?.get(field.id);
|
|
1050
|
-
input.disabled = progress !== void 0;
|
|
1056
|
+
input.disabled = progress !== void 0 || storageBlocked;
|
|
1051
1057
|
applyFieldAria(input, field, context.error, context.ids);
|
|
1052
1058
|
input.addEventListener("change", () => {
|
|
1059
|
+
if (storageBlocked) return;
|
|
1053
1060
|
const files = Array.from(input.files ?? []);
|
|
1054
1061
|
void context.uploadFiles?.(field, files);
|
|
1055
1062
|
});
|
|
1056
1063
|
const wrap = el("div");
|
|
1057
1064
|
wrap.appendChild(input);
|
|
1065
|
+
if (storageBlocked) {
|
|
1066
|
+
const hint = el("p", {
|
|
1067
|
+
className: "fillo-upload-notice",
|
|
1068
|
+
text: internal.storageFixUrl ? "Connect file storage to enable uploads" : DEFAULT_FIELD_STRINGS.uploadsUnavailable,
|
|
1069
|
+
attrs: { role: "status" }
|
|
1070
|
+
});
|
|
1071
|
+
if (internal.storageFixUrl) {
|
|
1072
|
+
hint.append(" \u2014 ");
|
|
1073
|
+
hint.appendChild(
|
|
1074
|
+
el("a", {
|
|
1075
|
+
text: internal.storageFixUrl,
|
|
1076
|
+
attrs: { href: internal.storageFixUrl, target: "_blank", rel: "noopener noreferrer" }
|
|
1077
|
+
})
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
wrap.appendChild(hint);
|
|
1081
|
+
}
|
|
1058
1082
|
if (progress !== void 0) {
|
|
1059
1083
|
const meter = el("div", {
|
|
1060
1084
|
className: "fillo-upload-progress",
|
|
@@ -1127,6 +1151,7 @@ var DomFormController = class {
|
|
|
1127
1151
|
this.target = target;
|
|
1128
1152
|
this.options = options;
|
|
1129
1153
|
this.element.className = "fillo-dom-root";
|
|
1154
|
+
if (options.preview === true) warnPreviewInProduction();
|
|
1130
1155
|
target.replaceChildren(this.element);
|
|
1131
1156
|
void this.resolve();
|
|
1132
1157
|
}
|
|
@@ -1162,6 +1187,13 @@ var DomFormController = class {
|
|
|
1162
1187
|
// Teardown callbacks for open overlays (phone country popover) whose listeners
|
|
1163
1188
|
// live on window/document and would otherwise leak past a re-render/unmount.
|
|
1164
1189
|
overlays = /* @__PURE__ */ new Set();
|
|
1190
|
+
/** Dev chrome = every developer-facing surface below (inline schema render,
|
|
1191
|
+
* notices, verbose submit errors, upload pre-emption). `preview` forces it
|
|
1192
|
+
* on off-localhost surfaces — cosmetic only, so correctness (where
|
|
1193
|
+
* submissions go, whether they are accepted) never depends on it. */
|
|
1194
|
+
devChrome() {
|
|
1195
|
+
return this.options.preview === true || isDevEnv();
|
|
1196
|
+
}
|
|
1165
1197
|
get status() {
|
|
1166
1198
|
if (this.phase === "ready") return this.engine?.getState().status ?? "idle";
|
|
1167
1199
|
return this.phase;
|
|
@@ -1199,13 +1231,29 @@ var DomFormController = class {
|
|
|
1199
1231
|
};
|
|
1200
1232
|
}
|
|
1201
1233
|
resolveFormIdFn;
|
|
1202
|
-
/**
|
|
1203
|
-
|
|
1234
|
+
/** The server refuses submissions right now — render the real form blurred
|
|
1235
|
+
* and display-only beneath the not-open card ("notOpen" for a draft /
|
|
1236
|
+
* storage-blocked form, "closed" for an expired/capped workspace). Doubles
|
|
1237
|
+
* as the display-only gate: while set, the engine is created without a
|
|
1238
|
+
* client/formId/resolver and every interactive entry point no-ops. */
|
|
1239
|
+
notOpenVariant = null;
|
|
1204
1240
|
/** Exact integration failure retained for custom error/dev surfaces. */
|
|
1205
1241
|
fatalError = null;
|
|
1206
1242
|
genericUnavailable = false;
|
|
1207
1243
|
syncErrorNotice = null;
|
|
1208
1244
|
stagedNotice = false;
|
|
1245
|
+
/** Dev chrome: the code form is a draft — a visible banner, not console-only. */
|
|
1246
|
+
draftNotice = false;
|
|
1247
|
+
/** Sync's owner-facing storage advisory + dashboard deep-link. It never
|
|
1248
|
+
* substitutes for the explicit uploadsAvailable control state. */
|
|
1249
|
+
syncWarningCode;
|
|
1250
|
+
syncWarningUrl;
|
|
1251
|
+
/** Server-authoritative ability to start a new file upload. */
|
|
1252
|
+
uploadsAvailable;
|
|
1253
|
+
/** Code of the last submit-time resolution failure (form_not_published,
|
|
1254
|
+
* form_schema_changed, …) — the connect-storage deep-link renders only with
|
|
1255
|
+
* the failure it explains, never glued onto transport/challenge errors. */
|
|
1256
|
+
resolutionFailureCode = null;
|
|
1209
1257
|
async resolve() {
|
|
1210
1258
|
try {
|
|
1211
1259
|
const codeForm = isCodeForm(this.options.form) ? this.options.form : null;
|
|
@@ -1215,6 +1263,9 @@ var DomFormController = class {
|
|
|
1215
1263
|
this.resolveFormIdFn = async () => {
|
|
1216
1264
|
try {
|
|
1217
1265
|
const result = await syncCodeForm(client, codeForm, { bypassCache: true });
|
|
1266
|
+
this.syncWarningCode = result.warningCode;
|
|
1267
|
+
this.syncWarningUrl = result.warningUrl;
|
|
1268
|
+
this.uploadsAvailable = result.uploadsAvailable;
|
|
1218
1269
|
if (result.status === "draft") {
|
|
1219
1270
|
throw new FilloError(
|
|
1220
1271
|
"This form is no longer published. Submission was stopped before sending answers.",
|
|
@@ -1223,6 +1274,14 @@ var DomFormController = class {
|
|
|
1223
1274
|
"form_not_published"
|
|
1224
1275
|
);
|
|
1225
1276
|
}
|
|
1277
|
+
if (result.accepting === false) {
|
|
1278
|
+
throw new FilloError(
|
|
1279
|
+
"This form is no longer accepting responses. Submission was stopped before sending answers.",
|
|
1280
|
+
403,
|
|
1281
|
+
void 0,
|
|
1282
|
+
result.acceptingReason ?? "form_not_accepting"
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1226
1285
|
const authoritative = normalizeFormSchema(result.resolvedSchema ?? codeForm.schema);
|
|
1227
1286
|
if (!authoritative.ok) {
|
|
1228
1287
|
throw new FilloError(
|
|
@@ -1250,9 +1309,11 @@ var DomFormController = class {
|
|
|
1250
1309
|
)
|
|
1251
1310
|
);
|
|
1252
1311
|
}
|
|
1312
|
+
this.resolutionFailureCode = null;
|
|
1253
1313
|
return result.formId;
|
|
1254
1314
|
} catch (error) {
|
|
1255
1315
|
const failure = toFilloError(error);
|
|
1316
|
+
this.resolutionFailureCode = failure.code ?? "sync_failed";
|
|
1256
1317
|
this.reportSubmitSyncFailure(failure);
|
|
1257
1318
|
throw failure;
|
|
1258
1319
|
}
|
|
@@ -1273,7 +1334,7 @@ var DomFormController = class {
|
|
|
1273
1334
|
return;
|
|
1274
1335
|
}
|
|
1275
1336
|
if (!client?.key) return;
|
|
1276
|
-
if (
|
|
1337
|
+
if (this.devChrome()) {
|
|
1277
1338
|
this.setSchema(codeForm.schema, this.options.theme ?? codeForm.theme, this.options.formId);
|
|
1278
1339
|
} else {
|
|
1279
1340
|
this.theme = this.options.theme ?? codeForm.theme;
|
|
@@ -1290,9 +1351,13 @@ var DomFormController = class {
|
|
|
1290
1351
|
"sync_snapshot_required"
|
|
1291
1352
|
);
|
|
1292
1353
|
}
|
|
1293
|
-
const development =
|
|
1294
|
-
this.
|
|
1354
|
+
const development = this.devChrome();
|
|
1355
|
+
this.notOpenVariant = development ? null : result.status === "draft" ? "notOpen" : result.accepting === false ? overlayVariant(result.acceptingReason) : null;
|
|
1295
1356
|
this.stagedNotice = Boolean(result.staged);
|
|
1357
|
+
this.draftNotice = result.status === "draft";
|
|
1358
|
+
this.syncWarningCode = result.warningCode;
|
|
1359
|
+
this.syncWarningUrl = result.warningUrl;
|
|
1360
|
+
this.uploadsAvailable = result.uploadsAvailable;
|
|
1296
1361
|
if (result.branding) {
|
|
1297
1362
|
this.fetchedPoweredBy = result.branding.poweredBy;
|
|
1298
1363
|
this.fetchedBrandLabel = result.branding.label;
|
|
@@ -1318,10 +1383,11 @@ var DomFormController = class {
|
|
|
1318
1383
|
)
|
|
1319
1384
|
);
|
|
1320
1385
|
}
|
|
1386
|
+
const connectStorage = result.warningUrl ? ` \u2014 connect storage to publish: ${result.warningUrl}` : "";
|
|
1321
1387
|
if (result.staged) {
|
|
1322
|
-
console.info(`[fillo] "${codeForm.id}" changes staged as a draft \u2014 rendering the live version until you publish them in your Fillo dashboard`);
|
|
1388
|
+
console.info(`[fillo] "${codeForm.id}" changes staged as a draft \u2014 rendering the live version until you publish them in your Fillo dashboard${connectStorage}`);
|
|
1323
1389
|
} else if (result.status === "draft") {
|
|
1324
|
-
console.info(`[fillo] "${codeForm.id}" is a draft \u2014 it renders here but won't accept responses until published`);
|
|
1390
|
+
console.info(`[fillo] "${codeForm.id}" is a draft \u2014 it renders here but won't accept responses until published${connectStorage}`);
|
|
1325
1391
|
}
|
|
1326
1392
|
} catch (error) {
|
|
1327
1393
|
if (this.destroyed) return;
|
|
@@ -1341,10 +1407,22 @@ var DomFormController = class {
|
|
|
1341
1407
|
this.render();
|
|
1342
1408
|
const published = await this.options.client.getForm(this.options.formId);
|
|
1343
1409
|
if (this.destroyed) return;
|
|
1344
|
-
|
|
1410
|
+
if (published.accepting === false) {
|
|
1411
|
+
this.notOpenVariant = overlayVariant(published.acceptingReason);
|
|
1412
|
+
} else {
|
|
1413
|
+
this.closed = Boolean(published.closed);
|
|
1414
|
+
}
|
|
1415
|
+
this.uploadsAvailable = published.uploadsAvailable;
|
|
1345
1416
|
this.fetchedPoweredBy = published.branding?.poweredBy ?? null;
|
|
1346
1417
|
this.fetchedBrandLabel = published.branding?.label;
|
|
1347
1418
|
this.fetchedBrandHref = published.branding?.href;
|
|
1419
|
+
if (published.accepting === false && this.options.renderError) {
|
|
1420
|
+
const message = this.notOpenVariant === "closed" ? "This form is no longer accepting responses." : "This form isn't accepting responses yet.";
|
|
1421
|
+
this.fatalError = new FilloError(message, 403, void 0, published.acceptingReason);
|
|
1422
|
+
this.phase = "error";
|
|
1423
|
+
this.render(this.fatalError);
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1348
1426
|
this.setSchema(published.schema, this.options.theme ?? published.theme ?? void 0, published.id);
|
|
1349
1427
|
} catch (error) {
|
|
1350
1428
|
if (this.destroyed) return;
|
|
@@ -1360,17 +1438,21 @@ var DomFormController = class {
|
|
|
1360
1438
|
this.schema = normalizedSchema;
|
|
1361
1439
|
this.theme = theme;
|
|
1362
1440
|
this.formId = formId;
|
|
1441
|
+
const displayOnly = this.notOpenVariant !== null;
|
|
1363
1442
|
this.engine = coreCreateFormController({
|
|
1364
1443
|
form: normalizedSchema,
|
|
1365
|
-
formId,
|
|
1366
|
-
client: this.options.client,
|
|
1444
|
+
formId: displayOnly ? void 0 : formId,
|
|
1445
|
+
client: displayOnly ? void 0 : this.options.client,
|
|
1367
1446
|
initialData: this.options.initialData,
|
|
1368
|
-
onChange: this.options.onChange,
|
|
1369
|
-
onSubmitted: this.options.onSubmitted,
|
|
1447
|
+
onChange: displayOnly ? void 0 : this.options.onChange,
|
|
1448
|
+
onSubmitted: displayOnly ? void 0 : this.options.onSubmitted,
|
|
1370
1449
|
getHoneypot: () => this.element.querySelector('input[name="fillo_hp_field"]')?.value ?? this.hpValue,
|
|
1371
1450
|
surface: "default",
|
|
1372
|
-
resolveFormId: this.resolveFormIdFn,
|
|
1373
|
-
|
|
1451
|
+
resolveFormId: displayOnly ? void 0 : this.resolveFormIdFn,
|
|
1452
|
+
// Dev chrome gets the developer-grade resolution failure (message +
|
|
1453
|
+
// machine code) instead of the respondent-safe "unavailable" fallback.
|
|
1454
|
+
verboseResolutionErrors: this.devChrome(),
|
|
1455
|
+
respondent: displayOnly ? void 0 : this.options.respondent
|
|
1374
1456
|
});
|
|
1375
1457
|
let lastStatus = this.engine.getState().status;
|
|
1376
1458
|
let lastResumed = this.engine.getState().resumedDraft;
|
|
@@ -1385,7 +1467,7 @@ var DomFormController = class {
|
|
|
1385
1467
|
this.queueRender();
|
|
1386
1468
|
}
|
|
1387
1469
|
});
|
|
1388
|
-
this.configureDraftFlush(normalizedSchema);
|
|
1470
|
+
if (!displayOnly) this.configureDraftFlush(normalizedSchema);
|
|
1389
1471
|
this.phase = this.closed ? "closed" : "ready";
|
|
1390
1472
|
this.render();
|
|
1391
1473
|
}
|
|
@@ -1404,7 +1486,7 @@ var DomFormController = class {
|
|
|
1404
1486
|
this.theme = theme;
|
|
1405
1487
|
this.formId = formId;
|
|
1406
1488
|
this.engine.setContext({ form: normalizedSchema, formId });
|
|
1407
|
-
this.configureDraftFlush(normalizedSchema);
|
|
1489
|
+
if (this.notOpenVariant === null) this.configureDraftFlush(normalizedSchema);
|
|
1408
1490
|
this.phase = this.closed ? "closed" : "ready";
|
|
1409
1491
|
this.render();
|
|
1410
1492
|
}
|
|
@@ -1438,24 +1520,25 @@ var DomFormController = class {
|
|
|
1438
1520
|
this.setValueInternal(fieldId, value);
|
|
1439
1521
|
}
|
|
1440
1522
|
setValueInternal(fieldId, value, opts = {}) {
|
|
1523
|
+
if (this.notOpenVariant) return;
|
|
1441
1524
|
this.engine?.setValue(fieldId, value);
|
|
1442
1525
|
if (opts.render !== false) this.queueRender();
|
|
1443
1526
|
}
|
|
1444
1527
|
next() {
|
|
1445
|
-
if (!this.engine) return;
|
|
1528
|
+
if (!this.engine || this.notOpenVariant) return;
|
|
1446
1529
|
const before = this.engine.getState().pageIndex;
|
|
1447
1530
|
this.engine.next();
|
|
1448
1531
|
this.focusIntent = this.engine.getState().pageIndex !== before ? "page" : "error";
|
|
1449
1532
|
this.render();
|
|
1450
1533
|
}
|
|
1451
1534
|
back() {
|
|
1452
|
-
if (!this.engine) return;
|
|
1535
|
+
if (!this.engine || this.notOpenVariant) return;
|
|
1453
1536
|
this.engine.back();
|
|
1454
1537
|
this.focusIntent = "page";
|
|
1455
1538
|
this.render();
|
|
1456
1539
|
}
|
|
1457
1540
|
async submit() {
|
|
1458
|
-
if (!this.engine || this.phase !== "ready") return;
|
|
1541
|
+
if (!this.engine || this.phase !== "ready" || this.notOpenVariant) return;
|
|
1459
1542
|
const pending = this.engine.submit();
|
|
1460
1543
|
if (this.hasErrors()) this.focusIntent = "error";
|
|
1461
1544
|
this.render();
|
|
@@ -1492,7 +1575,7 @@ var DomFormController = class {
|
|
|
1492
1575
|
this.engine?.setContext({ respondent });
|
|
1493
1576
|
}
|
|
1494
1577
|
async uploadFiles(field, files) {
|
|
1495
|
-
if (!this.schema) return;
|
|
1578
|
+
if (!this.schema || this.notOpenVariant) return;
|
|
1496
1579
|
if (this.uploadProgress.has(field.id)) {
|
|
1497
1580
|
this.uploadNotices.set(field.id, "Wait for the current upload to finish before adding more files.");
|
|
1498
1581
|
this.queueRender();
|
|
@@ -1585,6 +1668,11 @@ var DomFormController = class {
|
|
|
1585
1668
|
this.engine = null;
|
|
1586
1669
|
this.target.replaceChildren();
|
|
1587
1670
|
}
|
|
1671
|
+
/** Keep an optional-upload form accepting while pre-empting only file
|
|
1672
|
+
* controls the current definition envelope says the server will refuse. */
|
|
1673
|
+
storageBlocked() {
|
|
1674
|
+
return this.uploadsAvailable === false;
|
|
1675
|
+
}
|
|
1588
1676
|
/** Add/return the phone popover teardown; called back on re-render or destroy. */
|
|
1589
1677
|
registerOverlay = (close) => {
|
|
1590
1678
|
this.overlays.add(close);
|
|
@@ -1687,7 +1775,7 @@ var DomFormController = class {
|
|
|
1687
1775
|
`[fillo] form sync failed${error.code ? ` (${error.code})` : ""}: ${error.message}`
|
|
1688
1776
|
);
|
|
1689
1777
|
this.syncErrorNotice = error;
|
|
1690
|
-
const development =
|
|
1778
|
+
const development = this.devChrome();
|
|
1691
1779
|
if (!development || this.options.renderError) {
|
|
1692
1780
|
this.genericUnavailable = !development;
|
|
1693
1781
|
this.handleError(error);
|
|
@@ -1704,7 +1792,7 @@ var DomFormController = class {
|
|
|
1704
1792
|
);
|
|
1705
1793
|
this.syncErrorNotice = error;
|
|
1706
1794
|
this.options.onError?.(error);
|
|
1707
|
-
if (
|
|
1795
|
+
if (this.devChrome()) this.render();
|
|
1708
1796
|
}
|
|
1709
1797
|
/** Submit-time verification failed: keep the form/answers for a safe retry. */
|
|
1710
1798
|
reportSubmitSyncFailure(error) {
|
|
@@ -1747,7 +1835,7 @@ var DomFormController = class {
|
|
|
1747
1835
|
if (this.phase === "error" && activeError) {
|
|
1748
1836
|
const errorView = this.options.renderError?.(activeError) ?? el("div", {
|
|
1749
1837
|
className: "fillo-form fillo-form--error",
|
|
1750
|
-
text: this.genericUnavailable ? "This form is unavailable." : activeError
|
|
1838
|
+
text: this.genericUnavailable ? "This form is unavailable." : loadFailureMessage(activeError)
|
|
1751
1839
|
});
|
|
1752
1840
|
if (!errorView.hasAttribute("data-fillo")) errorView.setAttribute("data-fillo", "root");
|
|
1753
1841
|
if (!errorView.hasAttribute("data-state")) errorView.setAttribute("data-state", "error");
|
|
@@ -1766,14 +1854,9 @@ var DomFormController = class {
|
|
|
1766
1854
|
this.element.appendChild(closed);
|
|
1767
1855
|
return;
|
|
1768
1856
|
}
|
|
1769
|
-
if (this.
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
attrs: { "data-fillo": "root", "data-state": "closed" }
|
|
1773
|
-
});
|
|
1774
|
-
themeStyle(panel, this.theme);
|
|
1775
|
-
panel.appendChild(el("p", { className: "fillo-closed", text: "This form isn't live yet." }));
|
|
1776
|
-
this.element.appendChild(panel);
|
|
1857
|
+
if (this.notOpenVariant && this.schema) {
|
|
1858
|
+
this.element.appendChild(this.renderNotOpen(this.notOpenVariant));
|
|
1859
|
+
if (this.brandingVisible()) this.element.appendChild(poweredByEl(this.fetchedBrandLabel, this.fetchedBrandHref));
|
|
1777
1860
|
return;
|
|
1778
1861
|
}
|
|
1779
1862
|
if (!this.schema) return;
|
|
@@ -1797,11 +1880,20 @@ var DomFormController = class {
|
|
|
1797
1880
|
}
|
|
1798
1881
|
return;
|
|
1799
1882
|
}
|
|
1800
|
-
if (
|
|
1801
|
-
|
|
1802
|
-
|
|
1883
|
+
if (this.devChrome()) {
|
|
1884
|
+
appendChildren(
|
|
1885
|
+
this.element,
|
|
1886
|
+
devChromeEls({
|
|
1887
|
+
preview: this.options.preview === true,
|
|
1888
|
+
devNotices: this.options.devNotices,
|
|
1889
|
+
syncError: this.syncErrorNotice,
|
|
1890
|
+
staged: this.stagedNotice,
|
|
1891
|
+
draft: this.draftNotice,
|
|
1892
|
+
warningUrl: this.syncWarningUrl,
|
|
1893
|
+
noClient: !this.options.client
|
|
1894
|
+
})
|
|
1895
|
+
);
|
|
1803
1896
|
}
|
|
1804
|
-
if (this.stagedNotice && isDevEnv()) this.element.appendChild(devStagedEl());
|
|
1805
1897
|
this.element.appendChild(this.renderForm());
|
|
1806
1898
|
if (this.brandingVisible()) this.element.appendChild(poweredByEl(this.fetchedBrandLabel, this.fetchedBrandHref));
|
|
1807
1899
|
this.applyFocus(intent, captured);
|
|
@@ -1810,27 +1902,71 @@ var DomFormController = class {
|
|
|
1810
1902
|
brandingVisible() {
|
|
1811
1903
|
return this.fetchedPoweredBy ?? true;
|
|
1812
1904
|
}
|
|
1905
|
+
/** Production not-accepting chrome: the REAL form rendered blurred and
|
|
1906
|
+
* display-only beneath a small card — honest about the state without a dead
|
|
1907
|
+
* grey panel, and impossible to fill. The wrapper is inert + aria-hidden +
|
|
1908
|
+
* pointer-events:none, the preview markup nests every control in a natively
|
|
1909
|
+
* disabled fieldset (see renderForm), and the engine behind it was created
|
|
1910
|
+
* without a client/formId/resolver — so nothing inside can focus, submit,
|
|
1911
|
+
* start an upload session, or touch saved-progress drafts. */
|
|
1912
|
+
renderNotOpen(variant) {
|
|
1913
|
+
const root = el("div", {
|
|
1914
|
+
className: `fillo-form fillo-form--not-open ${this.options.className ?? ""}`,
|
|
1915
|
+
attrs: { "data-fillo": "root", "data-state": "closed" }
|
|
1916
|
+
});
|
|
1917
|
+
themeStyle(root, this.theme);
|
|
1918
|
+
const preview = el("div", {
|
|
1919
|
+
className: "fillo-not-open-preview",
|
|
1920
|
+
attrs: { "aria-hidden": "true", inert: "" }
|
|
1921
|
+
});
|
|
1922
|
+
preview.appendChild(this.renderForm());
|
|
1923
|
+
const card = el("div", {
|
|
1924
|
+
className: "fillo-not-open-card",
|
|
1925
|
+
attrs: { role: "status", "data-fillo": "not-open-card" }
|
|
1926
|
+
});
|
|
1927
|
+
card.appendChild(
|
|
1928
|
+
el("h2", {
|
|
1929
|
+
className: "fillo-not-open-title",
|
|
1930
|
+
text: variant === "closed" ? DEFAULT_STRINGS.closedTitle : DEFAULT_STRINGS.notOpenTitle
|
|
1931
|
+
})
|
|
1932
|
+
);
|
|
1933
|
+
card.appendChild(
|
|
1934
|
+
el("p", {
|
|
1935
|
+
className: "fillo-not-open-body",
|
|
1936
|
+
text: variant === "closed" ? DEFAULT_STRINGS.closed : DEFAULT_STRINGS.notOpenBody
|
|
1937
|
+
})
|
|
1938
|
+
);
|
|
1939
|
+
appendChildren(root, [preview, card]);
|
|
1940
|
+
return root;
|
|
1941
|
+
}
|
|
1813
1942
|
renderForm() {
|
|
1814
1943
|
const api = this.api();
|
|
1815
|
-
const
|
|
1944
|
+
const displayOnly = this.notOpenVariant !== null;
|
|
1945
|
+
const form = el(displayOnly ? "div" : "form", {
|
|
1816
1946
|
className: `fillo-form ${this.options.className ?? ""}`,
|
|
1817
1947
|
attrs: {
|
|
1818
|
-
"data-fillo": "root",
|
|
1819
|
-
"data-state": api.status,
|
|
1948
|
+
"data-fillo": displayOnly ? "preview-form" : "root",
|
|
1949
|
+
"data-state": displayOnly ? "preview" : api.status,
|
|
1820
1950
|
"data-page": String(api.pageIndex + 1)
|
|
1821
1951
|
}
|
|
1822
1952
|
});
|
|
1823
1953
|
form.toggleAttribute("data-last-page", api.isLastPage);
|
|
1824
1954
|
themeStyle(form, this.theme);
|
|
1825
|
-
form.noValidate = true;
|
|
1826
1955
|
const advance = () => {
|
|
1827
1956
|
if (api.isLastPage) void this.submit();
|
|
1828
1957
|
else this.next();
|
|
1829
1958
|
};
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1959
|
+
if (!displayOnly) {
|
|
1960
|
+
const formEl = form;
|
|
1961
|
+
formEl.noValidate = true;
|
|
1962
|
+
formEl.addEventListener("submit", (event) => {
|
|
1963
|
+
event.preventDefault();
|
|
1964
|
+
advance();
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
const content = displayOnly ? form.appendChild(
|
|
1968
|
+
el("fieldset", { className: "fillo-not-open-fields", attrs: { disabled: "" } })
|
|
1969
|
+
) : form;
|
|
1834
1970
|
const restoreState = this.engine?.getState();
|
|
1835
1971
|
if (restoreState?.resumedDraft || restoreState?.editingPrevious) {
|
|
1836
1972
|
const resume = el("div", {
|
|
@@ -1850,7 +1986,7 @@ var DomFormController = class {
|
|
|
1850
1986
|
});
|
|
1851
1987
|
clear.addEventListener("click", () => this.resetDraft());
|
|
1852
1988
|
resume.appendChild(clear);
|
|
1853
|
-
|
|
1989
|
+
content.appendChild(resume);
|
|
1854
1990
|
}
|
|
1855
1991
|
if (api.pageCount > 1 && this.schema?.settings.showProgress !== false) {
|
|
1856
1992
|
const progress = el("div", {
|
|
@@ -1871,7 +2007,7 @@ var DomFormController = class {
|
|
|
1871
2007
|
"--fillo-progress-value",
|
|
1872
2008
|
`${(api.pageIndex + 1) / api.pageCount * 100}%`
|
|
1873
2009
|
);
|
|
1874
|
-
|
|
2010
|
+
content.appendChild(progress);
|
|
1875
2011
|
}
|
|
1876
2012
|
if (api.pageIndex === 0) {
|
|
1877
2013
|
const header = el("header", { className: "fillo-header", attrs: { "data-fillo": "header" } });
|
|
@@ -1889,9 +2025,9 @@ var DomFormController = class {
|
|
|
1889
2025
|
})
|
|
1890
2026
|
);
|
|
1891
2027
|
}
|
|
1892
|
-
|
|
2028
|
+
content.appendChild(header);
|
|
1893
2029
|
} else if (api.page.title) {
|
|
1894
|
-
|
|
2030
|
+
content.appendChild(
|
|
1895
2031
|
el("h2", {
|
|
1896
2032
|
className: "fillo-page-title",
|
|
1897
2033
|
text: api.page.title,
|
|
@@ -1904,25 +2040,33 @@ var DomFormController = class {
|
|
|
1904
2040
|
const rendered = this.renderBlock(pipeBlock(block, this.mutableData, api.form), api);
|
|
1905
2041
|
if (rendered) blocks.appendChild(rendered);
|
|
1906
2042
|
}
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
2043
|
+
content.appendChild(blocks);
|
|
2044
|
+
if (!displayOnly) {
|
|
2045
|
+
const hp = el("input", { className: "fillo-hp", attrs: { type: "text", name: "fillo_hp_field", tabindex: "-1", autocomplete: "off", "aria-hidden": "true" } });
|
|
2046
|
+
hp.style.position = "absolute";
|
|
2047
|
+
hp.style.left = "-9999px";
|
|
2048
|
+
hp.style.top = "auto";
|
|
2049
|
+
hp.style.width = "1px";
|
|
2050
|
+
hp.style.height = "1px";
|
|
2051
|
+
hp.style.opacity = "0";
|
|
2052
|
+
hp.style.overflow = "hidden";
|
|
2053
|
+
hp.style.pointerEvents = "none";
|
|
2054
|
+
hp.addEventListener("input", () => {
|
|
2055
|
+
this.hpValue = hp.value;
|
|
2056
|
+
});
|
|
2057
|
+
content.appendChild(hp);
|
|
2058
|
+
}
|
|
1921
2059
|
const submitError = api.status === "error" ? "This form can't submit right now. Please try again in a moment." : this.engine?.getState().submitError;
|
|
1922
2060
|
if (submitError) {
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
)
|
|
2061
|
+
const alert = el("p", { className: "fillo-submit-error", attrs: { role: "alert" }, text: submitError });
|
|
2062
|
+
const fixUrl = this.devChrome() && this.resolutionFailureCode === "form_not_published" && this.syncWarningCode === "storage_required" ? safeHttpUrl(this.syncWarningUrl) : null;
|
|
2063
|
+
if (fixUrl) {
|
|
2064
|
+
alert.append(" Connect storage: ");
|
|
2065
|
+
alert.appendChild(
|
|
2066
|
+
el("a", { text: fixUrl, attrs: { href: fixUrl, target: "_blank", rel: "noopener noreferrer" } })
|
|
2067
|
+
);
|
|
2068
|
+
}
|
|
2069
|
+
content.appendChild(alert);
|
|
1926
2070
|
}
|
|
1927
2071
|
const autoSubmit = api.form.settings.submitMode === "auto";
|
|
1928
2072
|
const showPrimaryButton = !autoSubmit || !api.isLastPage || needsExplicitSubmit(visibleFields(api.form, api.data));
|
|
@@ -1951,7 +2095,7 @@ var DomFormController = class {
|
|
|
1951
2095
|
});
|
|
1952
2096
|
footer.appendChild(submit);
|
|
1953
2097
|
}
|
|
1954
|
-
|
|
2098
|
+
content.appendChild(footer);
|
|
1955
2099
|
}
|
|
1956
2100
|
return form;
|
|
1957
2101
|
}
|
|
@@ -1982,7 +2126,9 @@ var DomFormController = class {
|
|
|
1982
2126
|
activeOtherFields: this.activeOtherFields,
|
|
1983
2127
|
getValue: () => this.engine?.getState().data[block.id],
|
|
1984
2128
|
registerOverlay: this.registerOverlay,
|
|
1985
|
-
uploadNotice: this.uploadNotices.get(block.id)
|
|
2129
|
+
uploadNotice: this.uploadNotices.get(block.id),
|
|
2130
|
+
storageBlocked: this.storageBlocked(),
|
|
2131
|
+
storageFixUrl: this.storageBlocked() && this.devChrome() ? safeHttpUrl(this.syncWarningUrl) : null
|
|
1986
2132
|
};
|
|
1987
2133
|
const renderer = block.kind === "custom" ? this.options.customComponents?.[block.component] ?? this.options.components?.custom : this.options.components?.[block.kind] ?? defaultFieldRenderer;
|
|
1988
2134
|
return renderer?.(context) ?? null;
|
|
@@ -2017,13 +2163,16 @@ function poweredByEl(label, href) {
|
|
|
2017
2163
|
attrs: { href: badgeUrl(href), target: "_blank", rel: "noopener noreferrer" }
|
|
2018
2164
|
});
|
|
2019
2165
|
}
|
|
2166
|
+
function overlayVariant(reason) {
|
|
2167
|
+
return reason === "expired" || reason === "capped" || reason === "storage_full" ? "closed" : "notOpen";
|
|
2168
|
+
}
|
|
2020
2169
|
function isDevEnv() {
|
|
2021
|
-
return
|
|
2170
|
+
return isLikelyDevEnv();
|
|
2022
2171
|
}
|
|
2023
2172
|
function devNoClientEl() {
|
|
2024
2173
|
return el("div", {
|
|
2025
2174
|
className: "fillo-devwarning",
|
|
2026
|
-
text: "No Fillo client connected \u2014 this form is render-only and won't save responses. Pass a client to collect them in Fillo, or forward with webhooks. (Shown only in development.)",
|
|
2175
|
+
text: "No Fillo client connected \u2014 this form is render-only and won't save responses. Pass a client to collect them in Fillo, or forward with webhooks. (Shown only in development and preview.)",
|
|
2027
2176
|
attrs: { role: "alert" }
|
|
2028
2177
|
});
|
|
2029
2178
|
}
|
|
@@ -2041,6 +2190,53 @@ function devStagedEl() {
|
|
|
2041
2190
|
attrs: { role: "alert" }
|
|
2042
2191
|
});
|
|
2043
2192
|
}
|
|
2193
|
+
function devDraftEl(warningUrl) {
|
|
2194
|
+
const node = el("div", { className: "fillo-devwarning", attrs: { role: "alert" } });
|
|
2195
|
+
node.append(
|
|
2196
|
+
"Draft form \u2014 it renders here, but it won't accept responses until you publish it in your Fillo dashboard. "
|
|
2197
|
+
);
|
|
2198
|
+
const safeUrl = safeHttpUrl(warningUrl);
|
|
2199
|
+
if (safeUrl) {
|
|
2200
|
+
node.append("Connect storage to publish: ");
|
|
2201
|
+
node.appendChild(
|
|
2202
|
+
el("a", { text: safeUrl, attrs: { href: safeUrl, target: "_blank", rel: "noopener noreferrer" } })
|
|
2203
|
+
);
|
|
2204
|
+
node.append(". ");
|
|
2205
|
+
}
|
|
2206
|
+
node.append("(Shown only in development and preview.)");
|
|
2207
|
+
return node;
|
|
2208
|
+
}
|
|
2209
|
+
function previewBadgeEl() {
|
|
2210
|
+
return el("span", {
|
|
2211
|
+
className: "fillo-preview-badge",
|
|
2212
|
+
text: "Preview",
|
|
2213
|
+
attrs: { "data-fillo": "preview-badge" }
|
|
2214
|
+
});
|
|
2215
|
+
}
|
|
2216
|
+
var warnedPreviewInProduction = false;
|
|
2217
|
+
function warnPreviewInProduction() {
|
|
2218
|
+
if (warnedPreviewInProduction || isBuildTimeDevEnv()) return;
|
|
2219
|
+
warnedPreviewInProduction = true;
|
|
2220
|
+
console.warn(
|
|
2221
|
+
"[fillo] `preview` is enabled in a production build. Preview is cosmetic only \u2014 it shows developer chrome and never changes where submissions go or whether they are accepted \u2014 but remove it before respondents see this page."
|
|
2222
|
+
);
|
|
2223
|
+
}
|
|
2224
|
+
function devChromeEls(state) {
|
|
2225
|
+
const nodes = [];
|
|
2226
|
+
if (state.preview) nodes.push(previewBadgeEl());
|
|
2227
|
+
if (state.devNotices === false) return nodes;
|
|
2228
|
+
if (state.syncError) nodes.push(devSyncErrorEl(state.syncError));
|
|
2229
|
+
else if (state.staged) nodes.push(devStagedEl());
|
|
2230
|
+
else if (state.draft) nodes.push(devDraftEl(state.warningUrl));
|
|
2231
|
+
else if (state.noClient) nodes.push(devNoClientEl());
|
|
2232
|
+
return nodes;
|
|
2233
|
+
}
|
|
2234
|
+
function loadFailureMessage(error) {
|
|
2235
|
+
if (error.status === 404) return DEFAULT_STRINGS.loadFailedNotFound;
|
|
2236
|
+
if (error.status === 0) return DEFAULT_STRINGS.loadFailedNetwork;
|
|
2237
|
+
if (error.status === 422) return DEFAULT_STRINGS.renderFailed;
|
|
2238
|
+
return DEFAULT_STRINGS.loadFailed;
|
|
2239
|
+
}
|
|
2044
2240
|
function toFilloError(error) {
|
|
2045
2241
|
if (isFilloError(error)) return error;
|
|
2046
2242
|
return new FilloError(error instanceof Error ? error.message : String(error), 0);
|
|
@@ -2066,7 +2262,7 @@ var FilloFormElement = class extends HTMLElementBase {
|
|
|
2066
2262
|
mounted = false;
|
|
2067
2263
|
mountScheduled = false;
|
|
2068
2264
|
static get observedAttributes() {
|
|
2069
|
-
return ["publishable-key", "form-id"];
|
|
2265
|
+
return ["publishable-key", "form-id", "data-preview"];
|
|
2070
2266
|
}
|
|
2071
2267
|
connectedCallback() {
|
|
2072
2268
|
this.scheduleMount();
|
|
@@ -2115,6 +2311,14 @@ var FilloFormElement = class extends HTMLElementBase {
|
|
|
2115
2311
|
if (this.isConnected) this.mount();
|
|
2116
2312
|
});
|
|
2117
2313
|
}
|
|
2314
|
+
/** data-preview is on when present UNLESS its value says off ("false"/"0"):
|
|
2315
|
+
* frameworks stringify booleans onto data-* attributes rather than omitting
|
|
2316
|
+
* them, and a stringified `false` must never enable dev chrome for real
|
|
2317
|
+
* respondents. Disable by removing the attribute or setting it to "false". */
|
|
2318
|
+
previewAttrEnabled() {
|
|
2319
|
+
const value = this.getAttribute("data-preview");
|
|
2320
|
+
return value !== null && value !== "false" && value !== "0";
|
|
2321
|
+
}
|
|
2118
2322
|
mount() {
|
|
2119
2323
|
this.instance?.destroy();
|
|
2120
2324
|
const key = this.getAttribute("publishable-key") ?? void 0;
|
|
@@ -2126,6 +2330,12 @@ var FilloFormElement = class extends HTMLElementBase {
|
|
|
2126
2330
|
formId,
|
|
2127
2331
|
theme: this.assignedTheme,
|
|
2128
2332
|
initialData: this.assignedInitialData,
|
|
2333
|
+
// Same cosmetic-only contract as renderForm's `preview` option: forces
|
|
2334
|
+
// the dev chrome + a visible Preview badge on off-localhost surfaces,
|
|
2335
|
+
// and never changes where submissions go or whether they are accepted.
|
|
2336
|
+
// Value-aware: frameworks stringify booleans onto data-* attributes, so
|
|
2337
|
+
// data-preview="false"/"0" must mean OFF, not presence-implies-on.
|
|
2338
|
+
preview: this.previewAttrEnabled() ? true : void 0,
|
|
2129
2339
|
onChange: (data) => this.dispatchEvent(new CustomEvent("fillo-change", { detail: { data } })),
|
|
2130
2340
|
onSubmitted: (responseId, data) => this.dispatchEvent(new CustomEvent("fillo-submit", { detail: { responseId, data } })),
|
|
2131
2341
|
onError: (error) => this.dispatchEvent(new CustomEvent("fillo-error", { detail: { error } }))
|