@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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/components/DataGrid.tsx
2
- import { useState as useState9, useMemo as useMemo9, useCallback as useCallback9, useEffect as useEffect5, useRef as useRef2 } from "react";
2
+ import { useState as useState9, useMemo as useMemo9, useCallback as useCallback9, useEffect as useEffect6, useRef as useRef2 } from "react";
3
3
  import { createPortal as createPortal2 } from "react-dom";
4
4
 
5
5
  // src/hooks/useExcelGrid.ts
@@ -1546,7 +1546,7 @@ function PivotConfig({
1546
1546
  }
1547
1547
 
1548
1548
  // src/components/PivotSkeleton.tsx
1549
- import { useState as useState8, useMemo as useMemo8, useCallback as useCallback8 } from "react";
1549
+ import { useState as useState8, useMemo as useMemo8, useCallback as useCallback8, useEffect as useEffect5 } from "react";
1550
1550
  import { getAggregationLabel as getAggregationLabel2, getAggregationSymbol as getAggregationSymbol2 } from "@smallwebco/tinypivot-core";
1551
1551
  import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1552
1552
  function PivotSkeleton({
@@ -1595,6 +1595,58 @@ function PivotSkeleton({
1595
1595
  setSortDirection("asc");
1596
1596
  }
1597
1597
  }, [sortTarget]);
1598
+ const [selectedCell, setSelectedCell] = useState8(null);
1599
+ const [selectionStart, setSelectionStart] = useState8(null);
1600
+ const [selectionEnd, setSelectionEnd] = useState8(null);
1601
+ const [isSelecting, setIsSelecting] = useState8(false);
1602
+ const [showCopyToast, setShowCopyToast] = useState8(false);
1603
+ const [copyToastMessage, setCopyToastMessage] = useState8("");
1604
+ const selectionBounds = useMemo8(() => {
1605
+ if (!selectionStart || !selectionEnd) return null;
1606
+ return {
1607
+ minRow: Math.min(selectionStart.row, selectionEnd.row),
1608
+ maxRow: Math.max(selectionStart.row, selectionEnd.row),
1609
+ minCol: Math.min(selectionStart.col, selectionEnd.col),
1610
+ maxCol: Math.max(selectionStart.col, selectionEnd.col)
1611
+ };
1612
+ }, [selectionStart, selectionEnd]);
1613
+ const handleCellMouseDown = useCallback8(
1614
+ (rowIndex, colIndex, event) => {
1615
+ event.preventDefault();
1616
+ if (event.shiftKey && selectedCell) {
1617
+ setSelectionEnd({ row: rowIndex, col: colIndex });
1618
+ } else {
1619
+ setSelectedCell({ row: rowIndex, col: colIndex });
1620
+ setSelectionStart({ row: rowIndex, col: colIndex });
1621
+ setSelectionEnd({ row: rowIndex, col: colIndex });
1622
+ setIsSelecting(true);
1623
+ }
1624
+ },
1625
+ [selectedCell]
1626
+ );
1627
+ const handleCellMouseEnter = useCallback8(
1628
+ (rowIndex, colIndex) => {
1629
+ if (isSelecting) {
1630
+ setSelectionEnd({ row: rowIndex, col: colIndex });
1631
+ }
1632
+ },
1633
+ [isSelecting]
1634
+ );
1635
+ const isCellSelected = useCallback8(
1636
+ (rowIndex, colIndex) => {
1637
+ if (!selectionBounds) {
1638
+ return selectedCell?.row === rowIndex && selectedCell?.col === colIndex;
1639
+ }
1640
+ const { minRow, maxRow, minCol, maxCol } = selectionBounds;
1641
+ return rowIndex >= minRow && rowIndex <= maxRow && colIndex >= minCol && colIndex <= maxCol;
1642
+ },
1643
+ [selectionBounds, selectedCell]
1644
+ );
1645
+ useEffect5(() => {
1646
+ const handleMouseUp = () => setIsSelecting(false);
1647
+ document.addEventListener("mouseup", handleMouseUp);
1648
+ return () => document.removeEventListener("mouseup", handleMouseUp);
1649
+ }, []);
1598
1650
  const sortedRowIndices = useMemo8(() => {
1599
1651
  if (!pivotResult) return [];
1600
1652
  const indices = pivotResult.rowHeaders.map((_, i) => i);
@@ -1619,6 +1671,78 @@ function PivotSkeleton({
1619
1671
  });
1620
1672
  return indices;
1621
1673
  }, [pivotResult, sortTarget, sortDirection]);
1674
+ const copySelectionToClipboard = useCallback8(() => {
1675
+ if (!selectionBounds || !pivotResult) return;
1676
+ const { minRow, maxRow, minCol, maxCol } = selectionBounds;
1677
+ const lines = [];
1678
+ for (let r = minRow; r <= maxRow; r++) {
1679
+ const sortedIdx = sortedRowIndices[r];
1680
+ if (sortedIdx === void 0) continue;
1681
+ const rowValues = [];
1682
+ for (let c = minCol; c <= maxCol; c++) {
1683
+ const cell = pivotResult.data[sortedIdx]?.[c];
1684
+ rowValues.push(cell?.formattedValue ?? "");
1685
+ }
1686
+ lines.push(rowValues.join(" "));
1687
+ }
1688
+ const text = lines.join("\n");
1689
+ navigator.clipboard.writeText(text).then(() => {
1690
+ const cellCount = (maxRow - minRow + 1) * (maxCol - minCol + 1);
1691
+ setCopyToastMessage(`Copied ${cellCount} cell${cellCount > 1 ? "s" : ""}`);
1692
+ setShowCopyToast(true);
1693
+ setTimeout(() => setShowCopyToast(false), 2e3);
1694
+ }).catch((err) => {
1695
+ console.error("Copy failed:", err);
1696
+ });
1697
+ }, [selectionBounds, pivotResult, sortedRowIndices]);
1698
+ useEffect5(() => {
1699
+ const handleKeydown = (event) => {
1700
+ if (!selectionBounds) return;
1701
+ if ((event.ctrlKey || event.metaKey) && event.key === "c") {
1702
+ event.preventDefault();
1703
+ copySelectionToClipboard();
1704
+ return;
1705
+ }
1706
+ if (event.key === "Escape") {
1707
+ setSelectedCell(null);
1708
+ setSelectionStart(null);
1709
+ setSelectionEnd(null);
1710
+ }
1711
+ };
1712
+ document.addEventListener("keydown", handleKeydown);
1713
+ return () => document.removeEventListener("keydown", handleKeydown);
1714
+ }, [selectionBounds, copySelectionToClipboard]);
1715
+ const selectionStats = useMemo8(() => {
1716
+ if (!selectionBounds || !pivotResult) return null;
1717
+ const { minRow, maxRow, minCol, maxCol } = selectionBounds;
1718
+ const values = [];
1719
+ let count = 0;
1720
+ for (let r = minRow; r <= maxRow; r++) {
1721
+ const sortedIdx = sortedRowIndices[r];
1722
+ if (sortedIdx === void 0) continue;
1723
+ for (let c = minCol; c <= maxCol; c++) {
1724
+ const cell = pivotResult.data[sortedIdx]?.[c];
1725
+ count++;
1726
+ if (cell?.value !== null && cell?.value !== void 0 && typeof cell.value === "number") {
1727
+ values.push(cell.value);
1728
+ }
1729
+ }
1730
+ }
1731
+ if (count <= 1) return null;
1732
+ const sum = values.reduce((a, b) => a + b, 0);
1733
+ const avg = values.length > 0 ? sum / values.length : 0;
1734
+ return {
1735
+ count,
1736
+ numericCount: values.length,
1737
+ sum,
1738
+ avg
1739
+ };
1740
+ }, [selectionBounds, pivotResult, sortedRowIndices]);
1741
+ const formatStatValue = useCallback8((val) => {
1742
+ if (Math.abs(val) >= 1e6) return `${(val / 1e6).toFixed(2)}M`;
1743
+ if (Math.abs(val) >= 1e3) return `${(val / 1e3).toFixed(2)}K`;
1744
+ return val.toFixed(2);
1745
+ }, []);
1622
1746
  const columnHeaderCells = useMemo8(() => {
1623
1747
  if (!pivotResult || pivotResult.headers.length === 0) {
1624
1748
  return [
@@ -1775,6 +1899,10 @@ function PivotSkeleton({
1775
1899
  {
1776
1900
  className: `vpg-pivot-skeleton vpg-font-${currentFontSize} ${draggingField ? "vpg-is-dragging" : ""}`,
1777
1901
  children: [
1902
+ showCopyToast && /* @__PURE__ */ jsxs4("div", { className: "vpg-toast", children: [
1903
+ /* @__PURE__ */ jsx4("svg", { className: "vpg-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx4("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M5 13l4 4L19 7" }) }),
1904
+ copyToastMessage
1905
+ ] }),
1778
1906
  /* @__PURE__ */ jsxs4("div", { className: "vpg-skeleton-header", children: [
1779
1907
  /* @__PURE__ */ jsxs4("div", { className: "vpg-skeleton-title", children: [
1780
1908
  /* @__PURE__ */ jsx4("svg", { className: "vpg-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx4(
@@ -2076,14 +2204,19 @@ function PivotSkeleton({
2076
2204
  /* @__PURE__ */ jsxs4("tbody", { children: [
2077
2205
  sortedRowIndices.map((sortedIdx) => /* @__PURE__ */ jsxs4("tr", { className: "vpg-data-row", children: [
2078
2206
  /* @__PURE__ */ jsx4("th", { className: "vpg-row-header-cell", children: pivotResult.rowHeaders[sortedIdx].map((val, idx) => /* @__PURE__ */ jsx4("span", { className: "vpg-row-value", children: val }, idx)) }),
2079
- pivotResult.data[sortedIdx].map((cell, colIdx) => /* @__PURE__ */ jsx4(
2080
- "td",
2081
- {
2082
- className: `vpg-data-cell ${cell.value === null ? "vpg-is-null" : ""}`,
2083
- children: cell.formattedValue
2084
- },
2085
- colIdx
2086
- )),
2207
+ pivotResult.data[sortedIdx].map((cell, colIdx) => {
2208
+ const displayRowIdx = sortedRowIndices.indexOf(sortedIdx);
2209
+ return /* @__PURE__ */ jsx4(
2210
+ "td",
2211
+ {
2212
+ className: `vpg-data-cell ${isCellSelected(displayRowIdx, colIdx) ? "selected" : ""} ${cell.value === null ? "vpg-is-null" : ""}`,
2213
+ onMouseDown: (e) => handleCellMouseDown(displayRowIdx, colIdx, e),
2214
+ onMouseEnter: () => handleCellMouseEnter(displayRowIdx, colIdx),
2215
+ children: cell.formattedValue
2216
+ },
2217
+ colIdx
2218
+ );
2219
+ }),
2087
2220
  pivotResult.rowTotals[sortedIdx] && /* @__PURE__ */ jsx4("td", { className: "vpg-data-cell vpg-total-cell", children: pivotResult.rowTotals[sortedIdx].formattedValue })
2088
2221
  ] }, sortedIdx)),
2089
2222
  pivotResult.columnTotals.length > 0 && /* @__PURE__ */ jsxs4("tr", { className: "vpg-totals-row", children: [
@@ -2093,12 +2226,32 @@ function PivotSkeleton({
2093
2226
  ] })
2094
2227
  ] })
2095
2228
  ] }) }),
2096
- isConfigured && pivotResult && /* @__PURE__ */ jsx4("div", { className: "vpg-skeleton-footer", children: /* @__PURE__ */ jsxs4("span", { children: [
2097
- pivotResult.rowHeaders.length,
2098
- " rows \xD7 ",
2099
- pivotResult.data[0]?.length || 0,
2100
- " columns"
2101
- ] }) })
2229
+ isConfigured && pivotResult && /* @__PURE__ */ jsxs4("div", { className: "vpg-skeleton-footer", children: [
2230
+ /* @__PURE__ */ jsxs4("span", { className: "vpg-footer-info", children: [
2231
+ pivotResult.rowHeaders.length,
2232
+ " rows \xD7 ",
2233
+ pivotResult.data[0]?.length || 0,
2234
+ " columns"
2235
+ ] }),
2236
+ selectionStats && selectionStats.count > 1 && /* @__PURE__ */ jsxs4("div", { className: "vpg-selection-stats", children: [
2237
+ /* @__PURE__ */ jsxs4("span", { className: "vpg-stat", children: [
2238
+ /* @__PURE__ */ jsx4("span", { className: "vpg-stat-label", children: "Count:" }),
2239
+ /* @__PURE__ */ jsx4("span", { className: "vpg-stat-value", children: selectionStats.count })
2240
+ ] }),
2241
+ selectionStats.numericCount > 0 && /* @__PURE__ */ jsxs4(Fragment2, { children: [
2242
+ /* @__PURE__ */ jsx4("span", { className: "vpg-stat-divider", children: "|" }),
2243
+ /* @__PURE__ */ jsxs4("span", { className: "vpg-stat", children: [
2244
+ /* @__PURE__ */ jsx4("span", { className: "vpg-stat-label", children: "Sum:" }),
2245
+ /* @__PURE__ */ jsx4("span", { className: "vpg-stat-value", children: formatStatValue(selectionStats.sum) })
2246
+ ] }),
2247
+ /* @__PURE__ */ jsx4("span", { className: "vpg-stat-divider", children: "|" }),
2248
+ /* @__PURE__ */ jsxs4("span", { className: "vpg-stat", children: [
2249
+ /* @__PURE__ */ jsx4("span", { className: "vpg-stat-label", children: "Avg:" }),
2250
+ /* @__PURE__ */ jsx4("span", { className: "vpg-stat-value", children: formatStatValue(selectionStats.avg) })
2251
+ ] })
2252
+ ] })
2253
+ ] })
2254
+ ] })
2102
2255
  ] }),
2103
2256
  showWatermark && canUsePivot && /* @__PURE__ */ jsx4("div", { className: `vpg-watermark ${isDemo ? "vpg-demo-mode" : ""}`, children: isDemo ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
2104
2257
  /* @__PURE__ */ jsx4("span", { className: "vpg-demo-badge", children: "DEMO" }),
@@ -2263,7 +2416,7 @@ function DataGrid({
2263
2416
  const end = start + pageSize;
2264
2417
  return searchFilteredData.slice(start, end);
2265
2418
  }, [enablePagination, searchFilteredData, currentPage, pageSize]);
2266
- useEffect5(() => {
2419
+ useEffect6(() => {
2267
2420
  setCurrentPage(1);
2268
2421
  }, [columnFilters, globalSearchTerm]);
2269
2422
  const selectionBounds = useMemo9(() => {
@@ -2301,7 +2454,7 @@ function DataGrid({
2301
2454
  const avg = sum / values.length;
2302
2455
  return { count, sum, avg, numericCount: values.length };
2303
2456
  }, [selectionBounds, rows, columnKeys]);
2304
- useEffect5(() => {
2457
+ useEffect6(() => {
2305
2458
  if (data.length === 0) return;
2306
2459
  const widths = {};
2307
2460
  const sampleSize = Math.min(100, data.length);
@@ -2332,7 +2485,7 @@ function DataGrid({
2332
2485
  },
2333
2486
  [enableColumnResize, columnWidths]
2334
2487
  );
2335
- useEffect5(() => {
2488
+ useEffect6(() => {
2336
2489
  if (!resizingColumnId) return;
2337
2490
  const handleResizeMove = (event) => {
2338
2491
  const diff = event.clientX - resizeStartX;
@@ -2362,7 +2515,7 @@ function DataGrid({
2362
2515
  },
2363
2516
  [enableVerticalResize, gridHeight]
2364
2517
  );
2365
- useEffect5(() => {
2518
+ useEffect6(() => {
2366
2519
  if (!isResizingVertically) return;
2367
2520
  const handleVerticalResizeMove = (event) => {
2368
2521
  const diff = event.clientY - verticalResizeStartY;
@@ -2480,12 +2633,12 @@ function DataGrid({
2480
2633
  },
2481
2634
  [isSelecting]
2482
2635
  );
2483
- useEffect5(() => {
2636
+ useEffect6(() => {
2484
2637
  const handleMouseUp = () => setIsSelecting(false);
2485
2638
  document.addEventListener("mouseup", handleMouseUp);
2486
2639
  return () => document.removeEventListener("mouseup", handleMouseUp);
2487
2640
  }, []);
2488
- useEffect5(() => {
2641
+ useEffect6(() => {
2489
2642
  const handleKeydown = (event) => {
2490
2643
  if ((event.ctrlKey || event.metaKey) && event.key === "c" && selectionBounds) {
2491
2644
  event.preventDefault();