2

Simple pagination algorithm · GitHub

 2 years ago
source link: https://gist.github.com/kottenator/9d936eb3e4e3c3e02598
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

rutu15 commented on Sep 12, 2019

edited

It can be optimized further like this..

var current = currentPage, rangeWithDots = [], l;
for (let i = 1; i <= response.total_pages; i++) {
if (i==1 || i == response.total_pages || i >= current-text/2 && i <= current+text/2) {
l = l ? i - l !== 1 ? rangeWithDots.push(...) : null : l
rangeWithDots.push(<a className={currentPage === i ? 'active' : ''} href='#' key={i} id={i}
onClick={() => this.fetchData(i)}>{i})
l = i;
}
}

Implementing @BaNru's modification in ReactJs.

Pagination.js

import React from 'react';
import PropTypes from 'prop-types';

const paginate = (currentPage, lastPage, clickEvent) => {
  const delta = 1;
  const range = [];

  for (let i = Math.max(2, (currentPage - delta)); i <= Math.min((lastPage - 1), (currentPage + delta)); i += 1) {
    range.push(i);
  }

  if ((currentPage - delta) > 2) {
    range.unshift('...');
  }

  if ((currentPage + delta) < (lastPage - 1)) {
    range.push('...');
  }

  range.unshift(1);
  if (lastPage !== 1) range.push(lastPage);

  return range.map((i, index) => {return (
    !isNaN(i) ?
      <button
        value={i}
        key={index}
        onClick={clickEvent}
        className={currentPage === i ? "active" : ""}
      >{i}</button>
      : <span key={index}>{i}</span>
  )
  });
};

const Pagination = ({ currentPage, lastPage, clickEvent }) =>{
  return(
    <section className="pagination">
      {paginate(currentPage, lastPage, clickEvent)}
    </section>
  )
};

Pagination.defaultProps = {
  currentPage: 0,
  lastPage: 0,
  clickEvent: null,
};

Pagination.propTypes = {
  currentPage: PropTypes.number,
  lastPage: PropTypes.number,
  clickEvent: PropTypes.func,
};

export default Pagination;

Usage

<Pagination currentPage={1} lastPage={10} clickEvent={handlePagination} />

BaNru commented on Nov 16, 2019

edited

My modification https://gist.github.com/kottenator/9d936eb3e4e3c3e02598#gistcomment-2871200

PHP version of my modification

function pagination($currentPage, $delta, $lastPage) {
	$lastPage = intval($lastPage); // see below UPDATE 2021
	$range = [];
	for ($i = max(2, ($currentPage - $delta)); $i <= min(($lastPage-1), ($currentPage + $delta)); $i += 1) {
		$range[] = $i;
	}

	if (($currentPage - $delta) > 2) {
		if (count($range) == $lastPage - 3) {
			array_unshift($range,2);
		} else {
			array_unshift($range,'...');
		}
	}

	if (($currentPage + $delta) < ($lastPage - 1)) {
		if (count($range) == $lastPage - 3) {
			$range[] = ($lastPage - 1);
		} else {
			$range[] = '...';
		}
	}

	array_unshift($range,1);
	if ($lastPage !== 1) $range[] = $lastPage;

	return $range;
}
pagination(2, 3, 7); // 1,2,3,4,5,6,7
pagination(6, 3, 7); // 1,2,3,4,5,6,7
pagination(3, 3, 8); // 1,2,3,4,5,6,7,8
pagination(6, 3, 8); // 1,2,3,4,5,6,7,8
pagination(4, 3, 9); // 1,2,3,4,5,6,7,8,9
pagination(6, 3, 9); // 1,2,3,4,5,6,7,8,9
pagination(5, 3, 10); // 1,2,3,4,5,6,7,8,9,10
pagination(6, 3, 10); // 1,2,3,4,5,6,7,8,9,10
pagination(5, 3, 11); // 1,2,3,4,5,6,7,8,'...',11
pagination(6, 3, 11); // 1,'...',3,4,5,6,7,8,9,10,11 // BAD
pagination(2, 3, 5); // 1,2,3,4,5
pagination(1, 3, 1); // 1
pagination(1, 3, 2); // 1,2
pagination(1, 3, 3); // 1,2,3
pagination(1, 3, 5); // 1,2,3,4,5
pagination(2, 3, 5); // 1,2,3,4,5
pagination(3, 3, 5); // 1,2,3,4,5
pagination(1, 3, 5); // 1,2,3,4,5
pagination(8, 3, 21); // 1,'...',5,6,7,8,9,10,11,'...',21
pagination(6, 3, 21); // 1,'...',3,4,5,6,7,8,9,'...',21 // BAD

