Update handleTableCopy.ts

This commit is contained in:
Nate Kelley 2025-09-25 16:24:22 -06:00
parent 0a0cabec8d
commit a214a778ae
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
2 changed files with 110 additions and 29 deletions

View File

@ -153,12 +153,15 @@ export const AppDataGrid: React.FC<TanStackDataGridProps> = React.memo(
// Handle clipboard copy events to preserve table structure
useEffect(() => {
const container = parentRef.current;
if (!container) return;
const copyHandler = (event: ClipboardEvent) => {
handleTableCopy(event, { table, parentRef });
};
document.addEventListener('copy', copyHandler);
return () => document.removeEventListener('copy', copyHandler);
container.addEventListener('copy', copyHandler);
return () => container.removeEventListener('copy', copyHandler);
}, [table]);
return (

View File

@ -1,4 +1,5 @@
import type { Table } from '@tanstack/react-table';
import { CELL_HEIGHT } from '../constants';
export interface HandleTableCopyOptions {
table: Table<Record<string, string | number | Date | null>>;
@ -25,38 +26,115 @@ export function handleTableCopy(event: ClipboardEvent, options: HandleTableCopyO
const selectedText = selection.toString().trim();
if (!selectedText) return;
// Build structured table data from the current table state
// Get the selected range
const range = selection.getRangeAt(0);
// Find all selected cells by checking which td elements intersect with the selection
const selectedCells: { rowIndex: number; columnId: string; value: string }[] = [];
// Get all td elements within the selection range
const walker = document.createTreeWalker(range.commonAncestorContainer, NodeFilter.SHOW_ELEMENT, {
acceptNode: (node) => {
if (node.nodeName === 'TD' && range.intersectsNode(node)) {
return NodeFilter.FILTER_ACCEPT;
}
return NodeFilter.FILTER_SKIP;
},
});
const visibleRows = table.getRowModel().rows;
const selectedTdElements: HTMLTableCellElement[] = [];
let node = walker.nextNode();
while (node) {
selectedTdElements.push(node as HTMLTableCellElement);
node = walker.nextNode();
}
// Map selected TD elements to their row and column data
for (const tdElement of selectedTdElements) {
// Find the row element (tr) that contains this td
const trElement = tdElement.closest('tr');
if (!trElement) continue;
// Find the row index by checking the transform translateY value
const style = trElement.getAttribute('style');
const translateYMatch = style?.match(/translateY\((\d+(?:\.\d+)?)px\)/);
if (!translateYMatch) continue;
const translateY = parseFloat(translateYMatch[1]);
const rowIndex = Math.floor(translateY / CELL_HEIGHT);
const row = visibleRows[rowIndex];
if (!row) continue;
// Find the column index by checking the position of this td within its row
const tdElements = Array.from(trElement.querySelectorAll('td'));
const columnIndex = tdElements.indexOf(tdElement);
const visibleCells = row.getVisibleCells();
const cell = visibleCells[columnIndex];
if (cell) {
const value = cell.getValue();
const stringValue = value !== null && value !== undefined ? String(value) : '';
selectedCells.push({
rowIndex,
columnId: cell.column.id,
value: stringValue,
});
}
}
// If no cells were found, fall back to plain text
if (selectedCells.length === 0) {
return; // Let the browser handle the default copy behavior
}
// Group cells by row and sort by column order
const cellsByRow = new Map<number, Map<string, string>>();
const columnOrder = table.getAllColumns().map((col) => col.id);
// Create tab-separated values for the entire visible data
const tsvData = visibleRows
.map((row) =>
columnOrder
.map((colId) => {
const cell = row.getVisibleCells().find((cell) => cell.column.id === colId);
if (!cell) return '';
const value = cell.getValue();
return value !== null && value !== undefined ? String(value) : '';
})
.join('\t')
)
for (const cell of selectedCells) {
if (!cellsByRow.has(cell.rowIndex)) {
cellsByRow.set(cell.rowIndex, new Map());
}
const rowMap = cellsByRow.get(cell.rowIndex);
if (rowMap) {
rowMap.set(cell.columnId, cell.value);
}
}
// Build TSV data respecting column order
const sortedRowIndices = Array.from(cellsByRow.keys()).sort((a, b) => a - b);
const tsvData = sortedRowIndices
.map((rowIndex) => {
const rowCells = cellsByRow.get(rowIndex);
if (!rowCells) return '';
const selectedColumnIds = Array.from(rowCells.keys());
// Maintain column order for multi-column selections
const orderedColumns = columnOrder.filter((colId) => selectedColumnIds.includes(colId));
return orderedColumns.map((colId) => rowCells.get(colId) || '').join('\t');
})
.join('\n');
// Create HTML table structure
const htmlTable = `<table><tbody>${visibleRows
.map(
(row) =>
`<tr>${columnOrder
.map((colId) => {
const cell = row.getVisibleCells().find((cell) => cell.column.id === colId);
if (!cell) return '<td></td>';
const value = cell.getValue();
const stringValue = value !== null && value !== undefined ? String(value) : '';
return `<td>${stringValue}</td>`;
})
.join('')}</tr>`
)
// Build HTML table structure
const htmlTable = `<table><tbody>${sortedRowIndices
.map((rowIndex) => {
const rowCells = cellsByRow.get(rowIndex);
if (!rowCells) return '';
const selectedColumnIds = Array.from(rowCells.keys());
const orderedColumns = columnOrder.filter((colId) => selectedColumnIds.includes(colId));
return `<tr>${orderedColumns
.map((colId) => `<td>${rowCells.get(colId) || ''}</td>`)
.join('')}</tr>`;
})
.join('')}</tbody></table>`;
// Set clipboard data with both formats