@dmitryvim/form-builder 0.5.1 → 0.5.3
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/browser/formbuilder.min.js +125 -112
- package/dist/browser/formbuilder.v0.5.3.min.js +1490 -0
- package/dist/cjs/index.cjs +276 -222
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +270 -216
- package/dist/esm/index.js.map +1 -1
- package/dist/form-builder.js +125 -112
- package/dist/types/components/container.d.ts +1 -0
- package/dist/types/types/config.d.ts +2 -0
- package/dist/types/utils/helpers.d.ts +8 -0
- package/package.json +1 -1
- package/dist/browser/formbuilder.v0.5.1.min.js +0 -1477
package/dist/esm/index.js
CHANGED
|
@@ -15,6 +15,71 @@ function t(key, state, params) {
|
|
|
15
15
|
return text;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// src/utils/helpers.ts
|
|
19
|
+
function isElementReadonly(element, state, ctx) {
|
|
20
|
+
return element.readonly === true || state.config.readonly === true || ctx?.inheritedReadonly === true;
|
|
21
|
+
}
|
|
22
|
+
function isPlainObject(obj) {
|
|
23
|
+
return obj && typeof obj === "object" && obj.constructor === Object;
|
|
24
|
+
}
|
|
25
|
+
function escapeHtml(text) {
|
|
26
|
+
const div = document.createElement("div");
|
|
27
|
+
div.textContent = text;
|
|
28
|
+
return div.innerHTML;
|
|
29
|
+
}
|
|
30
|
+
function getElementLookupKey(element, state) {
|
|
31
|
+
if (element.key) {
|
|
32
|
+
return element.key;
|
|
33
|
+
}
|
|
34
|
+
const cached = state.syntheticElementIds.get(element);
|
|
35
|
+
if (cached !== void 0) {
|
|
36
|
+
return cached;
|
|
37
|
+
}
|
|
38
|
+
const id = `fb-synthetic-${state.syntheticElementIdCounter++}`;
|
|
39
|
+
state.syntheticElementIds.set(element, id);
|
|
40
|
+
return id;
|
|
41
|
+
}
|
|
42
|
+
function pathJoin(base, key) {
|
|
43
|
+
return base ? `${base}.${key}` : key;
|
|
44
|
+
}
|
|
45
|
+
function findDirectContainerRows(scopeRoot, containerPath) {
|
|
46
|
+
if (!containerPath) return [];
|
|
47
|
+
const all = scopeRoot.querySelectorAll("[data-container-item]");
|
|
48
|
+
return Array.from(all).filter((el) => {
|
|
49
|
+
const attr = el.getAttribute("data-container-item") || "";
|
|
50
|
+
if (!attr.startsWith(`${containerPath}[`)) return false;
|
|
51
|
+
return /^\[\d+\]$/.test(attr.slice(containerPath.length));
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function clear(node) {
|
|
55
|
+
while (node.firstChild) node.removeChild(node.firstChild);
|
|
56
|
+
}
|
|
57
|
+
function formatFileSize(bytes) {
|
|
58
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
59
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
60
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
61
|
+
}
|
|
62
|
+
function serializeHiddenValue(value) {
|
|
63
|
+
if (value === null || value === void 0) return "";
|
|
64
|
+
return typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
65
|
+
}
|
|
66
|
+
function deserializeHiddenValue(raw) {
|
|
67
|
+
if (raw === "") return null;
|
|
68
|
+
try {
|
|
69
|
+
return JSON.parse(raw);
|
|
70
|
+
} catch {
|
|
71
|
+
return raw;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function createHiddenInput(name, value) {
|
|
75
|
+
const input = document.createElement("input");
|
|
76
|
+
input.type = "hidden";
|
|
77
|
+
input.name = name;
|
|
78
|
+
input.setAttribute("data-hidden-field", "true");
|
|
79
|
+
input.value = serializeHiddenValue(value);
|
|
80
|
+
return input;
|
|
81
|
+
}
|
|
82
|
+
|
|
18
83
|
// src/utils/validation.ts
|
|
19
84
|
function addLengthHint(element, parts, state) {
|
|
20
85
|
if (element.minLength != null || element.maxLength != null) {
|
|
@@ -184,6 +249,64 @@ function validateSchema(schema) {
|
|
|
184
249
|
}
|
|
185
250
|
}
|
|
186
251
|
}
|
|
252
|
+
function validateCountBounds(element, elementPath, errors2) {
|
|
253
|
+
const el = element;
|
|
254
|
+
if (el.type === "group") {
|
|
255
|
+
if (!isPlainObject(el.repeat)) return;
|
|
256
|
+
checkBounds(
|
|
257
|
+
elementPath,
|
|
258
|
+
el.repeat?.min,
|
|
259
|
+
el.repeat?.max,
|
|
260
|
+
"repeat.min",
|
|
261
|
+
"repeat.max",
|
|
262
|
+
el.required === true,
|
|
263
|
+
errors2
|
|
264
|
+
);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const isMultiple = el.multiple === true || el.type === "files";
|
|
268
|
+
if (!isMultiple) return;
|
|
269
|
+
checkBounds(
|
|
270
|
+
elementPath,
|
|
271
|
+
el.minCount,
|
|
272
|
+
el.maxCount,
|
|
273
|
+
"minCount",
|
|
274
|
+
"maxCount",
|
|
275
|
+
el.required === true,
|
|
276
|
+
errors2
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
function checkBounds(elementPath, minCount, maxCount, minName, maxName, requiredImpliesFloor, errors2) {
|
|
280
|
+
for (const [name, bound] of [
|
|
281
|
+
[minName, minCount],
|
|
282
|
+
[maxName, maxCount]
|
|
283
|
+
]) {
|
|
284
|
+
if (bound !== void 0 && typeof bound !== "number") {
|
|
285
|
+
errors2.push(
|
|
286
|
+
`${elementPath}: ${name} must be a number (got ${typeof bound})`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const min = typeof minCount === "number" ? minCount : void 0;
|
|
291
|
+
const max = typeof maxCount === "number" ? maxCount : void 0;
|
|
292
|
+
if (max !== void 0 && (max < 0 || Number.isNaN(max))) {
|
|
293
|
+
errors2.push(
|
|
294
|
+
`${elementPath}: ${maxName} must be a non-negative number or Infinity (got ${max})`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
if (min !== void 0 && (min < 0 || !Number.isFinite(min))) {
|
|
298
|
+
errors2.push(
|
|
299
|
+
`${elementPath}: ${minName} must be a finite non-negative number (got ${min})`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
const effectiveMin = min ?? (requiredImpliesFloor ? 1 : void 0);
|
|
303
|
+
if (effectiveMin !== void 0 && max !== void 0 && effectiveMin > max) {
|
|
304
|
+
const shown = min !== void 0 ? `${minName} (${min})` : `required: true (implies ${minName} 1)`;
|
|
305
|
+
errors2.push(
|
|
306
|
+
`${elementPath}: ${shown} cannot be greater than ${maxName} (${max})`
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
187
310
|
function validateElements(elements, path) {
|
|
188
311
|
elements.forEach((element, index) => {
|
|
189
312
|
const elementPath = `${path}[${index}]`;
|
|
@@ -193,6 +316,7 @@ function validateSchema(schema) {
|
|
|
193
316
|
if (!element.key && element.type !== "markdown") {
|
|
194
317
|
errors.push(`${elementPath}: missing key`);
|
|
195
318
|
}
|
|
319
|
+
validateCountBounds(element, elementPath, errors);
|
|
196
320
|
if (element.type === "markdown") {
|
|
197
321
|
const content = element.content;
|
|
198
322
|
if (typeof content !== "string") {
|
|
@@ -273,62 +397,6 @@ function validateSchema(schema) {
|
|
|
273
397
|
return errors;
|
|
274
398
|
}
|
|
275
399
|
|
|
276
|
-
// src/utils/helpers.ts
|
|
277
|
-
function isElementReadonly(element, state, ctx) {
|
|
278
|
-
return element.readonly === true || state.config.readonly === true || ctx?.inheritedReadonly === true;
|
|
279
|
-
}
|
|
280
|
-
function isPlainObject(obj) {
|
|
281
|
-
return obj && typeof obj === "object" && obj.constructor === Object;
|
|
282
|
-
}
|
|
283
|
-
function escapeHtml(text) {
|
|
284
|
-
const div = document.createElement("div");
|
|
285
|
-
div.textContent = text;
|
|
286
|
-
return div.innerHTML;
|
|
287
|
-
}
|
|
288
|
-
function getElementLookupKey(element, state) {
|
|
289
|
-
if (element.key) {
|
|
290
|
-
return element.key;
|
|
291
|
-
}
|
|
292
|
-
const cached = state.syntheticElementIds.get(element);
|
|
293
|
-
if (cached !== void 0) {
|
|
294
|
-
return cached;
|
|
295
|
-
}
|
|
296
|
-
const id = `fb-synthetic-${state.syntheticElementIdCounter++}`;
|
|
297
|
-
state.syntheticElementIds.set(element, id);
|
|
298
|
-
return id;
|
|
299
|
-
}
|
|
300
|
-
function pathJoin(base, key) {
|
|
301
|
-
return base ? `${base}.${key}` : key;
|
|
302
|
-
}
|
|
303
|
-
function clear(node) {
|
|
304
|
-
while (node.firstChild) node.removeChild(node.firstChild);
|
|
305
|
-
}
|
|
306
|
-
function formatFileSize(bytes) {
|
|
307
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
308
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
309
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
310
|
-
}
|
|
311
|
-
function serializeHiddenValue(value) {
|
|
312
|
-
if (value === null || value === void 0) return "";
|
|
313
|
-
return typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
314
|
-
}
|
|
315
|
-
function deserializeHiddenValue(raw) {
|
|
316
|
-
if (raw === "") return null;
|
|
317
|
-
try {
|
|
318
|
-
return JSON.parse(raw);
|
|
319
|
-
} catch {
|
|
320
|
-
return raw;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
function createHiddenInput(name, value) {
|
|
324
|
-
const input = document.createElement("input");
|
|
325
|
-
input.type = "hidden";
|
|
326
|
-
input.name = name;
|
|
327
|
-
input.setAttribute("data-hidden-field", "true");
|
|
328
|
-
input.value = serializeHiddenValue(value);
|
|
329
|
-
return input;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
400
|
// src/utils/enable-conditions.ts
|
|
333
401
|
function getValueByPath(data, path) {
|
|
334
402
|
if (!data || typeof data !== "object") {
|
|
@@ -449,6 +517,19 @@ function ensureThemingHooks(doc) {
|
|
|
449
517
|
color: var(--fb-error-color);
|
|
450
518
|
background-color: var(--fb-background-hover-color);
|
|
451
519
|
}
|
|
520
|
+
.fb-item-remove:disabled {
|
|
521
|
+
opacity: 0.35;
|
|
522
|
+
cursor: not-allowed;
|
|
523
|
+
}
|
|
524
|
+
/* Cards carrying a row-level remove button reserve a right-hand lane for it
|
|
525
|
+
(24px button + inset + gap) so it never sits on top of the first field's
|
|
526
|
+
control. A class rather than an inline style, so host CSS can override the
|
|
527
|
+
gutter without !important; the compound selector keeps it ahead of the
|
|
528
|
+
card's own p-2 utility regardless of stylesheet order. Its own token, not
|
|
529
|
+
--fb-slide-card-padding, which is slides-only and always resolves. */
|
|
530
|
+
.containerItem.fb-row-removable {
|
|
531
|
+
padding-right: var(--fb-row-remove-gutter, 40px);
|
|
532
|
+
}
|
|
452
533
|
/* Prefill-suggestion pills rendered by createPrefillHints. Outline pill at rest,
|
|
453
534
|
soft-fill on hover, solid-fill when selected. All colors flow from the active
|
|
454
535
|
theme \u2014 consumers don't need to ship their own CSS. */
|
|
@@ -4856,12 +4937,21 @@ function buildClearAllRow(state, ridCount, onClearAll) {
|
|
|
4856
4937
|
return row;
|
|
4857
4938
|
}
|
|
4858
4939
|
var gridResizeObservers = /* @__PURE__ */ new WeakMap();
|
|
4940
|
+
var gridMeasureFrames = /* @__PURE__ */ new WeakMap();
|
|
4941
|
+
function cancelPendingMeasure(container) {
|
|
4942
|
+
const frame = gridMeasureFrames.get(container);
|
|
4943
|
+
if (frame !== void 0) {
|
|
4944
|
+
cancelAnimationFrame(frame);
|
|
4945
|
+
gridMeasureFrames.delete(container);
|
|
4946
|
+
}
|
|
4947
|
+
}
|
|
4859
4948
|
function disposePlaceholdersForUpload(container) {
|
|
4860
4949
|
const observer = gridResizeObservers.get(container);
|
|
4861
4950
|
if (observer) {
|
|
4862
4951
|
observer.disconnect();
|
|
4863
4952
|
gridResizeObservers.delete(container);
|
|
4864
4953
|
}
|
|
4954
|
+
cancelPendingMeasure(container);
|
|
4865
4955
|
container.querySelectorAll(".fb-multi-placeholder").forEach((p) => p.remove());
|
|
4866
4956
|
}
|
|
4867
4957
|
function renderResourcePills(opts) {
|
|
@@ -4886,6 +4976,7 @@ function renderResourcePills(opts) {
|
|
|
4886
4976
|
previousObserver.disconnect();
|
|
4887
4977
|
gridResizeObservers.delete(container);
|
|
4888
4978
|
}
|
|
4979
|
+
cancelPendingMeasure(container);
|
|
4889
4980
|
while (container.firstChild) container.removeChild(container.firstChild);
|
|
4890
4981
|
const ridList = rids ?? [];
|
|
4891
4982
|
const effectiveMax = maxCount ?? Infinity;
|
|
@@ -4985,7 +5076,7 @@ function renderResourcePills(opts) {
|
|
|
4985
5076
|
if (effectiveMax === Infinity || effectiveMax > occupied) {
|
|
4986
5077
|
grid.appendChild(buildPlaceholderTile());
|
|
4987
5078
|
}
|
|
4988
|
-
requestAnimationFrame(adjustPlaceholders);
|
|
5079
|
+
gridMeasureFrames.set(container, requestAnimationFrame(adjustPlaceholders));
|
|
4989
5080
|
if (typeof ResizeObserver !== "undefined") {
|
|
4990
5081
|
const ro = new ResizeObserver(() => adjustPlaceholders());
|
|
4991
5082
|
ro.observe(grid);
|
|
@@ -6740,11 +6831,14 @@ function getChildWrapperClass(columns) {
|
|
|
6740
6831
|
const cols = columns || 1;
|
|
6741
6832
|
return cols === 1 ? "fb-row" : `grid grid-cols-${cols} gap-2`;
|
|
6742
6833
|
}
|
|
6743
|
-
function mountRemoveButton(item, onRemove, state) {
|
|
6834
|
+
function mountRemoveButton(item, onRemove, state, containerLabel) {
|
|
6744
6835
|
const rem = document.createElement("button");
|
|
6745
6836
|
rem.type = "button";
|
|
6746
6837
|
rem.className = "fb-item-remove";
|
|
6747
|
-
rem.setAttribute(
|
|
6838
|
+
rem.setAttribute(
|
|
6839
|
+
"aria-label",
|
|
6840
|
+
containerLabel ? t("removeRowFrom", state, { label: containerLabel }) : t("removeRow", state)
|
|
6841
|
+
);
|
|
6748
6842
|
rem.style.cssText = `
|
|
6749
6843
|
width: 24px;
|
|
6750
6844
|
height: 24px;
|
|
@@ -6759,17 +6853,13 @@ function mountRemoveButton(item, onRemove, state) {
|
|
|
6759
6853
|
`;
|
|
6760
6854
|
rem.innerHTML = BIN_ICON_SVG;
|
|
6761
6855
|
rem.onclick = onRemove;
|
|
6762
|
-
const labelRow = item.querySelector("[data-fb-label-row]");
|
|
6763
|
-
if (labelRow) {
|
|
6764
|
-
rem.style.marginLeft = "auto";
|
|
6765
|
-
labelRow.appendChild(rem);
|
|
6766
|
-
return;
|
|
6767
|
-
}
|
|
6768
6856
|
rem.style.position = "absolute";
|
|
6769
|
-
rem.style.top = "8px";
|
|
6770
|
-
rem.style.right = "8px";
|
|
6857
|
+
rem.style.top = "var(--fb-row-remove-inset, 8px)";
|
|
6858
|
+
rem.style.right = "var(--fb-row-remove-inset, 8px)";
|
|
6859
|
+
rem.style.zIndex = "1";
|
|
6771
6860
|
item.style.position = "relative";
|
|
6772
|
-
item.
|
|
6861
|
+
item.classList.add("fb-row-removable");
|
|
6862
|
+
item.insertBefore(rem, item.firstChild);
|
|
6773
6863
|
}
|
|
6774
6864
|
function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
6775
6865
|
const state = ctx.state;
|
|
@@ -6794,24 +6884,29 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
6794
6884
|
const max = element.maxCount ?? Infinity;
|
|
6795
6885
|
const pre = Array.isArray(ctx.prefill?.[element.key]) ? ctx.prefill[element.key] : null;
|
|
6796
6886
|
const childDefaults = extractChildDefaults(element.elements);
|
|
6797
|
-
|
|
6798
|
-
const
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6887
|
+
let nextRowIndex = 0;
|
|
6888
|
+
const takeRowIndex = () => nextRowIndex++;
|
|
6889
|
+
const directRows = () => Array.from(itemsWrap.children).filter(
|
|
6890
|
+
(el) => el.classList.contains("containerItem")
|
|
6891
|
+
);
|
|
6892
|
+
const countItems = () => directRows().length;
|
|
6893
|
+
const handleRemoveItem = (item) => {
|
|
6894
|
+
if (countItems() <= min) return;
|
|
6895
|
+
item.remove();
|
|
6896
|
+
updateAddButton();
|
|
6897
|
+
};
|
|
6898
|
+
const createContainerItem = (idx, rowPrefill, formData) => {
|
|
6802
6899
|
const subCtx = {
|
|
6803
|
-
state
|
|
6900
|
+
state,
|
|
6804
6901
|
instance: ctx.instance,
|
|
6805
6902
|
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
6806
|
-
prefill: childDefaults,
|
|
6807
|
-
|
|
6808
|
-
formData: currentFormData,
|
|
6809
|
-
// Current root data from DOM for enableIf
|
|
6903
|
+
prefill: mergeWithDefaults(rowPrefill ?? {}, childDefaults),
|
|
6904
|
+
formData,
|
|
6810
6905
|
inheritedReadonly: childInheritedReadonly
|
|
6811
6906
|
};
|
|
6812
6907
|
const item = document.createElement("div");
|
|
6813
6908
|
item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
|
|
6814
|
-
item.setAttribute("data-container-item",
|
|
6909
|
+
item.setAttribute("data-container-item", subCtx.path);
|
|
6815
6910
|
if (isSlides) {
|
|
6816
6911
|
item.setAttribute("data-fb-slide-card", "");
|
|
6817
6912
|
}
|
|
@@ -6821,11 +6916,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
6821
6916
|
);
|
|
6822
6917
|
element.elements.forEach((child) => {
|
|
6823
6918
|
if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
|
|
6919
|
+
const hiddenValue = rowPrefill?.[child.key] ?? ("default" in child ? child.default : null) ?? null;
|
|
6824
6920
|
childWrapper.appendChild(
|
|
6825
|
-
createHiddenInput(
|
|
6826
|
-
pathJoin(subCtx.path, child.key),
|
|
6827
|
-
("default" in child ? child.default : null) ?? null
|
|
6828
|
-
)
|
|
6921
|
+
createHiddenInput(pathJoin(subCtx.path, child.key), hiddenValue)
|
|
6829
6922
|
);
|
|
6830
6923
|
} else {
|
|
6831
6924
|
childWrapper.appendChild(renderElement(child, subCtx));
|
|
@@ -6833,8 +6926,24 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
6833
6926
|
});
|
|
6834
6927
|
item.appendChild(childWrapper);
|
|
6835
6928
|
if (!containerIsReadonly) {
|
|
6836
|
-
mountRemoveButton(
|
|
6929
|
+
mountRemoveButton(
|
|
6930
|
+
item,
|
|
6931
|
+
() => handleRemoveItem(item),
|
|
6932
|
+
state,
|
|
6933
|
+
element.label
|
|
6934
|
+
);
|
|
6837
6935
|
}
|
|
6936
|
+
return item;
|
|
6937
|
+
};
|
|
6938
|
+
const handleAddItem = () => {
|
|
6939
|
+
if (countItems() >= max) return;
|
|
6940
|
+
const item = createContainerItem(
|
|
6941
|
+
takeRowIndex(),
|
|
6942
|
+
null,
|
|
6943
|
+
// Current root data read back from the DOM, so enableIf sees what the
|
|
6944
|
+
// user has actually typed rather than the original prefill.
|
|
6945
|
+
state.formRoot ? extractRootFormData(state.formRoot) : {}
|
|
6946
|
+
);
|
|
6838
6947
|
if (slideAddTile && slideAddTile.parentElement === itemsWrap) {
|
|
6839
6948
|
itemsWrap.insertBefore(item, slideAddTile);
|
|
6840
6949
|
} else {
|
|
@@ -6847,109 +6956,43 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
6847
6956
|
let pillAddUpdate = null;
|
|
6848
6957
|
const syncSlideTileSize = () => {
|
|
6849
6958
|
if (!slideAddTile) return;
|
|
6850
|
-
const firstSlide =
|
|
6851
|
-
":scope > .containerItem"
|
|
6852
|
-
);
|
|
6959
|
+
const firstSlide = directRows()[0];
|
|
6853
6960
|
if (firstSlide && firstSlide.offsetHeight > 0) {
|
|
6854
6961
|
slideAddTile.style.minHeight = `${firstSlide.offsetHeight}px`;
|
|
6855
6962
|
}
|
|
6856
6963
|
};
|
|
6964
|
+
const syncRemoveButtons = () => {
|
|
6965
|
+
const atFloor = countItems() <= min;
|
|
6966
|
+
directRows().forEach((row) => {
|
|
6967
|
+
const btn = Array.from(row.children).find(
|
|
6968
|
+
(el) => el.classList.contains("fb-item-remove")
|
|
6969
|
+
);
|
|
6970
|
+
if (btn) btn.disabled = atFloor;
|
|
6971
|
+
});
|
|
6972
|
+
};
|
|
6857
6973
|
const updateAddButton = () => {
|
|
6858
6974
|
const currentCount = countItems();
|
|
6859
6975
|
if (slideAddUpdate) slideAddUpdate(currentCount, max);
|
|
6860
6976
|
if (pillAddUpdate) pillAddUpdate(currentCount, max);
|
|
6861
6977
|
if (slideAddTile) syncSlideTileSize();
|
|
6978
|
+
syncRemoveButtons();
|
|
6862
6979
|
};
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
state: ctx.state,
|
|
6872
|
-
instance: ctx.instance,
|
|
6873
|
-
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
6874
|
-
prefill: mergedPrefill,
|
|
6875
|
-
// Merged prefill with defaults for enableIf
|
|
6876
|
-
formData: ctx.formData ?? ctx.prefill,
|
|
6877
|
-
// Complete root data for enableIf
|
|
6878
|
-
inheritedReadonly: childInheritedReadonly
|
|
6879
|
-
};
|
|
6880
|
-
const item = document.createElement("div");
|
|
6881
|
-
item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
|
|
6882
|
-
item.setAttribute("data-container-item", `${element.key}[${idx}]`);
|
|
6883
|
-
if (isSlides) {
|
|
6884
|
-
item.setAttribute("data-fb-slide-card", "");
|
|
6885
|
-
}
|
|
6886
|
-
const childWrapper = document.createElement("div");
|
|
6887
|
-
childWrapper.className = getChildWrapperClass(
|
|
6888
|
-
isSlides ? void 0 : element.columns
|
|
6980
|
+
if (pre) {
|
|
6981
|
+
pre.forEach((prefillObj) => {
|
|
6982
|
+
itemsWrap.appendChild(
|
|
6983
|
+
createContainerItem(
|
|
6984
|
+
takeRowIndex(),
|
|
6985
|
+
prefillObj ?? {},
|
|
6986
|
+
ctx.formData ?? ctx.prefill
|
|
6987
|
+
)
|
|
6889
6988
|
);
|
|
6890
|
-
element.elements.forEach((child) => {
|
|
6891
|
-
if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
|
|
6892
|
-
const prefillVal = prefillObj?.[child.key] ?? ("default" in child ? child.default : null) ?? null;
|
|
6893
|
-
childWrapper.appendChild(
|
|
6894
|
-
createHiddenInput(pathJoin(subCtx.path, child.key), prefillVal)
|
|
6895
|
-
);
|
|
6896
|
-
} else {
|
|
6897
|
-
childWrapper.appendChild(renderElement(child, subCtx));
|
|
6898
|
-
}
|
|
6899
|
-
});
|
|
6900
|
-
item.appendChild(childWrapper);
|
|
6901
|
-
if (!containerIsReadonly) {
|
|
6902
|
-
mountRemoveButton(item, () => handleRemoveItem(item), ctx.state);
|
|
6903
|
-
}
|
|
6904
|
-
itemsWrap.appendChild(item);
|
|
6905
6989
|
});
|
|
6906
6990
|
}
|
|
6907
6991
|
if (!containerIsReadonly) {
|
|
6908
6992
|
while (countItems() < min) {
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
state: ctx.state,
|
|
6912
|
-
instance: ctx.instance,
|
|
6913
|
-
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
6914
|
-
prefill: childDefaults,
|
|
6915
|
-
// Defaults for enableIf evaluation
|
|
6916
|
-
formData: ctx.formData ?? ctx.prefill,
|
|
6917
|
-
// Complete root data for enableIf
|
|
6918
|
-
inheritedReadonly: childInheritedReadonly
|
|
6919
|
-
};
|
|
6920
|
-
const item = document.createElement("div");
|
|
6921
|
-
item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
|
|
6922
|
-
item.setAttribute("data-container-item", `${element.key}[${idx}]`);
|
|
6923
|
-
if (isSlides) {
|
|
6924
|
-
item.setAttribute("data-fb-slide-card", "");
|
|
6925
|
-
}
|
|
6926
|
-
const childWrapper = document.createElement("div");
|
|
6927
|
-
childWrapper.className = getChildWrapperClass(
|
|
6928
|
-
isSlides ? void 0 : element.columns
|
|
6929
|
-
);
|
|
6930
|
-
element.elements.forEach((child) => {
|
|
6931
|
-
if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
|
|
6932
|
-
childWrapper.appendChild(
|
|
6933
|
-
createHiddenInput(
|
|
6934
|
-
pathJoin(subCtx.path, child.key),
|
|
6935
|
-
("default" in child ? child.default : null) ?? null
|
|
6936
|
-
)
|
|
6937
|
-
);
|
|
6938
|
-
} else {
|
|
6939
|
-
childWrapper.appendChild(renderElement(child, subCtx));
|
|
6940
|
-
}
|
|
6941
|
-
});
|
|
6942
|
-
item.appendChild(childWrapper);
|
|
6943
|
-
mountRemoveButton(
|
|
6944
|
-
item,
|
|
6945
|
-
() => {
|
|
6946
|
-
if (countItems() > min) {
|
|
6947
|
-
handleRemoveItem(item);
|
|
6948
|
-
}
|
|
6949
|
-
},
|
|
6950
|
-
ctx.state
|
|
6993
|
+
itemsWrap.appendChild(
|
|
6994
|
+
createContainerItem(takeRowIndex(), null, ctx.formData ?? ctx.prefill)
|
|
6951
6995
|
);
|
|
6952
|
-
itemsWrap.appendChild(item);
|
|
6953
6996
|
}
|
|
6954
6997
|
}
|
|
6955
6998
|
containerWrap.appendChild(itemsWrap);
|
|
@@ -7016,15 +7059,7 @@ function validateContainerElement(element, key, context) {
|
|
|
7016
7059
|
};
|
|
7017
7060
|
if ("multiple" in element && element.multiple) {
|
|
7018
7061
|
const items = [];
|
|
7019
|
-
const
|
|
7020
|
-
"[data-container-item]"
|
|
7021
|
-
);
|
|
7022
|
-
const containerWrappers = Array.from(allContainerWrappers).filter((el) => {
|
|
7023
|
-
const attr = el.getAttribute("data-container-item") || "";
|
|
7024
|
-
if (!attr.startsWith(`${key}[`)) return false;
|
|
7025
|
-
const suffix = attr.slice(key.length);
|
|
7026
|
-
return /^\[\d+\]$/.test(suffix);
|
|
7027
|
-
});
|
|
7062
|
+
const containerWrappers = findDirectContainerRows(scopeRoot, key);
|
|
7028
7063
|
containerWrappers.forEach((itemContainer) => {
|
|
7029
7064
|
const itemData = {};
|
|
7030
7065
|
const containerAttr = itemContainer.getAttribute("data-container-item") || "";
|
|
@@ -7059,7 +7094,7 @@ function validateContainerElement(element, key, context) {
|
|
|
7059
7094
|
);
|
|
7060
7095
|
if (childResult.spread && childResult.value !== null && typeof childResult.value === "object") {
|
|
7061
7096
|
Object.assign(itemData, childResult.value);
|
|
7062
|
-
} else {
|
|
7097
|
+
} else if (!childResult.skip && child.key) {
|
|
7063
7098
|
itemData[child.key] = childResult.value;
|
|
7064
7099
|
}
|
|
7065
7100
|
});
|
|
@@ -7100,7 +7135,7 @@ function validateContainerElement(element, key, context) {
|
|
|
7100
7135
|
);
|
|
7101
7136
|
if (childResult.spread && childResult.value !== null && typeof childResult.value === "object") {
|
|
7102
7137
|
Object.assign(containerData, childResult.value);
|
|
7103
|
-
} else {
|
|
7138
|
+
} else if (!childResult.skip && child.key) {
|
|
7104
7139
|
containerData[child.key] = childResult.value;
|
|
7105
7140
|
}
|
|
7106
7141
|
}
|
|
@@ -7120,11 +7155,13 @@ function updateContainerField(element, fieldPath, value, context) {
|
|
|
7120
7155
|
);
|
|
7121
7156
|
return;
|
|
7122
7157
|
}
|
|
7158
|
+
const rows = findDirectContainerRows(scopeRoot, fieldPath);
|
|
7123
7159
|
value.forEach((itemValue, index) => {
|
|
7124
|
-
|
|
7160
|
+
const rowPath = rows[index]?.getAttribute("data-container-item");
|
|
7161
|
+
if (isPlainObject(itemValue) && rowPath) {
|
|
7125
7162
|
element.elements.forEach((childElement) => {
|
|
7126
7163
|
if (childElement.type === "markdown" || !childElement.key) return;
|
|
7127
|
-
const childPath = `${
|
|
7164
|
+
const childPath = `${rowPath}.${childElement.key}`;
|
|
7128
7165
|
if (childElement.type === "richinput" && childElement.flatOutput) {
|
|
7129
7166
|
const richChild = childElement;
|
|
7130
7167
|
const textKey = richChild.textKey ?? "text";
|
|
@@ -7147,12 +7184,9 @@ function updateContainerField(element, fieldPath, value, context) {
|
|
|
7147
7184
|
});
|
|
7148
7185
|
}
|
|
7149
7186
|
});
|
|
7150
|
-
|
|
7151
|
-
`[data-container-item^="${fieldPath}["]`
|
|
7152
|
-
);
|
|
7153
|
-
if (value.length !== existingContainers.length) {
|
|
7187
|
+
if (value.length !== rows.length) {
|
|
7154
7188
|
console.warn(
|
|
7155
|
-
`updateContainerField: Multiple container field "${fieldPath}"
|
|
7189
|
+
`updateContainerField: Multiple container field "${fieldPath}" received ${value.length} items for ${rows.length} rendered rows. Rows are not added or removed here \u2014 re-render for add/remove.`
|
|
7156
7190
|
);
|
|
7157
7191
|
}
|
|
7158
7192
|
} else {
|
|
@@ -10644,6 +10678,8 @@ var defaultConfig = {
|
|
|
10644
10678
|
en: {
|
|
10645
10679
|
// UI texts
|
|
10646
10680
|
removeElement: "Remove",
|
|
10681
|
+
removeRow: "Remove row",
|
|
10682
|
+
removeRowFrom: "Remove row from {label}",
|
|
10647
10683
|
clickDragText: "Click or drag file",
|
|
10648
10684
|
clickDragTextMultiple: "Click or drag files",
|
|
10649
10685
|
noFileSelected: "No file selected",
|
|
@@ -10717,6 +10753,8 @@ var defaultConfig = {
|
|
|
10717
10753
|
ru: {
|
|
10718
10754
|
// UI texts
|
|
10719
10755
|
removeElement: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C",
|
|
10756
|
+
removeRow: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443",
|
|
10757
|
+
removeRowFrom: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443 \u0438\u0437 \xAB{label}\xBB",
|
|
10720
10758
|
clickDragText: "\u041D\u0430\u0436\u043C\u0438\u0442\u0435 \u0438\u043B\u0438 \u043F\u0435\u0440\u0435\u0442\u0430\u0449\u0438\u0442\u0435 \u0444\u0430\u0439\u043B",
|
|
10721
10759
|
clickDragTextMultiple: "\u041D\u0430\u0436\u043C\u0438\u0442\u0435 \u0438\u043B\u0438 \u043F\u0435\u0440\u0435\u0442\u0430\u0449\u0438\u0442\u0435 \u0444\u0430\u0439\u043B\u044B",
|
|
10722
10760
|
noFileSelected: "\u0424\u0430\u0439\u043B \u043D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D",
|
|
@@ -11106,6 +11144,18 @@ var exampleThemes = {
|
|
|
11106
11144
|
};
|
|
11107
11145
|
|
|
11108
11146
|
// src/instance/FormBuilderInstance.ts
|
|
11147
|
+
function findOwnField(scope, lookupKey, ownBoundary) {
|
|
11148
|
+
const matches = scope.querySelectorAll(
|
|
11149
|
+
`[data-field-key="${lookupKey}"]`
|
|
11150
|
+
);
|
|
11151
|
+
for (const el of Array.from(matches)) {
|
|
11152
|
+
const boundary = el.closest(
|
|
11153
|
+
"[data-container-item], [data-container]"
|
|
11154
|
+
);
|
|
11155
|
+
if (boundary === ownBoundary) return el;
|
|
11156
|
+
}
|
|
11157
|
+
return null;
|
|
11158
|
+
}
|
|
11109
11159
|
var FormBuilderInstance = class {
|
|
11110
11160
|
constructor(config) {
|
|
11111
11161
|
this.instanceId = generateInstanceId();
|
|
@@ -11828,28 +11878,28 @@ var FormBuilderInstance = class {
|
|
|
11828
11878
|
const formRoot = this.state.formRoot;
|
|
11829
11879
|
const lookupKey = getElementLookupKey(element, this.state);
|
|
11830
11880
|
if (!currentPath) {
|
|
11831
|
-
return formRoot
|
|
11881
|
+
return findOwnField(formRoot, lookupKey, null);
|
|
11832
11882
|
}
|
|
11833
11883
|
const pathMatch = currentPath.match(/^(.+)\[(\d+)\]$/);
|
|
11834
11884
|
if (pathMatch) {
|
|
11835
11885
|
const containerEl2 = formRoot.querySelector(
|
|
11836
11886
|
`[data-container-item="${pathMatch[1]}[${pathMatch[2]}]"]`
|
|
11837
11887
|
);
|
|
11838
|
-
return containerEl2 ? containerEl2
|
|
11888
|
+
return containerEl2 ? findOwnField(containerEl2, lookupKey, containerEl2) : null;
|
|
11839
11889
|
}
|
|
11840
11890
|
const containerEl = formRoot.querySelector(
|
|
11841
11891
|
`[data-container="${currentPath}"]`
|
|
11842
11892
|
);
|
|
11843
|
-
return containerEl ? containerEl
|
|
11893
|
+
return containerEl ? findOwnField(containerEl, lookupKey, containerEl) : null;
|
|
11844
11894
|
}
|
|
11845
11895
|
/**
|
|
11846
11896
|
* Apply enableIf show/hide logic to a single field wrapper.
|
|
11847
11897
|
* Extracted to reduce cyclomatic complexity of checkElements.
|
|
11848
11898
|
*/
|
|
11849
|
-
applyEnableIfVisibility(element, wrapper,
|
|
11899
|
+
applyEnableIfVisibility(element, wrapper, domPath, dataPath, fullDomPath, formData) {
|
|
11850
11900
|
try {
|
|
11851
11901
|
const scope = element.enableIf.scope ?? "relative";
|
|
11852
|
-
const containerData = scope === "relative" &&
|
|
11902
|
+
const containerData = scope === "relative" && dataPath ? getValueByPath(formData, dataPath) : void 0;
|
|
11853
11903
|
const shouldEnable = evaluateEnableCondition(
|
|
11854
11904
|
element.enableIf,
|
|
11855
11905
|
formData,
|
|
@@ -11857,10 +11907,12 @@ var FormBuilderInstance = class {
|
|
|
11857
11907
|
);
|
|
11858
11908
|
const isCurrentlyDisabled = wrapper.getAttribute("data-conditionally-disabled") === "true";
|
|
11859
11909
|
if (shouldEnable && isCurrentlyDisabled) {
|
|
11860
|
-
const containerPrefill =
|
|
11910
|
+
const containerPrefill = dataPath ? getValueByPath(formData, dataPath) : formData;
|
|
11861
11911
|
const prefillContext = containerPrefill && typeof containerPrefill === "object" ? containerPrefill : {};
|
|
11862
11912
|
const newWrapper = renderElement2(element, {
|
|
11863
|
-
path:
|
|
11913
|
+
// DOM path: the re-rendered control's `name` must match the row it
|
|
11914
|
+
// lives in, not the row's position in the extracted array.
|
|
11915
|
+
path: domPath,
|
|
11864
11916
|
prefill: prefillContext,
|
|
11865
11917
|
formData,
|
|
11866
11918
|
state: this.state,
|
|
@@ -11880,7 +11932,7 @@ var FormBuilderInstance = class {
|
|
|
11880
11932
|
}
|
|
11881
11933
|
} catch (error) {
|
|
11882
11934
|
console.error(
|
|
11883
|
-
`Error re-evaluating enableIf for field "${element.key ?? "<no key>"}" at path "${
|
|
11935
|
+
`Error re-evaluating enableIf for field "${element.key ?? "<no key>"}" at path "${fullDomPath}":`,
|
|
11884
11936
|
error
|
|
11885
11937
|
);
|
|
11886
11938
|
}
|
|
@@ -11892,44 +11944,46 @@ var FormBuilderInstance = class {
|
|
|
11892
11944
|
reevaluateConditionalFields() {
|
|
11893
11945
|
if (!this.state.schema || !this.state.formRoot) return;
|
|
11894
11946
|
const formData = this.validateForm(true).data;
|
|
11895
|
-
const checkElements = (elements,
|
|
11947
|
+
const checkElements = (elements, domPath, dataPath) => {
|
|
11896
11948
|
elements.forEach((element) => {
|
|
11897
|
-
const
|
|
11949
|
+
const key = element.key ?? "";
|
|
11950
|
+
const fullDomPath = domPath ? `${domPath}.${key}` : key;
|
|
11951
|
+
const fullDataPath = dataPath ? `${dataPath}.${key}` : key;
|
|
11898
11952
|
if (element.enableIf) {
|
|
11899
|
-
const fieldWrapper = this.findFieldWrapper(element,
|
|
11953
|
+
const fieldWrapper = this.findFieldWrapper(element, domPath);
|
|
11900
11954
|
if (fieldWrapper) {
|
|
11901
11955
|
this.applyEnableIfVisibility(
|
|
11902
11956
|
element,
|
|
11903
11957
|
fieldWrapper,
|
|
11904
|
-
|
|
11905
|
-
|
|
11958
|
+
domPath,
|
|
11959
|
+
dataPath,
|
|
11960
|
+
fullDomPath,
|
|
11906
11961
|
formData
|
|
11907
11962
|
);
|
|
11908
11963
|
}
|
|
11909
11964
|
}
|
|
11910
11965
|
if ((element.type === "container" || element.type === "group") && "elements" in element && element.elements) {
|
|
11911
|
-
const containerData = element.key ? formData
|
|
11966
|
+
const containerData = element.key ? getValueByPath(formData, fullDataPath) : void 0;
|
|
11912
11967
|
if (Array.isArray(containerData)) {
|
|
11913
|
-
const
|
|
11914
|
-
|
|
11968
|
+
const directItems = findDirectContainerRows(
|
|
11969
|
+
this.state.formRoot,
|
|
11970
|
+
fullDomPath
|
|
11915
11971
|
);
|
|
11916
|
-
|
|
11917
|
-
const
|
|
11918
|
-
|
|
11919
|
-
|
|
11920
|
-
|
|
11921
|
-
|
|
11922
|
-
|
|
11923
|
-
const attr = el.getAttribute("data-container-item") || "";
|
|
11924
|
-
checkElements(element.elements, attr);
|
|
11972
|
+
directItems.forEach((el, rowIndex) => {
|
|
11973
|
+
const marker = el.getAttribute("data-container-item") || "";
|
|
11974
|
+
checkElements(
|
|
11975
|
+
element.elements,
|
|
11976
|
+
marker,
|
|
11977
|
+
`${fullDataPath}[${rowIndex}]`
|
|
11978
|
+
);
|
|
11925
11979
|
});
|
|
11926
11980
|
} else {
|
|
11927
|
-
checkElements(element.elements,
|
|
11981
|
+
checkElements(element.elements, fullDomPath, fullDataPath);
|
|
11928
11982
|
}
|
|
11929
11983
|
}
|
|
11930
11984
|
});
|
|
11931
11985
|
};
|
|
11932
|
-
checkElements(this.state.schema.elements, "");
|
|
11986
|
+
checkElements(this.state.schema.elements, "", "");
|
|
11933
11987
|
}
|
|
11934
11988
|
/**
|
|
11935
11989
|
* Destroy instance and clean up resources
|