Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Simple pagination algorithm
// Implementation in ES6
function pagination(c, m) {
var current = c,
last = m,
delta = 2,
left = current - delta,
right = current + delta + 1,
range = [],
rangeWithDots = [],
l;
for (let i = 1; i <= last; i++) {
if (i == 1 || i == last || i >= left && i < right) {
range.push(i);
}
}
for (let i of range) {
if (l) {
if (i - l === 2) {
rangeWithDots.push(l + 1);
} else if (i - l !== 1) {
rangeWithDots.push('...');
}
}
rangeWithDots.push(i);
l = i;
}
return rangeWithDots;
}
/*
Test it:
for (let i = 1, l = 20; i <= l; i++)
console.log(`Selected page ${i}:`, pagination(i, l));
Expected output:
Selected page 1: [1, 2, 3, "...", 20]
Selected page 2: [1, 2, 3, 4, "...", 20]
Selected page 3: [1, 2, 3, 4, 5, "...", 20]
Selected page 4: [1, 2, 3, 4, 5, 6, "...", 20]
Selected page 5: [1, 2, 3, 4, 5, 6, 7, "...", 20]
Selected page 6: [1, "...", 4, 5, 6, 7, 8, "...", 20]
Selected page 7: [1, "...", 5, 6, 7, 8, 9, "...", 20]
Selected page 8: [1, "...", 6, 7, 8, 9, 10, "...", 20]
Selected page 9: [1, "...", 7, 8, 9, 10, 11, "...", 20]
Selected page 10: [1, "...", 8, 9, 10, 11, 12, "...", 20]
Selected page 11: [1, "...", 9, 10, 11, 12, 13, "...", 20]
Selected page 12: [1, "...", 10, 11, 12, 13, 14, "...", 20]
Selected page 13: [1, "...", 11, 12, 13, 14, 15, "...", 20]
Selected page 14: [1, "...", 12, 13, 14, 15, 16, "...", 20]
Selected page 15: [1, "...", 13, 14, 15, 16, 17, "...", 20]
Selected page 16: [1, "...", 14, 15, 16, 17, 18, 19, 20]
Selected page 17: [1, "...", 15, 16, 17, 18, 19, 20]
Selected page 18: [1, "...", 16, 17, 18, 19, 20]
Selected page 19: [1, "...", 17, 18, 19, 20]
Selected page 20: [1, "...", 18, 19, 20]
*/
@rutu15
Copy link

rutu15 commented Sep 12, 2019

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;
}
}

@adedayomoshood
Copy link

adedayomoshood commented Sep 13, 2019

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
Copy link

BaNru commented Nov 16, 2019

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 = [];

@jomlamladen
Copy link

jomlamladen commented Dec 23, 2019

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
Copy link

sarifmiaa commented Jan 29, 2020

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
Copy link

mothsART commented Feb 15, 2020

@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

@joekelley
Copy link

joekelley commented Mar 15, 2020

Thanks!

@tomblanchard
Copy link

tomblanchard commented Mar 18, 2020

Thanks a lot - saved me a ton of time.

@jorrit91
Copy link

jorrit91 commented Apr 3, 2020

