jquery.tablesorter.js 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341
  1. /*!
  2. * TableSorter 2.4.8 - Client-side table sorting with ease!
  3. * @requires jQuery v1.2.6+
  4. *
  5. * Copyright (c) 2007 Christian Bach
  6. * Examples and docs at: http://tablesorter.com
  7. * Dual licensed under the MIT and GPL licenses:
  8. * http://www.opensource.org/licenses/mit-license.php
  9. * http://www.gnu.org/licenses/gpl.html
  10. *
  11. * @type jQuery
  12. * @name tablesorter
  13. * @cat Plugins/Tablesorter
  14. * @author Christian Bach/christian.bach@polyester.se
  15. * @contributor Rob Garrison/https://github.com/Mottie/tablesorter
  16. */
  17. /*jshint evil:true, browser:true, jquery:true, unused:false */
  18. /*global console:false, alert:false */
  19. !(function($) {
  20. "use strict";
  21. $.extend({
  22. tablesorter: new function() {
  23. var ts = this;
  24. ts.version = "2.4.8";
  25. ts.parsers = [];
  26. ts.widgets = [];
  27. ts.defaults = {
  28. // appearance
  29. theme : 'default', // adds tablesorter-{theme} to the table for styling
  30. widthFixed : false, // adds colgroup to fix widths of columns
  31. showProcessing : false, // show an indeterminate timer icon in the header when the table is sorted or filtered.
  32. // functionality
  33. cancelSelection : true, // prevent text selection in the header
  34. dateFormat : 'mmddyyyy', // other options: "ddmmyyy" or "yyyymmdd"
  35. sortMultiSortKey : 'shiftKey', // key used to select additional columns
  36. usNumberFormat : true, // false for German "1.234.567,89" or French "1 234 567,89"
  37. delayInit : false, // if false, the parsed table contents will not update until the first sort
  38. // sort options
  39. headers : {}, // set sorter, string, empty, locked order, sortInitialOrder, filter, etc.
  40. ignoreCase : true, // ignore case while sorting
  41. sortForce : null, // column(s) first sorted; always applied
  42. sortList : [], // Initial sort order; applied initially; updated when manually sorted
  43. sortAppend : null, // column(s) sorted last; always applied
  44. sortInitialOrder : 'asc', // sort direction on first click
  45. sortLocaleCompare: false, // replace equivalent character (accented characters)
  46. sortReset : false, // third click on the header will reset column to default - unsorted
  47. sortRestart : false, // restart sort to "sortInitialOrder" when clicking on previously unsorted columns
  48. emptyTo : 'bottom', // sort empty cell to bottom, top, none, zero
  49. stringTo : 'max', // sort strings in numerical column as max, min, top, bottom, zero
  50. textExtraction : 'simple', // text extraction method/function - function(node, table, cellIndex){}
  51. textSorter : null, // use custom text sorter - function(a,b){ return a.sort(b); } // basic sort
  52. // widget options
  53. widgets: [], // method to add widgets, e.g. widgets: ['zebra']
  54. widgetOptions : {
  55. zebra : [ 'even', 'odd' ] // zebra widget alternating row class names
  56. },
  57. initWidgets : true, // apply widgets on tablesorter initialization
  58. // callbacks
  59. initialized : null, // function(table){},
  60. onRenderHeader : null, // function(index){},
  61. // css class names
  62. tableClass : 'tablesorter',
  63. cssAsc : 'tablesorter-headerSortUp',
  64. cssChildRow : 'tablesorter-childRow', // previously "expand-child"
  65. cssDesc : 'tablesorter-headerSortDown',
  66. cssHeader : 'tablesorter-header',
  67. cssHeaderRow : 'tablesorter-headerRow',
  68. cssIcon : 'tablesorter-icon', // if this class exists, a <i> will be added to the header automatically
  69. cssInfoBlock : 'tablesorter-infoOnly', // don't sort tbody with this class name
  70. cssProcessing : 'tablesorter-processing', // processing icon applied to header during sort/filter
  71. // selectors
  72. selectorHeaders : '> thead th, > thead td',
  73. selectorSort : 'th, td', // jQuery selector of content within selectorHeaders that is clickable to trigger a sort
  74. selectorRemove : '.remove-me',
  75. // advanced
  76. debug : false,
  77. // Internal variables
  78. headerList: [],
  79. empties: {},
  80. strings: {},
  81. parsers: []
  82. // deprecated; but retained for backwards compatibility
  83. // widgetZebra: { css: ["even", "odd"] }
  84. };
  85. /* debuging utils */
  86. function log(s) {
  87. if (typeof console !== "undefined" && typeof console.log !== "undefined") {
  88. console.log(s);
  89. } else {
  90. alert(s);
  91. }
  92. }
  93. function benchmark(s, d) {
  94. log(s + " (" + (new Date().getTime() - d.getTime()) + "ms)");
  95. }
  96. ts.benchmark = benchmark;
  97. function getElementText(table, node, cellIndex) {
  98. if (!node) { return ""; }
  99. var c = table.config,
  100. t = c.textExtraction, text = "";
  101. if (t === "simple") {
  102. if (c.supportsTextContent) {
  103. text = node.textContent; // newer browsers support this
  104. } else {
  105. text = $(node).text();
  106. }
  107. } else {
  108. if (typeof(t) === "function") {
  109. text = t(node, table, cellIndex);
  110. } else if (typeof(t) === "object" && t.hasOwnProperty(cellIndex)) {
  111. text = t[cellIndex](node, table, cellIndex);
  112. } else {
  113. text = c.supportsTextContent ? node.textContent : $(node).text();
  114. }
  115. }
  116. return $.trim(text);
  117. }
  118. function detectParserForColumn(table, rows, rowIndex, cellIndex) {
  119. var i, l = ts.parsers.length,
  120. node = false,
  121. nodeValue = '',
  122. keepLooking = true;
  123. while (nodeValue === '' && keepLooking) {
  124. rowIndex++;
  125. if (rows[rowIndex]) {
  126. node = rows[rowIndex].cells[cellIndex];
  127. nodeValue = getElementText(table, node, cellIndex);
  128. if (table.config.debug) {
  129. log('Checking if value was empty on row ' + rowIndex + ', column: ' + cellIndex + ': ' + nodeValue);
  130. }
  131. } else {
  132. keepLooking = false;
  133. }
  134. }
  135. for (i = 1; i < l; i++) {
  136. if (ts.parsers[i].is(nodeValue, table, node)) {
  137. return ts.parsers[i];
  138. }
  139. }
  140. // 0 is always the generic parser (text)
  141. return ts.parsers[0];
  142. }
  143. function buildParserCache(table) {
  144. var c = table.config,
  145. tb = $(table.tBodies).filter(':not(.' + c.cssInfoBlock + ')'),
  146. rows, list, l, i, h, ch, p, parsersDebug = "";
  147. if ( tb.length === 0) { return; } // In the case of empty tables
  148. rows = tb[0].rows;
  149. if (rows[0]) {
  150. list = [];
  151. l = rows[0].cells.length;
  152. for (i = 0; i < l; i++) {
  153. // tons of thanks to AnthonyM1229 for working out the following selector (issue #74) to make this work in IE8!
  154. // More fixes to this selector to work properly in iOS and jQuery 1.8+ (issue #132 & #174)
  155. h = c.$headers.filter(':not([colspan])');
  156. h = h.add( c.$headers.filter('[colspan="1"]') ) // ie8 fix
  157. .filter('[data-column="' + i + '"]:last');
  158. ch = c.headers[i];
  159. // get column parser
  160. p = ts.getParserById( ts.getData(h, ch, 'sorter') );
  161. // empty cells behaviour - keeping emptyToBottom for backwards compatibility
  162. c.empties[i] = ts.getData(h, ch, 'empty') || c.emptyTo || (c.emptyToBottom ? 'bottom' : 'top' );
  163. // text strings behaviour in numerical sorts
  164. c.strings[i] = ts.getData(h, ch, 'string') || c.stringTo || 'max';
  165. if (!p) {
  166. p = detectParserForColumn(table, rows, -1, i);
  167. }
  168. if (c.debug) {
  169. parsersDebug += "column:" + i + "; parser:" + p.id + "; string:" + c.strings[i] + '; empty: ' + c.empties[i] + "\n";
  170. }
  171. list.push(p);
  172. }
  173. }
  174. if (c.debug) {
  175. log(parsersDebug);
  176. }
  177. return list;
  178. }
  179. /* utils */
  180. function buildCache(table) {
  181. var b = table.tBodies,
  182. tc = table.config,
  183. totalRows,
  184. totalCells,
  185. parsers = tc.parsers,
  186. t, i, j, k, c, cols, cacheTime;
  187. tc.cache = {};
  188. if (tc.debug) {
  189. cacheTime = new Date();
  190. }
  191. // processing icon
  192. if (tc.showProcessing) {
  193. ts.isProcessing(table, true);
  194. }
  195. for (k = 0; k < b.length; k++) {
  196. tc.cache[k] = { row: [], normalized: [] };
  197. // ignore tbodies with class name from css.cssInfoBlock
  198. if (!$(b[k]).hasClass(tc.cssInfoBlock)) {
  199. totalRows = (b[k] && b[k].rows.length) || 0;
  200. totalCells = (b[k].rows[0] && b[k].rows[0].cells.length) || 0;
  201. for (i = 0; i < totalRows; ++i) {
  202. /** Add the table data to main data array */
  203. c = $(b[k].rows[i]);
  204. cols = [];
  205. // if this is a child row, add it to the last row's children and continue to the next row
  206. if (c.hasClass(tc.cssChildRow)) {
  207. tc.cache[k].row[tc.cache[k].row.length - 1] = tc.cache[k].row[tc.cache[k].row.length - 1].add(c);
  208. // go to the next for loop
  209. continue;
  210. }
  211. tc.cache[k].row.push(c);
  212. for (j = 0; j < totalCells; ++j) {
  213. t = getElementText(table, c[0].cells[j], j);
  214. // allow parsing if the string is empty, previously parsing would change it to zero,
  215. // in case the parser needs to extract data from the table cell attributes
  216. cols.push( parsers[j].format(t, table, c[0].cells[j], j) );
  217. }
  218. cols.push(tc.cache[k].normalized.length); // add position for rowCache
  219. tc.cache[k].normalized.push(cols);
  220. }
  221. }
  222. }
  223. if (tc.showProcessing) {
  224. ts.isProcessing(table); // remove processing icon
  225. }
  226. if (tc.debug) {
  227. benchmark("Building cache for " + totalRows + " rows", cacheTime);
  228. }
  229. }
  230. // init flag (true) used by pager plugin to prevent widget application
  231. function appendToTable(table, init) {
  232. var c = table.config,
  233. b = table.tBodies,
  234. rows = [],
  235. c2 = c.cache,
  236. r, n, totalRows, checkCell, $bk, $tb,
  237. i, j, k, l, pos, appendTime;
  238. if (c.debug) {
  239. appendTime = new Date();
  240. }
  241. for (k = 0; k < b.length; k++) {
  242. $bk = $(b[k]);
  243. if (!$bk.hasClass(c.cssInfoBlock)) {
  244. // get tbody
  245. $tb = ts.processTbody(table, $bk, true);
  246. r = c2[k].row;
  247. n = c2[k].normalized;
  248. totalRows = n.length;
  249. checkCell = totalRows ? (n[0].length - 1) : 0;
  250. for (i = 0; i < totalRows; i++) {
  251. pos = n[i][checkCell];
  252. rows.push(r[pos]);
  253. // removeRows used by the pager plugin
  254. if (!c.appender || !c.removeRows) {
  255. l = r[pos].length;
  256. for (j = 0; j < l; j++) {
  257. $tb.append(r[pos][j]);
  258. }
  259. }
  260. }
  261. // restore tbody
  262. ts.processTbody(table, $tb, false);
  263. }
  264. }
  265. if (c.appender) {
  266. c.appender(table, rows);
  267. }
  268. if (c.debug) {
  269. benchmark("Rebuilt table", appendTime);
  270. }
  271. // apply table widgets
  272. if (!init) { ts.applyWidget(table); }
  273. // trigger sortend
  274. $(table).trigger("sortEnd", table);
  275. }
  276. // computeTableHeaderCellIndexes from:
  277. // http://www.javascripttoolbox.com/lib/table/examples.php
  278. // http://www.javascripttoolbox.com/temp/table_cellindex.html
  279. function computeThIndexes(t) {
  280. var matrix = [],
  281. lookup = {},
  282. trs = $(t).find('thead:eq(0) tr, tfoot tr'),
  283. i, j, k, l, c, cells, rowIndex, cellId, rowSpan, colSpan, firstAvailCol, matrixrow;
  284. for (i = 0; i < trs.length; i++) {
  285. cells = trs[i].cells;
  286. for (j = 0; j < cells.length; j++) {
  287. c = cells[j];
  288. rowIndex = c.parentNode.rowIndex;
  289. cellId = rowIndex + "-" + c.cellIndex;
  290. rowSpan = c.rowSpan || 1;
  291. colSpan = c.colSpan || 1;
  292. if (typeof(matrix[rowIndex]) === "undefined") {
  293. matrix[rowIndex] = [];
  294. }
  295. // Find first available column in the first row
  296. for (k = 0; k < matrix[rowIndex].length + 1; k++) {
  297. if (typeof(matrix[rowIndex][k]) === "undefined") {
  298. firstAvailCol = k;
  299. break;
  300. }
  301. }
  302. lookup[cellId] = firstAvailCol;
  303. // add data-column
  304. $(c).attr({ 'data-column' : firstAvailCol }); // 'data-row' : rowIndex
  305. for (k = rowIndex; k < rowIndex + rowSpan; k++) {
  306. if (typeof(matrix[k]) === "undefined") {
  307. matrix[k] = [];
  308. }
  309. matrixrow = matrix[k];
  310. for (l = firstAvailCol; l < firstAvailCol + colSpan; l++) {
  311. matrixrow[l] = "x";
  312. }
  313. }
  314. }
  315. }
  316. return lookup;
  317. }
  318. function formatSortingOrder(v) {
  319. // look for "d" in "desc" order; return true
  320. return (/^d/i.test(v) || v === 1);
  321. }
  322. function buildHeaders(table) {
  323. var header_index = computeThIndexes(table), ch, $t,
  324. t, lock, time, $tableHeaders, c = table.config;
  325. c.headerList = [];
  326. if (c.debug) {
  327. time = new Date();
  328. }
  329. $tableHeaders = $(table).find(c.selectorHeaders).each(function(index) {
  330. $t = $(this);
  331. ch = c.headers[index];
  332. t = c.cssIcon ? '<i class="' + c.cssIcon + '"></i>' : ''; // add icon if cssIcon option exists
  333. this.innerHTML = '<div class="tablesorter-header-inner">' + this.innerHTML + t + '</div>'; // faster than wrapInner
  334. if (c.onRenderHeader) { c.onRenderHeader.apply($t, [index]); }
  335. this.column = header_index[this.parentNode.rowIndex + "-" + this.cellIndex];
  336. this.order = formatSortingOrder( ts.getData($t, ch, 'sortInitialOrder') || c.sortInitialOrder ) ? [1,0,2] : [0,1,2];
  337. this.count = -1; // set to -1 because clicking on the header automatically adds one
  338. if (ts.getData($t, ch, 'sorter') === 'false') {
  339. this.sortDisabled = true;
  340. $t.addClass('sorter-false');
  341. } else {
  342. $t.removeClass('sorter-false');
  343. }
  344. this.lockedOrder = false;
  345. lock = ts.getData($t, ch, 'lockedOrder') || false;
  346. if (typeof(lock) !== 'undefined' && lock !== false) {
  347. this.order = this.lockedOrder = formatSortingOrder(lock) ? [1,1,1] : [0,0,0];
  348. }
  349. $t.addClass( (this.sortDisabled ? 'sorter-false ' : ' ') + c.cssHeader );
  350. // add cell to headerList
  351. c.headerList[index] = this;
  352. // add to parent in case there are multiple rows
  353. $t.parent().addClass(c.cssHeaderRow);
  354. });
  355. if (table.config.debug) {
  356. benchmark("Built headers:", time);
  357. log($tableHeaders);
  358. }
  359. return $tableHeaders;
  360. }
  361. function setHeadersCss(table) {
  362. var f, i, j, l,
  363. c = table.config,
  364. list = c.sortList,
  365. css = [c.cssDesc, c.cssAsc],
  366. // find the footer
  367. $t = $(table).find('tfoot tr').children().removeClass(css.join(' '));
  368. // remove all header information
  369. c.$headers.removeClass(css.join(' '));
  370. l = list.length;
  371. for (i = 0; i < l; i++) {
  372. // direction = 2 means reset!
  373. if (list[i][1] !== 2) {
  374. // multicolumn sorting updating - choose the :last in case there are nested columns
  375. f = c.$headers.not('.sorter-false').filter('[data-column="' + list[i][0] + '"]' + (l === 1 ? ':last' : '') );
  376. if (f.length) {
  377. for (j = 0; j < f.length; j++) {
  378. if (!f[j].sortDisabled) {
  379. f.eq(j).addClass(css[list[i][1]]);
  380. // add sorted class to footer, if it exists
  381. if ($t.length) {
  382. $t.filter('[data-column="' + list[i][0] + '"]').eq(j).addClass(css[list[i][1]]);
  383. }
  384. }
  385. }
  386. }
  387. }
  388. }
  389. }
  390. function fixColumnWidth(table) {
  391. if (table.config.widthFixed && $(table).find('colgroup').length === 0) {
  392. var colgroup = $('<colgroup>'),
  393. overallWidth = $(table).width();
  394. $("tr:first td", table.tBodies[0]).each(function() {
  395. colgroup.append($('<col>').css('width', parseInt(($(this).width()/overallWidth)*1000, 10)/10 + '%'));
  396. });
  397. $(table).prepend(colgroup);
  398. }
  399. }
  400. function updateHeaderSortCount(table, list) {
  401. var s, o, c = table.config,
  402. l = c.headerList.length,
  403. sl = list || c.sortList;
  404. c.sortList = [];
  405. $.each(sl, function(i,v){
  406. // ensure all sortList values are numeric - fixes #127
  407. s = [ parseInt(v[0], 10), parseInt(v[1], 10) ];
  408. // make sure header exists
  409. o = c.headerList[s[0]];
  410. if (o) { // prevents error if sorton array is wrong
  411. c.sortList.push(s);
  412. o.count = s[1] % (c.sortReset ? 3 : 2);
  413. }
  414. });
  415. }
  416. function getCachedSortType(parsers, i) {
  417. return (parsers && parsers[i]) ? parsers[i].type || '' : '';
  418. }
  419. // sort multiple columns
  420. function multisort(table) {
  421. var dynamicExp, sortWrapper, col, mx = 0, dir = 0, tc = table.config,
  422. sortList = tc.sortList, l = sortList.length, bl = table.tBodies.length,
  423. sortTime, i, j, k, c, cache, lc, s, e, order, orgOrderCol;
  424. if (tc.debug) { sortTime = new Date(); }
  425. for (k = 0; k < bl; k++) {
  426. dynamicExp = "sortWrapper = function(a,b) {";
  427. cache = tc.cache[k];
  428. lc = cache.normalized.length;
  429. for (i = 0; i < l; i++) {
  430. c = sortList[i][0];
  431. order = sortList[i][1];
  432. // fallback to natural sort since it is more robust
  433. s = /n/i.test(getCachedSortType(tc.parsers, c)) ? "Numeric" : "Text";
  434. s += order === 0 ? "" : "Desc";
  435. e = "e" + i;
  436. // get max column value (ignore sign)
  437. if (/Numeric/.test(s) && tc.strings[c]) {
  438. for (j = 0; j < lc; j++) {
  439. col = Math.abs(parseFloat(cache.normalized[j][c]));
  440. mx = Math.max( mx, isNaN(col) ? 0 : col );
  441. }
  442. // sort strings in numerical columns
  443. if (typeof(tc.string[tc.strings[c]]) === 'boolean') {
  444. dir = (order === 0 ? 1 : -1) * (tc.string[tc.strings[c]] ? -1 : 1);
  445. } else {
  446. dir = (tc.strings[c]) ? tc.string[tc.strings[c]] || 0 : 0;
  447. }
  448. }
  449. dynamicExp += "var " + e + " = $.tablesorter.sort" + s + "(table,a[" + c + "],b[" + c + "]," + c + "," + mx + "," + dir + "); ";
  450. dynamicExp += "if (" + e + ") { return " + e + "; } ";
  451. dynamicExp += "else { ";
  452. }
  453. // if value is the same keep orignal order
  454. orgOrderCol = (cache.normalized && cache.normalized[0]) ? cache.normalized[0].length - 1 : 0;
  455. dynamicExp += "return a[" + orgOrderCol + "]-b[" + orgOrderCol + "];";
  456. for (i=0; i < l; i++) {
  457. dynamicExp += "}; ";
  458. }
  459. dynamicExp += "return 0; ";
  460. dynamicExp += "}; ";
  461. cache.normalized.sort(eval(dynamicExp)); // sort using eval expression
  462. }
  463. if (tc.debug) { benchmark("Sorting on " + sortList.toString() + " and dir " + order + " time", sortTime); }
  464. }
  465. function resortComplete($table, callback){
  466. $table.trigger('updateComplete');
  467. if (typeof callback === "function") {
  468. callback($table[0]);
  469. }
  470. }
  471. function checkResort($table, flag, callback) {
  472. if (flag !== false) {
  473. $table.trigger("sorton", [$table[0].config.sortList, function(){
  474. resortComplete($table, callback);
  475. }]);
  476. } else {
  477. resortComplete($table, callback);
  478. }
  479. }
  480. /* public methods */
  481. ts.construct = function(settings) {
  482. return this.each(function() {
  483. // if no thead or tbody, or tablesorter is already present, quit
  484. if (!this.tHead || this.tBodies.length === 0 || this.hasInitialized === true) { return; }
  485. // declare
  486. var $cell, $this = $(this),
  487. c, i, j, k = '', a, s, o, downTime,
  488. m = $.metadata;
  489. // initialization flag
  490. this.hasInitialized = false;
  491. // new blank config object
  492. this.config = {};
  493. // merge and extend
  494. c = $.extend(true, this.config, ts.defaults, settings);
  495. // save the settings where they read
  496. $.data(this, "tablesorter", c);
  497. if (c.debug) { $.data( this, 'startoveralltimer', new Date()); }
  498. // constants
  499. c.supportsTextContent = $('<span>x</span>')[0].textContent === 'x';
  500. c.supportsDataObject = parseFloat($.fn.jquery) >= 1.4;
  501. // digit sort text location; keeping max+/- for backwards compatibility
  502. c.string = { 'max': 1, 'min': -1, 'max+': 1, 'max-': -1, 'zero': 0, 'none': 0, 'null': 0, 'top': true, 'bottom': false };
  503. // add table theme class only if there isn't already one there
  504. if (!/tablesorter\-/.test($this.attr('class'))) {
  505. k = (c.theme !== '' ? ' tablesorter-' + c.theme : '');
  506. }
  507. $this.addClass(c.tableClass + k);
  508. // build headers
  509. c.$headers = buildHeaders(this);
  510. // try to auto detect column type, and store in tables config
  511. c.parsers = buildParserCache(this);
  512. // build the cache for the tbody cells
  513. // delayInit will delay building the cache until the user starts a sort
  514. if (!c.delayInit) { buildCache(this); }
  515. // apply event handling to headers
  516. // this is to big, perhaps break it out?
  517. c.$headers
  518. // http://stackoverflow.com/questions/5312849/jquery-find-self
  519. .find('*').andSelf().filter(c.selectorSort)
  520. .unbind('mousedown.tablesorter mouseup.tablesorter')
  521. .bind('mousedown.tablesorter mouseup.tablesorter', function(e, external) {
  522. // jQuery v1.2.6 doesn't have closest()
  523. var $cell = this.tagName.match('TH|TD') ? $(this) : $(this).parents('th, td').filter(':last'), cell = $cell[0];
  524. // only recognize left clicks
  525. if ((e.which || e.button) !== 1) { return false; }
  526. // set timer on mousedown
  527. if (e.type === 'mousedown') {
  528. downTime = new Date().getTime();
  529. return e.target.tagName === "INPUT" ? '' : !c.cancelSelection;
  530. }
  531. // ignore long clicks (prevents resizable widget from initializing a sort)
  532. if (external !== true && (new Date().getTime() - downTime > 250)) { return false; }
  533. if (c.delayInit && !c.cache) { buildCache($this[0]); }
  534. if (!cell.sortDisabled) {
  535. // Only call sortStart if sorting is enabled
  536. $this.trigger("sortStart", $this[0]);
  537. // store exp, for speed
  538. // $cell = $(this);
  539. k = !e[c.sortMultiSortKey];
  540. // get current column sort order
  541. cell.count = (cell.count + 1) % (c.sortReset ? 3 : 2);
  542. // reset all sorts on non-current column - issue #30
  543. if (c.sortRestart) {
  544. i = cell;
  545. c.$headers.each(function() {
  546. // only reset counts on columns that weren't just clicked on and if not included in a multisort
  547. if (this !== i && (k || !$(this).is('.' + c.cssDesc + ',.' + c.cssAsc))) {
  548. this.count = -1;
  549. }
  550. });
  551. }
  552. // get current column index
  553. i = cell.column;
  554. // user only wants to sort on one column
  555. if (k) {
  556. // flush the sort list
  557. c.sortList = [];
  558. if (c.sortForce !== null) {
  559. a = c.sortForce;
  560. for (j = 0; j < a.length; j++) {
  561. if (a[j][0] !== i) {
  562. c.sortList.push(a[j]);
  563. }
  564. }
  565. }
  566. // add column to sort list
  567. o = cell.order[cell.count];
  568. if (o < 2) {
  569. c.sortList.push([i, o]);
  570. // add other columns if header spans across multiple
  571. if (cell.colSpan > 1) {
  572. for (j = 1; j < cell.colSpan; j++) {
  573. c.sortList.push([i + j, o]);
  574. }
  575. }
  576. }
  577. // multi column sorting
  578. } else {
  579. // get rid of the sortAppend before adding more - fixes issue #115
  580. if (c.sortAppend && c.sortList.length > 1) {
  581. if (ts.isValueInArray(c.sortAppend[0][0], c.sortList)) {
  582. c.sortList.pop();
  583. }
  584. }
  585. // the user has clicked on an already sorted column
  586. if (ts.isValueInArray(i, c.sortList)) {
  587. // reverse the sorting direction for all tables
  588. for (j = 0; j < c.sortList.length; j++) {
  589. s = c.sortList[j];
  590. o = c.headerList[s[0]];
  591. if (s[0] === i) {
  592. s[1] = o.order[o.count];
  593. if (s[1] === 2) {
  594. c.sortList.splice(j,1);
  595. o.count = -1;
  596. }
  597. }
  598. }
  599. } else {
  600. // add column to sort list array
  601. o = cell.order[cell.count];
  602. if (o < 2) {
  603. c.sortList.push([i, o]);
  604. // add other columns if header spans across multiple
  605. if (cell.colSpan > 1) {
  606. for (j = 1; j < cell.colSpan; j++) {
  607. c.sortList.push([i + j, o]);
  608. }
  609. }
  610. }
  611. }
  612. }
  613. if (c.sortAppend !== null) {
  614. a = c.sortAppend;
  615. for (j = 0; j < a.length; j++) {
  616. if (a[j][0] !== i) {
  617. c.sortList.push(a[j]);
  618. }
  619. }
  620. }
  621. // sortBegin event triggered immediately before the sort
  622. $this.trigger("sortBegin", $this[0]);
  623. // setTimeout needed so the processing icon shows up
  624. setTimeout(function(){
  625. // set css for headers
  626. setHeadersCss($this[0]);
  627. multisort($this[0]);
  628. appendToTable($this[0]);
  629. }, 1);
  630. }
  631. });
  632. if (c.cancelSelection) {
  633. // cancel selection
  634. c.$headers.each(function() {
  635. this.onselectstart = function() {
  636. return false;
  637. };
  638. });
  639. }
  640. // apply easy methods that trigger binded events
  641. $this
  642. .unbind('sortReset update updateCell addRows sorton appendCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave')
  643. .bind("sortReset", function(){
  644. c.sortList = [];
  645. setHeadersCss(this);
  646. multisort(this);
  647. appendToTable(this);
  648. })
  649. .bind("update", function(e, resort, callback) {
  650. // remove rows/elements before update
  651. $(c.selectorRemove, this).remove();
  652. // rebuild parsers
  653. c.parsers = buildParserCache(this);
  654. // rebuild the cache map
  655. buildCache(this);
  656. checkResort($this, resort, callback);
  657. })
  658. .bind("updateCell", function(e, cell, resort, callback) {
  659. // get position from the dom
  660. var l, row, icell,
  661. t = this, $tb = $(this).find('tbody'),
  662. // update cache - format: function(s, table, cell, cellIndex)
  663. // no closest in jQuery v1.2.6 - tbdy = $tb.index( $(cell).closest('tbody') ),$row = $(cell).closest('tr');
  664. tbdy = $tb.index( $(cell).parents('tbody').filter(':last') ),
  665. $row = $(cell).parents('tr').filter(':last');
  666. // tbody may not exist if update is initialized while tbody is removed for processing
  667. if ($tb.length && tbdy >= 0) {
  668. row = $tb.eq(tbdy).find('tr').index( $row );
  669. icell = cell.cellIndex;
  670. l = t.config.cache[tbdy].normalized[row].length - 1;
  671. t.config.cache[tbdy].row[t.config.cache[tbdy].normalized[row][l]] = $row;
  672. t.config.cache[tbdy].normalized[row][icell] = c.parsers[icell].format( getElementText(t, cell, icell), t, cell, icell );
  673. checkResort($this, resort, callback);
  674. }
  675. })
  676. .bind("addRows", function(e, $row, resort, callback) {
  677. var i, rows = $row.filter('tr').length,
  678. dat = [], l = $row[0].cells.length, t = this,
  679. tbdy = $(this).find('tbody').index( $row.closest('tbody') );
  680. // add each row
  681. for (i = 0; i < rows; i++) {
  682. // add each cell
  683. for (j = 0; j < l; j++) {
  684. dat[j] = c.parsers[j].format( getElementText(t, $row[i].cells[j], j), t, $row[i].cells[j], j );
  685. }
  686. // add the row index to the end
  687. dat.push(c.cache[tbdy].row.length);
  688. // update cache
  689. c.cache[tbdy].row.push([$row[i]]);
  690. c.cache[tbdy].normalized.push(dat);
  691. dat = [];
  692. }
  693. // resort using current settings
  694. checkResort($this, resort, callback);
  695. })
  696. .bind("sorton", function(e, list, callback, init) {
  697. $(this).trigger("sortStart", this);
  698. // update header count index
  699. updateHeaderSortCount(this, list);
  700. // set css for headers
  701. setHeadersCss(this);
  702. // sort the table and append it to the dom
  703. multisort(this);
  704. appendToTable(this, init);
  705. if (typeof callback === "function") {
  706. callback(this);
  707. }
  708. })
  709. .bind("appendCache", function(e, callback, init) {
  710. appendToTable(this, init);
  711. if (typeof callback === "function") {
  712. callback(this);
  713. }
  714. })
  715. .bind("applyWidgetId", function(e, id) {
  716. ts.getWidgetById(id).format(this, c, c.widgetOptions);
  717. })
  718. .bind("applyWidgets", function(e, init) {
  719. // apply widgets
  720. ts.applyWidget(this, init);
  721. })
  722. .bind("refreshWidgets", function(e, all, dontapply){
  723. ts.refreshWidgets(this, all, dontapply);
  724. })
  725. .bind("destroy", function(e, c, cb){
  726. ts.destroy(this, c, cb);
  727. });
  728. // get sort list from jQuery data or metadata
  729. // in jQuery < 1.4, an error occurs when calling $this.data()
  730. if (c.supportsDataObject && typeof $this.data().sortlist !== 'undefined') {
  731. c.sortList = $this.data().sortlist;
  732. } else if (m && ($this.metadata() && $this.metadata().sortlist)) {
  733. c.sortList = $this.metadata().sortlist;
  734. }
  735. // apply widget init code
  736. ts.applyWidget(this, true);
  737. // if user has supplied a sort list to constructor
  738. if (c.sortList.length > 0) {
  739. $this.trigger("sorton", [c.sortList, {}, !c.initWidgets]);
  740. } else if (c.initWidgets) {
  741. // apply widget format
  742. ts.applyWidget(this);
  743. }
  744. // fixate columns if the users supplies the fixedWidth option
  745. // do this after theme has been applied
  746. fixColumnWidth(this);
  747. // show processesing icon
  748. if (c.showProcessing) {
  749. $this
  750. .unbind('sortBegin sortEnd')
  751. .bind('sortBegin sortEnd', function(e) {
  752. ts.isProcessing($this[0], e.type === 'sortBegin');
  753. });
  754. }
  755. // initialized
  756. this.hasInitialized = true;
  757. if (c.debug) {
  758. ts.benchmark("Overall initialization time", $.data( this, 'startoveralltimer'));
  759. }
  760. $this.trigger('tablesorter-initialized', this);
  761. if (typeof c.initialized === 'function') { c.initialized(this); }
  762. });
  763. };
  764. // *** Process table ***
  765. // add processing indicator
  766. ts.isProcessing = function(table, toggle, $ths) {
  767. var c = table.config,
  768. // default to all headers
  769. $h = $ths || $(table).find('.' + c.cssHeader);
  770. if (toggle) {
  771. if (c.sortList.length > 0) {
  772. // get headers from the sortList
  773. $h = $h.filter(function(){
  774. // get data-column from attr to keep compatibility with jQuery 1.2.6
  775. return this.sortDisabled ? false : ts.isValueInArray( parseFloat($(this).attr('data-column')), c.sortList);
  776. });
  777. }
  778. $h.addClass(c.cssProcessing);
  779. } else {
  780. $h.removeClass(c.cssProcessing);
  781. }
  782. };
  783. // detach tbody but save the position
  784. // don't use tbody because there are portions that look for a tbody index (updateCell)
  785. ts.processTbody = function(table, $tb, getIt){
  786. var t, holdr;
  787. if (getIt) {
  788. $tb.before('<span class="tablesorter-savemyplace"/>');
  789. holdr = ($.fn.detach) ? $tb.detach() : $tb.remove();
  790. return holdr;
  791. }
  792. holdr = $(table).find('span.tablesorter-savemyplace');
  793. $tb.insertAfter( holdr );
  794. holdr.remove();
  795. };
  796. ts.clearTableBody = function(table) {
  797. $(table.tBodies).filter(':not(.' + table.config.cssInfoBlock + ')').empty();
  798. };
  799. ts.destroy = function(table, removeClasses, callback){
  800. var $t = $(table), c = table.config,
  801. $h = $t.find('thead:first');
  802. // clear flag in case the plugin is initialized again
  803. table.hasInitialized = false;
  804. // remove widget added rows
  805. $h.find('tr:not(.' + c.cssHeaderRow + ')').remove();
  806. // remove resizer widget stuff
  807. $h.find('.tablesorter-resizer').remove();
  808. // remove all widgets
  809. ts.refreshWidgets(table, true, true);
  810. // disable tablesorter
  811. $t
  812. .removeData('tablesorter')
  813. .unbind('sortReset update updateCell addRows sorton appendCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave')
  814. .find('.' + c.cssHeader)
  815. .unbind('click mousedown mousemove mouseup')
  816. .removeClass(c.cssHeader + ' ' + c.cssAsc + ' ' + c.cssDesc)
  817. .find('.tablesorter-header-inner').each(function(){
  818. if (c.cssIcon !== '') { $(this).find('.' + c.cssIcon).remove(); }
  819. $(this).replaceWith( $(this).contents() );
  820. });
  821. if (removeClasses !== false) {
  822. $t.removeClass(c.tableClass);
  823. }
  824. if (typeof callback === 'function') {
  825. callback(table);
  826. }
  827. };
  828. // *** sort functions ***
  829. // regex used in natural sort
  830. ts.regex = [
  831. /(^-?[0-9]+(\.?[0-9]*)[df]?e?[0-9]?$|^0x[0-9a-f]+$|[0-9]+)/gi, // chunk/tokenize numbers & letters
  832. /(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/, //date
  833. /^0x[0-9a-f]+$/i // hex
  834. ];
  835. // Natural sort - https://github.com/overset/javascript-natural-sort
  836. ts.sortText = function(table, a, b, col) {
  837. if (a === b) { return 0; }
  838. var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ],
  839. r = ts.regex, xN, xD, yN, yD, xF, yF, i, mx;
  840. if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : -e || -1; }
  841. if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : e || 1; }
  842. if (typeof c.textSorter === 'function') { return c.textSorter(a, b, table, col); }
  843. // chunk/tokenize
  844. xN = a.replace(r[0], '\\0$1\\0').replace(/\\0$/, '').replace(/^\\0/, '').split('\\0');
  845. yN = b.replace(r[0], '\\0$1\\0').replace(/\\0$/, '').replace(/^\\0/, '').split('\\0');
  846. // numeric, hex or date detection
  847. xD = parseInt(a.match(r[2]),16) || (xN.length !== 1 && a.match(r[1]) && Date.parse(a));
  848. yD = parseInt(b.match(r[2]),16) || (xD && b.match(r[1]) && Date.parse(b)) || null;
  849. // first try and sort Hex codes or Dates
  850. if (yD) {
  851. if ( xD < yD ) { return -1; }
  852. if ( xD > yD ) { return 1; }
  853. }
  854. mx = Math.max(xN.length, yN.length);
  855. // natural sorting through split numeric strings and default strings
  856. for (i = 0; i < mx; i++) {
  857. // find floats not starting with '0', string or 0 if not defined
  858. xF = isNaN(xN[i]) ? xN[i] || 0 : parseFloat(xN[i]) || 0;
  859. yF = isNaN(yN[i]) ? yN[i] || 0 : parseFloat(yN[i]) || 0;
  860. // handle numeric vs string comparison - number < string - (Kyle Adams)
  861. if (isNaN(xF) !== isNaN(yF)) { return (isNaN(xF)) ? 1 : -1; }
  862. // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2'
  863. if (typeof xF !== typeof yF) {
  864. xF += '';
  865. yF += '';
  866. }
  867. if (xF < yF) { return -1; }
  868. if (xF > yF) { return 1; }
  869. }
  870. return 0;
  871. };
  872. ts.sortTextDesc = function(table, a, b, col) {
  873. if (a === b) { return 0; }
  874. var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ];
  875. if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : e || 1; }
  876. if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : -e || -1; }
  877. if (typeof c.textSorter === 'function') { return c.textSorter(b, a, table, col); }
  878. return ts.sortText(table, b, a);
  879. };
  880. // return text string value by adding up ascii value
  881. // so the text is somewhat sorted when using a digital sort
  882. // this is NOT an alphanumeric sort
  883. ts.getTextValue = function(a, mx, d) {
  884. if (mx) {
  885. // make sure the text value is greater than the max numerical value (mx)
  886. var i, l = a.length, n = mx + d;
  887. for (i = 0; i < l; i++) {
  888. n += a.charCodeAt(i);
  889. }
  890. return d * n;
  891. }
  892. return 0;
  893. };
  894. ts.sortNumeric = function(table, a, b, col, mx, d) {
  895. if (a === b) { return 0; }
  896. var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ];
  897. if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : -e || -1; }
  898. if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : e || 1; }
  899. if (isNaN(a)) { a = ts.getTextValue(a, mx, d); }
  900. if (isNaN(b)) { b = ts.getTextValue(b, mx, d); }
  901. return a - b;
  902. };
  903. ts.sortNumericDesc = function(table, a, b, col, mx, d) {
  904. if (a === b) { return 0; }
  905. var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ];
  906. if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : e || 1; }
  907. if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : -e || -1; }
  908. if (isNaN(a)) { a = ts.getTextValue(a, mx, d); }
  909. if (isNaN(b)) { b = ts.getTextValue(b, mx, d); }
  910. return b - a;
  911. };
  912. // used when replacing accented characters during sorting
  913. ts.characterEquivalents = {
  914. "a" : "\u00e1\u00e0\u00e2\u00e3\u00e4", // áàâãä
  915. "A" : "\u00c1\u00c0\u00c2\u00c3\u00c4", // ÁÀÂÃÄ
  916. "c" : "\u00e7", // ç
  917. "C" : "\u00c7", // Ç
  918. "e" : "\u00e9\u00e8\u00ea\u00eb", // éèêë
  919. "E" : "\u00c9\u00c8\u00ca\u00cb", // ÉÈÊË
  920. "i" : "\u00ed\u00ec\u0130\u00ee\u00ef", // íìİîï
  921. "I" : "\u00cd\u00cc\u0130\u00ce\u00cf", // ÍÌİÎÏ
  922. "o" : "\u00f3\u00f2\u00f4\u00f5\u00f6", // óòôõö
  923. "O" : "\u00d3\u00d2\u00d4\u00d5\u00d6", // ÓÒÔÕÖ
  924. "S" : "\u00df", // ß
  925. "u" : "\u00fa\u00f9\u00fb\u00fc", // úùûü
  926. "U" : "\u00da\u00d9\u00db\u00dc" // ÚÙÛÜ
  927. };
  928. ts.replaceAccents = function(s) {
  929. var a, acc = '[', eq = ts.characterEquivalents;
  930. if (!ts.characterRegex) {
  931. ts.characterRegexArray = {};
  932. for (a in eq) {
  933. if (typeof a === 'string') {
  934. acc += eq[a];
  935. ts.characterRegexArray[a] = new RegExp('[' + eq[a] + ']', 'g');
  936. }
  937. }
  938. ts.characterRegex = new RegExp(acc + ']');
  939. }
  940. if (ts.characterRegex.test(s)) {
  941. for (a in eq) {
  942. if (typeof a === 'string') {
  943. s = s.replace( ts.characterRegexArray[a], a );
  944. }
  945. }
  946. }
  947. return s;
  948. };
  949. // *** utilities ***
  950. ts.isValueInArray = function(v, a) {
  951. var i, l = a.length;
  952. for (i = 0; i < l; i++) {
  953. if (a[i][0] === v) {
  954. return true;
  955. }
  956. }
  957. return false;
  958. };
  959. ts.addParser = function(parser) {
  960. var i, l = ts.parsers.length, a = true;
  961. for (i = 0; i < l; i++) {
  962. if (ts.parsers[i].id.toLowerCase() === parser.id.toLowerCase()) {
  963. a = false;
  964. }
  965. }
  966. if (a) {
  967. ts.parsers.push(parser);
  968. }
  969. };
  970. ts.getParserById = function(name) {
  971. var i, l = ts.parsers.length;
  972. for (i = 0; i < l; i++) {
  973. if (ts.parsers[i].id.toLowerCase() === (name.toString()).toLowerCase()) {
  974. return ts.parsers[i];
  975. }
  976. }
  977. return false;
  978. };
  979. ts.addWidget = function(widget) {
  980. ts.widgets.push(widget);
  981. };
  982. ts.getWidgetById = function(name) {
  983. var i, w, l = ts.widgets.length;
  984. for (i = 0; i < l; i++) {
  985. w = ts.widgets[i];
  986. if (w && w.hasOwnProperty('id') && w.id.toLowerCase() === name.toLowerCase()) {
  987. return w;
  988. }
  989. }
  990. };
  991. ts.applyWidget = function(table, init) {
  992. var c = table.config,
  993. wo = c.widgetOptions,
  994. ws = c.widgets.sort().reverse(), // ensure that widgets are always applied in a certain order
  995. time, i, w, l = ws.length;
  996. // make zebra last
  997. i = $.inArray('zebra', c.widgets);
  998. if (i >= 0) {
  999. c.widgets.splice(i,1);
  1000. c.widgets.push('zebra');
  1001. }
  1002. if (c.debug) {
  1003. time = new Date();
  1004. }
  1005. // add selected widgets
  1006. for (i = 0; i < l; i++) {
  1007. w = ts.getWidgetById(ws[i]);
  1008. if ( w ) {
  1009. if (init === true && w.hasOwnProperty('init')) {
  1010. w.init(table, w, c, wo);
  1011. } else if (!init && w.hasOwnProperty('format')) {
  1012. w.format(table, c, wo);
  1013. }
  1014. }
  1015. }
  1016. if (c.debug) {
  1017. benchmark("Completed " + (init === true ? "initializing" : "applying") + " widgets", time);
  1018. }
  1019. };
  1020. ts.refreshWidgets = function(table, doAll, dontapply) {
  1021. var i, c = table.config,
  1022. cw = c.widgets,
  1023. w = ts.widgets, l = w.length;
  1024. // remove previous widgets
  1025. for (i = 0; i < l; i++){
  1026. if ( w[i] && w[i].id && (doAll || $.inArray( w[i].id, cw ) < 0) ) {
  1027. if (c.debug) { log( 'removing ' + w[i].id ); }
  1028. if (w[i].hasOwnProperty('remove')) { w[i].remove(table, c, c.widgetOptions); }
  1029. }
  1030. }
  1031. if (dontapply !== true) {
  1032. ts.applyWidget(table, doAll);
  1033. }
  1034. };
  1035. // get sorter, string, empty, etc options for each column from
  1036. // jQuery data, metadata, header option or header class name ("sorter-false")
  1037. // priority = jQuery data > meta > headers option > header class name
  1038. ts.getData = function(h, ch, key) {
  1039. var val = '', $h = $(h), m, cl;
  1040. if (!$h.length) { return ''; }
  1041. m = $.metadata ? $h.metadata() : false;
  1042. cl = ' ' + ($h.attr('class') || '');
  1043. if (typeof $h.data(key) !== 'undefined' || typeof $h.data(key.toLowerCase()) !== 'undefined'){
  1044. // "data-lockedOrder" is assigned to "lockedorder"; but "data-locked-order" is assigned to "lockedOrder"
  1045. // "data-sort-initial-order" is assigned to "sortInitialOrder"
  1046. val += $h.data(key) || $h.data(key.toLowerCase());
  1047. } else if (m && typeof m[key] !== 'undefined') {
  1048. val += m[key];
  1049. } else if (ch && typeof ch[key] !== 'undefined') {
  1050. val += ch[key];
  1051. } else if (cl !== ' ' && cl.match(' ' + key + '-')) {
  1052. // include sorter class name "sorter-text", etc
  1053. val = cl.match( new RegExp(' ' + key + '-(\\w+)') )[1] || '';
  1054. }
  1055. return $.trim(val);
  1056. };
  1057. ts.formatFloat = function(s, table) {
  1058. if (typeof(s) !== 'string' || s === '') { return s; }
  1059. if (table.config.usNumberFormat !== false) {
  1060. // US Format - 1,234,567.89 -> 1234567.89
  1061. s = s.replace(/,/g,'');
  1062. } else {
  1063. // German Format = 1.234.567,89 -> 1234567.89
  1064. // French Format = 1 234 567,89 -> 1234567.89
  1065. s = s.replace(/[\s|\.]/g,'').replace(/,/g,'.');
  1066. }
  1067. if(/^\s*\([.\d]+\)/.test(s)) {
  1068. // make (#) into a negative number -> (10) = -10
  1069. s = s.replace(/^\s*\(/,'-').replace(/\)/,'');
  1070. }
  1071. var i = parseFloat(s);
  1072. // return the text instead of zero
  1073. return isNaN(i) ? $.trim(s) : i;
  1074. };
  1075. ts.isDigit = function(s) {
  1076. // replace all unwanted chars and match
  1077. return isNaN(s) ? (/^[\-+(]?\d+[)]?$/).test(s.toString().replace(/[,.'\s]/g, '')) : true;
  1078. };
  1079. }()
  1080. });
  1081. // make shortcut
  1082. var ts = $.tablesorter;
  1083. // extend plugin scope
  1084. $.fn.extend({
  1085. tablesorter: ts.construct
  1086. });
  1087. // add default parsers
  1088. ts.addParser({
  1089. id: "text",
  1090. is: function(s, table, node) {
  1091. return true;
  1092. },
  1093. format: function(s, table, cell, cellIndex) {
  1094. var c = table.config;
  1095. s = $.trim( c.ignoreCase ? s.toLocaleLowerCase() : s );
  1096. return c.sortLocaleCompare ? ts.replaceAccents(s) : s;
  1097. },
  1098. type: "text"
  1099. });
  1100. ts.addParser({
  1101. id: "currency",
  1102. is: function(s) {
  1103. return (/^\(?[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]\d+/).test(s); // £$€¤¥¢
  1104. },
  1105. format: function(s, table) {
  1106. return ts.formatFloat(s.replace(/[^\w,. \-()]/g, ""), table);
  1107. },
  1108. type: "numeric"
  1109. });
  1110. ts.addParser({
  1111. id: "ipAddress",
  1112. is: function(s) {
  1113. return (/^\d{1,3}[\.]\d{1,3}[\.]\d{1,3}[\.]\d{1,3}$/).test(s);
  1114. },
  1115. format: function(s, table) {
  1116. var i, a = s.split("."),
  1117. r = "",
  1118. l = a.length;
  1119. for (i = 0; i < l; i++) {
  1120. r += ("00" + a[i]).slice(-3);
  1121. }
  1122. return ts.formatFloat(r, table);
  1123. },
  1124. type: "numeric"
  1125. });
  1126. ts.addParser({
  1127. id: "url",
  1128. is: function(s) {
  1129. return (/^(https?|ftp|file):\/\//).test(s);
  1130. },
  1131. format: function(s) {
  1132. return $.trim(s.replace(/(https?|ftp|file):\/\//, ''));
  1133. },
  1134. type: "text"
  1135. });
  1136. ts.addParser({
  1137. id: "isoDate",
  1138. is: function(s) {
  1139. return (/^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}$/).test(s);
  1140. },
  1141. format: function(s, table) {
  1142. return ts.formatFloat((s !== "") ? (new Date(s.replace(/-/g, "/")).getTime() || "") : "", table);
  1143. },
  1144. type: "numeric"
  1145. });
  1146. ts.addParser({
  1147. id: "percent",
  1148. is: function(s) {
  1149. return (/\d%\)?$/).test(s);
  1150. },
  1151. format: function(s, table) {
  1152. return ts.formatFloat(s.replace(/%/g, ""), table);
  1153. },
  1154. type: "numeric"
  1155. });
  1156. ts.addParser({
  1157. id: "usLongDate",
  1158. is: function(s) {
  1159. return (/^[A-Z]{3,10}\.?\s+\d{1,2},?\s+(\d{4}|'?\d{2})\s+(([0-2]?\d:[0-5]\d)|([0-1]?\d:[0-5]\d\s?([AP]M)))$/i).test(s);
  1160. },
  1161. format: function(s, table) {
  1162. return ts.formatFloat( (new Date(s.replace(/(\S)([AP]M)$/i, "$1 $2")).getTime() || ''), table);
  1163. },
  1164. type: "numeric"
  1165. });
  1166. ts.addParser({
  1167. id: "shortDate", // "mmddyyyy", "ddmmyyyy" or "yyyymmdd"
  1168. is: function(s) {
  1169. // testing for ####-##-#### - so it's not perfect
  1170. return (/^(\d{2}|\d{4})[\/\-\,\.\s+]\d{2}[\/\-\.\,\s+](\d{2}|\d{4})$/).test(s);
  1171. },
  1172. format: function(s, table, cell, cellIndex) {
  1173. var c = table.config, ci = c.headerList[cellIndex],
  1174. format = ci.shortDateFormat;
  1175. if (typeof format === 'undefined') {
  1176. // cache header formatting so it doesn't getData for every cell in the column
  1177. format = ci.shortDateFormat = ts.getData( ci, c.headers[cellIndex], 'dateFormat') || c.dateFormat;
  1178. }
  1179. s = s.replace(/\s+/g," ").replace(/[\-|\.|\,]/g, "/");
  1180. if (format === "mmddyyyy") {
  1181. s = s.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, "$3/$1/$2");
  1182. } else if (format === "ddmmyyyy") {
  1183. s = s.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, "$3/$2/$1");
  1184. } else if (format === "yyyymmdd") {
  1185. s = s.replace(/(\d{4})[\/\s](\d{1,2})[\/\s](\d{1,2})/, "$1/$2/$3");
  1186. }
  1187. return ts.formatFloat( (new Date(s).getTime() || ''), table);
  1188. },
  1189. type: "numeric"
  1190. });
  1191. ts.addParser({
  1192. id: "time",
  1193. is: function(s) {
  1194. return (/^(([0-2]?\d:[0-5]\d)|([0-1]?\d:[0-5]\d\s?([AP]M)))$/i).test(s);
  1195. },
  1196. format: function(s, table) {
  1197. return ts.formatFloat( (new Date("2000/01/01 " + s.replace(/(\S)([AP]M)$/i, "$1 $2")).getTime() || ""), table);
  1198. },
  1199. type: "numeric"
  1200. });
  1201. ts.addParser({
  1202. id: "digit",
  1203. is: function(s) {
  1204. return ts.isDigit(s);
  1205. },
  1206. format: function(s, table) {
  1207. return ts.formatFloat(s.replace(/[^\w,. \-()]/g, ""), table);
  1208. },
  1209. type: "numeric"
  1210. });
  1211. ts.addParser({
  1212. id: "metadata",
  1213. is: function(s) {
  1214. return false;
  1215. },
  1216. format: function(s, table, cell) {
  1217. var c = table.config,
  1218. p = (!c.parserMetadataName) ? 'sortValue' : c.parserMetadataName;
  1219. return $(cell).metadata()[p];
  1220. },
  1221. type: "numeric"
  1222. });
  1223. // add default widgets
  1224. ts.addWidget({
  1225. id: "zebra",
  1226. format: function(table, c, wo) {
  1227. var $tb, $tv, $tr, row, even, time, k, l,
  1228. child = new RegExp(c.cssChildRow, 'i'),
  1229. b = $(table).children('tbody:not(.' + c.cssInfoBlock + ')');
  1230. if (c.debug) {
  1231. time = new Date();
  1232. }
  1233. for (k = 0; k < b.length; k++ ) {
  1234. // loop through the visible rows
  1235. $tb = $(b[k]);
  1236. l = $tb.children('tr').length;
  1237. if (l > 1) {
  1238. row = 0;
  1239. $tv = $tb.children('tr:visible');
  1240. // revered back to using jQuery each - strangely it's the fastest method
  1241. $tv.each(function(){
  1242. $tr = $(this);
  1243. // style children rows the same way the parent row was styled
  1244. if (!child.test(this.className)) { row++; }
  1245. even = (row % 2 === 0);
  1246. $tr.removeClass(wo.zebra[even ? 1 : 0]).addClass(wo.zebra[even ? 0 : 1]);
  1247. });
  1248. }
  1249. }
  1250. if (c.debug) {
  1251. ts.benchmark("Applying Zebra widget", time);
  1252. }
  1253. },
  1254. remove: function(table, c, wo){
  1255. var k, $tb,
  1256. b = $(table).children('tbody:not(.' + c.cssInfoBlock + ')'),
  1257. rmv = (c.widgetOptions.zebra || [ "even", "odd" ]).join(' ');
  1258. for (k = 0; k < b.length; k++ ){
  1259. $tb = $.tablesorter.processTbody(table, $(b[k]), true); // remove tbody
  1260. $tb.children().removeClass(rmv);
  1261. $.tablesorter.processTbody(table, $tb, false); // restore tbody
  1262. }
  1263. }
  1264. });
  1265. })(jQuery);