Robert Eisele
Systems Engineer, Architect and DBA

jQuery Pagination revised

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!

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="http://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 words. In the following example, there are 5 areas that can be partially adjusted visually. For this purpose, I found a good 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("http://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

socket.io example

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

var Paging = $("#pager").Paging(100, {...}),
socket = io.connect('http://localhost');
socket.on('pager', function (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 »</a>';
			}
			return '<span class="disabled">Next »</span>';

		case 'prev':

			if (this.active) {
				return '<a href="#' + this.value + '" class="prev">« Previous</a>';
			}
			return '<span class="disabled">« 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!

You might also be interested in the following

24 Comments on „jQuery Pagination revised”

BrynM
BrynM

Currently, if in your onRefresh function you erase the url (trying to cancel refreshes) by setting it to false, '', null or something similar, the refresh attempts continue with a malformed URL.
The fix is to move the line
if (this.interval) window.clearInterval(this.interval);
to the outside of (just above)
if (this.opts["refresh"]["url"]) {

Doing so lets a cancelled url correctly cancel refreshes.

JC
JC

@David B,

Hi, if you need to call a function you can use:

function foo(D) {
return "PAGE " + D.page + " FROM " + D.pages;
}

case "fill":
if (this.active)
return foo(this)
return "";

Otherwise, you can return the string:

case "fill":
if (this.active)
return 'PAGE ' + this.page + ' FROM ' + this.pages;
return "";

JC

Ben

"Teh Code" of your example "Average temperature of some cities in June" is incomplete: the onFormat-section is missing. What is "formatCallback"? You should mention the necessity to hide the elements with display:none. Otherwise it does not work in f.e. IE7. I would suggest to set the display:none via JQuery, otherwise it would be invisible if JavaScript is deactivated.
$('#content div.element').css('display', 'none');

David B
David B

from your sample code above, example 5 generates a javascript compile error.

case 'fill':

if (this.active)
return "function (data) {
if (data.pos > 1)
return "...";
return "PAGE " + data.page + " OF " + data.pages;
}";
return "";
}

I'm trying to get the page 1 of 100 part working, and can't seem to get the format case for fill to work properly

Paul Michelotti

In attempting to setup and utilize this plugin I realized that the plugin itself makes no attempt to paginate your data for you. Unless I am misunderstanding the plugin completely, it appears it only concerns itself with the pagination controls and exposes the onSelect method so that a user may handle page swapping in whatever way they see fit. This was actually ideal for my use case, but does set this plugin appart from some other pagination plugins/frameworks which try to take a set of DOM elements and separate them into pages for you.

While this point was, to some extent, implicitly denoted in the examples, I feel the documentation could profit from a more explicit statement of this usage.

Jeeba

Hi Robert, Im trying your pagination with some Ajax, I have one problem. Lets say I put the number of element to 1 at the beginning. Then at the onSelect method I call an Ajax that give me the real number of element of the page. Whe doing this the pagger doesnt change. But when I try with Paging.setPage() to reload the pagination, it just loop endlesly, calling onSelect again, ergo, calling the Ajax over an over again. Is there anything wrong with the way I work this?

Olivier ROMAND
Olivier ROMAND

Thanks for the work done. This plugin has all the assets to become one of the most successful pagination plugin out there.
Sadly, I must agree with Lar and Ethan Kramer reviews for what concern that user experience.
I have spend couple of hours already and yet can't get the plugin to work.
An out of the box example would be such a nice solution and bring you a lot more of positive feedbacks.
Thanks for bringing it up ;)

belak
belak

This script is butter. Very nice. The best and most flexible pagination script i've found. Thank you!

Dion
Dion

Guy Really cool!! Great Job!!
However I was not manage to get the gist of its use, would it be possible to do a detailed tutorial about it, something like step by step.. =D

I guess we all would be pleased =D

Thanks

Robert

Richard,

Why not just open the "Teh Code" section where you can get the full snippet?

Robert

Richard
Richard

Hi,

Really great work!!

I'd like to use your plugin as you have it with the temperatures example and hashchange.

Which bits of your code do I require to do this?

Thanks,

Richard

Rob
Rob

It seems like a great plugin, but could you provide a basic example of the part with the temperatures? I can't get it to work, even though the code can count the amount of elements nothing seems to happen. (I can't even see the bar to select the different pages.

Just copy & paste from the code stuff doesn't work (and yes i have created links to both jquery.js and your .js file)

Steve
Steve

Nice plugin, but do you have a sample that will update the number for the pager? I have a menu that allows the user to change the type of info that is shown in the data the pager is controlling and I need to set the number after the data is updated. I see that I can set the in the onselect/onformat methods but how do I do it externally? or just reinitialize the pager?

Alex
Alex

Can you specify a little more the ajax example "// content replace" with the example 1 supposing you obtain in the ajax request:

aaa
111


bbb
222

...

How the // content replace could be implemented?
Thank you! Maybe a newbie question!

Robert

John,

I think this could be a cool native feature, but the URL structure comes from the onFormat callback, thus I don't know what URL should be called. Anyway, you can simply add this feature for your own in the user space with the following onSelect-callback:

onSelect: function(page) {
window.location = "#page-" + page;
}

Make sure, you don't return true in the onSelect callback, to avoid a double location. As noted, the return code of onSelect states if the selected page should be used as new URL destination and you do it by yourself this way. If you use hashes instead of permalinks, I would suggest to use the hashchange-plugin, used in example 1.

Ethan Kramer

I agree with Lars. When I was asked to test it out I was looking for a minimal example of how to get started with the plugin.Which I didn't find. The long detailed descriptions and extensive examples are nice, and they really show off the power of the plugin. But from a user experience perspective, people who don't see a simple example at the top of your website (or anywhere) are less likely to use your plugin, simply because of the lack of a simple example. IMHO your site for the plugin should be layed out like this:

Logo, Introduction, tagline and sharing options (e.g. FB, twitter) (don't make it look crowded though)
Simple example
Code for simple example
[Optional download link] (The download buttons will be at the bottom so that's why I say it's optional here, although from a user experience stand point it would be good to have download buttons in both places)
other more more complex examples - first put the example then after each example put the code for that example
Next put the download options at the bottom of the page
Lastly the footer

I know you didn't ask me for a critique of your website but the layout of your website directly affects the success of your plugin. For example if you don't provide a simple example and give the code to it people are less likely to download your plugin because 'I can't just download it and use it'. They feel like they have to read a ton of documentation before getting started with your plugin. Which should never be the case with jQuery plugins. jQuery plugins are nice and simple to use, so your site should portray that. For an example of what I am talking about take a look at http://ek.alphaschildren.org/resources/jquery-plugins/storagify . Its the website for my jQuery plugin Storagify which allows you to enable simple html5 page edits with the new html5 attribute 'contenteditable' and localStorage.

Now that I've said all this. This really is a great plugin, no doubt. Good job. It's really powerful too. Its definitely more powerful than gbirke's jQuery pagination plugin. I can tell you that. The only thing that gbirke's plugin has on yours is the simplicity of its website. I'm not saying strip everything down because your page is just cluttered with too much stuff. No, I'm not saying that, just reorganize your page to let your plugin show off it's true beauty.

I hope nothing that I have said in this comment offends you, I am giving you my constructive criticism of your plugin's website and your plugin

John
John

I have the page starting at the end with -1 for setpage(). I want the page number to show up in the url when the page is loaded.

Robert

Hey Demitry,

a pagination with back-button support is really simple to implement by using the mentioned hashchange plugin of example one. I think you can copy & paste the example codes directly and adapt the onSelect-callback to your needs.

If the back functionality of your content needs to set the page directly, use setPage().

Robert

Demitry

Hey man,

I'm looking for a nice pagination plugin and I think this is it! Before I implement it on my site, could you just tell me if someone is on let's say page 3 of results and then view the details of something and then click back, would they still be seeing page 3 or would they be back on page 1?

With server side pagination I think they would be back on page 3, but I'm not sure about this plugin.

Thanks!

Robert

@TruckTurner, did you noticed the "The Code"-sections? There you can copy&paste the example codes :)

TruckTurner
TruckTurner

Thanks for sharing - really helpful!
A downloadable working example would have even been better - so I had to dig through the source code of this page. But, you know, that way I really understood what I was doing instead of just doing copy & paste ;)

Lars Ole
Lars Ole

Seems I was a bit fast on the trigger there. I'll go get my eye sight checked :)

Lars Ole
Lars Ole

Nice work indeed.

It would have been nice with a minimal example of how to instantiate and configure rather than the prose description.

Examples, like pictures, tend to speak 1000 words :)

I can't seem to find any information on the signature of the onFormat callback

Sam

Nice work mate!

 

Sorry, comments are closed for this article. Contact me if you have an inventive contribution.