trotl-table 1.0.10 → 1.0.12

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 CHANGED
@@ -26,6 +26,7 @@ yarn add trotl-table
26
26
 
27
27
  ## Versions
28
28
  ```js
29
+ 1.0.9 => add search input
29
30
  1.0.9 => fix readme, whatever
30
31
  1.0.8 => fix readme
31
32
  1.0.7 => removed context (better for npm consumers)
package/dist/index.cjs.js CHANGED
@@ -9612,7 +9612,8 @@ const DraggableRow = ({
9612
9612
  index,
9613
9613
  moveRow,
9614
9614
  children,
9615
- item,
9615
+ row,
9616
+ tableId,
9616
9617
  enableDrag,
9617
9618
  listRef
9618
9619
  }) => {
@@ -9621,7 +9622,6 @@ const DraggableRow = ({
9621
9622
  accept: ITEM_TYPE,
9622
9623
  hover(draggedItem, monitor) {
9623
9624
  if (!ref.current || !enableDrag) return;
9624
- ref.current.getBoundingClientRect();
9625
9625
  const clientOffset = monitor.getClientOffset();
9626
9626
 
9627
9627
  // Auto-scroll logic
@@ -9651,23 +9651,31 @@ const DraggableRow = ({
9651
9651
  }, drag] = reactDnd.useDrag({
9652
9652
  type: ITEM_TYPE,
9653
9653
  item: {
9654
- index
9654
+ index,
9655
+ row,
9656
+ sourceTableId: tableId
9655
9657
  },
9656
9658
  canDrag: enableDrag,
9657
9659
  collect: monitor => ({
9658
9660
  isDragging: monitor.isDragging()
9659
9661
  })
9660
9662
  });
9661
- drag(drop(ref));
9663
+ // Compose drag & drop refs in effect to avoid reading ref during render
9664
+ const setRef = React.useCallback(node => {
9665
+ if (node) {
9666
+ ref.current = node;
9667
+ drag(drop(node));
9668
+ }
9669
+ }, [drag, drop]);
9662
9670
  return /*#__PURE__*/React.createElement("div", {
9663
- ref: ref,
9671
+ ref: setRef,
9664
9672
  style: {
9665
9673
  opacity: isDragging ? 0.5 : 1,
9666
9674
  cursor: enableDrag ? "move" : "default"
9667
9675
  }
9668
9676
  }, children);
9669
9677
  };
9670
- function Table({
9678
+ function TableInner({
9671
9679
  tableId = "default",
9672
9680
  columns = [],
9673
9681
  data = [],
@@ -9684,7 +9692,11 @@ function Table({
9684
9692
  deleteCallback = null,
9685
9693
  buttons = ["view", "edit", "delete"],
9686
9694
  enableDragRow = false,
9687
- enableSearchUrlParam = false
9695
+ enableSearchUrlParam = false,
9696
+ enableMultiSelect = false,
9697
+ enableExternalRowDrop = false,
9698
+ onExternalRowDrop = () => {},
9699
+ useExternalDndContext = false
9688
9700
  }) {
9689
9701
  // Local selection & sorting state (removed TableContext dependency)
9690
9702
  const [selectedRows, setSelectedRows] = React.useState([]);
@@ -9693,26 +9705,49 @@ function Table({
9693
9705
  // Option to read search param from URL
9694
9706
  const [searchTerm, setSearchTerm] = React.useState(extraSearchTerm);
9695
9707
 
9696
- // Recognize "entry" in URL search param and set as searchTerm if present
9697
- // Extract window.location.search to a variable for useEffect dependency
9698
- const locationSearch = typeof window !== "undefined" ? window.location.search : "";
9708
+ // Listen to URL changes via popstate (back/forward) and custom events
9699
9709
  React.useEffect(() => {
9700
9710
  if (!enableSearchUrlParam) {
9701
9711
  setSearchTerm(extraSearchTerm);
9702
9712
  return;
9703
9713
  }
9704
- const params = new URLSearchParams(locationSearch);
9705
- const entrySearch = params.get("entry");
9706
- const urlSearch = params.get("search");
9707
- console.log(urlSearch);
9708
- if (entrySearch) {
9709
- setSearchTerm(entrySearch);
9710
- } else if (urlSearch) {
9711
- setSearchTerm(urlSearch);
9712
- } else {
9713
- setSearchTerm(extraSearchTerm);
9714
- }
9715
- }, [locationSearch, enableSearchUrlParam, extraSearchTerm]);
9714
+ const updateSearchFromUrl = () => {
9715
+ const params = new URLSearchParams(window.location.search);
9716
+ const entrySearch = params.get("entry");
9717
+ const urlSearch = params.get("search");
9718
+ if (entrySearch) {
9719
+ setSearchTerm(entrySearch);
9720
+ } else if (urlSearch) {
9721
+ setSearchTerm(urlSearch);
9722
+ } else {
9723
+ setSearchTerm(extraSearchTerm);
9724
+ }
9725
+ };
9726
+
9727
+ // Initial sync
9728
+ updateSearchFromUrl();
9729
+
9730
+ // Listen for URL changes (back/forward navigation)
9731
+ window.addEventListener('popstate', updateSearchFromUrl);
9732
+
9733
+ // Listen for programmatic URL changes if you use history.pushState/replaceState
9734
+ const originalPushState = window.history.pushState;
9735
+ const originalReplaceState = window.history.replaceState;
9736
+ window.history.pushState = function (...args) {
9737
+ originalPushState.apply(this, args);
9738
+ updateSearchFromUrl();
9739
+ };
9740
+ window.history.replaceState = function (...args) {
9741
+ originalReplaceState.apply(this, args);
9742
+ updateSearchFromUrl();
9743
+ };
9744
+ return () => {
9745
+ window.removeEventListener('popstate', updateSearchFromUrl);
9746
+ // Restore original methods
9747
+ window.history.pushState = originalPushState;
9748
+ window.history.replaceState = originalReplaceState;
9749
+ };
9750
+ }, [enableSearchUrlParam, extraSearchTerm]);
9716
9751
  const toggleRowSelection = React.useCallback(rowId => {
9717
9752
  setSelectedRows(prev => prev.includes(rowId) ? prev.filter(r => r !== rowId) : [...prev, rowId]);
9718
9753
  }, []);
@@ -9799,7 +9834,7 @@ function Table({
9799
9834
  const toggleGroup = gid => {
9800
9835
  setExpandedGroups(prev => ({
9801
9836
  ...prev,
9802
- [gid]: !Boolean(prev[gid])
9837
+ [gid]: !prev[gid]
9803
9838
  }));
9804
9839
  };
9805
9840
  React.useEffect(() => {
@@ -9912,40 +9947,90 @@ function Table({
9912
9947
  const showDelete = buttons.includes("delete");
9913
9948
  const showActions = showView || showEdit || showDelete;
9914
9949
 
9915
- // MOVE ROW
9916
- const moveRow = React.useCallback((fromIndex, toIndex) => {
9950
+ // MOVE ROW (internal + between groups)
9951
+ // moveRow: allow toIndex to be null or an object { emptyGroupId } for empty group drop
9952
+ const moveRow = React.useCallback((fromIndex, toIndexOrGroup) => {
9917
9953
  const from = tableDataFlat[fromIndex];
9918
- const to = tableDataFlat[toIndex];
9919
- if (!from || !to || from.type !== "row" || to.type !== "row") return;
9920
-
9921
- // Only reorder within the same group to keep grouping consistent
9922
- if (isGrouped && from.groupId !== to.groupId) return;
9954
+ let to = null;
9955
+ let emptyGroupId = null;
9956
+ if (typeof toIndexOrGroup === 'object' && toIndexOrGroup && toIndexOrGroup.emptyGroupId) {
9957
+ emptyGroupId = toIndexOrGroup.emptyGroupId;
9958
+ } else {
9959
+ to = tableDataFlat[toIndexOrGroup];
9960
+ }
9961
+ if (!from || from.type !== "row" || toIndexOrGroup !== null && !to && !emptyGroupId) return;
9923
9962
  setLocalData(prev => {
9924
9963
  if (isGrouped) {
9925
- // Reorder within the target group
9926
- const gid = from.groupId;
9927
- return prev.map(group => {
9964
+ // Remove from old group
9965
+ let movedRow;
9966
+ const newGroups = prev.map(group => {
9928
9967
  const groupId = group[groupKey] ?? group.groupId ?? group.groupName;
9929
- if (groupId !== gid) return group;
9930
- const rows = [...group.rows];
9931
- const fromPos = rows.findIndex(r => r.id === from.row.id);
9932
- const toPos = rows.findIndex(r => r.id === to.row.id);
9933
- if (fromPos === -1 || toPos === -1) return group;
9934
- const [moved] = rows.splice(fromPos, 1);
9935
- rows.splice(toPos, 0, moved);
9936
- return {
9937
- ...group,
9938
- rows
9939
- };
9968
+ if (groupId === from.groupId) {
9969
+ const rows = [...group.rows];
9970
+ const fromPos = rows.findIndex(r => r.id === from.row.id);
9971
+ if (fromPos !== -1) {
9972
+ [movedRow] = rows.splice(fromPos, 1);
9973
+ }
9974
+ return {
9975
+ ...group,
9976
+ rows
9977
+ };
9978
+ }
9979
+ return group;
9980
+ });
9981
+ if (!movedRow) return prev;
9982
+ // Insert into new group at correct position
9983
+ return newGroups.map(group => {
9984
+ const groupId = group[groupKey] ?? group.groupId ?? group.groupName;
9985
+ // If dropping into empty group
9986
+ if (emptyGroupId && groupId === emptyGroupId) {
9987
+ const rows = [...group.rows];
9988
+ if (groupKey && movedRow[groupKey] !== undefined) {
9989
+ movedRow = {
9990
+ ...movedRow,
9991
+ [groupKey]: groupId
9992
+ };
9993
+ }
9994
+ rows.push(movedRow);
9995
+ return {
9996
+ ...group,
9997
+ rows
9998
+ };
9999
+ }
10000
+ // Normal drop
10001
+ if (to && groupId === to.groupId) {
10002
+ const rows = [...group.rows];
10003
+ const toPos = rows.findIndex(r => r.id === to.row.id);
10004
+ if (groupKey && movedRow[groupKey] !== undefined) {
10005
+ movedRow = {
10006
+ ...movedRow,
10007
+ [groupKey]: groupId
10008
+ };
10009
+ }
10010
+ if (toPos === -1) {
10011
+ rows.push(movedRow);
10012
+ } else {
10013
+ rows.splice(toPos, 0, movedRow);
10014
+ }
10015
+ return {
10016
+ ...group,
10017
+ rows
10018
+ };
10019
+ }
10020
+ return group;
9940
10021
  });
9941
10022
  } else {
9942
10023
  // Ungrouped: reorder the flat list
9943
10024
  const arr = Array.isArray(prev) ? [...prev] : [];
9944
10025
  const fromPos = arr.findIndex(r => r.id === from.row.id);
9945
- const toPos = arr.findIndex(r => r.id === to.row.id);
9946
- if (fromPos === -1 || toPos === -1) return prev;
10026
+ const toPos = to ? arr.findIndex(r => r.id === to.row.id) : -1;
10027
+ if (fromPos === -1 || to && toPos === -1) return prev;
9947
10028
  const [moved] = arr.splice(fromPos, 1);
9948
- arr.splice(toPos, 0, moved);
10029
+ if (to) {
10030
+ arr.splice(toPos, 0, moved);
10031
+ } else {
10032
+ arr.push(moved);
10033
+ }
9949
10034
  return arr;
9950
10035
  }
9951
10036
  });
@@ -9963,12 +10048,81 @@ function Table({
9963
10048
  const gid = item.groupId;
9964
10049
  const rows = groupRowsById[gid] || [];
9965
10050
  const allSelected = rows.length > 0 && rows.every(r => selectedRows.includes(r.id));
10051
+
10052
+ // If expanded and group is empty and drag enabled, make the group header itself a drop target
10053
+ if (item.expanded && rows.length === 0 && enableDragRow) {
10054
+ // For empty group, drop target pushes into this group
10055
+ const GroupHeaderDrop = () => {
10056
+ const ref = React.useRef(null);
10057
+ const [, drop] = reactDnd.useDrop({
10058
+ accept: ITEM_TYPE,
10059
+ drop(draggedItem) {
10060
+ if (draggedItem && typeof draggedItem.index === "number") {
10061
+ moveRow(draggedItem.index, {
10062
+ emptyGroupId: gid
10063
+ });
10064
+ }
10065
+ },
10066
+ canDrop: () => true,
10067
+ collect: monitor => ({
10068
+ isOver: monitor.isOver(),
10069
+ canDrop: monitor.canDrop()
10070
+ })
10071
+ });
10072
+ drop(ref);
10073
+ return /*#__PURE__*/React.createElement("div", {
10074
+ ref: ref,
10075
+ key: key,
10076
+ style: {
10077
+ ...style,
10078
+ background: '#f6f6fa',
10079
+ border: '2px dashed #bbb',
10080
+ textAlign: 'center',
10081
+ color: '#888',
10082
+ minHeight: rowHeight,
10083
+ display: 'flex',
10084
+ alignItems: 'center'
10085
+ },
10086
+ className: "table-row group-row empty-group-drop",
10087
+ onClick: () => toggleGroup(gid)
10088
+ }, enableMultiSelect && showDelete && /*#__PURE__*/React.createElement("div", {
10089
+ className: "table-cell checkbox-cell"
10090
+ }, /*#__PURE__*/React.createElement("input", {
10091
+ type: "checkbox",
10092
+ checked: allSelected,
10093
+ onChange: e => {
10094
+ e.stopPropagation();
10095
+ const ids = rows.map(r => r.id);
10096
+ if (allSelected) {
10097
+ setSelectedRows(prev => prev.filter(id => !ids.includes(id)));
10098
+ } else {
10099
+ setSelectedRows(prev => [...prev, ...ids.filter(id => !prev.includes(id))]);
10100
+ }
10101
+ },
10102
+ onClick: e => e.stopPropagation()
10103
+ })), showKey && /*#__PURE__*/React.createElement("div", {
10104
+ className: "table-cell key-cell"
10105
+ }), /*#__PURE__*/React.createElement("div", {
10106
+ className: "table-cell group-header",
10107
+ style: {
10108
+ width: '100%'
10109
+ }
10110
+ }, item.expanded ? "▾" : "▸", " ", item.groupName, " (0) \u2014 Drop here to move into this group"), columns.slice(1).map((_, i) => /*#__PURE__*/React.createElement("div", {
10111
+ key: i,
10112
+ className: "table-cell"
10113
+ })), showActions && /*#__PURE__*/React.createElement("div", {
10114
+ className: "table-cell action-cell"
10115
+ }));
10116
+ };
10117
+ return /*#__PURE__*/React.createElement(GroupHeaderDrop, null);
10118
+ }
10119
+ // Default: normal group header
9966
10120
  return /*#__PURE__*/React.createElement("div", {
9967
10121
  key: key,
9968
10122
  style: style,
9969
10123
  className: "table-row group-row",
9970
10124
  onClick: () => toggleGroup(gid)
9971
- }, /*#__PURE__*/React.createElement("div", {
10125
+ }, enableMultiSelect && showDelete && /*#__PURE__*/React.createElement("div", {
9972
10126
  className: "table-cell checkbox-cell"
9973
10127
  }, /*#__PURE__*/React.createElement("input", {
9974
10128
  type: "checkbox",
@@ -9976,7 +10130,11 @@ function Table({
9976
10130
  onChange: e => {
9977
10131
  e.stopPropagation();
9978
10132
  const ids = rows.map(r => r.id);
9979
- ids.forEach(id => toggleRowSelection(tableId, id));
10133
+ if (allSelected) {
10134
+ setSelectedRows(prev => prev.filter(id => !ids.includes(id)));
10135
+ } else {
10136
+ setSelectedRows(prev => [...prev, ...ids.filter(id => !prev.includes(id))]);
10137
+ }
9980
10138
  },
9981
10139
  onClick: e => e.stopPropagation()
9982
10140
  })), showKey && /*#__PURE__*/React.createElement("div", {
@@ -9996,7 +10154,7 @@ function Table({
9996
10154
  key: key,
9997
10155
  style: style,
9998
10156
  className: "table-row"
9999
- }, showDelete && /*#__PURE__*/React.createElement("div", {
10157
+ }, enableMultiSelect && showDelete && /*#__PURE__*/React.createElement("div", {
10000
10158
  className: "table-cell checkbox-cell"
10001
10159
  }, /*#__PURE__*/React.createElement("input", {
10002
10160
  type: "checkbox",
@@ -10037,20 +10195,46 @@ function Table({
10037
10195
  key: key,
10038
10196
  index: index,
10039
10197
  moveRow: moveRow,
10040
- item: item,
10198
+ row: row,
10199
+ tableId: tableId,
10041
10200
  enableDrag: true,
10042
10201
  listRef: listRef
10043
10202
  }, content);
10044
10203
  }
10045
10204
  return content;
10046
- }, [tableDataFlat, columns, selectedRows, toggleRowSelection, groupRowsById, renderCell, showActions, showDelete, showEdit, showKey, showView, tableId, viewCallback, editCallback, enableDragRow, moveRow]);
10205
+ }, [tableDataFlat, columns, selectedRows, toggleRowSelection, groupRowsById, renderCell, showActions, showDelete, showEdit, showKey, showView, viewCallback, editCallback, enableDragRow, moveRow, enableMultiSelect, rowHeight, tableId]);
10047
10206
  const rowHeightGetter = ({
10048
10207
  index
10049
10208
  }) => tableDataFlat[index]?.type === "group" ? groupHeaderHeight : rowHeight;
10050
- return /*#__PURE__*/React.createElement(reactDnd.DndProvider, {
10051
- backend: reactDndHtml5Backend.HTML5Backend
10052
- }, /*#__PURE__*/React.createElement("div", {
10053
- className: "table-container"
10209
+
10210
+ // Table-level drop target only when using external DnD context
10211
+ const tableContainerRef = React.useRef(null);
10212
+ const [{
10213
+ isOver: isExternalOver,
10214
+ canDrop: canExternalDrop
10215
+ }, externalDrop] = reactDnd.useDrop({
10216
+ accept: ITEM_TYPE,
10217
+ drop: draggedItem => {
10218
+ if (!useExternalDndContext) return; // safety
10219
+ if (!enableExternalRowDrop || isGrouped) return;
10220
+ if (!draggedItem || draggedItem.sourceTableId === tableId) return;
10221
+ onExternalRowDrop(draggedItem.row, draggedItem.sourceTableId, tableId);
10222
+ }
10223
+ });
10224
+ const setTableRef = React.useCallback(node => {
10225
+ tableContainerRef.current = node;
10226
+ if (node && useExternalDndContext && enableExternalRowDrop) {
10227
+ externalDrop(node);
10228
+ }
10229
+ }, [useExternalDndContext, enableExternalRowDrop, externalDrop]);
10230
+ const tableMarkup = /*#__PURE__*/React.createElement("div", {
10231
+ className: "table-container",
10232
+ ref: setTableRef,
10233
+ style: useExternalDndContext && enableExternalRowDrop ? {
10234
+ transition: 'background 0.15s',
10235
+ background: isExternalOver && canExternalDrop ? 'rgba(100,108,255,0.15)' : undefined,
10236
+ outline: isExternalOver && canExternalDrop ? '2px dashed #646cff' : undefined
10237
+ } : undefined
10054
10238
  }, /*#__PURE__*/React.createElement("div", {
10055
10239
  className: "table-header",
10056
10240
  style: {
@@ -10061,7 +10245,7 @@ function Table({
10061
10245
  }
10062
10246
  }, /*#__PURE__*/React.createElement("div", {
10063
10247
  className: "table-row header-row"
10064
- }, showDelete && /*#__PURE__*/React.createElement("div", {
10248
+ }, enableMultiSelect && showDelete && /*#__PURE__*/React.createElement("div", {
10065
10249
  className: "table-cell checkbox-cell"
10066
10250
  }, /*#__PURE__*/React.createElement("input", {
10067
10251
  type: "checkbox",
@@ -10109,7 +10293,20 @@ function Table({
10109
10293
  cancelLabel: "Cancel",
10110
10294
  showCancel: true,
10111
10295
  showConfirm: true
10112
- }, /*#__PURE__*/React.createElement("p", null, "Are you sure you want to delete ", /*#__PURE__*/React.createElement("strong", null, pendingDelete?.name || pendingDelete?.id), "?"))));
10296
+ }, /*#__PURE__*/React.createElement("p", null, "Are you sure you want to delete ", /*#__PURE__*/React.createElement("strong", null, pendingDelete?.name || pendingDelete?.id), "?")));
10297
+ return tableMarkup;
10298
+ }
10299
+ function Table(props) {
10300
+ const {
10301
+ useExternalDndContext = false
10302
+ } = props;
10303
+ if (useExternalDndContext) {
10304
+ return /*#__PURE__*/React.createElement(TableInner, props);
10305
+ }
10306
+ // Provide DnD context when not supplied externally
10307
+ return /*#__PURE__*/React.createElement(reactDnd.DndProvider, {
10308
+ backend: reactDndHtml5Backend.HTML5Backend
10309
+ }, /*#__PURE__*/React.createElement(TableInner, props));
10113
10310
  }
10114
10311
 
10115
10312
  // src/index.js