From c4f10de3665a75cae6453ff3357a7621cfc8f24c Mon Sep 17 00:00:00 2001 From: Mottie Date: Thu, 17 Oct 2013 22:59:07 -0500 Subject: [PATCH] Added pager widget, pager cleanup & filter widget tweaks for ajax. Fixes #388 --- addons/pager/jquery.tablesorter.pager.js | 50 +- docs/example-widget-pager-ajax.html | 379 ++++++++++++ docs/example-widget-pager.html | 648 ++++++++++++++++++++ docs/index.html | 9 +- js/jquery.tablesorter.js | 3 +- js/jquery.tablesorter.widgets.js | 8 +- js/widgets/widget-pager.js | 741 +++++++++++++++++++++++ 7 files changed, 1817 insertions(+), 21 deletions(-) create mode 100644 docs/example-widget-pager-ajax.html create mode 100644 docs/example-widget-pager.html create mode 100644 js/widgets/widget-pager.js diff --git a/addons/pager/jquery.tablesorter.pager.js b/addons/pager/jquery.tablesorter.pager.js index d720dfdf..5db28222 100644 --- a/addons/pager/jquery.tablesorter.pager.js +++ b/addons/pager/jquery.tablesorter.pager.js @@ -1,6 +1,6 @@ /*! * tablesorter pager plugin - * updated 10/11/2013 + * updated 10/17/2013 */ /*jshint browser:true, jquery:true, unused:false */ /*global toString:true */ @@ -87,7 +87,12 @@ totalRows: 0, totalPages: 0, filteredRows: 0, - filteredPages: 0 + filteredPages: 0, + currentFilters: [], + startRow: 0, + endRow: 0, + $size: null, + last: {} }; @@ -121,7 +126,7 @@ p.endRow = Math.min( p.filteredRows, p.totalRows, p.size * ( p.page + 1 ) ); out = p.$container.find(p.cssPageDisplay); // form the output string (can now get a new output string from the server) - s = ( p.ajaxData && p.ajaxData.hasOwnProperty('output') ? p.ajaxData.output || p.output : p.output ) + s = ( p.ajaxData && p.ajaxData.output ? p.ajaxData.output || p.output : p.output ) // {page} = one-based index; {page+#} = zero based index +/- value .replace(/\{page([\-+]\d+)?\}/gi, function(m,n){ return p.page + (n ? parseInt(n, 10) : 1); @@ -242,7 +247,7 @@ c.$tbodies.eq(0).empty(); } else { // process ajax object - if (toString.call(result) !== "[object Array]") { + if (!$.isArray(result)) { p.ajaxData = result; p.totalRows = result.total; th = result.headers; @@ -380,13 +385,18 @@ }, renderTable = function(table, rows, p) { - p.isDisabled = false; // needed because sorting will change the page and re-enable the pager var i, j, o, $tb, - l = rows.length, - s = ( p.page * p.size ), - e = ( s + p.size ); + l = rows && rows.length || 0, // rows may be undefined + s = ( p.page * p.size ), + e = ( s + p.size ); if ( l < 1 ) { return; } // empty table, abort! + if ( p.page >= p.totalPages ) { + // lets not render the table more than once + moveToLastPage(table, p); + } + p.isDisabled = false; // needed because sorting will change the page and re-enable the pager if (p.initialized) { $(table).trigger('pagerChange', p); } + if ( !p.removeRows ) { hideRows(table, p); } else { @@ -400,9 +410,7 @@ } ts.processTbody(table, $tb, false); } - if ( p.page >= p.totalPages ) { - moveToLastPage(table, p); - } + updatePageDisplay(table, p); if ( !p.isDisabled ) { fixHeight(table, p); } $(table).trigger('applyWidgets'); @@ -418,7 +426,7 @@ p.page = 0; p.size = p.totalRows; p.totalPages = 1; - $(table).find('tr.pagerSavedHeightSpacer').remove(); + $(table).addClass('pagerDisabled').find('tr.pagerSavedHeightSpacer').remove(); renderTable(table, table.config.rowsCopy, p); } // disable size selector @@ -429,9 +437,18 @@ moveToPage = function(table, p, flag) { if ( p.isDisabled ) { return; } - var pg = Math.min( p.totalPages, p.filteredPages ); + var l = p.last, + pg = Math.min( p.totalPages, p.filteredPages ); if ( p.page < 0 ) { p.page = 0; } if ( p.page > ( pg - 1 ) && pg !== 0 ) { p.page = pg - 1; } + // don't allow rendering multiple times on the same page/size/totalpages/filters + if (l.page === p.page && l.size === p.size && l.total === p.totalPages && l.filters === p.currentFilters ) { return; } + p.last = { + page : p.page, + size : p.size, + totalPages : p.totalPages, + currentFilters : p.currentFilters + }; if (p.ajax) { getAjax(table, p); } else if (!p.ajax) { @@ -483,7 +500,11 @@ showAllRows(table, p); p.$container.hide(); // hide pager table.config.appender = null; // remove pager appender function + p.initialized = false; $(table).unbind('destroy.pager sortEnd.pager filterEnd.pager enable.pager disable.pager'); + if (ts.storage) { + ts.storage(table, 'tablesorter-pager', ''); + } }, enablePager = function(table, p, triggered){ @@ -528,7 +549,7 @@ c.appender = $this.appender; if (p.savePages && ts.storage) { - t = ts.storage(table, 'tablesorter-pager') || {}; + t = ts.storage(table, 'tablesorter-pager') || {}; // fixes #387 p.page = isNaN(t.page) ? p.page : t.page; p.size = isNaN(t.size) ? p.size : t.size; } @@ -610,6 +631,7 @@ .bind('change', function(){ p.page = $(this).val() - 1; moveToPage(table, p); + updatePageDisplay(table, p, false) }); } diff --git a/docs/example-widget-pager-ajax.html b/docs/example-widget-pager-ajax.html new file mode 100644 index 00000000..51b354b2 --- /dev/null +++ b/docs/example-widget-pager-ajax.html @@ -0,0 +1,379 @@ + + + + + jQuery plugin: Tablesorter 2.0 - Pager Widget - Ajax + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

