jquery.tablesorter.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727
  1. (function($) {
  2. $.extend({
  3. tablesorter: new function() {
  4. var parsers = [], widgets = [];
  5. this.defaults = {
  6. cssHeader: "header",
  7. cssAsc: "headerSortUp",
  8. cssDesc: "headerSortDown",
  9. sortInitialOrder: "asc",
  10. sortMultiSortKey: "shiftKey",
  11. sortForce: null,
  12. textExtraction: "simple",
  13. parsers: {},
  14. widgets: [],
  15. widgetZebra: {css: ["even","odd"]},
  16. headers: {},
  17. widthFixed: false,
  18. cancelSelection: true,
  19. sortList: [],
  20. headerList: [],
  21. dateFormat: "us",
  22. debug: false
  23. };
  24. /* debuging utils */
  25. function benchmark(label,stamp) {
  26. log(label + "," + (new Date().getTime() - stamp.getTime()) + "ms");
  27. }
  28. function log(s) {
  29. if (typeof console != "undefined" && typeof console.debug != "undefined") {
  30. console.log(s);
  31. } else {
  32. alert(s);
  33. }
  34. }
  35. /* parsers utils */
  36. function buildParserCache(table,$headers) {
  37. if(table.config.debug) { var parsersDebug = ""; }
  38. var list = [], cells = table.tBodies[0].rows[0].cells, l = cells.length;
  39. for (var i=0;i < l; i++) {
  40. var p = false;
  41. if($.meta && ($($headers[i]).data() && $($headers[i]).data().sorter) ) {
  42. p = getParserById($($headers[i]).data().sorter);
  43. } else if((table.config.headers[i] && table.config.headers[i].sorter)) {
  44. p = getParserById(table.config.headers[i].sorter);
  45. }
  46. if(!p) {
  47. p = detectParserForColumn(table.config,cells[i]);
  48. }
  49. if(table.config.debug) { parsersDebug += "column:" + i + " parser:" +p.id + "\n"; }
  50. list.push(p);
  51. }
  52. if(table.config.debug) { log(parsersDebug); }
  53. return list;
  54. };
  55. function detectParserForColumn(config,node) {
  56. var l = parsers.length;
  57. for(var i=1; i < l; i++) {
  58. if(parsers[i].is($.trim(getElementText(config,node)))) {
  59. return parsers[i];
  60. }
  61. }
  62. // 0 is always the generic parser (text)
  63. return parsers[0];
  64. }
  65. function getParserById(name) {
  66. var l = parsers.length;
  67. for(var i=0; i < l; i++) {
  68. if(parsers[i].id.toLowerCase() == name.toLowerCase()) {
  69. return parsers[i];
  70. }
  71. }
  72. return false;
  73. }
  74. /* utils */
  75. function buildCache(table) {
  76. if(table.config.debug) { var cacheTime = new Date(); }
  77. var totalRows = (table.tBodies[0] && table.tBodies[0].rows.length) || 0,
  78. totalCells = table.tBodies[0].rows[0].cells.length,
  79. parsers = table.config.parsers,
  80. cache = {row: [], normalized: []};
  81. for (var i=0;i < totalRows; ++i) {
  82. /** Add the table data to main data array */
  83. var c = table.tBodies[0].rows[i], cols = [];
  84. cache.row.push($(c));
  85. for(var j=0; j < totalCells; ++j) {
  86. cols.push(parsers[j].format(getElementText(table.config,c.cells[j]),table,c.cells[j]));
  87. }
  88. cols.push(i); // add position for rowCache
  89. cache.normalized.push(cols);
  90. cols = null;
  91. };
  92. if(table.config.debug) { benchmark("Building cache for " + totalRows + " rows:", cacheTime); }
  93. return cache;
  94. };
  95. function getElementText(config,node) {
  96. if(!node) return "";
  97. var t = "";
  98. if(typeof(config.textExtraction) == "function") {
  99. t = config.textExtraction(node);
  100. } else if(config.textExtraction == "complex") {
  101. t = $(node).text();
  102. } else {
  103. if(node.childNodes[0] && node.childNodes[0].hasChildNodes()) {
  104. t = node.childNodes[0].innerHTML;
  105. } else {
  106. t = node.innerHTML;
  107. }
  108. }
  109. return t;
  110. }
  111. function appendToTable(table,cache) {
  112. if(table.config.debug) {var appendTime = new Date()}
  113. var c = cache,
  114. r = c.row,
  115. n= c.normalized,
  116. totalRows = n.length,
  117. checkCell = (n[0].length-1),
  118. tableBody = $("tbody:first",table).empty();
  119. rows = [];
  120. for (var i=0;i < totalRows; i++) {
  121. rows.push(r[n[i][checkCell]]);
  122. if(table.config.appender == null) {
  123. tableBody.append(r[n[i][checkCell]]);
  124. }
  125. }
  126. if(table.config.appender != null) {
  127. table.config.appender(table,rows);
  128. }
  129. rows = null;
  130. //apply table widgets
  131. applyWidget(table);
  132. if(table.config.debug) { benchmark("Rebuilt table:", appendTime); }
  133. };
  134. function buildHeaders(table) {
  135. if(table.config.debug) { var time = new Date(); }
  136. var meta = ($.meta) ? true : false, tableHeadersRows = [];
  137. for(var i = 0; i < table.tHead.rows.length; i++) { tableHeadersRows[i]=0; };
  138. $tableHeaders = $(checkCellColSpan(table, tableHeadersRows, 0,table.tHead.rows[0].cells.length));
  139. $tableHeaders.each(function(index) {
  140. this.count = 0;
  141. this.column = index;
  142. this.order = formatSortingOrder(table.config.sortInitialOrder);
  143. if(checkHeaderMetadata(this) || checkHeaderOptions(table,index)) this.sortDisabled = true;
  144. if(!this.sortDisabled) {
  145. $(this).addClass(table.config.cssHeader);
  146. }
  147. // add cell to headerList
  148. table.config.headerList[index]= this;
  149. });
  150. if(table.config.debug) { benchmark("Built headers:", time); log($tableHeaders); }
  151. return $tableHeaders;
  152. };
  153. function checkCellColSpan(table, headerArr, row) {
  154. var arr = [], r = table.tHead.rows, c = r[row].cells;
  155. for(var i=headerArr[row]; i < c.length; i++) {
  156. var cell = c[i];
  157. if ( cell.colSpan > 1) {
  158. arr = arr.concat(checkCellColSpan(table, headerArr,row+cell.rowSpan));
  159. } else {
  160. if(table.tHead.length == 1 || (cell.rowSpan > 1 || !r[row+1])) {
  161. arr.push(cell);
  162. }
  163. headerArr[row] = (i+row);
  164. }
  165. }
  166. return arr;
  167. };
  168. function checkHeaderMetadata(cell) {
  169. if(($.meta) && ($(cell).data().sorter === false)) { return true; };
  170. return false;
  171. }
  172. function checkHeaderOptions(table,i) {
  173. if((table.config.headers[i]) && (table.config.headers[i].sorter === false)) { return true; };
  174. return false;
  175. }
  176. function applyWidget(table) {
  177. var c = table.config.widgets;
  178. var l = c.length;
  179. for(var i=0; i < l; i++) {
  180. getWidgetById(c[i]).format(table);
  181. }
  182. }
  183. function getWidgetById(name) {
  184. var l = widgets.length;
  185. for(var i=0; i < l; i++) {
  186. if(widgets[i].id.toLowerCase() == name.toLowerCase() ) {
  187. return widgets[i];
  188. }
  189. }
  190. };
  191. function formatSortingOrder(v) {
  192. if(typeof(v) != "Number") {
  193. i = (v.toLowerCase() == "desc") ? 1 : 0;
  194. } else {
  195. i = (v == (0 || 1)) ? v : 0;
  196. }
  197. return i;
  198. }
  199. function isValueInArray(v, a) {
  200. var l = a.length;
  201. for(var i=0; i < l; i++) {
  202. if(a[i][0] == v) {
  203. return true;
  204. }
  205. }
  206. return false;
  207. }
  208. function setHeadersCss(table,$headers, list, css) {
  209. // remove all header information
  210. $headers.removeClass(css[0]).removeClass(css[1]);
  211. var h = [];
  212. $headers.each(function(offset) {
  213. if(!this.sortDisabled) {
  214. h[this.column] = $(this);
  215. }
  216. });
  217. var l = list.length;
  218. for(var i=0; i < l; i++) {
  219. h[list[i][0]].addClass(css[list[i][1]]);
  220. }
  221. }
  222. function fixColumnWidth(table,$headers) {
  223. var c = table.config;
  224. if(c.widthFixed) {
  225. var colgroup = $('<colgroup>');
  226. $("tbody:first tr:first td",table).each(function() {
  227. colgroup.append($('<col>').css('width',$(this).width()));
  228. });
  229. $(table).prepend(colgroup);
  230. };
  231. }
  232. function updateHeaderSortCount(table,sortList) {
  233. var c = table.config, l = sortList.length;
  234. for(var i=0; i < l; i++) {
  235. var s = sortList[i], o = c.headerList[s[0]];
  236. o.count = s[1];
  237. o.count++;
  238. }
  239. }
  240. /* sorting methods */
  241. function multisort(table,sortList,cache) {
  242. if(table.config.debug) { var sortTime = new Date(); }
  243. var dynamicExp = "var sortWrapper = function(a,b) {", l = sortList.length;
  244. for(var i=0; i < l; i++) {
  245. var c = sortList[i][0];
  246. var order = sortList[i][1];
  247. var s = (getCachedSortType(table.config.parsers,c) == "text") ? ((order == 0) ? "sortText" : "sortTextDesc") : ((order == 0) ? "sortNumeric" : "sortNumericDesc");
  248. var e = "e" + i;
  249. dynamicExp += "var " + e + " = " + s + "(a[" + c + "],b[" + c + "]); ";
  250. dynamicExp += "if(" + e + ") { return " + e + "; } ";
  251. dynamicExp += "else { ";
  252. }
  253. for(var i=0; i < l; i++) {
  254. dynamicExp += "}; ";
  255. }
  256. dynamicExp += "return 0; ";
  257. dynamicExp += "}; ";
  258. eval(dynamicExp);
  259. cache.normalized.sort(sortWrapper);
  260. if(table.config.debug) { benchmark("Sorting on " + sortList.toString() + " and dir " + order+ " time:", sortTime); }
  261. return cache;
  262. };
  263. function sortText(a,b) {
  264. return ((a < b) ? -1 : ((a > b) ? 1 : 0));
  265. };
  266. function sortTextDesc(a,b) {
  267. return ((b < a) ? -1 : ((b > a) ? 1 : 0));
  268. };
  269. function sortNumeric(a,b) {
  270. return a-b;
  271. };
  272. function sortNumericDesc(a,b) {
  273. return b-a;
  274. };
  275. function getCachedSortType(parsers,i) {
  276. return parsers[i].type;
  277. };
  278. /* public methods */
  279. this.construct = function(settings) {
  280. return this.each(function() {
  281. var $this, $document,$headers, cache, config, shiftDown = 0, sortOrder;
  282. this.config = {};
  283. config = $.extend(this.config, $.tablesorter.defaults, settings);
  284. if(!this.tHead || !this.tBodies) return true;
  285. // store common expression for speed
  286. $this = $(this);
  287. // build headers
  288. $headers = buildHeaders(this);
  289. // try to auto detect column type, and store in tables config
  290. this.config.parsers = buildParserCache(this,$headers);
  291. // build the cache for the tbody cells
  292. cache = buildCache(this);
  293. // get the css class names, could be done else where.
  294. var sortCSS = [config.cssDesc,config.cssAsc];
  295. // fixate columns if the users supplies the fixedWidth option
  296. fixColumnWidth(this);
  297. // apply event handling to headers
  298. // this is to big, perhaps break it out?
  299. $headers.click(function(e) {
  300. if(!this.sortDisabled) {
  301. // store exp, for speed
  302. var $cell = $(this);
  303. // get current column index
  304. var i = this.column;
  305. // get current column sort order
  306. this.order = this.count++ % 2;
  307. // user only whants to sort on one column
  308. if(!e[config.sortMultiSortKey]) {
  309. // flush the sort list
  310. config.sortList = [];
  311. if(config.sortForce != null) {
  312. var a = config.sortForce;
  313. for(var j=0; j < a.length; j++) {
  314. config.sortList.push(a[j]);
  315. }
  316. }
  317. // add column to sort list
  318. config.sortList.push([i,this.order]);
  319. // multi column sorting
  320. } else {
  321. // the user has clicked on an all ready sortet column.
  322. if(isValueInArray(i,config.sortList)) {
  323. // revers the sorting direction for all tables.
  324. for(var j=0; j < config.sortList.length; j++) {
  325. var s = config.sortList[j], o = config.headerList[s[0]];
  326. if(s[0] == i) {
  327. o.count = s[1];
  328. o.count++;
  329. s[1] = o.count % 2;
  330. }
  331. }
  332. } else {
  333. // add column to sort list array
  334. config.sortList.push([i,this.order]);
  335. }
  336. };
  337. //set css for headers
  338. setHeadersCss($this[0],$headers,config.sortList,sortCSS);
  339. // sort the table and append it to the dom
  340. appendToTable($this[0],multisort($this[0],config.sortList,cache));
  341. // stop normal event by returning false
  342. return false;
  343. }
  344. // cancel selection
  345. }).mousedown(function() {
  346. if(config.cancelSelection) {
  347. this.onselectstart = function() {return false};
  348. //alert(this.onselectstart);
  349. return false;
  350. }
  351. });
  352. // apply easy methods that trigger binded events
  353. $this.bind("update",function() {
  354. // rebuild the cache map
  355. cache = buildCache(this);
  356. }).bind("sorton",function(e,list) {
  357. // update and store the sortlist
  358. var sortList = config.sortList = list;
  359. // update header count index
  360. updateHeaderSortCount(this,sortList);
  361. //set css for headers
  362. setHeadersCss(this,$headers,sortList,sortCSS);
  363. // sort the table and append it to the dom
  364. appendToTable(this,multisort(this,sortList,cache));
  365. }).bind("appendCache",function() {
  366. appendToTable(this,cache);
  367. }).bind("applyWidgetId",function(e,id) {
  368. getWidgetById(id).format(this);
  369. });
  370. if($.meta && ($(this).data() && $(this).data().sortlist)) {
  371. config.sortList = $(this).data().sortlist;
  372. }
  373. // if user has supplied a sort list to constructor.
  374. if(config.sortList.length > 0) {
  375. $this.trigger("sorton",[config.sortList]);
  376. }
  377. // apply widgets
  378. applyWidget(this);
  379. });
  380. };
  381. this.addParser = function(parser) {
  382. var l = parsers.length, a = true;
  383. for(var i=0; i < l; i++) {
  384. if(parsers[i].id.toLowerCase() == parser.id.toLowerCase()) {
  385. a = false;
  386. }
  387. }
  388. if(a) { parsers.push(parser); };
  389. };
  390. this.addWidget = function(widget) {
  391. widgets.push(widget);
  392. };
  393. this.formatFloat = function(s) {
  394. var i = parseFloat(s);
  395. return (isNaN(i)) ? 0 : i;
  396. };
  397. this.formatInt = function(s) {
  398. var i = parseInt(s);
  399. return (isNaN(i)) ? 0 : i;
  400. };
  401. }
  402. });
  403. // extend plugin scope
  404. $.fn.extend({
  405. tablesorter: $.tablesorter.construct
  406. });
  407. // add default parsers
  408. $.tablesorter.addParser({
  409. id: "text",
  410. is: function(s) {
  411. return true;
  412. },
  413. format: function(s) {
  414. return $.trim(s.toLowerCase());
  415. },
  416. type: "text"
  417. });
  418. $.tablesorter.addParser({
  419. id: "integer",
  420. is: function(s) {
  421. return s.match(new RegExp(/^\d+$/));
  422. },
  423. format: function(s) {
  424. return $.tablesorter.formatInt(s);
  425. },
  426. type: "numeric"
  427. });
  428. $.tablesorter.addParser({
  429. id: "currency",
  430. is: function(s) {
  431. return /^[£$€?.]/.test(s);
  432. },
  433. format: function(s) {
  434. return $.tablesorter.formatFloat(s.replace(new RegExp(/[^0-9.]/g),""));
  435. },
  436. type: "numeric"
  437. });
  438. $.tablesorter.addParser({
  439. id: "integer",
  440. is: function(s) {
  441. return /^\d+$/.test(s);
  442. },
  443. format: function(s) {
  444. return $.tablesorter.formatFloat(s);
  445. },
  446. type: "numeric"
  447. });
  448. $.tablesorter.addParser({
  449. id: "floating",
  450. is: function(s) {
  451. return s.match(new RegExp(/^(\+|-)?[0-9]+\.[0-9]+((E|e)(\+|-)?[0-9]+)?$/));
  452. },
  453. format: function(s) {
  454. return $.tablesorter.formatFloat(s.replace(new RegExp(/,/),""));
  455. },
  456. type: "numeric"
  457. });
  458. $.tablesorter.addParser({
  459. id: "ipAddress",
  460. is: function(s) {
  461. return /^\d{2,3}[\.]\d{2,3}[\.]\d{2,3}[\.]\d{2,3}$/.test(s);
  462. },
  463. format: function(s) {
  464. var a = s.split(".");
  465. var r = "";
  466. for (var i = 0, item; item = a[i]; i++) {
  467. if(item.length == 2) {
  468. r += "0" + item;
  469. } else {
  470. r += item;
  471. }
  472. }
  473. return $.tablesorter.formatFloat(s);
  474. },
  475. type: "numeric"
  476. });
  477. $.tablesorter.addParser({
  478. id: "url",
  479. is: function(s) {
  480. return /^(https?|ftp|file):\/\/$/.test(s);
  481. },
  482. format: function(s) {
  483. return jQuery.trim(s.replace(new RegExp(/(https?|ftp|file):\/\//),''));
  484. },
  485. type: "text"
  486. });
  487. $.tablesorter.addParser({
  488. id: "isoDate",
  489. is: function(s) {
  490. return /^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(s);
  491. },
  492. format: function(s) {
  493. return $.tablesorter.formatFloat((s != "") ? new Date(s.replace(new RegExp(/-/g),"/")).getTime() : "0");
  494. },
  495. type: "numeric"
  496. });
  497. $.tablesorter.addParser({
  498. id: "percent",
  499. is: function(s) {
  500. return /^\d{1,3}%$/.test(s);
  501. },
  502. format: function(s) {
  503. return $.tablesorter.formatFloat(s.replace(new RegExp(/%/g),""));
  504. },
  505. type: "numeric"
  506. });
  507. $.tablesorter.addParser({
  508. id: "usLongDate",
  509. is: function(s) {
  510. return /^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|\'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/.test(s);
  511. },
  512. format: function(s) {
  513. return $.tablesorter.formatFloat(new Date(s).getTime());
  514. },
  515. type: "numeric"
  516. });
  517. $.tablesorter.addParser({
  518. id: "shortDate",
  519. is: function(s) {
  520. return /\d{1,2}[\/-]\d{1,2}[\/-]\d{2,4}/.test(s);
  521. },
  522. format: function(s,table) {
  523. var c = table.config;
  524. s = s.replace(new RegExp(/-/g),"/");
  525. if(c.dateFormat == "us") {
  526. /** reformat the string in ISO format */
  527. s = s.replace(new RegExp(/(\d{1,2})[\/-](\d{1,2})[\/-](\d{4})/), "$3/$1/$2");
  528. } else if(c.dateFormat == "uk") {
  529. /** reformat the string in ISO format */
  530. s = s.replace(new RegExp(/(\d{1,2})[\/-](\d{1,2})[\/-](\d{4})/), "$3/$2/$1");
  531. } else if(c.dateFormat == "dd/mm/yy" || c.dateFormat == "dd-mm-yy") {
  532. s = s.replace(new RegExp(/(\d{1,2})[\/-](\d{1,2})[\/-](\d{2})/), "$1/$2/$3");
  533. }
  534. return $.tablesorter.formatFloat(new Date(s).getTime());
  535. },
  536. type: "numeric"
  537. });
  538. $.tablesorter.addParser({
  539. id: "time",
  540. is: function(s) {
  541. return /^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/.test(s);
  542. },
  543. format: function(s) {
  544. return $.tablesorter.formatFloat(new Date("2000/01/01 " + s).getTime());
  545. },
  546. type: "numeric"
  547. });
  548. $.tablesorter.addParser({
  549. id: "metadata",
  550. is: function(s) {
  551. return false;
  552. },
  553. format: function(s,table,cell) {
  554. var c = table.config, p = (!c.parserMetadataName) ? 'sortValue' : c.parserMetadataName;
  555. return $(cell).data()[p];
  556. },
  557. type: "numeric"
  558. });
  559. // add default widgets
  560. $.tablesorter.addWidget({
  561. id: "zebra",
  562. format: function(table) {
  563. $("> tbody:first/tr:visible:even",table).removeClass(table.config.widgetZebra.css[1]).addClass(table.config.widgetZebra.css[0]);
  564. $("> tbody:first/tr:visible:odd",table).removeClass(table.config.widgetZebra.css[0]).addClass(table.config.widgetZebra.css[1]);
  565. }
  566. });
  567. })(jQuery);