trotl-table 1.0.2 → 1.0.4

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/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # trotl-table
2
+
3
+ A simple, flexible **Table UI** for React — with rows drag‑and‑drop, context menus, and persistence.
4
+
5
+ ---
6
+ **DEMO:** [https://table.linijart.eu/](https://table.linijart.eu/)
7
+ ---
8
+
9
+ ## 🚀 Installation
10
+
11
+ ```bash
12
+ npm install trotl-table
13
+
14
+
15
+ ⚡ Usage
16
+
17
+ import React, { useState } from "react";
18
+ import { Table } from "trotl-table";
19
+ import "./Table.css";
20
+
21
+ const columns = [
22
+ { header: "ID", accessor: "id" },
23
+ { header: "Name", accessor: "name" },
24
+ { header: "Email", accessor: "email" }
25
+ ];
26
+
27
+ const data = [
28
+ { id: "1", name: "Alice", email: "alice@example.com" },
29
+ { id: "2", name: "Bob", email: "bob@example.com" }
30
+ ];
31
+
32
+ export default function Demo() {
33
+ const [rows] = useState(data);
34
+
35
+ const deleteCallback = async (row) => {
36
+ // replace with real API call
37
+ await new Promise((r) => setTimeout(r, 200));
38
+ return { success: true };
39
+ };
40
+
41
+ const editCallback = (row) => {
42
+ console.log("edit", row);
43
+ };
44
+
45
+ const viewCallback = (row) => {
46
+ console.log("view", row);
47
+ };
48
+
49
+ return (
50
+ <Table
51
+ tableId="demo-grouped"
52
+ columns={columns}
53
+ data={data}
54
+ isGrouped={false}
55
+ groupKey="groupId"
56
+ extraSearchTerm=""
57
+ rowHeight={40}
58
+ groupHeaderHeight={40}
59
+ showKey={true}
60
+ enableDragRow={true}
61
+ selectedRowsCallback={(arr) => console.log("selected", arr)}
62
+ deleteCallback={deleteCallback}
63
+ editCallback={editCallback}
64
+ viewCallback={viewCallback}
65
+ buttons={["view", "edit", "delete"]}
66
+ />
67
+ );
68
+ }
69
+
70
+
71
+ 🛠 Features
72
+ - Row selection with context‑aware state
73
+ - Sorting (asc/desc cycle per column)
74
+ - Grouped rows with expandable headers
75
+ - Search + highlight across all columns
76
+ - Drag‑and‑drop rows with auto‑scroll support
77
+ - Callbacks for view, edit, delete actions
78
+ - Context API for global table management
79
+ - Groupe tables
80
+
81
+ 📚 API Reference
82
+ <Table /> Props
83
+ - tableId: unique string ID for the table
84
+ - columns: array of { header, accessor }
85
+ - data: array of row objects
86
+ - isGrouped: enable grouped mode (true/false)
87
+ - groupKey: key to group rows by
88
+ - extraSearchTerm: string to filter + highlight rows
89
+ - rowHeight: row height in px
90
+ - groupHeaderHeight: group header height in px
91
+ - showKey: show row key (true/false)
92
+ - enableDragRow: enable drag‑and‑drop (true/false)
93
+ - selectedRowsCallback: callback with selected row IDs
94
+ - editCallback: callback when editing a row
95
+ - viewCallback: callback when viewing a row
96
+ - deleteCallback: async callback for deleting a row
97
+ - buttons: array of action buttons to show
98
+
99
+ 🌟 Context Hooks
100
+ Use useTable to access global state:
101
+
102
+ import { useTable } from "trotltable";
103
+
104
+ function Toolbar({ tableId }) {
105
+ const { selectedRows, clearSelection } = useTable();
106
+
107
+ return (
108
+ <div>
109
+ <span>Selected: {selectedRows[tableId]?.length || 0}</span>
110
+ <button onClick={() => clearSelection(tableId)}>Clear</button>
111
+ </div>
112
+ );
113
+ }
114
+
115
+ 🎨 Styling
116
+
117
+ .highlight {
118
+ background-color: yellow;
119
+ font-weight: bold;
120
+ }
121
+
122
+
123
+ 🏆 License
124
+ MIT — free to use, modify, and share.
125
+ ---
126
+
package/dist/index.cjs.js CHANGED
@@ -9832,8 +9832,10 @@ function Table({
9832
9832
  sorting,
9833
9833
  setSort
9834
9834
  } = ctx;
9835
- const tableSelected = selectedRows[tableId] || [];
9836
- const tableSort = sorting[tableId] || null;
9835
+ const tableSelected = React.useMemo(() => selectedRows[tableId] || [], [selectedRows, tableId]);
9836
+ console.log(extraSearchTerm);
9837
+ const [localData, setLocalData] = React.useState(data);
9838
+ React.useEffect(() => setLocalData(data), [data]);
9837
9839
  const listRef = React.useRef(null);
9838
9840
  React.useEffect(() => {
9839
9841
  registerTable(tableId, {
@@ -9848,39 +9850,84 @@ function Table({
9848
9850
  });
9849
9851
  }, [data.length]);
9850
9852
  const normalizedGroups = React.useMemo(() => {
9851
- return normalizeData(isGrouped ? data : [], isGrouped ? [] : data);
9852
- }, [data, isGrouped]);
9853
+ return normalizeData(isGrouped ? localData : [], isGrouped ? [] : localData);
9854
+ }, [localData, isGrouped]);
9855
+ const filterRows = React.useCallback(rows => {
9856
+ if (!extraSearchTerm) return rows;
9857
+ const query = extraSearchTerm.toLowerCase();
9858
+ return rows.filter(row => columns.some(col => {
9859
+ const val = row[col.accessor];
9860
+ if (val == null) return false;
9861
+ return String(val).toLowerCase().includes(query);
9862
+ }));
9863
+ }, [extraSearchTerm, columns]);
9864
+
9865
+ // Natural-ish comparator: handles nulls, numbers, strings, ISO dates
9866
+ const compare = (x, y, dir = "asc") => {
9867
+ const nullCmp = () => {
9868
+ if (x == null && y == null) return 0;
9869
+ if (x == null) return 1; // nulls last
9870
+ if (y == null) return -1;
9871
+ return null;
9872
+ };
9873
+ const n = nullCmp();
9874
+ if (n !== null) return n;
9875
+
9876
+ // Try numeric first
9877
+ const nx = typeof x === "number" ? x : Number.isFinite(Number(x)) ? Number(x) : null;
9878
+ const ny = typeof y === "number" ? y : Number.isFinite(Number(y)) ? Number(y) : null;
9879
+ if (nx !== null && ny !== null) {
9880
+ return dir === "asc" ? nx - ny : ny - nx;
9881
+ }
9882
+
9883
+ // Try date (ISO or parseable strings)
9884
+ const dx = typeof x === "string" ? Date.parse(x) : NaN;
9885
+ const dy = typeof y === "string" ? Date.parse(y) : NaN;
9886
+ if (!Number.isNaN(dx) && !Number.isNaN(dy)) {
9887
+ return dir === "asc" ? dx - dy : dy - dx;
9888
+ }
9889
+
9890
+ // Fallback string compare
9891
+ const sx = String(x).toLowerCase();
9892
+ const sy = String(y).toLowerCase();
9893
+ if (sx < sy) return dir === "asc" ? -1 : 1;
9894
+ if (sx > sy) return dir === "asc" ? 1 : -1;
9895
+ return 0;
9896
+ };
9853
9897
  const sortedGroupedData = React.useMemo(() => {
9854
- if (!tableSort?.column) return normalizedGroups;
9855
- const sortKey = tableSort.column;
9856
- const direction = tableSort.direction;
9857
- const sortFn = rows => {
9898
+ const sortKey = sorting[tableId]?.column;
9899
+ const direction = sorting[tableId]?.direction || "asc";
9900
+ const sortRows = rows => {
9858
9901
  const arr = [...rows];
9859
- arr.sort((a, b) => {
9860
- const x = a[sortKey];
9861
- const y = b[sortKey];
9862
- if (x == null) return 1;
9863
- if (y == null) return -1;
9864
- const ax = typeof x === "string" ? x.toLowerCase() : x;
9865
- const ay = typeof y === "string" ? y.toLowerCase() : y;
9866
- if (ax < ay) return direction === "asc" ? -1 : 1;
9867
- if (ax > ay) return direction === "asc" ? 1 : -1;
9868
- return 0;
9869
- });
9870
- return arr;
9902
+ if (!sortKey) return filterRows(arr); // fallback to drag order
9903
+ arr.sort((a, b) => compare(a?.[sortKey], b?.[sortKey], direction));
9904
+ return filterRows(arr);
9871
9905
  };
9872
9906
  return normalizedGroups.map(g => ({
9873
9907
  ...g,
9874
- rows: sortFn(g.rows)
9908
+ rows: sortRows(g.rows || [])
9875
9909
  }));
9876
- }, [normalizedGroups, tableSort]);
9910
+ }, [normalizedGroups, sorting, tableId, filterRows]);
9877
9911
  const [expandedGroups, setExpandedGroups] = React.useState({});
9878
9912
  const toggleGroup = gid => {
9879
- setExpandedGroups(p => ({
9880
- ...p,
9881
- [gid]: !p[gid]
9913
+ setExpandedGroups(prev => ({
9914
+ ...prev,
9915
+ [gid]: !Boolean(prev[gid])
9882
9916
  }));
9883
9917
  };
9918
+ React.useEffect(() => {
9919
+ // Pre-populate expansion state for all groups
9920
+ setExpandedGroups(prev => {
9921
+ const next = {
9922
+ ...prev
9923
+ };
9924
+ for (const g of normalizedGroups) {
9925
+ const gid = g[groupKey] ?? g.groupId ?? g.groupName;
9926
+ if (!(gid in next)) next[gid] = true; // default expanded
9927
+ }
9928
+ return next;
9929
+ });
9930
+ }, [normalizedGroups, groupKey]);
9884
9931
  const groupRowsById = React.useMemo(() => {
9885
9932
  const map = {};
9886
9933
  for (const g of sortedGroupedData) {
@@ -9892,7 +9939,8 @@ function Table({
9892
9939
  const [tableDataFlat, setTableDataFlat] = React.useState([]);
9893
9940
  const flattened = React.useMemo(() => {
9894
9941
  const items = [];
9895
- sortedGroupedData.forEach(g => {
9942
+ // Use the sortedGroupedData here so the flattened list reflects current sorting and filtering
9943
+ for (const g of sortedGroupedData) {
9896
9944
  const gid = g[groupKey] ?? g.groupId ?? g.groupName;
9897
9945
  const expanded = expandedGroups[gid] ?? true;
9898
9946
  if (isGrouped) {
@@ -9900,22 +9948,28 @@ function Table({
9900
9948
  type: "group",
9901
9949
  groupId: gid,
9902
9950
  groupName: g.groupName,
9903
- rowCount: g.rows.length,
9951
+ rowCount: (g.rows || []).length,
9904
9952
  expanded
9905
9953
  });
9906
9954
  }
9907
9955
  if (!isGrouped || expanded) {
9908
- g.rows.forEach(row => items.push({
9909
- type: "row",
9910
- groupId: gid,
9911
- row
9912
- }));
9956
+ for (const row of g.rows) {
9957
+ items.push({
9958
+ type: "row",
9959
+ groupId: gid,
9960
+ row
9961
+ });
9962
+ }
9913
9963
  }
9914
- });
9964
+ }
9915
9965
  return items;
9916
9966
  }, [sortedGroupedData, expandedGroups, isGrouped, groupKey]);
9917
- React.useEffect(() => setTableDataFlat(flattened), [flattened]);
9918
- React.useEffect(() => selectedRowsCallback(tableSelected), [tableSelected]);
9967
+ React.useEffect(() => {
9968
+ setTableDataFlat(flattened);
9969
+ }, [flattened]);
9970
+
9971
+ // useEffect(() => setTableDataFlat(flattened), [flattened]);
9972
+ React.useEffect(() => selectedRowsCallback(tableSelected), [tableSelected, selectedRowsCallback]);
9919
9973
 
9920
9974
  // DELETE
9921
9975
  const [showConfirm, setShowConfirm] = React.useState(false);
@@ -9924,19 +9978,25 @@ function Table({
9924
9978
  if (!pendingDelete) return;
9925
9979
  if (deleteCallback) {
9926
9980
  const res = await deleteCallback(pendingDelete);
9927
- if (res?.success) {
9928
- setTableDataFlat(prev => prev.filter(i => !(i.type === "row" && i.row.id === pendingDelete.id)));
9929
- refreshTrigger();
9930
- }
9931
- } else {
9932
- setTableDataFlat(prev => prev.filter(i => !(i.type === "row" && i.row.id === pendingDelete.id)));
9981
+ if (!res?.success) return;
9933
9982
  }
9983
+ setLocalData(prev => {
9984
+ if (isGrouped) {
9985
+ return prev.map(g => ({
9986
+ ...g,
9987
+ rows: (g.rows || []).filter(r => r.id !== pendingDelete.id)
9988
+ }));
9989
+ } else {
9990
+ return (prev || []).filter(r => r.id !== pendingDelete.id);
9991
+ }
9992
+ });
9993
+ refreshTrigger?.();
9934
9994
  setShowConfirm(false);
9935
9995
  setPendingDelete(null);
9936
9996
  };
9937
9997
 
9938
9998
  // RENDER CELL
9939
- const highlight = text => {
9999
+ const highlight = React.useCallback(text => {
9940
10000
  if (!extraSearchTerm || typeof text !== "string") return text;
9941
10001
  const lower = text.toLowerCase();
9942
10002
  const query = extraSearchTerm.toLowerCase();
@@ -9945,30 +10005,63 @@ function Table({
9945
10005
  return /*#__PURE__*/React.createElement(React.Fragment, null, text.slice(0, idx), /*#__PURE__*/React.createElement("span", {
9946
10006
  className: "highlight"
9947
10007
  }, text.slice(idx, idx + query.length)), text.slice(idx + query.length));
9948
- };
9949
- const renderCell = v => {
9950
- if (Array.isArray(v)) {
9951
- return v.map((x, i) => /*#__PURE__*/React.createElement("span", {
9952
- key: i
9953
- }, highlight(String(x)), i < v.length - 1 ? ", " : ""));
9954
- }
9955
- if (v && typeof v === "object") return JSON.stringify(v);
10008
+ }, [extraSearchTerm]);
10009
+ const renderCell = React.useCallback((v, accessor) => {
10010
+ if (accessor === "deadlineISO") {
10011
+ return v ? new Date(v).toLocaleString("sl-SI", {
10012
+ day: "2-digit",
10013
+ month: "2-digit",
10014
+ year: "numeric",
10015
+ hour: "2-digit",
10016
+ minute: "2-digit"
10017
+ }) : "-";
10018
+ }
10019
+ // existing highlight logic for other fields
9956
10020
  return highlight(String(v));
9957
- };
10021
+ }, [highlight]);
9958
10022
  const showView = buttons.includes("view");
9959
10023
  const showEdit = buttons.includes("edit");
9960
10024
  const showDelete = buttons.includes("delete");
9961
10025
  const showActions = showView || showEdit || showDelete;
9962
10026
 
9963
10027
  // MOVE ROW
9964
- const moveRow = (fromIndex, toIndex) => {
9965
- setTableDataFlat(prev => {
9966
- const newData = [...prev];
9967
- const [removed] = newData.splice(fromIndex, 1);
9968
- newData.splice(toIndex, 0, removed);
9969
- return newData;
10028
+ const moveRow = React.useCallback((fromIndex, toIndex) => {
10029
+ const from = tableDataFlat[fromIndex];
10030
+ const to = tableDataFlat[toIndex];
10031
+ if (!from || !to || from.type !== "row" || to.type !== "row") return;
10032
+
10033
+ // Only reorder within the same group to keep grouping consistent
10034
+ if (isGrouped && from.groupId !== to.groupId) return;
10035
+ setLocalData(prev => {
10036
+ if (isGrouped) {
10037
+ // Reorder within the target group
10038
+ const gid = from.groupId;
10039
+ return prev.map(group => {
10040
+ const groupId = group[groupKey] ?? group.groupId ?? group.groupName;
10041
+ if (groupId !== gid) return group;
10042
+ const rows = [...group.rows];
10043
+ const fromPos = rows.findIndex(r => r.id === from.row.id);
10044
+ const toPos = rows.findIndex(r => r.id === to.row.id);
10045
+ if (fromPos === -1 || toPos === -1) return group;
10046
+ const [moved] = rows.splice(fromPos, 1);
10047
+ rows.splice(toPos, 0, moved);
10048
+ return {
10049
+ ...group,
10050
+ rows
10051
+ };
10052
+ });
10053
+ } else {
10054
+ // Ungrouped: reorder the flat list
10055
+ const arr = Array.isArray(prev) ? [...prev] : [];
10056
+ const fromPos = arr.findIndex(r => r.id === from.row.id);
10057
+ const toPos = arr.findIndex(r => r.id === to.row.id);
10058
+ if (fromPos === -1 || toPos === -1) return prev;
10059
+ const [moved] = arr.splice(fromPos, 1);
10060
+ arr.splice(toPos, 0, moved);
10061
+ return arr;
10062
+ }
9970
10063
  });
9971
- };
10064
+ }, [tableDataFlat, isGrouped, groupKey]);
9972
10065
 
9973
10066
  // ROW RENDERER
9974
10067
  const rowRenderer = React.useCallback(({
@@ -10026,13 +10119,25 @@ function Table({
10026
10119
  }, visualIndex), columns.map((col, i) => /*#__PURE__*/React.createElement("div", {
10027
10120
  key: i,
10028
10121
  className: "table-cell"
10029
- }, renderCell(row[col.accessor]))), showActions && /*#__PURE__*/React.createElement("div", {
10122
+ }, renderCell(row[col.accessor], col.accessor))), showActions && /*#__PURE__*/React.createElement("div", {
10030
10123
  className: "table-cell action-cell"
10031
10124
  }, showView && /*#__PURE__*/React.createElement("button", {
10125
+ className: "action-btn-table",
10126
+ style: {
10127
+ background: "#646cffaa"
10128
+ },
10032
10129
  onClick: () => viewCallback(row)
10033
10130
  }, "View"), showEdit && /*#__PURE__*/React.createElement("button", {
10131
+ className: "action-btn-table",
10132
+ style: {
10133
+ background: "#4caf50"
10134
+ },
10034
10135
  onClick: () => editCallback(row)
10035
10136
  }, "Edit"), showDelete && /*#__PURE__*/React.createElement("button", {
10137
+ className: "action-btn-table",
10138
+ style: {
10139
+ background: "#f44336"
10140
+ },
10036
10141
  onClick: e => {
10037
10142
  e.stopPropagation();
10038
10143
  setPendingDelete(row);
@@ -10050,7 +10155,7 @@ function Table({
10050
10155
  }, content);
10051
10156
  }
10052
10157
  return content;
10053
- }, [tableDataFlat, columns, tableSelected, toggleRowSelection, groupRowsById, renderCell, showActions, showDelete, showEdit, showKey, showView, tableId, viewCallback, editCallback, enableDragRow]);
10158
+ }, [tableDataFlat, columns, tableSelected, toggleRowSelection, groupRowsById, renderCell, showActions, showDelete, showEdit, showKey, showView, tableId, viewCallback, editCallback, enableDragRow, moveRow]);
10054
10159
  const rowHeightGetter = ({
10055
10160
  index
10056
10161
  }) => tableDataFlat[index]?.type === "group" ? groupHeaderHeight : rowHeight;
@@ -10090,14 +10195,14 @@ function Table({
10090
10195
  style: {
10091
10196
  cursor: "pointer"
10092
10197
  }
10093
- }, col.header, tableSort?.column === col.accessor && (tableSort.direction === "asc" ? " ↑" : " ↓"))), showActions && /*#__PURE__*/React.createElement("div", {
10198
+ }, col.header, sorting[tableId]?.column === col.accessor && (sorting[tableId].direction === "asc" ? " ↑" : " ↓"))), showActions && /*#__PURE__*/React.createElement("div", {
10094
10199
  className: "table-cell action-cell"
10095
10200
  }, "Action"))), /*#__PURE__*/React.createElement("div", {
10096
10201
  className: "main-table",
10097
10202
  ref: listRef
10098
- }, tableDataFlat.filter(i => i.type === "row").length === 0 ? /*#__PURE__*/React.createElement("div", {
10203
+ }, tableDataFlat.length === 0 ? /*#__PURE__*/React.createElement("div", {
10099
10204
  className: "table-empty"
10100
- }, "No rows") : /*#__PURE__*/React.createElement(AutoSizer, null, ({
10205
+ }, "No items") : /*#__PURE__*/React.createElement(AutoSizer, null, ({
10101
10206
  height,
10102
10207
  width
10103
10208
  }) => /*#__PURE__*/React.createElement(List, {