453 lines
13 KiB
JavaScript
453 lines
13 KiB
JavaScript
import React from 'react';
|
||
import ColumnResizer from './ColumnResizer';
|
||
import { columnPinningDefault } from '../constants/columnConfig';
|
||
|
||
const getPinnedColumnIds = (table) =>
|
||
(table.getState().columnPinning.left || []).filter(Boolean);
|
||
|
||
const getSortedColumns = (table) => {
|
||
const allLeafColumns = table.getAllLeafColumns();
|
||
const pinnedColumns = getPinnedColumnIds(table);
|
||
|
||
return [...allLeafColumns].sort((a, b) => {
|
||
const aIsPinned = pinnedColumns.includes(a.id);
|
||
const bIsPinned = pinnedColumns.includes(b.id);
|
||
|
||
if (aIsPinned && bIsPinned) {
|
||
return pinnedColumns.indexOf(a.id) - pinnedColumns.indexOf(b.id);
|
||
}
|
||
if (aIsPinned) return -1;
|
||
if (bIsPinned) return 1;
|
||
return 0;
|
||
});
|
||
};
|
||
|
||
const getColumnId = (header) => {
|
||
//в header.id содержаться колонки по типу depth_idColParent_idChild, а в pinningColumn только idCol, которые закреплены
|
||
//header.id.split('_').at(-1) - получение самой нижней колонки
|
||
let columnId = header?.id?.match(/data\..*$/)?.[0] || header?.id || header;
|
||
columnPinningDefault.left.forEach(colPinned => {
|
||
if (columnId.includes(colPinned)) {
|
||
columnId = colPinned;
|
||
}
|
||
})
|
||
return columnId;
|
||
}
|
||
|
||
// Ширина закреплённых дочерних колонок группового заголовка
|
||
const getPinnedColumnsWidth = (header, pinningColumn) => {
|
||
let width = 0;
|
||
for (const leaf of header.getLeafHeaders()) {
|
||
if (pinningColumn.includes(leaf.column.id)) {
|
||
width += leaf.column.getSize();
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
return width;
|
||
};
|
||
|
||
const calculateLeftOffset = (table, columnOrHeader) => {
|
||
const columnId = getColumnId(columnOrHeader);
|
||
const pinnedColumns = getPinnedColumnIds(table);
|
||
const currentPinnedIndex = pinnedColumns.indexOf(columnId);
|
||
|
||
let leftOffset = 0;
|
||
|
||
if (currentPinnedIndex > 0) {
|
||
for (let i = 0; i < currentPinnedIndex; i++) {
|
||
const prevColumn = table.getColumn(pinnedColumns[i]);
|
||
if (prevColumn) {
|
||
leftOffset += prevColumn.getSize();
|
||
}
|
||
}
|
||
}
|
||
|
||
return leftOffset;
|
||
};
|
||
|
||
const isColumnPin = (table, header) => {
|
||
const pinningColumn = getPinnedColumnIds(table);
|
||
const columnId = getColumnId(header);
|
||
const isPinned = pinningColumn.includes(columnId);
|
||
return isPinned;
|
||
};
|
||
|
||
const ColumnNumbersRow = ({ table, selectedColumnId, onColumnSelect }) => {
|
||
const sortedColumns = getSortedColumns(table);
|
||
const tableState = table.getState();
|
||
const filteredColumns = sortedColumns.filter(
|
||
(col) =>
|
||
!(col.id in tableState.columnVisibility) ||
|
||
(col.id in tableState.columnVisibility &&
|
||
tableState.columnVisibility[col.id]),
|
||
);
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
position: 'sticky',
|
||
top: 0,
|
||
zIndex: (theme) => theme.zIndex.table + 1,
|
||
backgroundColor: 'white',
|
||
}}
|
||
>
|
||
{filteredColumns.map((column) => (
|
||
<ColumnNumberCell
|
||
key={`col-number-${column.id}`}
|
||
column={column}
|
||
table={table}
|
||
selectedColumnId={selectedColumnId}
|
||
onColumnSelect={onColumnSelect}
|
||
/>
|
||
))}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const ColumnNumberCell = ({ column, table, selectedColumnId, onColumnSelect }) => {
|
||
const isPinned = isColumnPin(table, column);
|
||
const allLeafColumns = table.getAllLeafColumns();
|
||
const originalIndex = allLeafColumns.findIndex((col) => col.id === column.id);
|
||
const leftOffset = calculateLeftOffset(table, column);
|
||
const selected = selectedColumnId === column.id;
|
||
|
||
return (
|
||
<div
|
||
key={`col-number-${column.id}`}
|
||
data-col-select-id={column.id}
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
onColumnSelect?.(column.id);
|
||
}}
|
||
onMouseDown={(e) => e.stopPropagation()}
|
||
style={{
|
||
width: column.getSize(),
|
||
minWidth: column.getSize(),
|
||
maxWidth: column.getSize(),
|
||
padding: '2px 6px',
|
||
textAlign: 'center',
|
||
backgroundColor: selected ? '#589A2F' : '#FBFBFB',
|
||
color: selected ? '#ffffff' : '#5E5E5E',
|
||
boxSizing: 'border-box',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
height: '1.5rem',
|
||
border: '.063rem solid rgba(209, 213, 220, 1)',
|
||
borderBottom: 'none',
|
||
position: isPinned ? 'sticky' : 'relative',
|
||
left: isPinned ? leftOffset : 'auto',
|
||
zIndex: selected ? 16 : isPinned ? 20 : 'auto',
|
||
cursor: 'pointer',
|
||
userSelect: 'none',
|
||
}}
|
||
>
|
||
{originalIndex}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const HeaderCell = ({ header, table, onClick, onChangeWidth }) => {
|
||
const column = header.column;
|
||
const pinningColumn = getPinnedColumnIds(table);
|
||
const isGroup = header.subHeaders?.length > 0;
|
||
const isPinned = isColumnPin(table, header);
|
||
const leftOffset = calculateLeftOffset(table, header);
|
||
const headerSize = header.getSize();
|
||
const pinnedBandWidth = isGroup ? getPinnedColumnsWidth(header, pinningColumn) : 0;
|
||
const splitGroup = isGroup && isPinned && pinnedBandWidth > 0 && pinnedBandWidth < headerSize;
|
||
|
||
const customBgColor = column.columnDef?.muiTableHeadCellProps?.sx?.backgroundColor;
|
||
const color = column.columnDef?.muiTableHeadCellProps?.sx?.color || '#000000';
|
||
|
||
const backgroundColor = customBgColor
|
||
? customBgColor
|
||
: 'rgba(243, 244, 246, 1)';
|
||
|
||
const handleChangeWidth = (deltaWidth) => {
|
||
const size = header.getSize();
|
||
onChangeWidth(header, size + deltaWidth);
|
||
};
|
||
|
||
// Вычисляем отступ для текста внутри ячейки
|
||
// Если колонка закреплена, текст должен быть привязан к левому краю ячейки
|
||
// Если колонка не закреплена, текст должен начинаться после всех закрепленных колонок
|
||
const getTextLeftOffset = () => {
|
||
if (isPinned) {
|
||
return '0';
|
||
}
|
||
// Для незакрепленных колонок текст должен начинаться после всех закрепленных
|
||
// Получаем общую ширину всех закрепленных колонок
|
||
const pinnedColumns = getPinnedColumnIds(table);
|
||
let totalPinnedWidth = 0;
|
||
for (const pinnedId of pinnedColumns) {
|
||
const pinnedCol = table.getColumn(pinnedId);
|
||
if (pinnedCol) {
|
||
totalPinnedWidth += pinnedCol.getSize();
|
||
}
|
||
}
|
||
return `${totalPinnedWidth}px`;
|
||
};
|
||
|
||
const renderCell = (width, textLeft) => (
|
||
<div style={{ position: 'relative' }}>
|
||
<div
|
||
onClick={() => onClick(header)}
|
||
style={{
|
||
width: width + 'px',
|
||
minWidth: width + 'px',
|
||
maxWidth: width + 'px',
|
||
position: 'relative',
|
||
padding: '4px 6px',
|
||
textAlign: 'center',
|
||
fontWeight: 600,
|
||
fontSize: '12px',
|
||
lineHeight: 1,
|
||
backgroundColor: backgroundColor,
|
||
color: '#424242',
|
||
boxSizing: 'border-box',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
height: '3rem',
|
||
border: '.063rem solid rgba(209, 213, 220, 1)',
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
textAlign: 'center',
|
||
position: 'sticky',
|
||
left: textLeft,
|
||
right: 0,
|
||
paddingRight: '0.5rem',
|
||
paddingLeft: '0.5rem',
|
||
backgroundColor: backgroundColor,
|
||
color: color,
|
||
}}
|
||
>
|
||
{header.isPlaceholder ? null : column.columnDef.header}
|
||
</div>
|
||
<ColumnResizer reportDragDeltaX={handleChangeWidth} />
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
if (splitGroup) {
|
||
return (
|
||
<>
|
||
<div style={{ position: 'sticky', left: leftOffset, zIndex: 21, flexShrink: 0 }}>
|
||
{renderCell(pinnedBandWidth, '0')}
|
||
</div>
|
||
<div style={{ position: 'relative', flexShrink: 0 }}>
|
||
{renderCell(headerSize - pinnedBandWidth, getTextLeftOffset(true))}
|
||
</div>
|
||
</>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
position: isPinned ? 'sticky' : 'relative',
|
||
left: isPinned ? leftOffset : 'auto',
|
||
zIndex: isPinned ? 21 : 'auto',
|
||
}}
|
||
>
|
||
{renderCell(headerSize, getTextLeftOffset())}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const FilterRow = ({ table }) => {
|
||
const sortedColumns = getSortedColumns(table);
|
||
const tableState = table.getState();
|
||
|
||
const filteredColumns = sortedColumns.filter(
|
||
(col) =>
|
||
!(col.id in tableState.columnVisibility) ||
|
||
(col.id in tableState.columnVisibility &&
|
||
tableState.columnVisibility[col.id]),
|
||
);
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
position: 'sticky',
|
||
top: '1.5rem',
|
||
zIndex: 20,
|
||
backgroundColor: 'white',
|
||
}}
|
||
>
|
||
{filteredColumns.map((column) => (
|
||
<FilterCell key={`filter-${column.id}`} column={column} table={table} />
|
||
))}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// Компонент ячейки фильтра
|
||
const FilterCell = ({ column, table }) => {
|
||
const isPinned = isColumnPin(table, column);
|
||
const leftOffset = calculateLeftOffset(table, column);
|
||
|
||
// Определяем тип фильтра на основе колонки
|
||
const filterVariant = column.columnDef.filterVariant || 'text';
|
||
|
||
// Для select фильтра
|
||
const selectOptions = column.columnDef.filterSelectOptions || [];
|
||
|
||
const handleFilterChange = (value) => {
|
||
column.setFilterValue(value);
|
||
};
|
||
|
||
// Рендер разных типов фильтров
|
||
const renderFilterInput = () => {
|
||
switch (filterVariant) {
|
||
case 'select':
|
||
return (
|
||
<select
|
||
value={column.getFilterValue() ?? ''}
|
||
onChange={(e) => handleFilterChange(e.target.value)}
|
||
style={{
|
||
width: '100%',
|
||
padding: '4px 6px',
|
||
fontSize: '11px',
|
||
border: '1px solid #ddd',
|
||
borderRadius: '4px',
|
||
backgroundColor: 'white',
|
||
}}
|
||
>
|
||
<option value="">Все</option>
|
||
{selectOptions.map((option) => (
|
||
<option key={option} value={option}>
|
||
{option}
|
||
</option>
|
||
))}
|
||
</select>
|
||
);
|
||
|
||
case 'range': {
|
||
// Для диапазона можно реализовать два поля
|
||
const [min, max] = column.getFilterValue() || ['', ''];
|
||
return (
|
||
<div style={{ display: 'flex', gap: '4px' }}>
|
||
<input
|
||
type="number"
|
||
placeholder="От"
|
||
value={min}
|
||
onChange={(e) => column.setFilterValue([e.target.value, max])}
|
||
style={{ width: '50%', padding: '4px', fontSize: '11px' }}
|
||
/>
|
||
<input
|
||
type="number"
|
||
placeholder="До"
|
||
value={max}
|
||
onChange={(e) => column.setFilterValue([min, e.target.value])}
|
||
style={{ width: '50%', padding: '4px', fontSize: '11px' }}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
default: // 'text'
|
||
return (
|
||
<input
|
||
type="text"
|
||
value={column.getFilterValue() ?? ''}
|
||
onChange={(e) => handleFilterChange(e.target.value)}
|
||
placeholder="Поиск..."
|
||
style={{
|
||
width: '100%',
|
||
padding: '4px 6px',
|
||
fontSize: '11px',
|
||
border: '1px solid #ddd',
|
||
borderRadius: '4px',
|
||
boxSizing: 'border-box',
|
||
}}
|
||
/>
|
||
);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
width: column.getSize(),
|
||
minWidth: column.getSize(),
|
||
maxWidth: column.getSize(),
|
||
padding: '4px 6px',
|
||
backgroundColor: isPinned ? '#f5f5f5' : 'white',
|
||
boxSizing: 'border-box',
|
||
height: '2.5rem',
|
||
border: '.063rem solid rgba(209, 213, 220, 1)',
|
||
borderTop: 'none',
|
||
position: isPinned ? 'sticky' : 'relative',
|
||
left: isPinned ? leftOffset : 'auto',
|
||
zIndex: isPinned ? 19 : 'auto',
|
||
}}
|
||
>
|
||
{renderFilterInput()}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export const TableHead = ({
|
||
table,
|
||
selectedColumnId,
|
||
onColumnSelect,
|
||
onChangeWidth,
|
||
isLoadingData,
|
||
}) => {
|
||
const handleHeaderClick = (header) => {
|
||
if (!header.subHeaders?.length && header.column?.id) {
|
||
onColumnSelect?.(header.column.id);
|
||
}
|
||
};
|
||
|
||
if (!isLoadingData) {
|
||
return <div></div>
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<div
|
||
style={{
|
||
zIndex: (theme) => theme.zIndex.table,
|
||
position: 'relative',
|
||
width: 'max-content',
|
||
}}
|
||
>
|
||
{/* Строка с номерами колонок */}
|
||
<ColumnNumbersRow
|
||
table={table}
|
||
selectedColumnId={selectedColumnId}
|
||
onColumnSelect={onColumnSelect}
|
||
/>
|
||
|
||
{table.getState().showColumnFilters && (
|
||
<FilterRow table={table} />
|
||
)}
|
||
|
||
{/* Существующие строки заголовков */}
|
||
{table.getHeaderGroups().map((headerGroup) => (
|
||
<div key={headerGroup.id} style={{ display: 'flex' }}>
|
||
{headerGroup.headers.map((header) => (
|
||
<React.Fragment key={header.id}>
|
||
<HeaderCell
|
||
header={header}
|
||
table={table}
|
||
onClick={handleHeaderClick}
|
||
onChangeWidth={onChangeWidth}
|
||
/>
|
||
</React.Fragment>
|
||
))}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</>
|
||
);
|
||
};
|