UPDATE 2021

pagination(1, 3, floor(1)); // [1,1] // BAD!!!
gettype( floor(1) ) => (ceil, round) type double, but need a integer

function pagination($currentPage, $delta, $lastPage) {
	$lastPage = intval($lastPage); // HOTFIX
	$range = [];

Thanks!

Python version:

def pagination(current, last, delta=2):
    _range = []
    rangeWithDots = []
    l = None

    for i in range(1, last):
        if i == 1 or i == last or i >= (current - delta) and i < (current + delta + 1):
            _range.append(i)
    
    for i in _range:
        if l is not None:
            
            if i - l == 2:
                rangeWithDots.append(l + 1)
            
            if i - l != 1:
                rangeWithDots.append('...')
                
        rangeWithDots.append(i)
        l = i
    
    return rangeWithDots

sarifmiaa commented on Jan 29, 2020

edited

Thanks!

An optimized version with capacity handling a large number of pages https://gist.github.com/sarifconrati/9f64c69757a95f2a34f679e9a330f72a .

/**
 * Generate pagination.
 * @param {number} current Current page.
 * @param {number} last Last page.
 * @param {number} width width.
 * @returns {Array} Returns array of pages.
 */

const paginationGenerator = (current, last, width = 2) => {
  const left = current - width;
  const right = current + width + 1;
  const range = [];
  const rangeWithDots = [];
  let l;

  for (let i = 1; i <= last; i += 1) {
    if (i === 1 || i === last || (i >= left && i <= right)) {
      range.push(i);
    } else if (i < left) {
      i = left - 1;
    } else if (i > right) {
      range.push(last);
      break;
    }
  }

  range.forEach(i => {
    if (l) {
      if (i - l === 2) {
        rangeWithDots.push(l + 1);
      } else if (i - l !== 1) {
        rangeWithDots.push('...');
      }
    }
    rangeWithDots.push(i);
    l = i;
  });

  return rangeWithDots;
};

mothsART commented on Feb 15, 2020

edited

@jomlamladen

not exactly the same implementation.
With 2 corrections, PEP8 and snake_case :

def pagination(current, last, delta=2):
    _range = []
    range_with_dots = []
    l = None

    for i in range(1, last + 1):
        if (
            i == 1 or i == last
            or i >= (current - delta)
            and i < (current + delta + 1)
        ):
            _range.append(i)

    for i in _range:
        if l is not None:
            if i - l == 2:
                range_with_dots.append(l + 1)
            elif i - l != 1:
                range_with_dots.append('...')
        range_with_dots.append(i)
        l = i
    return range_with_dots

Thanks!

Thanks a lot - saved me a ton of time.

jorrit91 commented on Apr 3, 2020

edited

@artanik

We ended up using a slightly altered version of your code. What I like about this approach is that the amount of UI elements is always the same as initial due to the variable delta, which makes this possible:

image
const getRange = (start: number, end: number) => {
  return Array(end - start + 1)
    .fill()
    .map((v, i) => i + start)
}

const pagination = (currentPage: number, pageCount: number) => {
  let delta: number
  if (pageCount <= 7) {
    // delta === 7: [1 2 3 4 5 6 7]
    delta = 7
  } else {
    // delta === 2: [1 ... 4 5 6 ... 10]
    // delta === 4: [1 2 3 4 5 ... 10]
    delta = currentPage > 4 && currentPage < pageCount - 3 ? 2 : 4
  }

  const range = {
    start: Math.round(currentPage - delta / 2),
    end: Math.round(currentPage + delta / 2)
  }

  if (range.start - 1 === 1 || range.end + 1 === pageCount) {
    range.start += 1
    range.end += 1
  }

  let pages: any =
    currentPage > delta
      ? getRange(Math.min(range.start, pageCount - delta), Math.min(range.end, pageCount))
      : getRange(1, Math.min(pageCount, delta + 1))

  const withDots = (value, pair) => (pages.length + 1 !== pageCount ? pair : [value])

  if (pages[0] !== 1) {
    pages = withDots(1, [1, '...']).concat(pages)
  }

  if (pages[pages.length - 1] < pageCount) {
    pages = pages.concat(withDots(pageCount, ['...', pageCount]))
  }

  return pages
}```

@jorrit91 very nice, works like a charm and feels pretty good to always have the same number of UI elements, indeed.

ktmud commented on Jun 3, 2020

edited

My simple implementation in TypeScript and Bootstrap:

// first, ..., prev, current, next, ..., last
const MINIMAL_PAGE_ITEM_COUNT = 7;

/**
 * Generate numeric page items around current page.
 *   - Always include first and last page
 *   - Add ellipsis if needed
 */
function generatePageItems(total: number, current: number, width: number) {
  if (width < MINIMAL_PAGE_ITEM_COUNT) {
    throw new Error(`Must allow at least ${MINIMAL_PAGE_ITEM_COUNT} page items`);
  }
  if (width % 2 === 0) {
    throw new Error(`Must allow odd number of page items`);
  }
  if (total < width) {
    return [...new Array(total).keys()];
  }
  const left = Math.max(0, Math.min(total - width, current - Math.floor(width / 2)));
  const items: (string | number)[] = new Array(width);
  for (let i = 0; i < width; i += 1) {
    items[i] = i + left;
  }
  // replace non-ending items with placeholders
  if (items[0] > 0) {
    items[0] = 0;
    items[1] = 'prev-more';
  }
  if (items[items.length - 1] < total - 1) {
    items[items.length - 1] = total - 1;
    items[items.length - 2] = 'next-more';
  }
  return items;
}
interface PaginationProps {
  pageCount: number; // number of pages
  currentPage?: number; // index of current page, zero-based
  maxPageItemCount?: number;
  ellipsis?: string; // content for ellipsis item
  gotoPage: (page: number) => void; // `page` is zero-based
}

export default React.forwardRef(function Pagination(
  { pageCount, currentPage = 0, maxPageItemCount = 9, gotoPage }: PaginationProps,
  ref: React.Ref<HTMLDivElement>,
) {
  const pageItems = generatePageItems(pageCount, currentPage, maxPageItemCount);
  return (
    <div ref={ref} className="dt-pagination">
      <ul className="pagination pagination-sm">
        {pageItems.map((item, i) =>
          typeof item === 'number' ? (
            // actual page number
            <li key={item} className={currentPage === item ? 'active' : undefined}>
              <a
                href={`#page-${item}`}
                role="button"
                onClick={e => {
                  e.preventDefault();
                  gotoPage(item);
                }}
              >
                {item + 1}
              </a>
            </li>
          ) : (
            <li key={item} className="dt-pagination-ellipsis">
              <span>…</span>
            </li>
          ),
        )}
      </ul>
    </div>
  );
});

