@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.
@@ -19,6 +19,71 @@ function t(key, state, params) {
19
19
  return text;
20
20
  }
21
21
 
22
+ // src/utils/helpers.ts
23
+ function isElementReadonly(element, state, ctx) {
24
+ return element.readonly === true || state.config.readonly === true || (ctx == null ? void 0 : ctx.inheritedReadonly) === true;
25
+ }
26
+ function isPlainObject(obj) {
27
+ return obj && typeof obj === "object" && obj.constructor === Object;
28
+ }
29
+ function escapeHtml(text) {
30
+ const div = document.createElement("div");
31
+ div.textContent = text;
32
+ return div.innerHTML;
33
+ }
34
+ function getElementLookupKey(element, state) {
35
+ if (element.key) {
36
+ return element.key;
37
+ }
38
+ const cached = state.syntheticElementIds.get(element);
39
+ if (cached !== void 0) {
40
+ return cached;
41
+ }
42
+ const id = `fb-synthetic-${state.syntheticElementIdCounter++}`;
43
+ state.syntheticElementIds.set(element, id);
44
+ return id;
45
+ }
46
+ function pathJoin(base, key) {
47
+ return base ? `${base}.${key}` : key;
48
+ }
49
+ function findDirectContainerRows(scopeRoot, containerPath) {
50
+ if (!containerPath) return [];
51
+ const all = scopeRoot.querySelectorAll("[data-container-item]");
52
+ return Array.from(all).filter((el) => {
53
+ const attr = el.getAttribute("data-container-item") || "";
54
+ if (!attr.startsWith(`${containerPath}[`)) return false;
55
+ return /^\[\d+\]$/.test(attr.slice(containerPath.length));
56
+ });
57
+ }
58
+ function clear(node) {
59
+ while (node.firstChild) node.removeChild(node.firstChild);
60
+ }
61
+ function formatFileSize(bytes) {
62
+ if (bytes < 1024) return `${bytes} B`;
63
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
64
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
65
+ }
66
+ function serializeHiddenValue(value) {
67
+ if (value === null || value === void 0) return "";
68
+ return typeof value === "object" ? JSON.stringify(value) : String(value);
69
+ }
70
+ function deserializeHiddenValue(raw) {
71
+ if (raw === "") return null;
72
+ try {
73
+ return JSON.parse(raw);
74
+ } catch {
75
+ return raw;
76
+ }
77
+ }
78
+ function createHiddenInput(name, value) {
79
+ const input = document.createElement("input");
80
+ input.type = "hidden";
81
+ input.name = name;
82
+ input.setAttribute("data-hidden-field", "true");
83
+ input.value = serializeHiddenValue(value);
84
+ return input;
85
+ }
86
+
22
87
  // src/utils/validation.ts
23
88
  function addLengthHint(element, parts, state) {
24
89
  if (element.minLength != null || element.maxLength != null) {
@@ -191,6 +256,65 @@ function validateSchema(schema) {
191
256
  }
192
257
  }
193
258
  }
259
+ function validateCountBounds(element, elementPath, errors2) {
260
+ var _a, _b;
261
+ const el = element;
262
+ if (el.type === "group") {
263
+ if (!isPlainObject(el.repeat)) return;
264
+ checkBounds(
265
+ elementPath,
266
+ (_a = el.repeat) == null ? void 0 : _a.min,
267
+ (_b = el.repeat) == null ? void 0 : _b.max,
268
+ "repeat.min",
269
+ "repeat.max",
270
+ el.required === true,
271
+ errors2
272
+ );
273
+ return;
274
+ }
275
+ const isMultiple = el.multiple === true || el.type === "files";
276
+ if (!isMultiple) return;
277
+ checkBounds(
278
+ elementPath,
279
+ el.minCount,
280
+ el.maxCount,
281
+ "minCount",
282
+ "maxCount",
283
+ el.required === true,
284
+ errors2
285
+ );
286
+ }
287
+ function checkBounds(elementPath, minCount, maxCount, minName, maxName, requiredImpliesFloor, errors2) {
288
+ for (const [name, bound] of [
289
+ [minName, minCount],
290
+ [maxName, maxCount]
291
+ ]) {
292
+ if (bound !== void 0 && typeof bound !== "number") {
293
+ errors2.push(
294
+ `${elementPath}: ${name} must be a number (got ${typeof bound})`
295
+ );
296
+ }
297
+ }
298
+ const min = typeof minCount === "number" ? minCount : void 0;
299
+ const max = typeof maxCount === "number" ? maxCount : void 0;
300
+ if (max !== void 0 && (max < 0 || Number.isNaN(max))) {
301
+ errors2.push(
302
+ `${elementPath}: ${maxName} must be a non-negative number or Infinity (got ${max})`
303
+ );
304
+ }
305
+ if (min !== void 0 && (min < 0 || !Number.isFinite(min))) {
306
+ errors2.push(
307
+ `${elementPath}: ${minName} must be a finite non-negative number (got ${min})`
308
+ );
309
+ }
310
+ const effectiveMin = min != null ? min : requiredImpliesFloor ? 1 : void 0;
311
+ if (effectiveMin !== void 0 && max !== void 0 && effectiveMin > max) {
312
+ const shown = min !== void 0 ? `${minName} (${min})` : `required: true (implies ${minName} 1)`;
313
+ errors2.push(
314
+ `${elementPath}: ${shown} cannot be greater than ${maxName} (${max})`
315
+ );
316
+ }
317
+ }
194
318
  function validateElements(elements, path) {
195
319
  elements.forEach((element, index) => {
196
320
  const elementPath = `${path}[${index}]`;
@@ -200,6 +324,7 @@ function validateSchema(schema) {
200
324
  if (!element.key && element.type !== "markdown") {
201
325
  errors.push(`${elementPath}: missing key`);
202
326
  }
327
+ validateCountBounds(element, elementPath, errors);
203
328
  if (element.type === "markdown") {
204
329
  const content = element.content;
205
330
  if (typeof content !== "string") {
@@ -280,62 +405,6 @@ function validateSchema(schema) {
280
405
  return errors;
281
406
  }
282
407
 
283
- // src/utils/helpers.ts
284
- function isElementReadonly(element, state, ctx) {
285
- return element.readonly === true || state.config.readonly === true || (ctx == null ? void 0 : ctx.inheritedReadonly) === true;
286
- }
287
- function isPlainObject(obj) {
288
- return obj && typeof obj === "object" && obj.constructor === Object;
289
- }
290
- function escapeHtml(text) {
291
- const div = document.createElement("div");
292
- div.textContent = text;
293
- return div.innerHTML;
294
- }
295
- function getElementLookupKey(element, state) {
296
- if (element.key) {
297
- return element.key;
298
- }
299
- const cached = state.syntheticElementIds.get(element);
300
- if (cached !== void 0) {
301
- return cached;
302
- }
303
- const id = `fb-synthetic-${state.syntheticElementIdCounter++}`;
304
- state.syntheticElementIds.set(element, id);
305
- return id;
306
- }
307
- function pathJoin(base, key) {
308
- return base ? `${base}.${key}` : key;
309
- }
310
- function clear(node) {
311
- while (node.firstChild) node.removeChild(node.firstChild);
312
- }
313
- function formatFileSize(bytes) {
314
- if (bytes < 1024) return `${bytes} B`;
315
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
316
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
317
- }
318
- function serializeHiddenValue(value) {
319
- if (value === null || value === void 0) return "";
320
- return typeof value === "object" ? JSON.stringify(value) : String(value);
321
- }
322
- function deserializeHiddenValue(raw) {
323
- if (raw === "") return null;
324
- try {
325
- return JSON.parse(raw);
326
- } catch {
327
- return raw;
328
- }
329
- }
330
- function createHiddenInput(name, value) {
331
- const input = document.createElement("input");
332
- input.type = "hidden";
333
- input.name = name;
334
- input.setAttribute("data-hidden-field", "true");
335
- input.value = serializeHiddenValue(value);
336
- return input;
337
- }
338
-
339
408
  // src/utils/enable-conditions.ts
340
409
  function getValueByPath(data, path) {
341
410
  if (!data || typeof data !== "object") {
@@ -457,6 +526,19 @@ function ensureThemingHooks(doc) {
457
526
  color: var(--fb-error-color);
458
527
  background-color: var(--fb-background-hover-color);
459
528
  }
529
+ .fb-item-remove:disabled {
530
+ opacity: 0.35;
531
+ cursor: not-allowed;
532
+ }
533
+ /* Cards carrying a row-level remove button reserve a right-hand lane for it
534
+ (24px button + inset + gap) so it never sits on top of the first field's
535
+ control. A class rather than an inline style, so host CSS can override the
536
+ gutter without !important; the compound selector keeps it ahead of the
537
+ card's own p-2 utility regardless of stylesheet order. Its own token, not
538
+ --fb-slide-card-padding, which is slides-only and always resolves. */
539
+ .containerItem.fb-row-removable {
540
+ padding-right: var(--fb-row-remove-gutter, 40px);
541
+ }
460
542
  /* Prefill-suggestion pills rendered by createPrefillHints. Outline pill at rest,
461
543
  soft-fill on hover, solid-fill when selected. All colors flow from the active
462
544
  theme \u2014 consumers don't need to ship their own CSS. */
@@ -4934,12 +5016,21 @@ function buildClearAllRow(state, ridCount, onClearAll) {
4934
5016
  return row;
4935
5017
  }
4936
5018
  var gridResizeObservers = /* @__PURE__ */ new WeakMap();
5019
+ var gridMeasureFrames = /* @__PURE__ */ new WeakMap();
5020
+ function cancelPendingMeasure(container) {
5021
+ const frame = gridMeasureFrames.get(container);
5022
+ if (frame !== void 0) {
5023
+ cancelAnimationFrame(frame);
5024
+ gridMeasureFrames.delete(container);
5025
+ }
5026
+ }
4937
5027
  function disposePlaceholdersForUpload(container) {
4938
5028
  const observer = gridResizeObservers.get(container);
4939
5029
  if (observer) {
4940
5030
  observer.disconnect();
4941
5031
  gridResizeObservers.delete(container);
4942
5032
  }
5033
+ cancelPendingMeasure(container);
4943
5034
  container.querySelectorAll(".fb-multi-placeholder").forEach((p) => p.remove());
4944
5035
  }
4945
5036
  function renderResourcePills(opts) {
@@ -4965,6 +5056,7 @@ function renderResourcePills(opts) {
4965
5056
  previousObserver.disconnect();
4966
5057
  gridResizeObservers.delete(container);
4967
5058
  }
5059
+ cancelPendingMeasure(container);
4968
5060
  while (container.firstChild) container.removeChild(container.firstChild);
4969
5061
  const ridList = rids != null ? rids : [];
4970
5062
  const effectiveMax = maxCount != null ? maxCount : Infinity;
@@ -5066,7 +5158,7 @@ function renderResourcePills(opts) {
5066
5158
  if (effectiveMax === Infinity || effectiveMax > occupied) {
5067
5159
  grid.appendChild(buildPlaceholderTile());
5068
5160
  }
5069
- requestAnimationFrame(adjustPlaceholders);
5161
+ gridMeasureFrames.set(container, requestAnimationFrame(adjustPlaceholders));
5070
5162
  if (typeof ResizeObserver !== "undefined") {
5071
5163
  const ro = new ResizeObserver(() => adjustPlaceholders());
5072
5164
  ro.observe(grid);
@@ -6846,11 +6938,14 @@ function getChildWrapperClass(columns) {
6846
6938
  const cols = columns || 1;
6847
6939
  return cols === 1 ? "fb-row" : `grid grid-cols-${cols} gap-2`;
6848
6940
  }
6849
- function mountRemoveButton(item, onRemove, state) {
6941
+ function mountRemoveButton(item, onRemove, state, containerLabel) {
6850
6942
  const rem = document.createElement("button");
6851
6943
  rem.type = "button";
6852
6944
  rem.className = "fb-item-remove";
6853
- rem.setAttribute("aria-label", t("removeElement", state));
6945
+ rem.setAttribute(
6946
+ "aria-label",
6947
+ containerLabel ? t("removeRowFrom", state, { label: containerLabel }) : t("removeRow", state)
6948
+ );
6854
6949
  rem.style.cssText = `
6855
6950
  width: 24px;
6856
6951
  height: 24px;
@@ -6865,17 +6960,13 @@ function mountRemoveButton(item, onRemove, state) {
6865
6960
  `;
6866
6961
  rem.innerHTML = BIN_ICON_SVG;
6867
6962
  rem.onclick = onRemove;
6868
- const labelRow = item.querySelector("[data-fb-label-row]");
6869
- if (labelRow) {
6870
- rem.style.marginLeft = "auto";
6871
- labelRow.appendChild(rem);
6872
- return;
6873
- }
6874
6963
  rem.style.position = "absolute";
6875
- rem.style.top = "8px";
6876
- rem.style.right = "8px";
6964
+ rem.style.top = "var(--fb-row-remove-inset, 8px)";
6965
+ rem.style.right = "var(--fb-row-remove-inset, 8px)";
6966
+ rem.style.zIndex = "1";
6877
6967
  item.style.position = "relative";
6878
- item.appendChild(rem);
6968
+ item.classList.add("fb-row-removable");
6969
+ item.insertBefore(rem, item.firstChild);
6879
6970
  }
6880
6971
  function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6881
6972
  var _a, _b, _c, _d;
@@ -6901,24 +6992,29 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6901
6992
  const max = (_b = element.maxCount) != null ? _b : Infinity;
6902
6993
  const pre = Array.isArray((_c = ctx.prefill) == null ? void 0 : _c[element.key]) ? ctx.prefill[element.key] : null;
6903
6994
  const childDefaults = extractChildDefaults(element.elements);
6904
- const countItems = () => itemsWrap.querySelectorAll(":scope > .containerItem").length;
6905
- const handleAddItem = () => {
6906
- if (countItems() >= max) return;
6907
- const idx = countItems();
6908
- const currentFormData = state.formRoot ? extractRootFormData(state.formRoot) : {};
6995
+ let nextRowIndex = 0;
6996
+ const takeRowIndex = () => nextRowIndex++;
6997
+ const directRows = () => Array.from(itemsWrap.children).filter(
6998
+ (el) => el.classList.contains("containerItem")
6999
+ );
7000
+ const countItems = () => directRows().length;
7001
+ const handleRemoveItem = (item) => {
7002
+ if (countItems() <= min) return;
7003
+ item.remove();
7004
+ updateAddButton();
7005
+ };
7006
+ const createContainerItem = (idx, rowPrefill, formData) => {
6909
7007
  const subCtx = {
6910
- state: ctx.state,
7008
+ state,
6911
7009
  instance: ctx.instance,
6912
7010
  path: pathJoin(ctx.path, `${element.key}[${idx}]`),
6913
- prefill: childDefaults,
6914
- // Defaults for enableIf evaluation
6915
- formData: currentFormData,
6916
- // Current root data from DOM for enableIf
7011
+ prefill: mergeWithDefaults(rowPrefill != null ? rowPrefill : {}, childDefaults),
7012
+ formData,
6917
7013
  inheritedReadonly: childInheritedReadonly
6918
7014
  };
6919
7015
  const item = document.createElement("div");
6920
7016
  item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
6921
- item.setAttribute("data-container-item", `${element.key}[${idx}]`);
7017
+ item.setAttribute("data-container-item", subCtx.path);
6922
7018
  if (isSlides) {
6923
7019
  item.setAttribute("data-fb-slide-card", "");
6924
7020
  }
@@ -6927,13 +7023,11 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6927
7023
  isSlides ? void 0 : element.columns
6928
7024
  );
6929
7025
  element.elements.forEach((child) => {
6930
- var _a2;
7026
+ var _a2, _b2;
6931
7027
  if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
7028
+ const hiddenValue = (_b2 = (_a2 = rowPrefill == null ? void 0 : rowPrefill[child.key]) != null ? _a2 : "default" in child ? child.default : null) != null ? _b2 : null;
6932
7029
  childWrapper.appendChild(
6933
- createHiddenInput(
6934
- pathJoin(subCtx.path, child.key),
6935
- (_a2 = "default" in child ? child.default : null) != null ? _a2 : null
6936
- )
7030
+ createHiddenInput(pathJoin(subCtx.path, child.key), hiddenValue)
6937
7031
  );
6938
7032
  } else {
6939
7033
  childWrapper.appendChild(renderElement(child, subCtx));
@@ -6941,8 +7035,24 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6941
7035
  });
6942
7036
  item.appendChild(childWrapper);
6943
7037
  if (!containerIsReadonly) {
6944
- mountRemoveButton(item, () => handleRemoveItem(item), state);
7038
+ mountRemoveButton(
7039
+ item,
7040
+ () => handleRemoveItem(item),
7041
+ state,
7042
+ element.label
7043
+ );
6945
7044
  }
7045
+ return item;
7046
+ };
7047
+ const handleAddItem = () => {
7048
+ if (countItems() >= max) return;
7049
+ const item = createContainerItem(
7050
+ takeRowIndex(),
7051
+ null,
7052
+ // Current root data read back from the DOM, so enableIf sees what the
7053
+ // user has actually typed rather than the original prefill.
7054
+ state.formRoot ? extractRootFormData(state.formRoot) : {}
7055
+ );
6946
7056
  if (slideAddTile && slideAddTile.parentElement === itemsWrap) {
6947
7057
  itemsWrap.insertBefore(item, slideAddTile);
6948
7058
  } else {
@@ -6955,112 +7065,44 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6955
7065
  let pillAddUpdate = null;
6956
7066
  const syncSlideTileSize = () => {
6957
7067
  if (!slideAddTile) return;
6958
- const firstSlide = itemsWrap.querySelector(
6959
- ":scope > .containerItem"
6960
- );
7068
+ const firstSlide = directRows()[0];
6961
7069
  if (firstSlide && firstSlide.offsetHeight > 0) {
6962
7070
  slideAddTile.style.minHeight = `${firstSlide.offsetHeight}px`;
6963
7071
  }
6964
7072
  };
7073
+ const syncRemoveButtons = () => {
7074
+ const atFloor = countItems() <= min;
7075
+ directRows().forEach((row) => {
7076
+ const btn = Array.from(row.children).find(
7077
+ (el) => el.classList.contains("fb-item-remove")
7078
+ );
7079
+ if (btn) btn.disabled = atFloor;
7080
+ });
7081
+ };
6965
7082
  const updateAddButton = () => {
6966
7083
  const currentCount = countItems();
6967
7084
  if (slideAddUpdate) slideAddUpdate(currentCount, max);
6968
7085
  if (pillAddUpdate) pillAddUpdate(currentCount, max);
6969
7086
  if (slideAddTile) syncSlideTileSize();
7087
+ syncRemoveButtons();
6970
7088
  };
6971
- const handleRemoveItem = (item) => {
6972
- item.remove();
6973
- updateAddButton();
6974
- };
6975
- if (pre && Array.isArray(pre)) {
6976
- pre.forEach((prefillObj, idx) => {
7089
+ if (pre) {
7090
+ pre.forEach((prefillObj) => {
6977
7091
  var _a2;
6978
- const mergedPrefill = mergeWithDefaults(prefillObj || {}, childDefaults);
6979
- const subCtx = {
6980
- state: ctx.state,
6981
- instance: ctx.instance,
6982
- path: pathJoin(ctx.path, `${element.key}[${idx}]`),
6983
- prefill: mergedPrefill,
6984
- // Merged prefill with defaults for enableIf
6985
- formData: (_a2 = ctx.formData) != null ? _a2 : ctx.prefill,
6986
- // Complete root data for enableIf
6987
- inheritedReadonly: childInheritedReadonly
6988
- };
6989
- const item = document.createElement("div");
6990
- item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
6991
- item.setAttribute("data-container-item", `${element.key}[${idx}]`);
6992
- if (isSlides) {
6993
- item.setAttribute("data-fb-slide-card", "");
6994
- }
6995
- const childWrapper = document.createElement("div");
6996
- childWrapper.className = getChildWrapperClass(
6997
- isSlides ? void 0 : element.columns
7092
+ itemsWrap.appendChild(
7093
+ createContainerItem(
7094
+ takeRowIndex(),
7095
+ prefillObj != null ? prefillObj : {},
7096
+ (_a2 = ctx.formData) != null ? _a2 : ctx.prefill
7097
+ )
6998
7098
  );
6999
- element.elements.forEach((child) => {
7000
- var _a3, _b2;
7001
- if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
7002
- const prefillVal = (_b2 = (_a3 = prefillObj == null ? void 0 : prefillObj[child.key]) != null ? _a3 : "default" in child ? child.default : null) != null ? _b2 : null;
7003
- childWrapper.appendChild(
7004
- createHiddenInput(pathJoin(subCtx.path, child.key), prefillVal)
7005
- );
7006
- } else {
7007
- childWrapper.appendChild(renderElement(child, subCtx));
7008
- }
7009
- });
7010
- item.appendChild(childWrapper);
7011
- if (!containerIsReadonly) {
7012
- mountRemoveButton(item, () => handleRemoveItem(item), ctx.state);
7013
- }
7014
- itemsWrap.appendChild(item);
7015
7099
  });
7016
7100
  }
7017
7101
  if (!containerIsReadonly) {
7018
7102
  while (countItems() < min) {
7019
- const idx = countItems();
7020
- const subCtx = {
7021
- state: ctx.state,
7022
- instance: ctx.instance,
7023
- path: pathJoin(ctx.path, `${element.key}[${idx}]`),
7024
- prefill: childDefaults,
7025
- // Defaults for enableIf evaluation
7026
- formData: (_d = ctx.formData) != null ? _d : ctx.prefill,
7027
- // Complete root data for enableIf
7028
- inheritedReadonly: childInheritedReadonly
7029
- };
7030
- const item = document.createElement("div");
7031
- item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
7032
- item.setAttribute("data-container-item", `${element.key}[${idx}]`);
7033
- if (isSlides) {
7034
- item.setAttribute("data-fb-slide-card", "");
7035
- }
7036
- const childWrapper = document.createElement("div");
7037
- childWrapper.className = getChildWrapperClass(
7038
- isSlides ? void 0 : element.columns
7039
- );
7040
- element.elements.forEach((child) => {
7041
- var _a2;
7042
- if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
7043
- childWrapper.appendChild(
7044
- createHiddenInput(
7045
- pathJoin(subCtx.path, child.key),
7046
- (_a2 = "default" in child ? child.default : null) != null ? _a2 : null
7047
- )
7048
- );
7049
- } else {
7050
- childWrapper.appendChild(renderElement(child, subCtx));
7051
- }
7052
- });
7053
- item.appendChild(childWrapper);
7054
- mountRemoveButton(
7055
- item,
7056
- () => {
7057
- if (countItems() > min) {
7058
- handleRemoveItem(item);
7059
- }
7060
- },
7061
- ctx.state
7103
+ itemsWrap.appendChild(
7104
+ createContainerItem(takeRowIndex(), null, (_d = ctx.formData) != null ? _d : ctx.prefill)
7062
7105
  );
7063
- itemsWrap.appendChild(item);
7064
7106
  }
7065
7107
  }
7066
7108
  containerWrap.appendChild(itemsWrap);
@@ -7128,15 +7170,7 @@ function validateContainerElement(element, key, context) {
7128
7170
  };
7129
7171
  if ("multiple" in element && element.multiple) {
7130
7172
  const items = [];
7131
- const allContainerWrappers = scopeRoot.querySelectorAll(
7132
- "[data-container-item]"
7133
- );
7134
- const containerWrappers = Array.from(allContainerWrappers).filter((el) => {
7135
- const attr = el.getAttribute("data-container-item") || "";
7136
- if (!attr.startsWith(`${key}[`)) return false;
7137
- const suffix = attr.slice(key.length);
7138
- return /^\[\d+\]$/.test(suffix);
7139
- });
7173
+ const containerWrappers = findDirectContainerRows(scopeRoot, key);
7140
7174
  containerWrappers.forEach((itemContainer) => {
7141
7175
  const itemData = {};
7142
7176
  const containerAttr = itemContainer.getAttribute("data-container-item") || "";
@@ -7172,7 +7206,7 @@ function validateContainerElement(element, key, context) {
7172
7206
  );
7173
7207
  if (childResult.spread && childResult.value !== null && typeof childResult.value === "object") {
7174
7208
  Object.assign(itemData, childResult.value);
7175
- } else {
7209
+ } else if (!childResult.skip && child.key) {
7176
7210
  itemData[child.key] = childResult.value;
7177
7211
  }
7178
7212
  });
@@ -7214,7 +7248,7 @@ function validateContainerElement(element, key, context) {
7214
7248
  );
7215
7249
  if (childResult.spread && childResult.value !== null && typeof childResult.value === "object") {
7216
7250
  Object.assign(containerData, childResult.value);
7217
- } else {
7251
+ } else if (!childResult.skip && child.key) {
7218
7252
  containerData[child.key] = childResult.value;
7219
7253
  }
7220
7254
  }
@@ -7234,15 +7268,18 @@ function updateContainerField(element, fieldPath, value, context) {
7234
7268
  );
7235
7269
  return;
7236
7270
  }
7271
+ const rows = findDirectContainerRows(scopeRoot, fieldPath);
7237
7272
  value.forEach((itemValue, index) => {
7238
- if (isPlainObject(itemValue)) {
7273
+ var _a;
7274
+ const rowPath = (_a = rows[index]) == null ? void 0 : _a.getAttribute("data-container-item");
7275
+ if (isPlainObject(itemValue) && rowPath) {
7239
7276
  element.elements.forEach((childElement) => {
7240
- var _a, _b;
7277
+ var _a2, _b;
7241
7278
  if (childElement.type === "markdown" || !childElement.key) return;
7242
- const childPath = `${fieldPath}[${index}].${childElement.key}`;
7279
+ const childPath = `${rowPath}.${childElement.key}`;
7243
7280
  if (childElement.type === "richinput" && childElement.flatOutput) {
7244
7281
  const richChild = childElement;
7245
- const textKey = (_a = richChild.textKey) != null ? _a : "text";
7282
+ const textKey = (_a2 = richChild.textKey) != null ? _a2 : "text";
7246
7283
  const filesKey = (_b = richChild.filesKey) != null ? _b : "files";
7247
7284
  const containerValue = itemValue;
7248
7285
  const compositeValue = {};
@@ -7262,12 +7299,9 @@ function updateContainerField(element, fieldPath, value, context) {
7262
7299
  });
7263
7300
  }
7264
7301
  });
7265
- const existingContainers = scopeRoot.querySelectorAll(
7266
- `[data-container-item^="${fieldPath}["]`
7267
- );
7268
- if (value.length !== existingContainers.length) {
7302
+ if (value.length !== rows.length) {
7269
7303
  console.warn(
7270
- `updateContainerField: Multiple container field "${fieldPath}" item count mismatch. Consider re-rendering for add/remove.`
7304
+ `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.`
7271
7305
  );
7272
7306
  }
7273
7307
  } else {
@@ -10836,6 +10870,8 @@ var defaultConfig = {
10836
10870
  en: {
10837
10871
  // UI texts
10838
10872
  removeElement: "Remove",
10873
+ removeRow: "Remove row",
10874
+ removeRowFrom: "Remove row from {label}",
10839
10875
  clickDragText: "Click or drag file",
10840
10876
  clickDragTextMultiple: "Click or drag files",
10841
10877
  noFileSelected: "No file selected",
@@ -10909,6 +10945,8 @@ var defaultConfig = {
10909
10945
  ru: {
10910
10946
  // UI texts
10911
10947
  removeElement: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C",
10948
+ removeRow: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443",
10949
+ removeRowFrom: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443 \u0438\u0437 \xAB{label}\xBB",
10912
10950
  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",
10913
10951
  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",
10914
10952
  noFileSelected: "\u0424\u0430\u0439\u043B \u043D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D",
@@ -11298,6 +11336,18 @@ var exampleThemes = {
11298
11336
  };
11299
11337
 
11300
11338
  // src/instance/FormBuilderInstance.ts
11339
+ function findOwnField(scope, lookupKey, ownBoundary) {
11340
+ const matches = scope.querySelectorAll(
11341
+ `[data-field-key="${lookupKey}"]`
11342
+ );
11343
+ for (const el of Array.from(matches)) {
11344
+ const boundary = el.closest(
11345
+ "[data-container-item], [data-container]"
11346
+ );
11347
+ if (boundary === ownBoundary) return el;
11348
+ }
11349
+ return null;
11350
+ }
11301
11351
  var FormBuilderInstance = class {
11302
11352
  constructor(config) {
11303
11353
  this.instanceId = generateInstanceId();
@@ -12024,29 +12074,29 @@ var FormBuilderInstance = class {
12024
12074
  const formRoot = this.state.formRoot;
12025
12075
  const lookupKey = getElementLookupKey(element, this.state);
12026
12076
  if (!currentPath) {
12027
- return formRoot.querySelector(`[data-field-key="${lookupKey}"]`);
12077
+ return findOwnField(formRoot, lookupKey, null);
12028
12078
  }
12029
12079
  const pathMatch = currentPath.match(/^(.+)\[(\d+)\]$/);
12030
12080
  if (pathMatch) {
12031
12081
  const containerEl2 = formRoot.querySelector(
12032
12082
  `[data-container-item="${pathMatch[1]}[${pathMatch[2]}]"]`
12033
12083
  );
12034
- return containerEl2 ? containerEl2.querySelector(`[data-field-key="${lookupKey}"]`) : null;
12084
+ return containerEl2 ? findOwnField(containerEl2, lookupKey, containerEl2) : null;
12035
12085
  }
12036
12086
  const containerEl = formRoot.querySelector(
12037
12087
  `[data-container="${currentPath}"]`
12038
12088
  );
12039
- return containerEl ? containerEl.querySelector(`[data-field-key="${lookupKey}"]`) : null;
12089
+ return containerEl ? findOwnField(containerEl, lookupKey, containerEl) : null;
12040
12090
  }
12041
12091
  /**
12042
12092
  * Apply enableIf show/hide logic to a single field wrapper.
12043
12093
  * Extracted to reduce cyclomatic complexity of checkElements.
12044
12094
  */
12045
- applyEnableIfVisibility(element, wrapper, currentPath, fullPath, formData) {
12095
+ applyEnableIfVisibility(element, wrapper, domPath, dataPath, fullDomPath, formData) {
12046
12096
  var _a, _b, _c, _d;
12047
12097
  try {
12048
12098
  const scope = (_a = element.enableIf.scope) != null ? _a : "relative";
12049
- const containerData = scope === "relative" && currentPath ? getValueByPath(formData, currentPath) : void 0;
12099
+ const containerData = scope === "relative" && dataPath ? getValueByPath(formData, dataPath) : void 0;
12050
12100
  const shouldEnable = evaluateEnableCondition(
12051
12101
  element.enableIf,
12052
12102
  formData,
@@ -12054,10 +12104,12 @@ var FormBuilderInstance = class {
12054
12104
  );
12055
12105
  const isCurrentlyDisabled = wrapper.getAttribute("data-conditionally-disabled") === "true";
12056
12106
  if (shouldEnable && isCurrentlyDisabled) {
12057
- const containerPrefill = currentPath ? getValueByPath(formData, currentPath) : formData;
12107
+ const containerPrefill = dataPath ? getValueByPath(formData, dataPath) : formData;
12058
12108
  const prefillContext = containerPrefill && typeof containerPrefill === "object" ? containerPrefill : {};
12059
12109
  const newWrapper = renderElement2(element, {
12060
- path: currentPath,
12110
+ // DOM path: the re-rendered control's `name` must match the row it
12111
+ // lives in, not the row's position in the extracted array.
12112
+ path: domPath,
12061
12113
  prefill: prefillContext,
12062
12114
  formData,
12063
12115
  state: this.state,
@@ -12077,7 +12129,7 @@ var FormBuilderInstance = class {
12077
12129
  }
12078
12130
  } catch (error) {
12079
12131
  console.error(
12080
- `Error re-evaluating enableIf for field "${(_d = element.key) != null ? _d : "<no key>"}" at path "${fullPath}":`,
12132
+ `Error re-evaluating enableIf for field "${(_d = element.key) != null ? _d : "<no key>"}" at path "${fullDomPath}":`,
12081
12133
  error
12082
12134
  );
12083
12135
  }
@@ -12089,45 +12141,47 @@ var FormBuilderInstance = class {
12089
12141
  reevaluateConditionalFields() {
12090
12142
  if (!this.state.schema || !this.state.formRoot) return;
12091
12143
  const formData = this.validateForm(true).data;
12092
- const checkElements = (elements, currentPath) => {
12144
+ const checkElements = (elements, domPath, dataPath) => {
12093
12145
  elements.forEach((element) => {
12094
- var _a, _b;
12095
- const fullPath = currentPath ? `${currentPath}.${(_a = element.key) != null ? _a : ""}` : (_b = element.key) != null ? _b : "";
12146
+ var _a;
12147
+ const key = (_a = element.key) != null ? _a : "";
12148
+ const fullDomPath = domPath ? `${domPath}.${key}` : key;
12149
+ const fullDataPath = dataPath ? `${dataPath}.${key}` : key;
12096
12150
  if (element.enableIf) {
12097
- const fieldWrapper = this.findFieldWrapper(element, currentPath);
12151
+ const fieldWrapper = this.findFieldWrapper(element, domPath);
12098
12152
  if (fieldWrapper) {
12099
12153
  this.applyEnableIfVisibility(
12100
12154
  element,
12101
12155
  fieldWrapper,
12102
- currentPath,
12103
- fullPath,
12156
+ domPath,
12157
+ dataPath,
12158
+ fullDomPath,
12104
12159
  formData
12105
12160
  );
12106
12161
  }
12107
12162
  }
12108
12163
  if ((element.type === "container" || element.type === "group") && "elements" in element && element.elements) {
12109
- const containerData = element.key ? formData == null ? void 0 : formData[element.key] : void 0;
12164
+ const containerData = element.key ? getValueByPath(formData, fullDataPath) : void 0;
12110
12165
  if (Array.isArray(containerData)) {
12111
- const containerItems = this.state.formRoot.querySelectorAll(
12112
- `[data-container-item]`
12166
+ const directItems = findDirectContainerRows(
12167
+ this.state.formRoot,
12168
+ fullDomPath
12113
12169
  );
12114
- const directItems = fullPath ? Array.from(containerItems).filter((el) => {
12115
- const attr = el.getAttribute("data-container-item") || "";
12116
- if (!attr.startsWith(`${fullPath}[`)) return false;
12117
- const suffix = attr.slice(fullPath.length);
12118
- return /^\[\d+\]$/.test(suffix);
12119
- }) : [];
12120
- directItems.forEach((el) => {
12121
- const attr = el.getAttribute("data-container-item") || "";
12122
- checkElements(element.elements, attr);
12170
+ directItems.forEach((el, rowIndex) => {
12171
+ const marker = el.getAttribute("data-container-item") || "";
12172
+ checkElements(
12173
+ element.elements,
12174
+ marker,
12175
+ `${fullDataPath}[${rowIndex}]`
12176
+ );
12123
12177
  });
12124
12178
  } else {
12125
- checkElements(element.elements, fullPath);
12179
+ checkElements(element.elements, fullDomPath, fullDataPath);
12126
12180
  }
12127
12181
  }
12128
12182
  });
12129
12183
  };
12130
- checkElements(this.state.schema.elements, "");
12184
+ checkElements(this.state.schema.elements, "", "");
12131
12185
  }
12132
12186
  /**
12133
12187
  * Destroy instance and clean up resources