2 * Copyright (c)2005-2009 Matt Kruse (javascripttoolbox.com)
4 * Dual licensed under the MIT and GPL licenses.
5 * This basically means you can use this code however you want for
6 * free, but don't claim to have written it yourself!
7 * Donations always accepted: http://www.JavascriptToolbox.com/donate/
9 * Please do not link to the .js files on javascripttoolbox.com from
10 * your site. Copy the files locally to your server instead.
15 * Functions for interactive Tables
17 * Copyright (c) 2007 Matt Kruse (javascripttoolbox.com)
18 * Dual licensed under the MIT and GPL licenses.
22 * @history 0.981 2007-03-19 Added Sort.numeric_comma, additional date parsing formats
23 * @history 0.980 2007-03-18 Release new BETA release pending some testing. Todo: Additional docs, examples, plus jQuery plugin.
24 * @history 0.959 2007-03-05 Added more "auto" functionality, couple bug fixes
25 * @history 0.958 2007-02-28 Added auto functionality based on class names
26 * @history 0.957 2007-02-21 Speed increases, more code cleanup, added Auto Sort functionality
27 * @history 0.956 2007-02-16 Cleaned up the code and added Auto Filter functionality.
28 * @history 0.950 2006-11-15 First BETA release.
30 * @todo Add more date format parsers
31 * @todo Add style classes to colgroup tags after sorting/filtering in case the user wants to highlight the whole column
32 * @todo Correct for colspans in data rows (this may slow it down)
33 * @todo Fix for IE losing form control values after sort?
39 var Sort = (function(){
41 // Default alpha-numeric sort
42 // --------------------------
43 sort.alphanumeric = function(a,b) {
44 return (a==b)?0:(a<b)?-1:1;
46 sort['default'] = sort.alphanumeric; // IE chokes on sort.default
48 // This conversion is generalized to work for either a decimal separator of , or .
49 sort.numeric_converter = function(separator) {
50 return function(val) {
51 if (typeof(val)=="string") {
52 val = parseFloat(val.replace(/^[^\d\.]*([\d., ]+).*/g,"$1").replace(new RegExp("[^\\\d"+separator+"]","g"),'').replace(/,/,'.')) || 0;
60 sort.numeric = function(a,b) {
61 return sort.numeric.convert(a)-sort.numeric.convert(b);
63 sort.numeric.convert = sort.numeric_converter(".");
65 // Numeric Sort - comma decimal separator
66 // --------------------------------------
67 sort.numeric_comma = function(a,b) {
68 return sort.numeric_comma.convert(a)-sort.numeric_comma.convert(b);
70 sort.numeric_comma.convert = sort.numeric_converter(",");
72 // Case-insensitive Sort
73 // ---------------------
74 sort.ignorecase = function(a,b) {
75 return sort.alphanumeric(sort.ignorecase.convert(a),sort.ignorecase.convert(b));
77 sort.ignorecase.convert = function(val) {
78 if (val==null) { return ""; }
79 return (""+val).toLowerCase();
84 sort.currency = sort.numeric; // Just treat it as numeric!
85 sort.currency_comma = sort.numeric_comma;
89 sort.date = function(a,b) {
90 return sort.numeric(sort.date.convert(a),sort.date.convert(b));
92 // Convert 2-digit years to 4
93 sort.date.fixYear=function(yr) {
95 if (yr<50) { yr += 2000; }
96 else if (yr<100) { yr += 1900; }
101 { re:/(\d{2,4})-(\d{1,2})-(\d{1,2})/ , f:function(x){ return (new Date(sort.date.fixYear(x[1]),+x[2],+x[3])).getTime(); } }
102 // MM/DD/YY[YY] or MM-DD-YY[YY]
103 ,{ re:/(\d{1,2})[\/-](\d{1,2})[\/-](\d{2,4})/ , f:function(x){ return (new Date(sort.date.fixYear(x[3]),+x[1],+x[2])).getTime(); } }
104 // Any catch-all format that new Date() can handle. This is not reliable except for long formats, for example: 31 Jan 2000 01:23:45 GMT
105 ,{ re:/(.*\d{4}.*\d+:\d+\d+.*)/, f:function(x){ var d=new Date(x[1]); if(d){return d.getTime();} } }
107 sort.date.convert = function(val) {
108 var m,v, f = sort.date.formats;
109 for (var i=0,L=f.length; i<L; i++) {
110 if (m=val.match(f[i].re)) {
112 if (typeof(v)!="undefined") { return v; }
115 return 9999999999999; // So non-parsed dates will be last, not first
122 * The main Table namespace
124 var Table = (function(){
127 * Determine if a reference is defined
129 function def(o) {return (typeof o!="undefined");};
132 * Determine if an object or class string contains a given class.
134 function hasClass(o,name) {
135 return new RegExp("(^|\\s)"+name+"(\\s|$)").test(o.className);
139 * Add a class to an object
141 function addClass(o,name) {
142 var c = o.className || "";
143 if (def(c) && !hasClass(o,name)) {
144 o.className += (c?" ":"") + name;
149 * Remove a class from an object
151 function removeClass(o,name) {
152 var c = o.className || "";
153 o.className = c.replace(new RegExp("(^|\\s)"+name+"(\\s|$)"),"$1");
157 * For classes that match a given substring, return the rest
159 function classValue(o,prefix) {
161 if (c.match(new RegExp("(^|\\s)"+prefix+"([^ ]+)"))) {
168 * Return true if an object is hidden.
169 * This uses the "russian doll" technique to unwrap itself to the most efficient
170 * function after the first pass. This avoids repeated feature detection that
171 * would always fall into the same block of code.
173 function isHidden(o) {
174 if (window.getComputedStyle) {
175 var cs = window.getComputedStyle;
176 return (isHidden = function(o) {
177 return 'none'==cs(o,null).getPropertyValue('display');
180 else if (window.currentStyle) {
181 return(isHidden = function(o) {
182 return 'none'==o.currentStyle['display'];
185 return (isHidden = function(o) {
186 return 'none'==o.style['display'];
191 * Get a parent element by tag name, or the original element if it is of the tag type
193 function getParent(o,a,b) {
194 if (o!=null && o.nodeName) {
195 if (o.nodeName==a || (b && o.nodeName==b)) {
198 while (o=o.parentNode) {
199 if (o.nodeName && (o.nodeName==a || (b && o.nodeName==b))) {
208 * Utility function to copy properties from one object to another
210 function copy(o1,o2) {
211 for (var i=2;i<arguments.length; i++) {
212 var a = arguments[i];
219 // The table object itself
221 //Class names used in the code
222 AutoStripeClassName:"table-autostripe",
223 StripeClassNamePrefix:"table-stripeclass:",
225 AutoSortClassName:"table-autosort",
226 AutoSortColumnPrefix:"table-autosort:",
227 AutoSortTitle:"Click to sort",
228 SortedAscendingClassName:"table-sorted-asc",
229 SortedDescendingClassName:"table-sorted-desc",
230 SortableClassName:"table-sortable",
231 SortableColumnPrefix:"table-sortable:",
232 NoSortClassName:"table-nosort",
234 AutoFilterClassName:"table-autofilter",
235 FilteredClassName:"table-filtered",
236 FilterableClassName:"table-filterable",
237 FilteredRowcountPrefix:"table-filtered-rowcount:",
238 RowcountPrefix:"table-rowcount:",
239 FilterAllLabel:"Filter: All",
241 AutoPageSizePrefix:"table-autopage:",
242 AutoPageJumpPrefix:"table-page:",
243 PageNumberPrefix:"table-page-number:",
244 PageCountPrefix:"table-page-count:"
248 * A place to store misc table information, rather than in the table objects themselves
250 table.tabledata = {};
253 * Resolve a table given an element reference, and make sure it has a unique ID
256 table.resolve = function(o,args) {
257 if (o!=null && o.nodeName && o.nodeName!="TABLE") {
258 o = getParent(o,"TABLE");
260 if (o==null) { return null; }
263 do { var id = "TABLE_"+(table.uniqueId++); }
264 while (document.getElementById(id)!=null);
267 this.tabledata[o.id] = this.tabledata[o.id] || {};
269 copy(args,this.tabledata[o.id],"stripeclass","ignorehiddenrows","useinnertext","sorttype","col","desc","page","pagesize");
276 * Run a function against each cell in a table header or footer, usually
277 * to add or remove css classes based on sorting, filtering, etc.
279 table.processTableCells = function(t, type, func, arg) {
281 if (t==null) { return; }
283 this.processCells(t.tHead, func, arg);
286 this.processCells(t.tFoot, func, arg);
291 * Internal method used to process an arbitrary collection of cells.
292 * Referenced by processTableCells.
293 * It's done this way to avoid getElementsByTagName() which would also return nested table cells.
295 table.processCells = function(section,func,arg) {
297 if (section.rows && section.rows.length && section.rows.length>0) {
298 var rows = section.rows;
299 for (var j=0,L2=rows.length; j<L2; j++) {
301 if (row.cells && row.cells.length && row.cells.length>0) {
302 var cells = row.cells;
303 for (var k=0,L3=cells.length; k<L3; k++) {
304 var cellsK = cells[k];
305 func.call(this,cellsK,arg);
314 * Get the cellIndex value for a cell. This is only needed because of a Safari
315 * bug that causes cellIndex to exist but always be 0.
316 * Rather than feature-detecting each time it is called, the function will
317 * re-write itself the first time it is called.
319 table.getCellIndex = function(td) {
320 var tr = td.parentNode;
321 var cells = tr.cells;
322 if (cells && cells.length) {
323 if (cells.length>1 && cells[cells.length-1].cellIndex>0) {
324 // Define the new function, overwrite the one we're running now, and then run the new one
325 (this.getCellIndex = function(td) {
329 // Safari will always go through this slower block every time. Oh well.
330 for (var i=0,L=cells.length; i<L; i++) {
331 if (tr.cells[i]==td) {
340 * A map of node names and how to convert them into their "value" for sorting, filtering, etc.
341 * These are put here so it is extensible.
344 'INPUT':function(node) {
345 if (def(node.value) && node.type && ((node.type!="checkbox" && node.type!="radio") || node.checked)) {
350 'SELECT':function(node) {
351 if (node.selectedIndex>=0 && node.options) {
352 // Sort select elements by the visible text
353 return node.options[node.selectedIndex].text;
357 'IMG':function(node) {
358 return node.name || "";
363 * Get the text value of a cell. Only use innerText if explicitly told to, because
364 * otherwise we want to be able to handle sorting on inputs and other types
366 table.getCellValue = function(td,useInnerText) {
367 if (useInnerText && def(td.innerText)) {
370 if (!td.childNodes) {
373 var childNodes=td.childNodes;
375 for (var i=0,L=childNodes.length; i<L; i++) {
376 var node = childNodes[i];
377 var type = node.nodeType;
378 // In order to get realistic sort results, we need to treat some elements in a special way.
379 // These behaviors are defined in the nodeValue() object, keyed by node name
381 var nname = node.nodeName;
382 if (this.nodeValue[nname]) {
383 ret += this.nodeValue[nname](node);
386 ret += this.getCellValue(node);
390 if (def(node.innerText)) {
391 ret += node.innerText;
393 else if (def(node.nodeValue)) {
394 ret += node.nodeValue;
402 * Consider colspan and rowspan values in table header cells to calculate the actual cellIndex
403 * of a given cell. This is necessary because if the first cell in row 0 has a rowspan of 2,
404 * then the first cell in row 1 will have a cellIndex of 0 rather than 1, even though it really
405 * starts in the second column rather than the first.
406 * See: http://www.javascripttoolbox.com/temp/table_cellindex.html
408 table.tableHeaderIndexes = {};
409 table.getActualCellIndex = function(tableCellObj) {
410 if (!def(tableCellObj.cellIndex)) { return null; }
411 var tableObj = getParent(tableCellObj,"TABLE");
412 var cellCoordinates = tableCellObj.parentNode.rowIndex+"-"+this.getCellIndex(tableCellObj);
414 // If it has already been computed, return the answer from the lookup table
415 if (def(this.tableHeaderIndexes[tableObj.id])) {
416 return this.tableHeaderIndexes[tableObj.id][cellCoordinates];
420 this.tableHeaderIndexes[tableObj.id] = {};
421 var thead = getParent(tableCellObj,"THEAD");
422 var trs = thead.getElementsByTagName('TR');
424 // Loop thru every tr and every cell in the tr, building up a 2-d array "grid" that gets
425 // populated with an "x" for each space that a cell takes up. If the first cell is colspan
426 // 2, it will fill in values [0] and [1] in the first array, so that the second cell will
427 // find the first empty cell in the first row (which will be [2]) and know that this is
428 // where it sits, rather than its internal .cellIndex value of [1].
429 for (var i=0; i<trs.length; i++) {
430 var cells = trs[i].cells;
431 for (var j=0; j<cells.length; j++) {
433 var rowIndex = c.parentNode.rowIndex;
434 var cellId = rowIndex+"-"+this.getCellIndex(c);
435 var rowSpan = c.rowSpan || 1;
436 var colSpan = c.colSpan || 1;
438 if(!def(matrix[rowIndex])) {
439 matrix[rowIndex] = [];
441 var m = matrix[rowIndex];
442 // Find first available column in the first row
443 for (var k=0; k<m.length+1; k++) {
449 this.tableHeaderIndexes[tableObj.id][cellId] = firstAvailCol;
450 for (var k=rowIndex; k<rowIndex+rowSpan; k++) {
451 if(!def(matrix[k])) {
454 var matrixrow = matrix[k];
455 for (var l=firstAvailCol; l<firstAvailCol+colSpan; l++) {
461 // Store the map so future lookups are fast.
462 return this.tableHeaderIndexes[tableObj.id][cellCoordinates];
466 * Sort all rows in each TBODY (tbodies are sorted independent of each other)
468 table.sort = function(o,args) {
469 var t, tdata, sortconvert=null;
470 // Allow for a simple passing of sort type as second parameter
471 if (typeof(args)=="function") {
472 args={sorttype:args};
476 // If no col is specified, deduce it from the object sent in
477 if (!def(args.col)) {
478 args.col = this.getActualCellIndex(o) || 0;
480 // If no sort type is specified, default to the default sort
481 args.sorttype = args.sorttype || Sort['default'];
484 t = this.resolve(o,args);
485 tdata = this.tabledata[t.id];
487 // If we are sorting on the same column as last time, flip the sort direction
488 if (def(tdata.lastcol) && tdata.lastcol==tdata.col && def(tdata.lastdesc)) {
489 tdata.desc = !tdata.lastdesc;
492 tdata.desc = !!args.desc;
495 // Store the last sorted column so clicking again will reverse the sort order
496 tdata.lastcol=tdata.col;
497 tdata.lastdesc=!!tdata.desc;
499 // If a sort conversion function exists, pre-convert cell values and then use a plain alphanumeric sort
500 var sorttype = tdata.sorttype;
501 if (typeof(sorttype.convert)=="function") {
502 sortconvert=tdata.sorttype.convert;
503 sorttype=Sort.alphanumeric;
506 // Loop through all THEADs and remove sorted class names, then re-add them for the col
507 // that is being sorted
508 this.processTableCells(t,"THEAD",
510 if (hasClass(cell,this.SortableClassName)) {
511 removeClass(cell,this.SortedAscendingClassName);
512 removeClass(cell,this.SortedDescendingClassName);
513 // If the computed colIndex of the cell equals the sorted colIndex, flag it as sorted
514 if (tdata.col==table.getActualCellIndex(cell) && (classValue(cell,table.SortableClassName))) {
515 addClass(cell,tdata.desc?this.SortedAscendingClassName:this.SortedDescendingClassName);
521 // Sort each tbody independently
522 var bodies = t.tBodies;
523 if (bodies==null || bodies.length==0) { return; }
525 // Define a new sort function to be called to consider descending or not
526 var newSortFunc = (tdata.desc)?
527 function(a,b){return sorttype(b[0],a[0]);}
528 :function(a,b){return sorttype(a[0],b[0]);};
530 var useinnertext=!!tdata.useinnertext;
533 for (var i=0,L=bodies.length; i<L; i++) {
534 var tb = bodies[i], tbrows = tb.rows, rows = [];
536 // Allow tbodies to request that they not be sorted
537 if(!hasClass(tb,table.NoSortClassName)) {
538 // Create a separate array which will store the converted values and refs to the
539 // actual rows. This is the array that will be sorted.
540 var cRow, cRowIndex=0;
541 if (cRow=tbrows[cRowIndex]){
542 // Funky loop style because it's considerably faster in IE
544 if (rowCells = cRow.cells) {
545 var cellValue = (col<rowCells.length)?this.getCellValue(rowCells[col],useinnertext):null;
546 if (sortconvert) cellValue = sortconvert(cellValue);
547 rows[cRowIndex] = [cellValue,tbrows[cRowIndex]];
549 } while (cRow=tbrows[++cRowIndex])
552 // Do the actual sorting
553 rows.sort(newSortFunc);
555 // Move the rows to the correctly sorted order. Appending an existing DOM object just moves it!
557 var displayedCount=0;
558 var f=[removeClass,addClass];
559 if (cRow=rows[cRowIndex]){
561 tb.appendChild(cRow[1]);
562 } while (cRow=rows[++cRowIndex])
567 // If paging is enabled on the table, then we need to re-page because the order of rows has changed!
568 if (tdata.pagesize) {
569 this.page(t); // This will internally do the striping
572 // Re-stripe if a class name was supplied
573 if (tdata.stripeclass) {
574 this.stripe(t,tdata.stripeclass,!!tdata.ignorehiddenrows);
580 * Apply a filter to rows in a table and hide those that do not match.
582 table.filter = function(o,filters,args) {
586 var t = this.resolve(o,args);
587 var tdata = this.tabledata[t.id];
589 // If new filters were passed in, apply them to the table's list of filters
591 // If a null or blank value was sent in for 'filters' then that means reset the table to no filters
592 tdata.filters = null;
595 // Allow for passing a select list in as the filter, since this is common design
596 if (filters.nodeName=="SELECT" && filters.type=="select-one" && filters.selectedIndex>-1) {
597 filters={ 'filter':filters.options[filters.selectedIndex].value };
599 // Also allow for a regular input
600 if (filters.nodeName=="INPUT" && filters.type=="text") {
601 filters={ 'filter':"/^"+filters.value+"/" };
603 // Force filters to be an array
604 if (typeof(filters)=="object" && !filters.length) {
608 // Convert regular expression strings to RegExp objects and function strings to function objects
609 for (var i=0,L=filters.length; i<L; i++) {
610 var filter = filters[i];
611 if (typeof(filter.filter)=="string") {
612 // If a filter string is like "/expr/" then turn it into a Regex
613 if (filter.filter.match(/^\/(.*)\/$/)) {
614 filter.filter = new RegExp(RegExp.$1);
615 filter.filter.regex=true;
617 // If filter string is like "function (x) { ... }" then turn it into a function
618 else if (filter.filter.match(/^function\s*\(([^\)]*)\)\s*\{(.*)}\s*$/)) {
619 filter.filter = Function(RegExp.$1,RegExp.$2);
622 // If some non-table object was passed in rather than a 'col' value, resolve it
623 // and assign it's column index to the filter if it doesn't have one. This way,
624 // passing in a cell reference or a select object etc instead of a table object
625 // will automatically set the correct column to filter.
626 if (filter && !def(filter.col) && (cell=getParent(o,"TD","TH"))) {
627 filter.col = this.getCellIndex(cell);
630 // Apply the passed-in filters to the existing list of filters for the table, removing those that have a filter of null or ""
631 if ((!filter || !filter.filter) && tdata.filters) {
632 delete tdata.filters[filter.col];
635 tdata.filters = tdata.filters || {};
636 tdata.filters[filter.col] = filter.filter;
639 // If no more filters are left, then make sure to empty out the filters object
640 for (var j in tdata.filters) { var keep = true; }
642 tdata.filters = null;
645 // Everything's been setup, so now scrape the table rows
646 return table.scrape(o);
650 * "Page" a table by showing only a subset of the rows
652 table.page = function(t,page,args) {
654 if (def(page)) { args.page = page; }
655 return table.scrape(t,args);
659 * Jump forward or back any number of pages
661 table.pageJump = function(t,count,args) {
662 t = this.resolve(t,args);
663 return this.page(t,(table.tabledata[t.id].page||0)+count,args);
667 * Go to the next page of a paged table
669 table.pageNext = function(t,args) {
670 return this.pageJump(t,1,args);
674 * Go to the previous page of a paged table
676 table.pagePrevious = function(t,args) {
677 return this.pageJump(t,-1,args);
681 * Scrape a table to either hide or show each row based on filters and paging
683 table.scrape = function(o,args) {
684 var col,cell,filterList,filterReset=false,filter;
685 var page,pagesize,pagestart,pageend;
686 var unfilteredrows=[],unfilteredrowcount=0,totalrows=0;
687 var t,tdata,row,hideRow;
690 // Resolve the table object
691 t = this.resolve(o,args);
692 tdata = this.tabledata[t.id];
695 var page = tdata.page;
697 // Don't let the page go before the beginning
698 if (page<0) { tdata.page=page=0; }
699 pagesize = tdata.pagesize || 25; // 25=arbitrary default
700 pagestart = page*pagesize+1;
701 pageend = pagestart + pagesize - 1;
704 // Scrape each row of each tbody
705 var bodies = t.tBodies;
706 if (bodies==null || bodies.length==0) { return; }
707 for (var i=0,L=bodies.length; i<L; i++) {
709 for (var j=0,L2=tb.rows.length; j<L2; j++) {
713 // Test if filters will hide the row
714 if (tdata.filters && row.cells) {
715 var cells = row.cells;
716 var cellsLength = cells.length;
718 for (col in tdata.filters) {
720 filter = tdata.filters[col];
721 if (filter && col<cellsLength) {
722 var val = this.getCellValue(cells[col]);
723 if (filter.regex && val.search) {
724 hideRow=(val.search(filter)<0);
726 else if (typeof(filter)=="function") {
727 hideRow=!filter(val,cells[col]);
730 hideRow = (val!=filter);
737 // Keep track of the total rows scanned and the total runs _not_ filtered out
740 unfilteredrowcount++;
742 // Temporarily keep an array of unfiltered rows in case the page we're on goes past
743 // the last page and we need to back up. Don't want to filter again!
744 unfilteredrows.push(row);
745 if (unfilteredrowcount<pagestart || unfilteredrowcount>pageend) {
751 row.style.display = hideRow?"none":"";
756 // Check to see if filtering has put us past the requested page index. If it has,
757 // then go back to the last page and show it.
758 if (pagestart>=unfilteredrowcount) {
759 pagestart = unfilteredrowcount-(unfilteredrowcount%pagesize);
760 tdata.page = page = pagestart/pagesize;
761 for (var i=pagestart,L=unfilteredrows.length; i<L; i++) {
762 unfilteredrows[i].style.display="";
767 // Loop through all THEADs and add/remove filtered class names
768 this.processTableCells(t,"THEAD",
770 ((tdata.filters && def(tdata.filters[table.getCellIndex(c)]) && hasClass(c,table.FilterableClassName))?addClass:removeClass)(c,table.FilteredClassName);
774 // Stripe the table if necessary
775 if (tdata.stripeclass) {
779 // Calculate some values to be returned for info and updating purposes
780 var pagecount = Math.floor(unfilteredrowcount/pagesize)+1;
782 // Update the page number/total containers if they exist
783 if (tdata.container_number) {
784 tdata.container_number.innerHTML = page+1;
786 if (tdata.container_count) {
787 tdata.container_count.innerHTML = pagecount;
791 // Update the row count containers if they exist
792 if (tdata.container_filtered_count) {
793 tdata.container_filtered_count.innerHTML = unfilteredrowcount;
795 if (tdata.container_all_count) {
796 tdata.container_all_count.innerHTML = totalrows;
798 return { 'data':tdata, 'unfilteredcount':unfilteredrowcount, 'total':totalrows, 'pagecount':pagecount, 'page':page, 'pagesize':pagesize };
802 * Shade alternate rows, aka Stripe the table.
804 table.stripe = function(t,className,args) {
806 args.stripeclass = className;
808 t = this.resolve(t,args);
809 var tdata = this.tabledata[t.id];
811 var bodies = t.tBodies;
812 if (bodies==null || bodies.length==0) {
816 className = tdata.stripeclass;
817 // Cache a shorter, quicker reference to either the remove or add class methods
818 var f=[removeClass,addClass];
819 for (var i=0,L=bodies.length; i<L; i++) {
820 var tb = bodies[i], tbrows = tb.rows, cRowIndex=0, cRow, displayedCount=0;
821 if (cRow=tbrows[cRowIndex]){
822 // The ignorehiddenrows test is pulled out of the loop for a slight speed increase.
823 // Makes a bigger difference in FF than in IE.
824 // In this case, speed always wins over brevity!
825 if (tdata.ignoreHiddenRows) {
827 f[displayedCount++%2](cRow,className);
828 } while (cRow=tbrows[++cRowIndex])
832 if (!isHidden(cRow)) {
833 f[displayedCount++%2](cRow,className);
835 } while (cRow=tbrows[++cRowIndex])
842 * Build up a list of unique values in a table column
844 table.getUniqueColValues = function(t,col) {
845 var values={}, bodies = this.resolve(t).tBodies;
846 for (var i=0,L=bodies.length; i<L; i++) {
847 var tbody = bodies[i];
848 for (var r=0,L2=tbody.rows.length; r<L2; r++) {
849 values[this.getCellValue(tbody.rows[r].cells[col])] = true;
853 for (var val in values) {
856 return valArray.sort();
860 * Scan the document on load and add sorting, filtering, paging etc ability automatically
861 * based on existence of class names on the table and cells.
863 table.auto = function(args) {
864 var cells = [], tables = document.getElementsByTagName("TABLE");
867 for (var i=0,L=tables.length; i<L; i++) {
868 var t = table.resolve(tables[i]);
869 tdata = table.tabledata[t.id];
870 if (val=classValue(t,table.StripeClassNamePrefix)) {
871 tdata.stripeclass=val;
873 // Do auto-filter if necessary
874 if (hasClass(t,table.AutoFilterClassName)) {
877 // Do auto-page if necessary
878 if (val = classValue(t,table.AutoPageSizePrefix)) {
879 table.autopage(t,{'pagesize':+val});
881 // Do auto-sort if necessary
882 if ((val = classValue(t,table.AutoSortColumnPrefix)) || (hasClass(t,table.AutoSortClassName))) {
883 table.autosort(t,{'col':(val==null)?null:+val});
885 // Do auto-stripe if necessary
886 if (tdata.stripeclass && hasClass(t,table.AutoStripeClassName)) {
894 * Add sorting functionality to a table header cell
896 table.autosort = function(t,args) {
897 t = this.resolve(t,args);
898 var tdata = this.tabledata[t.id];
899 this.processTableCells(t, "THEAD", function(c) {
900 var type = classValue(c,table.SortableColumnPrefix);
902 type = type || "default";
903 c.title =c.title || table.AutoSortTitle;
904 addClass(c,table.SortableClassName);
905 c.onclick = Function("","Table.sort(this,{'sorttype':Sort['"+type+"']})");
906 // If we are going to auto sort on a column, we need to keep track of what kind of sort it will be
907 if (args.col!=null) {
908 if (args.col==table.getActualCellIndex(c)) {
909 tdata.sorttype=Sort['"+type+"'];
914 if (args.col!=null) {
920 * Add paging functionality to a table
922 table.autopage = function(t,args) {
923 t = this.resolve(t,args);
924 var tdata = this.tabledata[t.id];
925 if (tdata.pagesize) {
926 this.processTableCells(t, "THEAD,TFOOT", function(c) {
927 var type = classValue(c,table.AutoPageJumpPrefix);
928 if (type=="next") { type = 1; }
929 else if (type=="previous") { type = -1; }
931 c.onclick = Function("","Table.pageJump(this,"+type+")");
934 if (val = classValue(t,table.PageNumberPrefix)) {
935 tdata.container_number = document.getElementById(val);
937 if (val = classValue(t,table.PageCountPrefix)) {
938 tdata.container_count = document.getElementById(val);
940 return table.page(t,0,args);
945 * A util function to cancel bubbling of clicks on filter dropdowns
947 table.cancelBubble = function(e) {
948 e = e || window.event;
949 if (typeof(e.stopPropagation)=="function") { e.stopPropagation(); }
950 if (def(e.cancelBubble)) { e.cancelBubble = true; }
954 * Auto-filter a table
956 table.autofilter = function(t,args) {
958 t = this.resolve(t,args);
959 var tdata = this.tabledata[t.id],val;
960 table.processTableCells(t, "THEAD", function(cell) {
961 if (hasClass(cell,table.FilterableClassName)) {
962 var cellIndex = table.getCellIndex(cell);
963 var colValues = table.getUniqueColValues(t,cellIndex);
964 if (colValues.length>0) {
965 if (typeof(args.insert)=="function") {
966 func.insert(cell,colValues);
969 var sel = '<select onchange="Table.filter(this,this)" onclick="Table.cancelBubble(event)" class="'+table.AutoFilterClassName+'"><option value="">'+table.FilterAllLabel+'</option>';
970 for (var i=0; i<colValues.length; i++) {
971 sel += '<option value="'+colValues[i]+'">'+colValues[i]+'</option>';
974 cell.innerHTML += "<br>"+sel;
979 if (val = classValue(t,table.FilteredRowcountPrefix)) {
980 tdata.container_filtered_count = document.getElementById(val);
982 if (val = classValue(t,table.RowcountPrefix)) {
983 tdata.container_all_count = document.getElementById(val);
988 * Attach the auto event so it happens on load.
989 * use jQuery's ready() function if available
991 if (typeof(jQuery)!="undefined") {
994 else if (window.addEventListener) {
995 window.addEventListener( "load", table.auto, false );
997 else if (window.attachEvent) {
998 window.attachEvent( "onload", table.auto );