/** * jQuery jPages v0.7 * Client side pagination with jQuery * http://luis-almeida.github.com/jPages * * Licensed under the MIT license. * Copyright 2012 Luís Almeida * https://github.com/luis-almeida */ ;(function($, window, document, undefined) { var name = "jPages", instance = null, defaults = { containerID: "", first: false, previous: "← previous", next: "next →", last: false, links: "numeric", // blank || title startPage: 1, perPage: 10, midRange: 5, startRange: 1, endRange: 1, keyBrowse: false, scrollBrowse: false, pause: 0, clickStop: false, delay: 50, direction: "forward", // backwards || auto || random || animation: "", // http://daneden.me/animate/ - any entrance animations fallback: 400, minHeight: true, callback: undefined // function( pages, items ) { } }; function Plugin(element, options) { this.options = $.extend({}, defaults, options); this._container = $("#" + this.options.containerID); if (!this._container.length) return; this.jQwindow = $(window); this.jQdocument = $(document); this._holder = $(element); this._nav = {}; this._first = $(this.options.first); this._previous = $(this.options.previous); this._next = $(this.options.next); this._last = $(this.options.last); /* only visible items! */ this._items = this._container.children(":visible"); this._itemsShowing = $([]); this._itemsHiding = $([]); this._numPages = Math.ceil(this._items.length / this.options.perPage); this._currentPageNum = this.options.startPage; this._clicked = false; this._cssAnimSupport = this.getCSSAnimationSupport(); this.init(); } Plugin.prototype = { constructor : Plugin, getCSSAnimationSupport : function() { var animation = false, animationstring = 'animation', keyframeprefix = '', domPrefixes = 'Webkit Moz O ms Khtml'.split(' '), pfx = '', elm = this._container.get(0); if (elm.style.animationName) animation = true; if (animation === false) { for (var i = 0; i < domPrefixes.length; i++) { if (elm.style[domPrefixes[i] + 'AnimationName'] !== undefined) { pfx = domPrefixes[i]; animationstring = pfx + 'Animation'; keyframeprefix = '-' + pfx.toLowerCase() + '-'; animation = true; break; } } } return animation; }, init : function() { this.setStyles(); this.setNav(); this.paginate(this._currentPageNum); this.setMinHeight(); }, setStyles : function() { var requiredStyles = ""; $(requiredStyles).appendTo("head"); if (this._cssAnimSupport && this.options.animation.length) this._items.addClass("animated jp-hidden"); else this._items.hide(); }, setNav : function() { var navhtml = this.writeNav(); this._holder.each(this.bind(function(index, element) { var holder = $(element); holder.html(navhtml); this.cacheNavElements(holder, index); this.bindNavHandlers(index); this.disableNavSelection(element); }, this)); if (this.options.keyBrowse) this.bindNavKeyBrowse(); if (this.options.scrollBrowse) this.bindNavScrollBrowse(); }, writeNav : function() { var i = 1, navhtml; navhtml = this.writeBtn("first") + this.writeBtn("previous"); for (; i <= this._numPages; i++) { if (i === 1 && this.options.startRange === 0) navhtml += "..."; if (i > this.options.startRange && i <= this._numPages - this.options.endRange) navhtml += ""; else navhtml += ""; switch (this.options.links) { case "numeric": navhtml += i; break; case "blank": break; case "title": var title = this._items.eq(i - 1).attr("data-title"); navhtml += title !== undefined ? title : ""; break; } navhtml += ""; if (i === this.options.startRange || i === this._numPages - this.options.endRange) navhtml += "..."; } navhtml += this.writeBtn("next") + this.writeBtn("last") + ""; return navhtml; }, writeBtn : function(which) { return this.options[which] !== false && !$(this["_" + which]).length ? "" + this.options[which] + "" : ""; }, cacheNavElements : function(holder, index) { this._nav[index] = {}; this._nav[index].holder = holder; this._nav[index].first = this._first.length ? this._first : this._nav[index].holder.find("a.jp-first"); this._nav[index].previous = this._previous.length ? this._previous : this._nav[index].holder.find("a.jp-previous"); this._nav[index].next = this._next.length ? this._next : this._nav[index].holder.find("a.jp-next"); this._nav[index].last = this._last.length ? this._last : this._nav[index].holder.find("a.jp-last"); this._nav[index].fstBreak = this._nav[index].holder.find("span:first"); this._nav[index].lstBreak = this._nav[index].holder.find("span:last"); this._nav[index].pages = this._nav[index].holder.find("a").not(".jp-first, .jp-previous, .jp-next, .jp-last"); this._nav[index].permPages = this._nav[index].pages.slice(0, this.options.startRange) .add(this._nav[index].pages.slice(this._numPages - this.options.endRange, this._numPages)); this._nav[index].pagesShowing = $([]); this._nav[index].currentPage = $([]); }, bindNavHandlers : function(index) { var nav = this._nav[index]; // default nav nav.holder.bind("click.jPages", this.bind(function(evt) { var newPage = this.getNewPage(nav, $(evt.target)); if (this.validNewPage(newPage)) { this._clicked = true; this.paginate(newPage); } evt.preventDefault(); }, this)); // custom first if (this._first.length) { this._first.bind("click.jPages", this.bind(function() { if (this.validNewPage(1)) { this._clicked = true; this.paginate(1); } }, this)); } // custom previous if (this._previous.length) { this._previous.bind("click.jPages", this.bind(function() { var newPage = this._currentPageNum - 1; if (this.validNewPage(newPage)) { this._clicked = true; this.paginate(newPage); } }, this)); } // custom next if (this._next.length) { this._next.bind("click.jPages", this.bind(function() { var newPage = this._currentPageNum + 1; if (this.validNewPage(newPage)) { this._clicked = true; this.paginate(newPage); } }, this)); } // custom last if (this._last.length) { this._last.bind("click.jPages", this.bind(function() { if (this.validNewPage(this._numPages)) { this._clicked = true; this.paginate(this._numPages); } }, this)); } }, disableNavSelection : function(element) { if (typeof element.onselectstart != "undefined") element.onselectstart = function() { return false; }; else if (typeof element.style.MozUserSelect != "undefined") element.style.MozUserSelect = "none"; else element.onmousedown = function() { return false; }; }, bindNavKeyBrowse : function() { this.jQdocument.bind("keydown.jPages", this.bind(function(evt) { var target = evt.target.nodeName.toLowerCase(); if (this.elemScrolledIntoView() && target !== "input" && target != "textarea") { var newPage = this._currentPageNum; if (evt.which == 37) newPage = this._currentPageNum - 1; if (evt.which == 39) newPage = this._currentPageNum + 1; if (this.validNewPage(newPage)) { this._clicked = true; this.paginate(newPage); } } }, this)); }, elemScrolledIntoView : function() { var docViewTop, docViewBottom, elemTop, elemBottom; docViewTop = this.jQwindow.scrollTop(); docViewBottom = docViewTop + this.jQwindow.height(); elemTop = this._container.offset().top; elemBottom = elemTop + this._container.height(); return ((elemBottom >= docViewTop) && (elemTop <= docViewBottom)); // comment above and uncomment below if you want keyBrowse to happen // only when container is completely visible in the page /*return ((elemBottom >= docViewTop) && (elemTop <= docViewBottom) && (elemBottom <= docViewBottom) && (elemTop >= docViewTop) );*/ }, bindNavScrollBrowse : function() { this._container.bind("mousewheel.jPages DOMMouseScroll.jPages", this.bind(function(evt) { var newPage = (evt.originalEvent.wheelDelta || -evt.originalEvent.detail) > 0 ? (this._currentPageNum - 1) : (this._currentPageNum + 1); if (this.validNewPage(newPage)) { this._clicked = true; this.paginate(newPage); } evt.preventDefault(); return false; }, this)); }, getNewPage : function(nav, target) { if (target.is(nav.currentPage)) return this._currentPageNum; if (target.is(nav.pages)) return nav.pages.index(target) + 1; if (target.is(nav.first)) return 1; if (target.is(nav.last)) return this._numPages; if (target.is(nav.previous)) return nav.pages.index(nav.currentPage); if (target.is(nav.next)) return nav.pages.index(nav.currentPage) + 2; }, validNewPage : function(newPage) { return newPage !== this._currentPageNum && newPage > 0 && newPage <= this._numPages; }, paginate : function(page) { var itemRange, pageInterval; itemRange = this.updateItems(page); pageInterval = this.updatePages(page); this._currentPageNum = page; if ($.isFunction(this.options.callback)) this.callback(page, itemRange, pageInterval); this.updatePause(); }, updateItems : function(page) { var range = this.getItemRange(page); this._itemsHiding = this._itemsShowing; this._itemsShowing = this._items.slice(range.start, range.end); if (this._cssAnimSupport && this.options.animation.length) this.cssAnimations(page); else this.jQAnimations(page); return range; }, getItemRange : function(page) { var range = {}; range.start = (page - 1) * this.options.perPage; range.end = range.start + this.options.perPage; if (range.end > this._items.length) range.end = this._items.length; return range; }, cssAnimations : function(page) { clearInterval(this._delay); this._itemsHiding .removeClass(this.options.animation + " jp-invisible") .addClass("jp-hidden"); this._itemsShowing .removeClass("jp-hidden") .addClass("jp-invisible"); this._itemsOriented = this.getDirectedItems(page); this._index = 0; this._delay = setInterval(this.bind(function() { if (this._index === this._itemsOriented.length) clearInterval(this._delay); else { this._itemsOriented .eq(this._index) .removeClass("jp-invisible") .addClass(this.options.animation); } this._index = this._index + 1; }, this), this.options.delay); }, jQAnimations : function(page) { clearInterval(this._delay); this._itemsHiding.addClass("jp-hidden"); this._itemsShowing.fadeTo(0, 0).removeClass("jp-hidden"); this._itemsOriented = this.getDirectedItems(page); this._index = 0; this._delay = setInterval(this.bind(function() { if (this._index === this._itemsOriented.length) clearInterval(this._delay); else { this._itemsOriented .eq(this._index) .fadeTo(this.options.fallback, 1); } this._index = this._index + 1; }, this), this.options.delay); }, getDirectedItems : function(page) { var itemsToShow; switch (this.options.direction) { case "backwards": itemsToShow = $(this._itemsShowing.get().reverse()); break; case "random": itemsToShow = $(this._itemsShowing.get().sort(function() { return (Math.round(Math.random()) - 0.5); })); break; case "auto": itemsToShow = page >= this._currentPageNum ? this._itemsShowing : $(this._itemsShowing.get().reverse()); break; default: itemsToShow = this._itemsShowing; } return itemsToShow; }, updatePages : function(page) { var interval, index, nav; interval = this.getInterval(page); for (index in this._nav) { if (this._nav.hasOwnProperty(index)) { nav = this._nav[index]; this.updateBtns(nav, page); this.updateCurrentPage(nav, page); this.updatePagesShowing(nav, interval); this.updateBreaks(nav, interval); } } return interval; }, getInterval : function(page) { var neHalf, upperLimit, start, end; neHalf = Math.ceil(this.options.midRange / 2); upperLimit = this._numPages - this.options.midRange; start = page > neHalf ? Math.max(Math.min(page - neHalf, upperLimit), 0) : 0; end = page > neHalf ? Math.min(page + neHalf - (this.options.midRange % 2 > 0 ? 1 : 0), this._numPages) : Math.min(this.options.midRange, this._numPages); return {start: start,end: end}; }, updateBtns : function(nav, page) { if (page === 1) { nav.first.addClass("jp-disabled"); nav.previous.addClass("jp-disabled"); } if (page === this._numPages) { nav.next.addClass("jp-disabled"); nav.last.addClass("jp-disabled"); } if (this._currentPageNum === 1 && page > 1) { nav.first.removeClass("jp-disabled"); nav.previous.removeClass("jp-disabled"); } if (this._currentPageNum === this._numPages && page < this._numPages) { nav.next.removeClass("jp-disabled"); nav.last.removeClass("jp-disabled"); } }, updateCurrentPage : function(nav, page) { nav.currentPage.removeClass("jp-current"); nav.currentPage = nav.pages.eq(page - 1).addClass("jp-current"); }, updatePagesShowing : function(nav, interval) { var newRange = nav.pages.slice(interval.start, interval.end).not(nav.permPages); nav.pagesShowing.not(newRange).addClass("jp-hidden"); newRange.not(nav.pagesShowing).removeClass("jp-hidden"); nav.pagesShowing = newRange; }, updateBreaks : function(nav, interval) { if ( interval.start > this.options.startRange || (this.options.startRange === 0 && interval.start > 0) ) nav.fstBreak.removeClass("jp-hidden"); else nav.fstBreak.addClass("jp-hidden"); if (interval.end < this._numPages - this.options.endRange) nav.lstBreak.removeClass("jp-hidden"); else nav.lstBreak.addClass("jp-hidden"); }, callback : function(page, itemRange, pageInterval) { var pages = { current: page, interval: pageInterval, count: this._numPages }, items = { showing: this._itemsShowing, oncoming: this._items.slice(itemRange.start + this.options.perPage, itemRange.end + this.options.perPage), range: itemRange, count: this._items.length }; pages.interval.start = pages.interval.start + 1; items.range.start = items.range.start + 1; this.options.callback(pages, items); }, updatePause : function() { if (this.options.pause && this._numPages > 1) { clearTimeout(this._pause); if (this.options.clickStop && this._clicked) return; else { this._pause = setTimeout(this.bind(function() { this.paginate(this._currentPageNum !== this._numPages ? this._currentPageNum + 1 : 1); }, this), this.options.pause); } } }, setMinHeight : function() { if (this.options.minHeight && !this._container.is("table, tbody")) { setTimeout(this.bind(function() { this._container.css({ }); }, this), 1000); } }, bind : function(fn, me) { return function() { return fn.apply(me, arguments); }; }, destroy : function() { this.jQdocument.unbind("keydown.jPages"); this._container.unbind("mousewheel.jPages DOMMouseScroll.jPages"); if (this.options.minHeight) this._container.css("min-height", ""); if (this._cssAnimSupport && this.options.animation.length) this._items.removeClass("animated jp-hidden jp-invisible " + this.options.animation); else this._items.removeClass("jp-hidden").fadeTo(0, 1); this._holder.unbind("click.jPages").empty(); } }; $.fn[name] = function(arg) { var type = $.type(arg); if (type === "object") { if (this.length && !$.data(this, name)) { instance = new Plugin(this, arg); this.each(function() { $.data(this, name, instance); }); } return this; } if (type === "string" && arg === "destroy") { instance.destroy(); this.each(function() { $.removeData(this, name); }); return this; } if (type === 'number' && arg % 1 === 0) { if (instance.validNewPage(arg)) instance.paginate(arg); return this; } return this; }; })(jQuery, window, document);