@usefillo/dom 0.5.4 → 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.js CHANGED
@@ -18,9 +18,10 @@ import {
18
18
  parsePhone,
19
19
  pipeBlock,
20
20
  provisionWorkspace,
21
+ needsExplicitSubmit,
22
+ shouldAutoSubmit,
21
23
  syncCodeForm,
22
24
  toE164,
23
- validateResponse,
24
25
  visibleFields
25
26
  } from "@usefillo/core";
26
27
  var FIELD_INPUT_PREFIX = "fillo-";
@@ -992,44 +993,12 @@ var DEFAULT_COMPONENTS = {
992
993
  signature,
993
994
  file_upload: fileUpload
994
995
  };
995
- function isAutoSubmitBlock(block) {
996
- return isField(block) && (block.kind === "select" || block.kind === "dropdown" || block.kind === "checkbox" || block.kind === "rating" || block.kind === "linear_scale");
997
- }
998
- function needsExplicitSubmit(visible) {
999
- const interactive = visible.filter((b) => !(isField(b) && b.kind === "hidden"));
1000
- if (interactive.length === 0) return false;
1001
- if (interactive.length > 1) return true;
1002
- return !isAutoSubmitBlock(interactive[0]);
1003
- }
1004
996
  function defaultFieldRenderer(context) {
1005
997
  if (context.field.kind === "hidden") return null;
1006
998
  if (context.field.kind === "custom") return null;
1007
999
  const renderer = DEFAULT_COMPONENTS[context.field.kind];
1008
1000
  return renderer ? renderer(context) : null;
1009
1001
  }
1010
- function shouldAutoSubmit(field, value, api, uploading) {
1011
- if (api.form.settings.submitMode !== "auto") return false;
1012
- if (!api.isLastPage || api.status !== "idle" || uploading) return false;
1013
- const hasDiscreteAnswer = (() => {
1014
- switch (field.kind) {
1015
- case "select":
1016
- case "dropdown":
1017
- return typeof value === "string" && field.options.some((option) => option.id === value);
1018
- case "checkbox":
1019
- return value === true;
1020
- case "rating":
1021
- case "linear_scale":
1022
- return typeof value === "number";
1023
- default:
1024
- return false;
1025
- }
1026
- })();
1027
- if (!hasDiscreteAnswer) return false;
1028
- const nextData = { ...api.data, [field.id]: value };
1029
- const interactive = visibleFields(api.form, nextData).filter((f) => f.kind !== "hidden");
1030
- if (interactive.length !== 1 || interactive[0]?.id !== field.id) return false;
1031
- return validateResponse(api.form, nextData).ok;
1032
- }
1033
1002
  var DomFormController = class {
1034
1003
  constructor(target, options) {
1035
1004
  this.target = target;
@@ -1104,22 +1073,43 @@ var DomFormController = class {
1104
1073
  submit: () => this.submit()
1105
1074
  };
1106
1075
  }
1076
+ resolveFormIdFn;
1077
+ /** Prod-only: the code form is an unpublished draft — submissions would 404. */
1078
+ notLive = false;
1107
1079
  async resolve() {
1108
1080
  try {
1109
1081
  const codeForm = isCodeForm(this.options.form) ? this.options.form : null;
1110
1082
  if (codeForm) {
1083
+ const client = this.options.client;
1084
+ if (client?.key) {
1085
+ this.resolveFormIdFn = () => syncCodeForm(client, codeForm, { bypassCache: true }).then((r) => r.formId);
1086
+ }
1111
1087
  this.setSchema(codeForm.schema, this.options.theme ?? codeForm.theme, this.options.formId);
1112
- if (this.options.client?.key) {
1113
- syncCodeForm(this.options.client, codeForm).then(({ formId, branding }) => {
1088
+ if (client?.key) {
1089
+ syncCodeForm(client, codeForm).then(({ formId, branding, staged, status, warning }) => {
1114
1090
  this.formId = formId;
1115
1091
  this.engine?.setContext({ formId });
1092
+ if (warning) console.warn(`[fillo] ${warning}`);
1093
+ if (staged) {
1094
+ console.info(`[fillo] "${codeForm.id}" changes staged as a draft \u2014 publish them in your Fillo dashboard`);
1095
+ } else if (status === "draft") {
1096
+ console.info(`[fillo] "${codeForm.id}" is a draft \u2014 it renders here but won't accept responses until published`);
1097
+ if (!isDevEnv()) {
1098
+ this.notLive = true;
1099
+ this.render();
1100
+ }
1101
+ }
1116
1102
  if (branding) {
1117
1103
  this.fetchedPoweredBy = branding.poweredBy;
1118
1104
  this.fetchedBrandLabel = branding.label;
1119
1105
  this.fetchedBrandHref = branding.href;
1120
1106
  this.render();
1121
1107
  }
1122
- }).catch((error) => this.handleError(toFilloError(error)));
1108
+ }).catch((error) => {
1109
+ const err = toFilloError(error);
1110
+ console.warn("[fillo] form sync failed:", err.message);
1111
+ this.options.onError?.(err);
1112
+ });
1123
1113
  }
1124
1114
  return;
1125
1115
  }
@@ -1159,7 +1149,16 @@ var DomFormController = class {
1159
1149
  onChange: this.options.onChange,
1160
1150
  onSubmitted: this.options.onSubmitted,
1161
1151
  getHoneypot: () => this.hpValue,
1162
- surface: "default"
1152
+ surface: "default",
1153
+ resolveFormId: this.resolveFormIdFn
1154
+ });
1155
+ let lastStatus = this.engine.getState().status;
1156
+ this.engine.subscribe(() => {
1157
+ const status = this.engine?.getState().status;
1158
+ if (status !== void 0 && status !== lastStatus) {
1159
+ lastStatus = status;
1160
+ this.queueRender();
1161
+ }
1163
1162
  });
1164
1163
  this.phase = this.closed ? "closed" : "ready";
1165
1164
  this.render();
@@ -1192,7 +1191,9 @@ var DomFormController = class {
1192
1191
  try {
1193
1192
  await pending;
1194
1193
  } catch (error) {
1195
- this.handleError(toFilloError(error));
1194
+ this.options.onError?.(toFilloError(error));
1195
+ this.focusIntent = "error";
1196
+ this.render();
1196
1197
  return;
1197
1198
  }
1198
1199
  if (this.hasErrors()) this.focusIntent = "error";
@@ -1396,6 +1397,13 @@ var DomFormController = class {
1396
1397
  this.element.appendChild(closed);
1397
1398
  return;
1398
1399
  }
1400
+ if (this.notLive) {
1401
+ const panel = el("div", { className: `fillo-form fillo-form--closed ${this.options.className ?? ""}` });
1402
+ themeStyle(panel, this.theme);
1403
+ panel.appendChild(el("p", { className: "fillo-closed", text: "This form isn't live yet." }));
1404
+ this.element.appendChild(panel);
1405
+ return;
1406
+ }
1399
1407
  if (!this.schema) return;
1400
1408
  if (this.engine?.getState().status === "submitted") {
1401
1409
  const api = this.api();
@@ -1439,7 +1447,7 @@ var DomFormController = class {
1439
1447
  });
1440
1448
  progress.appendChild(el("div", { className: "fillo-progress-fill" }));
1441
1449
  progress.firstElementChild?.style.setProperty(
1442
- "width",
1450
+ "--fillo-progress-value",
1443
1451
  `${(api.pageIndex + 1) / api.pageCount * 100}%`
1444
1452
  );
1445
1453
  form.appendChild(progress);
@@ -1459,10 +1467,24 @@ var DomFormController = class {
1459
1467
  }
1460
1468
  form.appendChild(blocks);
1461
1469
  const hp = el("input", { className: "fillo-hp", attrs: { type: "text", name: "fillo_hp_field", tabindex: "-1", autocomplete: "off", "aria-hidden": "true" } });
1470
+ hp.style.position = "absolute";
1471
+ hp.style.left = "-9999px";
1472
+ hp.style.top = "auto";
1473
+ hp.style.width = "1px";
1474
+ hp.style.height = "1px";
1475
+ hp.style.opacity = "0";
1476
+ hp.style.overflow = "hidden";
1477
+ hp.style.pointerEvents = "none";
1462
1478
  hp.addEventListener("input", () => {
1463
1479
  this.hpValue = hp.value;
1464
1480
  });
1465
1481
  form.appendChild(hp);
1482
+ const submitError = api.status === "error" ? "This form can't submit right now. Please try again in a moment." : this.engine?.getState().submitError;
1483
+ if (submitError) {
1484
+ form.appendChild(
1485
+ el("p", { className: "fillo-submit-error", attrs: { role: "alert" }, text: submitError })
1486
+ );
1487
+ }
1466
1488
  const autoSubmit = api.form.settings.submitMode === "auto";
1467
1489
  const showPrimaryButton = !autoSubmit || !api.isLastPage || needsExplicitSubmit(visibleFields(api.form, api.data));
1468
1490
  const showFooter = showPrimaryButton || api.pageCount > 1 && !api.isFirstPage;
@@ -1495,7 +1517,15 @@ var DomFormController = class {
1495
1517
  const setValue = (value, options) => {
1496
1518
  this.setValueInternal(block.id, value, options);
1497
1519
  if (options?.render === false) return;
1498
- if (shouldAutoSubmit(block, value, api, this.uploadProgress.size > 0)) void this.submit();
1520
+ const autoCtx = {
1521
+ form: api.form,
1522
+ data: api.data,
1523
+ // dom's status union adds render phases; non-"idle" is a no anyway.
1524
+ status: api.status,
1525
+ isLastPage: api.isLastPage,
1526
+ uploading: this.uploadProgress.size > 0
1527
+ };
1528
+ if (shouldAutoSubmit(block, value, autoCtx)) void this.submit();
1499
1529
  };
1500
1530
  const context = {
1501
1531
  field: block,