trotl-table 1.0.97 → 1.0.99

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
@@ -127,6 +127,9 @@ export default function Demo() {
127
127
  - `extraSearchTerm`: string to filter + highlight rows
128
128
  - `rowHeight`: row height in px
129
129
  - `groupHeaderHeight`: group header height in px
130
+ - `mobileColumnCount`: number of data columns shown at viewport widths of 768px or less (1 or 2; default `2`). The full row details are shown in a modal when a mobile row is clicked.
131
+ - `mobileRowClick`: optional callback `(row) => {}` invoked instead of opening the built-in details modal when a mobile row is clicked. Clicks on row controls and action buttons keep their existing behavior.
132
+ - On mobile, when `enableMouseRightClick` actions are provided, double-tapping a row opens the same context menu as a right click (a single tap then opens details after a short ~280ms delay).
130
133
  - `showKey`: show row key (true/false)
131
134
  - `enableDragRow`: enable drag‑and‑drop (true/false)
132
135
  - `selectedRowsCallback`: callback with selected row IDs
@@ -174,4 +177,3 @@ function Toolbar({ tableId }) {
174
177
  ## 🏆 License
175
178
 
176
179
  MIT — free to use, modify, and share.
177
-
package/dist/Table.cjs.js CHANGED
@@ -9687,6 +9687,8 @@ function TableInner({
9687
9687
  tableId = "default",
9688
9688
  columns = [],
9689
9689
  data = [],
9690
+ mobileColumnCount = 2,
9691
+ mobileRowClick = null,
9690
9692
  highlightRow = null,
9691
9693
  isGrouped = false,
9692
9694
  groupKey = "groupId",
@@ -9754,6 +9756,8 @@ function TableInner({
9754
9756
  share: "Share",
9755
9757
  duplicate: "Duplicate",
9756
9758
  download: "Download",
9759
+ details: "Details",
9760
+ close: "Close",
9757
9761
  confirmDelete: "Confirm Delete",
9758
9762
  yesDelete: "Yes, Delete",
9759
9763
  areYouSureYouWantToDelete: "Are you sure you want to delete",
@@ -9777,6 +9781,11 @@ function TableInner({
9777
9781
  const duplicateCallbackRef = React.useRef(duplicateCallback);
9778
9782
  const downloadCallbackRef = React.useRef(downloadCallback);
9779
9783
  const undeleteCallbackRef = React.useRef(undeleteCallback);
9784
+ const mobileRowClickRef = React.useRef(mobileRowClick);
9785
+ const lastTapRef = React.useRef({
9786
+ timer: null,
9787
+ rowId: null
9788
+ });
9780
9789
  const onCellChangeRef = React.useRef(onCellChange);
9781
9790
  const cellCallbackRef = React.useRef(cellCallback);
9782
9791
 
@@ -9788,9 +9797,16 @@ function TableInner({
9788
9797
  editCallbackRef.current = editCallback;
9789
9798
  deleteCallbackRef.current = deleteCallback;
9790
9799
  undeleteCallbackRef.current = undeleteCallback;
9800
+ mobileRowClickRef.current = mobileRowClick;
9791
9801
  onCellChangeRef.current = onCellChange;
9792
9802
  cellCallbackRef.current = cellCallback;
9793
9803
  });
9804
+ const [isMobile, setIsMobile] = React.useState(() => typeof window !== "undefined" && window.innerWidth <= 768);
9805
+ React.useEffect(() => {
9806
+ const updateMobileState = () => setIsMobile(window.innerWidth <= 768);
9807
+ window.addEventListener("resize", updateMobileState);
9808
+ return () => window.removeEventListener("resize", updateMobileState);
9809
+ }, []);
9794
9810
 
9795
9811
  // Context menu right click state
9796
9812
  const [contextMenu, setContextMenu] = React.useState({
@@ -9876,6 +9892,22 @@ function TableInner({
9876
9892
  flex: "1 1 0%"
9877
9893
  };
9878
9894
  }, [columnWidths, getColumnKey]);
9895
+ const allColumns = React.useMemo(() => {
9896
+ const composedColumns = [...columns];
9897
+ if (Array.isArray(customColumns)) {
9898
+ customColumns.forEach(col => {
9899
+ if (typeof col.place === "number") {
9900
+ composedColumns.splice(col.place, 0, {
9901
+ ...col,
9902
+ isCustom: true
9903
+ });
9904
+ }
9905
+ });
9906
+ }
9907
+ return composedColumns;
9908
+ }, [columns, customColumns]);
9909
+ const normalizedMobileColumnCount = Math.min(2, Math.max(1, Math.floor(Number(mobileColumnCount) || 2)));
9910
+ const visibleColumns = isMobile ? allColumns.slice(0, normalizedMobileColumnCount) : allColumns;
9879
9911
  const getNestedValue = React.useCallback((row, accessor) => {
9880
9912
  if (!row || accessor == null) return undefined;
9881
9913
  if (Array.isArray(accessor)) {
@@ -10254,6 +10286,21 @@ function TableInner({
10254
10286
  }).filter(Boolean);
10255
10287
  return [...new Set(parts)].join(", ");
10256
10288
  }, []);
10289
+ const formatMobileDetailValue = React.useCallback((value, col) => {
10290
+ if (value == null || value === "") return "...";
10291
+ if (typeof value === "boolean") return value ? "✓" : "✗";
10292
+ if (Array.isArray(value)) return formatArrayValue(value, col) || "...";
10293
+ if (value instanceof Date) {
10294
+ return formatDateValue(value, getColumnDateFormat(col)) || "...";
10295
+ }
10296
+ if (getColumnType(col) === "date" || col?.dateFormat) {
10297
+ return formatDateValue(value, getColumnDateFormat(col)) || String(value);
10298
+ }
10299
+ if (typeof value === "object") {
10300
+ return String(value.label ?? value.name ?? value.value ?? value);
10301
+ }
10302
+ return String(col?.translate ? translate(String(value)) : value);
10303
+ }, [formatArrayValue, translate]);
10257
10304
 
10258
10305
  // console.log(extraSearchTerm)
10259
10306
 
@@ -10568,6 +10615,7 @@ function TableInner({
10568
10615
  // DELETE
10569
10616
  const [showConfirm, setShowConfirm] = React.useState(false);
10570
10617
  const [pendingDelete, setPendingDelete] = React.useState(null);
10618
+ const [mobileDetailRow, setMobileDetailRow] = React.useState(null);
10571
10619
 
10572
10620
  // perform deletion for a given row (shared by immediate & modal-confirm flows)
10573
10621
  const doDelete = React.useCallback(async row => {
@@ -10935,7 +10983,7 @@ function TableInner({
10935
10983
  const showUndelete = buttons.includes("undelete");
10936
10984
  const showDuplicate = buttons.includes("duplicate") || buttons.includes("duplicates");
10937
10985
  const showDownload = buttons.includes("download");
10938
- const showActions = showView || showEdit || showDelete || showShare || showUndelete || showDuplicate || showDownload;
10986
+ const showActions = !isMobile && (showView || showEdit || showDelete || showShare || showUndelete || showDuplicate || showDownload);
10939
10987
  const visibleActionCount = [showView, showEdit, showDuplicate, showDelete, showShare, showUndelete, showDownload].filter(Boolean).length;
10940
10988
  const actionColumnStyle = React.useMemo(() => {
10941
10989
  if (!showActions) return undefined;
@@ -11163,19 +11211,6 @@ function TableInner({
11163
11211
  const visualIndex = tableDataFlat.slice(0, index).filter(i => i.type === "row").length + 1;
11164
11212
  const highlightRowStyle = getHighlightRowStyle(row);
11165
11213
 
11166
- // Compose columns + custom columns
11167
- const allColumns = [...columns];
11168
- // Insert custom columns at specified places
11169
- if (Array.isArray(customColumns)) {
11170
- customColumns.forEach(col => {
11171
- if (typeof col.place === "number") {
11172
- allColumns.splice(col.place, 0, {
11173
- ...col,
11174
- isCustom: true
11175
- });
11176
- }
11177
- });
11178
- }
11179
11214
  // handler called when user double-clicks the non-action portion of the row
11180
11215
  const handleRowDoubleClick = e => {
11181
11216
  if (!doubleClickEnable) return;
@@ -11185,13 +11220,60 @@ function TableInner({
11185
11220
  }
11186
11221
  editCallbackRef.current(row);
11187
11222
  };
11223
+ const handleMobileRowClick = event => {
11224
+ if (!isMobile || event.target.closest(".checkbox-cell, .action-cell, button, input, select, textarea, a, [contenteditable='true']")) {
11225
+ return;
11226
+ }
11227
+ const openSingleTap = () => {
11228
+ if (typeof mobileRowClickRef.current === "function") {
11229
+ mobileRowClickRef.current(row);
11230
+ } else {
11231
+ setMobileDetailRow(row);
11232
+ }
11233
+ };
11234
+ const hasContextActions = Array.isArray(enableMouseRightClick) && enableMouseRightClick.length > 0;
11235
+ if (!hasContextActions) {
11236
+ openSingleTap();
11237
+ return;
11238
+ }
11239
+
11240
+ // Double tap opens the context menu; a single tap waits briefly so
11241
+ // the first tap of a double tap does not open the details modal.
11242
+ const tap = lastTapRef.current;
11243
+ if (tap.timer && tap.rowId === row.id) {
11244
+ window.clearTimeout(tap.timer);
11245
+ lastTapRef.current = {
11246
+ timer: null,
11247
+ rowId: null
11248
+ };
11249
+ setContextMenu({
11250
+ visible: true,
11251
+ x: Math.max(0, Math.min(event.clientX, window.innerWidth - 180)),
11252
+ y: Math.max(0, Math.min(event.clientY, window.innerHeight - 160)),
11253
+ row
11254
+ });
11255
+ return;
11256
+ }
11257
+ window.clearTimeout(tap.timer);
11258
+ lastTapRef.current = {
11259
+ rowId: row.id,
11260
+ timer: window.setTimeout(() => {
11261
+ lastTapRef.current = {
11262
+ timer: null,
11263
+ rowId: null
11264
+ };
11265
+ openSingleTap();
11266
+ }, 280)
11267
+ };
11268
+ };
11188
11269
  const content = /*#__PURE__*/React.createElement("div", {
11189
11270
  key: key,
11190
11271
  style: highlightRowStyle ? {
11191
11272
  ...style,
11192
11273
  ...highlightRowStyle
11193
11274
  } : style,
11194
- className: "table-row",
11275
+ className: isMobile ? "table-row mobile-clickable" : "table-row",
11276
+ onClick: isMobile ? handleMobileRowClick : undefined,
11195
11277
  onContextMenu: e => {
11196
11278
  if (!Array.isArray(enableMouseRightClick) || enableMouseRightClick.length === 0) return;
11197
11279
  e.preventDefault();
@@ -11225,14 +11307,20 @@ function TableInner({
11225
11307
  minWidth: keyWidth,
11226
11308
  maxWidth: keyWidth
11227
11309
  } : undefined
11228
- }, visualIndex), allColumns.map((col, i) => {
11310
+ }, visualIndex), visibleColumns.map((col, i) => {
11229
11311
  const resolvedAccessor = getAccessorKey(row, col.accessor);
11230
11312
  const cellValue = getAccessorValue(row, col.accessor);
11231
11313
  const cellStyle = getCellStyle(col, i);
11232
11314
  return /*#__PURE__*/React.createElement("div", {
11233
11315
  key: i,
11234
11316
  className: "table-cell",
11235
- style: cellStyle,
11317
+ style: isMobile ? {
11318
+ ...cellStyle,
11319
+ flex: "1 1 0%",
11320
+ width: undefined,
11321
+ minWidth: 0,
11322
+ maxWidth: "100%"
11323
+ } : cellStyle,
11236
11324
  onClick: () => triggerCellCallback(row, col)
11237
11325
  }, col.isCustom ? typeof col.render === "function" ? col.render(row, i) : col.render : renderCell(cellValue, resolvedAccessor, row, col));
11238
11326
  })), showActions && /*#__PURE__*/React.createElement("div", {
@@ -11317,7 +11405,7 @@ function TableInner({
11317
11405
  }, content);
11318
11406
  }
11319
11407
  return content;
11320
- }, [tableDataFlat, columns, selectedRows, toggleRowSelection, groupRowsById, renderCell, getCellStyle, showActions, showDelete, showEdit, showKey, showView, translate, enableDragRow, moveRow, enableMultiSelect, rowHeight, tableId, customColumns, doubleClickEnable, keyWidth, actionColumnStyle, showDuplicate, showDownload, showIcons, showShare, getHighlightRowStyle, triggerCellCallback, getAccessorKey, getAccessorValue]);
11408
+ }, [tableDataFlat, visibleColumns, selectedRows, toggleRowSelection, groupRowsById, renderCell, getCellStyle, showActions, showDelete, showEdit, showKey, showView, translate, enableDragRow, moveRow, enableMultiSelect, rowHeight, tableId, customColumns, isMobile, enableMouseRightClick, doubleClickEnable, keyWidth, actionColumnStyle, showDuplicate, showDownload, showIcons, showShare, getHighlightRowStyle, triggerCellCallback, getAccessorKey, getAccessorValue]);
11321
11409
  const rightClickActions = Array.isArray(enableMouseRightClick) ? enableMouseRightClick : [];
11322
11410
  const [hoveredActionIndex, setHoveredActionIndex] = React.useState(null);
11323
11411
  const rowHeightGetter = ({
@@ -11347,19 +11435,6 @@ function TableInner({
11347
11435
  externalDrop(node);
11348
11436
  }
11349
11437
  }, [useExternalDndContext, enableExternalRowDrop, externalDrop]);
11350
-
11351
- // Compose columns + custom columns for header
11352
- const allColumns = [...columns];
11353
- if (Array.isArray(customColumns)) {
11354
- customColumns.forEach(col => {
11355
- if (typeof col.place === "number") {
11356
- allColumns.splice(col.place, 0, {
11357
- ...col,
11358
- isCustom: true
11359
- });
11360
- }
11361
- });
11362
- }
11363
11438
  const tableMarkup = /*#__PURE__*/React.createElement("div", {
11364
11439
  className: "table-container",
11365
11440
  ref: setTableRef,
@@ -11400,14 +11475,20 @@ function TableInner({
11400
11475
  minWidth: keyWidth,
11401
11476
  maxWidth: keyWidth
11402
11477
  } : undefined
11403
- }, "#"), allColumns.map((col, i) => {
11478
+ }, "#"), visibleColumns.map((col, i) => {
11404
11479
  const cellStyle = getCellStyle(col, i);
11405
11480
  const translatedHeader = typeof col.header === "string" ? translate(col.header) : col.header;
11406
11481
  const headerTitle = typeof translatedHeader === "string" ? translatedHeader : undefined;
11407
11482
  return /*#__PURE__*/React.createElement("div", {
11408
11483
  key: i,
11409
11484
  className: "table-cell header-cell",
11410
- style: cellStyle,
11485
+ style: isMobile ? {
11486
+ ...cellStyle,
11487
+ flex: "1 1 0%",
11488
+ width: undefined,
11489
+ minWidth: 0,
11490
+ maxWidth: "100%"
11491
+ } : cellStyle,
11411
11492
  title: headerTitle,
11412
11493
  onClick: col.isCustom ? undefined : () => setSort(col.accessor)
11413
11494
  }, col.isCustom ? translatedHeader ?? "" : translatedHeader, !col.isCustom && isSameAccessor(sorting?.column, col.accessor) && (sorting.direction === "asc" ? " ↑" : " ↓"), /*#__PURE__*/React.createElement("div", {
@@ -11500,7 +11581,22 @@ function TableInner({
11500
11581
  }, typeof contextMenuModal.component === "function" ? /*#__PURE__*/React.createElement(contextMenuModal.component, {
11501
11582
  row: contextMenu.row
11502
11583
  }) : contextMenuModal.component) : null;
11503
- return /*#__PURE__*/React.createElement(React.Fragment, null, tableMarkup, contextMenuMarkup, contextMenuModalMarkup);
11584
+ const mobileDetailModalMarkup = mobileDetailRow ? /*#__PURE__*/React.createElement(Modal.default, {
11585
+ isOpen: true,
11586
+ title: String(mobileDetailRow.name ?? mobileDetailRow.title ?? translate("details")),
11587
+ onCancel: () => setMobileDetailRow(null),
11588
+ cancelLabel: translate("close"),
11589
+ showCancel: true
11590
+ }, /*#__PURE__*/React.createElement("dl", {
11591
+ className: "mobile-row-details"
11592
+ }, allColumns.map((col, index) => {
11593
+ const header = typeof col.header === "string" ? translate(col.header) : col.header;
11594
+ const value = col.isCustom ? typeof col.render === "function" ? col.render(mobileDetailRow, index) : col.render ?? "..." : formatMobileDetailValue(getAccessorValue(mobileDetailRow, col.accessor), col);
11595
+ return /*#__PURE__*/React.createElement(React.Fragment, {
11596
+ key: index
11597
+ }, /*#__PURE__*/React.createElement("dt", null, header), /*#__PURE__*/React.createElement("dd", null, value));
11598
+ }))) : null;
11599
+ return /*#__PURE__*/React.createElement(React.Fragment, null, tableMarkup, contextMenuMarkup, contextMenuModalMarkup, mobileDetailModalMarkup);
11504
11600
  }
11505
11601
  function Table(props) {
11506
11602
  const {