From 545ea25e43b926607f004f4700514219d6c7d6f6 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sat, 4 Oct 2025 15:10:27 -0700 Subject: [PATCH] implement table list callbacks --- .../columns/row-index-column.tsx | 2 +- .../item-table-list-column.tsx | 29 ++- .../item-table-list/item-table-list.tsx | 183 +++++++++++++++--- src/shared/utils/double-click-handler.ts | 47 +++++ 4 files changed, 226 insertions(+), 35 deletions(-) create mode 100644 src/shared/utils/double-click-handler.ts diff --git a/src/renderer/components/item-list/item-table-list/columns/row-index-column.tsx b/src/renderer/components/item-list/item-table-list/columns/row-index-column.tsx index 40832ad5e..6780abb5d 100644 --- a/src/renderer/components/item-list/item-table-list/columns/row-index-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/row-index-column.tsx @@ -4,5 +4,5 @@ import { } from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; export const RowIndexColumn = (props: ItemTableListInnerColumn) => { - return {props.rowIndex + 1}; + return {props.rowIndex}; }; diff --git a/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx b/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx index c7f0fa769..a72eb1a32 100644 --- a/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx +++ b/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx @@ -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 {} @@ -116,6 +117,7 @@ export const TableColumnTextContainer = ( ) => { const containerRef = useRef(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({ + 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(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({ + 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} > diff --git a/src/renderer/components/item-list/item-table-list/item-table-list.tsx b/src/renderer/components/item-list/item-table-list/item-table-list.tsx index e09fcb914..92ff22616 100644 --- a/src/renderer/components/item-list/item-table-list/item-table-list.tsx +++ b/src/renderer/components/item-list/item-table-list/item-table-list.tsx @@ -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, item: unknown, itemType: LibraryItem) => void; itemType: LibraryItem; + onItemClick?: (item: unknown, index: number, event: MouseEvent) => void; + onItemContextMenu?: (item: unknown, index: number, event: MouseEvent) => void; + onItemDoubleClick?: (item: unknown, index: number, event: MouseEvent) => 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['onCellsRendered']; + onCellsRendered?: GridProps['onCellsRendered']; onEndReached?: (index: number) => void; onItemClick?: (item: unknown, index: number, event: MouseEvent) => void; onItemContextMenu?: (item: unknown, index: number, event: MouseEvent) => void; onItemDoubleClick?: (item: unknown, index: number, event: MouseEvent) => void; onRangeChanged?: (range: { endIndex: number; startIndex: number }) => void; - onScroll?: (event: UIEvent) => void; - onScrollEnd?: () => void; + onScrollEnd?: (offset: number) => void; onStartReached?: (index: number) => void; ref?: Ref; 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(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, 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 = ({ { - // 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(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).current = combinedAPI; + } + } + }, [ref]); + return ( { + delay?: number; + onDoubleClick?: (event: MouseEvent) => void; + onSingleClick?: (event: MouseEvent) => void; +} + +/** + * Creates a handler that manages single and double-click events, + * ensuring double-click doesn't trigger single-click + */ +export const createDoubleClickHandler = ( + options: DoubleClickHandlerOptions, +) => { + const { delay = 200, onDoubleClick, onSingleClick } = options; + + let clickTimeout: NodeJS.Timeout | null = null; + let clickCount = 0; + + const handleClick = (event: MouseEvent) => { + 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; +};