@ktmud Bro your implementation helped me a lot, thank you very much for sharing it +1

great !

Version that is just as fast regardless of number of pages:

export function pagination(current, total) {
    const center = [current - 2, current - 1, current, current + 1, current + 2],
        filteredCenter = center.filter((p) => p > 1 && p < total),
        includeThreeLeft = current === 5,
        includeThreeRight = current === total - 4,
        includeLeftDots = current > 5,
        includeRightDots = current < total - 4;

    if (includeThreeLeft) filteredCenter.unshift(2)
    if (includeThreeRight) filteredCenter.push(total - 1)

    if (includeLeftDots) filteredCenter.unshift('...');
    if (includeRightDots) filteredCenter.push('...');

    return [1, ...filteredCenter, total]
}

Tests:

describe("pagination algorithm", () => {
    // https://gist.github.com/kottenator/9d936eb3e4e3c3e02598
    const runner = test.each([
        [1,[1, 2, 3, "...", 20]],
        [2,[1, 2, 3, 4, "...", 20]],
        [3,[1, 2, 3, 4, 5, "...", 20]],
        [4,[1, 2, 3, 4, 5, 6, "...", 20]],
        [5,[1, 2, 3, 4, 5, 6, 7, "...", 20]],
        [6,[1, "...", 4, 5, 6, 7, 8, "...", 20]],
        [7,[1, "...", 5, 6, 7, 8, 9, "...", 20]],
        [8,[1, "...", 6, 7, 8, 9, 10, "...", 20]],
        [9,[1, "...", 7, 8, 9, 10, 11, "...", 20]],
        [10,[1, "...", 8, 9, 10, 11, 12, "...", 20]],
        [11,[1, "...", 9, 10, 11, 12, 13, "...", 20]],
        [12,[1, "...", 10, 11, 12, 13, 14, "...", 20]],
        [13,[1, "...", 11, 12, 13, 14, 15, "...", 20]],
        [14,[1, "...", 12, 13, 14, 15, 16, "...", 20]],
        [15,[1, "...", 13, 14, 15, 16, 17, "...", 20]],
        [16,[1, "...", 14, 15, 16, 17, 18, 19, 20]],
        [17,[1, "...", 15, 16, 17, 18, 19, 20]],
        [18,[1, "...", 16, 17, 18, 19, 20]],
        [19,[1, "...", 17, 18, 19, 20]],
        [20,[1, "...", 18, 19, 20]],
    ])

    runner('pagination(%i, 20)', (index, expected) => {
        expect(pagination(index, 20)).toStrictEqual(expected)
    })

    it("maintains performance", () => {
        const t0 = performance.now()
        pagination(1, 99999999999)
        const t1 = performance.now()

        expect(t1 - t0).toBeLessThan(1)
    })
})

