Removed /usr/local from CDPATH
[clearscm.git] / rmc / rmctable.js
1 /**
2  * Copyright (c)2005-2009 Matt Kruse (javascripttoolbox.com)
3  * 
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/
8  * 
9  * Please do not link to the .js files on javascripttoolbox.com from
10  * your site. Copy the files locally to your server instead.
11  * 
12  */
13 /**
14  * Table.js
15  * Functions for interactive Tables
16  *
17  * Copyright (c) 2007 Matt Kruse (javascripttoolbox.com)
18  * Dual licensed under the MIT and GPL licenses. 
19  *
20  * @version 0.981
21  *
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.
29  *
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?
34  */
35
36 /**
37  * Sort Functions
38  */
39 var Sort = (function(){
40   var sort = {};
41   // Default alpha-numeric sort
42   // --------------------------
43   sort.alphanumeric = function(a,b) {
44     return (a==b)?0:(a<b)?-1:1;
45   };
46   sort['default'] = sort.alphanumeric; // IE chokes on sort.default
47
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;
53       }
54       return val || 0;
55     };
56   };
57
58   // Numeric Sort  
59   // ------------
60   sort.numeric = function(a,b) {
61     return sort.numeric.convert(a)-sort.numeric.convert(b);
62   };
63   sort.numeric.convert = sort.numeric_converter(".");
64
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);
69   };
70   sort.numeric_comma.convert = sort.numeric_converter(",");
71
72   // Case-insensitive Sort
73   // ---------------------
74   sort.ignorecase = function(a,b) {
75     return sort.alphanumeric(sort.ignorecase.convert(a),sort.ignorecase.convert(b));
76   };
77   sort.ignorecase.convert = function(val) {
78     if (val==null) { return ""; }
79     return (""+val).toLowerCase();
80   };
81
82   // Currency Sort
83   // -------------
84   sort.currency = sort.numeric; // Just treat it as numeric!
85   sort.currency_comma = sort.numeric_comma;
86
87   // Date sort
88   // ---------
89   sort.date = function(a,b) {
90     return sort.numeric(sort.date.convert(a),sort.date.convert(b));
91   };
92   // Convert 2-digit years to 4
93   sort.date.fixYear=function(yr) {
94     yr = +yr;
95     if (yr<50) { yr += 2000; }
96     else if (yr<100) { yr += 1900; }
97     return yr;
98   };
99   sort.date.formats = [
100     // YY[YY]-MM-DD
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();} } }
106   ];
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)) {
111         v=f[i].f(m);
112         if (typeof(v)!="undefined") { return v; }
113       }
114     }
115     return 9999999999999; // So non-parsed dates will be last, not first
116   };
117
118   return sort;
119 })();
120
121 /**
122  * The main Table namespace
123  */
124 var Table = (function(){
125
126   /**
127    * Determine if a reference is defined
128    */
129   function def(o) {return (typeof o!="undefined");};
130
131   /**
132    * Determine if an object or class string contains a given class.
133    */
134   function hasClass(o,name) {
135     return new RegExp("(^|\\s)"+name+"(\\s|$)").test(o.className);
136   };
137
138   /**
139    * Add a class to an object
140    */
141   function addClass(o,name) {
142     var c = o.className || "";
143     if (def(c) && !hasClass(o,name)) {
144       o.className += (c?" ":"") + name;
145     }
146   };
147
148   /**
149    * Remove a class from an object
150    */
151   function removeClass(o,name) {
152     var c = o.className || "";
153     o.className = c.replace(new RegExp("(^|\\s)"+name+"(\\s|$)"),"$1");
154   };
155
156   /**
157    * For classes that match a given substring, return the rest
158    */
159   function classValue(o,prefix) {
160     var c = o.className;
161     if (c.match(new RegExp("(^|\\s)"+prefix+"([^ ]+)"))) {
162       return RegExp.$2;
163     }
164     return null;
165   };
166
167   /**
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.
172    */
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');
178       })(o);
179     }
180     else if (window.currentStyle) {
181       return(isHidden = function(o) {
182         return 'none'==o.currentStyle['display'];
183       })(o);
184     }
185     return (isHidden = function(o) {
186       return 'none'==o.style['display'];
187     })(o);
188   };
189
190   /**
191    * Get a parent element by tag name, or the original element if it is of the tag type
192    */
193   function getParent(o,a,b) {
194     if (o!=null && o.nodeName) {
195       if (o.nodeName==a || (b && o.nodeName==b)) {
196         return o;
197       }
198       while (o=o.parentNode) {
199         if (o.nodeName && (o.nodeName==a || (b && o.nodeName==b))) {
200           return o;
201         }
202       }
203     }
204     return null;
205   };
206
207   /**
208    * Utility function to copy properties from one object to another
209    */
210   function copy(o1,o2) {
211     for (var i=2;i<arguments.length; i++) {
212       var a = arguments[i];
213       if (def(o1[a])) {
214         o2[a] = o1[a];
215       }
216     }
217   }
218
219   // The table object itself
220   var table = {
221     //Class names used in the code
222     AutoStripeClassName:"table-autostripe",
223     StripeClassNamePrefix:"table-stripeclass:",
224
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",
233
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",
240
241     AutoPageSizePrefix:"table-autopage:",
242     AutoPageJumpPrefix:"table-page:",
243     PageNumberPrefix:"table-page-number:",
244     PageCountPrefix:"table-page-count:"
245   };
246
247   /**
248    * A place to store misc table information, rather than in the table objects themselves
249    */
250   table.tabledata = {};
251
252   /**
253    * Resolve a table given an element reference, and make sure it has a unique ID
254    */
255   table.uniqueId=1;
256   table.resolve = function(o,args) {
257     if (o!=null && o.nodeName && o.nodeName!="TABLE") {
258       o = getParent(o,"TABLE");
259     }
260     if (o==null) { return null; }
261     if (!o.id) {
262       var id = null;
263       do { var id = "TABLE_"+(table.uniqueId++); } 
264         while (document.getElementById(id)!=null);
265       o.id = id;
266     }
267     this.tabledata[o.id] = this.tabledata[o.id] || {};
268     if (args) {
269       copy(args,this.tabledata[o.id],"stripeclass","ignorehiddenrows","useinnertext","sorttype","col","desc","page","pagesize");
270     }
271     return o;
272   };
273
274
275   /**
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.
278    */
279   table.processTableCells = function(t, type, func, arg) {
280     t = this.resolve(t);
281     if (t==null) { return; }
282     if (type!="TFOOT") {
283       this.processCells(t.tHead, func, arg);
284     }
285     if (type!="THEAD") {
286       this.processCells(t.tFoot, func, arg);
287     }
288   };
289
290   /**
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.
294    */
295   table.processCells = function(section,func,arg) {
296     if (section!=null) {
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++) { 
300           var row = rows[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);
306             }
307           }
308         }
309       }
310     }
311   };
312
313   /**
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.
318    */
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) {
326           return td.cellIndex;
327         })(td);
328       }
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) {
332           return i;
333         }
334       }
335     }
336     return 0;
337   };
338
339   /**
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.
342    */
343   table.nodeValue = {
344     'INPUT':function(node) { 
345       if (def(node.value) && node.type && ((node.type!="checkbox" && node.type!="radio") || node.checked)) {
346         return node.value;
347       }
348       return "";
349     },
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;
354       }
355       return "";
356     },
357     'IMG':function(node) {
358       return node.name || "";
359     }
360   };
361
362   /**
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
365    */
366   table.getCellValue = function(td,useInnerText) {
367     if (useInnerText && def(td.innerText)) {
368       return td.innerText;
369     }
370     if (!td.childNodes) { 
371       return ""; 
372     }
373     var childNodes=td.childNodes;
374     var ret = "";
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
380       if (type==1) {
381         var nname = node.nodeName;
382         if (this.nodeValue[nname]) {
383           ret += this.nodeValue[nname](node);
384         }
385         else {
386           ret += this.getCellValue(node);
387         }
388       }
389       else if (type==3) {
390         if (def(node.innerText)) {
391           ret += node.innerText;
392         }
393         else if (def(node.nodeValue)) {
394           ret += node.nodeValue;
395         }
396       }
397     }
398     return ret;
399   };
400
401   /**
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
407    */
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);
413
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];      
417     } 
418
419     var matrix = [];
420     this.tableHeaderIndexes[tableObj.id] = {};
421     var thead = getParent(tableCellObj,"THEAD");
422     var trs = thead.getElementsByTagName('TR');
423
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++) {
432         var c = cells[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;
437         var firstAvailCol;
438         if(!def(matrix[rowIndex])) { 
439           matrix[rowIndex] = []; 
440         }
441         var m = matrix[rowIndex];
442         // Find first available column in the first row
443         for (var k=0; k<m.length+1; k++) {
444           if (!def(m[k])) {
445             firstAvailCol = k;
446             break;
447           }
448         }
449         this.tableHeaderIndexes[tableObj.id][cellId] = firstAvailCol;
450         for (var k=rowIndex; k<rowIndex+rowSpan; k++) {
451           if(!def(matrix[k])) { 
452             matrix[k] = []; 
453           }
454           var matrixrow = matrix[k];
455           for (var l=firstAvailCol; l<firstAvailCol+colSpan; l++) {
456             matrixrow[l] = "x";
457           }
458         }
459       }
460     }
461     // Store the map so future lookups are fast.
462     return this.tableHeaderIndexes[tableObj.id][cellCoordinates];
463   };
464
465   /**
466    * Sort all rows in each TBODY (tbodies are sorted independent of each other)
467    */
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};
473     }
474     args = args || {};
475
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; 
479     }
480     // If no sort type is specified, default to the default sort
481     args.sorttype = args.sorttype || Sort['default'];
482
483     // Resolve the table
484     t = this.resolve(o,args);
485     tdata = this.tabledata[t.id];
486
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;
490     }
491     else {
492       tdata.desc = !!args.desc;
493     }
494
495     // Store the last sorted column so clicking again will reverse the sort order
496     tdata.lastcol=tdata.col;
497     tdata.lastdesc=!!tdata.desc;
498
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;
504     }
505
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",
509       function(cell) {
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);
516           }
517         }
518       }
519     );
520
521     // Sort each tbody independently
522     var bodies = t.tBodies;
523     if (bodies==null || bodies.length==0) { return; }
524
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]);};
529
530     var useinnertext=!!tdata.useinnertext;
531     var col = tdata.col;
532
533     for (var i=0,L=bodies.length; i<L; i++) {
534       var tb = bodies[i], tbrows = tb.rows, rows = [];
535
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
543           do {
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]];
548             }
549           } while (cRow=tbrows[++cRowIndex])
550         }
551
552         // Do the actual sorting
553         rows.sort(newSortFunc);
554
555         // Move the rows to the correctly sorted order. Appending an existing DOM object just moves it!
556         cRowIndex=0;
557         var displayedCount=0;
558         var f=[removeClass,addClass];
559         if (cRow=rows[cRowIndex]){
560           do { 
561             tb.appendChild(cRow[1]); 
562           } while (cRow=rows[++cRowIndex])
563         }
564       }
565     }
566
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
570     }
571     else {
572       // Re-stripe if a class name was supplied
573       if (tdata.stripeclass) {
574         this.stripe(t,tdata.stripeclass,!!tdata.ignorehiddenrows);
575       }
576     }
577   };
578
579   /**
580   * Apply a filter to rows in a table and hide those that do not match.
581   */
582   table.filter = function(o,filters,args) {
583     var cell;
584     args = args || {};
585
586     var t = this.resolve(o,args);
587     var tdata = this.tabledata[t.id];
588
589     // If new filters were passed in, apply them to the table's list of filters
590     if (!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;
593     }
594     else {
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 };
598       }
599       // Also allow for a regular input
600       if (filters.nodeName=="INPUT" && filters.type=="text") {
601         filters={ 'filter':"/^"+filters.value+"/" };
602       }
603       // Force filters to be an array
604       if (typeof(filters)=="object" && !filters.length) {
605         filters = [filters];
606       }
607
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;
616           }
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);
620           }
621         }
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);
628         }
629
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];
633         }
634         else {
635           tdata.filters = tdata.filters || {};
636           tdata.filters[filter.col] = filter.filter;
637         }
638       }
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; }
641       if (!keep) {
642         tdata.filters = null;
643       }
644     }    
645     // Everything's been setup, so now scrape the table rows
646     return table.scrape(o);
647   };
648
649   /**
650    * "Page" a table by showing only a subset of the rows
651    */
652   table.page = function(t,page,args) {
653     args = args || {};
654     if (def(page)) { args.page = page; }
655     return table.scrape(t,args);
656   };
657
658   /**
659    * Jump forward or back any number of pages
660    */
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);
664   };
665
666   /**
667    * Go to the next page of a paged table
668    */  
669   table.pageNext = function(t,args) {
670     return this.pageJump(t,1,args);
671   };
672
673   /**
674    * Go to the previous page of a paged table
675    */  
676   table.pagePrevious = function(t,args) {
677     return this.pageJump(t,-1,args);
678   };
679
680   /**
681   * Scrape a table to either hide or show each row based on filters and paging
682   */
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;
688     args = args || {};
689
690     // Resolve the table object
691     t = this.resolve(o,args);
692     tdata = this.tabledata[t.id];
693
694     // Setup for Paging
695     var page = tdata.page;
696     if (def(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;
702     }
703
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++) {
708       var tb = bodies[i];
709       for (var j=0,L2=tb.rows.length; j<L2; j++) {
710         row = tb.rows[j];
711         hideRow = false;
712
713         // Test if filters will hide the row
714         if (tdata.filters && row.cells) {
715           var cells = row.cells;
716           var cellsLength = cells.length;
717           // Test each filter
718           for (col in tdata.filters) {
719             if (!hideRow) {
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);
725                 }
726                 else if (typeof(filter)=="function") {
727                   hideRow=!filter(val,cells[col]);
728                 }
729                 else {
730                   hideRow = (val!=filter);
731                 }
732               }
733             }
734           }
735         }
736
737         // Keep track of the total rows scanned and the total runs _not_ filtered out
738         totalrows++;
739         if (!hideRow) {
740           unfilteredrowcount++;
741           if (def(page)) {
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) {
746               hideRow = true;
747             }
748           }
749         }
750
751         row.style.display = hideRow?"none":"";
752       }
753     }
754
755     if (def(page)) {
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="";
763         }
764       }
765     }
766
767     // Loop through all THEADs and add/remove filtered class names
768     this.processTableCells(t,"THEAD",
769       function(c) {
770         ((tdata.filters && def(tdata.filters[table.getCellIndex(c)]) && hasClass(c,table.FilterableClassName))?addClass:removeClass)(c,table.FilteredClassName);
771       }
772     );
773
774     // Stripe the table if necessary
775     if (tdata.stripeclass) {
776       this.stripe(t);
777     }
778
779     // Calculate some values to be returned for info and updating purposes
780     var pagecount = Math.floor(unfilteredrowcount/pagesize)+1;
781     if (def(page)) {
782       // Update the page number/total containers if they exist
783       if (tdata.container_number) {
784         tdata.container_number.innerHTML = page+1;
785       }
786       if (tdata.container_count) {
787         tdata.container_count.innerHTML = pagecount;
788       }
789     }
790
791     // Update the row count containers if they exist
792     if (tdata.container_filtered_count) {
793       tdata.container_filtered_count.innerHTML = unfilteredrowcount;
794     }
795     if (tdata.container_all_count) {
796       tdata.container_all_count.innerHTML = totalrows;
797     }
798     return { 'data':tdata, 'unfilteredcount':unfilteredrowcount, 'total':totalrows, 'pagecount':pagecount, 'page':page, 'pagesize':pagesize };
799   };
800
801   /**
802    * Shade alternate rows, aka Stripe the table.
803    */
804   table.stripe = function(t,className,args) { 
805     args = args || {};
806     args.stripeclass = className;
807
808     t = this.resolve(t,args);
809     var tdata = this.tabledata[t.id];
810
811     var bodies = t.tBodies;
812     if (bodies==null || bodies.length==0) { 
813       return; 
814     }
815
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) {
826           do {
827             f[displayedCount++%2](cRow,className);
828           } while (cRow=tbrows[++cRowIndex])
829         }
830         else {
831           do {
832             if (!isHidden(cRow)) {
833               f[displayedCount++%2](cRow,className);
834             }
835           } while (cRow=tbrows[++cRowIndex])
836         }
837       }
838     }
839   };
840
841   /**
842    * Build up a list of unique values in a table column
843    */
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;
850       }
851     }
852     var valArray = [];
853     for (var val in values) {
854       valArray.push(val);
855     }
856     return valArray.sort();
857   };
858
859   /**
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.
862    */
863   table.auto = function(args) {
864     var cells = [], tables = document.getElementsByTagName("TABLE");
865     var val,tdata;
866     if (tables!=null) {
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;
872         }
873         // Do auto-filter if necessary
874         if (hasClass(t,table.AutoFilterClassName)) {
875           table.autofilter(t);
876         }
877         // Do auto-page if necessary
878         if (val = classValue(t,table.AutoPageSizePrefix)) {
879           table.autopage(t,{'pagesize':+val});
880         }
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});
884         }
885         // Do auto-stripe if necessary
886         if (tdata.stripeclass && hasClass(t,table.AutoStripeClassName)) {
887           table.stripe(t);
888         }
889       }
890     }
891   };
892
893   /**
894    * Add sorting functionality to a table header cell
895    */
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);
901       if (type!=null) {
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+"'];
910           }
911         }
912       }
913     } );
914     if (args.col!=null) {
915       table.sort(t,args);
916     }
917   };
918
919   /**
920    * Add paging functionality to a table 
921    */
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; }
930         if (type!=null) {
931           c.onclick = Function("","Table.pageJump(this,"+type+")");
932         }
933       } );
934       if (val = classValue(t,table.PageNumberPrefix)) {
935         tdata.container_number = document.getElementById(val);
936       }
937       if (val = classValue(t,table.PageCountPrefix)) {
938         tdata.container_count = document.getElementById(val);
939       }
940       return table.page(t,0,args);
941     }
942   };
943
944   /**
945    * A util function to cancel bubbling of clicks on filter dropdowns
946    */
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; }
951   };
952
953   /**
954    * Auto-filter a table
955    */
956   table.autofilter = function(t,args) {
957     args = 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);
967           }
968           else {
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>';
972             }
973             sel += '</select>';
974             cell.innerHTML += "<br>"+sel;
975           }
976         }
977       }
978     });
979     if (val = classValue(t,table.FilteredRowcountPrefix)) {
980       tdata.container_filtered_count = document.getElementById(val);
981     }
982     if (val = classValue(t,table.RowcountPrefix)) {
983       tdata.container_all_count = document.getElementById(val);
984     }
985   };
986
987   /**
988    * Attach the auto event so it happens on load.
989    * use jQuery's ready() function if available
990    */
991   if (typeof(jQuery)!="undefined") {
992     jQuery(table.auto);
993   }
994   else if (window.addEventListener) {
995     window.addEventListener( "load", table.auto, false );
996   }
997   else if (window.attachEvent) {
998     window.attachEvent( "onload", table.auto );
999   }
1000
1001   return table;
1002 })();