mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +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';
|
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
||||||
|
|
||||||
export const RowIndexColumn = (props: ItemTableListInnerColumn) => {
|
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 { Icon } from '/@/shared/components/icon/icon';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { TableColumn } from '/@/shared/types/types';
|
import { TableColumn } from '/@/shared/types/types';
|
||||||
|
import { createDoubleClickHandler } from '/@/shared/utils/double-click-handler';
|
||||||
|
|
||||||
export interface ItemTableListColumn extends CellComponentProps<CellProps> {}
|
export interface ItemTableListColumn extends CellComponentProps<CellProps> {}
|
||||||
|
|
||||||
@@ -116,6 +117,7 @@ export const TableColumnTextContainer = (
|
|||||||
) => {
|
) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const isDataRow = props.enableHeader && props.rowIndex > 0;
|
const isDataRow = props.enableHeader && props.rowIndex > 0;
|
||||||
|
const dataIndex = isDataRow ? props.rowIndex - 1 : props.rowIndex;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDataRow || !containerRef.current || !props.enableRowHover) return;
|
if (!isDataRow || !containerRef.current || !props.enableRowHover) return;
|
||||||
@@ -157,7 +159,18 @@ export const TableColumnTextContainer = (
|
|||||||
props.enableRowBorders && props.enableHeader && props.rowIndex > 0,
|
props.enableRowBorders && props.enableHeader && props.rowIndex > 0,
|
||||||
})}
|
})}
|
||||||
data-row-index={isDataRow ? props.rowIndex : undefined}
|
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}
|
ref={containerRef}
|
||||||
style={props.style}
|
style={props.style}
|
||||||
>
|
>
|
||||||
@@ -182,6 +195,7 @@ export const TableColumnContainer = (
|
|||||||
) => {
|
) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const isDataRow = props.enableHeader && props.rowIndex > 0;
|
const isDataRow = props.enableHeader && props.rowIndex > 0;
|
||||||
|
const dataIndex = isDataRow ? props.rowIndex - 1 : props.rowIndex;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDataRow || !containerRef.current || !props.enableRowHover) return;
|
if (!isDataRow || !containerRef.current || !props.enableRowHover) return;
|
||||||
@@ -223,7 +237,18 @@ export const TableColumnContainer = (
|
|||||||
props.enableRowBorders && props.enableHeader && props.rowIndex > 0,
|
props.enableRowBorders && props.enableHeader && props.rowIndex > 0,
|
||||||
})}
|
})}
|
||||||
data-row-index={isDataRow ? props.rowIndex : undefined}
|
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}
|
ref={containerRef}
|
||||||
style={props.style}
|
style={props.style}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
type JSXElementConstructor,
|
type JSXElementConstructor,
|
||||||
MouseEvent,
|
MouseEvent,
|
||||||
Ref,
|
Ref,
|
||||||
UIEvent,
|
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
@@ -33,6 +32,9 @@ export interface CellProps {
|
|||||||
enableRowHover?: boolean;
|
enableRowHover?: boolean;
|
||||||
handleExpand: (e: MouseEvent<HTMLDivElement>, item: unknown, itemType: LibraryItem) => void;
|
handleExpand: (e: MouseEvent<HTMLDivElement>, item: unknown, itemType: LibraryItem) => void;
|
||||||
itemType: LibraryItem;
|
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';
|
size?: 'compact' | 'default';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,23 +55,19 @@ interface ItemTableListProps {
|
|||||||
enableRowHover?: boolean;
|
enableRowHover?: boolean;
|
||||||
enableSelection?: boolean;
|
enableSelection?: boolean;
|
||||||
headerHeight?: number;
|
headerHeight?: number;
|
||||||
initialTopMostItemIndex?:
|
initialTop?: {
|
||||||
| number
|
behavior?: 'auto' | 'smooth';
|
||||||
| {
|
to: number;
|
||||||
align: 'center' | 'end' | 'start';
|
type: 'index' | 'offset';
|
||||||
behavior: 'auto' | 'smooth';
|
};
|
||||||
index: number;
|
|
||||||
offset?: number;
|
|
||||||
};
|
|
||||||
itemType: LibraryItem;
|
itemType: LibraryItem;
|
||||||
onCellsRendered: GridProps<CellProps>['onCellsRendered'];
|
onCellsRendered?: GridProps<CellProps>['onCellsRendered'];
|
||||||
onEndReached?: (index: number) => void;
|
onEndReached?: (index: number) => void;
|
||||||
onItemClick?: (item: unknown, index: number, event: MouseEvent<HTMLDivElement>) => void;
|
onItemClick?: (item: unknown, index: number, event: MouseEvent<HTMLDivElement>) => void;
|
||||||
onItemContextMenu?: (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;
|
onItemDoubleClick?: (item: unknown, index: number, event: MouseEvent<HTMLDivElement>) => void;
|
||||||
onRangeChanged?: (range: { endIndex: number; startIndex: number }) => void;
|
onRangeChanged?: (range: { endIndex: number; startIndex: number }) => void;
|
||||||
onScroll?: (event: UIEvent<HTMLDivElement>) => void;
|
onScrollEnd?: (offset: number) => void;
|
||||||
onScrollEnd?: () => void;
|
|
||||||
onStartReached?: (index: number) => void;
|
onStartReached?: (index: number) => void;
|
||||||
ref?: Ref<GridImperativeAPI>;
|
ref?: Ref<GridImperativeAPI>;
|
||||||
rowHeight: ((index: number, cellProps: CellProps) => number) | number;
|
rowHeight: ((index: number, cellProps: CellProps) => number) | number;
|
||||||
@@ -95,11 +93,12 @@ export const ItemTableList = ({
|
|||||||
CellComponent,
|
CellComponent,
|
||||||
columns,
|
columns,
|
||||||
data,
|
data,
|
||||||
|
enableExpansion = false,
|
||||||
enableHeader = true,
|
enableHeader = true,
|
||||||
enableRowBorders = false,
|
enableRowBorders = false,
|
||||||
enableRowHover = false,
|
enableRowHover = false,
|
||||||
headerHeight = 40,
|
headerHeight = 40,
|
||||||
initialTopMostItemIndex,
|
initialTop,
|
||||||
itemType,
|
itemType,
|
||||||
onCellsRendered,
|
onCellsRendered,
|
||||||
onEndReached,
|
onEndReached,
|
||||||
@@ -107,7 +106,6 @@ export const ItemTableList = ({
|
|||||||
onItemContextMenu,
|
onItemContextMenu,
|
||||||
onItemDoubleClick,
|
onItemDoubleClick,
|
||||||
onRangeChanged,
|
onRangeChanged,
|
||||||
onScroll,
|
|
||||||
onScrollEnd,
|
onScrollEnd,
|
||||||
onStartReached,
|
onStartReached,
|
||||||
ref,
|
ref,
|
||||||
@@ -134,6 +132,8 @@ export const ItemTableList = ({
|
|||||||
const [showLeftShadow, setShowLeftShadow] = useState(false);
|
const [showLeftShadow, setShowLeftShadow] = useState(false);
|
||||||
const [showRightShadow, setShowRightShadow] = useState(false);
|
const [showRightShadow, setShowRightShadow] = useState(false);
|
||||||
|
|
||||||
|
const onScrollEndRef = useRef<ItemTableListProps['onScrollEnd']>(onScrollEnd);
|
||||||
|
|
||||||
const [initialize] = useOverlayScrollbars({
|
const [initialize] = useOverlayScrollbars({
|
||||||
defer: true,
|
defer: true,
|
||||||
events: {
|
events: {
|
||||||
@@ -230,6 +230,10 @@ export const ItemTableList = ({
|
|||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
scrollingElements.delete(element);
|
scrollingElements.delete(element);
|
||||||
scrollTimeouts.delete(element);
|
scrollTimeouts.delete(element);
|
||||||
|
|
||||||
|
if (element === row && onScrollEndRef.current) {
|
||||||
|
onScrollEndRef.current(row.scrollTop);
|
||||||
|
}
|
||||||
}, 150);
|
}, 150);
|
||||||
|
|
||||||
scrollTimeouts.set(element, timeout);
|
scrollTimeouts.set(element, timeout);
|
||||||
@@ -446,6 +450,10 @@ export const ItemTableList = ({
|
|||||||
|
|
||||||
const handleExpand = useCallback(
|
const handleExpand = useCallback(
|
||||||
(_e: MouseEvent<HTMLDivElement>, item: unknown, itemType: LibraryItem) => {
|
(_e: MouseEvent<HTMLDivElement>, item: unknown, itemType: LibraryItem) => {
|
||||||
|
if (!enableExpansion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (item && typeof item === 'object' && 'id' in item && 'serverId' in item) {
|
if (item && typeof item === 'object' && 'id' in item && 'serverId' in item) {
|
||||||
internalState.toggleExpanded({
|
internalState.toggleExpanded({
|
||||||
id: item.id as string,
|
id: item.id as string,
|
||||||
@@ -454,7 +462,7 @@ export const ItemTableList = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[internalState],
|
[enableExpansion, internalState],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOnCellsRendered = useCallback(
|
const handleOnCellsRendered = useCallback(
|
||||||
@@ -469,21 +477,41 @@ export const ItemTableList = ({
|
|||||||
startIndex: cells.rowStartIndex,
|
startIndex: cells.rowStartIndex,
|
||||||
});
|
});
|
||||||
|
|
||||||
return onCellsRendered
|
if (onStartReached || onEndReached) {
|
||||||
? ({ columnStartIndex, columnStopIndex, rowStartIndex, rowStopIndex }) => {
|
if (cells.rowStartIndex === 0) {
|
||||||
return onCellsRendered!(
|
onStartReached?.(0);
|
||||||
{
|
}
|
||||||
columnStartIndex: columnStartIndex + pinnedLeftColumnCount,
|
|
||||||
columnStopIndex: columnStopIndex + pinnedLeftColumnCount,
|
if (cells.rowStopIndex + 10 >= totalItemCount) {
|
||||||
rowStartIndex: rowStartIndex + pinnedRowCount,
|
onEndReached?.(totalItemCount);
|
||||||
rowStopIndex: rowStopIndex + pinnedRowCount,
|
}
|
||||||
},
|
}
|
||||||
cells,
|
|
||||||
);
|
if (onCellsRendered) {
|
||||||
}
|
return ({ columnStartIndex, columnStopIndex, rowStartIndex, rowStopIndex }) => {
|
||||||
: undefined;
|
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(
|
const PinnedRowCell = useCallback(
|
||||||
@@ -536,9 +564,6 @@ export const ItemTableList = ({
|
|||||||
<CellComponent
|
<CellComponent
|
||||||
{...cellProps}
|
{...cellProps}
|
||||||
columnIndex={cellProps.columnIndex + pinnedLeftColumnCount}
|
columnIndex={cellProps.columnIndex + pinnedLeftColumnCount}
|
||||||
// onClick={(e) => {
|
|
||||||
// onItemClick?.(cellProps.data[cellProps.rowIndex], cellProps.rowIndex, e);
|
|
||||||
// }}
|
|
||||||
rowIndex={cellProps.rowIndex + pinnedRowCount}
|
rowIndex={cellProps.rowIndex + pinnedRowCount}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -554,9 +579,103 @@ export const ItemTableList = ({
|
|||||||
enableRowHover,
|
enableRowHover,
|
||||||
handleExpand,
|
handleExpand,
|
||||||
itemType,
|
itemType,
|
||||||
|
onItemClick,
|
||||||
|
onItemContextMenu,
|
||||||
|
onItemDoubleClick,
|
||||||
size,
|
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 (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
animate={{
|
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