+ NOTE! +

+

+ + +

Demo

+Original Ajax url:
+Current Ajax url: +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ First + Prev + + Next + Last + +
12345
12345
+ First + Prev + + Next + Last + +
+ +

Javascript

+
+

+	
+ +

CSS

+
+
/* pager wrapper, div */
+.pager {
+  padding: 5px;
+}
+/* pager wrapper, in thead/tfoot */
+td.pager {
+  background-color: #e6eeee;
+}
+/* pager navigation arrows */
+.pager img {
+  vertical-align: middle;
+  margin-right: 2px;
+}
+/* pager output text */
+.pager .pagedisplay {
+  font-size: 11px;
+  padding: 0 5px 0 5px;
+  width: 50px;
+  text-align: center;
+}
+
+/*** loading ajax indeterminate progress indicator ***/
+#tablesorterPagerLoading {
+  background: rgba(255,255,255,0.8) url(icons/loading.gif) center center no-repeat;
+  position: absolute;
+  z-index: 1000;
+}
+
+/*** css used when "updateArrows" option is true ***/
+/* the pager itself gets a disabled class when the number of rows is less than the size */
+.pager.disabled {
+  display: none;
+}
+/* hide or fade out pager arrows when the first or last row is visible */
+.pager img.disabled {
+  /* visibility: hidden */
+  opacity: 0.5;
+  filter: alpha(opacity=50);
+}
+
+ +

HTML

+
+
<!-- jQuery -->
+<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8/jquery.min.js"></script>
+
+<!-- Tablesorter: required -->
+<link href="css/theme.blue.css" rel="stylesheet">
+<script src="js/jquery.tablesorter.js"></script>
+<script src="js/jquery.tablesorter.widgets.js"></script>
+
+<!-- Tablesorter: pager widget -->
+<link href="css/jquery.tablesorter.pager.css" rel="stylesheet">
+<script src="js/widget-pager.js"></script>
+
+<table class="tablesorter">
+  <thead>
+    <tr>
+      <td class="pager" colspan="5">
+        <img src="../addons/pager/icons/first.png" class="first"/>
+        <img src="../addons/pager/icons/prev.png" class="prev"/>
+        <span class="pagedisplay"></span> <!-- this can be any element, including an input -->
+        <img src="../addons/pager/icons/next.png" class="next"/>
+        <img src="../addons/pager/icons/last.png" class="last"/>
+        <select class="pagesize">
+          <option value="25">25</option>
+        </select>
+      </td>
+    </tr>
+    <tr>
+      <th>1</th> <!-- thead text will be updated from the JSON; make sure the number of columns matches the JSON data -->
+      <th>2</th>
+      <th>3</th>
+      <th>4</th>
+      <th>5</th>
+    </tr>
+  </thead>
+  <tfoot>
+    <tr>
+      <th>1</th> <!-- tfoot text will be updated at the same time as the thead -->
+      <th>2</th>
+      <th>3</th>
+      <th>4</th>
+      <th>5</th>
+    </tr>
+    <tr>
+      <td class="pager" colspan="5">
+        <img src="../addons/pager/icons/first.png" class="first"/>
+        <img src="../addons/pager/icons/prev.png" class="prev"/>
+        <span class="pagedisplay"></span> <!-- this can be any element, including an input -->
+        <img src="../addons/pager/icons/next.png" class="next"/>
+        <img src="../addons/pager/icons/last.png" class="last"/>
+        <select class="pagesize">
+          <option value="25">25</option>
+        </select>
+      </td>
+    </tr>
+  </tfoot>
+  <tbody> <!-- tbody will be loaded via JSON -->
+  </tbody>
+</table>
+ +
+ + + +
+ + + + + diff --git a/docs/example-widget-pager.html b/docs/example-widget-pager.html new file mode 100644 index 00000000..3d73f7d8 --- /dev/null +++ b/docs/example-widget-pager.html @@ -0,0 +1,648 @@ + + + + + jQuery plugin: Tablesorter 2.0 - Pager Widget + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

+ NOTE! +

+

+ +

Triggered Events

+ + +

Demo

+
+ +