@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
}```

@iiceman40
Copy link

iiceman40 commented Apr 15, 2020

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

@ktmud
Copy link

ktmud commented Jun 3, 2020

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>
  );
});

@edgarjaviertec
Copy link

edgarjaviertec commented Jun 4, 2020

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

@marcofbb
Copy link

marcofbb commented Aug 11, 2020

great !

@narthur
Copy link

narthur commented Aug 11, 2020

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)
    })
})

@mavyfaby
Copy link

mavyfaby commented Aug 15, 2020

Thanks bro!

@hazmihaz
Copy link

hazmihaz commented Nov 18, 2020

@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]

@eliot-akira
Copy link

eliot-akira commented Nov 26, 2020

@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.

@RoLYroLLs
Copy link

RoLYroLLs commented Dec 8, 2020

@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
Copy link

RoLYroLLs commented Dec 8, 2020

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

or

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;
}

@danielkochdakitec
Copy link

danielkochdakitec commented Mar 22, 2021

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);

@chrisk8er
Copy link

chrisk8er commented Jun 23, 2021

Thanks dude, you saved my day...

@taythebot
Copy link

taythebot commented Jul 17, 2021

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

@robozb
Copy link

robozb commented Aug 25, 2021

Thank you so much!

@YuraKostin
Copy link

YuraKostin commented Aug 25, 2021

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;
};

@roblevintennis
Copy link

roblevintennis commented Dec 27, 2021

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.

@farnaz-kakhsaz
Copy link

farnaz-kakhsaz commented Jun 3, 2022

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
Copy link

ponnex commented Jun 29, 2022

@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
Copy link

zacfukuda commented Jul 1, 2022

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
Copy link

orso081980 commented Jul 7, 2022

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..

@rogerfar
Copy link

rogerfar commented Sep 19, 2022

Simple solution if you want the elements to always remain equal and not having your UI shift around:

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

  const pagination = (currentPage: number, pageCount: number, delta: number) => {
    const pages: number[] = [];

    if (currentPage <= delta) {
      pages.push(...getRange(1, Math.min(pageCount, delta * 2 + 1)));
    } else if (currentPage > pageCount - delta) {
      pages.push(...getRange(Math.max(1, pageCount - delta * 2), pageCount));
    } else {
      pages.push(...getRange(Math.max(1, currentPage - delta), Math.min(pageCount, currentPage + delta)));
    }

    return pages;
  };
max: 1
   c:1 [1]
max: 3
   c:1 (3) [1, 2, 3]
   c:2 (3) [1, 2, 3]
   c:3 (3) [1, 2, 3]
max: 5
   c:1 (5) [1, 2, 3, 4, 5]
   c:2 (5) [1, 2, 3, 4, 5]
   c:3 (5) [1, 2, 3, 4, 5]
   c:4 (5) [1, 2, 3, 4, 5]
   c:5 (5) [1, 2, 3, 4, 5]
max: 7
   c:1 (7) [1, 2, 3, 4, 5, 6, 7]
   c:2 (7) [1, 2, 3, 4, 5, 6, 7]
   c:3 (7) [1, 2, 3, 4, 5, 6, 7]
   c:4 (7) [1, 2, 3, 4, 5, 6, 7]
   c:5 (7) [1, 2, 3, 4, 5, 6, 7]
   c:6 (7) [1, 2, 3, 4, 5, 6, 7]
   c:7 (7) [1, 2, 3, 4, 5, 6, 7]
max: 9
   c:1 (7) [1, 2, 3, 4, 5, 6, 7]
   c:2 (7) [1, 2, 3, 4, 5, 6, 7]
   c:3 (7) [1, 2, 3, 4, 5, 6, 7]
   c:4 (7) [1, 2, 3, 4, 5, 6, 7]
   c:5 (7) [2, 3, 4, 5, 6, 7, 8]
   c:6 (7) [3, 4, 5, 6, 7, 8, 9]
   c:7 (7) [3, 4, 5, 6, 7, 8, 9]
   c:8 (7) [3, 4, 5, 6, 7, 8, 9]
   c:9 (7) [3, 4, 5, 6, 7, 8, 9]

@Daudongit
Copy link

Daudongit commented Sep 25, 2022

@narthur version in Rust
https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=660c01b68363ea506b1c20fc8bdcda37

use std::collections::VecDeque;

fn pagination(current_page:i64, number_of_pages:i64)->VecDeque<String>{
    const GAP: &str = "...";
    let center = vec![
        current_page - 2, current_page - 1, current_page, current_page + 1, current_page + 2
    ];
    let mut center_deque:VecDeque<String> = center.iter()
        .filter(|&p| *p > 1i64 && *p < number_of_pages).map(i64::to_string).collect();
    let include_three_left = current_page == 5;
    let include_three_right = current_page == number_of_pages - 4;
    let include_left_dots = current_page > 5;
    let include_right_dots = current_page < number_of_pages - 4;

    if include_three_left {center_deque.push_front("2".into());}
    if include_three_right {center_deque.push_back((number_of_pages - 1i64).to_string());}
    if include_left_dots {center_deque.push_front(GAP.into());}
    if include_right_dots {center_deque.push_back(GAP.into());}
    center_deque.push_front("1".into());
    if number_of_pages > 1i64 {
        center_deque.push_back(number_of_pages.to_string());
    }
    center_deque
}

fn main(){
    let (_current_page, per_page, total) = (1u64, 20, 100);
    let mut number_of_pages = total / per_page;
    if (total % per_page) != 0u64  {
        number_of_pages += 1
    }
    _=number_of_pages; // currently not in use
    // println!("{:?}", pagination(1, number_of_pages));
    // println!("{:?}", pagination(1, 1));
    // println!("{:?}", pagination(15, 51));
    // println!("{:?}", pagination(90, 100));
    assert_eq!(pagination(1, 1), ["1"]);
    assert_eq!(pagination(15, 51), ["1", "...", "13", "14", "15", "16", "17", "...", "51"]);
    assert_eq!(pagination(90, 100), ["1", "...", "88", "89", "90", "91", "92", "...", "100"]);
}

@rrmesquita
Copy link

rrmesquita commented Sep 30, 2022

@narthur's approach but supporting custom delta:

function pagination(current: number, total: number, delta = 2, gap = '...') {
  if (total <= 1) return [1]

  const center = [current] as (number | typeof gap)[]

  // no longer O(1) but still very fast
  for (let i = 1; i <= delta; i++) {
    center.unshift(current - i)
    center.push(current + i)
  }

  const filteredCenter = center.filter((page) => page > 1 && page < total)

  const includeLeftGap = current > 3 + delta
  const includeLeftPages = current === 3 + delta
  const includeRightGap = current < total - (2 + delta)
  const includeRightPages = current === total - (2 + delta)

  if (includeLeftPages) filteredCenter.unshift(2)
  if (includeRightPages) filteredCenter.push(total - 1)
  if (includeLeftGap) filteredCenter.unshift(gap)
  if (includeRightGap) filteredCenter.push(gap)

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

/*

tests:

for (let i = 1, l = 20; i <= l; i++)
  console.log(`Page ${i}:`, pagination(i, l, 4)); // delta 4

expected output:

page 1: [1, 2, 3, 4, 5, '...', 20]
page 2: [1, 2, 3, 4, 5, 6, '...', 20]
page 3: [1, 2, 3, 4, 5, 6, 7, '...', 20]
page 4: [1, 2, 3, 4, 5, 6, 7, 8, '...', 20]
page 5: [1, 2, 3, 4, 5, 6, 7, 8, 9, '...', 20]
page 6: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, '...', 20]
page 7: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, '...', 20]
page 8: [1, '...', 4, 5, 6, 7, 8, 9, 10, 11, 12, '...', 20]
page 9: [1, '...', 5, 6, 7, 8, 9, 10, 11, 12, 13, '...', 20]
page 10: [1, '...', 6, 7, 8, 9, 10, 11, 12, 13, 14, '...', 20]
page 11: [1, '...', 7, 8, 9, 10, 11, 12, 13, 14, 15, '...', 20]
page 12: [1, '...', 8, 9, 10, 11, 12, 13, 14, 15, 16, '...', 20]
page 13: [1, '...', 9, 10, 11, 12, 13, 14, 15, 16, 17, '...', 20]
page 14: [1, '...', 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
page 15: [1, '...', 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
page 16: [1, '...', 12, 13, 14, 15, 16, 17, 18, 19, 20]
page 17: [1, '...', 13, 14, 15, 16, 17, 18, 19, 20]
page 18: [1, '...', 14, 15, 16, 17, 18, 19, 20]
page 19: [1, '...', 15, 16, 17, 18, 19, 20]
page 20: [1, '...', 16, 17, 18, 19, 20]

*/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment