raw code

jQuery Pagination revised

Robert Eisele

There are many nasty pagination implementations and certainly sooner or later I've also written such a cruelty. That's why I've been working on a generalized solution for long in order to combine the variety of needs that are imposed on such a navigation to finally put an end to blocks of code that are copied from one project to another with the consequence that it gets no longer understood over the time.

The result of these efforts is my third jQuery plugin that has another big advantage over server side pagination; by generating the links virtually on the client, the risk of duplicate content drops and the need of ugly follow, noindex link attributes left unnecessary. The pagination plugin also combines a varity of features. It can be used to divide long lists or areas of content into multiple seperate pages, load paged content with pre-calculated database offset-parameters via Ajax and anything with full control to adapt the style properly to your site-layout. Of course, creating simple links with no event triggering is possible as well. The plugin also offers the facility to "overlap" pages, which means you can show elements of previous pages on the subsequent sites in order to allow a straightforward flow of reading. A gloss for all performance enthusiasts: The library is just 1.5kb as minified and gzipped bundle!

Note: While many other jQuery pagination implementation give you less flexebility and rely on a pre-defined DOM, jQuery Paging only solves the ugly part - like formatting the links and calculating all offsets - for you and you can use it in any fashion or scenario you can imagine!

Download the jQuery Paging plugin

Basic usage

The Pagination plugin can be used for complex navigations and also for very basic things. You can start with a simple code snippet:

<div id="pagination"></div>

<script src="https://code.jquery.com/jquery-latest.min.js"></script>

<script>

$("#pagination").paging(1337, {
	format: '[< ncnnn >]',
	onSelect: function (page) {
		// add code which gets executed when user selects a page
	},
	onFormat: function (type) {
		switch (type) {
		case 'block': // n and c
			return '<a>' + this.value + '</a>';
		case 'next': // >
			return '<a>&gt;</a>';
		case 'prev': // <
			return '<a>&lt;</a>';
		case 'first': // [
			return '<a>first</a>';
		case 'last': // ]
			return '<a>last</a>';
		}
	}
});

</script>

The selected id #pagination is the container, which will hold the paginator. 1337 is the number of elements that will be paginated. The minimum of code which is needed to show up a paginator is the format and the onFormat directive - there is no default behavior on this! The onFormat directive simply returns HTML or plain texts. For further details of how to format your pagination, take a look at the formatting section!

Examples

I think examples speak louder than a thousand words. In the following example, there are 5 areas that can be partially adjusted visually. For this purpose, I added a list of pre-defined pagination CSS styles. In addition, the number of items you can choose for pagination and the number of overlapping items can be changed as well via the select boxes if a certain example doesn't have an exception on this.

Style: Lapping: Number:

Page navigation

If you didn't noticed the blue dots on the right side, you can use them to scroll the entire page to an individual example. This site navigation also involves the Paging plugin with the aid of Ariel Flesler's scrollTo plugin.

Examples

Using Paging with multiple paginators

It's possible to attach multiple paginators to your content and access them via their class name. In the following example the list becomes visible by using jQuery's slice() method and is colorized by my xcolor plugin's gradientlevel()-method. Moreover, Ben Alman's hashchange plugin is used to make the site history browsable via the browsers back-button (look into the address bar when you click through the sites).

Average temperature of some cities in June

Las Vegas

37°C / 98.6°F

Cairo

35°C / 95°F

Houston

33°C / 91.4°F

Madrid

31°C / 87.8°F

Athens

30°C / 86°F

Lisbon

27°C / 80.6°F

Rome

27°C / 80.6°F

Moscow

23°C / 73.4°F

Los Angeles

22°C / 71.6°F

San Francisco

22°C / 71.6°F

Lima

19°C / 66.2°F

Sydney

18°C / 64.4°F

Johannesburg

17°C / 62.6°F

Buenos Aires

16° / 60.8°F

Teh Code

var prev = {start: 0, stop: 0},
    cont = $('#content div.element');

$(".pagination").paging(cont.length, {
	format: '[< ncnnn! >]',
	perpage: 3,
	lapping: 0,
	page: null, // we await hashchange() event
	onSelect: function (page) {

		var data = this.slice;

		cont.slice(prev[0], prev[1]).css('display', 'none');
		cont.slice(data[0], data[1]).fadeIn("slow");

		prev = data;

		return true; // locate!
	},
	onFormat: formatCallback
});

$(window).hashchange(function() {

	if (window.location.hash)
		Paging.setPage(window.location.hash.substr(1));
	else
		Paging.setPage(1); // we dropped the initial page selection and need to run it manually
});

$(window).hashchange();

Navigate a table

There are also some pagination plugins which are specialized to make a table navigable. These plugins do their job well, but I think they do the abstraction at the wrong place. The Paging-plugin allows you to navigate a table very easily. A table of numbers is generated into the following table and get tagged if they have a special meaning. As you can see, the navigation is implemented by a select box and buttons instead of a pure link navigation. This freedom comes from the simple callback interface, which allows you to define almost any semantic in form of HTML or as another callback, as you can see in the example.

#abcd

P: Prime number, F: Fibonacci number, E: Perfect number, B: Power of two, T: Power of ten

Teh Code

TABLE    = $('table tr'),
BTN1     = $('button:eq(1)'),
BTN2     = $('button:eq(2)'),

BTN1.click(function() {
  TabPager.setPage(CURTAB - 1);
}),

BTN2.click(function() {
  TabPager.setPage(CURTAB + 1);
}),

TabPager = $("select").change(function() {
  TabPager.setPage($(this).val());
}).paging(100, {
  format: '<.> *',
  perpage: 40,
  lapping: 0,
  page: 1,
  onSelect: function (page) {

    var k = this.start;
    var data = this;

    TABLE.each(function(i) {

      $(this).find("td").each(function(j){

        if (j) {

          if (k < data.number) {
            this.innerHTML = k;
          } else {
            this.innerHTML = "";
          }
          ++k;
        }
      });
    });
    return false; // don't locate!
  },
  onFormat: function pageFormat(type) {

    switch (type) {

      case 'prev':
        CURTAB = this.page;
        B1.attr('disabled', !this.active);
        break;

      case 'next':
        CURTAB = this.page;
        B2.attr('disabled', !this.active);
        break;

      case 'block':

        if (!this.active)
          return '<option class="disabled">' + this.value + '</option>';
        else if (this.value != this.page)
          return '<option>' + this.value + '</option>';
        return '<option style="font-weight:bold;" selected="selected">' + this.value + '</option>';
    }
    return "";
  }
});

Image slider

Galleries are another example where many developers tried to solve the navigation problem with specialized plugins. In the following example, the public flickr API is tapped and integrated dynamically using jQuery. As you see, you can also use the Paging plugin here, in order to save the calculations and the event management. This way you can concentrate on the more important components of your project. By the way, sorry for the simple layout, but I just wasn't blessed with any visual ability.

Teh Code

var selectedImg = null;

$.getJSON("https://api.flickr.com/services/feeds/photos_public.gne?jsoncallback=?", {
	tags: "city,nature",
	format: "json"
},
function(data) {

	$.each(data.items, function(i, item) {

		var rotateCSS = 'rotate(' + (Math.random() - 0.5) * 30 + 'deg)';

		$("<img/>")
			.attr('src', item.media.m)
			.css({
				'-moz-transform': rotateCSS,
				'-webkit-transform': rotateCSS
			})
			.css('top', i * 10 + "px")
			.appendTo("#images");

	});

	$('#pager').paging(20,{

		onSelect: function(page) {

			$("#imageshadow").animate({
				left: ((page - 1) * 19 - 2) + "px"
			});

			if (null !== selectedImg) {
				$('#images').find("img").eq(selectedImg).animate({
					left: '0px'
				}, function() {
					$('#images').find("img").eq(page - 1).animate({
						left: '400px'
					});
				});
			} else {
				$('#images').find("img").eq(page-1).animate({
					left: '400px'
				});
			}

			selectedImg = page - 1;

			return false;
		},
		onFormat: function() {

				if (this.value != this.page)
					return '<a style="left:'+((this.pos-1)*19)+'px;" href="#' + this.value + '"></a>';
				return '<span style="left:'+((this.pos-1)*19)+'px;background-color:#fc0;"></span>';
		},
		format: '*',
		perpage: 1,
		page:1
	});

});

Generate your paginator

You can modify the following config or choose an example by clicking on the buttons besides the text-area of this pagination example. I used a JSON-like format here, where the options-attribute is passed to setOptions(), the number-attribute is passed to setNumber() and the labels-attribute is used by onFormat. The generated code of your paginator can be found at the "Teh Code" section.

Result:

Teh Code

$("#paging").paging([[[number]]], {

	format: "[[[format]]]",
	perpage: [[[perpage]]],
	lapping: [[[lapping]]],
	page: [[[page]]],
	onSelect: function(page) {
		...
	},
	onFormat: function(type) {

		switch (type) {

		case 'block':

			if (!this.active)
				return '<span class="disabled">[[number]]</span>';
			else if (this.value != this.page)
				return '<em><a href="#' + this.value + '">[[number]]</a></em>';
			return '<span class="current">[[current]]</span>';

		case 'next':

			if (this.active)
				return '<a href="#' + this.value + '" class="next">[[next]]</a>';
			return '<span class="disabled">[[next]]</span>';

		case 'prev':

			if (this.active)
				return '<a href="#' + this.value + '" class="prev">[[prev]]</a>';
			return '<span class="disabled">[[prev]]</span>';

		case 'first':

			if (this.active)
				return '<a href="#' + this.value + '" class="first">[[first]]</a>';
			return '<span class="disabled">[[first]]</span>';

		case 'last':

			if (this.active)
				return '<a href="#' + this.value + '" class="last">[[last]]</a>';
			return '<span class="disabled">[[last]]</span>';

		case "leap":

			if (this.active)
				return "[[leap]]";
			return "";

		case 'fill':

			if (this.active)
				return "[[fill]]";
			return "";
		}
	}
});

Did you use the style-selector at the top? This way you have the chance to further customize your paginatation. If that's still not enough, you can find some more inspiration on the Smashing Magazine.

Using Ajax

I think generating pages based on content which is already on the page is surely one of the exceptions. More often, howewer, the content is loaded asynchronously via Ajax. This is as simple as using the following LOC to use Ajax with the Paging-plugin:

...
onSelect: function(page) {

	Spinner.spin();

	$.ajax({
		"url": '/data.php?start=' + this.slice[0] + '&end=' + this.slice[1] + '&page=' + page,
		"success": function(data) {
			Spinner.stop();
			// content replace
		}
	});
}
...

As you can see, the script get all the parameters passed, which could be needed to formulate a range SQL statement. For example, if you are a MySQL user, you could do something like this without any further offset calculation:

<?php

$start = (int) $_GET['start'];
$end   = (int) $_GET['end'];

$res = mysql_query("SELECT * FROM t1 LIMIT " . $start . ", " . ($end - $start));

Please note: The LIMIT-statement for paginations with MySQL is really slow for large offsets! I've published a faster method with my DB-Class and abstraction layer, but I wanted to keep the example simple and intuitive here. Another way would be using the page offset as index, in order to fulfill an index scan, but this requires a special table design, which is not the topic of this article.

If you've wondered about the Spinner-object, this comes from a very cool library of Felix Gnass, called spin.js

Websocket example

Another cool thing besides Ajax is something more realtime like Websockets. In a following example the reload mechanism is built with Websockets instead of the built in polling method (refresh/onRefresh)

var Paging = $("#pager").Paging(100, {...}),
  socket = new WebSocket('wss://example.com/paging');
  
  socket.addEventListener('message', function (ev) {
  
    var data = JSON.parse(ev.data);
    
    Paging.setNumber(data.number);
    Paging.setPage(); // reload pagination
  });

Options and Parameters

The following list of options enables you to create a really flexible pagination. There are no format strings or stuff like that, which restricts you on how you have to design the site-navigation.

In order to create a new Paging object, you have to pass the number of elements as parameter. The number is fix until you overwrite it with setNumber(), which is a method of the Paging object. Additionally, the number can be negative to allow an endless processing (like browsing google serps) which, however, disables the usage of the asterisk (*) format operator.

The second parameter of the constructor is an option object. The following list of options is available:

lapping: Lapping indicates the number of elements per page, which will be displayed on the subsequent page. This is useful to ensure a smoother reading flow. This option defaults to 0 and therefore no lapping.
perpage: Perpage indicates the number of elements per page. Please note, if you want to design a table pagination, you have to use the absolute number of elements per page (widht * height), not just the number of elements per line. This option defaults to 10.
page: Page indicates the page you want to start. This option can be negative to start at the end. This option could also be set to null to bypass the page jump, which is useful if you await an event like in the first example above (hashchange). This option defaults to 1.
format: The format option indicates the order you want to get onFormat-callbacks. This option is described in detail in the next paragraph and defaults to an empty string. Thus, this parameter MUST be set, otherwise you'll see an empty paginator - there is no default paginator!
refresh: The refresh option holds another object containing the properties interval (defaults to 10) and url (defaults to an empty string). This option is useful to periodically check for updates on the server via Ajax. You simply pass in an URL which should be checked in a certain interval of seconds. For each interval the callback onRefresh is called. Your server MUST return a JSON-string, which gets expanded to a real object, using jQueries $.parseJSON(). Take a look at the Paging source for an example.
onFormat: This callback is called for every "button"/link on the pagination navigation. Take a look at the last example above and the following paragraph on how you can format the pagination. Please note, the callback is only called once for every link, even if you use multiple paginators via classes. This callback gets the format type as argument.
onRefresh: This callback is called whenever a refresh interval ends. This callback receives the object generated from the Ajax request.
onSelect: This callback is called when a new page is selected in order to retrieve the new data and replace contents on the page. The argument of this callback is the selected page number. The return-value of onSelect indicates if the link (href attribute) of the clicked format element should be followed (otherwise only the click-event is used).

Additionally, you have access to an extensive this-Object inside the callbacks onFormat and onSelect, which supplies the following attributes:

number: The number of elements.
lapping: Number of overlapping elements (see options).
pages: Number of pages.
perpage: Number of elements per page (see options).
page: Current page.
value: Current value for the supplied format type.
pos: Position of the format element. Useful, if you have multiple elements of the same type.
active: Is the element active?
first: Is the element the first element (only for block format element).
last: Is the element the last element (only for block format element).
slice : An array of two elements with start and end position of the current range. It's an array because you can do much more cool things with arrays, like:

onSelect: function() {
	...
	var data = this.slice;
	$selector.slice.apply($selector, data).fadeIn();
	...
}

Formatting

I think it's better to start with an example of how to format a paginator. Let's assume you use the following snippet and the stated format string:

$("#pagination").paging(100, {
	...
	format: "[< nnncnnn >]",
	onFormat: function(type) {

		switch (type) {
		case 'block':

			if (!this.active)
				return '<span class="disabled">' + this.value + '</span>';
			else if (this.value != this.page)
				return '<em><a href="#' + this.value + '">' + this.value + '</a></em>';
			return '<span class="current">' + this.value + '</span>';

		case 'next':

			if (this.active) {
				return '<a href="#' + this.value + '" class="next">Next &raquo;</a>';
			}
			return '<span class="disabled">Next &raquo;</span>';

		case 'prev':

			if (this.active) {
				return '<a href="#' + this.value + '" class="prev">&laquo; Previous</a>';
			}
			return '<span class="disabled">&laquo; Previous</span>';

		case 'first':

			if (this.active) {
				return '<a href="#' + this.value + '" class="first">|<</a>';
			}
			return '<span class="disabled">|<</span>';

		case 'last':

			if (this.active) {
				return '<a href="#' + this.value + '" class="prev">>|</a>';
			}
			return '<span class="disabled">>|</span>';

		case 'fill':
			if (this.active) {
				return "...";
			}
		}
	},
	...
});

As you can see, the return value is a valid HTML-snippet which is used to format the paginator. One important thing you must know is that every a-tag will get an event-listener, any other element is only passed as it is. The following list demostrates the translation and what data you could expect:

block: Type for the number block. Characters: n, c, * where c is the current element and * the type to get all site links. n is just a number.
first: Type for the first page. Character: [
last: Type for the last page. Character: ]
prev: Type for the previous page. Character: <
next: Type for the following page. Character: >
left: Type for the previous page with multiple elements. Character: q
right: Type for the following page with multiple elements. Character: p
fill/leap: A stupid fill element. Character: - and . respectively. They can be used to fill in statistics or other simple placeholders. You can use the pos-attribute in order to distingush between different occurences.

There is one other opterator character: ! The exclamation mark is used after a block to "stretch" the block, even if the number of pages is less than the number of pages needed to fill the entire block. All "inactive" pages also have their active attribute set to false.

The reason why I decided to implement a format string (and not an array with the string elements) is that you can wrap brackets around parts of characters you want to keep as one block. For example, take a look at this format string:

var format = "[(qq-) ncnnn (-pp)]";

In this example, you'll get the two buttons first and last and a block, where the current element is at position two (which is said by "c"). Additionally, you have two left/right blocks, which will translate to something like this: 1 2 BLOCK 98 99, where 98 and 99 are the last pages. By wrapping the fill-element into this groups, the fill element is only visible if all other elements are visible, too. Visible means active or not. What you'll do with this information depends on you.

All onFormat-calls can access the following default attributes of the this-object: number, lapping, pages, perpage, page, slice. All elements member of a block will have access to value, pos and active, the fill-element gets pos and active and all other elements can access value, pos and active.

Okay, I hope you enjoy the jQuery Paging plugin. I am relieved that the long development time is finally over and I am satisfied with the outcome now. However, it was not easy to realize such a simplified API that combines the various aspects and needs of such a navigation. Let me hear, what you think about it in the comments below!