@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/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("aria-label", t("removeElement", state));
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.appendChild(rem);
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
- const countItems = () => itemsWrap.querySelectorAll(":scope > .containerItem").length;
6798
- const handleAddItem = () => {
6799
- if (countItems() >= max) return;
6800
- const idx = countItems();
6801
- const currentFormData = state.formRoot ? extractRootFormData(state.formRoot) : {};
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: ctx.state,
6900
+ state,
6804
6901
  instance: ctx.instance,
6805
6902
  path: pathJoin(ctx.path, `${element.key}[${idx}]`),
6806
- prefill: childDefaults,
6807
- // Defaults for enableIf evaluation
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", `${element.key}[${idx}]`);
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(item, () => handleRemoveItem(item), state);
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 = itemsWrap.querySelector(
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
- const handleRemoveItem = (item) => {
6864
- item.remove();
6865
- updateAddButton();
6866
- };
6867
- if (pre && Array.isArray(pre)) {
6868
- pre.forEach((prefillObj, idx) => {
6869
- const mergedPrefill = mergeWithDefaults(prefillObj || {}, childDefaults);
6870
- const subCtx = {
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
- const idx = countItems();
6910
- const subCtx = {
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 allContainerWrappers = scopeRoot.querySelectorAll(
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
- if (isPlainObject(itemValue)) {
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 = `${fieldPath}[${index}].${childElement.key}`;
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
- const existingContainers = scopeRoot.querySelectorAll(
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}" item count mismatch. Consider re-rendering for add/remove.`
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.querySelector(`[data-field-key="${lookupKey}"]`);
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.querySelector(`[data-field-key="${lookupKey}"]`) : null;
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.querySelector(`[data-field-key="${lookupKey}"]`) : null;
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, currentPath, fullPath, formData) {
11899
+ applyEnableIfVisibility(element, wrapper, domPath, dataPath, fullDomPath, formData) {
11850
11900
  try {
11851
11901
  const scope = element.enableIf.scope ?? "relative";
11852
- const containerData = scope === "relative" && currentPath ? getValueByPath(formData, currentPath) : void 0;
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 = currentPath ? getValueByPath(formData, currentPath) : formData;
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: currentPath,
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 "${fullPath}":`,
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, currentPath) => {
11947
+ const checkElements = (elements, domPath, dataPath) => {
11896
11948
  elements.forEach((element) => {
11897
- const fullPath = currentPath ? `${currentPath}.${element.key ?? ""}` : element.key ?? "";
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, currentPath);
11953
+ const fieldWrapper = this.findFieldWrapper(element, domPath);
11900
11954
  if (fieldWrapper) {
11901
11955
  this.applyEnableIfVisibility(
11902
11956
  element,
11903
11957
  fieldWrapper,
11904
- currentPath,
11905
- fullPath,
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?.[element.key] : void 0;
11966
+ const containerData = element.key ? getValueByPath(formData, fullDataPath) : void 0;
11912
11967
  if (Array.isArray(containerData)) {
11913
- const containerItems = this.state.formRoot.querySelectorAll(
11914
- `[data-container-item]`
11968
+ const directItems = findDirectContainerRows(
11969
+ this.state.formRoot,
11970
+ fullDomPath
11915
11971
  );
11916
- const directItems = fullPath ? Array.from(containerItems).filter((el) => {
11917
- const attr = el.getAttribute("data-container-item") || "";
11918
- if (!attr.startsWith(`${fullPath}[`)) return false;
11919
- const suffix = attr.slice(fullPath.length);
11920
- return /^\[\d+\]$/.test(suffix);
11921
- }) : [];
11922
- directItems.forEach((el) => {
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, fullPath);
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