386 lines
12 KiB
JavaScript
386 lines
12 KiB
JavaScript
import React from 'react';
|
||
import ColumnResizer from './ColumnResizer';
|
||
|
||
const getSortedColumns = (table) => {
|
||
const allLeafColumns = table.getAllLeafColumns();
|
||
const pinnedColumns = table.getState().columnPinning.left || [];
|
||
|
||
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 calculateLeftOffset = (table, columnOrHeader) => {
|
||
const columnId =
|
||
columnOrHeader.id.match(/data\..*$/)?.[0] || columnOrHeader.id;
|
||
const pinnedColumns = table.getState().columnPinning.left || [];
|
||
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 = table.getState().columnPinning.left;
|
||
//в header.id содержаться колонки по типу depth_idColParent_idChild, а в pinningColumn только idCol, которые закреплены
|
||
//header.id.split('_').at(-1) - получение самой нижней колонки
|
||
const columnId = header.id.match(/data\..*$/)?.[0] || header.id;
|
||
const isPinned = pinningColumn.includes(columnId);
|
||
return isPinned;
|
||
};
|
||
const ColumnNumbersRow = ({ 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: 0,
|
||
zIndex: (theme) => theme.zIndex.table + 1,
|
||
backgroundColor: 'white',
|
||
}}
|
||
>
|
||
{filteredColumns.map((column) => (
|
||
<ColumnNumberCell
|
||
key={`col-number-${column.id}`}
|
||
column={column}
|
||
table={table}
|
||
/>
|
||
))}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const ColumnNumberCell = ({ column, table }) => {
|
||
const isPinned = isColumnPin(table, column);
|
||
const allLeafColumns = table.getAllLeafColumns();
|
||
const originalIndex = allLeafColumns.findIndex((col) => col.id === column.id);
|
||
const leftOffset = calculateLeftOffset(table, column);
|
||
return (
|
||
<div
|
||
key={`col-number-${column.id}`}
|
||
style={{
|
||
width: column.getSize(),
|
||
minWidth: column.getSize(),
|
||
maxWidth: column.getSize(),
|
||
padding: '2px 6px',
|
||
textAlign: 'center',
|
||
backgroundColor: isPinned ? '#f5f5f5' : '#F3F4F6',
|
||
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: isPinned ? 20 : 'auto',
|
||
}}
|
||
>
|
||
{originalIndex}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const HeaderCell = ({ header, table, onClick, hightlight, onChangeWidth }) => {
|
||
const column = header.column;
|
||
const isPinned = isColumnPin(table, header);
|
||
const leftOffset = calculateLeftOffset(table, header);
|
||
|
||
const customBgColor = column.columnDef?.muiTableHeadCellProps?.sx?.backgroundColor;
|
||
const color = column.columnDef?.muiTableHeadCellProps?.sx?.color || '#000000';
|
||
|
||
const backgroundColor = customBgColor
|
||
? customBgColor
|
||
: (hightlight ? '#cbccce' : 'rgba(243, 244, 246, 1)');
|
||
|
||
const handleChangeWidth = (deltaWidth) => {
|
||
const size = header.getSize();
|
||
onChangeWidth(header, size + deltaWidth);
|
||
};
|
||
|
||
// Вычисляем отступ для текста внутри ячейки
|
||
// Если колонка закреплена, текст должен быть привязан к левому краю ячейки
|
||
// Если колонка не закреплена, текст должен начинаться после всех закрепленных колонок
|
||
const getTextLeftOffset = () => {
|
||
if (isPinned) {
|
||
return '0';
|
||
}
|
||
// Для незакрепленных колонок текст должен начинаться после всех закрепленных
|
||
// Получаем общую ширину всех закрепленных колонок
|
||
const pinnedColumns = table.getState().columnPinning.left || [];
|
||
let totalPinnedWidth = 0;
|
||
for (const pinnedId of pinnedColumns) {
|
||
const pinnedCol = table.getColumn(pinnedId);
|
||
if (pinnedCol) {
|
||
totalPinnedWidth += pinnedCol.getSize();
|
||
}
|
||
}
|
||
return `${totalPinnedWidth}px`;
|
||
};
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
position: isPinned ? 'sticky' : 'relative',
|
||
left: isPinned ? leftOffset : 'auto',
|
||
zIndex: isPinned ? 21 : 'auto',
|
||
}}
|
||
>
|
||
<div style={{ position: 'relative' }}>
|
||
<div
|
||
key={header.id}
|
||
onClick={() => onClick(header)}
|
||
style={{
|
||
width: header.getSize() + 'px',
|
||
minWidth: header.getSize() + 'px',
|
||
maxWidth: header.getSize() + 'px',
|
||
position: 'relative',
|
||
padding: '4px 6px',
|
||
textAlign: 'center',
|
||
fontWeight: 600,
|
||
fontSize: '12px',
|
||
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: getTextLeftOffset(),
|
||
right: 0,
|
||
paddingRight: '0.5rem',
|
||
paddingLeft: '0.5rem',
|
||
backgroundColor: backgroundColor,
|
||
color: color,
|
||
}}
|
||
>
|
||
{header.isPlaceholder ? null : column.columnDef.header}
|
||
</div>
|
||
<ColumnResizer reportDragDeltaX={handleChangeWidth} />
|
||
</div>
|
||
</div>
|
||
</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,
|
||
onHeaderSelect,
|
||
selectHeader,
|
||
onChangeWidth,
|
||
isLoadingData,
|
||
}) => {
|
||
const handleHeaderClick = (header) => {
|
||
onHeaderSelect(header);
|
||
};
|
||
|
||
if (!isLoadingData) {
|
||
return <div></div>
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<div
|
||
style={{
|
||
zIndex: (theme) => theme.zIndex.table,
|
||
position: 'relative',
|
||
width: 'max-content',
|
||
}}
|
||
>
|
||
{/* Строка с номерами колонок */}
|
||
<ColumnNumbersRow table={table} />
|
||
|
||
{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}
|
||
hightlight={
|
||
selectHeader ? header.id == selectHeader.id : false
|
||
}
|
||
table={table}
|
||
onClick={handleHeaderClick}
|
||
onChangeWidth={onChangeWidth}
|
||
/>
|
||
</React.Fragment>
|
||
))}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</>
|
||
);
|
||
};
|