form-page-builder 2.3.0 → 2.4.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 CHANGED
@@ -123,7 +123,7 @@ Every optional UI surface can be switched on or off independently through one `f
123
123
  | `sectionBackground` | `boolean` | inherits `sections` | Per-section background-color swatches + custom-color picker in the section header. Independent of `sections` — set it `false` to lock section backgrounds while keeping the structural controls, or leave it unset and it follows whatever `sections` is. |
124
124
  | `dragReorder` | `boolean` | `true` | Drag-to-reorder fields within a section. |
125
125
  | `deviceToggle` | `boolean` | `true` | The Laptop/Tablet/Mobile width switcher above the Preview canvas. When `false`, Preview always renders at the Laptop (full) width. |
126
- | `maxFields` | `number` | `undefined` (unlimited) | Caps the total number of input-type fields (not content blocks) addable across the whole document. Once reached, the Form Fields palette buttons disable until a field is removed. |
126
+ | `maxFields` | `number` | `undefined` (unlimited) | Caps the total number of input-type fields (not content blocks) addable across the whole document. The palette shows a live "Fields: n / max" count; once reached, the Form Fields buttons disable and a limit message replaces the count until a field is removed. |
127
127
 
128
128
  Disabling `contentBlocks`/`fieldTypes` for a given type only hides it from the palette going forward — if a document loaded via `initialDocument` (or a saved template) already contains fields of a now-disabled type, they still render and remain editable in Build mode; nothing is stripped or hidden.
129
129
 
@@ -273,6 +273,20 @@ const json = serializeTemplate(ref.current.getDocument());
273
273
  const doc = parseTemplate(json); // also accepts bare "View JSON" output; null if not a document
274
274
  ```
275
275
 
