@smallwebco/tinypivot-react 1.0.28 → 1.0.29

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/index.cjs CHANGED
@@ -1610,6 +1610,58 @@ function PivotSkeleton({
1610
1610
  setSortDirection("asc");
1611
1611
  }
1612
1612
  }, [sortTarget]);
1613
+ const [selectedCell, setSelectedCell] = (0, import_react8.useState)(null);
1614
+ const [selectionStart, setSelectionStart] = (0, import_react8.useState)(null);
1615
+ const [selectionEnd, setSelectionEnd] = (0, import_react8.useState)(null);
1616
+ const [isSelecting, setIsSelecting] = (0, import_react8.useState)(false);
1617
+ const [showCopyToast, setShowCopyToast] = (0, import_react8.useState)(false);
1618
+ const [copyToastMessage, setCopyToastMessage] = (0, import_react8.useState)("");
1619
+ const selectionBounds = (0, import_react8.useMemo)(() => {
1620
+ if (!selectionStart || !selectionEnd) return null;
1621
+ return {
1622
+ minRow: Math.min(selectionStart.row, selectionEnd.row),
1623
+ maxRow: Math.max(selectionStart.row, selectionEnd.row),
1624
+ minCol: Math.min(selectionStart.col, selectionEnd.col),
1625
+ maxCol: Math.max(selectionStart.col, selectionEnd.col)
1626
+ };
1627
+ }, [selectionStart, selectionEnd]);
1628
+ const handleCellMouseDown = (0, import_react8.useCallback)(
1629
+ (rowIndex, colIndex, event) => {
1630
+ event.preventDefault();
1631
+ if (event.shiftKey && selectedCell) {
1632
+ setSelectionEnd({ row: rowIndex, col: colIndex });
1633
+ } else {
1634
+ setSelectedCell({ row: rowIndex, col: colIndex });
1635
+ setSelectionStart({ row: rowIndex, col: colIndex });
1636
+ setSelectionEnd({ row: rowIndex, col: colIndex });
1637
+ setIsSelecting(true);
1638
+ }
1639
+ },
1640
+ [selectedCell]
1641
+ );
1642
+ const handleCellMouseEnter = (0, import_react8.useCallback)(
1643
+ (rowIndex, colIndex) => {
1644
+ if (isSelecting) {
1645
+ setSelectionEnd({ row: rowIndex, col: colIndex });
1646
+ }
1647
+ },
1648
+ [isSelecting]
1649
+ );
1650
+ const isCellSelected = (0, import_react8.useCallback)(
1651
+ (rowIndex, colIndex) => {
1652
+ if (!selectionBounds) {
1653
+ return selectedCell?.row === rowIndex && selectedCell?.col === colIndex;
1654
+ }
1655
+ const { minRow, maxRow, minCol, maxCol } = selectionBounds;
1656
+ return rowIndex >= minRow && rowIndex <= maxRow && colIndex >= minCol && colIndex <= maxCol;
1657
+ },
1658
+ [selectionBounds, selectedCell]
1659
+ );
1660
+ (0, import_react8.useEffect)(() => {
1661
+ const handleMouseUp = () => setIsSelecting(false);
1662
+ document.addEventListener("mouseup", handleMouseUp);
1663
+ return () => document.removeEventListener("mouseup", handleMouseUp);
1664
+ }, []);
1613
1665
  const sortedRowIndices = (0, import_react8.useMemo)(() => {
1614
1666
  if (!pivotResult) return [];
1615
1667
  const indices = pivotResult.rowHeaders.map((_, i) => i);
@@ -1634,6 +1686,78 @@ function PivotSkeleton({
1634
1686
  });
1635
1687
  return indices;
1636
1688
  }, [pivotResult, sortTarget, sortDirection]);
1689
+ const copySelectionToClipboard = (0, import_react8.useCallback)(() => {
1690
+ if (!selectionBounds || !pivotResult) return;
1691
+ const { minRow, maxRow, minCol, maxCol } = selectionBounds;
1692
+ const lines = [];
1693
+ for (let r = minRow; r <= maxRow; r++) {
1694
+ const sortedIdx = sortedRowIndices[r];
1695
+ if (sortedIdx === void 0) continue;
1696
+ const rowValues = [];
1697
+ for (let c = minCol; c <= maxCol; c++) {
1698
+ const cell = pivotResult.data[sortedIdx]?.[c];
1699
+ rowValues.push(cell?.formattedValue ?? "");
1700
+ }
1701
+ lines.push(rowValues.join(" "));
1702
+ }
1703
+ const text = lines.join("\n");
1704
+ navigator.clipboard.writeText(text).then(() => {
1705
+ const cellCount = (maxRow - minRow + 1) * (maxCol - minCol + 1);
1706
+ setCopyToastMessage(`Copied ${cellCount} cell${cellCount > 1 ? "s" : ""}`);
1707
+ setShowCopyToast(true);
1708
+ setTimeout(() => setShowCopyToast(false), 2e3);
1709
+ }).catch((err) => {
1710
+ console.error("Copy failed:", err);
1711
+ });
1712
+ }, [selectionBounds, pivotResult, sortedRowIndices]);
1713
+ (0, import_react8.useEffect)(() => {
1714
+ const handleKeydown = (event) => {
1715
+ if (!selectionBounds) return;
1716
+ if ((event.ctrlKey || event.metaKey) && event.key === "c") {
1717
+ event.preventDefault();
1718
+ copySelectionToClipboard();
1719
+ return;
1720
+ }
1721
+ if (event.key === "Escape") {
1722
+ setSelectedCell(null);
1723
+ setSelectionStart(null);
1724
+ setSelectionEnd(null);
1725
+ }
1726
+ };
1727
+ document.addEventListener("keydown", handleKeydown);
1728
+ return () => document.removeEventListener("keydown", handleKeydown);
1729
+ }, [selectionBounds, copySelectionToClipboard]);
1730
+ const selectionStats = (0, import_react8.useMemo)(() => {
1731
+ if (!selectionBounds || !pivotResult) return null;
1732
+ const { minRow, maxRow, minCol, maxCol } = selectionBounds;
1733
+ const values = [];
1734
+ let count = 0;
1735
+ for (let r = minRow; r <= maxRow; r++) {
1736
+ const sortedIdx = sortedRowIndices[r];
1737
+ if (sortedIdx === void 0) continue;
1738
+ for (let c = minCol; c <= maxCol; c++) {
1739
+ const cell = pivotResult.data[sortedIdx]?.[c];
1740
+ count++;
1741
+ if (cell?.value !== null && cell?.value !== void 0 && typeof cell.value === "number") {
1742
+ values.push(cell.value);
1743
+ }
1744
+ }
1745
+ }
1746
+ if (count <= 1) return null;
1747
+ const sum = values.reduce((a, b) => a + b, 0);
1748
+ const avg = values.length > 0 ? sum / values.length : 0;
1749
+ return {
1750
+ count,
1751
+ numericCount: values.length,
1752
+ sum,
1753
+ avg
1754
+ };
1755
+ }, [selectionBounds, pivotResult, sortedRowIndices]);
1756
+ const formatStatValue = (0, import_react8.useCallback)((val) => {
1757
+ if (Math.abs(val) >= 1e6) return `${(val / 1e6).toFixed(2)}M`;
1758
+ if (Math.abs(val) >= 1e3) return `${(val / 1e3).toFixed(2)}K`;
1759
+ return val.toFixed(2);
1760
+ }, []);
1637
1761
  const columnHeaderCells = (0, import_react8.useMemo)(() => {
1638
1762
  if (!pivotResult || pivotResult.headers.length === 0) {
1639
1763
  return [
@@ -1790,6 +1914,10 @@ function PivotSkeleton({
1790
1914
  {
1791
1915
  className: `vpg-pivot-skeleton vpg-font-${currentFontSize} ${draggingField ? "vpg-is-dragging" : ""}`,
1792
1916
  children: [
1917
+ showCopyToast && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "vpg-toast", children: [
1918
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("svg", { className: "vpg-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M5 13l4 4L19 7" }) }),
1919
+ copyToastMessage
1920
+ ] }),
1793
1921
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "vpg-skeleton-header", children: [
1794
1922
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "vpg-skeleton-title", children: [
1795
1923
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("svg", { className: "vpg-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
@@ -2091,14 +2219,19 @@ function PivotSkeleton({
2091
2219
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tbody", { children: [
2092
2220
  sortedRowIndices.map((sortedIdx) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { className: "vpg-data-row", children: [
2093
2221
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("th", { className: "vpg-row-header-cell", children: pivotResult.rowHeaders[sortedIdx].map((val, idx) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "vpg-row-value", children: val }, idx)) }),
2094
- pivotResult.data[sortedIdx].map((cell, colIdx) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2095
- "td",
2096
- {
2097
- className: `vpg-data-cell ${cell.value === null ? "vpg-is-null" : ""}`,
2098
- children: cell.formattedValue
2099
- },
2100
- colIdx
2101
- )),
2222
+ pivotResult.data[sortedIdx].map((cell, colIdx) => {
2223
+ const displayRowIdx = sortedRowIndices.indexOf(sortedIdx);
2224
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2225
+ "td",
2226
+ {
2227
+ className: `vpg-data-cell ${isCellSelected(displayRowIdx, colIdx) ? "selected" : ""} ${cell.value === null ? "vpg-is-null" : ""}`,
2228
+ onMouseDown: (e) => handleCellMouseDown(displayRowIdx, colIdx, e),
2229
+ onMouseEnter: () => handleCellMouseEnter(displayRowIdx, colIdx),
2230
+ children: cell.formattedValue
2231
+ },
2232
+ colIdx
2233
+ );
2234
+ }),
2102
2235
  pivotResult.rowTotals[sortedIdx] && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { className: "vpg-data-cell vpg-total-cell", children: pivotResult.rowTotals[sortedIdx].formattedValue })
2103
2236
  ] }, sortedIdx)),
2104
2237
  pivotResult.columnTotals.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { className: "vpg-totals-row", children: [
@@ -2108,12 +2241,32 @@ function PivotSkeleton({
2108
2241
  ] })
2109
2242
  ] })
2110
2243
  ] }) }),
