implement table list callbacks

This commit is contained in:
jeffvli
2025-10-04 15:10:27 -07:00
parent 02d9e8328f
commit 545ea25e43
4 changed files with 226 additions and 35 deletions
@@ -4,5 +4,5 @@ import {
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
export const RowIndexColumn = (props: ItemTableListInnerColumn) => {
return <TableColumnTextContainer {...props}>{props.rowIndex + 1}</TableColumnTextContainer>;
return <TableColumnTextContainer {...props}>{props.rowIndex}</TableColumnTextContainer>;
};
@@ -27,6 +27,7 @@ import { CellProps } from '/@/renderer/components/item-list/item-table-list/item
import { Icon } from '/@/shared/components/icon/icon';
import { Text } from '/@/shared/components/text/text';
import { TableColumn } from '/@/shared/types/types';
import { createDoubleClickHandler } from '/@/shared/utils/double-click-handler';
export interface ItemTableListColumn extends CellComponentProps<CellProps> {}
@@ -116,6 +117,7 @@ export const TableColumnTextContainer = (
) => {
const containerRef = useRef<HTMLDivElement>(null);
const isDataRow = props.enableHeader && props.rowIndex > 0;
const dataIndex = isDataRow ? props.rowIndex - 1 : props.rowIndex;
useEffect(() => {
if (!isDataRow || !containerRef.current || !props.enableRowHover) return;
@@ -157,7 +159,18 @@ export const TableColumnTextContainer = (
props.enableRowBorders && props.enableHeader && props.rowIndex > 0,
})}
data-row-index={isDataRow ? props.rowIndex : undefined}
onClick={(e) => props.handleExpand(e, props.data[props.rowIndex], props.itemType)}
onClick={createDoubleClickHandler<HTMLDivElement>({
onDoubleClick: (e) => {
props.onItemDoubleClick?.(props.data[props.rowIndex], dataIndex, e);
},
onSingleClick: (e) => {
props.onItemClick?.(props.data[props.rowIndex], dataIndex, e);
props.handleExpand(e, props.data[props.rowIndex], props.itemType);
},
})}
onContextMenu={(e) => {
props.onItemContextMenu?.(props.data[props.rowIndex], dataIndex, e);
}}
ref={containerRef}
style={props.style}
>
@@ -182,6 +195,7 @@ export const TableColumnContainer = (
) => {
const containerRef = useRef<HTMLDivElement>(null);
const isDataRow = props.enableHeader && props.rowIndex > 0;
const dataIndex = isDataRow ? props.rowIndex - 1 : props.rowIndex;
useEffect(() => {
if (!isDataRow || !containerRef.current || !props.enableRowHover) return;
@@ -223,7 +237,18 @@ export const TableColumnContainer = (
props.enableRowBorders && props.enableHeader && props.rowIndex > 0,
})}
data-row-index={isDataRow ? props.rowIndex : undefined}
onClick={(e) => props.handleExpand(e, props.data[props.rowIndex], props.itemType)}
onClick={createDoubleClickHandler<HTMLDivElement>({
onDoubleClick: (e) => {
props.onItemDoubleClick?.(props.data[props.rowIndex], dataIndex, e);
},
onSingleClick: (e) => {
props.onItemClick?.(props.data[props.rowIndex], dataIndex, e);
props.handleExpand(e, props.data[props.rowIndex], props.itemType);
},
})}
onContextMenu={(e) => {
props.onItemContextMenu?.(props.data[props.rowIndex], dataIndex, e);
}}
ref={containerRef}
style={props.style}
>
@@ -8,7 +8,6 @@ import {
type JSXElementConstructor,
MouseEvent,
Ref,
UIEvent,
useCallback,
useEffect,
useMemo,
@@ -33,6 +32,9 @@ export interface CellProps {
enableRowHover?: boolean;
handleExpand: (e: MouseEvent<HTMLDivElement>, item: unknown, itemType: LibraryItem) => void;
itemType: LibraryItem;
onItemClick?: (item: unknown, index: number, event: MouseEvent<HTMLDivElement>) => void;
onItemContextMenu?: (item: unknown, index: number, event: MouseEvent<HTMLDivElement>) => void;
onItemDoubleClick?: (item: unknown, index: number, event: MouseEvent<HTMLDivElement>) => void;
size?: 'compact' | 'default';
}
@@ -53,23 +55,19 @@ interface ItemTableListProps {
enableRowHover?: boolean;
enableSelection?: boolean;
headerHeight?: number;
initialTopMostItemIndex?:
| number
| {
align: 'center' | 'end' | 'start';
behavior: 'auto' | 'smooth';
index: number;
offset?: number;
};
initialTop?: {
behavior?: 'auto' | 'smooth';
to: number;
type: 'index' | 'offset';
};
itemType: LibraryItem;
onCellsRendered: GridProps<CellProps>['onCellsRendered'];
onCellsRendered?: GridProps<CellProps>['onCellsRendered'];
onEndReached?: (index: number) => void;
onItemClick?: (item: unknown, index: number, event: MouseEvent<HTMLDivElement>) => void;
onItemContextMenu?: (item: unknown, index: number, event: MouseEvent<HTMLDivElement>) => void;
onItemDoubleClick?: (item: unknown, index: number, event: MouseEvent<HTMLDivElement>) => void;
onRangeChanged?: (range: { endIndex: number; startIndex: number }) => void;
onScroll?: (event: UIEvent<HTMLDivElement>) => void;
onScrollEnd?: () => void;
onScrollEnd?: (offset: number) => void;
onStartReached?: (index: number) => void;
ref?: Ref<GridImperativeAPI>;
rowHeight: ((index: number, cellProps: CellProps) => number) | number;
@@ -95,11 +93,12 @@ export const ItemTableList = ({
CellComponent,
columns,
data,
enableExpansion = false,
enableHeader = true,
enableRowBorders = false,
enableRowHover = false,
headerHeight = 40,
initialTopMostItemIndex,
initialTop,
itemType,
onCellsRendered,
onEndReached,
@@ -107,7 +106,6 @@ export const ItemTableList = ({
onItemContextMenu,
onItemDoubleClick,
onRangeChanged,
onScroll,
onScrollEnd,
onStartReached,
ref,
@@ -134,6 +132,8 @@ export const ItemTableList = ({
const [showLeftShadow, setShowLeftShadow] = useState(false);
const [showRightShadow, setShowRightShadow] = useState(false);
const onScrollEndRef = useRef<ItemTableListProps['onScrollEnd']>(onScrollEnd);
const [initialize] = useOverlayScrollbars({
defer: true,
events: {
@@ -230,6 +230,10 @@ export const ItemTableList = ({
const timeout = setTimeout(() => {
scrollingElements.delete(element);
scrollTimeouts.delete(element);
if (element === row && onScrollEndRef.current) {
onScrollEndRef.current(row.scrollTop);
}
}, 150);
scrollTimeouts.set(element, timeout);
@@ -446,6 +450,10 @@ export const ItemTableList = ({
const handleExpand = useCallback(
(_e: MouseEvent<HTMLDivElement>, item: unknown, itemType: LibraryItem) => {
if (!enableExpansion) {
return;
}
if (item && typeof item === 'object' && 'id' in item && 'serverId' in item) {
internalState.toggleExpanded({
id: item.id as string,
@@ -454,7 +462,7 @@ export const ItemTableList = ({
});
}
},
[internalState],
[enableExpansion, internalState],
);
const handleOnCellsRendered = useCallback(
@@ -469,21 +477,41 @@ export const ItemTableList = ({
startIndex: cells.rowStartIndex,
});
return onCellsRendered
? ({ columnStartIndex, columnStopIndex, rowStartIndex, rowStopIndex }) => {
return onCellsRendered!(
{
columnStartIndex: columnStartIndex + pinnedLeftColumnCount,
columnStopIndex: columnStopIndex + pinnedLeftColumnCount,
rowStartIndex: rowStartIndex + pinnedRowCount,
rowStopIndex: rowStopIndex + pinnedRowCount,
},
cells,
);
}
: undefined;
if (onStartReached || onEndReached) {
if (cells.rowStartIndex === 0) {
onStartReached?.(0);
}
if (cells.rowStopIndex + 10 >= totalItemCount) {
onEndReached?.(totalItemCount);
}
}
if (onCellsRendered) {
return ({ columnStartIndex, columnStopIndex, rowStartIndex, rowStopIndex }) => {
return onCellsRendered!(
{
columnStartIndex: columnStartIndex + pinnedLeftColumnCount,
columnStopIndex: columnStopIndex + pinnedLeftColumnCount,
rowStartIndex: rowStartIndex + pinnedRowCount,
rowStopIndex: rowStopIndex + pinnedRowCount,
},
cells,
);
};
}
return undefined;
},
[onCellsRendered, onRangeChanged, pinnedLeftColumnCount, pinnedRowCount],
[
onCellsRendered,
onEndReached,
onRangeChanged,
onStartReached,
pinnedLeftColumnCount,
pinnedRowCount,
totalItemCount,
],
);
const PinnedRowCell = useCallback(
@@ -536,9 +564,6 @@ export const ItemTableList = ({
<CellComponent
{...cellProps}
columnIndex={cellProps.columnIndex + pinnedLeftColumnCount}
// onClick={(e) => {
// onItemClick?.(cellProps.data[cellProps.rowIndex], cellProps.rowIndex, e);
// }}
rowIndex={cellProps.rowIndex + pinnedRowCount}
/>
);
@@ -554,9 +579,103 @@ export const ItemTableList = ({
enableRowHover,
handleExpand,
itemType,
onItemClick,
onItemContextMenu,
onItemDoubleClick,
size,
};
const isInitialScrollPositionSet = useRef<boolean>(false);
useEffect(() => {
if (!initialTop || isInitialScrollPositionSet.current) return;
const scrollToIndex = (index: number, behavior: 'auto' | 'smooth' = 'auto') => {
isInitialScrollPositionSet.current = true;
const adjustedIndex = enableHeader ? Math.max(0, index - 1) : index;
// Calculate scroll position based on row heights
const calculateScrollTop = (rowIndex: number) => {
let scrollTop = 0;
for (let i = 0; i < rowIndex; i++) {
const height = rowHeight as number;
scrollTop += height;
}
return scrollTop;
};
const scrollTop = calculateScrollTop(adjustedIndex);
// Get the scroll containers and scroll them directly
const mainContainer = rowRef.current?.childNodes[0] as HTMLDivElement;
const pinnedLeftContainer = pinnedLeftColumnRef.current
?.childNodes[0] as HTMLDivElement;
const pinnedRightContainer = pinnedRightColumnRef.current
?.childNodes[0] as HTMLDivElement;
if (initialTop.type === 'offset') {
if (mainContainer) {
mainContainer.scrollTo({
behavior,
top: initialTop.to,
});
}
if (pinnedLeftContainer) {
pinnedLeftContainer.scrollTo({
behavior,
top: initialTop.to,
});
}
if (pinnedRightContainer) {
pinnedRightContainer.scrollTo({
behavior,
top: initialTop.to,
});
}
} else {
if (mainContainer) {
mainContainer.scrollTo({
behavior,
top: scrollTop,
});
}
if (pinnedLeftContainer) {
pinnedLeftContainer.scrollTo({
behavior,
top: scrollTop,
});
}
if (pinnedRightContainer) {
pinnedRightContainer.scrollTo({
behavior,
top: scrollTop,
});
}
}
};
scrollToIndex(initialTop.to, initialTop.behavior);
}, [initialTop, enableHeader, pinnedLeftColumnCount, pinnedRightColumnCount, rowHeight]);
// Expose grid refs to parent component
useEffect(() => {
if (ref && typeof ref === 'object') {
// Create a simple API that exposes the main container
const combinedAPI: GridImperativeAPI = {
// We'll create a minimal API that can be extended later
// For now, we'll just expose the main container ref
} as GridImperativeAPI;
if ('current' in ref) {
(ref as React.MutableRefObject<GridImperativeAPI>).current = combinedAPI;
}
}
}, [ref]);
return (
<motion.div
animate={{
+47
View File
@@ -0,0 +1,47 @@
import { MouseEvent } from 'react';
interface DoubleClickHandlerOptions<T extends HTMLElement = HTMLElement> {
delay?: number;
onDoubleClick?: (event: MouseEvent<T>) => void;
onSingleClick?: (event: MouseEvent<T>) => void;
}
/**
* Creates a handler that manages single and double-click events,
* ensuring double-click doesn't trigger single-click
*/
export const createDoubleClickHandler = <T extends HTMLElement = HTMLElement>(
options: DoubleClickHandlerOptions<T>,
) => {
const { delay = 200, onDoubleClick, onSingleClick } = options;
let clickTimeout: NodeJS.Timeout | null = null;
let clickCount = 0;
const handleClick = (event: MouseEvent<T>) => {
clickCount++;
if (clickCount === 1) {
// First click - set a timeout to handle single click
clickTimeout = setTimeout(() => {
if (clickCount === 1) {
// Only single click occurred
onSingleClick?.(event);
}
clickCount = 0;
clickTimeout = null;
}, delay);
} else if (clickCount === 2) {
// Double click detected
if (clickTimeout) {
clearTimeout(clickTimeout);
clickTimeout = null;
}
onDoubleClick?.(event);
clickCount = 0;
}
};
return handleClick;
};