jPages.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. /**
  2. * jQuery jPages v0.7
  3. * Client side pagination with jQuery
  4. * http://luis-almeida.github.com/jPages
  5. *
  6. * Licensed under the MIT license.
  7. * Copyright 2012 Luís Almeida
  8. * https://github.com/luis-almeida
  9. */
  10. ;(function($, window, document, undefined) {
  11. var name = "jPages",
  12. instance = null,
  13. defaults = {
  14. containerID: "",
  15. first: false,
  16. previous: "← previous",
  17. next: "next →",
  18. last: false,
  19. links: "numeric", // blank || title
  20. startPage: 1,
  21. perPage: 10,
  22. midRange: 5,
  23. startRange: 1,
  24. endRange: 1,
  25. keyBrowse: false,
  26. scrollBrowse: false,
  27. pause: 0,
  28. clickStop: false,
  29. delay: 50,
  30. direction: "forward", // backwards || auto || random ||
  31. animation: "", // http://daneden.me/animate/ - any entrance animations
  32. fallback: 400,
  33. minHeight: true,
  34. callback: undefined // function( pages, items ) { }
  35. };
  36. function Plugin(element, options) {
  37. this.options = $.extend({}, defaults, options);
  38. this._container = $("#" + this.options.containerID);
  39. if (!this._container.length) return;
  40. this.jQwindow = $(window);
  41. this.jQdocument = $(document);
  42. this._holder = $(element);
  43. this._nav = {};
  44. this._first = $(this.options.first);
  45. this._previous = $(this.options.previous);
  46. this._next = $(this.options.next);
  47. this._last = $(this.options.last);
  48. /* only visible items! */
  49. this._items = this._container.children(":visible");
  50. this._itemsShowing = $([]);
  51. this._itemsHiding = $([]);
  52. this._numPages = Math.ceil(this._items.length / this.options.perPage);
  53. this._currentPageNum = this.options.startPage;
  54. this._clicked = false;
  55. this._cssAnimSupport = this.getCSSAnimationSupport();
  56. this.init();
  57. }
  58. Plugin.prototype = {
  59. constructor : Plugin,
  60. getCSSAnimationSupport : function() {
  61. var animation = false,
  62. animationstring = 'animation',
  63. keyframeprefix = '',
  64. domPrefixes = 'Webkit Moz O ms Khtml'.split(' '),
  65. pfx = '',
  66. elm = this._container.get(0);
  67. if (elm.style.animationName) animation = true;
  68. if (animation === false) {
  69. for (var i = 0; i < domPrefixes.length; i++) {
  70. if (elm.style[domPrefixes[i] + 'AnimationName'] !== undefined) {
  71. pfx = domPrefixes[i];
  72. animationstring = pfx + 'Animation';
  73. keyframeprefix = '-' + pfx.toLowerCase() + '-';
  74. animation = true;
  75. break;
  76. }
  77. }
  78. }
  79. return animation;
  80. },
  81. init : function() {
  82. this.setStyles();
  83. this.setNav();
  84. this.paginate(this._currentPageNum);
  85. this.setMinHeight();
  86. },
  87. setStyles : function() {
  88. var requiredStyles = "<style>" +
  89. ".jp-invisible { visibility: hidden !important; } " +
  90. ".jp-hidden { display: none !important; }" +
  91. "</style>";
  92. $(requiredStyles).appendTo("head");
  93. if (this._cssAnimSupport && this.options.animation.length)
  94. this._items.addClass("animated jp-hidden");
  95. else this._items.hide();
  96. },
  97. setNav : function() {
  98. var navhtml = this.writeNav();
  99. this._holder.each(this.bind(function(index, element) {
  100. var holder = $(element);
  101. holder.html(navhtml);
  102. this.cacheNavElements(holder, index);
  103. this.bindNavHandlers(index);
  104. this.disableNavSelection(element);
  105. }, this));
  106. if (this.options.keyBrowse) this.bindNavKeyBrowse();
  107. if (this.options.scrollBrowse) this.bindNavScrollBrowse();
  108. },
  109. writeNav : function() {
  110. var i = 1, navhtml;
  111. navhtml = this.writeBtn("first") + this.writeBtn("previous");
  112. for (; i <= this._numPages; i++) {
  113. if (i === 1 && this.options.startRange === 0) navhtml += "<span>...</span>";
  114. if (i > this.options.startRange && i <= this._numPages - this.options.endRange)
  115. navhtml += "<a href='#' class='jp-hidden'>";
  116. else
  117. navhtml += "<a>";
  118. switch (this.options.links) {
  119. case "numeric":
  120. navhtml += i;
  121. break;
  122. case "blank":
  123. break;
  124. case "title":
  125. var title = this._items.eq(i - 1).attr("data-title");
  126. navhtml += title !== undefined ? title : "";
  127. break;
  128. }
  129. navhtml += "</a>";
  130. if (i === this.options.startRange || i === this._numPages - this.options.endRange)
  131. navhtml += "<span>...</span>";
  132. }
  133. navhtml += this.writeBtn("next") + this.writeBtn("last") + "</div>";
  134. return navhtml;
  135. },
  136. writeBtn : function(which) {
  137. return this.options[which] !== false && !$(this["_" + which]).length ?
  138. "<a class='jp-" + which + "'>" + this.options[which] + "</a>" : "";
  139. },
  140. cacheNavElements : function(holder, index) {
  141. this._nav[index] = {};
  142. this._nav[index].holder = holder;
  143. this._nav[index].first = this._first.length ? this._first : this._nav[index].holder.find("a.jp-first");
  144. this._nav[index].previous = this._previous.length ? this._previous : this._nav[index].holder.find("a.jp-previous");
  145. this._nav[index].next = this._next.length ? this._next : this._nav[index].holder.find("a.jp-next");
  146. this._nav[index].last = this._last.length ? this._last : this._nav[index].holder.find("a.jp-last");
  147. this._nav[index].fstBreak = this._nav[index].holder.find("span:first");
  148. this._nav[index].lstBreak = this._nav[index].holder.find("span:last");
  149. this._nav[index].pages = this._nav[index].holder.find("a").not(".jp-first, .jp-previous, .jp-next, .jp-last");
  150. this._nav[index].permPages =
  151. this._nav[index].pages.slice(0, this.options.startRange)
  152. .add(this._nav[index].pages.slice(this._numPages - this.options.endRange, this._numPages));
  153. this._nav[index].pagesShowing = $([]);
  154. this._nav[index].currentPage = $([]);
  155. },
  156. bindNavHandlers : function(index) {
  157. var nav = this._nav[index];
  158. // default nav
  159. nav.holder.bind("click.jPages", this.bind(function(evt) {
  160. var newPage = this.getNewPage(nav, $(evt.target));
  161. if (this.validNewPage(newPage)) {
  162. this._clicked = true;
  163. this.paginate(newPage);
  164. }
  165. evt.preventDefault();
  166. }, this));
  167. // custom first
  168. if (this._first.length) {
  169. this._first.bind("click.jPages", this.bind(function() {
  170. if (this.validNewPage(1)) {
  171. this._clicked = true;
  172. this.paginate(1);
  173. }
  174. }, this));
  175. }
  176. // custom previous
  177. if (this._previous.length) {
  178. this._previous.bind("click.jPages", this.bind(function() {
  179. var newPage = this._currentPageNum - 1;
  180. if (this.validNewPage(newPage)) {
  181. this._clicked = true;
  182. this.paginate(newPage);
  183. }
  184. }, this));
  185. }
  186. // custom next
  187. if (this._next.length) {
  188. this._next.bind("click.jPages", this.bind(function() {
  189. var newPage = this._currentPageNum + 1;
  190. if (this.validNewPage(newPage)) {
  191. this._clicked = true;
  192. this.paginate(newPage);
  193. }
  194. }, this));
  195. }
  196. // custom last
  197. if (this._last.length) {
  198. this._last.bind("click.jPages", this.bind(function() {
  199. if (this.validNewPage(this._numPages)) {
  200. this._clicked = true;
  201. this.paginate(this._numPages);
  202. }
  203. }, this));
  204. }
  205. },
  206. disableNavSelection : function(element) {
  207. if (typeof element.onselectstart != "undefined")
  208. element.onselectstart = function() {
  209. return false;
  210. };
  211. else if (typeof element.style.MozUserSelect != "undefined")
  212. element.style.MozUserSelect = "none";
  213. else
  214. element.onmousedown = function() {
  215. return false;
  216. };
  217. },
  218. bindNavKeyBrowse : function() {
  219. this.jQdocument.bind("keydown.jPages", this.bind(function(evt) {
  220. var target = evt.target.nodeName.toLowerCase();
  221. if (this.elemScrolledIntoView() && target !== "input" && target != "textarea") {
  222. var newPage = this._currentPageNum;
  223. if (evt.which == 37) newPage = this._currentPageNum - 1;
  224. if (evt.which == 39) newPage = this._currentPageNum + 1;
  225. if (this.validNewPage(newPage)) {
  226. this._clicked = true;
  227. this.paginate(newPage);
  228. }
  229. }
  230. }, this));
  231. },
  232. elemScrolledIntoView : function() {
  233. var docViewTop, docViewBottom, elemTop, elemBottom;
  234. docViewTop = this.jQwindow.scrollTop();
  235. docViewBottom = docViewTop + this.jQwindow.height();
  236. elemTop = this._container.offset().top;
  237. elemBottom = elemTop + this._container.height();
  238. return ((elemBottom >= docViewTop) && (elemTop <= docViewBottom));
  239. // comment above and uncomment below if you want keyBrowse to happen
  240. // only when container is completely visible in the page
  241. /*return ((elemBottom >= docViewTop) && (elemTop <= docViewBottom) &&
  242. (elemBottom <= docViewBottom) && (elemTop >= docViewTop) );*/
  243. },
  244. bindNavScrollBrowse : function() {
  245. this._container.bind("mousewheel.jPages DOMMouseScroll.jPages", this.bind(function(evt) {
  246. var newPage = (evt.originalEvent.wheelDelta || -evt.originalEvent.detail) > 0 ?
  247. (this._currentPageNum - 1) : (this._currentPageNum + 1);
  248. if (this.validNewPage(newPage)) {
  249. this._clicked = true;
  250. this.paginate(newPage);
  251. }
  252. evt.preventDefault();
  253. return false;
  254. }, this));
  255. },
  256. getNewPage : function(nav, target) {
  257. if (target.is(nav.currentPage)) return this._currentPageNum;
  258. if (target.is(nav.pages)) return nav.pages.index(target) + 1;
  259. if (target.is(nav.first)) return 1;
  260. if (target.is(nav.last)) return this._numPages;
  261. if (target.is(nav.previous)) return nav.pages.index(nav.currentPage);
  262. if (target.is(nav.next)) return nav.pages.index(nav.currentPage) + 2;
  263. },
  264. validNewPage : function(newPage) {
  265. return newPage !== this._currentPageNum && newPage > 0 && newPage <= this._numPages;
  266. },
  267. paginate : function(page) {
  268. var itemRange, pageInterval;
  269. itemRange = this.updateItems(page);
  270. pageInterval = this.updatePages(page);
  271. this._currentPageNum = page;
  272. if ($.isFunction(this.options.callback))
  273. this.callback(page, itemRange, pageInterval);
  274. this.updatePause();
  275. },
  276. updateItems : function(page) {
  277. var range = this.getItemRange(page);
  278. this._itemsHiding = this._itemsShowing;
  279. this._itemsShowing = this._items.slice(range.start, range.end);
  280. if (this._cssAnimSupport && this.options.animation.length) this.cssAnimations(page);
  281. else this.jQAnimations(page);
  282. return range;
  283. },
  284. getItemRange : function(page) {
  285. var range = {};
  286. range.start = (page - 1) * this.options.perPage;
  287. range.end = range.start + this.options.perPage;
  288. if (range.end > this._items.length) range.end = this._items.length;
  289. return range;
  290. },
  291. cssAnimations : function(page) {
  292. clearInterval(this._delay);
  293. this._itemsHiding
  294. .removeClass(this.options.animation + " jp-invisible")
  295. .addClass("jp-hidden");
  296. this._itemsShowing
  297. .removeClass("jp-hidden")
  298. .addClass("jp-invisible");
  299. this._itemsOriented = this.getDirectedItems(page);
  300. this._index = 0;
  301. this._delay = setInterval(this.bind(function() {
  302. if (this._index === this._itemsOriented.length) clearInterval(this._delay);
  303. else {
  304. this._itemsOriented
  305. .eq(this._index)
  306. .removeClass("jp-invisible")
  307. .addClass(this.options.animation);
  308. }
  309. this._index = this._index + 1;
  310. }, this), this.options.delay);
  311. },
  312. jQAnimations : function(page) {
  313. clearInterval(this._delay);
  314. this._itemsHiding.addClass("jp-hidden");
  315. this._itemsShowing.fadeTo(0, 0).removeClass("jp-hidden");
  316. this._itemsOriented = this.getDirectedItems(page);
  317. this._index = 0;
  318. this._delay = setInterval(this.bind(function() {
  319. if (this._index === this._itemsOriented.length) clearInterval(this._delay);
  320. else {
  321. this._itemsOriented
  322. .eq(this._index)
  323. .fadeTo(this.options.fallback, 1);
  324. }
  325. this._index = this._index + 1;
  326. }, this), this.options.delay);
  327. },
  328. getDirectedItems : function(page) {
  329. var itemsToShow;
  330. switch (this.options.direction) {
  331. case "backwards":
  332. itemsToShow = $(this._itemsShowing.get().reverse());
  333. break;
  334. case "random":
  335. itemsToShow = $(this._itemsShowing.get().sort(function() {
  336. return (Math.round(Math.random()) - 0.5);
  337. }));
  338. break;
  339. case "auto":
  340. itemsToShow = page >= this._currentPageNum ?
  341. this._itemsShowing : $(this._itemsShowing.get().reverse());
  342. break;
  343. default:
  344. itemsToShow = this._itemsShowing;
  345. }
  346. return itemsToShow;
  347. },
  348. updatePages : function(page) {
  349. var interval, index, nav;
  350. interval = this.getInterval(page);
  351. for (index in this._nav) {
  352. if (this._nav.hasOwnProperty(index)) {
  353. nav = this._nav[index];
  354. this.updateBtns(nav, page);
  355. this.updateCurrentPage(nav, page);
  356. this.updatePagesShowing(nav, interval);
  357. this.updateBreaks(nav, interval);
  358. }
  359. }
  360. return interval;
  361. },
  362. getInterval : function(page) {
  363. var neHalf, upperLimit, start, end;
  364. neHalf = Math.ceil(this.options.midRange / 2);
  365. upperLimit = this._numPages - this.options.midRange;
  366. start = page > neHalf ? Math.max(Math.min(page - neHalf, upperLimit), 0) : 0;
  367. end = page > neHalf ?
  368. Math.min(page + neHalf - (this.options.midRange % 2 > 0 ? 1 : 0), this._numPages) :
  369. Math.min(this.options.midRange, this._numPages);
  370. return {start: start,end: end};
  371. },
  372. updateBtns : function(nav, page) {
  373. if (page === 1) {
  374. nav.first.addClass("jp-disabled");
  375. nav.previous.addClass("jp-disabled");
  376. }
  377. if (page === this._numPages) {
  378. nav.next.addClass("jp-disabled");
  379. nav.last.addClass("jp-disabled");
  380. }
  381. if (this._currentPageNum === 1 && page > 1) {
  382. nav.first.removeClass("jp-disabled");
  383. nav.previous.removeClass("jp-disabled");
  384. }
  385. if (this._currentPageNum === this._numPages && page < this._numPages) {
  386. nav.next.removeClass("jp-disabled");
  387. nav.last.removeClass("jp-disabled");
  388. }
  389. },
  390. updateCurrentPage : function(nav, page) {
  391. nav.currentPage.removeClass("jp-current");
  392. nav.currentPage = nav.pages.eq(page - 1).addClass("jp-current");
  393. },
  394. updatePagesShowing : function(nav, interval) {
  395. var newRange = nav.pages.slice(interval.start, interval.end).not(nav.permPages);
  396. nav.pagesShowing.not(newRange).addClass("jp-hidden");
  397. newRange.not(nav.pagesShowing).removeClass("jp-hidden");
  398. nav.pagesShowing = newRange;
  399. },
  400. updateBreaks : function(nav, interval) {
  401. if (
  402. interval.start > this.options.startRange ||
  403. (this.options.startRange === 0 && interval.start > 0)
  404. ) nav.fstBreak.removeClass("jp-hidden");
  405. else nav.fstBreak.addClass("jp-hidden");
  406. if (interval.end < this._numPages - this.options.endRange) nav.lstBreak.removeClass("jp-hidden");
  407. else nav.lstBreak.addClass("jp-hidden");
  408. },
  409. callback : function(page, itemRange, pageInterval) {
  410. var pages = {
  411. current: page,
  412. interval: pageInterval,
  413. count: this._numPages
  414. },
  415. items = {
  416. showing: this._itemsShowing,
  417. oncoming: this._items.slice(itemRange.start + this.options.perPage, itemRange.end + this.options.perPage),
  418. range: itemRange,
  419. count: this._items.length
  420. };
  421. pages.interval.start = pages.interval.start + 1;
  422. items.range.start = items.range.start + 1;
  423. this.options.callback(pages, items);
  424. },
  425. updatePause : function() {
  426. if (this.options.pause && this._numPages > 1) {
  427. clearTimeout(this._pause);
  428. if (this.options.clickStop && this._clicked) return;
  429. else {
  430. this._pause = setTimeout(this.bind(function() {
  431. this.paginate(this._currentPageNum !== this._numPages ? this._currentPageNum + 1 : 1);
  432. }, this), this.options.pause);
  433. }
  434. }
  435. },
  436. setMinHeight : function() {
  437. if (this.options.minHeight && !this._container.is("table, tbody")) {
  438. setTimeout(this.bind(function() {
  439. this._container.css({ "min-height": this._container.css("height") });
  440. }, this), 1000);
  441. }
  442. },
  443. bind : function(fn, me) {
  444. return function() {
  445. return fn.apply(me, arguments);
  446. };
  447. },
  448. destroy : function() {
  449. this.jQdocument.unbind("keydown.jPages");
  450. this._container.unbind("mousewheel.jPages DOMMouseScroll.jPages");
  451. if (this.options.minHeight) this._container.css("min-height", "");
  452. if (this._cssAnimSupport && this.options.animation.length)
  453. this._items.removeClass("animated jp-hidden jp-invisible " + this.options.animation);
  454. else this._items.removeClass("jp-hidden").fadeTo(0, 1);
  455. this._holder.unbind("click.jPages").empty();
  456. }
  457. };
  458. $.fn[name] = function(arg) {
  459. var type = $.type(arg);
  460. if (type === "object") {
  461. if (this.length && !$.data(this, name)) {
  462. instance = new Plugin(this, arg);
  463. this.each(function() {
  464. $.data(this, name, instance);
  465. });
  466. }
  467. return this;
  468. }
  469. if (type === "string" && arg === "destroy") {
  470. instance.destroy();
  471. this.each(function() {
  472. $.removeData(this, name);
  473. });
  474. return this;
  475. }
  476. if (type === 'number' && arg % 1 === 0) {
  477. if (instance.validNewPage(arg)) instance.paginate(arg);
  478. return this;
  479. }
  480. return this;
  481. };
  482. })(jQuery, window, document);