Thanks bro!

hazmihaz commented on Nov 18, 2020

edited

@narthur I like your implementation. Thanks for sharing!

edit: I found a bug
If total page is 1, it returns [1,1] instead of [1].

So I added simple checking at line 2:
if (total <= 1) return [1]

@jorrit91 Thanks for sharing your implementation, brilliant.

Of all the variations in this thread, yours made the most sense for my use case; I believe it provides the best UX with a constant number of visible page elements.

@jorrit91 Love your version and was exactly what I'm looking for, but I need it for C#. I'll be trying to convert it, unless someone gets in before me.

RoLYroLLs commented on Dec 8, 2020

edited

Here's my C# version @jorrit91. I'm sure some of you can optimize it, so let me know if you do.. Thanks.

https://gist.github.com/RoLYroLLs/c165202f72a256938da15c916b1362b8

public IEnumerable<object> Pages(int current, int pageCount) {
    List<object> pages = new List<object>();
    var delta = 7;

    if (pageCount > 7) {
            delta = current > 4 && current < pageCount - 3 ? 2 : 4;
    }

    var startIndex = (int)Math.Round(current - delta / (double)2);
    var endIndex = (int)Math.Round(current + delta / (double)2);

    if (startIndex - 1 == 1 || endIndex + 1 == pageCount) {
            startIndex += 1;
            endIndex += 1;
    }

    var to = Math.Min(pageCount, delta + 1);
    for (int i = 1; i <= to; i++) {
            pages.Add(i);
    }

    if (current > delta) {
            pages.Clear();
            var from = Math.Min(startIndex, pageCount - delta);
            to = Math.Min(endIndex, pageCount);
            for (int i = from; i <= to; i++) {
                    pages.Add(i);
            }
    }

        if (pages[0].ToString() != "1") {
                if (pages.Count() + 1 != pageCount) {
                        pages.Insert(0, "...");
                }
                pages.Insert(0, 1);
        }

        if ((int)pages.Last() < pageCount) {
                if (pages.Count() + 1 != pageCount) {
                        pages.Add("...");
                }
                pages.Add(pageCount);
        }

    return pages;
}

Thank you very much! In case someone needs this in PHP, I took your example and moved it to PHP:

