Created
January 6, 2026 11:25
-
-
Save vidyesh95/7a7e690cfef5af5e9e4fa52299321931 to your computer and use it in GitHub Desktop.
dynamic pagination
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <script lang="ts"> | |
| /** | |
| * WatchlistPagination Component | |
| * | |
| * A smart pagination component that handles large page counts by showing | |
| * a condensed view with ellipses. | |
| * | |
| * Core Behavior: | |
| * - Always shows the first page (1) and the last page (totalPages). | |
| * - Near the start (pages 1-3): Shows first 5 pages, then ellipsis, then last page. | |
| * - Near the end (last 3 pages): Shows first page, ellipsis, then last 5 pages. | |
| * - In the middle: Shows first page, ellipsis, 3-page window around current, ellipsis, last page. | |
| * - Clicking an ellipsis (...) opens a Popover input for direct page jump. | |
| * | |
| * Pagination Pattern Examples (Total = 99): | |
| * - Page 1: [1], 2, 3, 4, 5, ..., 99 | |
| * - Page 2: 1, [2], 3, 4, 5, ..., 99 | |
| * - Page 3: 1, 2, [3], 4, 5, ..., 99 | |
| * - Page 7: 1, ..., 6, [7], 8, ..., 99 | |
| * - Page 32: 1, ..., 31, [32], 33, ..., 99 | |
| * - Page 97: 1, ..., 95, 96, [97], 98, 99 | |
| * - Page 98: 1, ..., 95, 96, 97, [98], 99 | |
| * - Page 99: 1, ..., 95, 96, 97, 98, [99] | |
| */ | |
| import { Popover } from '@pratikbhadane24/nimbus-ui'; | |
| /** | |
| * Props for the WatchlistPagination component. | |
| */ | |
| interface Props { | |
| /** Total number of pages/watchlists available */ | |
| totalPages?: number; | |
| /** Currently active page (1-indexed). Can be bound to the parent state. */ | |
| activePage?: number; | |
| /** Callback triggered when the page is changed via click or direct input */ | |
| onPageChange?: (page: number) => void; | |
| } | |
| let { totalPages = 13, activePage = $bindable(1), onPageChange }: Props = $props(); | |
| // State for ellipsis popover UI | |
| let leftPopoverOpen = $state(false); | |
| let rightPopoverOpen = $state(false); | |
| let popupInputValue = $state(''); | |
| /** | |
| * Updates the active page and triggers the change callback. | |
| * @param page The page number to navigate to. | |
| */ | |
| function handlePageClick(page: number) { | |
| activePage = page; | |
| onPageChange?.(page); | |
| } | |
| /** | |
| * Handles keyboard input in the ellipsis jump popover. | |
| * Navigates on 'Enter' if the input is valid. | |
| * @param e Keyboard event. | |
| */ | |
| function handlePopupSubmit(e: KeyboardEvent) { | |
| if (e.key === 'Enter') { | |
| const page = parseInt(popupInputValue, 10); | |
| if (!isNaN(page) && page >= 1 && page <= totalPages) { | |
| handlePageClick(page); | |
| } | |
| closePopovers(); | |
| } else if (e.key === 'Escape') { | |
| closePopovers(); | |
| } | |
| } | |
| /** | |
| * Closes any open ellipsis jump popovers and resets the input value. | |
| */ | |
| function closePopovers() { | |
| leftPopoverOpen = false; | |
| rightPopoverOpen = false; | |
| popupInputValue = ''; | |
| } | |
| /** | |
| * Computes the array of page numbers and ellipsis markers to display. | |
| * | |
| * Implementation Logic: | |
| * 1. If totalPages <= 7, show all pages without ellipses. | |
| * 2. If the activePage is near the start (1-3), show pages 1-5, ellipsis, lastPage. | |
| * 3. If the activePage is near the end (lastPage-2 to lastPage), show 1, ellipsis, lastPage-4 to lastPage. | |
| * 4. Otherwise (middle), show 1, ellipsis, activePage-1 to activePage+1, ellipsis, lastPage. | |
| * | |
| * @returns Array of page numbers or ellipsis identifiers. | |
| */ | |
| function getVisiblePages(): (number | 'left-ellipsis' | 'right-ellipsis')[] { | |
| // Show all pages if total is small enough | |
| if (totalPages <= 7) { | |
| return Array.from({ length: totalPages }, (_, i) => i + 1); | |
| } | |
| const pages: (number | 'left-ellipsis' | 'right-ellipsis')[] = []; | |
| // Case 1: Near the start (pages 1, 2, 3) - show first 5 pages | |
| if (activePage <= 3) { | |
| for (let i = 1; i <= 5; i++) { | |
| pages.push(i); | |
| } | |
| pages.push('right-ellipsis'); | |
| pages.push(totalPages); | |
| } | |
| // Case 2: Near the end (last 3 pages) - show last 5 pages | |
| else if (activePage >= totalPages - 2) { | |
| pages.push(1); | |
| pages.push('left-ellipsis'); | |
| for (let i = totalPages - 4; i <= totalPages; i++) { | |
| pages.push(i); | |
| } | |
| } | |
| // Case 3: Middle - show 3-page window around current | |
| else { | |
| pages.push(1); | |
| pages.push('left-ellipsis'); | |
| pages.push(activePage - 1); | |
| pages.push(activePage); | |
| pages.push(activePage + 1); | |
| pages.push('right-ellipsis'); | |
| pages.push(totalPages); | |
| } | |
| return pages; | |
| } | |
| let visiblePages = $derived(getVisiblePages()); | |
| </script> | |
| <div class="flex items-center gap-2 overflow-x-auto py-2 px-1 scrollbar-hide"> | |
| {#each visiblePages as item} | |
| {#if item === 'left-ellipsis'} | |
| <Popover.Root bind:open={leftPopoverOpen}> | |
| <Popover.Trigger | |
| class="shrink-0 w-7 h-7 cursor-pointer flex items-center justify-center text-sm font-medium rounded-sm transition-all text-text-muted hover:bg-primary/10 hover:text-text" | |
| > | |
| ... | |
| </Popover.Trigger> | |
| <Popover.Content | |
| class="w-fit p-2 bg-card-background! border border-border rounded-md shadow-lg" | |
| side="bottom" | |
| align="center" | |
| sideOffset={4} | |
| > | |
| <input | |
| type="number" | |
| min="1" | |
| max={totalPages} | |
| placeholder="Page" | |
| bind:value={popupInputValue} | |
| onkeydown={handlePopupSubmit} | |
| class="w-16 px-2 py-1 text-sm text-text bg-background border border-border rounded focus:outline-none focus:ring-1 focus:ring-primary" | |
| /> | |
| </Popover.Content> | |
| </Popover.Root> | |
| {:else if item === 'right-ellipsis'} | |
| <Popover.Root bind:open={rightPopoverOpen}> | |
| <Popover.Trigger | |
| class="shrink-0 w-7 h-7 cursor-pointer flex items-center justify-center text-sm font-medium rounded-sm transition-all text-text-muted hover:bg-primary/10 hover:text-text" | |
| > | |
| ... | |
| </Popover.Trigger> | |
| <Popover.Content | |
| class="w-fit p-2 bg-card-background! border border-border rounded-md shadow-lg" | |
| side="bottom" | |
| align="center" | |
| sideOffset={4} | |
| > | |
| <input | |
| type="number" | |
| min="1" | |
| max={totalPages} | |
| placeholder="Page" | |
| bind:value={popupInputValue} | |
| onkeydown={handlePopupSubmit} | |
| class="w-16 px-2 py-1 text-sm text-text bg-background border border-border rounded focus:outline-none focus:ring-1 focus:ring-primary" | |
| /> | |
| </Popover.Content> | |
| </Popover.Root> | |
| {:else} | |
| <button | |
| type="button" | |
| onclick={() => handlePageClick(item)} | |
| class="shrink-0 w-7 h-7 cursor-pointer flex items-center justify-center text-sm font-medium rounded-sm transition-all {activePage === | |
| item | |
| ? 'bg-primary/10 text-primary shadow-sm' | |
| : 'text-text-muted bg-fixed-state hover:bg-primary/10 hover:text-text'}" | |
| > | |
| {item} | |
| </button> | |
| {/if} | |
| {/each} | |
| </div> | |
| <style> | |
| /* Hide scrollbar but allow scrolling */ | |
| .scrollbar-hide { | |
| -ms-overflow-style: none; | |
| scrollbar-width: none; | |
| } | |
| .scrollbar-hide::-webkit-scrollbar { | |
| display: none; | |
| } | |
| /* Remove spinner from number input */ | |
| input[type='number']::-webkit-outer-spin-button, | |
| input[type='number']::-webkit-inner-spin-button { | |
| -webkit-appearance: none; | |
| margin: 0; | |
| } | |
| input[type='number'] { | |
| -moz-appearance: textfield; | |
| appearance: textfield; | |
| } | |
| </style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment