/admin/themes/trailhead/js/jquery.tablesorter.js

https://bitbucket.org/andymoogaloo/beamarshall · JavaScript · 1167 lines · 945 code · 67 blank · 155 comment · 281 complexity · bd451c1359751f6b6da4997668dfcd3c MD5 · raw file

Large files are truncated click here to view the full file

  1. /*!
  2. * TableSorter 2.3.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. !(function($) {
  18. $.extend({
  19. tablesorter: new function() {
  20. this.version = "2.3.8";
  21. var parsers = [], widgets = [];
  22. this.defaults = {
  23. // appearance
  24. widthFixed : false, // adds colgroup to fix widths of columns
  25. // functionality
  26. cancelSelection : true, // prevent text selection in the header
  27. dateFormat : "mmddyyyy", // other options: "ddmmyyy" or "yyyymmdd"
  28. sortMultiSortKey : "shiftKey", // key used to select additional columns
  29. usNumberFormat : true, // false for German "1.234.567,89" or French "1 234 567,89"
  30. delayInit : false, // if false, the parsed table contents will not update until the first sort.
  31. // sort options
  32. headers : {}, // set sorter, string, empty, locked order, sortInitialOrder, filter, etc.
  33. ignoreCase : true, // ignore case while sorting
  34. sortForce : null, // column(s) first sorted; always applied
  35. sortList : [], // Initial sort order; applied initially; updated when manually sorted
  36. sortAppend : null, // column(s) sorted last; always applied
  37. sortInitialOrder : "asc", // sort direction on first click
  38. sortLocaleCompare: false, // replace equivalent character (accented characters)
  39. sortReset : false, // third click on the header will reset column to default - unsorted
  40. sortRestart : false, // restart sort to "sortInitialOrder" when clicking on previously unsorted columns
  41. emptyTo : "bottom", // sort empty cell to bottom, top, none, zero
  42. stringTo : "max", // sort strings in numerical column as max, min, top, bottom, zero
  43. textExtraction : "simple", // text extraction method/function - function(node, table, cellIndex){}
  44. textSorter : null, // use custom text sorter - function(a,b){ return a.sort(b); } // basic sort
  45. // widget options
  46. widgets: [], // method to add widgets, e.g. widgets: ['zebra']
  47. widgetOptions : {
  48. zebra : [ "even", "odd" ] // zebra widget alternating row class names
  49. },
  50. initWidgets : true, // apply widgets on tablesorter initialization
  51. // callbacks
  52. initialized : null, // function(table){},
  53. onRenderHeader : null, // function(index){},
  54. // css class names
  55. tableClass : 'tablesorter',
  56. cssAsc : "tablesorter-headerSortUp",
  57. cssChildRow : "expand-child",
  58. cssDesc : "tablesorter-headerSortDown",
  59. cssHeader : "tablesorter-header",
  60. cssInfoBlock : "tablesorter-infoOnly", // don't sort tbody with this class name
  61. // selectors
  62. selectorHeaders : '> thead th',
  63. selectorRemove : "tr.remove-me",
  64. // advanced
  65. debug : false,
  66. // Internal variables
  67. headerList: [],
  68. empties: {},
  69. strings: {},
  70. parsers: []
  71. // deprecated; but retained for backwards compatibility
  72. // widgetZebra: { css: ["even", "odd"] }
  73. };
  74. /* debuging utils */
  75. function log(s) {
  76. if (typeof console !== "undefined" && typeof console.log !== "undefined") {
  77. console.log(s);
  78. } else {
  79. alert(s);
  80. }
  81. }
  82. function benchmark(s, d) {
  83. log(s + " (" + (new Date().getTime() - d.getTime()) + "ms)");
  84. }
  85. this.benchmark = benchmark;
  86. this.hasInitialized = false;
  87. function getElementText(table, node, cellIndex) {
  88. if (!node) { return ""; }
  89. var c = table.config,
  90. t = c.textExtraction, text = "";
  91. if (t === "simple") {
  92. if (c.supportsTextContent) {
  93. text = node.textContent; // newer browsers support this
  94. } else {
  95. if (node.childNodes[0] && node.childNodes[0].hasChildNodes()) {
  96. text = node.childNodes[0].innerHTML;
  97. } else {
  98. text = node.innerHTML;
  99. }
  100. }
  101. } else {
  102. if (typeof(t) === "function") {
  103. text = t(node, table, cellIndex);
  104. } else if (typeof(t) === "object" && t.hasOwnProperty(cellIndex)) {
  105. text = t[cellIndex](node, table, cellIndex);
  106. } else {
  107. text = c.supportsTextContent ? node.textContent : $(node).text();
  108. }
  109. }
  110. return $.trim(text);
  111. }
  112. /* parsers utils */
  113. function getParserById(name) {
  114. var i, l = parsers.length;
  115. for (i = 0; i < l; i++) {
  116. if (parsers[i].id.toLowerCase() === (name.toString()).toLowerCase()) {
  117. return parsers[i];
  118. }
  119. }
  120. return false;
  121. }
  122. function detectParserForColumn(table, rows, rowIndex, cellIndex) {
  123. var i, l = parsers.length,
  124. node = false,
  125. nodeValue = '',
  126. keepLooking = true;
  127. while (nodeValue === '' && keepLooking) {
  128. rowIndex++;
  129. if (rows[rowIndex]) {
  130. node = rows[rowIndex].cells[cellIndex];
  131. nodeValue = getElementText(table, node, cellIndex);
  132. if (table.config.debug) {
  133. log('Checking if value was empty on row ' + rowIndex + ', column: ' + cellIndex + ': ' + nodeValue);
  134. }
  135. } else {
  136. keepLooking = false;
  137. }
  138. }
  139. for (i = 1; i < l; i++) {
  140. if (parsers[i].is(nodeValue, table, node)) {
  141. return parsers[i];
  142. }
  143. }
  144. // 0 is always the generic parser (text)
  145. return parsers[0];
  146. }
  147. function buildParserCache(table, $headers) {
  148. var c = table.config,
  149. tb = $(table.tBodies).filter(':not(.' + c.cssInfoBlock + ')'),
  150. ts = $.tablesorter, rows, list, l, i, h, m, ch, cl, p, parsersDebug = "";
  151. if ( tb.length === 0) { return; } // In the case of empty tables
  152. rows = tb[0].rows;
  153. if (rows[0]) {
  154. list = [];
  155. l = rows[0].cells.length;
  156. for (i = 0; i < l; i++) {
  157. // tons of thanks to AnthonyM1229 for working out the following selector (issue #74) to make this work in IE8!
  158. h = $headers.filter(':not([colspan])[data-column="'+i+'"]:last,[colspan="1"][data-column="'+i+'"]:last');
  159. ch = c.headers[i];
  160. // get column parser
  161. p = getParserById( ts.getData(h, ch, 'sorter') );
  162. // empty cells behaviour - keeping emptyToBottom for backwards compatibility.
  163. c.empties[i] = ts.getData(h, ch, 'empty') || c.emptyTo || (c.emptyToBottom ? 'bottom' : 'top' );
  164. // text strings behaviour in numerical sorts
  165. c.strings[i] = ts.getData(h, ch, 'string') || c.stringTo || 'max';
  166. if (!p) {
  167. p = detectParserForColumn(table, rows, -1, i);
  168. }
  169. if (c.debug) {
  170. parsersDebug += "column:" + i + "; parser:" + p.id + "; string:" + c.strings[i] + '; empty: ' + c.empties[i] + "\n";
  171. }
  172. list.push(p);
  173. }
  174. }
  175. if (c.debug) {
  176. log(parsersDebug);
  177. }
  178. return list;
  179. }
  180. /* utils */
  181. function buildCache(table) {
  182. var b = table.tBodies,
  183. tc = table.config,
  184. totalRows,
  185. totalCells,
  186. parsers = tc.parsers,
  187. t, i, j, k, c, cols, cacheTime;
  188. tc.cache = {};
  189. if (tc.debug) {
  190. cacheTime = new Date();
  191. }
  192. for (k = 0; k < b.length; k++) {
  193. tc.cache[k] = { row: [], normalized: [] };
  194. // ignore tbodies with class name from css.cssInfoBlock
  195. if (!$(b[k]).hasClass(tc.cssInfoBlock)) {
  196. $(b[k]).addClass('tablesorter-hidden');
  197. totalRows = (b[k] && b[k].rows.length) || 0;
  198. totalCells = (b[k].rows[0] && b[k].rows[0].cells.length) || 0;
  199. for (i = 0; i < totalRows; ++i) {
  200. /** Add the table data to main data array */
  201. c = $(b[k].rows[i]);
  202. cols = [];
  203. // if this is a child row, add it to the last row's children and continue to the next row
  204. if (c.hasClass(tc.cssChildRow)) {
  205. tc.cache[k].row[tc.cache[k].row.length - 1] = tc.cache[k].row[tc.cache[k].row.length - 1].add(c);
  206. // go to the next for loop
  207. continue;
  208. }
  209. tc.cache[k].row.push(c);
  210. for (j = 0; j < totalCells; ++j) {
  211. t = getElementText(table, c[0].cells[j], j);
  212. // allow parsing if the string is empty, previously parsing would change it to zero,
  213. // in case the parser needs to extract data from the table cell attributes
  214. cols.push( parsers[j].format(t, table, c[0].cells[j], j) );
  215. }
  216. cols.push(tc.cache[k].normalized.length); // add position for rowCache
  217. tc.cache[k].normalized.push(cols);
  218. }
  219. $(b[k]).removeClass('tablesorter-hidden');
  220. }
  221. }
  222. if (tc.debug) {
  223. benchmark("Building cache for " + totalRows + " rows", cacheTime);
  224. }
  225. }
  226. function getWidgetById(name) {
  227. var i, w, l = widgets.length;
  228. for (i = 0; i < l; i++) {
  229. w = widgets[i];
  230. if (w && w.hasOwnProperty('id') && w.id.toLowerCase() === name.toLowerCase()) {
  231. return w;
  232. }
  233. }
  234. }
  235. function applyWidget(table, init) {
  236. var tc = table.config, c = tc.widgets,
  237. time, i, w, l = c.length;
  238. if (tc.debug) {
  239. time = new Date();
  240. }
  241. for (i = 0; i < l; i++) {
  242. w = getWidgetById(c[i]);
  243. if ( w ) {
  244. if (init === true && w.hasOwnProperty('init')) {
  245. w.init(table, widgets, w);
  246. } else if (!init && w.hasOwnProperty('format')) {
  247. w.format(table);
  248. }
  249. }
  250. }
  251. if (tc.debug) {
  252. benchmark("Completed " + (init === true ? "initializing" : "applying") + " widgets", time);
  253. }
  254. }
  255. // init flag (true) used by pager plugin to prevent widget application
  256. function appendToTable(table, init) {
  257. var c = table.config,
  258. b = table.tBodies,
  259. rows = [],
  260. r, n, totalRows, checkCell, c2 = c.cache,
  261. f, i, j, k, l, pos, appendTime;
  262. if (c.debug) {
  263. appendTime = new Date();
  264. }
  265. for (k = 0; k < b.length; k++) {
  266. if (!$(b[k]).hasClass(c.cssInfoBlock)){
  267. $(b[k]).addClass('tablesorter-hidden');
  268. f = document.createDocumentFragment();
  269. r = c2[k].row;
  270. n = c2[k].normalized;
  271. totalRows = n.length;
  272. checkCell = totalRows ? (n[0].length - 1) : 0;
  273. for (i = 0; i < totalRows; i++) {
  274. pos = n[i][checkCell];
  275. rows.push(r[pos]);
  276. // removeRows used by the pager plugin
  277. if (!c.appender || !c.removeRows) {
  278. l = r[pos].length;
  279. for (j = 0; j < l; j++) {
  280. f.appendChild(r[pos][j]);
  281. }
  282. }
  283. }
  284. table.tBodies[k].appendChild(f);
  285. $(b[k]).removeClass('tablesorter-hidden');
  286. }
  287. }
  288. if (c.appender) {
  289. c.appender(table, rows);
  290. }
  291. if (c.debug) {
  292. benchmark("Rebuilt table", appendTime);
  293. }
  294. // apply table widgets
  295. if (!init) { applyWidget(table); }
  296. // trigger sortend
  297. $(table).trigger("sortEnd", table);
  298. }
  299. // computeTableHeaderCellIndexes from:
  300. // http://www.javascripttoolbox.com/lib/table/examples.php
  301. // http://www.javascripttoolbox.com/temp/table_cellindex.html
  302. function computeThIndexes(t) {
  303. var matrix = [],
  304. lookup = {},
  305. trs = $(t).find('thead:eq(0) tr'),
  306. i, j, k, l, c, cells, rowIndex, cellId, rowSpan, colSpan, firstAvailCol, matrixrow;
  307. for (i = 0; i < trs.length; i++) {
  308. cells = trs[i].cells;
  309. for (j = 0; j < cells.length; j++) {
  310. c = cells[j];
  311. rowIndex = c.parentNode.rowIndex;
  312. cellId = rowIndex + "-" + c.cellIndex;
  313. rowSpan = c.rowSpan || 1;
  314. colSpan = c.colSpan || 1;
  315. if (typeof(matrix[rowIndex]) === "undefined") {
  316. matrix[rowIndex] = [];
  317. }
  318. // Find first available column in the first row
  319. for (k = 0; k < matrix[rowIndex].length + 1; k++) {
  320. if (typeof(matrix[rowIndex][k]) === "undefined") {
  321. firstAvailCol = k;
  322. break;
  323. }
  324. }
  325. lookup[cellId] = firstAvailCol;
  326. // add data-column
  327. $(c).attr({ 'data-column' : firstAvailCol }); // 'data-row' : rowIndex
  328. for (k = rowIndex; k < rowIndex + rowSpan; k++) {
  329. if (typeof(matrix[k]) === "undefined") {
  330. matrix[k] = [];
  331. }
  332. matrixrow = matrix[k];
  333. for (l = firstAvailCol; l < firstAvailCol + colSpan; l++) {
  334. matrixrow[l] = "x";
  335. }
  336. }
  337. }
  338. }
  339. return lookup;
  340. }
  341. function formatSortingOrder(v) {
  342. // look for "d" in "desc" order; return true
  343. return (/^d/i.test(v) || v === 1);
  344. }
  345. function buildHeaders(table) {
  346. var header_index = computeThIndexes(table), ch, $t,
  347. $th, lock, time, $tableHeaders, c = table.config, ts = $.tablesorter;
  348. c.headerList = [];
  349. if (c.debug) {
  350. time = new Date();
  351. }
  352. $tableHeaders = $(table).find(c.selectorHeaders)
  353. .each(function(index) {
  354. $t = $(this);
  355. ch = c.headers[index];
  356. this.innerHTML = '<div class="tablesorter-header-inner">' + this.innerHTML + '</div>'; // faster than wrapInner
  357. if (c.onRenderHeader) { c.onRenderHeader.apply($th, [index]); }
  358. this.column = header_index[this.parentNode.rowIndex + "-" + this.cellIndex];
  359. this.order = formatSortingOrder( ts.getData($t, ch, 'sortInitialOrder') || c.sortInitialOrder ) ? [1,0,2] : [0,1,2];
  360. this.count = -1; // set to -1 because clicking on the header automatically adds one
  361. if (ts.getData($t, ch, 'sorter') === 'false') { this.sortDisabled = true; }
  362. this.lockedOrder = false;
  363. lock = ts.getData($t, ch, 'lockedOrder') || false;
  364. if (typeof(lock) !== 'undefined' && lock !== false) {
  365. this.order = this.lockedOrder = formatSortingOrder(lock) ? [1,1,1] : [0,0,0];
  366. }
  367. if (!this.sortDisabled) {
  368. $th = $t.addClass(c.cssHeader);
  369. }
  370. // add cell to headerList
  371. c.headerList[index] = this;
  372. // add to parent in case there are multiple rows
  373. $t.parent().addClass(c.cssHeader);
  374. });
  375. if (table.config.debug) {
  376. benchmark("Built headers:", time);
  377. log($tableHeaders);
  378. }
  379. return $tableHeaders;
  380. }
  381. function isValueInArray(v, a) {
  382. var i, l = a.length;
  383. for (i = 0; i < l; i++) {
  384. if (a[i][0] === v) {
  385. return true;
  386. }
  387. }
  388. return false;
  389. }
  390. function setHeadersCss(table, $headers, list) {
  391. var f, h = [], i, j, l, css = [table.config.cssDesc, table.config.cssAsc];
  392. // remove all header information
  393. $headers
  394. .removeClass(css.join(' '))
  395. .each(function() {
  396. if (!this.sortDisabled) {
  397. h[this.column] = $(this);
  398. }
  399. });
  400. l = list.length;
  401. for (i = 0; i < l; i++) {
  402. if (list[i][1] === 2) { continue; } // direction = 2 means reset!
  403. if (h[list[i][0]]) {
  404. // add class if cell exists - fix for issue #78
  405. h[list[i][0]].addClass(css[list[i][1]]);
  406. }
  407. // multicolumn sorting updating
  408. f = $headers.filter('[data-column="' + list[i][0] + '"]');
  409. if (l > 1 && f.length) {
  410. for (j = 0; j < f.length; j++) {
  411. if (!f[j].sortDisabled) {
  412. $(f[j]).addClass(css[list[i][1]]);
  413. }
  414. }
  415. }
  416. }
  417. }
  418. function fixColumnWidth(table) {
  419. if (table.config.widthFixed) {
  420. var colgroup = $('<colgroup>');
  421. $("tr:first td", table.tBodies[0]).each(function() {
  422. colgroup.append($('<col>').css('width', $(this).width()));
  423. });
  424. $(table).prepend(colgroup);
  425. }
  426. }
  427. function updateHeaderSortCount(table, sortList) {
  428. var i, s, o, c = table.config,
  429. l = sortList.length;
  430. for (i = 0; i < l; i++) {
  431. s = sortList[i];
  432. o = c.headerList[s[0]];
  433. o.count = s[1] % (c.sortReset ? 3 : 2);
  434. }
  435. }
  436. function getCachedSortType(parsers, i) {
  437. return (parsers && parsers[i]) ? parsers[i].type || '' : '';
  438. }
  439. /* sorting methods - reverted sorting method back to version 2.0.3 */
  440. function multisort(table, sortList) {
  441. var dynamicExp, col, mx = 0, dir = 0, tc = table.config,
  442. l = sortList.length, bl = table.tBodies.length,
  443. sortTime, i, j, k, c, cache, lc, s, e, order, orgOrderCol;
  444. if (tc.debug) { sortTime = new Date(); }
  445. for (k = 0; k < bl; k++) {
  446. dynamicExp = "var sortWrapper = function(a,b) {";
  447. cache = tc.cache[k];
  448. lc = cache.normalized.length;
  449. for (i = 0; i < l; i++) {
  450. c = sortList[i][0];
  451. order = sortList[i][1];
  452. // fallback to natural sort since it is more robust
  453. s = /n/i.test(getCachedSortType(tc.parsers, c)) ? "Numeric" : "Text";
  454. s += order === 0 ? "" : "Desc";
  455. e = "e" + i;
  456. // get max column value (ignore sign)
  457. if (/Numeric/.test(s) && tc.strings[c]) {
  458. for (j = 0; j < lc; j++) {
  459. col = Math.abs(parseFloat(cache.normalized[j][c]));
  460. mx = Math.max( mx, isNaN(col) ? 0 : col );
  461. }
  462. // sort strings in numerical columns
  463. if (typeof(tc.string[tc.strings[c]]) === 'boolean') {
  464. dir = (order === 0 ? 1 : -1) * (tc.string[tc.strings[c]] ? -1 : 1);
  465. } else {
  466. dir = (tc.strings[c]) ? tc.string[tc.strings[c]] || 0 : 0;
  467. }
  468. }
  469. dynamicExp += "var " + e + " = sort" + s + "(table,a[" + c + "],b[" + c + "]," + c + "," + mx + "," + dir + "); ";
  470. dynamicExp += "if (" + e + ") { return " + e + "; } ";
  471. dynamicExp += "else { ";
  472. }
  473. // if value is the same keep orignal order
  474. orgOrderCol = (cache.normalized && cache.normalized[0]) ? cache.normalized[0].length - 1 : 0;
  475. dynamicExp += "return a[" + orgOrderCol + "]-b[" + orgOrderCol + "];";
  476. for (i=0; i < l; i++) {
  477. dynamicExp += "}; ";
  478. }
  479. dynamicExp += "return 0; ";
  480. dynamicExp += "}; ";
  481. eval(dynamicExp);
  482. cache.normalized.sort(sortWrapper); // sort using eval expression
  483. }
  484. if (tc.debug) { benchmark("Sorting on " + sortList.toString() + " and dir " + order+ " time", sortTime); }
  485. }
  486. // Natural sort - https://github.com/overset/javascript-natural-sort
  487. function sortText(table, a, b, col) {
  488. if (a === b) { return 0; }
  489. var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ],
  490. r = $.tablesorter.regex, xN, xD, yN, yD, xF, yF, i, mx;
  491. if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : -e || -1; }
  492. if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : e || 1; }
  493. if (typeof c.textSorter === 'function') { return c.textSorter(a, b, table, col); }
  494. // chunk/tokenize
  495. xN = a.replace(r[0], '\0$1\0').replace(/\0$/, '').replace(/^\0/, '').split('\0');
  496. yN = b.replace(r[0], '\0$1\0').replace(/\0$/, '').replace(/^\0/, '').split('\0');
  497. // numeric, hex or date detection
  498. xD = parseInt(a.match(r[2])) || (xN.length !== 1 && a.match(r[1]) && Date.parse(a));
  499. yD = parseInt(b.match(r[2])) || (xD && b.match(r[1]) && Date.parse(b)) || null;
  500. // first try and sort Hex codes or Dates
  501. if (yD) {
  502. if ( xD < yD ) { return -1; }
  503. if ( xD > yD ) { return 1; }
  504. }
  505. mx = Math.max(xN.length, yN.length);
  506. // natural sorting through split numeric strings and default strings
  507. for (i = 0; i < mx; i++) {
  508. // find floats not starting with '0', string or 0 if not defined (Clint Priest)
  509. xF = (!(xN[i] || '').match(r[3]) && parseFloat(xN[i])) || xN[i] || 0;
  510. yF = (!(yN[i] || '').match(r[3]) && parseFloat(yN[i])) || yN[i] || 0;
  511. // handle numeric vs string comparison - number < string - (Kyle Adams)
  512. if (isNaN(xF) !== isNaN(yF)) { return (isNaN(xF)) ? 1 : -1; }
  513. // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2'
  514. if (typeof xF !== typeof yF) {
  515. xF += '';
  516. yF += '';
  517. }
  518. if (xF < yF) { return -1; }
  519. if (xF > yF) { return 1; }
  520. }
  521. return 0;
  522. }
  523. function sortTextDesc(table, a, b, col) {
  524. if (a === b) { return 0; }
  525. var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ];
  526. if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : e || 1; }
  527. if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : -e || -1; }
  528. if (typeof c.textSorter === 'function') { return c.textSorter(b, a, table, col); }
  529. return sortText(table, b, a);
  530. }
  531. // return text string value by adding up ascii value
  532. // so the text is somewhat sorted when using a digital sort
  533. // this is NOT an alphanumeric sort
  534. function getTextValue(a, mx, d) {
  535. if (mx) {
  536. // make sure the text value is greater than the max numerical value (mx)
  537. var i, l = a.length, n = mx + d;
  538. for (i = 0; i < l; i++) {
  539. n += a.charCodeAt(i);
  540. }
  541. return d * n;
  542. }
  543. return 0;
  544. }
  545. function sortNumeric(table, a, b, col, mx, d) {
  546. if (a === b) { return 0; }
  547. var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ];
  548. if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : -e || -1; }
  549. if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : e || 1; }
  550. if (isNaN(a)) { a = getTextValue(a, mx, d); }
  551. if (isNaN(b)) { b = getTextValue(b, mx, d); }
  552. return a - b;
  553. }
  554. function sortNumericDesc(table, a, b, col, mx, d) {
  555. if (a === b) { return 0; }
  556. var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ];
  557. if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : e || 1; }
  558. if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : -e || -1; }
  559. if (isNaN(a)) { a = getTextValue(a, mx, d); }
  560. if (isNaN(b)) { b = getTextValue(b, mx, d); }
  561. return b - a;
  562. }
  563. /* public methods */
  564. this.construct = function(settings) {
  565. return this.each(function() {
  566. // if no thead or tbody quit.
  567. if (!this.tHead || this.tBodies.length === 0) { return; }
  568. // declare
  569. var $headers, $cell, $this,
  570. config, c, i, j, k, a, s, o, downTime,
  571. m = $.metadata;
  572. // new blank config object
  573. this.config = {};
  574. // merge and extend.
  575. c = config = $.extend(true, this.config, $.tablesorter.defaults, settings);
  576. if (c.debug) { $.data( this, 'startoveralltimer', new Date()); }
  577. // store common expression for speed
  578. $this = $(this).addClass(c.tableClass);
  579. // save the settings where they read
  580. $.data(this, "tablesorter", c);
  581. c.supportsTextContent = $('<span>x</span>')[0].textContent === 'x';
  582. // digit sort text location; keeping max+/- for backwards compatibility
  583. c.string = { 'max': 1, 'min': -1, 'max+': 1, 'max-': -1, 'zero': 0, 'none': 0, 'null': 0, 'top': true, 'bottom': false };
  584. // build headers
  585. $headers = buildHeaders(this);
  586. // try to auto detect column type, and store in tables config
  587. c.parsers = buildParserCache(this, $headers);
  588. // build the cache for the tbody cells
  589. // delayInit will delay building the cache until the user starts a sort
  590. if (!c.delayInit) { buildCache(this); }
  591. // fixate columns if the users supplies the fixedWidth option
  592. fixColumnWidth(this);
  593. // apply event handling to headers
  594. // this is to big, perhaps break it out?
  595. $headers.bind('mousedown.tablesorter mouseup.tablesorter', function(e, external) {
  596. if (e.type === 'mousedown') {
  597. downTime = new Date().getTime();
  598. return !c.cancelSelection;
  599. }
  600. // prevent resizable widget from initializing a sort (long clicks are ignored)
  601. if (external !== true && (new Date().getTime() - downTime > 500)) { return false; }
  602. if (c.delayInit && !c.cache) { buildCache($this[0]); }
  603. if (!this.sortDisabled) {
  604. // Only call sortStart if sorting is enabled.
  605. $this.trigger("sortStart", $this[0]);
  606. // store exp, for speed
  607. $cell = $(this);
  608. k = !e[c.sortMultiSortKey];
  609. // get current column sort order
  610. this.count = (this.count + 1) % (c.sortReset ? 3 : 2);
  611. // reset all sorts on non-current column - issue #30
  612. if (c.sortRestart) {
  613. i = this;
  614. $headers.each(function() {
  615. // only reset counts on columns that weren't just clicked on and if not included in a multisort
  616. if (this !== i && (k || !$(this).is('.' + c.cssDesc + ',.' + c.cssAsc))) {
  617. this.count = -1;
  618. }
  619. });
  620. }
  621. // get current column index
  622. i = this.column;
  623. // user only wants to sort on one column
  624. if (k) {
  625. // flush the sort list
  626. c.sortList = [];
  627. if (c.sortForce !== null) {
  628. a = c.sortForce;
  629. for (j = 0; j < a.length; j++) {
  630. if (a[j][0] !== i) {
  631. c.sortList.push(a[j]);
  632. }
  633. }
  634. }
  635. // add column to sort list
  636. o = this.order[this.count];
  637. if (o < 2) {
  638. c.sortList.push([i, o]);
  639. // add other columns if header spans across multiple
  640. if (this.colSpan > 1) {
  641. for (j = 1; j < this.colSpan; j++) {
  642. c.sortList.push([i+j, o]);
  643. }
  644. }
  645. }
  646. // multi column sorting
  647. } else {
  648. // the user has clicked on an already sorted column.
  649. if (isValueInArray(i, c.sortList)) {
  650. // reverse the sorting direction for all tables.
  651. for (j = 0; j < c.sortList.length; j++) {
  652. s = c.sortList[j];
  653. o = c.headerList[s[0]];
  654. if (s[0] === i) {
  655. s[1] = o.order[o.count];
  656. if (s[1] === 2) {
  657. c.sortList.splice(j,1);
  658. o.count = -1;
  659. }
  660. }
  661. }
  662. } else {
  663. // add column to sort list array
  664. o = this.order[this.count];
  665. if (o < 2) {
  666. c.sortList.push([i, o]);
  667. // add other columns if header spans across multiple
  668. if (this.colSpan > 1) {
  669. for (j = 1; j < this.colSpan; j++) {
  670. c.sortList.push([i+j, o]);
  671. }
  672. }
  673. }
  674. }
  675. }
  676. if (c.sortAppend !== null) {
  677. a = c.sortAppend;
  678. for (j = 0; j < a.length; j++) {
  679. if (a[j][0] !== i) {
  680. c.sortList.push(a[j]);
  681. }
  682. }
  683. }
  684. // sortBegin event triggered immediately before the sort
  685. $this.trigger("sortBegin", $this[0]);
  686. // set css for headers
  687. setHeadersCss($this[0], $headers, c.sortList);
  688. multisort($this[0], c.sortList);
  689. appendToTable($this[0]);
  690. // stop normal event by returning false
  691. return false;
  692. }
  693. });
  694. if (c.cancelSelection) {
  695. // cancel selection
  696. $headers.each(function() {
  697. this.onselectstart = function() {
  698. return false;
  699. };
  700. });
  701. }
  702. // apply easy methods that trigger binded events
  703. $this
  704. .bind("update", function(e, resort) {
  705. // remove rows/elements before update
  706. $(c.selectorRemove, this).remove();
  707. // rebuild parsers.
  708. c.parsers = buildParserCache(this, $headers);
  709. // rebuild the cache map
  710. buildCache(this);
  711. if (resort !== false) { $(this).trigger("sorton", [c.sortList]); }
  712. })
  713. .bind("updateCell", function(e, cell, resort) {
  714. // get position from the dom.
  715. var t = this, $tb = $(this).find('tbody'), row, pos,
  716. // update cache - format: function(s, table, cell, cellIndex)
  717. tbdy = $tb.index( $(cell).closest('tbody') );
  718. row = $tb.eq(tbdy).find('tr').index( $(cell).closest('tr') );
  719. pos = [ row, cell.cellIndex];
  720. t.config.cache[tbdy].normalized[pos[0]][pos[1]] = c.parsers[pos[1]].format( getElementText(t, cell, pos[1]), t, cell, pos[1] );
  721. if (resort !== false) { $(this).trigger("sorton", [c.sortList]); }
  722. })
  723. .bind("addRows", function(e, $row, resort) {
  724. var i, rows = $row.filter('tr').length,
  725. dat = [], l = $row[0].cells.length, t = this,
  726. tbdy = $(this).find('tbody').index( $row.closest('tbody') );
  727. // add each row
  728. for (i = 0; i < rows; i++) {
  729. // add each cell
  730. for (j = 0; j < l; j++) {
  731. dat[j] = c.parsers[j].format( getElementText(t, $row[i].cells[j], j), t, $row[i].cells[j], j );
  732. }
  733. // add the row index to the end
  734. dat.push(c.cache[tbdy].row.length);
  735. // update cache
  736. c.cache[tbdy].row.push([$row[i]]);
  737. c.cache[tbdy].normalized.push(dat);
  738. dat = [];
  739. }
  740. // resort using current settings
  741. if (resort !== false) { $(this).trigger("sorton", [c.sortList]); }
  742. })
  743. .bind("sorton", function(e, list, init) {
  744. $(this).trigger("sortStart", this);
  745. // update and store the sortlist
  746. c.sortList = list;
  747. // update header count index
  748. updateHeaderSortCount(this, c.sortList);
  749. // set css for headers
  750. setHeadersCss(this, $headers, c.sortList);
  751. // sort the table and append it to the dom
  752. multisort(this, c.sortList);
  753. appendToTable(this, init);
  754. })
  755. .bind("appendCache", function(e, init) {
  756. appendToTable(this, init);
  757. })
  758. .bind("applyWidgetId", function(e, id) {
  759. getWidgetById(id).format(this);
  760. })
  761. .bind("applyWidgets", function(e, init) {
  762. // apply widgets
  763. applyWidget(this, init);
  764. })
  765. .bind("destroy", function(e,c){
  766. $.tablesorter.destroy(this, c);
  767. });
  768. // get sort list from jQuery data or metadata
  769. if ($this.data() && typeof $this.data().sortlist !== 'undefined') {
  770. c.sortList = $this.data().sortlist;
  771. } else if (m && ($this.metadata() && $this.metadata().sortlist)) {
  772. c.sortList = $this.metadata().sortlist;
  773. }
  774. // apply widget init code
  775. applyWidget(this, true);
  776. // if user has supplied a sort list to constructor.
  777. if (c.sortList.length > 0) {
  778. $this.trigger("sorton", [c.sortList, !c.initWidgets]);
  779. } else if (c.initWidgets) {
  780. // apply widget format
  781. applyWidget(this);
  782. }
  783. // initialized
  784. this.hasInitialized = true;
  785. if (c.debug) {
  786. $.tablesorter.benchmark("Overall initialization time", $.data( this, 'startoveralltimer'));
  787. }
  788. $this.trigger('tablesorter-initialized', this);
  789. if (typeof c.initialized === 'function') { c.initialized(this); }
  790. });
  791. };
  792. this.destroy = function(table, removeClasses){
  793. var $t = $(table), c = table.config;
  794. // remove widget added rows
  795. $t.find('thead:first tr:not(.' + c.cssHeader + ')').remove();
  796. // remove resizer widget stuff
  797. $t.find('thead:first .tablesorter-resizer').remove();
  798. // disable tablesorter
  799. $t
  800. .unbind('update updateCell addRows sorton appendCache applyWidgetId applyWidgets destroy mouseup mouseleave')
  801. .find(c.selectorHeaders)
  802. .unbind('click mousedown mousemove mouseup')
  803. .removeClass(c.cssHeader + ' ' + c.cssAsc + ' ' + c.cssDesc);
  804. if (removeClasses !== false) {
  805. $t.removeClass(c.tableClass);
  806. }
  807. };
  808. this.addParser = function(parser) {
  809. var i, l = parsers.length, a = true;
  810. for (i = 0; i < l; i++) {
  811. if (parsers[i].id.toLowerCase() === parser.id.toLowerCase()) {
  812. a = false;
  813. }
  814. }
  815. if (a) {
  816. parsers.push(parser);
  817. }
  818. };
  819. this.addWidget = function(widget) {
  820. widgets.push(widget);
  821. };
  822. this.formatFloat = function(s, table) {
  823. if (typeof(s) !== 'string' || s === '') { return s; }
  824. if (table.config.usNumberFormat !== false) {
  825. // US Format - 1,234,567.89 -> 1234567.89
  826. s = s.replace(/,/g,'');
  827. } else {
  828. // German Format = 1.234.567,89 -> 1234567.89
  829. // French Format = 1 234 567,89 -> 1234567.89
  830. s = s.replace(/[\s|\.]/g,'').replace(/,/g,'.');
  831. }
  832. if(/^\s*\([.\d]+\)/.test(s)) {
  833. s = s.replace(/^\s*\(/,'-').replace(/\)/,'');
  834. }
  835. var i = parseFloat(s);
  836. // return the text instead of zero
  837. return isNaN(i) ? $.trim(s) : i;
  838. };
  839. this.isDigit = function(s) {
  840. // replace all unwanted chars and match.
  841. return (/^[\-+(]?\d*[)]?$/).test(s.replace(/[,.'\s]/g, ''));
  842. };
  843. // regex used in natural sort
  844. this.regex = [
  845. /(^-?[0-9]+(\.?[0-9]*)[df]?e?[0-9]?$|^0x[0-9a-f]+$|[0-9]+)/gi, // chunk/tokenize numbers & letters
  846. /(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/, //date
  847. /^0x[0-9a-f]+$/i, // hex
  848. /^0/ // leading zeros
  849. ];
  850. // used when replacing accented characters during sorting
  851. this.characterEquivalents = {
  852. "a" : "\u00e1\u00e0\u00e2\u00e3\u00e4", // áàâãä
  853. "A" : "\u00c1\u00c0\u00c2\u00c3\u00c4", // ÁÀÂÃÄ
  854. "c" : "\u00e7", // ç
  855. "C" : "\u00c7", // Ç
  856. "e" : "\u00e9\u00e8\u00ea\u00eb", // éèêë
  857. "E" : "\u00c9\u00c8\u00ca\u00cb", // ÉÈÊË
  858. "i" : "\u00ed\u00ec\u0130\u00ee\u00ef", // íì?îï
  859. "I" : "\u00cd\u00cc\u0130\u00ce\u00cf", // ÍÌ?ÎÏ
  860. "o" : "\u00f3\u00f2\u00f4\u00f5\u00f6", // óòôõö
  861. "O" : "\u00d3\u00d2\u00d4\u00d5\u00d6", // ÓÒÔÕÖ
  862. "S" : "\u00df", // ß
  863. "u" : "\u00fa\u00f9\u00fb\u00fc", // úùûü
  864. "U" : "\u00da\u00d9\u00db\u00dc" // ÚÙÛÜ
  865. };
  866. this.replaceAccents = function(s) {
  867. var a, acc = '[', eq = this.characterEquivalents;
  868. if (!this.characterRegex) {
  869. this.characterRegexArray = {};
  870. for (a in eq) {
  871. if (typeof a === 'string') {
  872. acc += eq[a];
  873. this.characterRegexArray[a] = new RegExp('[' + eq[a] + ']', 'g');
  874. }
  875. }
  876. this.characterRegex = new RegExp(acc + ']');
  877. }
  878. if (this.characterRegex.test(s)) {
  879. for (a in eq) {
  880. if (typeof a === 'string') {
  881. s = s.replace( this.characterRegexArray[a], a );
  882. }
  883. }
  884. }
  885. return s;
  886. };
  887. // get sorter, string, empty, etc options for each column from
  888. // jQuery data, metadata, header option or header class name ("sorter-false")
  889. // priority = jQuery data > meta > headers option > header class name
  890. this.getData = function(h, ch, key) {
  891. var val = '', $h = $(h), m, cl;
  892. if (!$h.length) { return ''; }
  893. m = $.metadata ? $h.metadata() : false;
  894. cl = ' ' + ($h.attr('class') || '');
  895. if ($h.data() && ( typeof $h.data(key) !== 'undefined' || typeof $h.data(key.toLowerCase()) !== 'undefined') ){
  896. // "data-lockedOrder" is assigned to "lockedorder"; but "data-locked-order" is assigned to "lockedOrder"
  897. // "data-sort-initial-order" is assigned to "sortInitialOrder"
  898. val += $h.data(key) || $h.data(key.toLowerCase());
  899. } else if (m && typeof m[key] !== 'undefined') {
  900. val += m[key];
  901. } else if (ch && typeof ch[key] !== 'undefined') {
  902. val += ch[key];
  903. } else if (cl && cl.match(' ' + key + '-')) {
  904. // include sorter class name "sorter-text", etc
  905. val = cl.match( new RegExp(' ' + key + '-(\\w+)') )[1] || '';
  906. }
  907. return $.trim(val);
  908. };
  909. this.clearTableBody = function(table) {
  910. $(table.tBodies).filter(':not(.' + table.config.cssInfoBlock + ')').empty();
  911. };
  912. }
  913. })();
  914. // make shortcut
  915. var ts = $.tablesorter;
  916. // extend plugin scope
  917. $.fn.extend({
  918. tablesorter: ts.construct
  919. });
  920. // add default parsers
  921. ts.addParser({
  922. id: "text",
  923. is: function(s, table, node) {
  924. return true;
  925. },
  926. format: function(s, table, cell, cellIndex) {
  927. var c = table.config;
  928. s = $.trim( c.ignoreCase ? s.toLocaleLowerCase() : s );
  929. return c.…