2111
- isConfigured && pivotResult && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "vpg-skeleton-footer", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { children: [
2112
- pivotResult.rowHeaders.length,
2113
- " rows \xD7 ",
2114
- pivotResult.data[0]?.length || 0,
2115
- " columns"
2116
- ] }) })
2244
+ isConfigured && pivotResult && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "vpg-skeleton-footer", children: [
2245
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "vpg-footer-info", children: [
2246
+ pivotResult.rowHeaders.length,
2247
+ " rows \xD7 ",
2248
+ pivotResult.data[0]?.length || 0,
2249
+ " columns"
2250
+ ] }),
2251
+ selectionStats && selectionStats.count > 1 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "vpg-selection-stats", children: [
2252
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "vpg-stat", children: [
2253
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "vpg-stat-label", children: "Count:" }),
2254
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "vpg-stat-value", children: selectionStats.count })
2255
+ ] }),
2256
+ selectionStats.numericCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
2257
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "vpg-stat-divider", children: "|" }),
2258
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "vpg-stat", children: [
2259
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "vpg-stat-label", children: "Sum:" }),
2260
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "vpg-stat-value", children: formatStatValue(selectionStats.sum) })
2261
+ ] }),
2262
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "vpg-stat-divider", children: "|" }),
2263
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "vpg-stat", children: [
2264
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "vpg-stat-label", children: "Avg:" }),
2265
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "vpg-stat-value", children: formatStatValue(selectionStats.avg) })
2266
+ ] })
2267
+ ] })
2268
+ ] })
2269
+ ] })
2117
2270
  ] }),
2118
2271
  showWatermark && canUsePivot && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: `vpg-watermark ${isDemo ? "vpg-demo-mode" : ""}`, children: isDemo ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
2119
2272
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "vpg-demo-badge", children: "DEMO" }),