<?php
  $currentPage = 10;
  $length = 20;
  
  $delta = 2;
  $left = $currentPage - $delta;
  $right = $currentPage + $delta + 1;
  $range = [];
  $rangeWithDots = [];
  $l;

  for($i = 1; $i <= $length; $i++) {
    if($i == 1 || $i == $length || $i >= $left && $i < $right) {
      $range[] = $i;
    }
  }

  foreach($range as $i) {
    if($l) {
      if ($i - $l === 2) {
        $rangeWithDots[] = $l + 1;
      } else if ($i - $l !== 1) {
        $rangeWithDots[] = '...';
      }
    }
    
    $rangeWithDots[] = $i;
    $l = $i;
  }
  
  print_r($rangeWithDots);

Thanks dude, you saved my day...

Thank you so much for this. Now I don't have to rip out my hair figuring this out..

Thank you so much!

YuraKostin commented on Aug 25, 2021

edited

Hello, everyone

Here is my view of paginator implementation

type PaginatorInput = {
    current: number;
    last: number;
    betweenFirstAndLast?: number;
};

type Paginator = {
    first: number;
    current: number;
    last: number;
    pages: Array<number>;
    leftCluster: number | null;
    rightCluster: number | null;
};

const thresholds = (current: number, side: number): [number, number] => [
    current - side,
    current + side,
];

const range = (from: number, to: number) =>
    Array.from({ length: to + 1 - from }).map((_, i) => i + from);

const middle = (n: number): number => Math.floor(n / 2);

const paginator = (options: PaginatorInput): Paginator | null => {
    const base = {
        first: 1,
        last: options.last,
        current: options.current,
    };

    const { betweenFirstAndLast = 7 } = options;

    if (options.last <= betweenFirstAndLast + 2) {
        return {
            pages: range(2, options.last - 1),
            leftCluster: null,
            rightCluster: null,
            ...base,
        };
    }

    const side = middle(betweenFirstAndLast);
    const [left, right] = thresholds(options.current, side);

    if (left > 1 && right < options.last) {
        return {
            pages: range(left, right),
            leftCluster: middle(1 + left),
            rightCluster: middle(right + options.last),
            ...base,
        };
    }

    if (left < 1) {
        return {
            pages: range(2, right + side),
            leftCluster: null,
            rightCluster: middle(right + options.last),
            ...base,
        };
    }

    if (right > options.last) {
        return {
            pages: range(left - side, options.last - 1),
            leftCluster: middle(right + options.last),
            rightCluster: null,
            ...base,
        };
    }

    return null;
};

And here is the example of render function

const paginatorList = (p: Paginator) => {
    if (!p) {
        return [];
    };

    const {first, current, last, pages, leftCluster, rightCluster} = p;
    const list = [];

    if (current !== first) {
        list.push(first);
    } else {
        list.push('[' + first +']')
    }

    if (leftCluster) {
        list.push('...')
    }

    pages.forEach(page => {
        if (page === current) {
            list.push('[' + page +']');
        } else {
            list.push(page);
        }
    });

    if (rightCluster) {
        list.push('...')
    }

    if (current !== last) {
        list.push(last);
    } else {
        list.push('[' + last +']')
    }

    return list;
};

I really liked the currying approach that @narthur used. It's so straight forward and readable and removes the need for looping! I thought about this and realized even a flexible API would only need to offer offset of say 1 | 2 and so I adapted his to support both. Here's the React hook version (I plan to also implement this in Vue 3, Svelte, and Angular):

export type allowedOffsets = 1 | 2;
export type GAP = '...';
export type PageArrayItem = number | GAP;

export interface PaginationProps {
  offset?: allowedOffsets;
  onChange?: (page: number, pages: PageArrayItem[]) => void;
}