+
+ First + Prev + + Next + Last + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameMajorSexEnglishJapaneseCalculusGeometry
NameMajorSexEnglishJapaneseCalculusGeometry
Student01Languagesmale80707580
Student02Mathematicsmale908810090
Student03Languagesfemale85958085
Student04Languagesmale6055100100
Student05Languagesfemale68809580
Student06Mathematicsmale1009910090
Student07Mathematicsmale85689090
Student08Languagesmale100909085
Student09Mathematicsmale80506575
Student10Languagesmale8510010090
Student11Languagesmale8685100100
Student12Mathematicsfemale100757085
Student13Languagesfemale1008010090
Student14Languagesfemale50455590
Student15Languagesmale953510090
Student16Languagesfemale100503070
Student17Languagesfemale801005565
Student18Mathematicsmale30495575
Student19Languagesmale68908870
Student20Mathematicsmale40454080
Student21Languagesmale5045100100
Student22Mathematicsmale1009910090
Student23Mathematicsmale8277079
Student24Languagesfemale100911382
Student25Mathematicsmale22968253
Student26Languagesfemale37295659
Student27Mathematicsmale86826923
Student28Languagesfemale4425431
Student29Mathematicsmale77472238
Student30Languagesfemale19352310
Student31Mathematicsmale90271750
Student32Languagesfemale60753338
Student33Mathematicsmale4313715
Student34Languagesfemale77978144
Student35Mathematicsmale5815195
Student36Languagesfemale70617094
Student37Mathematicsmale6036184
Student38Languagesfemale6339011
Student39Mathematicsmale50463238
Student40Languagesfemale5175253
Student41Mathematicsmale43342878
Student42Languagesfemale11896095
Student43Mathematicsmale48921888
Student44Languagesfemale8225973
Student45Mathematicsmale91733739
Student46Languagesfemale481210
Student47Mathematicsmale8910611
Student48Languagesfemale90322118
Student49Mathematicsmale42494972
Student50Languagesfemale56376754
+ +
+ First + Prev + + Next + Last + + +
+ +

Javascript

+
+

+	
+ +

CSS

+
+
/* pager wrapper, div */
+.tablesorter-pager {
+  padding: 5px;
+}
+/* pager wrapper, in thead/tfoot */
+td.tablesorter-pager {
+  background-color: #e6eeee;
+  margin: 0; /* needed for bootstrap .pager gets a 18px bottom margin */
+}
+/* pager navigation arrows */
+.tablesorter-pager img {
+  vertical-align: middle;
+  margin-right: 2px;
+  cursor: pointer;
+}
+
+/* pager output text */
+.tablesorter-pager .pagedisplay {
+  padding: 0 5px 0 5px;
+  width: 50px;
+  text-align: center;
+}
+
+/* pager element reset (needed for bootstrap) */
+.tablesorter-pager select {
+  margin: 0;
+  padding: 0;
+}
+
+/*** css used when "updateArrows" option is true ***/
+/* the pager itself gets a disabled class when the number of rows is less than the size */
+.tablesorter-pager.disabled {
+  display: none;
+}
+/* hide or fade out pager arrows when the first or last row is visible */
+.tablesorter-pager .disabled {
+  /* visibility: hidden */
+  opacity: 0.5;
+  filter: alpha(opacity=50);
+  cursor: default;
+}
+
+ +

HTML

+
+
<!-- jQuery -->
+<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8/jquery.min.js"></script>
+
+<!-- Tablesorter: required -->
+<link href="css/theme.blue.css" rel="stylesheet">
+<script src="js/jquery.tablesorter.js"></script>
+<script src="js/jquery.tablesorter.widgets.js"></script>
+
+<!-- Tablesorter: pager widget -->
+<link href="css/jquery.tablesorter.pager.css" rel="stylesheet">
+<script src="js/widget-pager.js"></script>
+
+<table class="tablesorter">
+<!-- view page source to see the entire table -->
+</table>
+
+<!-- pager -->
+<div id="pager" class="pager">
+  <form>
+    <img src="first.png" class="first"/>
+    <img src="prev.png" class="prev"/>
+    <span class="pagedisplay"></span> <!-- this can be any element, including an input -->
+    <img src="next.png" class="next"/>
+    <img src="last.png" class="last"/>
+    <select class="pagesize">
+      <option selected="selected" value="10">10</option>
+      <option value="20">20</option>
+      <option value="30">30</option>
+      <option value="40">40</option>
+    </select>
+  </form>
+</div>
+ +
+ + +

Pager Change Log

