@dmitryvim/form-builder 0.2.4 → 0.2.6

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.
@@ -62,9 +62,6 @@ function validateSchema(schema) {
62
62
  errors.push("Schema must be an object");
63
63
  return errors;
64
64
  }
65
- if (!schema.version) {
66
- errors.push("Schema missing version");
67
- }
68
65
  if (!Array.isArray(schema.elements)) {
69
66
  errors.push("Schema missing elements array");
70
67
  return errors;
@@ -78,6 +75,20 @@ function validateSchema(schema) {
78
75
  if (!element.key) {
79
76
  errors.push(`${elementPath}: missing key`);
80
77
  }
78
+ if (element.displayIf) {
79
+ const displayIf = element.displayIf;
80
+ if (!displayIf.key || typeof displayIf.key !== "string") {
81
+ errors.push(
82
+ `${elementPath}: displayIf must have a 'key' property of type string`
83
+ );
84
+ }
85
+ const hasOperator = "equals" in displayIf;
86
+ if (!hasOperator) {
87
+ errors.push(
88
+ `${elementPath}: displayIf must have at least one operator (equals, etc.)`
89
+ );
90
+ }
91
+ }
81
92
  if (element.type === "group" && "elements" in element && element.elements) {
82
93
  validateElements(element.elements, `${elementPath}.elements`);
83
94
  }
@@ -115,6 +126,66 @@ function clear(node) {
115
126
  while (node.firstChild) node.removeChild(node.firstChild);
116
127
  }
117
128
 
129
+ // src/utils/display-conditions.ts
130
+ function getValueByPath(data, path) {
131
+ if (!data || typeof data !== "object") {
132
+ return void 0;
133
+ }
134
+ const segments = path.match(/[^.[\]]+|\[\d+\]/g);
135
+ if (!segments || segments.length === 0) {
136
+ return void 0;
137
+ }
138
+ let current = data;
139
+ for (const segment of segments) {
140
+ if (current === void 0 || current === null) {
141
+ return void 0;
142
+ }
143
+ if (segment.startsWith("[") && segment.endsWith("]")) {
144
+ const index = parseInt(segment.slice(1, -1), 10);
145
+ if (!Array.isArray(current) || isNaN(index)) {
146
+ return void 0;
147
+ }
148
+ current = current[index];
149
+ } else {
150
+ current = current[segment];
151
+ }
152
+ }
153
+ return current;
154
+ }
155
+ function evaluateDisplayCondition(condition, formData) {
156
+ if (!condition || !condition.key) {
157
+ throw new Error(
158
+ "Invalid displayIf condition: must have a 'key' property"
159
+ );
160
+ }
161
+ const actualValue = getValueByPath(formData, condition.key);
162
+ if ("equals" in condition) {
163
+ return deepEqual(actualValue, condition.equals);
164
+ }
165
+ throw new Error(
166
+ `Invalid displayIf condition: no recognized operator (equals, etc.)`
167
+ );
168
+ }
169
+ function deepEqual(a, b) {
170
+ if (a === b) return true;
171
+ if (a == null || b == null) return a === b;
172
+ if (typeof a !== typeof b) return false;
173
+ if (typeof a === "object" && typeof b === "object") {
174
+ try {
175
+ return JSON.stringify(a) === JSON.stringify(b);
176
+ } catch (e) {
177
+ if (e instanceof TypeError && (e.message.includes("circular") || e.message.includes("cyclic"))) {
178
+ console.warn(
179
+ "deepEqual: Circular reference detected in displayIf comparison, using reference equality"
180
+ );
181
+ return a === b;
182
+ }
183
+ throw e;
184
+ }
185
+ }
186
+ return a === b;
187
+ }
188
+
118
189
  // src/components/text.ts
119
190
  function renderTextElement(element, ctx, wrapper, pathKey) {
120
191
  const state = ctx.state;
@@ -1289,8 +1360,22 @@ async function renderFilePreview(container, resourceId, state, options = {}) {
1289
1360
  const thumbnailUrl = await state.config.getThumbnail(resourceId);
1290
1361
  if (thumbnailUrl) {
1291
1362
  clear(container);
1292
- img.src = thumbnailUrl;
1293
- container.appendChild(img);
1363
+ if (meta && meta.type && meta.type.startsWith("video/")) {
1364
+ const video = document.createElement("video");
1365
+ video.className = "w-full h-full object-contain";
1366
+ video.controls = true;
1367
+ video.preload = "metadata";
1368
+ video.muted = true;
1369
+ const source = document.createElement("source");
1370
+ source.src = thumbnailUrl;
1371
+ source.type = meta.type;
1372
+ video.appendChild(source);
1373
+ video.appendChild(document.createTextNode("Your browser does not support the video tag."));
1374
+ container.appendChild(video);
1375
+ } else {
1376
+ img.src = thumbnailUrl;
1377
+ container.appendChild(img);
1378
+ }
1294
1379
  } else {
1295
1380
  setEmptyFileContainer(container, state);
1296
1381
  }
@@ -2235,7 +2320,7 @@ function renderElement(element, ctx) {
2235
2320
  return renderElementFunc(element, ctx);
2236
2321
  }
2237
2322
  function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
2238
- var _a;
2323
+ var _a, _b;
2239
2324
  const containerWrap = document.createElement("div");
2240
2325
  containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
2241
2326
  containerWrap.setAttribute("data-container", pathKey);
@@ -2250,6 +2335,9 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
2250
2335
  const subCtx = {
2251
2336
  path: pathJoin(ctx.path, element.key),
2252
2337
  prefill: ((_a = ctx.prefill) == null ? void 0 : _a[element.key]) || {},
2338
+ // Sliced data for value population
2339
+ formData: (_b = ctx.formData) != null ? _b : ctx.prefill,
2340
+ // Complete root data for displayIf evaluation
2253
2341
  state: ctx.state
2254
2342
  };
2255
2343
  element.elements.forEach((child) => {
@@ -2262,7 +2350,7 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
2262
2350
  wrapper.appendChild(containerWrap);
2263
2351
  }
2264
2352
  function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
2265
- var _a, _b, _c;
2353
+ var _a, _b, _c, _d;
2266
2354
  const state = ctx.state;
2267
2355
  const containerWrap = document.createElement("div");
2268
2356
  containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
@@ -2289,12 +2377,15 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
2289
2377
  add.className = "px-3 py-1.5 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors";
2290
2378
  add.textContent = t("addElement", state);
2291
2379
  add.onclick = () => {
2380
+ var _a2;
2292
2381
  if (countItems() < max) {
2293
2382
  const idx = countItems();
2294
2383
  const subCtx = {
2295
2384
  state: ctx.state,
2296
2385
  path: pathJoin(ctx.path, `${element.key}[${idx}]`),
2297
- prefill: {}
2386
+ prefill: {},
2387
+ formData: (_a2 = ctx.formData) != null ? _a2 : ctx.prefill
2388
+ // Complete root data for displayIf
2298
2389
  };
2299
2390
  const item = document.createElement("div");
2300
2391
  item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
@@ -2336,10 +2427,13 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
2336
2427
  }
2337
2428
  if (pre && Array.isArray(pre)) {
2338
2429
  pre.forEach((prefillObj, idx) => {
2430
+ var _a2;
2339
2431
  const subCtx = {
2340
2432
  state: ctx.state,
2341
2433
  path: pathJoin(ctx.path, `${element.key}[${idx}]`),
2342
- prefill: prefillObj || {}
2434
+ prefill: prefillObj || {},
2435
+ formData: (_a2 = ctx.formData) != null ? _a2 : ctx.prefill
2436
+ // Complete root data for displayIf
2343
2437
  };
2344
2438
  const item = document.createElement("div");
2345
2439
  item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
@@ -2370,7 +2464,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
2370
2464
  const subCtx = {
2371
2465
  state: ctx.state,
2372
2466
  path: pathJoin(ctx.path, `${element.key}[${idx}]`),
2373
- prefill: {}
2467
+ prefill: {},
2468
+ formData: (_d = ctx.formData) != null ? _d : ctx.prefill
2469
+ // Complete root data for displayIf
2374
2470
  };
2375
2471
  const item = document.createElement("div");
2376
2472
  item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
@@ -2654,8 +2750,32 @@ if (typeof document !== "undefined") {
2654
2750
  });
2655
2751
  }
2656
2752
  function renderElement2(element, ctx) {
2753
+ var _a;
2754
+ if (element.displayIf) {
2755
+ try {
2756
+ const dataForCondition = (_a = ctx.formData) != null ? _a : ctx.prefill;
2757
+ const shouldDisplay = evaluateDisplayCondition(
2758
+ element.displayIf,
2759
+ dataForCondition
2760
+ );
2761
+ if (!shouldDisplay) {
2762
+ const hiddenWrapper = document.createElement("div");
2763
+ hiddenWrapper.className = "fb-field-wrapper-hidden";
2764
+ hiddenWrapper.style.display = "none";
2765
+ hiddenWrapper.setAttribute("data-field-key", element.key);
2766
+ hiddenWrapper.setAttribute("data-conditionally-hidden", "true");
2767
+ return hiddenWrapper;
2768
+ }
2769
+ } catch (error) {
2770
+ console.error(
2771
+ `Error evaluating displayIf for field "${element.key}":`,
2772
+ error
2773
+ );
2774
+ }
2775
+ }
2657
2776
  const wrapper = document.createElement("div");
2658
2777
  wrapper.className = "mb-6 fb-field-wrapper";
2778
+ wrapper.setAttribute("data-field-key", element.key);
2659
2779
  const label = document.createElement("div");
2660
2780
  label.className = "flex items-center mb-2";
2661
2781
  const title = document.createElement("label");
@@ -3148,6 +3268,7 @@ var FormBuilderInstance = class {
3148
3268
  }
3149
3269
  this.state.debounceTimer = setTimeout(() => {
3150
3270
  const formData = this.validateForm(true);
3271
+ this.reevaluateConditionalFields();
3151
3272
  if (this.state.config.onChange) {
3152
3273
  this.state.config.onChange(formData);
3153
3274
  }
@@ -3407,6 +3528,8 @@ var FormBuilderInstance = class {
3407
3528
  const block = renderElement2(element, {
3408
3529
  path: "",
3409
3530
  prefill: prefill || {},
3531
+ formData: prefill || {},
3532
+ // Pass complete root data for displayIf evaluation
3410
3533
  state: this.state,
3411
3534
  instance: this
3412
3535
  });
@@ -3451,6 +3574,19 @@ var FormBuilderInstance = class {
3451
3574
  };
3452
3575
  setValidateElement(validateElement2);
3453
3576
  this.state.schema.elements.forEach((element) => {
3577
+ if (element.displayIf) {
3578
+ try {
3579
+ const shouldDisplay = evaluateDisplayCondition(element.displayIf, data);
3580
+ if (!shouldDisplay) {
3581
+ return;
3582
+ }
3583
+ } catch (error) {
3584
+ console.error(
3585
+ `Error evaluating displayIf for field "${element.key}" during validation:`,
3586
+ error
3587
+ );
3588
+ }
3589
+ }
3454
3590
  if (element.hidden) {
3455
3591
  data[element.key] = element.default !== void 0 ? element.default : null;
3456
3592
  } else {
@@ -3607,6 +3743,71 @@ var FormBuilderInstance = class {
3607
3743
  );
3608
3744
  }
3609
3745
  }
3746
+ /**
3747
+ * Re-evaluate all conditional fields (displayIf) based on current form data
3748
+ * This is called automatically when form data changes (via onChange events)
3749
+ */
3750
+ reevaluateConditionalFields() {
3751
+ if (!this.state.schema || !this.state.formRoot) return;
3752
+ const formData = this.validateForm(true).data;
3753
+ const checkElements = (elements, currentPath) => {
3754
+ elements.forEach((element) => {
3755
+ const fullPath = currentPath ? `${currentPath}.${element.key}` : element.key;
3756
+ if (element.displayIf) {
3757
+ const fieldWrappers = this.state.formRoot.querySelectorAll(
3758
+ `[data-field-key="${element.key}"]`
3759
+ );
3760
+ fieldWrappers.forEach((wrapper) => {
3761
+ var _a, _b;
3762
+ try {
3763
+ const shouldDisplay = evaluateDisplayCondition(
3764
+ element.displayIf,
3765
+ formData
3766
+ // Use complete formData for condition evaluation
3767
+ );
3768
+ const isCurrentlyHidden = wrapper.getAttribute("data-conditionally-hidden") === "true";
3769
+ if (shouldDisplay && isCurrentlyHidden) {
3770
+ const newWrapper = renderElement2(element, {
3771
+ path: fullPath,
3772
+ // Use accumulated path
3773
+ prefill: formData,
3774
+ // Use complete formData for root-level elements
3775
+ formData,
3776
+ // Pass complete formData for displayIf evaluation
3777
+ state: this.state,
3778
+ instance: this
3779
+ });
3780
+ (_a = wrapper.parentNode) == null ? void 0 : _a.replaceChild(newWrapper, wrapper);
3781
+ } else if (!shouldDisplay && !isCurrentlyHidden) {
3782
+ const hiddenWrapper = document.createElement("div");
3783
+ hiddenWrapper.className = "fb-field-wrapper-hidden";
3784
+ hiddenWrapper.style.display = "none";
3785
+ hiddenWrapper.setAttribute("data-field-key", element.key);
3786
+ hiddenWrapper.setAttribute("data-conditionally-hidden", "true");
3787
+ (_b = wrapper.parentNode) == null ? void 0 : _b.replaceChild(hiddenWrapper, wrapper);
3788
+ }
3789
+ } catch (error) {
3790
+ console.error(
3791
+ `Error re-evaluating displayIf for field "${element.key}":`,
3792
+ error
3793
+ );
3794
+ }
3795
+ });
3796
+ }
3797
+ if ((element.type === "container" || element.type === "group") && "elements" in element && element.elements) {
3798
+ const containerData = formData == null ? void 0 : formData[element.key];
3799
+ if (Array.isArray(containerData)) {
3800
+ containerData.forEach((_, index) => {
3801
+ checkElements(element.elements, `${fullPath}[${index}]`);
3802
+ });
3803
+ } else {
3804
+ checkElements(element.elements, fullPath);
3805
+ }
3806
+ }
3807
+ });
3808
+ };
3809
+ checkElements(this.state.schema.elements, "");
3810
+ }
3610
3811
  /**
3611
3812
  * Destroy instance and clean up resources
3612
3813
  */