276
+ ### Smaller stored documents
277
+
278
+ Drafts and saved templates written through `storage` are compacted automatically: props equal to their default (`hideLabel`, `width`, `verticalAlign`, `labelPosition`, `showIcon`, `required`, the type's default icon, empty placeholders/default values, empty non-`en` translations) are omitted and the document is stamped `compact: 1`. Loading restores them, so older full documents still load unchanged.
279
+
280
+ `getDocument()` still returns the full document. If you store it yourself, compact it first:
281
+
282
+ ```tsx
283
+ import { compactDocument } from "form-page-builder";
284
+
285
+ await save(JSON.stringify(compactDocument(ref.current.getDocument())));
286
+ ```
287
+
288
+ Documents saved in the compact form need this package version (or newer) to be read back correctly, so upgrade every app that reads them together.
289
+
276
290
  ## Using in a plain HTML page (no bundler)
277
291
 
278
292
  There's intentionally no UMD/IIFE global-script build (see below), but you don't need a bundler to use this package — a browser-native ES module setup works too, via an [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) and a CDN like [esm.sh](https://esm.sh/):
package/dist/index.cjs CHANGED
@@ -28,6 +28,7 @@ __export(index_exports, {
28
28
  DRAFT_KEY: () => DRAFT_KEY,
29
29
  FormBuilder: () => FormBuilder_default,
30
30
  INDEX_KEY: () => INDEX_KEY,
31
+ compactDocument: () => compactDocument,
31
32
  default: () => FormBuilder_default,
32
33
  formKey: () => formKey,
33
34
  localStorageAdapter: () => localStorageAdapter,
@@ -222,6 +223,8 @@ var CHROME = {
222
223
  loadingDraft: "Loading your draft...",
223
224
  templatesLimitReached: (max) => `You can save up to ${max} template${max === 1 ? "" : "s"}. Delete one to save another.`,
224
225
  templatesLimitTitle: "Template limit reached",
226
+ fieldCount: (n, max) => `Fields: ${n} / ${max}`,
227
+ fieldLimitReached: (max) => `Field limit reached (${max}). Delete a field to add another.`,
225
228
  confirm: "Confirm",
226
229
  cancel: "Cancel",
227
230
  dismiss: "OK",
@@ -375,6 +378,8 @@ var CHROME = {
375
378
  loadingDraft: "\u4E0B\u66F8\u304D\u3092\u8AAD\u307F\u8FBC\u3093\u3067\u3044\u307E\u3059...",
376
379
  templatesLimitReached: (max) => `\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u306F${max}\u4EF6\u307E\u3067\u4FDD\u5B58\u3067\u304D\u307E\u3059\u30021\u4EF6\u524A\u9664\u3057\u3066\u304B\u3089\u4FDD\u5B58\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
377
380
  templatesLimitTitle: "\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u306E\u4E0A\u9650\u306B\u9054\u3057\u307E\u3057\u305F",
381
+ fieldCount: (n, max) => `\u9805\u76EE\u6570: ${n} / ${max}`,
382
+ fieldLimitReached: (max) => `\u9805\u76EE\u6570\u306E\u4E0A\u9650\uFF08${max}\u4EF6\uFF09\u306B\u9054\u3057\u307E\u3057\u305F\u3002\u8FFD\u52A0\u3059\u308B\u306B\u306F\u9805\u76EE\u3092\u524A\u9664\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
378
383
  confirm: "\u78BA\u5B9A",
379
384
  cancel: "\u30AD\u30E3\u30F3\u30BB\u30EB",
380
385
  dismiss: "OK",
@@ -611,6 +616,16 @@ function migrateField(field) {
611
616
  f.submitScope = f.submitScope === "section" ? "section" : "form";
612
617
  }
613
618
  f.verticalAlign = f.verticalAlign || "top";
619
+ const meta = getMeta(f.type);
620
+ f.width = f.width || "1/1";
621
+ f.labelPosition = f.labelPosition || "top";
622
+ f.showIcon = !!f.showIcon;
623
+ f.displayIcon = f.displayIcon || meta.defaultIcon;
624
+ if (!meta.isContent) {
625
+ f.required = !!f.required;
626
+ if (meta.placeholder && !f.placeholder) f.placeholder = bi();
627
+ if (f.defaultValue === void 0) f.defaultValue = meta.boolean ? false : meta.multiValue ? [] : "";
628
+ }
614
629
  return f;
615
630
  }
616
631
  function migrateFields(fields) {
@@ -863,7 +878,9 @@ function useFormDocument({ language, chrome }) {
863
878
  ...s,
864
879
  fields: s.fields.map((f) => {
865
880
  if (f.id !== fieldId || !("options" in f) || !f.options) return f;
866
- const n = f.options.length + 1;
881
+ const used = new Set(f.options.map((o) => o.value));
882
+ let n = f.options.length + 1;
883
+ while (used.has(`option_${n}`)) n++;
867
884
  return { ...f, options: [...f.options, { label: withLang(bi(), language, chrome.optionSeed(n)), value: `option_${n}` }] };
868
885
  })
869
886
  })));
@@ -871,7 +888,14 @@ function useFormDocument({ language, chrome }) {
871
888
  function removeOption(fieldId, optIdx) {
872
889
  setSections((prev) => prev.map((s) => ({
873
890
  ...s,
874
- fields: s.fields.map((f) => f.id !== fieldId || !("options" in f) || !f.options ? f : { ...f, options: f.options.filter((_, i) => i !== optIdx) })
891
+ fields: s.fields.map((f) => {
892
+ if (f.id !== fieldId || !("options" in f) || !f.options) return f;
893
+ const removed = f.options[optIdx]?.value;
894
+ const options = f.options.filter((_, i) => i !== optIdx);
895
+ if (f.type === "checkboxGroup") return { ...f, options, defaultValue: f.defaultValue.filter((v) => v !== removed) };
896
+ if ((f.type === "select" || f.type === "radio") && f.defaultValue === removed) return { ...f, options, defaultValue: "" };
897
+ return { ...f, options };
898
+ })
875
899
  })));
876
900
  }
877
901
  function loadDocument(doc) {
@@ -931,6 +955,51 @@ var formKey = (id) => `form-page-builder:saved:${id}`;
931
955
  var savedFormId = (key) => key.startsWith("form-page-builder:saved:") ? key.slice("form-page-builder:saved:".length) : null;
932
956
  var CLIPBOARD_KEY = "form-page-builder:clipboard";
933
957
 
958
+ // src/lib/compact.ts
959
+ var BASE_DEFAULTS = {
960
+ hideLabel: false,
961
+ width: "1/1",
962
+ verticalAlign: "top",
963
+ labelPosition: "top",
964
+ showIcon: false,
965
+ required: false
966
+ };
967
+ var COMPACT_VERSION = 1;
968
+ var slimLocalized = (v) => v && typeof v === "object" ? Object.fromEntries(Object.entries(v).filter(([lang, s]) => lang === "en" || s !== "")) : v;
969
+ var isEmptyLocalized = (v) => !!v && typeof v === "object" && Object.values(v).every((s) => !s);
970
+ var isEmptyDefault = (v) => v === "" || v === false || Array.isArray(v) && v.length === 0;
971
+ function compactField(field) {
972
+ const out = {};
973
+ const meta = getMeta(field.type);
974
+ for (const [key, value] of Object.entries(field)) {
975
+ if (key in BASE_DEFAULTS && value === BASE_DEFAULTS[key]) continue;
976
+ if (key === "displayIcon" && value === meta.defaultIcon) continue;
977
+ if (key === "placeholder" && isEmptyLocalized(value)) continue;
978
+ if (key === "defaultValue" && !meta.isContent && isEmptyDefault(value)) continue;
979
+ if (key === "label" || key === "placeholder" || key === "content" || key === "alt") out[key] = slimLocalized(value);
980
+ else if (key === "options" && Array.isArray(value)) out[key] = value.map((o) => ({ ...o, label: slimLocalized(o.label) }));
981
+ else out[key] = value;
982
+ }
983
+ return out;
984
+ }
985
+ function compactDocument(doc) {
986
+ return {
987
+ ...doc,
988
+ compact: COMPACT_VERSION,
989
+ ...doc.title !== void 0 ? { title: slimLocalized(doc.title) } : {},
990
+ sections: doc.sections.map((s) => {
991
+ const { background, collapsed, fields, ...rest } = s;
992
+ return {
993
+ ...rest,
994
+ ...s.title !== void 0 ? { title: slimLocalized(s.title) } : {},
995
+ ...background ? { background } : {},
996
+ ...collapsed ? { collapsed } : {},
997
+ fields: fields.map(compactField)
998
+ };
999
+ })
1000
+ };
1001
+ }
1002
+
934
1003
  // src/hooks/usePersistence.ts
935
1004
  function snapshotOf(d) {
936
1005
  return JSON.stringify({ title: d.title, themeOverrides: d.themeOverrides, sections: d.sections });
@@ -1023,7 +1092,7 @@ function usePersistence({
1023
1092
  if (autosaveTimer.current) clearTimeout(autosaveTimer.current);
1024
1093
  autosaveTimer.current = setTimeout(async () => {
1025
1094
  try {
1026
- await storage.set(DRAFT_KEY, JSON.stringify({ version: DOCUMENT_VERSION, ...document2, currentFormId }));
1095
+ await storage.set(DRAFT_KEY, JSON.stringify({ version: DOCUMENT_VERSION, ...compactDocument(document2), currentFormId }));
1027
1096
  setSaveState("saved");
1028
1097
  } catch {
1029
1098
  setSaveState("error");
@@ -1046,7 +1115,7 @@ function usePersistence({
1046
1115
  try {
1047
1116
  const newTitle = bi(name, "");
1048
1117
  const docToSave = { ...document2, title: newTitle };
1049
- await storage.set(formKey(id), JSON.stringify({ version: DOCUMENT_VERSION, ...docToSave, id, updatedAt: now }));
1118
+ await storage.set(formKey(id), JSON.stringify({ version: DOCUMENT_VERSION, ...compactDocument(docToSave), id, updatedAt: now }));
1050
1119
  const next = [...savedForms, { id, title: name, updatedAt: now }];
1051
1120
  await storage.set(INDEX_KEY, JSON.stringify(next));
1052
1121
  setSavedForms(next);
@@ -1069,7 +1138,7 @@ function usePersistence({
1069
1138
  const now = Date.now();
1070
1139
  setTemplateState("saving");
1071
1140
  try {
1072
- await storage.set(formKey(currentFormId), JSON.stringify({ version: DOCUMENT_VERSION, ...document2, id: currentFormId, updatedAt: now }));
1141
+ await storage.set(formKey(currentFormId), JSON.stringify({ version: DOCUMENT_VERSION, ...compactDocument(document2), id: currentFormId, updatedAt: now }));
1073
1142
  const next = savedForms.map((f) => f.id === currentFormId ? { ...f, title: t(document2.title, "en"), updatedAt: now } : f);
1074
1143
  setSavedForms(next);
1075
1144
  await storage.set(INDEX_KEY, JSON.stringify(next));
@@ -1085,11 +1154,13 @@ function usePersistence({
1085
1154
  try {
1086
1155
  const raw = await storage.get(formKey(id));
1087
1156
  if (!raw) {
1157
+ console.error(`[form-page-builder] loadForm(${id}): storage returned no data for this id`);
1088
1158
  setTemplateState("error");
1089
1159
  return false;
1090
1160
  }
1091
1161
  const doc = migrateDocument(JSON.parse(raw));
1092
1162
  if (!doc) {
1163
+ console.error(`[form-page-builder] loadForm(${id}): stored data failed migrateDocument()`, raw);
1093
1164
  setTemplateState("error");
1094
1165
  return false;
1095
1166
  }
@@ -1101,7 +1172,8 @@ function usePersistence({
1101
1172
  setTemplateState("idle");
1102
1173
  onTemplateChange?.({ id, title: t(doc.title, language), source: "applied" });
1103
1174
  return true;
1104
- } catch {
1175
+ } catch (err) {
1176
+ console.error(`[form-page-builder] loadForm(${id}) threw`, err);
1105
1177
  setTemplateState("error");
1106
1178
  return false;
1107
1179
  }
@@ -1109,6 +1181,7 @@ function usePersistence({
1109
1181
  async function deleteForm(id) {
1110
1182
  if (!templateManage) return;
1111
1183
  const removed = savedForms.find((f) => f.id === id);
1184
+ setTemplateState("loading");
1112
1185
  try {
1113
1186
  await storage.delete(formKey(id));
1114
1187
  const next = savedForms.filter((f) => f.id !== id);
@@ -1118,6 +1191,7 @@ function usePersistence({
1118
1191
  setCurrentFormId(null);
1119
1192
  savedSnapshot.current = null;
1120
1193
  }
1194
+ setTemplateState("idle");
1121
1195
  onTemplateChange?.({ id: currentFormId === id ? null : id, title: removed?.title ?? "", source: "deleted" });
1122
1196
  } catch {
1123
1197
  setTemplateState("error");
@@ -1625,6 +1699,7 @@ function Palette({
1625
1699
  ] }),
1626
1700
  formTypes.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
1627
1701
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { ...styles.panelHeading, marginTop: 16 }, children: chrome.formFields }),
1702
+ typeof features.maxFields === "number" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { role: "status", style: { ...styles.activeSectionHint, marginBottom: 8, ...atFieldCap ? { color: "var(--fb-danger)" } : {} }, children: atFieldCap ? chrome.fieldLimitReached(features.maxFields) : chrome.fieldCount(fieldCount, features.maxFields) }),
1628
1703
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: styles.paletteList, children: formTypes.map((f) => {
1629
1704
  const Icon = f.icon;
1630
1705
  const itemStyle = atFieldCap ? { ...styles.paletteItem, opacity: 0.45, cursor: "not-allowed" } : styles.paletteItem;
@@ -2987,6 +3062,7 @@ var DARK_THEME = {
2987
3062
  DRAFT_KEY,
2988
3063
  FormBuilder,
2989
3064
  INDEX_KEY,
3065
+ compactDocument,
2990
3066
  formKey,
2991
3067
  localStorageAdapter,
2992
3068
  parseTemplate,