mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
optimize item table
This commit is contained in:
@@ -611,33 +611,52 @@ export const TableColumnTextContainer = (
|
|||||||
: props.rowIndex === props.data.length - 1);
|
: props.rowIndex === props.data.length - 1);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDataRow || !containerRef.current) return;
|
if (!isDataRow || !containerRef.current || !props.enableRowHoverHighlight) return;
|
||||||
|
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
const rowIndex = props.rowIndex;
|
const rowIndex = props.rowIndex;
|
||||||
|
const rowSelector = `[data-row-index="${props.tableId}-${rowIndex}"]`;
|
||||||
|
let rafId: null | number = null;
|
||||||
|
let cachedCells: NodeListOf<Element> | null = null;
|
||||||
|
|
||||||
|
const getCells = () => {
|
||||||
|
if (!cachedCells) {
|
||||||
|
cachedCells = document.querySelectorAll(rowSelector);
|
||||||
|
}
|
||||||
|
return cachedCells;
|
||||||
|
};
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
const handleMouseEnter = () => {
|
||||||
// Find all cells in the same row and add hover class
|
if (rafId !== null) {
|
||||||
const allCells = document.querySelectorAll(
|
cancelAnimationFrame(rafId);
|
||||||
`[data-row-index="${props.tableId}-${rowIndex}"]`,
|
}
|
||||||
);
|
rafId = requestAnimationFrame(() => {
|
||||||
allCells.forEach((cell) => cell.classList.add(styles.rowHovered));
|
const cells = getCells();
|
||||||
|
cells.forEach((cell) => cell.classList.add(styles.rowHovered));
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
const handleMouseLeave = () => {
|
||||||
// Remove hover class from all cells in the same row
|
if (rafId !== null) {
|
||||||
const allCells = document.querySelectorAll(
|
cancelAnimationFrame(rafId);
|
||||||
`[data-row-index="${props.tableId}-${rowIndex}"]`,
|
}
|
||||||
);
|
rafId = requestAnimationFrame(() => {
|
||||||
allCells.forEach((cell) => cell.classList.remove(styles.rowHovered));
|
const cells = getCells();
|
||||||
|
cells.forEach((cell) => cell.classList.remove(styles.rowHovered));
|
||||||
|
cachedCells = null;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
container.addEventListener('mouseenter', handleMouseEnter);
|
container.addEventListener('mouseenter', handleMouseEnter);
|
||||||
container.addEventListener('mouseleave', handleMouseLeave);
|
container.addEventListener('mouseleave', handleMouseLeave);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
if (rafId !== null) {
|
||||||
|
cancelAnimationFrame(rafId);
|
||||||
|
}
|
||||||
container.removeEventListener('mouseenter', handleMouseEnter);
|
container.removeEventListener('mouseenter', handleMouseEnter);
|
||||||
container.removeEventListener('mouseleave', handleMouseLeave);
|
container.removeEventListener('mouseleave', handleMouseLeave);
|
||||||
|
cachedCells = null;
|
||||||
};
|
};
|
||||||
}, [isDataRow, props.rowIndex, props.enableRowHoverHighlight, props.tableId]);
|
}, [isDataRow, props.rowIndex, props.enableRowHoverHighlight, props.tableId]);
|
||||||
|
|
||||||
@@ -647,13 +666,26 @@ export const TableColumnTextContainer = (
|
|||||||
|
|
||||||
const rowIndex = props.rowIndex;
|
const rowIndex = props.rowIndex;
|
||||||
const draggedOverState = props.isDraggedOver;
|
const draggedOverState = props.isDraggedOver;
|
||||||
|
const rowSelector = `[data-row-index="${props.tableId}-${rowIndex}"]`;
|
||||||
|
let rafId: null | number = null;
|
||||||
|
let cachedCells: NodeListOf<Element> | null = null;
|
||||||
|
|
||||||
|
const getCells = () => {
|
||||||
|
if (!cachedCells) {
|
||||||
|
cachedCells = document.querySelectorAll(rowSelector);
|
||||||
|
}
|
||||||
|
return cachedCells;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (rafId !== null) {
|
||||||
|
cancelAnimationFrame(rafId);
|
||||||
|
}
|
||||||
|
|
||||||
|
rafId = requestAnimationFrame(() => {
|
||||||
|
const cells = getCells();
|
||||||
|
|
||||||
if (draggedOverState) {
|
if (draggedOverState) {
|
||||||
// Find all cells in the same row and add dragged over class
|
cells.forEach((cell, index) => {
|
||||||
const allCells = document.querySelectorAll(
|
|
||||||
`[data-row-index="${props.tableId}-${rowIndex}"]`,
|
|
||||||
);
|
|
||||||
allCells.forEach((cell, index) => {
|
|
||||||
if (draggedOverState === 'top') {
|
if (draggedOverState === 'top') {
|
||||||
cell.classList.add(styles.draggedOverTop);
|
cell.classList.add(styles.draggedOverTop);
|
||||||
cell.classList.remove(styles.draggedOverBottom);
|
cell.classList.remove(styles.draggedOverBottom);
|
||||||
@@ -676,15 +708,22 @@ export const TableColumnTextContainer = (
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Remove dragged over classes from all cells in the same row
|
// Remove dragged over classes from all cells in the same row
|
||||||
const allCells = document.querySelectorAll(
|
cells.forEach((cell) => {
|
||||||
`[data-row-index="${props.tableId}-${rowIndex}"]`,
|
|
||||||
);
|
|
||||||
allCells.forEach((cell) => {
|
|
||||||
cell.classList.remove(styles.draggedOverTop);
|
cell.classList.remove(styles.draggedOverTop);
|
||||||
cell.classList.remove(styles.draggedOverBottom);
|
cell.classList.remove(styles.draggedOverBottom);
|
||||||
cell.classList.remove(styles.draggedOverFirstCell);
|
cell.classList.remove(styles.draggedOverFirstCell);
|
||||||
});
|
});
|
||||||
|
// Clear cache when state is cleared
|
||||||
|
cachedCells = null;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (rafId !== null) {
|
||||||
|
cancelAnimationFrame(rafId);
|
||||||
|
}
|
||||||
|
cachedCells = null;
|
||||||
|
};
|
||||||
}, [isDataRow, props.rowIndex, props.isDraggedOver, props.tableId]);
|
}, [isDataRow, props.rowIndex, props.isDraggedOver, props.tableId]);
|
||||||
|
|
||||||
const handleClick = useDoubleClick({
|
const handleClick = useDoubleClick({
|
||||||
@@ -824,33 +863,52 @@ export const TableColumnContainer = (
|
|||||||
: props.rowIndex === props.data.length - 1);
|
: props.rowIndex === props.data.length - 1);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDataRow || !containerRef.current) return;
|
if (!isDataRow || !containerRef.current || !props.enableRowHoverHighlight) return;
|
||||||
|
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
const rowIndex = props.rowIndex;
|
const rowIndex = props.rowIndex;
|
||||||
|
const rowSelector = `[data-row-index="${props.tableId}-${rowIndex}"]`;
|
||||||
|
let rafId: null | number = null;
|
||||||
|
let cachedCells: NodeListOf<Element> | null = null;
|
||||||
|
|
||||||
|
const getCells = () => {
|
||||||
|
if (!cachedCells) {
|
||||||
|
cachedCells = document.querySelectorAll(rowSelector);
|
||||||
|
}
|
||||||
|
return cachedCells;
|
||||||
|
};
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
const handleMouseEnter = () => {
|
||||||
// Find all cells in the same row and add hover class
|
if (rafId !== null) {
|
||||||
const allCells = document.querySelectorAll(
|
cancelAnimationFrame(rafId);
|
||||||
`[data-row-index="${props.tableId}-${rowIndex}"]`,
|
}
|
||||||
);
|
rafId = requestAnimationFrame(() => {
|
||||||
allCells.forEach((cell) => cell.classList.add(styles.rowHovered));
|
const cells = getCells();
|
||||||
|
cells.forEach((cell) => cell.classList.add(styles.rowHovered));
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
const handleMouseLeave = () => {
|
||||||
// Remove hover class from all cells in the same row
|
if (rafId !== null) {
|
||||||
const allCells = document.querySelectorAll(
|
cancelAnimationFrame(rafId);
|
||||||
`[data-row-index="${props.tableId}-${rowIndex}"]`,
|
}
|
||||||
);
|
rafId = requestAnimationFrame(() => {
|
||||||
allCells.forEach((cell) => cell.classList.remove(styles.rowHovered));
|
const cells = getCells();
|
||||||
|
cells.forEach((cell) => cell.classList.remove(styles.rowHovered));
|
||||||
|
cachedCells = null;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
container.addEventListener('mouseenter', handleMouseEnter);
|
container.addEventListener('mouseenter', handleMouseEnter);
|
||||||
container.addEventListener('mouseleave', handleMouseLeave);
|
container.addEventListener('mouseleave', handleMouseLeave);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
if (rafId !== null) {
|
||||||
|
cancelAnimationFrame(rafId);
|
||||||
|
}
|
||||||
container.removeEventListener('mouseenter', handleMouseEnter);
|
container.removeEventListener('mouseenter', handleMouseEnter);
|
||||||
container.removeEventListener('mouseleave', handleMouseLeave);
|
container.removeEventListener('mouseleave', handleMouseLeave);
|
||||||
|
cachedCells = null;
|
||||||
};
|
};
|
||||||
}, [isDataRow, props.rowIndex, props.enableRowHoverHighlight, props.tableId]);
|
}, [isDataRow, props.rowIndex, props.enableRowHoverHighlight, props.tableId]);
|
||||||
|
|
||||||
@@ -860,13 +918,26 @@ export const TableColumnContainer = (
|
|||||||
|
|
||||||
const rowIndex = props.rowIndex;
|
const rowIndex = props.rowIndex;
|
||||||
const draggedOverState = props.isDraggedOver;
|
const draggedOverState = props.isDraggedOver;
|
||||||
|
const rowSelector = `[data-row-index="${props.tableId}-${rowIndex}"]`;
|
||||||
|
let rafId: null | number = null;
|
||||||
|
let cachedCells: NodeListOf<Element> | null = null;
|
||||||
|
|
||||||
|
const getCells = () => {
|
||||||
|
if (!cachedCells) {
|
||||||
|
cachedCells = document.querySelectorAll(rowSelector);
|
||||||
|
}
|
||||||
|
return cachedCells;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (rafId !== null) {
|
||||||
|
cancelAnimationFrame(rafId);
|
||||||
|
}
|
||||||
|
|
||||||
|
rafId = requestAnimationFrame(() => {
|
||||||
|
const cells = getCells();
|
||||||
|
|
||||||
if (draggedOverState) {
|
if (draggedOverState) {
|
||||||
// Find all cells in the same row and add dragged over class
|
cells.forEach((cell, index) => {
|
||||||
const allCells = document.querySelectorAll(
|
|
||||||
`[data-row-index="${props.tableId}-${rowIndex}"]`,
|
|
||||||
);
|
|
||||||
allCells.forEach((cell, index) => {
|
|
||||||
if (draggedOverState === 'top') {
|
if (draggedOverState === 'top') {
|
||||||
cell.classList.add(styles.draggedOverTop);
|
cell.classList.add(styles.draggedOverTop);
|
||||||
cell.classList.remove(styles.draggedOverBottom);
|
cell.classList.remove(styles.draggedOverBottom);
|
||||||
@@ -889,15 +960,22 @@ export const TableColumnContainer = (
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Remove dragged over classes from all cells in the same row
|
// Remove dragged over classes from all cells in the same row
|
||||||
const allCells = document.querySelectorAll(
|
cells.forEach((cell) => {
|
||||||
`[data-row-index="${props.tableId}-${rowIndex}"]`,
|
|
||||||
);
|
|
||||||
allCells.forEach((cell) => {
|
|
||||||
cell.classList.remove(styles.draggedOverTop);
|
cell.classList.remove(styles.draggedOverTop);
|
||||||
cell.classList.remove(styles.draggedOverBottom);
|
cell.classList.remove(styles.draggedOverBottom);
|
||||||
cell.classList.remove(styles.draggedOverFirstCell);
|
cell.classList.remove(styles.draggedOverFirstCell);
|
||||||
});
|
});
|
||||||
|
// Clear cache when state is cleared
|
||||||
|
cachedCells = null;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (rafId !== null) {
|
||||||
|
cancelAnimationFrame(rafId);
|
||||||
|
}
|
||||||
|
cachedCells = null;
|
||||||
|
};
|
||||||
}, [isDataRow, props.rowIndex, props.isDraggedOver, props.tableId]);
|
}, [isDataRow, props.rowIndex, props.isDraggedOver, props.tableId]);
|
||||||
|
|
||||||
const handleClick = useDoubleClick({
|
const handleClick = useDoubleClick({
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ interface VirtualizedTableGridProps {
|
|||||||
cellPadding: 'lg' | 'md' | 'sm' | 'xl' | 'xs';
|
cellPadding: 'lg' | 'md' | 'sm' | 'xl' | 'xs';
|
||||||
controls: ItemControls;
|
controls: ItemControls;
|
||||||
data: unknown[];
|
data: unknown[];
|
||||||
|
dataWithGroups: (null | unknown)[];
|
||||||
enableAlternateRowColors: boolean;
|
enableAlternateRowColors: boolean;
|
||||||
enableColumnReorder: boolean;
|
enableColumnReorder: boolean;
|
||||||
enableColumnResize: boolean;
|
enableColumnResize: boolean;
|
||||||
@@ -134,7 +135,8 @@ const VirtualizedTableGrid = ({
|
|||||||
CellComponent,
|
CellComponent,
|
||||||
cellPadding,
|
cellPadding,
|
||||||
controls,
|
controls,
|
||||||
data,
|
// data,
|
||||||
|
dataWithGroups,
|
||||||
enableAlternateRowColors,
|
enableAlternateRowColors,
|
||||||
enableColumnReorder,
|
enableColumnReorder,
|
||||||
enableColumnResize,
|
enableColumnResize,
|
||||||
@@ -185,53 +187,6 @@ const VirtualizedTableGrid = ({
|
|||||||
);
|
);
|
||||||
}, [pinnedRightColumnCount, pinnedLeftColumnCount, totalColumnCount, columnWidth]);
|
}, [pinnedRightColumnCount, pinnedLeftColumnCount, totalColumnCount, columnWidth]);
|
||||||
|
|
||||||
// Create data array with group headers inserted as null values
|
|
||||||
// Groups are defined by itemCount, so we calculate indexes based on cumulative item counts
|
|
||||||
const dataWithGroups = useMemo(() => {
|
|
||||||
const result: (null | unknown)[] = enableHeader ? [null] : [];
|
|
||||||
|
|
||||||
if (!groups || groups.length === 0) {
|
|
||||||
// No groups, just add all data
|
|
||||||
result.push(...data);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate group header indexes based on itemCounts
|
|
||||||
const groupIndexes: number[] = [];
|
|
||||||
let cumulativeDataIndex = 0;
|
|
||||||
const headerOffset = enableHeader ? 1 : 0;
|
|
||||||
|
|
||||||
groups.forEach((group, groupIndex) => {
|
|
||||||
// Group header appears before its items
|
|
||||||
// Index = header offset + cumulative data index + number of previous group headers
|
|
||||||
const groupHeaderIndex = headerOffset + cumulativeDataIndex + groupIndex;
|
|
||||||
groupIndexes.push(groupHeaderIndex);
|
|
||||||
cumulativeDataIndex += group.itemCount;
|
|
||||||
});
|
|
||||||
|
|
||||||
let dataIndex = 0;
|
|
||||||
const startIndex = enableHeader ? 1 : 0;
|
|
||||||
let groupHeaderCount = 0;
|
|
||||||
|
|
||||||
// Iterate through the expanded row space (data + group headers)
|
|
||||||
for (
|
|
||||||
let rowIndex = startIndex;
|
|
||||||
rowIndex < startIndex + data.length + groupIndexes.length;
|
|
||||||
rowIndex++
|
|
||||||
) {
|
|
||||||
// Check if this row should have a group header
|
|
||||||
const expectedGroupIndex = groupIndexes[groupHeaderCount];
|
|
||||||
if (expectedGroupIndex !== undefined && rowIndex === expectedGroupIndex) {
|
|
||||||
result.push(null); // Group header row
|
|
||||||
groupHeaderCount++;
|
|
||||||
} else if (dataIndex < data.length) {
|
|
||||||
result.push(data[dataIndex]);
|
|
||||||
dataIndex++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}, [data, enableHeader, groups]);
|
|
||||||
|
|
||||||
const adjustedRowIndexMap = useMemo(() => {
|
const adjustedRowIndexMap = useMemo(() => {
|
||||||
const map = new Map<number, number>();
|
const map = new Map<number, number>();
|
||||||
|
|
||||||
@@ -275,71 +230,94 @@ const VirtualizedTableGrid = ({
|
|||||||
return map;
|
return map;
|
||||||
}, [dataWithGroups, enableHeader, groups]);
|
}, [dataWithGroups, enableHeader, groups]);
|
||||||
|
|
||||||
const itemProps: TableItemProps = useMemo(
|
const stableConfigProps = useMemo(
|
||||||
|
() => ({
|
||||||
|
cellPadding,
|
||||||
|
columns: parsedColumns,
|
||||||
|
controls,
|
||||||
|
enableHeader,
|
||||||
|
getRowHeight,
|
||||||
|
internalState,
|
||||||
|
itemType,
|
||||||
|
playerContext,
|
||||||
|
size,
|
||||||
|
tableId,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
cellPadding,
|
||||||
|
parsedColumns,
|
||||||
|
controls,
|
||||||
|
enableHeader,
|
||||||
|
getRowHeight,
|
||||||
|
internalState,
|
||||||
|
itemType,
|
||||||
|
playerContext,
|
||||||
|
size,
|
||||||
|
tableId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const dynamicDataProps = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
activeRowId,
|
activeRowId,
|
||||||
adjustedRowIndexMap,
|
adjustedRowIndexMap,
|
||||||
calculatedColumnWidths,
|
calculatedColumnWidths,
|
||||||
cellPadding,
|
|
||||||
columns: parsedColumns,
|
|
||||||
controls,
|
|
||||||
data: dataWithGroups,
|
data: dataWithGroups,
|
||||||
enableAlternateRowColors,
|
|
||||||
enableColumnReorder,
|
|
||||||
enableColumnResize,
|
|
||||||
enableDrag,
|
|
||||||
enableExpansion,
|
|
||||||
enableHeader,
|
|
||||||
enableHorizontalBorders,
|
|
||||||
enableRowHoverHighlight,
|
|
||||||
enableSelection,
|
|
||||||
enableVerticalBorders,
|
|
||||||
getRowHeight,
|
|
||||||
groups,
|
|
||||||
internalState,
|
|
||||||
itemType,
|
|
||||||
pinnedLeftColumnCount,
|
pinnedLeftColumnCount,
|
||||||
pinnedLeftColumnWidths,
|
pinnedLeftColumnWidths,
|
||||||
pinnedRightColumnCount,
|
pinnedRightColumnCount,
|
||||||
pinnedRightColumnWidths,
|
pinnedRightColumnWidths,
|
||||||
playerContext,
|
|
||||||
size,
|
|
||||||
startRowIndex,
|
startRowIndex,
|
||||||
tableId,
|
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
activeRowId,
|
activeRowId,
|
||||||
adjustedRowIndexMap,
|
adjustedRowIndexMap,
|
||||||
calculatedColumnWidths,
|
calculatedColumnWidths,
|
||||||
cellPadding,
|
|
||||||
parsedColumns,
|
|
||||||
controls,
|
|
||||||
dataWithGroups,
|
dataWithGroups,
|
||||||
|
pinnedLeftColumnCount,
|
||||||
|
pinnedLeftColumnWidths,
|
||||||
|
pinnedRightColumnCount,
|
||||||
|
pinnedRightColumnWidths,
|
||||||
|
startRowIndex,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const featureFlags = useMemo(
|
||||||
|
() => ({
|
||||||
|
enableAlternateRowColors,
|
||||||
|
enableColumnReorder,
|
||||||
|
enableColumnResize,
|
||||||
|
enableDrag,
|
||||||
|
enableExpansion,
|
||||||
|
enableHorizontalBorders,
|
||||||
|
enableRowHoverHighlight,
|
||||||
|
enableSelection,
|
||||||
|
enableVerticalBorders,
|
||||||
|
groups,
|
||||||
|
}),
|
||||||
|
[
|
||||||
enableAlternateRowColors,
|
enableAlternateRowColors,
|
||||||
enableColumnReorder,
|
enableColumnReorder,
|
||||||
enableColumnResize,
|
enableColumnResize,
|
||||||
enableDrag,
|
enableDrag,
|
||||||
enableExpansion,
|
enableExpansion,
|
||||||
enableHeader,
|
|
||||||
enableHorizontalBorders,
|
enableHorizontalBorders,
|
||||||
enableRowHoverHighlight,
|
enableRowHoverHighlight,
|
||||||
enableSelection,
|
enableSelection,
|
||||||
enableVerticalBorders,
|
enableVerticalBorders,
|
||||||
getRowHeight,
|
|
||||||
groups,
|
groups,
|
||||||
internalState,
|
|
||||||
itemType,
|
|
||||||
pinnedLeftColumnCount,
|
|
||||||
pinnedLeftColumnWidths,
|
|
||||||
pinnedRightColumnCount,
|
|
||||||
pinnedRightColumnWidths,
|
|
||||||
playerContext,
|
|
||||||
size,
|
|
||||||
startRowIndex,
|
|
||||||
tableId,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const itemProps: TableItemProps = useMemo(
|
||||||
|
() => ({
|
||||||
|
...stableConfigProps,
|
||||||
|
...dynamicDataProps,
|
||||||
|
...featureFlags,
|
||||||
|
}),
|
||||||
|
[stableConfigProps, dynamicDataProps, featureFlags],
|
||||||
|
);
|
||||||
|
|
||||||
const PinnedRowCell = useCallback(
|
const PinnedRowCell = useCallback(
|
||||||
(cellProps: CellComponentProps & TableItemProps) => {
|
(cellProps: CellComponentProps & TableItemProps) => {
|
||||||
return (
|
return (
|
||||||
@@ -612,6 +590,53 @@ const VirtualizedTableGrid = ({
|
|||||||
|
|
||||||
VirtualizedTableGrid.displayName = 'VirtualizedTableGrid';
|
VirtualizedTableGrid.displayName = 'VirtualizedTableGrid';
|
||||||
|
|
||||||
|
const MemoizedVirtualizedTableGrid = memo(VirtualizedTableGrid, (prevProps, nextProps) => {
|
||||||
|
return (
|
||||||
|
prevProps.activeRowId === nextProps.activeRowId &&
|
||||||
|
prevProps.calculatedColumnWidths === nextProps.calculatedColumnWidths &&
|
||||||
|
prevProps.cellPadding === nextProps.cellPadding &&
|
||||||
|
prevProps.controls === nextProps.controls &&
|
||||||
|
prevProps.data === nextProps.data &&
|
||||||
|
prevProps.dataWithGroups === nextProps.dataWithGroups &&
|
||||||
|
prevProps.enableAlternateRowColors === nextProps.enableAlternateRowColors &&
|
||||||
|
prevProps.enableColumnReorder === nextProps.enableColumnReorder &&
|
||||||
|
prevProps.enableColumnResize === nextProps.enableColumnResize &&
|
||||||
|
prevProps.enableDrag === nextProps.enableDrag &&
|
||||||
|
prevProps.enableExpansion === nextProps.enableExpansion &&
|
||||||
|
prevProps.enableHeader === nextProps.enableHeader &&
|
||||||
|
prevProps.enableHorizontalBorders === nextProps.enableHorizontalBorders &&
|
||||||
|
prevProps.enableRowHoverHighlight === nextProps.enableRowHoverHighlight &&
|
||||||
|
prevProps.enableSelection === nextProps.enableSelection &&
|
||||||
|
prevProps.enableVerticalBorders === nextProps.enableVerticalBorders &&
|
||||||
|
prevProps.getRowHeight === nextProps.getRowHeight &&
|
||||||
|
prevProps.groups === nextProps.groups &&
|
||||||
|
prevProps.headerHeight === nextProps.headerHeight &&
|
||||||
|
prevProps.internalState === nextProps.internalState &&
|
||||||
|
prevProps.itemType === nextProps.itemType &&
|
||||||
|
prevProps.mergedRowRef === nextProps.mergedRowRef &&
|
||||||
|
prevProps.onRangeChanged === nextProps.onRangeChanged &&
|
||||||
|
prevProps.parsedColumns === nextProps.parsedColumns &&
|
||||||
|
prevProps.pinnedLeftColumnCount === nextProps.pinnedLeftColumnCount &&
|
||||||
|
prevProps.pinnedLeftColumnRef === nextProps.pinnedLeftColumnRef &&
|
||||||
|
prevProps.pinnedRightColumnCount === nextProps.pinnedRightColumnCount &&
|
||||||
|
prevProps.pinnedRightColumnRef === nextProps.pinnedRightColumnRef &&
|
||||||
|
prevProps.pinnedRowCount === nextProps.pinnedRowCount &&
|
||||||
|
prevProps.pinnedRowRef === nextProps.pinnedRowRef &&
|
||||||
|
prevProps.playerContext === nextProps.playerContext &&
|
||||||
|
prevProps.showLeftShadow === nextProps.showLeftShadow &&
|
||||||
|
prevProps.showRightShadow === nextProps.showRightShadow &&
|
||||||
|
prevProps.showTopShadow === nextProps.showTopShadow &&
|
||||||
|
prevProps.size === nextProps.size &&
|
||||||
|
prevProps.startRowIndex === nextProps.startRowIndex &&
|
||||||
|
prevProps.tableId === nextProps.tableId &&
|
||||||
|
prevProps.totalColumnCount === nextProps.totalColumnCount &&
|
||||||
|
prevProps.totalRowCount === nextProps.totalRowCount &&
|
||||||
|
prevProps.CellComponent === nextProps.CellComponent
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
MemoizedVirtualizedTableGrid.displayName = 'MemoizedVirtualizedTableGrid';
|
||||||
|
|
||||||
export interface TableGroupHeader {
|
export interface TableGroupHeader {
|
||||||
itemCount: number;
|
itemCount: number;
|
||||||
render: (props: {
|
render: (props: {
|
||||||
@@ -738,6 +763,53 @@ const BaseItemTableList = ({
|
|||||||
const [centerContainerWidth, setCenterContainerWidth] = useState(0);
|
const [centerContainerWidth, setCenterContainerWidth] = useState(0);
|
||||||
const [totalContainerWidth, setTotalContainerWidth] = useState(0);
|
const [totalContainerWidth, setTotalContainerWidth] = useState(0);
|
||||||
|
|
||||||
|
// Compute dataWithGroups once to avoid duplicate computation
|
||||||
|
// This is used by both VirtualizedTableGrid and getDataFn
|
||||||
|
const dataWithGroups = useMemo(() => {
|
||||||
|
const result: (null | unknown)[] = enableHeader ? [null] : [];
|
||||||
|
|
||||||
|
if (!groups || groups.length === 0) {
|
||||||
|
// No groups, just add all data
|
||||||
|
result.push(...data);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate group header indexes based on itemCounts
|
||||||
|
const groupIndexes: number[] = [];
|
||||||
|
let cumulativeDataIndex = 0;
|
||||||
|
const headerOffset = enableHeader ? 1 : 0;
|
||||||
|
|
||||||
|
groups.forEach((group, groupIndex) => {
|
||||||
|
// Group header appears before its items
|
||||||
|
// Index = header offset + cumulative data index + number of previous group headers
|
||||||
|
const groupHeaderIndex = headerOffset + cumulativeDataIndex + groupIndex;
|
||||||
|
groupIndexes.push(groupHeaderIndex);
|
||||||
|
cumulativeDataIndex += group.itemCount;
|
||||||
|
});
|
||||||
|
|
||||||
|
let dataIndex = 0;
|
||||||
|
const startIndex = enableHeader ? 1 : 0;
|
||||||
|
let groupHeaderCount = 0;
|
||||||
|
|
||||||
|
// Iterate through the expanded row space (data + group headers)
|
||||||
|
for (
|
||||||
|
let rowIndex = startIndex;
|
||||||
|
rowIndex < startIndex + data.length + groupIndexes.length;
|
||||||
|
rowIndex++
|
||||||
|
) {
|
||||||
|
// Check if this row should have a group header
|
||||||
|
const expectedGroupIndex = groupIndexes[groupHeaderCount];
|
||||||
|
if (expectedGroupIndex !== undefined && rowIndex === expectedGroupIndex) {
|
||||||
|
result.push(null); // Group header row
|
||||||
|
groupHeaderCount++;
|
||||||
|
} else if (dataIndex < data.length) {
|
||||||
|
result.push(data[dataIndex]);
|
||||||
|
dataIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, [data, enableHeader, groups]);
|
||||||
|
|
||||||
// Compute distributed widths: unpinned columns with autoWidth will share any remaining space
|
// Compute distributed widths: unpinned columns with autoWidth will share any remaining space
|
||||||
// When autoSizeColumns is true, all column widths are treated as proportions and scaled to fit the container
|
// When autoSizeColumns is true, all column widths are treated as proportions and scaled to fit the container
|
||||||
const calculatedColumnWidths = useMemo(() => {
|
const calculatedColumnWidths = useMemo(() => {
|
||||||
@@ -867,11 +939,20 @@ const BaseItemTableList = ({
|
|||||||
|
|
||||||
const stickyHeader = stickyHeaderRef.current;
|
const stickyHeader = stickyHeaderRef.current;
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
|
let isMounted = true;
|
||||||
|
|
||||||
const updatePosition = () => {
|
const updatePosition = () => {
|
||||||
|
// Guard against updates after unmount
|
||||||
|
if (!isMounted || !stickyHeader || !container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
const containerRect = container.getBoundingClientRect();
|
const containerRect = container.getBoundingClientRect();
|
||||||
stickyHeader.style.left = `${containerRect.left}px`;
|
stickyHeader.style.left = `${containerRect.left}px`;
|
||||||
stickyHeader.style.width = `${containerRect.width}px`;
|
stickyHeader.style.width = `${containerRect.width}px`;
|
||||||
|
} catch {
|
||||||
|
// Silently handle errors if elements are no longer in DOM
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
updatePosition();
|
updatePosition();
|
||||||
@@ -880,6 +961,7 @@ const BaseItemTableList = ({
|
|||||||
window.addEventListener('scroll', updatePosition, true);
|
window.addEventListener('scroll', updatePosition, true);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
window.removeEventListener('resize', updatePosition);
|
window.removeEventListener('resize', updatePosition);
|
||||||
window.removeEventListener('scroll', updatePosition, true);
|
window.removeEventListener('scroll', updatePosition, true);
|
||||||
};
|
};
|
||||||
@@ -895,13 +977,22 @@ const BaseItemTableList = ({
|
|||||||
|
|
||||||
updateWidth();
|
updateWidth();
|
||||||
|
|
||||||
|
let debounceTimeout: NodeJS.Timeout | null = null;
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
if (debounceTimeout) {
|
||||||
|
clearTimeout(debounceTimeout);
|
||||||
|
}
|
||||||
|
debounceTimeout = setTimeout(() => {
|
||||||
updateWidth();
|
updateWidth();
|
||||||
|
}, 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
resizeObserver.observe(el);
|
resizeObserver.observe(el);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
if (debounceTimeout) {
|
||||||
|
clearTimeout(debounceTimeout);
|
||||||
|
}
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
@@ -917,13 +1008,22 @@ const BaseItemTableList = ({
|
|||||||
|
|
||||||
updateWidth();
|
updateWidth();
|
||||||
|
|
||||||
|
let debounceTimeout: NodeJS.Timeout | null = null;
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
if (debounceTimeout) {
|
||||||
|
clearTimeout(debounceTimeout);
|
||||||
|
}
|
||||||
|
debounceTimeout = setTimeout(() => {
|
||||||
updateWidth();
|
updateWidth();
|
||||||
|
}, 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
resizeObserver.observe(el);
|
resizeObserver.observe(el);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
if (debounceTimeout) {
|
||||||
|
clearTimeout(debounceTimeout);
|
||||||
|
}
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
};
|
};
|
||||||
}, [autoFitColumns]);
|
}, [autoFitColumns]);
|
||||||
@@ -1366,7 +1466,6 @@ const BaseItemTableList = ({
|
|||||||
const scrollTop = (e.currentTarget as HTMLDivElement).scrollTop;
|
const scrollTop = (e.currentTarget as HTMLDivElement).scrollTop;
|
||||||
const scrollLeft = (e.currentTarget as HTMLDivElement).scrollLeft;
|
const scrollLeft = (e.currentTarget as HTMLDivElement).scrollLeft;
|
||||||
|
|
||||||
// Prevent recursive scroll events
|
|
||||||
const isScrolling = {
|
const isScrolling = {
|
||||||
header: false,
|
header: false,
|
||||||
pinnedLeft: false,
|
pinnedLeft: false,
|
||||||
@@ -1490,8 +1589,14 @@ const BaseItemTableList = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add resize observer to maintain height sync
|
// Add resize observer to maintain height sync
|
||||||
|
let heightSyncDebounceTimeout: NodeJS.Timeout | null = null;
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
if (heightSyncDebounceTimeout) {
|
||||||
|
clearTimeout(heightSyncDebounceTimeout);
|
||||||
|
}
|
||||||
|
heightSyncDebounceTimeout = setTimeout(() => {
|
||||||
syncHeights();
|
syncHeights();
|
||||||
|
}, 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
resizeObserver.observe(row);
|
resizeObserver.observe(row);
|
||||||
@@ -1526,6 +1631,9 @@ const BaseItemTableList = ({
|
|||||||
pinnedRight.removeEventListener('wheel', setActiveElementFromWheel);
|
pinnedRight.removeEventListener('wheel', setActiveElementFromWheel);
|
||||||
pinnedRight.removeEventListener('scroll', syncScroll);
|
pinnedRight.removeEventListener('scroll', syncScroll);
|
||||||
}
|
}
|
||||||
|
if (heightSyncDebounceTimeout) {
|
||||||
|
clearTimeout(heightSyncDebounceTimeout);
|
||||||
|
}
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1546,19 +1654,28 @@ const BaseItemTableList = ({
|
|||||||
return () => clearTimeout(timeout);
|
return () => clearTimeout(timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let debounceTimeout: NodeJS.Timeout | null = null;
|
||||||
const checkScrollPosition = () => {
|
const checkScrollPosition = () => {
|
||||||
|
if (debounceTimeout) {
|
||||||
|
clearTimeout(debounceTimeout);
|
||||||
|
}
|
||||||
|
debounceTimeout = setTimeout(() => {
|
||||||
const scrollLeft = row.scrollLeft;
|
const scrollLeft = row.scrollLeft;
|
||||||
const maxScrollLeft = row.scrollWidth - row.clientWidth;
|
const maxScrollLeft = row.scrollWidth - row.clientWidth;
|
||||||
|
|
||||||
setShowLeftShadow(pinnedLeftColumnCount > 0 && scrollLeft > 0);
|
setShowLeftShadow(pinnedLeftColumnCount > 0 && scrollLeft > 0);
|
||||||
setShowRightShadow(pinnedRightColumnCount > 0 && scrollLeft < maxScrollLeft);
|
setShowRightShadow(pinnedRightColumnCount > 0 && scrollLeft < maxScrollLeft);
|
||||||
|
}, 50); // 50ms debounce for shadow visibility
|
||||||
};
|
};
|
||||||
|
|
||||||
checkScrollPosition();
|
checkScrollPosition();
|
||||||
|
|
||||||
row.addEventListener('scroll', checkScrollPosition);
|
row.addEventListener('scroll', checkScrollPosition, { passive: true });
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
if (debounceTimeout) {
|
||||||
|
clearTimeout(debounceTimeout);
|
||||||
|
}
|
||||||
row.removeEventListener('scroll', checkScrollPosition);
|
row.removeEventListener('scroll', checkScrollPosition);
|
||||||
};
|
};
|
||||||
}, [pinnedLeftColumnCount, pinnedRightColumnCount]);
|
}, [pinnedLeftColumnCount, pinnedRightColumnCount]);
|
||||||
@@ -1579,16 +1696,25 @@ const BaseItemTableList = ({
|
|||||||
// When right-pinned columns exist, use right pinned column's scroll position
|
// When right-pinned columns exist, use right pinned column's scroll position
|
||||||
const scrollElement = pinnedRightColumnCount > 0 && pinnedRight ? pinnedRight : row;
|
const scrollElement = pinnedRightColumnCount > 0 && pinnedRight ? pinnedRight : row;
|
||||||
|
|
||||||
|
let debounceTimeout: NodeJS.Timeout | null = null;
|
||||||
const checkScrollPosition = () => {
|
const checkScrollPosition = () => {
|
||||||
|
if (debounceTimeout) {
|
||||||
|
clearTimeout(debounceTimeout);
|
||||||
|
}
|
||||||
|
debounceTimeout = setTimeout(() => {
|
||||||
const currentScrollTop = scrollElement.scrollTop;
|
const currentScrollTop = scrollElement.scrollTop;
|
||||||
setShowTopShadow(currentScrollTop > 0);
|
setShowTopShadow(currentScrollTop > 0);
|
||||||
|
}, 50);
|
||||||
};
|
};
|
||||||
|
|
||||||
checkScrollPosition();
|
checkScrollPosition();
|
||||||
|
|
||||||
scrollElement.addEventListener('scroll', checkScrollPosition);
|
scrollElement.addEventListener('scroll', checkScrollPosition, { passive: true });
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
if (debounceTimeout) {
|
||||||
|
clearTimeout(debounceTimeout);
|
||||||
|
}
|
||||||
scrollElement.removeEventListener('scroll', checkScrollPosition);
|
scrollElement.removeEventListener('scroll', checkScrollPosition);
|
||||||
};
|
};
|
||||||
}, [enableHeader, pinnedRightColumnCount]);
|
}, [enableHeader, pinnedRightColumnCount]);
|
||||||
@@ -1653,11 +1779,20 @@ const BaseItemTableList = ({
|
|||||||
|
|
||||||
const stickyGroupRow = stickyGroupRowRef.current;
|
const stickyGroupRow = stickyGroupRowRef.current;
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
|
let isMounted = true;
|
||||||
|
|
||||||
const updatePosition = () => {
|
const updatePosition = () => {
|
||||||
|
// Guard against updates after unmount
|
||||||
|
if (!isMounted || !stickyGroupRow || !container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
const containerRect = container.getBoundingClientRect();
|
const containerRect = container.getBoundingClientRect();
|
||||||
stickyGroupRow.style.left = `${containerRect.left}px`;
|
stickyGroupRow.style.left = `${containerRect.left}px`;
|
||||||
stickyGroupRow.style.width = `${containerRect.width}px`;
|
stickyGroupRow.style.width = `${containerRect.width}px`;
|
||||||
|
} catch {
|
||||||
|
// Silently handle errors if elements are no longer in DOM
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
updatePosition();
|
updatePosition();
|
||||||
@@ -1666,52 +1801,15 @@ const BaseItemTableList = ({
|
|||||||
window.addEventListener('scroll', updatePosition, true);
|
window.addEventListener('scroll', updatePosition, true);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
window.removeEventListener('resize', updatePosition);
|
window.removeEventListener('resize', updatePosition);
|
||||||
window.removeEventListener('scroll', updatePosition, true);
|
window.removeEventListener('scroll', updatePosition, true);
|
||||||
};
|
};
|
||||||
}, [shouldRenderStickyGroupRow]);
|
}, [shouldRenderStickyGroupRow]);
|
||||||
|
|
||||||
const getDataFn = useCallback(() => {
|
const getDataFn = useCallback(() => {
|
||||||
const result: (null | unknown)[] = enableHeader ? [null] : [];
|
return dataWithGroups;
|
||||||
|
}, [dataWithGroups]);
|
||||||
if (!groups || groups.length === 0) {
|
|
||||||
// No groups, just add all data
|
|
||||||
result.push(...data);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate group header indexes based on itemCounts
|
|
||||||
const groupIndexes: number[] = [];
|
|
||||||
let cumulativeDataIndex = 0;
|
|
||||||
const headerOffset = enableHeader ? 1 : 0;
|
|
||||||
|
|
||||||
groups.forEach((group, groupIndex) => {
|
|
||||||
// Index = header offset + cumulative data index + number of previous group headers
|
|
||||||
const groupHeaderIndex = headerOffset + cumulativeDataIndex + groupIndex;
|
|
||||||
groupIndexes.push(groupHeaderIndex);
|
|
||||||
cumulativeDataIndex += group.itemCount;
|
|
||||||
});
|
|
||||||
|
|
||||||
let dataIndex = 0;
|
|
||||||
const startIndex = enableHeader ? 1 : 0;
|
|
||||||
let groupHeaderCount = 0;
|
|
||||||
|
|
||||||
for (
|
|
||||||
let rowIndex = startIndex;
|
|
||||||
rowIndex < startIndex + data.length + groupIndexes.length;
|
|
||||||
rowIndex++
|
|
||||||
) {
|
|
||||||
const expectedGroupIndex = groupIndexes[groupHeaderCount];
|
|
||||||
if (expectedGroupIndex !== undefined && rowIndex === expectedGroupIndex) {
|
|
||||||
result.push(null);
|
|
||||||
groupHeaderCount++;
|
|
||||||
} else if (dataIndex < data.length) {
|
|
||||||
result.push(data[dataIndex]);
|
|
||||||
dataIndex++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}, [data, enableHeader, groups]);
|
|
||||||
|
|
||||||
const extractRowId = useMemo(() => createExtractRowId(getRowId), [getRowId]);
|
const extractRowId = useMemo(() => createExtractRowId(getRowId), [getRowId]);
|
||||||
|
|
||||||
@@ -2287,13 +2385,14 @@ const BaseItemTableList = ({
|
|||||||
>
|
>
|
||||||
{StickyHeader}
|
{StickyHeader}
|
||||||
{StickyGroupRow}
|
{StickyGroupRow}
|
||||||
<VirtualizedTableGrid
|
<MemoizedVirtualizedTableGrid
|
||||||
activeRowId={activeRowId}
|
activeRowId={activeRowId}
|
||||||
calculatedColumnWidths={calculatedColumnWidths}
|
calculatedColumnWidths={calculatedColumnWidths}
|
||||||
CellComponent={CellComponent}
|
CellComponent={CellComponent}
|
||||||
cellPadding={cellPadding}
|
cellPadding={cellPadding}
|
||||||
controls={controls}
|
controls={controls}
|
||||||
data={data}
|
data={data}
|
||||||
|
dataWithGroups={dataWithGroups}
|
||||||
enableAlternateRowColors={enableAlternateRowColors}
|
enableAlternateRowColors={enableAlternateRowColors}
|
||||||
enableColumnReorder={!!onColumnReordered}
|
enableColumnReorder={!!onColumnReordered}
|
||||||
enableColumnResize={!!onColumnResized}
|
enableColumnResize={!!onColumnResized}
|
||||||
|
|||||||
Reference in New Issue
Block a user