@dmitryvim/form-builder 0.2.0 → 0.2.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
@@ -1496,6 +1496,14 @@ function renderResourcePills(container, rids, state, onRemove) {
1496
1496
  container.appendChild(slot);
1497
1497
  }
1498
1498
  }
1499
+ function renderThumbnailError(slot, iconSize = "w-12 h-12") {
1500
+ slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
1501
+ <svg class="${iconSize} text-red-400" fill="currentColor" viewBox="0 0 24 24">
1502
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
1503
+ </svg>
1504
+ <div class="text-xs mt-1 text-red-600">Preview error</div>
1505
+ </div>`;
1506
+ }
1499
1507
  async function renderThumbnailForResource(slot, rid, meta, state) {
1500
1508
  if (meta && meta.type?.startsWith("image/")) {
1501
1509
  if (meta.file && meta.file instanceof File) {
@@ -1509,19 +1517,27 @@ async function renderThumbnailForResource(slot, rid, meta, state) {
1509
1517
  reader.readAsDataURL(meta.file);
1510
1518
  slot.appendChild(img);
1511
1519
  } else if (state.config.getThumbnail) {
1512
- const url = await state.config.getThumbnail(rid);
1513
- if (url) {
1514
- const img = document.createElement("img");
1515
- img.className = "w-full h-full object-contain";
1516
- img.alt = meta.name;
1517
- img.src = url;
1518
- slot.appendChild(img);
1519
- } else {
1520
- slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
1521
- <svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
1522
- <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
1523
- </svg>
1524
- </div>`;
1520
+ try {
1521
+ const url = await state.config.getThumbnail(rid);
1522
+ if (url) {
1523
+ const img = document.createElement("img");
1524
+ img.className = "w-full h-full object-contain";
1525
+ img.alt = meta.name;
1526
+ img.src = url;
1527
+ slot.appendChild(img);
1528
+ } else {
1529
+ slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
1530
+ <svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
1531
+ <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
1532
+ </svg>
1533
+ </div>`;
1534
+ }
1535
+ } catch (error) {
1536
+ const err = error instanceof Error ? error : new Error(String(error));
1537
+ if (state.config.onThumbnailError) {
1538
+ state.config.onThumbnailError(err, rid);
1539
+ }
1540
+ renderThumbnailError(slot);
1525
1541
  }
1526
1542
  } else {
1527
1543
  slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
@@ -1548,29 +1564,37 @@ async function renderThumbnailForResource(slot, rid, meta, state) {
1548
1564
  </div>
1549
1565
  `;
1550
1566
  } else if (state.config.getThumbnail) {
1551
- const videoUrl = await state.config.getThumbnail(rid);
1552
- if (videoUrl) {
1553
- slot.innerHTML = `
1554
- <div class="relative group h-full w-full">
1555
- <video class="w-full h-full object-contain" preload="metadata" muted>
1556
- <source src="${videoUrl}" type="${meta.type}">
1557
- </video>
1558
- <div class="absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center">
1559
- <div class="bg-white bg-opacity-90 rounded-full p-1">
1560
- <svg class="w-4 h-4 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
1561
- <path d="M8 5v14l11-7z"/>
1562
- </svg>
1567
+ try {
1568
+ const videoUrl = await state.config.getThumbnail(rid);
1569
+ if (videoUrl) {
1570
+ slot.innerHTML = `
1571
+ <div class="relative group h-full w-full">
1572
+ <video class="w-full h-full object-contain" preload="metadata" muted>
1573
+ <source src="${videoUrl}" type="${meta.type}">
1574
+ </video>
1575
+ <div class="absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center">
1576
+ <div class="bg-white bg-opacity-90 rounded-full p-1">
1577
+ <svg class="w-4 h-4 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
1578
+ <path d="M8 5v14l11-7z"/>
1579
+ </svg>
1580
+ </div>
1563
1581
  </div>
1564
1582
  </div>
1565
- </div>
1566
- `;
1567
- } else {
1568
- slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
1569
- <svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
1570
- <path d="M8 5v14l11-7z"/>
1571
- </svg>
1572
- <div class="text-xs mt-1">${meta?.name || "Video"}</div>
1573
- </div>`;
1583
+ `;
1584
+ } else {
1585
+ slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
1586
+ <svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
1587
+ <path d="M8 5v14l11-7z"/>
1588
+ </svg>
1589
+ <div class="text-xs mt-1">${meta?.name || "Video"}</div>
1590
+ </div>`;
1591
+ }
1592
+ } catch (error) {
1593
+ const err = error instanceof Error ? error : new Error(String(error));
1594
+ if (state.config.onThumbnailError) {
1595
+ state.config.onThumbnailError(err, rid);
1596
+ }
1597
+ renderThumbnailError(slot, "w-8 h-8");
1574
1598
  }
1575
1599
  } else {
1576
1600
  slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
@@ -1606,7 +1630,11 @@ async function handleFileSelect(file, container, fieldName, state, deps = null,
1606
1630
  throw new Error("Upload handler must return a string resource ID");
1607
1631
  }
1608
1632
  } catch (error) {
1609
- throw new Error(`File upload failed: ${error.message}`);
1633
+ const err = error instanceof Error ? error : new Error(String(error));
1634
+ if (state.config.onUploadError) {
1635
+ state.config.onUploadError(err, file);
1636
+ }
1637
+ throw new Error(`File upload failed: ${err.message}`);
1610
1638
  }
1611
1639
  } else {
1612
1640
  throw new Error(
@@ -1683,7 +1711,11 @@ async function uploadSingleFile(file, state) {
1683
1711
  }
1684
1712
  return rid;
1685
1713
  } catch (error) {
1686
- throw new Error(`File upload failed: ${error.message}`);
1714
+ const err = error instanceof Error ? error : new Error(String(error));
1715
+ if (state.config.onUploadError) {
1716
+ state.config.onUploadError(err, file);
1717
+ }
1718
+ throw new Error(`File upload failed: ${err.message}`);
1687
1719
  }
1688
1720
  } else {
1689
1721
  throw new Error(
@@ -1692,26 +1724,31 @@ async function uploadSingleFile(file, state) {
1692
1724
  }
1693
1725
  }
1694
1726
  async function forceDownload(resourceId, fileName, state) {
1695
- let fileUrl = null;
1696
- if (state.config.getDownloadUrl) {
1697
- fileUrl = state.config.getDownloadUrl(resourceId);
1698
- } else if (state.config.getThumbnail) {
1699
- fileUrl = await state.config.getThumbnail(resourceId);
1700
- }
1701
- if (fileUrl) {
1702
- const finalUrl = fileUrl.startsWith("http") ? fileUrl : new URL(fileUrl, window.location.href).href;
1703
- fetch(finalUrl).then((response) => {
1727
+ try {
1728
+ let fileUrl = null;
1729
+ if (state.config.getDownloadUrl) {
1730
+ fileUrl = state.config.getDownloadUrl(resourceId);
1731
+ } else if (state.config.getThumbnail) {
1732
+ fileUrl = await state.config.getThumbnail(resourceId);
1733
+ }
1734
+ if (fileUrl) {
1735
+ const finalUrl = fileUrl.startsWith("http") ? fileUrl : new URL(fileUrl, window.location.href).href;
1736
+ const response = await fetch(finalUrl);
1704
1737
  if (!response.ok) {
1705
1738
  throw new Error(`HTTP error! status: ${response.status}`);
1706
1739
  }
1707
- return response.blob();
1708
- }).then((blob) => {
1740
+ const blob = await response.blob();
1709
1741
  downloadBlob(blob, fileName);
1710
- }).catch((error) => {
1711
- throw new Error(`File download failed: ${error.message}`);
1712
- });
1713
- } else {
1714
- console.warn("No download URL available for resource:", resourceId);
1742
+ } else {
1743
+ throw new Error("No download URL available for resource");
1744
+ }
1745
+ } catch (error) {
1746
+ const err = error instanceof Error ? error : new Error(String(error));
1747
+ if (state.config.onDownloadError) {
1748
+ state.config.onDownloadError(err, resourceId, fileName);
1749
+ }
1750
+ console.error(`File download failed for ${fileName}:`, err);
1751
+ throw err;
1715
1752
  }
1716
1753
  }
1717
1754
  function downloadBlob(blob, fileName) {
@@ -1737,7 +1774,14 @@ function addPrefillFilesToIndex(initialFiles, state) {
1737
1774
  if (!state.resourceIndex.has(resourceId)) {
1738
1775
  const filename = resourceId.split("/").pop() || "file";
1739
1776
  const extension = filename.split(".").pop()?.toLowerCase();
1740
- const fileType = extension && ["jpg", "jpeg", "png", "gif", "webp"].includes(extension) ? `image/${extension === "jpg" ? "jpeg" : extension}` : "application/octet-stream";
1777
+ let fileType = "application/octet-stream";
1778
+ if (extension) {
1779
+ if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
1780
+ fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
1781
+ } else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
1782
+ fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
1783
+ }
1784
+ }
1741
1785
  state.resourceIndex.set(resourceId, {
1742
1786
  name: filename,
1743
1787
  type: fileType,
@@ -2670,6 +2714,9 @@ var defaultConfig = {
2670
2714
  actionHandler: null,
2671
2715
  onChange: null,
2672
2716
  onFieldChange: null,
2717
+ onThumbnailError: null,
2718
+ onUploadError: null,
2719
+ onDownloadError: null,
2673
2720
  debounceMs: 300,
2674
2721
  enableFilePreview: true,
2675
2722
  maxPreviewSize: "200px",
@@ -2970,6 +3017,25 @@ var FormBuilderInstance = class {
2970
3017
  constructor(config) {
2971
3018
  this.instanceId = generateInstanceId();
2972
3019
  this.state = createInstanceState(config);
3020
+ if (typeof process !== "undefined" && process.env.NODE_ENV === "development") {
3021
+ if (config?.getThumbnail) {
3022
+ const testResult = config.getThumbnail("test-id");
3023
+ if (!(testResult instanceof Promise)) {
3024
+ console.warn(
3025
+ "[form-builder] getThumbnail must be async (return Promise). Wrap your function: async (id) => { ... }"
3026
+ );
3027
+ }
3028
+ }
3029
+ if (!globalThis.__formBuilderInstances) {
3030
+ globalThis.__formBuilderInstances = /* @__PURE__ */ new Set();
3031
+ }
3032
+ globalThis.__formBuilderInstances.add(this.instanceId);
3033
+ if (globalThis.__formBuilderInstances.size > 10) {
3034
+ console.warn(
3035
+ `[form-builder] ${globalThis.__formBuilderInstances.size} instances active. Possible memory leak - ensure you call destroy() when done.`
3036
+ );
3037
+ }
3038
+ }
2973
3039
  }
2974
3040
  /**
2975
3041
  * Get instance ID (useful for debugging and resource prefixing)
@@ -3515,6 +3581,9 @@ var FormBuilderInstance = class {
3515
3581
  this.state.formRoot = null;
3516
3582
  this.state.schema = null;
3517
3583
  this.state.externalActions = null;
3584
+ if (typeof process !== "undefined" && process.env.NODE_ENV === "development") {
3585
+ globalThis.__formBuilderInstances?.delete(this.instanceId);
3586
+ }
3518
3587
  }
3519
3588
  };
3520
3589