+ + + + +
+ + + + diff --git a/docs/index.html b/docs/index.html index 80dd361b..b364eb97 100644 --- a/docs/index.html +++ b/docs/index.html @@ -381,10 +381,11 @@
  • tablesorter basic demo using jQuery UI theme
  • tablesorter basic demo with pager plugin
  • tableSorter LESS theme; modify the colors dynamically in this LESS theme demo!
  • -
  • Custom pager control script
  • -
  • External filters using Select2 plugin
  • -
  • Column reorder widget - not working 100% with sticky headers
  • -
  • Column sum widget - still needs LOTS of work!
  • +
  • pager widget (basic & ajax demos) (v2.12).
  • +
  • Custom pager control script
  • +
  • External filters using Select2 plugin
  • +
  • Column reorder widget - not working 100% with sticky headers
  • +
  • Column sum widget - still needs LOTS of work!
  • Check out the home wiki page more demos!
  • diff --git a/js/jquery.tablesorter.js b/js/jquery.tablesorter.js index 3acb332e..142dd661 100644 --- a/js/jquery.tablesorter.js +++ b/js/jquery.tablesorter.js @@ -388,7 +388,8 @@ } } } - t.config.columns = cols; // may not be accurate if # header columns !== # tbody columns + // may not be accurate if # header columns !== # tbody columns + t.config.columns = cols + 1; // add one because it's a zero-based index return lookup; } diff --git a/js/jquery.tablesorter.widgets.js b/js/jquery.tablesorter.widgets.js index 281b35d0..129417a6 100644 --- a/js/jquery.tablesorter.widgets.js +++ b/js/jquery.tablesorter.widgets.js @@ -355,7 +355,9 @@ ts.addWidget({ } }, format: function(table, c, wo){ - if (c.parsers && !c.$table.hasClass('hasFilters')){ + if (c.$table.hasClass('hasFilters')) { return; } + // allow filter widget to work if it is being used + if (c.parsers || !c.parsers && wo.filter_serversideFiltering){ var i, j, k, l, val, ff, x, xi, st, sel, str, ft, ft2, $th, rg, s, t, dis, col, fmt = ts.formatFloat, @@ -363,7 +365,8 @@ ts.addWidget({ $ths = c.$headers, $t = c.$table.addClass('hasFilters'), b = c.$tbodies, - cols = c.parsers.length, + // c.columns defined in computeThIndexes() + cols = c.columns || c.$headers.filter('th').length, parsed, time, timer, // dig fer gold @@ -615,6 +618,7 @@ ts.addWidget({ wo.filter_regex.child = new RegExp(c.cssChildRow); wo.filter_regex.filtered = new RegExp(wo.filter_filteredRow); // don't build filter row if columnFilters is false or all columns are set to "filter-false" - issue #156 + if (wo.filter_columnFilters !== false && $ths.filter('.filter-false').length !== $ths.length){ // build filter row t = ''; diff --git a/js/widgets/widget-pager.js b/js/widgets/widget-pager.js new file mode 100644 index 00000000..1f36547c --- /dev/null +++ b/js/widgets/widget-pager.js @@ -0,0 +1,741 @@ +/* Pager widget (beta) for TableSorter 10/17/2013 */ +/*jshint browser:true, jquery:true, unused:false */ +;(function($){ +"use strict"; +var tsp, + ts = $.tablesorter; + +ts.addWidget({ + id: "pager", + priority: 60, + options : { + // output default: '{page}/{totalPages}' + // possible variables: {page}, {totalPages}, {filteredPages}, {startRow}, {endRow}, {filteredRows} and {totalRows} + pager_output: '{startRow} to {endRow} of {totalRows} rows', // '{page}/{totalPages}' + + // apply disabled classname to the pager arrows when the rows at either extreme is visible + pager_updateArrows: true, + + // starting page of the pager (zero based index) + pager_startPage: 0, + + // Number of visible rows + pager_size: 10, + + // Save pager page & size if the storage script is loaded (requires $.tablesorter.storage in jquery.tablesorter.widgets.js) + pager_savePages: true, + + // if true, the table will remain the same height no matter how many records are displayed. The space is made up by an empty + // table row set to a height to compensate; default is false + pager_fixedHeight: false, + + // remove rows from the table to speed up the sort of large tables. + // setting this to false, only hides the non-visible rows; needed if you plan to add/remove rows with the pager enabled. + pager_removeRows: false, // removing rows in larger tables speeds up the sort + + // use this format: "http://mydatabase.com?page={page}&size={size}&{sortList:col}&{filterList:fcol}" + // where {page} is replaced by the page number, {size} is replaced by the number of records to show, + // {sortList:col} adds the sortList to the url into a "col" array, and {filterList:fcol} adds + // the filterList to the url into an "fcol" array. + // So a sortList = [[2,0],[3,0]] becomes "&col[2]=0&col[3]=0" in the url + // and a filterList = [[2,Blue],[3,13]] becomes "&fcol[2]=Blue&fcol[3]=13" in the url + pager_ajaxUrl: null, + + // modify the url after all processing has been applied + pager_customAjaxUrl: function(table, url) { return url; }, + + // modify the $.ajax object to allow complete control over your ajax requests + pager_ajaxObject: { + dataType: 'json' + }, + + // process ajax so that the following information is returned: + // [ total_rows (number), rows (array of arrays), headers (array; optional) ] + // example: + // [ + // 100, // total rows + // [ + // [ "row1cell1", "row1cell2", ... "row1cellN" ], + // [ "row2cell1", "row2cell2", ... "row2cellN" ], + // ... + // [ "rowNcell1", "rowNcell2", ... "rowNcellN" ] + // ], + // [ "header1", "header2", ... "headerN" ] // optional + // ] + pager_ajaxProcessing: function(ajax){ return [ 0, [], null ]; }, + + // css class names of pager arrows + pager_css: { + container : 'tablesorter-pager', + errorRow : 'tablesorter-errorRow', // error information row (don't include period at beginning) + disabled : 'disabled' // class added to arrows @ extremes (i.e. prev/first arrows "disabled" on first page) + }, + + // jQuery selectors + pager_selectors: { + container : '.pager', // target the pager markup + first : '.first', // go to first page arrow + prev : '.prev', // previous page arrow + next : '.next', // next page arrow + last : '.last', // go to last page arrow + goto : '.gotoPage', // go to page selector - select dropdown that sets the current page + pageDisplay : '.pagedisplay', // location of where the "output" is displayed + pageSize : '.pagesize' // page size selector - select dropdown that sets the "size" option + } + }, + init: function(table){ + tsp.init(table); + }, + // only update to complete sorter initialization + format: function(table, c){ + if (!(c.pager && c.pager.initialized)){ + return tsp.initComplete(table, c); + } + tsp.moveToPage(table, c.pager, false); + }, + remove: function(table, c, wo){ + tsp.destroyPager(table, c); + } +}); + +/* pager widget functions */ +tsp = ts.pager = { + + init: function(table) { + // check if tablesorter has initialized + if (table.hasInitialized && table.config.pager.initialized) { return; } + var t, + c = table.config, + wo = c.widgetOptions, + s = wo.pager_selectors, + + // save pager variables + p = c.pager = $.extend({ + totalPages: 0, + filteredRows: 0, + filteredPages: 0, + currentFilters: [], + page: wo.pager_startPage, + size: wo.pager_size, + startRow: 0, + endRow: 0, + $size: null, + last: {} + }, c.pager); + + // added in case the pager is reinitialized after being destroyed. + p.$container = $(s.container).addClass(wo.pager_css.container).show(); + // goto selector + p.$goto = p.$container.find(s.goto); + // page size selector + p.$size = p.$container.find(s.pageSize); + + p.totalRows = c.$tbodies.eq(0).children().length; + + p.oldAjaxSuccess = p.oldAjaxSuccess || wo.pager_ajaxObject.success; + c.appender = tsp.appender; + + if (wo.pager_savePages && ts.storage) { + t = ts.storage(table, 'tablesorter-pager') || {}; // fixes #387 + p.page = isNaN(t.page) ? p.page : t.page; + p.size = isNaN(t.size) ? p.size : t.size; + } + + // clear initialized flag + p.initialized = false; + // before initialization event + c.$table.trigger('pagerBeforeInitialized', c); + + tsp.enablePager(table, c, false); + + if ( typeof(wo.pager_ajaxUrl) === 'string' ) { + // ajax pager; interact with database + p.ajax = true; + // When filtering with ajax, allow only custom filtering function, disable default filtering since it will be done server side. + wo.filter_serversideFiltering = true; + c.serverSideSorting = true; + tsp.moveToPage(table, p); + } else { + p.ajax = false; + // Regular pager; all rows stored in memory + c.$table.trigger("appendCache", true); + tsp.hideRowsSetup(table, c); + } + + }, + + initComplete: function(table, c){ + var p = c.pager; + tsp.changeHeight(table, c); + + tsp.bindEvents(table, c); + + // pager initialized + if (!p.ajax) { + p.initialized = true; + tsp.setPageSize(table, 0, c); // page size 0 is ignored + c.$table.trigger('pagerInitialized', c); + } + + }, + + bindEvents: function(table, c){ + var ctrls, fxn, + p = c.pager, + wo = c.widgetOptions, + s = wo.pager_selectors; + + c.$table + .unbind('filterStart filterEnd sortEnd disable enable destroy update pageSize '.split(' ').join('.pager ')) + .bind('filterStart.pager', function(e, filters) { + $.data(table, 'pagerUpdateTriggered', false); + p.currentFilters = filters; + }) + // update pager after filter widget completes + .bind('filterEnd.pager sortEnd.pager', function(e) { + //Prevent infinite event loops from occuring by setting this in all moveToPage calls and catching it here. + if ($.data(table, 'pagerUpdateTriggered')) { + $.data(table, 'pagerUpdateTriggered', false); + return; + } + //only run the server side sorting if it has been enabled + if (e.type === "filterEnd" || (e.type === "sortEnd" && c.serverSideSorting)) { + tsp.moveToPage(table, p, false); + } + tsp.updatePageDisplay(table, c, false); + tsp.fixHeight(table, c); + }) + .bind('disable.pager', function(e){ + e.stopPropagation(); + tsp.showAllRows(table, c); + }) + .on('enable.pager', function(e){ + e.stopPropagation(); + tsp.enablePager(table, c, true); + }) + .on('destroy.pager', function(e){ + e.stopPropagation(); + tsp.destroyPager(table, c); + }) + .on('update.pager', function(e){ + e.stopPropagation(); + tsp.hideRows(table, c); + }) + .on('pageSize.pager', function(e,v){ + e.stopPropagation(); + tsp.setPageSize(table, parseInt(v, 10) || 10, c); + tsp.hideRows(table, c); + tsp.updatePageDisplay(table, c, false); + if (p.$size.length) { p.$size.val(p.size); } // twice? + }) + .on('pageSet.pager', function(e,v){ + e.stopPropagation(); + p.page = (parseInt(v, 10) || 1) - 1; + if (p.$goto.length) { p.$goto.val(c.size); } // twice? + tsp.moveToPage(table, p); + tsp.updatePageDisplay(table, c, false); + }); + + // clicked controls + ctrls = [ s.first, s.prev, s.next, s.last ]; + fxn = [ 'moveToFirstPage', 'moveToPrevPage', 'moveToNextPage', 'moveToLastPage' ]; + p.$container.find(ctrls.join(',')) + .unbind('click.pager') + .bind('click.pager', function(){ + var i, + $c = $(this), + l = ctrls.length; + if ( !$c.hasClass(wo.pager_css.disabled) ) { + for (i = 0; i < l; i++) { + if ($c.is(ctrls[i])) { + tsp[fxn[i]](table, p); + break; + } + } + } + return false; + }); + + if ( p.$goto.length ) { + p.$goto + .unbind('change') + .bind('change', function(){ + p.page = $(this).val() - 1; + tsp.moveToPage(table, p); + tsp.updatePageDisplay(table, c, false); + }); + } + + if ( p.$size.length ) { + p.$size + .unbind('change.pager') + .bind('change.pager', function() { + p.$size.val( $(this).val() ); // in case there are more than one pagers + if ( !$(this).hasClass(wo.pager_css.disabled) ) { + tsp.setPageSize(table, parseInt( $(this).val(), 10 ), c); + tsp.changeHeight(table, c); + } + return false; + }); + } + + }, + + // hide arrows at extremes + pagerArrows: function(c, disable) { + var p = c.pager, + dis = !!disable, + wo = c.widgetOptions, + s = wo.pager_selectors, + tp = Math.min( p.totalPages, p.filteredPages ); + if ( wo.pager_updateArrows ) { + p.$container.find(s.first + ',' + s.prev).toggleClass(wo.pager_css.disabled, dis || p.page === 0); + p.$container.find(s.next + ',' + s.last).toggleClass(wo.pager_css.disabled, dis || p.page === tp - 1); + } + }, + + updatePageDisplay: function(table, c, flag) { + var i, pg, s, t, out, + wo = c.widgetOptions, + p = c.pager, + f = c.$table.hasClass('hasFilters') && !wo.pager_ajaxUrl, + t = (c.widgetOptions && c.widgetOptions.filter_filteredRow || 'filtered') + ',' + c.selectorRemove; + p.$size.removeClass(wo.pager_css.disabled).removeAttr('disabled'); + p.$goto.removeClass(wo.pager_css.disabled).removeAttr('disabled'); + p.totalPages = Math.ceil( p.totalRows / p.size ); // needed for "pageSize" method + p.filteredRows = (f) ? c.$tbodies.eq(0).children('tr:not(.' + t + ')').length : p.totalRows; + p.filteredPages = (f) ? Math.ceil( p.filteredRows / p.size ) || 1 : p.totalPages; + if ( Math.min( p.totalPages, p.filteredPages ) >= 0 ) { + t = (p.size * p.page > p.filteredRows); + p.startRow = (t) ? 1 : (p.filteredRows === 0 ? 0 : p.size * p.page + 1); + p.page = (t) ? 0 : p.page; + p.endRow = Math.min( p.filteredRows, p.totalRows, p.size * ( p.page + 1 ) ); + out = p.$container.find(wo.pager_selectors.pageDisplay); + // form the output string (can now get a new output string from the server) + s = ( p.ajaxData && p.ajaxData.output ? p.ajaxData.output || wo.pager_output : wo.pager_output ) + // {page} = one-based index; {page+#} = zero based index +/- value + .replace(/\{page([\-+]\d+)?\}/gi, function(m,n){ + return p.page + (n ? parseInt(n, 10) : 1); + }) + // {totalPages}, {extra}, {extra:0} (array) or {extra : key} (object) + .replace(/\{\w+(\s*:\s*\w+)?\}/gi, function(m){ + var t = m.replace(/[{}\s]/g,''), a = t.split(':'), d = p.ajaxData; + return a.length > 1 && d && d[a[0]] ? d[a[0]][a[1]] : p[t] || (d ? d[t] : '') || ''; + }); + if (out.length) { + out[ (out[0].tagName === 'INPUT') ? 'val' : 'html' ](s); + if ( p.$goto.length ) { + t = ''; + pg = Math.min( p.totalPages, p.filteredPages ); + for ( i = 1; i <= pg; i++ ) { + t += ''; + } + p.$goto.html(t).val( p.page + 1 ); + } + } + } + tsp.pagerArrows(c); + if (p.initialized && flag !== false) { + c.$table.trigger('pagerComplete', c); + // save pager info to storage + if (wo.pager_savePages && ts.storage) { + ts.storage(table, 'tablesorter-pager', { + page : p.page, + size : p.size + }); + } + } + }, + + fixHeight: function(table, c) { + var d, h, + p = c.pager, + wo = c.widgetOptions, + $b = c.$tbodies.eq(0); + if (wo.pager_fixedHeight) { + $b.find('tr.pagerSavedHeightSpacer').remove(); + h = $.data(table, 'pagerSavedHeight'); + if (h) { + d = h - $b.height(); + if ( d > 5 && $.data(table, 'pagerLastSize') === p.size && $b.children('tr:visible').length < p.size ) { + $b.append(''); + } + } + } + }, + + changeHeight: function(table, c) { + var $b = c.$tbodies.eq(0); + $b.find('tr.pagerSavedHeightSpacer').remove(); + $.data(table, 'pagerSavedHeight', $b.height()); + tsp.fixHeight(table, c); + $.data(table, 'pagerLastSize', c.pager.size); + }, + + hideRows: function(table, c){ + if (!c.widgetOptions.pager_ajaxUrl) { + var i, + p = c.pager, + rows = c.$tbodies.eq(0).children(), + l = rows.length, + s = ( p.page * p.size ), + e = s + p.size, + f = c.widgetOptions && c.widgetOptions.filter_filteredRow || 'filtered', + j = 0; // size counter + for ( i = 0; i < l; i++ ){ + if ( !rows[i].className.match(f) ) { + rows[i].style.display = ( j >= s && j < e ) ? '' : 'none'; + // don't count child rows + j += rows[i].className.match(c.cssChildRow + '|' + c.selectorRemove.slice(1)) ? 0 : 1; + } + } + } + }, + + hideRowsSetup: function(table, c){ + var p = c.pager; + p.size = parseInt( p.$size.val(), 10 ) || p.size; + $.data(table, 'pagerLastSize', p.size); + tsp.pagerArrows(c); + if ( !c.widgetOptions.pager_removeRows ) { + tsp.hideRows(table, c); + c.$table.on('sortEnd.pager filterEnd.pager', function(){ + tsp.hideRows(table, c); + }); + } + }, + + renderAjax: function(data, table, c, xhr, exception){ + var p = c.pager, + wo = c.widgetOptions; + // process data + if ( $.isFunction(wo.pager_ajaxProcessing) ) { + // ajaxProcessing result: [ total, rows, headers ] + var i, j, t, hsh, $f, $sh, th, d, l, $err, rr_count, + $t = c.$table, + tds = '', + result = wo.pager_ajaxProcessing(data, table) || [ 0, [] ], + hl = $t.find('thead th').length; + + $t.find('thead tr.' + wo.pager_css.errorRow).remove(); // Clean up any previous error. + + if ( exception ) { + $err = $('' + exception.message + ' (' + xhr.status + ')') + .click(function(){ + $(this).remove(); + }) + // add error row to thead instead of tbody, or clicking on the header will result in a parser error + .appendTo( $t.find('thead:first') ); + c.$tbodies.eq(0).empty(); + if (c.debug) { ts.log({ 'exception' : exception, 'jqxhr' : xhr }); } + } else { + // process ajax object + if (!$.isArray(result)) { + p.ajaxData = result; + p.totalRows = result.total; + th = result.headers; + d = result.rows; + } else { + // allow [ total, rows, headers ] or [ rows, total, headers ] + t = isNaN(result[0]) && !isNaN(result[1]); + //ensure a zero returned row count doesn't fail the logical || + rr_count = result[t ? 1 : 0]; + p.totalRows = isNaN(rr_count) ? p.totalRows || 0 : rr_count; + d = result[t ? 0 : 1] || []; // row data + th = result[2]; // headers + } + l = d.length; + if (d instanceof jQuery) { + // append jQuery object + c.$tbodies.eq(0).empty().append(d); + } else if (d.length) { + // build table from array + if ( l > 0 ) { + for ( i = 0; i < l; i++ ) { + tds += ''; + for ( j = 0; j < d[i].length; j++ ) { + // build tbody cells + tds += '' + d[i][j] + ''; + } + tds += ''; + } + } + // add rows to first tbody + c.$tbodies.eq(0).html( tds ); + } + // only add new header text if the length matches + if ( th && th.length === hl ) { + hsh = $t.hasClass('hasStickyHeaders'); + $sh = hsh ? c.$sticky.children('thead:first').children().children() : ''; + $f = $t.find('tfoot tr:first').children(); + // don't change td headers (may contain pager) + c.$headers.filter('th').each(function(j){ + var $t = $(this), icn; + // add new test within the first span it finds, or just in the header + if ( $t.find('.' + ts.css.icon).length ) { + icn = $t.find('.' + ts.css.icon).clone(true); + $t.find('.tablesorter-header-inner').html( th[j] ).append(icn); + if ( hsh && $sh.length ) { + icn = $sh.eq(j).find('.' + ts.css.icon).clone(true); + $sh.eq(j).find('.tablesorter-header-inner').html( th[j] ).append(icn); + } + } else { + $t.find('.tablesorter-header-inner').html( th[j] ); + if (hsh && $sh.length) { + $sh.eq(j).find('.tablesorter-header-inner').html( th[j] ); + } + } + $f.eq(j).html( th[j] ); + }); + } + } + if (c.showProcessing) { + ts.isProcessing(table); // remove loading icon + } + p.totalPages = Math.ceil( p.totalRows / p.size ); + tsp.updatePageDisplay(table, c); + tsp.fixHeight(table, c); + if (p.initialized) { + $t.trigger('pagerChange', c); + $t.trigger('updateComplete'); + } else { + $t.trigger('update'); + } + } + if (!p.initialized) { + p.initialized = true; + c.$table.trigger('pagerInitialized', c); + } + }, + + getAjax: function(table, c){ + var url = tsp.getAjaxUrl(table, c), + $doc = $(document), + wo = c.widgetOptions, + p = c.pager; + if ( url !== '' ) { + if (c.showProcessing) { + ts.isProcessing(table, true); // show loading icon + } + $doc.on('ajaxError.pager', function(e, xhr, settings, exception) { + tsp.renderAjax(null, table, c, xhr, exception); + $doc.unbind('ajaxError.pager'); + }); + wo.pager_ajaxObject.url = url; // from the ajaxUrl option and modified by customAjaxUrl + wo.pager_ajaxObject.success = function(data) { + tsp.renderAjax(data, table, c); + $doc.unbind('ajaxError.pager'); + if (typeof p.oldAjaxSuccess === 'function') { + p.oldAjaxSuccess(data); + } + }; + $.ajax(wo.pager_ajaxObject); + } + }, + + getAjaxUrl: function(table, c) { + var p = c.pager, + wo = c.widgetOptions, + url = (wo.pager_ajaxUrl) ? wo.pager_ajaxUrl + // allow using "{page+1}" in the url string to switch to a non-zero based index + .replace(/\{page([\-+]\d+)?\}/, function(s,n){ return p.page + (n ? parseInt(n, 10) : 0); }) + .replace(/\{size\}/g, p.size) : '', + sl = c.sortList, + fl = p.currentFilters || [], + sortCol = url.match(/\{\s*sort(?:List)?\s*:\s*(\w*)\s*\}/), + filterCol = url.match(/\{\s*filter(?:List)?\s*:\s*(\w*)\s*\}/), + arry = []; + if (sortCol) { + sortCol = sortCol[1]; + $.each(sl, function(i,v){ + arry.push(sortCol + '[' + v[0] + ']=' + v[1]); + }); + // if the arry is empty, just add the col parameter... "&{sortList:col}" becomes "&col" + url = url.replace(/\{\s*sort(?:List)?\s*:\s*(\w*)\s*\}/g, arry.length ? arry.join('&') : sortCol ); + arry = []; + } + if (filterCol) { + filterCol = filterCol[1]; + $.each(fl, function(i,v){ + if (v) { + arry.push(filterCol + '[' + i + ']=' + encodeURIComponent(v)); + } + }); + // if the arry is empty, just add the fcol parameter... "&{filterList:fcol}" becomes "&fcol" + url = url.replace(/\{\s*filter(?:List)?\s*:\s*(\w*)\s*\}/g, arry.length ? arry.join('&') : filterCol ); + } + if ( $.isFunction(wo.pager_customAjaxUrl) ) { + url = wo.pager_customAjaxUrl(table, url); + } + return url; + }, + + renderTable: function(table, rows) { + var i, $tb, + c = table.config, + p = c.pager, + wo = c.widgetOptions, + l = rows && rows.length || 0, // rows may be undefined + s = ( p.page * p.size ), + e = ( s + p.size ); + if ( l < 1 ) { return; } // empty table, abort! + if ( p.page >= p.totalPages ) { + // lets not render the table more than once + return tsp.moveToLastPage(table, p); + } + p.isDisabled = false; // needed because sorting will change the page and re-enable the pager + if (p.initialized) { c.$table.trigger('pagerChange', c); } + + if ( !wo.pager_removeRows ) { + tsp.hideRows(table, c); + } else { + if ( e > rows.length ) { + e = rows.length; + } + ts.clearTableBody(table); + $tb = ts.processTbody(table, c.$tbodies.eq(0), true); + for ( i = s; i < e; i++ ) { + $tb.append(rows[i]); + } + ts.processTbody(table, $tb, false); + } + + tsp.updatePageDisplay(table, c); + if ( !p.isDisabled ) { tsp.fixHeight(table, c); } + + wo.pager_startPage = p.page; + wo.pager_size = p.size; + + }, + + showAllRows: function(table, c){ + var p = c.pager, + wo = c.widgetOptions; + if ( p.ajax ) { + tsp.pagerArrows(c, true); + } else { + p.isDisabled = true; + $.data(table, 'pagerLastPage', p.page); + $.data(table, 'pagerLastSize', p.size); + p.page = 0; + p.size = p.totalRows; + p.totalPages = 1; + c.$table.addClass('pagerDisabled').find('tr.pagerSavedHeightSpacer').remove(); + tsp.renderTable(table, c.rowsCopy); + } + // disable size selector + p.$size.add(p.$goto).each(function(){ + $(this).addClass(wo.pager_css.disabled).attr('disabled', 'disabled'); + }); + c.$table.trigger('applyWidgets'); + }, + + moveToPage: function(table, p, flag) { + if ( p.isDisabled ) { return; } + var c = table.config, + l = p.last, + pg = Math.min( p.totalPages, p.filteredPages ); + if ( p.page < 0 ) { p.page = 0; } + if ( p.page > ( pg - 1 ) && pg !== 0 ) { p.page = pg - 1; } + // don't allow rendering multiple times on the same page/size/totalpages/filters + if (l.page === p.page && l.size === p.size && l.total === p.totalPages && l.filters === p.currentFilters ) { return; } + p.last = { + page : p.page, + size : p.size, + totalPages : p.totalPages, + currentFilters : p.currentFilters + }; + if (p.ajax) { + tsp.getAjax(table, c); + } else if (!p.ajax) { + tsp.renderTable(table, c.rowsCopy); + } + $.data(table, 'pagerLastPage', p.page); + $.data(table, 'pagerUpdateTriggered', true); + if (p.initialized && flag !== false) { + c.$table.trigger('pageMoved', c); + c.$table.trigger('applyWidgets'); + } + }, + + setPageSize: function(table, size, c) { + var p = c.pager; + p.size = size; + p.$size.val(size); + $.data(table, 'pagerLastPage', p.page); + $.data(table, 'pagerLastSize', p.size); + p.totalPages = Math.ceil( p.totalRows / p.size ); + tsp.moveToPage(table, p); + }, + + moveToFirstPage: function(table, p) { + p.page = 0; + tsp.moveToPage(table, p); + }, + + moveToLastPage: function(table, p) { + p.page = ( Math.min( p.totalPages, p.filteredPages ) - 1 ); + tsp.moveToPage(table, p); + }, + + moveToNextPage: function(table, p) { + p.page++; + if ( p.page >= ( Math.min( p.totalPages, p.filteredPages ) - 1 ) ) { + p.page = ( Math.min( p.totalPages, p.filteredPages ) - 1 ); + } + tsp.moveToPage(table, p); + }, + + moveToPrevPage: function(table, p) { + p.page--; + if ( p.page <= 0 ) { + p.page = 0; + } + tsp.moveToPage(table, p); + }, + + destroyPager: function(table, c){ + var p = c.pager; + tsp.showAllRows(table, c); + p.$container.hide(); // hide pager + c.appender = null; // remove pager appender function + p.initialized = false; + c.$table.unbind('destroy.pager sortEnd.pager filterEnd.pager enable.pager disable.pager'); + if (ts.storage) { + ts.storage(table, 'tablesorter-pager', ''); + } + }, + + enablePager: function(table, c, triggered){ + var p = c.pager, + wo = c.widgetOptions; + p.isDisabled = false; + p.page = $.data(table, 'pagerLastPage') || p.page || 0; + p.size = $.data(table, 'pagerLastSize') || parseInt(p.$size.find('option[selected]').val(), 10) || p.size; + p.$size.val(p.size); // set page size + p.totalPages = Math.ceil( Math.min( p.totalPages, p.filteredPages ) / p.size); + c.$table.removeClass('pagerDisabled'); + if ( triggered ) { + c.$table.trigger('update'); + tsp.setPageSize(table, p.size, c); + tsp.hideRowsSetup(table, c); + tsp.fixHeight(table, c); + } + }, + + appender: function(table, rows) { + var p = table.config.pager; + if ( !p.ajax ) { + table.config.rowsCopy = rows; + p.totalRows = rows.length; + p.size = $.data(table, 'pagerLastSize') || p.size; + p.totalPages = Math.ceil(p.totalRows / p.size); + tsp.moveToPage(table, p); + // tsp.renderTable(table, rows); + } + } + +}; + +})(jQuery);