/ commands / plugin / usePagination.ts
usePagination.ts
  1  import { useCallback, useMemo, useRef } from 'react'
  2  
  3  const DEFAULT_MAX_VISIBLE = 5
  4  
  5  type UsePaginationOptions = {
  6    totalItems: number
  7    maxVisible?: number
  8    selectedIndex?: number
  9  }
 10  
 11  type UsePaginationResult<T> = {
 12    // For backwards compatibility with page-based terminology
 13    currentPage: number
 14    totalPages: number
 15    startIndex: number
 16    endIndex: number
 17    needsPagination: boolean
 18    pageSize: number
 19    // Get visible slice of items
 20    getVisibleItems: (items: T[]) => T[]
 21    // Convert visible index to actual index
 22    toActualIndex: (visibleIndex: number) => number
 23    // Check if actual index is visible
 24    isOnCurrentPage: (actualIndex: number) => boolean
 25    // Navigation (kept for API compatibility)
 26    goToPage: (page: number) => void
 27    nextPage: () => void
 28    prevPage: () => void
 29    // Handle selection - just updates the index, scrolling is automatic
 30    handleSelectionChange: (
 31      newIndex: number,
 32      setSelectedIndex: (index: number) => void,
 33    ) => void
 34    // Page navigation - returns false for continuous scrolling (not needed)
 35    handlePageNavigation: (
 36      direction: 'left' | 'right',
 37      setSelectedIndex: (index: number) => void,
 38    ) => boolean
 39    // Scroll position info for UI display
 40    scrollPosition: {
 41      current: number
 42      total: number
 43      canScrollUp: boolean
 44      canScrollDown: boolean
 45    }
 46  }
 47  
 48  export function usePagination<T>({
 49    totalItems,
 50    maxVisible = DEFAULT_MAX_VISIBLE,
 51    selectedIndex = 0,
 52  }: UsePaginationOptions): UsePaginationResult<T> {
 53    const needsPagination = totalItems > maxVisible
 54  
 55    // Use a ref to track the previous scroll offset for smooth scrolling
 56    const scrollOffsetRef = useRef(0)
 57  
 58    // Compute the scroll offset based on selectedIndex
 59    // This ensures the selected item is always visible
 60    const scrollOffset = useMemo(() => {
 61      if (!needsPagination) return 0
 62  
 63      const prevOffset = scrollOffsetRef.current
 64  
 65      // If selected item is above the visible window, scroll up
 66      if (selectedIndex < prevOffset) {
 67        scrollOffsetRef.current = selectedIndex
 68        return selectedIndex
 69      }
 70  
 71      // If selected item is below the visible window, scroll down
 72      if (selectedIndex >= prevOffset + maxVisible) {
 73        const newOffset = selectedIndex - maxVisible + 1
 74        scrollOffsetRef.current = newOffset
 75        return newOffset
 76      }
 77  
 78      // Selected item is within visible window, keep current offset
 79      // But ensure offset is still valid
 80      const maxOffset = Math.max(0, totalItems - maxVisible)
 81      const clampedOffset = Math.min(prevOffset, maxOffset)
 82      scrollOffsetRef.current = clampedOffset
 83      return clampedOffset
 84    }, [selectedIndex, maxVisible, needsPagination, totalItems])
 85  
 86    const startIndex = scrollOffset
 87    const endIndex = Math.min(scrollOffset + maxVisible, totalItems)
 88  
 89    const getVisibleItems = useCallback(
 90      (items: T[]): T[] => {
 91        if (!needsPagination) return items
 92        return items.slice(startIndex, endIndex)
 93      },
 94      [needsPagination, startIndex, endIndex],
 95    )
 96  
 97    const toActualIndex = useCallback(
 98      (visibleIndex: number): number => {
 99        return startIndex + visibleIndex
100      },
101      [startIndex],
102    )
103  
104    const isOnCurrentPage = useCallback(
105      (actualIndex: number): boolean => {
106        return actualIndex >= startIndex && actualIndex < endIndex
107      },
108      [startIndex, endIndex],
109    )
110  
111    // These are mostly no-ops for continuous scrolling but kept for API compatibility
112    const goToPage = useCallback((_page: number) => {
113      // No-op - scrolling is controlled by selectedIndex
114    }, [])
115  
116    const nextPage = useCallback(() => {
117      // No-op - scrolling is controlled by selectedIndex
118    }, [])
119  
120    const prevPage = useCallback(() => {
121      // No-op - scrolling is controlled by selectedIndex
122    }, [])
123  
124    // Simple selection handler - just updates the index
125    // Scrolling happens automatically via the useMemo above
126    const handleSelectionChange = useCallback(
127      (newIndex: number, setSelectedIndex: (index: number) => void) => {
128        const clampedIndex = Math.max(0, Math.min(newIndex, totalItems - 1))
129        setSelectedIndex(clampedIndex)
130      },
131      [totalItems],
132    )
133  
134    // Page navigation - disabled for continuous scrolling
135    const handlePageNavigation = useCallback(
136      (
137        _direction: 'left' | 'right',
138        _setSelectedIndex: (index: number) => void,
139      ): boolean => {
140        return false
141      },
142      [],
143    )
144  
145    // Calculate page-like values for backwards compatibility
146    const totalPages = Math.max(1, Math.ceil(totalItems / maxVisible))
147    const currentPage = Math.floor(scrollOffset / maxVisible)
148  
149    return {
150      currentPage,
151      totalPages,
152      startIndex,
153      endIndex,
154      needsPagination,
155      pageSize: maxVisible,
156      getVisibleItems,
157      toActualIndex,
158      isOnCurrentPage,
159      goToPage,
160      nextPage,
161      prevPage,
162      handleSelectionChange,
163      handlePageNavigation,
164      scrollPosition: {
165        current: selectedIndex + 1,
166        total: totalItems,
167        canScrollUp: scrollOffset > 0,
168        canScrollDown: scrollOffset + maxVisible < totalItems,
169      },
170    }
171  }