@dmitryvim/form-builder 0.2.28 → 0.2.30

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
@@ -153,12 +153,14 @@ function validateSchema(schema) {
153
153
  allOutputKeys.add(textKey);
154
154
  allOutputKeys.add(filesKey);
155
155
  } else {
156
- if (allOutputKeys.has(el.key)) {
157
- errors.push(
158
- `${scopePath}: Element key "${el.key}" collides with a flatOutput richinput key`
159
- );
156
+ if (el.key) {
157
+ if (allOutputKeys.has(el.key)) {
158
+ errors.push(
159
+ `${scopePath}: Element key "${el.key}" collides with a flatOutput richinput key`
160
+ );
161
+ }
162
+ allOutputKeys.add(el.key);
160
163
  }
161
- allOutputKeys.add(el.key);
162
164
  }
163
165
  }
164
166
  }
@@ -168,9 +170,17 @@ function validateSchema(schema) {
168
170
  if (!element.type) {
169
171
  errors.push(`${elementPath}: missing type`);
170
172
  }
171
- if (!element.key) {
173
+ if (!element.key && element.type !== "markdown") {
172
174
  errors.push(`${elementPath}: missing key`);
173
175
  }
176
+ if (element.type === "markdown") {
177
+ const content = element.content;
178
+ if (typeof content !== "string") {
179
+ errors.push(
180
+ `${elementPath}: markdown element requires "content" to be a string (got ${content === null ? "null" : typeof content})`
181
+ );
182
+ }
183
+ }
174
184
  if (element.enableIf) {
175
185
  const enableIf = element.enableIf;
176
186
  if (!enableIf.key || typeof enableIf.key !== "string") {
@@ -263,6 +273,18 @@ function escapeHtml(text) {
263
273
  div.textContent = text;
264
274
  return div.innerHTML;
265
275
  }
276
+ function getElementLookupKey(element, state) {
277
+ if (element.key) {
278
+ return element.key;
279
+ }
280
+ const cached = state.syntheticElementIds.get(element);
281
+ if (cached !== void 0) {
282
+ return cached;
283
+ }
284
+ const id = `fb-synthetic-${state.syntheticElementIdCounter++}`;
285
+ state.syntheticElementIds.set(element, id);
286
+ return id;
287
+ }
266
288
  function pathJoin(base, key) {
267
289
  return base ? `${base}.${key}` : key;
268
290
  }
@@ -2628,22 +2650,34 @@ function setEmptyFileContainer(fileContainer, state, hint) {
2628
2650
  </div>
2629
2651
  `;
2630
2652
  }
2653
+ var dragDropHandlers = /* @__PURE__ */ new WeakMap();
2631
2654
  function setupDragAndDrop(element, dropHandler) {
2632
- element.addEventListener("dragover", (e) => {
2655
+ const prev = dragDropHandlers.get(element);
2656
+ if (prev) {
2657
+ element.removeEventListener("dragover", prev.dragover);
2658
+ element.removeEventListener("dragleave", prev.dragleave);
2659
+ element.removeEventListener("drop", prev.drop);
2660
+ }
2661
+ const dragover = (e) => {
2633
2662
  e.preventDefault();
2634
2663
  element.classList.add("border-blue-500", "bg-blue-50");
2635
- });
2636
- element.addEventListener("dragleave", (e) => {
2664
+ };
2665
+ const dragleave = (e) => {
2637
2666
  e.preventDefault();
2638
2667
  element.classList.remove("border-blue-500", "bg-blue-50");
2639
- });
2640
- element.addEventListener("drop", (e) => {
2668
+ };
2669
+ const drop = (e) => {
2641
2670
  e.preventDefault();
2642
2671
  element.classList.remove("border-blue-500", "bg-blue-50");
2643
- if (e.dataTransfer?.files) {
2644
- dropHandler(e.dataTransfer.files);
2672
+ const files = e.dataTransfer?.files;
2673
+ if (files) {
2674
+ dropHandler(files);
2645
2675
  }
2646
- });
2676
+ };
2677
+ element.addEventListener("dragover", dragover);
2678
+ element.addEventListener("dragleave", dragleave);
2679
+ element.addEventListener("drop", drop);
2680
+ dragDropHandlers.set(element, { dragover, dragleave, drop });
2647
2681
  }
2648
2682
 
2649
2683
  // src/components/file/preview.ts
@@ -3898,7 +3932,7 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3898
3932
  releaseLocalFileUrl(state.resourceIndex.get(currentRid)?.file);
3899
3933
  }
3900
3934
  if (hiddenInput) hiddenInput.value = "";
3901
- handlers.restoreDropzone();
3935
+ renderEmptySingleState();
3902
3936
  }
3903
3937
  };
3904
3938
  const buildDeps = () => ({
@@ -3913,6 +3947,9 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3913
3947
  fileContainer.className = "file-preview-container";
3914
3948
  fileContainer.removeAttribute("style");
3915
3949
  fileContainer.onclick = null;
3950
+ while (fileContainer.firstChild) {
3951
+ fileContainer.removeChild(fileContainer.firstChild);
3952
+ }
3916
3953
  const row = document.createElement("div");
3917
3954
  row.className = "fb-file-card-row";
3918
3955
  row.style.cssText = "display:flex;gap:8px;align-items:stretch;";
@@ -5439,7 +5476,7 @@ function updateSliderField(element, fieldPath, value, context) {
5439
5476
  function extractChildDefaults(elements) {
5440
5477
  const defaults = {};
5441
5478
  for (const child of elements) {
5442
- if ("default" in child && child.default !== void 0) {
5479
+ if (child.key && "default" in child && child.default !== void 0) {
5443
5480
  defaults[child.key] = child.default;
5444
5481
  }
5445
5482
  }
@@ -5534,8 +5571,8 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
5534
5571
  inheritedReadonly: containerIsReadonly || ctx.inheritedReadonly
5535
5572
  };
5536
5573
  element.elements.forEach((child) => {
5537
- if (child.hidden || child.type === "hidden") {
5538
- const prefillVal = containerPrefill[child.key] ?? child.default ?? null;
5574
+ if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
5575
+ const prefillVal = containerPrefill[child.key] ?? ("default" in child ? child.default : null) ?? null;
5539
5576
  itemsWrap.appendChild(
5540
5577
  createHiddenInput(pathJoin(subCtx.path, child.key), prefillVal)
5541
5578
  );
@@ -5609,11 +5646,11 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
5609
5646
  childWrapper.className = `grid grid-cols-${columns} gap-4`;
5610
5647
  }
5611
5648
  element.elements.forEach((child) => {
5612
- if (child.hidden || child.type === "hidden") {
5649
+ if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
5613
5650
  childWrapper.appendChild(
5614
5651
  createHiddenInput(
5615
5652
  pathJoin(subCtx.path, child.key),
5616
- child.default ?? null
5653
+ ("default" in child ? child.default : null) ?? null
5617
5654
  )
5618
5655
  );
5619
5656
  } else {
@@ -5686,8 +5723,8 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
5686
5723
  childWrapper.className = `grid grid-cols-${columns} gap-4`;
5687
5724
  }
5688
5725
  element.elements.forEach((child) => {
5689
- if (child.hidden || child.type === "hidden") {
5690
- const prefillVal = prefillObj?.[child.key] ?? child.default ?? null;
5726
+ if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
5727
+ const prefillVal = prefillObj?.[child.key] ?? ("default" in child ? child.default : null) ?? null;
5691
5728
  childWrapper.appendChild(
5692
5729
  createHiddenInput(pathJoin(subCtx.path, child.key), prefillVal)
5693
5730
  );
@@ -5742,11 +5779,11 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
5742
5779
  childWrapper.className = `grid grid-cols-${columns} gap-4`;
5743
5780
  }
5744
5781
  element.elements.forEach((child) => {
5745
- if (child.hidden || child.type === "hidden") {
5782
+ if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
5746
5783
  childWrapper.appendChild(
5747
5784
  createHiddenInput(
5748
5785
  pathJoin(subCtx.path, child.key),
5749
- child.default ?? null
5786
+ ("default" in child ? child.default : null) ?? null
5750
5787
  )
5751
5788
  );
5752
5789
  } else {
@@ -5932,6 +5969,7 @@ function updateContainerField(element, fieldPath, value, context) {
5932
5969
  value.forEach((itemValue, index) => {
5933
5970
  if (isPlainObject(itemValue)) {
5934
5971
  element.elements.forEach((childElement) => {
5972
+ if (childElement.type === "markdown" || !childElement.key) return;
5935
5973
  const childPath = `${fieldPath}[${index}].${childElement.key}`;
5936
5974
  if (childElement.type === "richinput" && childElement.flatOutput) {
5937
5975
  const richChild = childElement;
@@ -5971,6 +6009,7 @@ function updateContainerField(element, fieldPath, value, context) {
5971
6009
  return;
5972
6010
  }
5973
6011
  element.elements.forEach((childElement) => {
6012
+ if (childElement.type === "markdown" || !childElement.key) return;
5974
6013
  const childPath = `${fieldPath}.${childElement.key}`;
5975
6014
  if (childElement.type === "richinput" && childElement.flatOutput) {
5976
6015
  const richChild = childElement;
@@ -8670,6 +8709,248 @@ function updateRichInputField(element, fieldPath, value, context) {
8670
8709
  }
8671
8710
  }
8672
8711
 
8712
+ // src/components/markdown/snarkdown.ts
8713
+ var TAGS = {
8714
+ "": ["<em>", "</em>"],
8715
+ _: ["<strong>", "</strong>"],
8716
+ "*": ["<strong>", "</strong>"],
8717
+ "~": ["<s>", "</s>"],
8718
+ "\n": ["<br />"],
8719
+ " ": ["<br />"],
8720
+ "-": ["<hr />"]
8721
+ };
8722
+ function outdent(str) {
8723
+ return str.replace(
8724
+ RegExp("^" + (str.match(/^(\t| )+/) || "")[0], "gm"),
8725
+ ""
8726
+ );
8727
+ }
8728
+ function encodeAttr(str) {
8729
+ return (str + "").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
8730
+ }
8731
+ function parse(md, prevLinks) {
8732
+ const tokenizer = /((?:^|\n+)(?:\n---+|\*[ ]\*(?:[ ]\*)+)\n)|(?:^```[ ]*(\w*)\n([\s\S]*?)\n```$)|((?:(?:^|\n+)(?:\t|[ ]{2,}).+)+\n*)|((?:(?:^|\n)([>*+-]|\d+\.)\s+.*)+)|(?:!\[([^\]]*?)\]\(([^)]+?)\))|(\[)|(\](?:\(([^)]+?)\))?)|(?:(?:^|\n+)([^\s].*)\n(-{3,}|={3,})(?:\n+|$))|(?:(?:^|\n+)(#{1,6})\s*(.+)(?:\n+|$))|(?:`([^`].*?)`)|([ ]{2}\n\n*|\n{2,}|__|\*\*|[_*]|~~)/gm;
8733
+ const context = [];
8734
+ let out = "";
8735
+ const links = prevLinks || {};
8736
+ let last = 0;
8737
+ let chunk;
8738
+ let prev;
8739
+ let token;
8740
+ let inner;
8741
+ let t2;
8742
+ function tag(token2) {
8743
+ const desc = TAGS[token2[1] || ""];
8744
+ const end = context[context.length - 1] === token2;
8745
+ if (!desc) return token2;
8746
+ if (!desc[1]) return desc[0];
8747
+ if (end) context.pop();
8748
+ else context.push(token2);
8749
+ return desc[end ? 1 : 0];
8750
+ }
8751
+ function flush() {
8752
+ let str = "";
8753
+ while (context.length) str += tag(context[context.length - 1]);
8754
+ return str;
8755
+ }
8756
+ md = md.replace(/^\[(.+?)\]:\s*(.+)$/gm, (_s, name, url) => {
8757
+ links[name.toLowerCase()] = url;
8758
+ return "";
8759
+ }).replace(/^\n+|\n+$/g, "");
8760
+ while (token = tokenizer.exec(md)) {
8761
+ prev = md.substring(last, token.index);
8762
+ last = tokenizer.lastIndex;
8763
+ chunk = token[0];
8764
+ if (prev.match(/[^\\](\\\\)*\\$/)) ; else if (t2 = token[3] || token[4]) {
8765
+ chunk = '<pre class="code ' + (token[4] ? "poetry" : token[2].toLowerCase()) + '"><code' + (token[2] ? ` class="language-${token[2].toLowerCase()}"` : "") + ">" + outdent(encodeAttr(t2).replace(/^\n+|\n+$/g, "")) + "</code></pre>";
8766
+ } else if (t2 = token[6]) {
8767
+ if (t2.match(/\./)) {
8768
+ token[5] = token[5].replace(/^\d+/gm, "");
8769
+ }
8770
+ inner = parse(outdent(token[5].replace(/^\s*[>*+.-]/gm, "")));
8771
+ if (t2 === ">") t2 = "blockquote";
8772
+ else {
8773
+ t2 = t2.match(/\./) ? "ol" : "ul";
8774
+ inner = inner.replace(/^(.*)(\n|$)/gm, "<li>$1</li>");
8775
+ }
8776
+ chunk = "<" + t2 + ">" + inner + "</" + t2 + ">";
8777
+ } else if (token[8]) {
8778
+ chunk = `<img src="${encodeAttr(token[8])}" alt="${encodeAttr(token[7])}">`;
8779
+ } else if (token[10]) {
8780
+ out = out.replace(
8781
+ "<a>",
8782
+ `<a href="${encodeAttr(token[11] || links[prev.toLowerCase()])}">`
8783
+ );
8784
+ chunk = flush() + "</a>";
8785
+ } else if (token[9]) {
8786
+ chunk = "<a>";
8787
+ } else if (token[12] || token[14]) {
8788
+ t2 = "h" + (token[14] ? token[14].length : token[13] > "=" ? 1 : 2);
8789
+ chunk = "<" + t2 + ">" + parse(token[12] || token[15], links) + "</" + t2 + ">";
8790
+ } else if (token[16]) {
8791
+ chunk = "<code>" + encodeAttr(token[16]) + "</code>";
8792
+ } else if (token[17] || token[1]) {
8793
+ chunk = tag(token[17] || "--");
8794
+ }
8795
+ out += prev;
8796
+ out += chunk;
8797
+ }
8798
+ return (out + md.substring(last) + flush()).replace(/^\n+|\n+$/g, "");
8799
+ }
8800
+
8801
+ // src/components/markdown/render.ts
8802
+ var STYLE_ID2 = "fb-markdown-styles";
8803
+ function ensureMarkdownStyles() {
8804
+ if (typeof document === "undefined") return;
8805
+ if (document.getElementById(STYLE_ID2)) return;
8806
+ const style = document.createElement("style");
8807
+ style.id = STYLE_ID2;
8808
+ style.setAttribute("data-fb-markdown-styles", "true");
8809
+ style.textContent = `
8810
+ .fb-markdown {
8811
+ font-family: var(--fb-font-family, inherit);
8812
+ font-size: var(--fb-font-size, 1rem);
8813
+ color: var(--fb-text-color, #1f2937);
8814
+ line-height: 1.6;
8815
+ }
8816
+ .fb-markdown h1,
8817
+ .fb-markdown h2,
8818
+ .fb-markdown h3,
8819
+ .fb-markdown h4,
8820
+ .fb-markdown h5,
8821
+ .fb-markdown h6 {
8822
+ font-weight: var(--fb-font-weight-medium, 600);
8823
+ color: var(--fb-text-color, #1f2937);
8824
+ margin-top: 0.75em;
8825
+ margin-bottom: 0.25em;
8826
+ }
8827
+ .fb-markdown h1 { font-size: 1.5rem; }
8828
+ .fb-markdown h2 { font-size: 1.25rem; }
8829
+ .fb-markdown h3 { font-size: 1.1rem; }
8830
+ .fb-markdown h4,
8831
+ .fb-markdown h5,
8832
+ .fb-markdown h6 { font-size: 1rem; }
8833
+ .fb-markdown p {
8834
+ margin-top: 0;
8835
+ margin-bottom: 0.5em;
8836
+ }
8837
+ .fb-markdown ul,
8838
+ .fb-markdown ol {
8839
+ margin: 0.25em 0 0.5em 1.5em;
8840
+ padding: 0;
8841
+ }
8842
+ .fb-markdown li {
8843
+ margin-bottom: 0.15em;
8844
+ }
8845
+ .fb-markdown a {
8846
+ color: var(--fb-primary-color, #2563eb);
8847
+ text-decoration: underline;
8848
+ }
8849
+ .fb-markdown a:hover {
8850
+ opacity: 0.8;
8851
+ }
8852
+ .fb-markdown code {
8853
+ background: var(--fb-input-background-color, #f3f4f6);
8854
+ border-radius: 3px;
8855
+ padding: 0.1em 0.35em;
8856
+ font-size: 0.9em;
8857
+ font-family: monospace;
8858
+ }
8859
+ .fb-markdown pre {
8860
+ background: var(--fb-input-background-color, #f3f4f6);
8861
+ border-radius: var(--fb-border-radius, 0.375rem);
8862
+ padding: 0.75em 1em;
8863
+ overflow-x: auto;
8864
+ margin: 0.5em 0;
8865
+ }
8866
+ .fb-markdown pre code {
8867
+ background: none;
8868
+ padding: 0;
8869
+ border-radius: 0;
8870
+ font-size: inherit;
8871
+ }
8872
+ .fb-markdown blockquote {
8873
+ border-left: 3px solid var(--fb-border-color, #d1d5db);
8874
+ margin: 0.5em 0;
8875
+ padding-left: 1em;
8876
+ color: var(--fb-text-secondary-color, #6b7280);
8877
+ }
8878
+ .fb-markdown hr {
8879
+ border: none;
8880
+ border-top: 1px solid var(--fb-border-color, #d1d5db);
8881
+ margin: 0.75em 0;
8882
+ }
8883
+ .fb-markdown strong { font-weight: var(--fb-font-weight-medium, 600); }
8884
+ .fb-markdown em { font-style: italic; }
8885
+ .fb-markdown s { text-decoration: line-through; }
8886
+ `;
8887
+ document.head.appendChild(style);
8888
+ }
8889
+ var ANCHOR_DANGEROUS_SCHEMES = [
8890
+ "javascript:",
8891
+ "data:",
8892
+ "vbscript:",
8893
+ "blob:"
8894
+ ];
8895
+ var IMG_DANGEROUS_SCHEMES = ["javascript:", "vbscript:", "blob:"];
8896
+ function isImgSrcDangerous(normalized) {
8897
+ if (IMG_DANGEROUS_SCHEMES.some((scheme) => normalized.startsWith(scheme))) {
8898
+ return true;
8899
+ }
8900
+ if (normalized.startsWith("data:") && !normalized.startsWith("data:image/")) {
8901
+ return true;
8902
+ }
8903
+ return false;
8904
+ }
8905
+ function escapeRawHtml(content) {
8906
+ return content.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
8907
+ }
8908
+ function sanitizeElements(container) {
8909
+ const anchors = container.querySelectorAll("a[href]");
8910
+ anchors.forEach((a) => {
8911
+ const href = a.getAttribute("href") ?? "";
8912
+ const normalized = href.trim().toLowerCase();
8913
+ const isDangerous = ANCHOR_DANGEROUS_SCHEMES.some(
8914
+ (scheme) => normalized.startsWith(scheme)
8915
+ );
8916
+ if (isDangerous) {
8917
+ a.setAttribute("href", "#");
8918
+ }
8919
+ a.setAttribute("target", "_blank");
8920
+ a.setAttribute("rel", "noopener noreferrer");
8921
+ });
8922
+ const images = container.querySelectorAll("img[src]");
8923
+ images.forEach((img) => {
8924
+ const src = img.getAttribute("src") ?? "";
8925
+ const normalized = src.trim().toLowerCase();
8926
+ if (isImgSrcDangerous(normalized)) {
8927
+ img.setAttribute("src", "");
8928
+ }
8929
+ });
8930
+ }
8931
+ function renderMarkdown(element, _ctx, parent) {
8932
+ if (typeof element.content !== "string") {
8933
+ throw new Error(
8934
+ `renderMarkdown: markdown element${element.key ? ` "${element.key}"` : ""} requires "content" to be a string (got ${element.content === null ? "null" : typeof element.content})`
8935
+ );
8936
+ }
8937
+ ensureMarkdownStyles();
8938
+ const wrapper = document.createElement("div");
8939
+ wrapper.className = "fb-markdown";
8940
+ const escaped = escapeRawHtml(element.content);
8941
+ wrapper.innerHTML = parse(escaped);
8942
+ sanitizeElements(wrapper);
8943
+ parent.appendChild(wrapper);
8944
+ return wrapper;
8945
+ }
8946
+
8947
+ // src/components/markdown/index.ts
8948
+ function validateMarkdown(_element, _key, _context) {
8949
+ return { value: void 0, errors: [], skip: true };
8950
+ }
8951
+ function updateMarkdown(_element, _fieldPath, _value, _context) {
8952
+ }
8953
+
8673
8954
  // src/components/index.ts
8674
8955
  function showTooltip(tooltipId, button) {
8675
8956
  const tooltip = document.getElementById(tooltipId);
@@ -8900,7 +9181,7 @@ function setupEnableIfListeners(wrapper, element, ctx) {
8900
9181
  function createFieldLabel(element) {
8901
9182
  const title = document.createElement("label");
8902
9183
  title.className = "text-sm font-medium text-gray-900";
8903
- title.textContent = element.label || element.key;
9184
+ title.textContent = element.label ?? element.key ?? "";
8904
9185
  if (element.required) {
8905
9186
  const req = document.createElement("span");
8906
9187
  req.className = "text-red-500 ml-1";
@@ -9028,6 +9309,29 @@ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
9028
9309
  }
9029
9310
  }
9030
9311
  function renderElement2(element, ctx) {
9312
+ if (element.type === "markdown") {
9313
+ if (element.hidden === true) {
9314
+ const placeholder = document.createElement("div");
9315
+ placeholder.style.display = "none";
9316
+ placeholder.setAttribute("data-fb-hidden-markdown", "true");
9317
+ return placeholder;
9318
+ }
9319
+ const initiallyDisabled2 = shouldDisableElement(element, ctx);
9320
+ const outerWrapper = document.createElement("div");
9321
+ outerWrapper.className = "mb-6 fb-field-wrapper fb-markdown-wrapper";
9322
+ outerWrapper.setAttribute(
9323
+ "data-field-key",
9324
+ getElementLookupKey(element, ctx.state)
9325
+ );
9326
+ renderMarkdown(element, ctx, outerWrapper);
9327
+ if (initiallyDisabled2) {
9328
+ outerWrapper.style.display = "none";
9329
+ outerWrapper.classList.add("fb-field-wrapper-disabled");
9330
+ outerWrapper.setAttribute("data-conditionally-disabled", "true");
9331
+ }
9332
+ setupEnableIfListeners(outerWrapper, element, ctx);
9333
+ return outerWrapper;
9334
+ }
9031
9335
  const initiallyDisabled = shouldDisableElement(element, ctx);
9032
9336
  const wrapper = document.createElement("div");
9033
9337
  wrapper.className = "mb-6 fb-field-wrapper";
@@ -9238,7 +9542,9 @@ function createInstanceState(config) {
9238
9542
  translations: mergedTranslations
9239
9543
  },
9240
9544
  debounceTimer: null,
9241
- prefill: {}
9545
+ prefill: {},
9546
+ syntheticElementIds: /* @__PURE__ */ new WeakMap(),
9547
+ syntheticElementIdCounter: 0
9242
9548
  };
9243
9549
  }
9244
9550
  function generateInstanceId() {
@@ -9515,6 +9821,11 @@ var componentRegistry = {
9515
9821
  // Legacy type: `type: "hidden"` — reads/writes DOM <input type="hidden"> element
9516
9822
  validate: validateHiddenElement,
9517
9823
  update: updateHiddenField
9824
+ },
9825
+ markdown: {
9826
+ // Display-only element — no value, no errors, skip from form data
9827
+ validate: validateMarkdown,
9828
+ update: updateMarkdown
9518
9829
  }
9519
9830
  };
9520
9831
  function getComponentOperations(elementType) {
@@ -9942,7 +10253,7 @@ var FormBuilderInstance = class {
9942
10253
  fieldsWrapper.className = `grid grid-cols-${columns} gap-4`;
9943
10254
  }
9944
10255
  schema.elements.forEach((element) => {
9945
- if (element.hidden || element.type === "hidden") {
10256
+ if (element.type !== "markdown" && (element.hidden || element.type === "hidden")) {
9946
10257
  const val = prefill?.[element.key] ?? element.default ?? null;
9947
10258
  fieldsWrapper.appendChild(createHiddenInput(element.key, val));
9948
10259
  return;
@@ -9977,7 +10288,7 @@ var FormBuilderInstance = class {
9977
10288
  const errors = [];
9978
10289
  const data = {};
9979
10290
  const validateElement2 = (element, ctx, customScopeRoot = null) => {
9980
- const key = element.key;
10291
+ const key = element.key ?? "";
9981
10292
  const scopeRoot = customScopeRoot || this.state.formRoot;
9982
10293
  const componentContext = {
9983
10294
  scopeRoot,
@@ -9995,7 +10306,8 @@ var FormBuilderInstance = class {
9995
10306
  errors.push(...componentResult.errors);
9996
10307
  return {
9997
10308
  value: componentResult.value,
9998
- spread: !!componentResult.spread
10309
+ spread: !!componentResult.spread,
10310
+ skip: !!componentResult.skip
9999
10311
  };
10000
10312
  }
10001
10313
  console.warn(`Unknown field type "${element.type}" for key "${key}"`);
@@ -10016,6 +10328,9 @@ var FormBuilderInstance = class {
10016
10328
  );
10017
10329
  }
10018
10330
  }
10331
+ if (element.type === "markdown") {
10332
+ return;
10333
+ }
10019
10334
  if (element.hidden || element.type === "hidden") {
10020
10335
  const hiddenInput = this.state.formRoot.querySelector(
10021
10336
  `input[type="hidden"][data-hidden-field="true"][name="${element.key}"]`
@@ -10028,9 +10343,10 @@ var FormBuilderInstance = class {
10028
10343
  }
10029
10344
  } else {
10030
10345
  const result = validateElement2(element, { path: "" });
10346
+ if (result.skip) return;
10031
10347
  if (result.spread && result.value !== null && typeof result.value === "object") {
10032
10348
  Object.assign(data, result.value);
10033
- } else {
10349
+ } else if (element.key) {
10034
10350
  data[element.key] = result.value;
10035
10351
  }
10036
10352
  }
@@ -10106,6 +10422,7 @@ var FormBuilderInstance = class {
10106
10422
  buildHiddenFieldsData(elements) {
10107
10423
  const data = {};
10108
10424
  for (const element of elements) {
10425
+ if (element.type === "markdown" || !element.key) continue;
10109
10426
  const key = element.key;
10110
10427
  if (element.hidden && element.default !== void 0) {
10111
10428
  data[key] = element.default;
@@ -10226,6 +10543,71 @@ var FormBuilderInstance = class {
10226
10543
  );
10227
10544
  }
10228
10545
  }
10546
+ /**
10547
+ * Find the field wrapper DOM element for a given element+path combo.
10548
+ * Used by reevaluateConditionalFields.
10549
+ */
10550
+ findFieldWrapper(element, currentPath) {
10551
+ const formRoot = this.state.formRoot;
10552
+ const lookupKey = getElementLookupKey(element, this.state);
10553
+ if (!currentPath) {
10554
+ return formRoot.querySelector(`[data-field-key="${lookupKey}"]`);
10555
+ }
10556
+ const pathMatch = currentPath.match(/^(.+)\[(\d+)\]$/);
10557
+ if (pathMatch) {
10558
+ const containerEl2 = formRoot.querySelector(
10559
+ `[data-container-item="${pathMatch[1]}[${pathMatch[2]}]"]`
10560
+ );
10561
+ return containerEl2 ? containerEl2.querySelector(`[data-field-key="${lookupKey}"]`) : null;
10562
+ }
10563
+ const containerEl = formRoot.querySelector(
10564
+ `[data-container="${currentPath}"]`
10565
+ );
10566
+ return containerEl ? containerEl.querySelector(`[data-field-key="${lookupKey}"]`) : null;
10567
+ }
10568
+ /**
10569
+ * Apply enableIf show/hide logic to a single field wrapper.
10570
+ * Extracted to reduce cyclomatic complexity of checkElements.
10571
+ */
10572
+ applyEnableIfVisibility(element, wrapper, currentPath, fullPath, formData) {
10573
+ try {
10574
+ const scope = element.enableIf.scope ?? "relative";
10575
+ const containerData = scope === "relative" && currentPath ? getValueByPath(formData, currentPath) : void 0;
10576
+ const shouldEnable = evaluateEnableCondition(
10577
+ element.enableIf,
10578
+ formData,
10579
+ containerData
10580
+ );
10581
+ const isCurrentlyDisabled = wrapper.getAttribute("data-conditionally-disabled") === "true";
10582
+ if (shouldEnable && isCurrentlyDisabled) {
10583
+ const containerPrefill = currentPath ? getValueByPath(formData, currentPath) : formData;
10584
+ const prefillContext = containerPrefill && typeof containerPrefill === "object" ? containerPrefill : {};
10585
+ const newWrapper = renderElement2(element, {
10586
+ path: currentPath,
10587
+ prefill: prefillContext,
10588
+ formData,
10589
+ state: this.state,
10590
+ instance: this
10591
+ });
10592
+ wrapper.parentNode?.replaceChild(newWrapper, wrapper);
10593
+ } else if (!shouldEnable && !isCurrentlyDisabled) {
10594
+ const disabledWrapper = document.createElement("div");
10595
+ disabledWrapper.className = "fb-field-wrapper-disabled";
10596
+ disabledWrapper.style.display = "none";
10597
+ disabledWrapper.setAttribute(
10598
+ "data-field-key",
10599
+ getElementLookupKey(element, this.state)
10600
+ );
10601
+ disabledWrapper.setAttribute("data-conditionally-disabled", "true");
10602
+ wrapper.parentNode?.replaceChild(disabledWrapper, wrapper);
10603
+ }
10604
+ } catch (error) {
10605
+ console.error(
10606
+ `Error re-evaluating enableIf for field "${element.key ?? "<no key>"}" at path "${fullPath}":`,
10607
+ error
10608
+ );
10609
+ }
10610
+ }
10229
10611
  /**
10230
10612
  * Re-evaluate all conditional fields (enableIf) based on current form data
10231
10613
  * This is called automatically when form data changes (via onChange events)
@@ -10235,97 +10617,31 @@ var FormBuilderInstance = class {
10235
10617
  const formData = this.validateForm(true).data;
10236
10618
  const checkElements = (elements, currentPath) => {
10237
10619
  elements.forEach((element) => {
10238
- const fullPath = currentPath ? `${currentPath}.${element.key}` : element.key;
10620
+ const fullPath = currentPath ? `${currentPath}.${element.key ?? ""}` : element.key ?? "";
10239
10621
  if (element.enableIf) {
10240
- let fieldWrapper = null;
10241
- if (currentPath) {
10242
- const pathMatch = currentPath.match(/^(.+)\[(\d+)\]$/);
10243
- if (pathMatch) {
10244
- const containerKey = pathMatch[1];
10245
- const containerIndex = pathMatch[2];
10246
- const containerElement = this.state.formRoot.querySelector(
10247
- `[data-container-item="${containerKey}[${containerIndex}]"]`
10248
- );
10249
- if (containerElement) {
10250
- fieldWrapper = containerElement.querySelector(
10251
- `[data-field-key="${element.key}"]`
10252
- );
10253
- }
10254
- } else {
10255
- const containerElement = this.state.formRoot.querySelector(
10256
- `[data-container="${currentPath}"]`
10257
- );
10258
- if (containerElement) {
10259
- fieldWrapper = containerElement.querySelector(
10260
- `[data-field-key="${element.key}"]`
10261
- );
10262
- }
10263
- }
10264
- } else {
10265
- fieldWrapper = this.state.formRoot.querySelector(
10266
- `[data-field-key="${element.key}"]`
10267
- );
10268
- }
10622
+ const fieldWrapper = this.findFieldWrapper(element, currentPath);
10269
10623
  if (fieldWrapper) {
10270
- const wrapper = fieldWrapper;
10271
- try {
10272
- let containerData = void 0;
10273
- const scope = element.enableIf.scope ?? "relative";
10274
- if (scope === "relative" && currentPath) {
10275
- containerData = getValueByPath(formData, currentPath);
10276
- }
10277
- const shouldEnable = evaluateEnableCondition(
10278
- element.enableIf,
10279
- formData,
10280
- // Use complete formData for absolute scope
10281
- containerData
10282
- // Use container-specific data for relative scope
10283
- );
10284
- const isCurrentlyDisabled = wrapper.getAttribute("data-conditionally-disabled") === "true";
10285
- if (shouldEnable && isCurrentlyDisabled) {
10286
- const containerPrefill = currentPath ? getValueByPath(formData, currentPath) : formData;
10287
- const prefillContext = containerPrefill && typeof containerPrefill === "object" ? containerPrefill : {};
10288
- const newWrapper = renderElement2(element, {
10289
- path: currentPath,
10290
- // Use container path (empty string for root-level)
10291
- prefill: prefillContext,
10292
- formData,
10293
- // Pass complete formData for enableIf evaluation
10294
- state: this.state,
10295
- instance: this
10296
- });
10297
- wrapper.parentNode?.replaceChild(newWrapper, wrapper);
10298
- } else if (!shouldEnable && !isCurrentlyDisabled) {
10299
- const disabledWrapper = document.createElement("div");
10300
- disabledWrapper.className = "fb-field-wrapper-disabled";
10301
- disabledWrapper.style.display = "none";
10302
- disabledWrapper.setAttribute("data-field-key", element.key);
10303
- disabledWrapper.setAttribute(
10304
- "data-conditionally-disabled",
10305
- "true"
10306
- );
10307
- wrapper.parentNode?.replaceChild(disabledWrapper, wrapper);
10308
- }
10309
- } catch (error) {
10310
- console.error(
10311
- `Error re-evaluating enableIf for field "${element.key}" at path "${fullPath}":`,
10312
- error
10313
- );
10314
- }
10624
+ this.applyEnableIfVisibility(
10625
+ element,
10626
+ fieldWrapper,
10627
+ currentPath,
10628
+ fullPath,
10629
+ formData
10630
+ );
10315
10631
  }
10316
10632
  }
10317
10633
  if ((element.type === "container" || element.type === "group") && "elements" in element && element.elements) {
10318
- const containerData = formData?.[element.key];
10634
+ const containerData = element.key ? formData?.[element.key] : void 0;
10319
10635
  if (Array.isArray(containerData)) {
10320
10636
  const containerItems = this.state.formRoot.querySelectorAll(
10321
10637
  `[data-container-item]`
10322
10638
  );
10323
- const directItems = Array.from(containerItems).filter((el) => {
10639
+ const directItems = fullPath ? Array.from(containerItems).filter((el) => {
10324
10640
  const attr = el.getAttribute("data-container-item") || "";
10325
10641
  if (!attr.startsWith(`${fullPath}[`)) return false;
10326
10642
  const suffix = attr.slice(fullPath.length);
10327
10643
  return /^\[\d+\]$/.test(suffix);
10328
- });
10644
+ }) : [];
10329
10645
  directItems.forEach((el) => {
10330
10646
  const attr = el.getAttribute("data-container-item") || "";
10331
10647
  checkElements(element.elements, attr);