export const usePagination = ({ offset = 2 }: PaginationProps) => {
  const getPaddedArray = (
    filtered: PageArrayItem[],
    shouldIncludeLeftDots: boolean,
    shouldIncludeRightDots: boolean,
    totalCount: number,
  ) => {
    if (shouldIncludeLeftDots) {
      filtered.unshift('...');
    }
    if (shouldIncludeRightDots) {
      filtered.push('...');
    }
    if (totalCount <= 1) {
      return [1];
    }
    return [1, ...filtered, totalCount];
  };

  const generatePagingPaddedByOne = (current: number, totalPageCount: number) => {
    const center = [current - 1, current, current + 1];
    const filteredCenter: PageArrayItem[] = center.filter((p) => p > 1 && p < totalPageCount);
    const includeLeftDots = current > 3;
    const includeRightDots = current < totalPageCount - 2;
    return getPaddedArray(filteredCenter, includeLeftDots, includeRightDots, totalPageCount);
  };

  const generatePagingPaddedByTwo = (current: number, totalPageCount: number) => {
    const center = [current - 2, current - 1, current, current + 1, current + 2];
    const filteredCenter: PageArrayItem[] = center.filter((p) => p > 1 && p < totalPageCount);
    const includeThreeLeft = current === 5;
    const includeThreeRight = current === totalPageCount - 4;
    const includeLeftDots = current > 5;
    const includeRightDots = current < totalPageCount - 4;

    if (includeThreeLeft) {
      filteredCenter.unshift(2);
    }
    if (includeThreeRight) {
      filteredCenter.push(totalPageCount - 1);
    }

    return getPaddedArray(filteredCenter, includeLeftDots, includeRightDots, totalPageCount);
  };

  // https://gist.github.com/kottenator/9d936eb3e4e3c3e02598#gistcomment-3413141
  const generate = (current: number, totalPageCount: number): PageArrayItem[] => {
    if (offset === 1) {
      const generatedPages = generatePagingPaddedByOne(current, totalPageCount);
      return generatedPages;
    }
    const generatedPages = generatePagingPaddedByTwo(current, totalPageCount);
    return generatedPages;
  };

  return {
    generate,
  };
};

