@dmitryvim/form-builder 0.5.1 → 0.5.2

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
@@ -300,6 +300,15 @@ function getElementLookupKey(element, state) {
300
300
  function pathJoin(base, key) {
301
301
  return base ? `${base}.${key}` : key;
302
302
  }
303
+ function findDirectContainerRows(scopeRoot, containerPath) {
304
+ if (!containerPath) return [];
305
+ const all = scopeRoot.querySelectorAll("[data-container-item]");
306
+ return Array.from(all).filter((el) => {
307
+ const attr = el.getAttribute("data-container-item") || "";
308
+ if (!attr.startsWith(`${containerPath}[`)) return false;
309
+ return /^\[\d+\]$/.test(attr.slice(containerPath.length));
310
+ });
311
+ }
303
312
  function clear(node) {
304
313
  while (node.firstChild) node.removeChild(node.firstChild);
305
314
  }
@@ -449,6 +458,19 @@ function ensureThemingHooks(doc) {
449
458
  color: var(--fb-error-color);
450
459
  background-color: var(--fb-background-hover-color);
451
460
  }
461
+ .fb-item-remove:disabled {
462
+ opacity: 0.35;
463
+ cursor: not-allowed;
464
+ }
465
+ /* Cards carrying a row-level remove button reserve a right-hand lane for it
466
+ (24px button + inset + gap) so it never sits on top of the first field's
467
+ control. A class rather than an inline style, so host CSS can override the
468
+ gutter without !important; the compound selector keeps it ahead of the
469
+ card's own p-2 utility regardless of stylesheet order. Its own token, not
470
+ --fb-slide-card-padding, which is slides-only and always resolves. */
471
+ .containerItem.fb-row-removable {
472
+ padding-right: var(--fb-row-remove-gutter, 40px);
473
+ }
452
474
  /* Prefill-suggestion pills rendered by createPrefillHints. Outline pill at rest,
453
475
  soft-fill on hover, solid-fill when selected. All colors flow from the active
454
476
  theme \u2014 consumers don't need to ship their own CSS. */
@@ -4856,12 +4878,21 @@ function buildClearAllRow(state, ridCount, onClearAll) {
4856
4878
  return row;
4857
4879
  }
4858
4880
  var gridResizeObservers = /* @__PURE__ */ new WeakMap();
4881
+ var gridMeasureFrames = /* @__PURE__ */ new WeakMap();
4882
+ function cancelPendingMeasure(container) {
4883
+ const frame = gridMeasureFrames.get(container);
4884
+ if (frame !== void 0) {
4885
+ cancelAnimationFrame(frame);
4886
+ gridMeasureFrames.delete(container);
4887
+ }
4888
+ }
4859
4889
  function disposePlaceholdersForUpload(container) {
4860
4890
  const observer = gridResizeObservers.get(container);
4861
4891
  if (observer) {
4862
4892
  observer.disconnect();
4863
4893
  gridResizeObservers.delete(container);
4864
4894
  }
4895
+ cancelPendingMeasure(container);
4865
4896
  container.querySelectorAll(".fb-multi-placeholder").forEach((p) => p.remove());
4866
4897
  }
4867
4898
  function renderResourcePills(opts) {
@@ -4886,6 +4917,7 @@ function renderResourcePills(opts) {
4886
4917
  previousObserver.disconnect();
4887
4918
  gridResizeObservers.delete(container);
4888
4919
  }
4920
+ cancelPendingMeasure(container);
4889
4921
  while (container.firstChild) container.removeChild(container.firstChild);
4890
4922
  const ridList = rids ?? [];
4891
4923
  const effectiveMax = maxCount ?? Infinity;
@@ -4985,7 +5017,7 @@ function renderResourcePills(opts) {
4985
5017
  if (effectiveMax === Infinity || effectiveMax > occupied) {
4986
5018
  grid.appendChild(buildPlaceholderTile());
4987
5019
  }
4988
- requestAnimationFrame(adjustPlaceholders);
5020
+ gridMeasureFrames.set(container, requestAnimationFrame(adjustPlaceholders));
4989
5021
  if (typeof ResizeObserver !== "undefined") {
4990
5022
  const ro = new ResizeObserver(() => adjustPlaceholders());
4991
5023
  ro.observe(grid);
@@ -6759,17 +6791,13 @@ function mountRemoveButton(item, onRemove, state) {
6759
6791
  `;
6760
6792
  rem.innerHTML = BIN_ICON_SVG;
6761
6793
  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
6794
  rem.style.position = "absolute";
6769
- rem.style.top = "8px";
6770
- rem.style.right = "8px";
6795
+ rem.style.top = "var(--fb-row-remove-inset, 8px)";
6796
+ rem.style.right = "var(--fb-row-remove-inset, 8px)";
6797
+ rem.style.zIndex = "1";
6771
6798
  item.style.position = "relative";
6772
- item.appendChild(rem);
6799
+ item.classList.add("fb-row-removable");
6800
+ item.insertBefore(rem, item.firstChild);
6773
6801
  }
6774
6802
  function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6775
6803
  const state = ctx.state;
@@ -6794,24 +6822,29 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6794
6822
  const max = element.maxCount ?? Infinity;
6795
6823
  const pre = Array.isArray(ctx.prefill?.[element.key]) ? ctx.prefill[element.key] : null;
6796
6824
  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) : {};
6825
+ let nextRowIndex = 0;
6826
+ const takeRowIndex = () => nextRowIndex++;
6827
+ const directRows = () => Array.from(itemsWrap.children).filter(
6828
+ (el) => el.classList.contains("containerItem")
6829
+ );
6830
+ const countItems = () => directRows().length;
6831
+ const handleRemoveItem = (item) => {
6832
+ if (countItems() <= min) return;
6833
+ item.remove();
6834
+ updateAddButton();
6835
+ };
6836
+ const createContainerItem = (idx, rowPrefill, formData) => {
6802
6837
  const subCtx = {
6803
- state: ctx.state,
6838
+ state,
6804
6839
  instance: ctx.instance,
6805
6840
  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
6841
+ prefill: mergeWithDefaults(rowPrefill ?? {}, childDefaults),
6842
+ formData,
6810
6843
  inheritedReadonly: childInheritedReadonly
6811
6844
  };
6812
6845
  const item = document.createElement("div");
6813
6846
  item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
6814
- item.setAttribute("data-container-item", `${element.key}[${idx}]`);
6847
+ item.setAttribute("data-container-item", subCtx.path);
6815
6848
  if (isSlides) {
6816
6849
  item.setAttribute("data-fb-slide-card", "");
6817
6850
  }
@@ -6821,11 +6854,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6821
6854
  );
6822
6855
  element.elements.forEach((child) => {
6823
6856
  if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
6857
+ const hiddenValue = rowPrefill?.[child.key] ?? ("default" in child ? child.default : null) ?? null;
6824
6858
  childWrapper.appendChild(
6825
- createHiddenInput(
6826
- pathJoin(subCtx.path, child.key),
6827
- ("default" in child ? child.default : null) ?? null
6828
- )
6859
+ createHiddenInput(pathJoin(subCtx.path, child.key), hiddenValue)
6829
6860
  );
6830
6861
  } else {
6831
6862
  childWrapper.appendChild(renderElement(child, subCtx));
@@ -6835,6 +6866,17 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6835
6866
  if (!containerIsReadonly) {
6836
6867
  mountRemoveButton(item, () => handleRemoveItem(item), state);
6837
6868
  }
6869
+ return item;
6870
+ };
6871
+ const handleAddItem = () => {
6872
+ if (countItems() >= max) return;
6873
+ const item = createContainerItem(
6874
+ takeRowIndex(),
6875
+ null,
6876
+ // Current root data read back from the DOM, so enableIf sees what the
6877
+ // user has actually typed rather than the original prefill.
6878
+ state.formRoot ? extractRootFormData(state.formRoot) : {}
6879
+ );
6838
6880
  if (slideAddTile && slideAddTile.parentElement === itemsWrap) {
6839
6881
  itemsWrap.insertBefore(item, slideAddTile);
6840
6882
  } else {
@@ -6847,109 +6889,43 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6847
6889
  let pillAddUpdate = null;
6848
6890
  const syncSlideTileSize = () => {
6849
6891
  if (!slideAddTile) return;
6850
- const firstSlide = itemsWrap.querySelector(
6851
- ":scope > .containerItem"
6852
- );
6892
+ const firstSlide = directRows()[0];
6853
6893
  if (firstSlide && firstSlide.offsetHeight > 0) {
6854
6894
  slideAddTile.style.minHeight = `${firstSlide.offsetHeight}px`;
6855
6895
  }
6856
6896
  };
6897
+ const syncRemoveButtons = () => {
6898
+ const atFloor = countItems() <= min;
6899
+ directRows().forEach((row) => {
6900
+ const btn = Array.from(row.children).find(
6901
+ (el) => el.classList.contains("fb-item-remove")
6902
+ );
6903
+ if (btn) btn.disabled = atFloor;
6904
+ });
6905
+ };
6857
6906
  const updateAddButton = () => {
6858
6907
  const currentCount = countItems();
6859
6908
  if (slideAddUpdate) slideAddUpdate(currentCount, max);
6860
6909
  if (pillAddUpdate) pillAddUpdate(currentCount, max);
6861
6910
  if (slideAddTile) syncSlideTileSize();
6911
+ syncRemoveButtons();
6862
6912
  };
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
6913
+ if (pre) {
6914
+ pre.forEach((prefillObj) => {
6915
+ itemsWrap.appendChild(
6916
+ createContainerItem(
6917
+ takeRowIndex(),
6918
+ prefillObj ?? {},
6919
+ ctx.formData ?? ctx.prefill
6920
+ )
6889
6921
  );
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
6922
  });
6906
6923
  }
6907
6924
  if (!containerIsReadonly) {
6908
6925
  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
6926
+ itemsWrap.appendChild(
6927
+ createContainerItem(takeRowIndex(), null, ctx.formData ?? ctx.prefill)
6951
6928
  );
6952
- itemsWrap.appendChild(item);
6953
6929
  }
6954
6930
  }
6955
6931
  containerWrap.appendChild(itemsWrap);
@@ -7016,15 +6992,7 @@ function validateContainerElement(element, key, context) {
7016
6992
  };
7017
6993
  if ("multiple" in element && element.multiple) {
7018
6994
  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
- });
6995
+ const containerWrappers = findDirectContainerRows(scopeRoot, key);
7028
6996
  containerWrappers.forEach((itemContainer) => {
7029
6997
  const itemData = {};
7030
6998
  const containerAttr = itemContainer.getAttribute("data-container-item") || "";
@@ -7147,9 +7115,7 @@ function updateContainerField(element, fieldPath, value, context) {
7147
7115
  });
7148
7116
  }
7149
7117
  });
7150
- const existingContainers = scopeRoot.querySelectorAll(
7151
- `[data-container-item^="${fieldPath}["]`
7152
- );
7118
+ const existingContainers = findDirectContainerRows(scopeRoot, fieldPath);
7153
7119
  if (value.length !== existingContainers.length) {
7154
7120
  console.warn(
7155
7121
  `updateContainerField: Multiple container field "${fieldPath}" item count mismatch. Consider re-rendering for add/remove.`
@@ -11908,17 +11874,12 @@ var FormBuilderInstance = class {
11908
11874
  }
11909
11875
  }
11910
11876
  if ((element.type === "container" || element.type === "group") && "elements" in element && element.elements) {
11911
- const containerData = element.key ? formData?.[element.key] : void 0;
11877
+ const containerData = element.key ? getValueByPath(formData, fullPath) : void 0;
11912
11878
  if (Array.isArray(containerData)) {
11913
- const containerItems = this.state.formRoot.querySelectorAll(
11914
- `[data-container-item]`
11879
+ const directItems = findDirectContainerRows(
11880
+ this.state.formRoot,
11881
+ fullPath
11915
11882
  );
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
11883
  directItems.forEach((el) => {
11923
11884
  const attr = el.getAttribute("data-container-item") || "";
11924
11885
  checkElements(element.elements, attr);