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 +15 -1
- package/dist/index.cjs +82 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.js +81 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
|
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) =>
|
|
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,
|