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 | })(); |
---|