Files
oranguru/src/Griddy/rendering/TableCell.tsx
2026-02-16 22:48:48 +02:00

178 lines
5.9 KiB
TypeScript

import { Checkbox } from '@mantine/core';
import { type Cell, flexRender } from '@tanstack/react-table';
import { getGriddyColumn } from '../core/columnMapper';
import { CSS, SELECTION_COLUMN_ID } from '../core/constants';
import { useGriddyStore } from '../core/GriddyStore';
import { TreeExpandButton } from '../features/tree/TreeExpandButton';
import styles from '../styles/griddy.module.css';
import { EditableCell } from './EditableCell';
interface TableCellProps<T> {
cell: Cell<T, unknown>;
showGrouping?: boolean;
}
export function TableCell<T>({ cell, showGrouping }: TableCellProps<T>) {
const isSelectionCol = cell.column.id === SELECTION_COLUMN_ID;
const isEditing = useGriddyStore((s) => s.isEditing);
const focusedRowIndex = useGriddyStore((s) => s.focusedRowIndex);
const focusedColumnId = useGriddyStore((s) => s.focusedColumnId);
const setEditing = useGriddyStore((s) => s.setEditing);
const setFocusedColumn = useGriddyStore((s) => s.setFocusedColumn);
const onEditCommit = useGriddyStore((s) => s.onEditCommit);
const tree = useGriddyStore((s) => s.tree);
const treeLoadingNodes = useGriddyStore((s) => s.treeLoadingNodes);
const selection = useGriddyStore((s) => s.selection);
if (isSelectionCol) {
return <RowCheckbox cell={cell} />;
}
const griddyColumn = getGriddyColumn(cell.column);
const rowIndex = cell.row.index;
const columnId = cell.column.id;
const isEditable = (griddyColumn as any)?.editable ?? false;
const isFocusedCell = isEditing && focusedRowIndex === rowIndex && focusedColumnId === columnId;
const handleCommit = async (value: unknown) => {
if (onEditCommit) {
await onEditCommit(cell.row.id, columnId, value);
}
setEditing(false);
setFocusedColumn(null);
};
const handleCancel = () => {
setEditing(false);
setFocusedColumn(null);
};
const handleDoubleClick = () => {
if (isEditable) {
setEditing(true);
setFocusedColumn(columnId);
}
};
const isPinned = cell.column.getIsPinned();
const leftOffset = isPinned === 'left' ? cell.column.getStart('left') : undefined;
const rightOffset = isPinned === 'right' ? cell.column.getAfter('right') : undefined;
const isGrouped = cell.getIsGrouped();
const isAggregated = cell.getIsAggregated();
const isPlaceholder = cell.getIsPlaceholder();
// Tree support
const depth = cell.row.depth;
const canExpand = cell.row.getCanExpand();
const isExpanded = cell.row.getIsExpanded();
const hasSelection = selection?.mode !== 'none';
const columnIndex = cell.column.getIndex();
// First content column is index 0 if no selection, or index 1 if selection enabled
const isFirstColumn = hasSelection ? columnIndex === 1 : columnIndex === 0;
const indentSize = tree?.indentSize ?? 20;
const showTreeButton = tree?.enabled && isFirstColumn && tree?.showExpandIcon !== false;
return (
<div
className={[
styles[CSS.cell],
isPinned === 'left' ? styles['griddy-cell--pinned-left'] : '',
isPinned === 'right' ? styles['griddy-cell--pinned-right'] : '',
]
.filter(Boolean)
.join(' ')}
onDoubleClick={handleDoubleClick}
role="gridcell"
style={{
left: leftOffset !== undefined ? `${leftOffset}px` : undefined,
paddingLeft: isFirstColumn && tree?.enabled ? `${depth * indentSize + 8}px` : undefined,
position: isPinned ? 'sticky' : 'relative',
right: rightOffset !== undefined ? `${rightOffset}px` : undefined,
width: cell.column.getSize(),
zIndex: isPinned ? 1 : 0,
}}
>
{showTreeButton && (
<TreeExpandButton
canExpand={canExpand}
icons={tree?.icons}
isExpanded={isExpanded}
isLoading={treeLoadingNodes.has(cell.row.id)}
onToggle={() => cell.row.toggleExpanded()}
/>
)}
{showGrouping && isGrouped && (
<button
onClick={() => cell.row.toggleExpanded()}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
marginRight: 4,
padding: 0,
}}
>
{cell.row.getIsExpanded() ? '\u25BC' : '\u25B6'}
</button>
)}
{isFocusedCell && isEditable ? (
<EditableCell
cell={cell}
isEditing={isFocusedCell}
onCancelEdit={handleCancel}
onCommitEdit={handleCommit}
/>
) : isGrouped ? (
<>
{flexRender(cell.column.columnDef.cell, cell.getContext())} ({cell.row.subRows.length})
</>
) : isAggregated ? (
flexRender(
cell.column.columnDef.aggregatedCell ?? cell.column.columnDef.cell,
cell.getContext()
)
) : isPlaceholder ? null : (
flexRender(cell.column.columnDef.cell, cell.getContext())
)}
</div>
);
}
function RowCheckbox<T>({ cell }: TableCellProps<T>) {
const row = cell.row;
const isPinned = cell.column.getIsPinned();
const leftOffset = isPinned === 'left' ? cell.column.getStart('left') : undefined;
const rightOffset = isPinned === 'right' ? cell.column.getAfter('right') : undefined;
return (
<div
className={[
styles[CSS.cell],
isPinned === 'left' ? styles['griddy-cell--pinned-left'] : '',
isPinned === 'right' ? styles['griddy-cell--pinned-right'] : '',
]
.filter(Boolean)
.join(' ')}
role="gridcell"
style={{
left: leftOffset !== undefined ? `${leftOffset}px` : undefined,
position: isPinned ? 'sticky' : 'relative',
right: rightOffset !== undefined ? `${rightOffset}px` : undefined,
width: cell.column.getSize(),
zIndex: isPinned ? 1 : 0,
}}
>
<Checkbox
aria-label={`Select row ${row.index + 1}`}
checked={row.getIsSelected()}
disabled={!row.getCanSelect()}
onChange={row.getToggleSelectedHandler()}
onClick={(e) => e.stopPropagation()}
size="xs"
/>
</div>
);
}