453 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
</>
);
};