178 lines
5.9 KiB
TypeScript
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>
|
|
);
|
|
}
|