Tests are here. This is what it looks like used in my pagination component when the offset is set to 2 (I've left the focus ring as it supports keyboard navigation via tabbing):

Screen Shot 2021-12-26 at 11 24 22 PM

I did notice Ant Design and Zendesk Garden ones used padding (offset, sibling) of just 1 on each side so it's probably worth supporting as well.

I do like the implementations I've seen here that keep the number of page links constant as they don't "jump around" but I think it's a bit of a trade off.

I hope this will help:

export const getPaginationGenerator = (
  currentPageNumber: number,
  totalPageNumber: number,
  offset = 2
): number[] | string[] => {
  // By doing this, when we are close to the beginning or end of the pagination, two numbers are generated after/before the current page, 
  // but when we are far from these points (in the middle of the pagination), we generate only one number after/before the current page.
  const offsetNumber =
    currentPageNumber <= offset || currentPageNumber > totalPageNumber - offset ? offset : offset - 1;
  const numbersList = [];
  const numbersListWithDots = [];

  // If itemsPerPage is less than what the user selected with the Select component or if there is no page or only one page:
  if (totalPageNumber <= 1 || totalPageNumber === undefined) return [1];

  // Create list of numbers:
  numbersList.push(1);
  for (let i = currentPageNumber - offsetNumber; i <= currentPageNumber + offsetNumber; i++) {
    if (i < totalPageNumber && i > 1) {
      numbersList.push(i);
    }
  }
  numbersList.push(totalPageNumber);

  // Add three dots to the list of numbers:
  numbersList.reduce((accumulator, currentValue) => {
    if (accumulator === 1) {
      numbersListWithDots.push(accumulator);
    }
    if (currentValue - accumulator !== 1) {
      numbersListWithDots.push('...');
    }
    numbersListWithDots.push(currentValue);

    return currentValue;
  });

  return numbersListWithDots;
};

image

ponnex commented on Jun 29

edited

@artanik @jorrit91

I've slightly altered the version of your codes by adding pagesShown to have a variable fixed length. pagesShown is clamped to 5 as it makes no sense to have a [1 ... ] or [1 ... ... 10] scenario in pages. With this, we could set whichever fixed length we want.

pagination
const getRange = (start: number, end: number) => {
    const length = end - start + 1;
    return Array.from({length}, (_, i) => start + i);
};

const clamp = (number: number, lower: number, upper: number) => {
    return Math.min(Math.max(number, lower), upper);
}

const pagination = (
    currentPage: number,
    pageCount: number,
    pagesShown: number,
    MINIMUM_PAGE_SIZE: number = 5,
) => {
    let delta: number;
    currentPage = clamp(currentPage, 1, pageCount);
    pagesShown = clamp(pagesShown, MINIMUM_PAGE_SIZE, pageCount);
    const centerPagesShown = pagesShown - 5;
    const boundaryPagesShown = pagesShown - 3;

    if (pageCount <= pagesShown) {
        delta = pagesShown;
    } else {
        delta =
            currentPage < boundaryPagesShown || currentPage > pageCount - boundaryPagesShown
                ? boundaryPagesShown
                : centerPagesShown;
    }

    const range = {
        start: Math.round(currentPage - delta / 2),
        end: Math.round(currentPage + delta / 2),
    };

    if (range.start - 1 === 1 || range.end + 1 === pageCount) {
        range.start += 1;
        range.end += 1;
    }
    let pages: (string | number)[] =
        currentPage > delta
            ? getRange(Math.min(range.start, pageCount - delta), Math.min(range.end, pageCount))
            : getRange(1, Math.min(pageCount, delta + 1));

    if (currentPage > pageCount - boundaryPagesShown && pageCount > pagesShown) {
        pages = getRange(pageCount - delta, pageCount);
    }

    const withDots = (value: number, pair: (string | number)[]) =>
        pages.length + 1 !== pageCount ? pair : [value];
    const lastPage = pages[pages.length - 1];

    if (pages[0] !== 1) {
        pages = withDots(1, [
            1,
            '...',
        ]).concat(pages);
    }

    if (lastPage && lastPage < pageCount) {
        pages = pages.concat(
            withDots(pageCount, [
                '...',
                pageCount,
            ]),
        );
    }

    return pages;
};

zacfukuda commented on Jul 1

edited

My personal implementation returns an object with current, prev, next:

function paginate({current, max}) {
  if (!current || !max) return null

  let prev = current === 1 ? null : current - 1,
      next = current === max ? null : current + 1,
      items = [1]
  
  if (current === 1 && max === 1) return {current, prev, next, items}
  if (current > 4) items.push('…')

  let r = 2, r1 = current - r, r2 = current + r

  for (let i = r1 > 2 ? r1 : 2; i <= Math.min(max, r2); i++) items.push(i)

  if (r2 + 1 < max) items.push('…')
  if (r2 < max) items.push(max)

  return {current, prev, next, items}
}
/* Test */
for (let max = 1; max < 10; max+=2) {
  console.log(`max: ${max}`)
  for (let current = 1; current <= max; current++) {
    let pagination = paginate({current, max})
    console.log(`  c:${current}`, pagination.items)
  }
}

/*
Output:
max: 1
  c:1 [1]
max: 3
  c:1 [1, 2, 3]
  c:2 [1, 2, 3]
  c:3 [1, 2, 3]
max: 5
  c:1 [1, 2, 3, '…', 5]
  c:2 [1, 2, 3, 4, 5]
  c:3 [1, 2, 3, 4, 5]
  c:4 [1, 2, 3, 4, 5]
  c:5 [1, '…', 3, 4, 5]
max: 7
  c:1 [1, 2, 3, '…', 7]
  c:2 [1, 2, 3, 4, '…', 7]
  c:3 [1, 2, 3, 4, 5, '…', 7]
  c:4 [1, 2, 3, 4, 5, 6, 7]
  c:5 [1, '…', 3, 4, 5, 6, 7]
  c:6 [1, '…', 4, 5, 6, 7]
  c:7 [1, '…', 5, 6, 7]
max: 9
  c:1 [1, 2, 3, '…', 9]
  c:2 [1, 2, 3, 4, '…', 9]
  c:3 [1, 2, 3, 4, 5, '…', 9]
  c:4 [1, 2, 3, 4, 5, 6, '…', 9]
  c:5 [1, '…', 3, 4, 5, 6, 7, '…', 9]
  c:6 [1, '…', 4, 5, 6, 7, 8, 9]
  c:7 [1, '…', 5, 6, 7, 8, 9]
  c:8 [1, '…', 6, 7, 8, 9]
  c:9 [1, '…', 7, 8, 9]
*/

orso081980 commented on Jul 7

Is there a way to add the prev and next arrow (< >) to the original script at the very beginning and at the very end? All the solutions that I found so far is going to another direction..


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK