mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
implement table list callbacks
This commit is contained in:
@@ -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={{
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user