source: extensions/LCAS/tags/2.3.0/admin/template/js/jquery.tablesorter.js @ 27186

Last change on this file since 27186 was 27186, checked in by LucMorizur, 10 years ago

Version 2.3.0, compatibility with Piwigo 2.6

  • Property svn:eol-style set to LF
File size: 55.3 KB
Line 
1/**!
2* TableSorter 2.14.5 - Client-side table sorting with ease!
3* @requires jQuery v1.2.6+
4*
5* Copyright (c) 2007 Christian Bach
6* Examples and docs at: http://tablesorter.com
7* Dual licensed under the MIT and GPL licenses:
8* http://www.opensource.org/licenses/mit-license.php
9* http://www.gnu.org/licenses/gpl.html
10*
11* @type jQuery
12* @name tablesorter
13* @cat Plugins/Tablesorter
14* @author Christian Bach/christian.bach@polyester.se
15* @contributor Rob Garrison/https://github.com/Mottie/tablesorter
16*/
17/*jshint browser:true, jquery:true, unused:false, expr: true */
18/*global console:false, alert:false */
19!(function($) {
20        "use strict";
21        $.extend({
22                /*jshint supernew:true */
23                tablesorter: new function() {
24
25                        var ts = this;
26
27                        ts.version = "2.14.5";
28
29                        ts.parsers = [];
30                        ts.widgets = [];
31                        ts.defaults = {
32
33                                // *** appearance
34                                theme            : 'default',  // adds tablesorter-{theme} to the table for styling
35                                widthFixed       : false,      // adds colgroup to fix widths of columns
36                                showProcessing   : false,      // show an indeterminate timer icon in the header when the table is sorted or filtered.
37
38                                headerTemplate   : '{content}',// header layout template (HTML ok); {content} = innerHTML, {icon} = <i/> (class from cssIcon)
39                                onRenderTemplate : null,       // function(index, template){ return template; }, (template is a string)
40                                onRenderHeader   : null,       // function(index){}, (nothing to return)
41
42                                // *** functionality
43                                cancelSelection  : true,       // prevent text selection in the header
44                                tabIndex         : true,       // add tabindex to header for keyboard accessibility
45                                dateFormat       : 'mmddyyyy', // other options: "ddmmyyy" or "yyyymmdd"
46                                sortMultiSortKey : 'shiftKey', // key used to select additional columns
47                                sortResetKey     : 'ctrlKey',  // key used to remove sorting on a column
48                                usNumberFormat   : true,       // false for German "1.234.567,89" or French "1 234 567,89"
49                                delayInit        : false,      // if false, the parsed table contents will not update until the first sort
50                                serverSideSorting: false,      // if true, server-side sorting should be performed because client-side sorting will be disabled, but the ui and events will still be used.
51
52                                // *** sort options
53                                headers          : {},         // set sorter, string, empty, locked order, sortInitialOrder, filter, etc.
54                                ignoreCase       : true,       // ignore case while sorting
55                                sortForce        : null,       // column(s) first sorted; always applied
56                                sortList         : [],         // Initial sort order; applied initially; updated when manually sorted
57                                sortAppend       : null,       // column(s) sorted last; always applied
58                                sortStable       : false,      // when sorting two rows with exactly the same content, the original sort order is maintained
59
60                                sortInitialOrder : 'asc',      // sort direction on first click
61                                sortLocaleCompare: false,      // replace equivalent character (accented characters)
62                                sortReset        : false,      // third click on the header will reset column to default - unsorted
63                                sortRestart      : false,      // restart sort to "sortInitialOrder" when clicking on previously unsorted columns
64
65                                emptyTo          : 'bottom',   // sort empty cell to bottom, top, none, zero
66                                stringTo         : 'max',      // sort strings in numerical column as max, min, top, bottom, zero
67                                textExtraction   : 'simple',   // text extraction method/function - function(node, table, cellIndex){}
68                                textSorter       : null,       // choose overall or specific column sorter function(a, b, direction, table, columnIndex) [alt: ts.sortText]
69                                numberSorter     : null,       // choose overall numeric sorter function(a, b, direction, maxColumnValue)
70
71                                // *** widget options
72                                widgets: [],                   // method to add widgets, e.g. widgets: ['zebra']
73                                widgetOptions    : {
74                                        zebra : [ 'even', 'odd' ]    // zebra widget alternating row class names
75                                },
76                                initWidgets      : true,       // apply widgets on tablesorter initialization
77
78                                // *** callbacks
79                                initialized      : null,       // function(table){},
80
81                                // *** extra css class names
82                                tableClass       : '',
83                                cssAsc           : '',
84                                cssDesc          : '',
85                                cssHeader        : '',
86                                cssHeaderRow     : '',
87                                cssProcessing    : '', // processing icon applied to header during sort/filter
88
89                                cssChildRow      : 'tablesorter-childRow', // class name indiciating that a row is to be attached to the its parent
90                                cssIcon          : 'tablesorter-icon',     //  if this class exists, a <i> will be added to the header automatically
91                                cssInfoBlock     : 'tablesorter-infoOnly', // don't sort tbody with this class name (only one class name allowed here!)
92
93                                // *** selectors
94                                selectorHeaders  : '> thead th, > thead td',
95                                selectorSort     : 'th, td',   // jQuery selector of content within selectorHeaders that is clickable to trigger a sort
96                                selectorRemove   : '.remove-me',
97
98                                // *** advanced
99                                debug            : false,
100
101                                // *** Internal variables
102                                headerList: [],
103                                empties: {},
104                                strings: {},
105                                parsers: []
106
107                                // deprecated; but retained for backwards compatibility
108                                // widgetZebra: { css: ["even", "odd"] }
109
110                        };
111
112                        // internal css classes - these will ALWAYS be added to
113                        // the table and MUST only contain one class name - fixes #381
114                        ts.css = {
115                                table      : 'tablesorter',
116                                childRow   : 'tablesorter-childRow',
117                                header     : 'tablesorter-header',
118                                headerRow  : 'tablesorter-headerRow',
119                                icon       : 'tablesorter-icon',
120                                info       : 'tablesorter-infoOnly',
121                                processing : 'tablesorter-processing',
122                                sortAsc    : 'tablesorter-headerAsc',
123                                sortDesc   : 'tablesorter-headerDesc'
124                        };
125
126                        /* debuging utils */
127                        function log() {
128                                var s = arguments.length > 1 ? Array.prototype.slice.call(arguments) : arguments[0];
129                                if (typeof console !== "undefined" && typeof console.log !== "undefined") {
130                                        console.log(s);
131                                } else {
132                                        alert(s);
133                                }
134                        }
135
136                        function benchmark(s, d) {
137                                log(s + " (" + (new Date().getTime() - d.getTime()) + "ms)");
138                        }
139
140                        ts.log = log;
141                        ts.benchmark = benchmark;
142
143                        // $.isEmptyObject from jQuery v1.4
144                        function isEmptyObject(obj) {
145                                /*jshint forin: false */
146                                for (var name in obj) {
147                                        return false;
148                                }
149                                return true;
150                        }
151
152                        function getElementText(table, node, cellIndex) {
153                                if (!node) { return ""; }
154                                var c = table.config,
155                                        t = c.textExtraction, text = "";
156                                if (t === "simple") {
157                                        if (c.supportsTextContent) {
158                                                text = node.textContent; // newer browsers support this
159                                        } else {
160                                                text = $(node).text();
161                                        }
162                                } else {
163                                        if (typeof t === "function") {
164                                                text = t(node, table, cellIndex);
165                                        } else if (typeof t === "object" && t.hasOwnProperty(cellIndex)) {
166                                                text = t[cellIndex](node, table, cellIndex);
167                                        } else {
168                                                text = c.supportsTextContent ? node.textContent : $(node).text();
169                                        }
170                                }
171                                return $.trim(text);
172                        }
173
174                        function detectParserForColumn(table, rows, rowIndex, cellIndex) {
175                                var cur,
176                                i = ts.parsers.length,
177                                node = false,
178                                nodeValue = '',
179                                keepLooking = true;
180                                while (nodeValue === '' && keepLooking) {
181                                        rowIndex++;
182                                        if (rows[rowIndex]) {
183                                                node = rows[rowIndex].cells[cellIndex];
184                                                nodeValue = getElementText(table, node, cellIndex);
185                                                if (table.config.debug) {
186                                                        log('Checking if value was empty on row ' + rowIndex + ', column: ' + cellIndex + ': "' + nodeValue + '"');
187                                                }
188                                        } else {
189                                                keepLooking = false;
190                                        }
191                                }
192                                while (--i >= 0) {
193                                        cur = ts.parsers[i];
194                                        // ignore the default text parser because it will always be true
195                                        if (cur && cur.id !== 'text' && cur.is && cur.is(nodeValue, table, node)) {
196                                                return cur;
197                                        }
198                                }
199                                // nothing found, return the generic parser (text)
200                                return ts.getParserById('text');
201                        }
202
203                        function buildParserCache(table) {
204                                var c = table.config,
205                                        // update table bodies in case we start with an empty table
206                                        tb = c.$tbodies = c.$table.children('tbody:not(.' + c.cssInfoBlock + ')'),
207                                        rows, list, l, i, h, ch, p, time, parsersDebug = "";
208                                if ( tb.length === 0) {
209                                        return c.debug ? log('*Empty table!* Not building a parser cache') : '';
210                                } else if (c.debug) {
211                                        time = new Date();
212                                        log('Detecting parsers for each column');
213                                }
214                                rows = tb[0].rows;
215                                if (rows[0]) {
216                                        list = [];
217                                        l = rows[0].cells.length;
218                                        for (i = 0; i < l; i++) {
219                                                // tons of thanks to AnthonyM1229 for working out the following selector (issue #74) to make this work in IE8!
220                                                // More fixes to this selector to work properly in iOS and jQuery 1.8+ (issue #132 & #174)
221                                                h = c.$headers.filter(':not([colspan])');
222                                                h = h.add( c.$headers.filter('[colspan="1"]') ) // ie8 fix
223                                                        .filter('[data-column="' + i + '"]:last');
224                                                ch = c.headers[i];
225                                                // get column parser
226                                                p = ts.getParserById( ts.getData(h, ch, 'sorter') );
227                                                // empty cells behaviour - keeping emptyToBottom for backwards compatibility
228                                                c.empties[i] = ts.getData(h, ch, 'empty') || c.emptyTo || (c.emptyToBottom ? 'bottom' : 'top' );
229                                                // text strings behaviour in numerical sorts
230                                                c.strings[i] = ts.getData(h, ch, 'string') || c.stringTo || 'max';
231                                                if (!p) {
232                                                        p = detectParserForColumn(table, rows, -1, i);
233                                                }
234                                                if (c.debug) {
235                                                        parsersDebug += "column:" + i + "; parser:" + p.id + "; string:" + c.strings[i] + '; empty: ' + c.empties[i] + "\n";
236                                                }
237                                                list.push(p);
238                                        }
239                                }
240                                if (c.debug) {
241                                        log(parsersDebug);
242                                        benchmark("Completed detecting parsers", time);
243                                }
244                                c.parsers = list;
245                        }
246
247                        /* utils */
248                        function buildCache(table) {
249                                var b = table.tBodies,
250                                tc = table.config,
251                                totalRows,
252                                totalCells,
253                                parsers = tc.parsers,
254                                t, v, i, j, k, c, cols, cacheTime, colMax = [];
255                                tc.cache = {};
256                                // if no parsers found, return - it's an empty table.
257                                if (!parsers) {
258                                        return tc.debug ? log('*Empty table!* Not building a cache') : '';
259                                }
260                                if (tc.debug) {
261                                        cacheTime = new Date();
262                                }
263                                // processing icon
264                                if (tc.showProcessing) {
265                                        ts.isProcessing(table, true);
266                                }
267                                for (k = 0; k < b.length; k++) {
268                                        tc.cache[k] = { row: [], normalized: [] };
269                                        // ignore tbodies with class name from c.cssInfoBlock
270                                        if (!$(b[k]).hasClass(tc.cssInfoBlock)) {
271                                                totalRows = (b[k] && b[k].rows.length) || 0;
272                                                totalCells = (b[k].rows[0] && b[k].rows[0].cells.length) || 0;
273                                                for (i = 0; i < totalRows; ++i) {
274                                                        /** Add the table data to main data array */
275                                                        c = $(b[k].rows[i]);
276                                                        cols = [];
277                                                        // if this is a child row, add it to the last row's children and continue to the next row
278                                                        if (c.hasClass(tc.cssChildRow)) {
279                                                                tc.cache[k].row[tc.cache[k].row.length - 1] = tc.cache[k].row[tc.cache[k].row.length - 1].add(c);
280                                                                // go to the next for loop
281                                                                continue;
282                                                        }
283                                                        tc.cache[k].row.push(c);
284                                                        for (j = 0; j < totalCells; ++j) {
285                                                                t = getElementText(table, c[0].cells[j], j);
286                                                                // allow parsing if the string is empty, previously parsing would change it to zero,
287                                                                // in case the parser needs to extract data from the table cell attributes
288                                                                v = parsers[j].format(t, table, c[0].cells[j], j);
289                                                                cols.push(v);
290                                                                if ((parsers[j].type || '').toLowerCase() === "numeric") {
291                                                                        colMax[j] = Math.max(Math.abs(v) || 0, colMax[j] || 0); // determine column max value (ignore sign)
292                                                                }
293                                                        }
294                                                        cols.push(tc.cache[k].normalized.length); // add position for rowCache
295                                                        tc.cache[k].normalized.push(cols);
296                                                }
297                                                tc.cache[k].colMax = colMax;
298                                        }
299                                }
300                                if (tc.showProcessing) {
301                                        ts.isProcessing(table); // remove processing icon
302                                }
303                                if (tc.debug) {
304                                        benchmark("Building cache for " + totalRows + " rows", cacheTime);
305                                }
306                        }
307
308                        // init flag (true) used by pager plugin to prevent widget application
309                        function appendToTable(table, init) {
310                                var c = table.config,
311                                        wo = c.widgetOptions,
312                                        b = table.tBodies,
313                                        rows = [],
314                                        c2 = c.cache,
315                                        r, n, totalRows, checkCell, $bk, $tb,
316                                        i, j, k, l, pos, appendTime;
317                                if (isEmptyObject(c2)) { return; } // empty table - fixes #206/#346
318                                if (c.debug) {
319                                        appendTime = new Date();
320                                }
321                                for (k = 0; k < b.length; k++) {
322                                        $bk = $(b[k]);
323                                        if ($bk.length && !$bk.hasClass(c.cssInfoBlock)) {
324                                                // get tbody
325                                                $tb = ts.processTbody(table, $bk, true);
326                                                r = c2[k].row;
327                                                n = c2[k].normalized;
328                                                totalRows = n.length;
329                                                checkCell = totalRows ? (n[0].length - 1) : 0;
330                                                for (i = 0; i < totalRows; i++) {
331                                                        pos = n[i][checkCell];
332                                                        rows.push(r[pos]);
333                                                        // removeRows used by the pager plugin; don't render if using ajax - fixes #411
334                                                        if (!c.appender || (c.pager && (!c.pager.removeRows || !wo.pager_removeRows) && !c.pager.ajax)) {
335                                                                l = r[pos].length;
336                                                                for (j = 0; j < l; j++) {
337                                                                        $tb.append(r[pos][j]);
338                                                                }
339                                                        }
340                                                }
341                                                // restore tbody
342                                                ts.processTbody(table, $tb, false);
343                                        }
344                                }
345                                if (c.appender) {
346                                        c.appender(table, rows);
347                                }
348                                if (c.debug) {
349                                        benchmark("Rebuilt table", appendTime);
350                                }
351                                // apply table widgets; but not before ajax completes
352                                if (!init && !c.appender) { ts.applyWidget(table); }
353                                // trigger sortend
354                                $(table).trigger("sortEnd", table);
355                                $(table).trigger("updateComplete", table);
356                        }
357
358                        // computeTableHeaderCellIndexes from:
359                        // http://www.javascripttoolbox.com/lib/table/examples.php
360                        // http://www.javascripttoolbox.com/temp/table_cellindex.html
361                        function computeThIndexes(t) {
362                                var matrix = [],
363                                lookup = {},
364                                cols = 0, // determine the number of columns
365                                trs = $(t).find('thead:eq(0), tfoot').children('tr'), // children tr in tfoot - see issue #196
366                                i, j, k, l, c, cells, rowIndex, cellId, rowSpan, colSpan, firstAvailCol, matrixrow;
367                                for (i = 0; i < trs.length; i++) {
368                                        cells = trs[i].cells;
369                                        for (j = 0; j < cells.length; j++) {
370                                                c = cells[j];
371                                                rowIndex = c.parentNode.rowIndex;
372                                                cellId = rowIndex + "-" + c.cellIndex;
373                                                rowSpan = c.rowSpan || 1;
374                                                colSpan = c.colSpan || 1;
375                                                if (typeof(matrix[rowIndex]) === "undefined") {
376                                                        matrix[rowIndex] = [];
377                                                }
378                                                // Find first available column in the first row
379                                                for (k = 0; k < matrix[rowIndex].length + 1; k++) {
380                                                        if (typeof(matrix[rowIndex][k]) === "undefined") {
381                                                                firstAvailCol = k;
382                                                                break;
383                                                        }
384                                                }
385                                                lookup[cellId] = firstAvailCol;
386                                                cols = Math.max(firstAvailCol, cols);
387                                                // add data-column
388                                                $(c).attr({ 'data-column' : firstAvailCol }); // 'data-row' : rowIndex
389                                                for (k = rowIndex; k < rowIndex + rowSpan; k++) {
390                                                        if (typeof(matrix[k]) === "undefined") {
391                                                                matrix[k] = [];
392                                                        }
393                                                        matrixrow = matrix[k];
394                                                        for (l = firstAvailCol; l < firstAvailCol + colSpan; l++) {
395                                                                matrixrow[l] = "x";
396                                                        }
397                                                }
398                                        }
399                                }
400                                // may not be accurate if # header columns !== # tbody columns
401                                t.config.columns = cols + 1; // add one because it's a zero-based index
402                                return lookup;
403                        }
404
405                        function formatSortingOrder(v) {
406                                // look for "d" in "desc" order; return true
407                                return (/^d/i.test(v) || v === 1);
408                        }
409
410                        function buildHeaders(table) {
411                                var header_index = computeThIndexes(table), ch, $t,
412                                        h, i, t, lock, time, c = table.config;
413                                c.headerList = [];
414                                c.headerContent = [];
415                                if (c.debug) {
416                                        time = new Date();
417                                }
418                                // add icon if cssIcon option exists
419                                i = c.cssIcon ? '<i class="' + ( c.cssIcon === ts.css.icon ? ts.css.icon : c.cssIcon + ' ' + ts.css.icon ) + '"></i>' : '';
420                                c.$headers = $(table).find(c.selectorHeaders).each(function(index) {
421                                        $t = $(this);
422                                        ch = c.headers[index];
423                                        c.headerContent[index] = $(this).html(); // save original header content
424                                        // set up header template
425                                        t = c.headerTemplate.replace(/\{content\}/g, $(this).html()).replace(/\{icon\}/g, i);
426                                        if (c.onRenderTemplate) {
427                                                h = c.onRenderTemplate.apply($t, [index, t]);
428                                                if (h && typeof h === 'string') { t = h; } // only change t if something is returned
429                                        }
430                                        $(this).html('<div class="tablesorter-header-inner">' + t + '</div>'); // faster than wrapInner
431
432                                        if (c.onRenderHeader) { c.onRenderHeader.apply($t, [index]); }
433
434                                        this.column = header_index[this.parentNode.rowIndex + "-" + this.cellIndex];
435                                        this.order = formatSortingOrder( ts.getData($t, ch, 'sortInitialOrder') || c.sortInitialOrder ) ? [1,0,2] : [0,1,2];
436                                        this.count = -1; // set to -1 because clicking on the header automatically adds one
437                                        this.lockedOrder = false;
438                                        lock = ts.getData($t, ch, 'lockedOrder') || false;
439                                        if (typeof lock !== 'undefined' && lock !== false) {
440                                                this.order = this.lockedOrder = formatSortingOrder(lock) ? [1,1,1] : [0,0,0];
441                                        }
442                                        $t.addClass(ts.css.header + ' ' + c.cssHeader);
443                                        // add cell to headerList
444                                        c.headerList[index] = this;
445                                        // add to parent in case there are multiple rows
446                                        $t.parent().addClass(ts.css.headerRow + ' ' + c.cssHeaderRow);
447                                        // allow keyboard cursor to focus on element
448                                        if (c.tabIndex) { $t.attr("tabindex", 0); }
449                                });
450                                // enable/disable sorting
451                                updateHeader(table);
452                                if (c.debug) {
453                                        benchmark("Built headers:", time);
454                                        log(c.$headers);
455                                }
456                        }
457
458                        function commonUpdate(table, resort, callback) {
459                                var c = table.config;
460                                // remove rows/elements before update
461                                c.$table.find(c.selectorRemove).remove();
462                                // rebuild parsers
463                                buildParserCache(table);
464                                // rebuild the cache map
465                                buildCache(table);
466                                checkResort(c.$table, resort, callback);
467                        }
468
469                        function updateHeader(table) {
470                                var s, c = table.config;
471                                c.$headers.each(function(index, th){
472                                        s = ts.getData( th, c.headers[index], 'sorter' ) === 'false';
473                                        th.sortDisabled = s;
474                                        $(th)[ s ? 'addClass' : 'removeClass' ]('sorter-false');
475                                });
476                        }
477
478                        function setHeadersCss(table) {
479                                var f, i, j, l,
480                                        c = table.config,
481                                        list = c.sortList,
482                                        css = [ts.css.sortAsc + ' ' + c.cssAsc, ts.css.sortDesc + ' ' + c.cssDesc],
483                                        // find the footer
484                                        $t = $(table).find('tfoot tr').children().removeClass(css.join(' '));
485                                // remove all header information
486                                c.$headers.removeClass(css.join(' '));
487                                l = list.length;
488                                for (i = 0; i < l; i++) {
489                                        // direction = 2 means reset!
490                                        if (list[i][1] !== 2) {
491                                                // multicolumn sorting updating - choose the :last in case there are nested columns
492                                                f = c.$headers.not('.sorter-false').filter('[data-column="' + list[i][0] + '"]' + (l === 1 ? ':last' : '') );
493                                                if (f.length) {
494                                                        for (j = 0; j < f.length; j++) {
495                                                                if (!f[j].sortDisabled) {
496                                                                        f.eq(j).addClass(css[list[i][1]]);
497                                                                        // add sorted class to footer, if it exists
498                                                                        if ($t.length) {
499                                                                                $t.filter('[data-column="' + list[i][0] + '"]').eq(j).addClass(css[list[i][1]]);
500                                                                        }
501                                                                }
502                                                        }
503                                                }
504                                        }
505                                }
506                        }
507
508                        // automatically add col group, and column sizes if set
509                        function fixColumnWidth(table) {
510                                if (table.config.widthFixed && $(table).find('colgroup').length === 0) {
511                                        var colgroup = $('<colgroup>'),
512                                                overallWidth = $(table).width();
513                                        // only add col for visible columns - fixes #371
514                                        $(table.tBodies[0]).find("tr:first").children("td:visible").each(function() {
515                                                colgroup.append($('<col>').css('width', parseInt(($(this).width()/overallWidth)*1000, 10)/10 + '%'));
516                                        });
517                                        $(table).prepend(colgroup);
518                                }
519                        }
520
521                        function updateHeaderSortCount(table, list) {
522                                var s, t, o, c = table.config,
523                                        sl = list || c.sortList;
524                                c.sortList = [];
525                                $.each(sl, function(i,v){
526                                        // ensure all sortList values are numeric - fixes #127
527                                        s = [ parseInt(v[0], 10), parseInt(v[1], 10) ];
528                                        // make sure header exists
529                                        o = c.$headers[s[0]];
530                                        if (o) { // prevents error if sorton array is wrong
531                                                c.sortList.push(s);
532                                                t = $.inArray(s[1], o.order); // fixes issue #167
533                                                o.count = t >= 0 ? t : s[1] % (c.sortReset ? 3 : 2);
534                                        }
535                                });
536                        }
537
538                        function getCachedSortType(parsers, i) {
539                                return (parsers && parsers[i]) ? parsers[i].type || '' : '';
540                        }
541
542                        function initSort(table, cell, e){
543                                var a, i, j, o, s,
544                                        c = table.config,
545                                        k = !e[c.sortMultiSortKey],
546                                        $this = $(table);
547                                // Only call sortStart if sorting is enabled
548                                $this.trigger("sortStart", table);
549                                // get current column sort order
550                                cell.count = e[c.sortResetKey] ? 2 : (cell.count + 1) % (c.sortReset ? 3 : 2);
551                                // reset all sorts on non-current column - issue #30
552                                if (c.sortRestart) {
553                                        i = cell;
554                                        c.$headers.each(function() {
555                                                // only reset counts on columns that weren't just clicked on and if not included in a multisort
556                                                if (this !== i && (k || !$(this).is('.' + ts.css.sortDesc + ',.' + ts.css.sortAsc))) {
557                                                        this.count = -1;
558                                                }
559                                        });
560                                }
561                                // get current column index
562                                i = cell.column;
563                                // user only wants to sort on one column
564                                if (k) {
565                                        // flush the sort list
566                                        c.sortList = [];
567                                        if (c.sortForce !== null) {
568                                                a = c.sortForce;
569                                                for (j = 0; j < a.length; j++) {
570                                                        if (a[j][0] !== i) {
571                                                                c.sortList.push(a[j]);
572                                                        }
573                                                }
574                                        }
575                                        // add column to sort list
576                                        o = cell.order[cell.count];
577                                        if (o < 2) {
578                                                c.sortList.push([i, o]);
579                                                // add other columns if header spans across multiple
580                                                if (cell.colSpan > 1) {
581                                                        for (j = 1; j < cell.colSpan; j++) {
582                                                                c.sortList.push([i + j, o]);
583                                                        }
584                                                }
585                                        }
586                                        // multi column sorting
587                                } else {
588                                        // get rid of the sortAppend before adding more - fixes issue #115
589                                        if (c.sortAppend && c.sortList.length > 1) {
590                                                if (ts.isValueInArray(c.sortAppend[0][0], c.sortList)) {
591                                                        c.sortList.pop();
592                                                }
593                                        }
594                                        // the user has clicked on an already sorted column
595                                        if (ts.isValueInArray(i, c.sortList)) {
596                                                // reverse the sorting direction
597                                                for (j = 0; j < c.sortList.length; j++) {
598                                                        s = c.sortList[j];
599                                                        o = c.$headers[s[0]];
600                                                        if (s[0] === i) {
601                                                                // o.count seems to be incorrect when compared to cell.count
602                                                                s[1] = o.order[cell.count];
603                                                                if (s[1] === 2) {
604                                                                        c.sortList.splice(j,1);
605                                                                        o.count = -1;
606                                                                }
607                                                        }
608                                                }
609                                        } else {
610                                                // add column to sort list array
611                                                o = cell.order[cell.count];
612                                                if (o < 2) {
613                                                        c.sortList.push([i, o]);
614                                                        // add other columns if header spans across multiple
615                                                        if (cell.colSpan > 1) {
616                                                                for (j = 1; j < cell.colSpan; j++) {
617                                                                        c.sortList.push([i + j, o]);
618                                                                }
619                                                        }
620                                                }
621                                        }
622                                }
623                                if (c.sortAppend !== null) {
624                                        a = c.sortAppend;
625                                        for (j = 0; j < a.length; j++) {
626                                                if (a[j][0] !== i) {
627                                                        c.sortList.push(a[j]);
628                                                }
629                                        }
630                                }
631                                // sortBegin event triggered immediately before the sort
632                                $this.trigger("sortBegin", table);
633                                // setTimeout needed so the processing icon shows up
634                                setTimeout(function(){
635                                        // set css for headers
636                                        setHeadersCss(table);
637                                        multisort(table);
638                                        appendToTable(table);
639                                }, 1);
640                        }
641
642                        // sort multiple columns
643                        function multisort(table) { /*jshint loopfunc:true */
644                                var i, k, num, col, colMax, cache, lc,
645                                        order, orgOrderCol, sortTime, sort, x, y,
646                                        dir = 0,
647                                        c = table.config,
648                                        cts = c.textSorter || '',
649                                        sortList = c.sortList,
650                                        l = sortList.length,
651                                        bl = table.tBodies.length;
652                                if (c.serverSideSorting || isEmptyObject(c.cache)) { // empty table - fixes #206/#346
653                                        return;
654                                }
655                                if (c.debug) { sortTime = new Date(); }
656                                for (k = 0; k < bl; k++) {
657                                        colMax = c.cache[k].colMax;
658                                        cache = c.cache[k].normalized;
659                                        lc = cache.length;
660                                        orgOrderCol = (cache && cache[0]) ? cache[0].length - 1 : 0;
661                                        cache.sort(function(a, b) {
662                                                // cache is undefined here in IE, so don't use it!
663                                                for (i = 0; i < l; i++) {
664                                                        col = sortList[i][0];
665                                                        order = sortList[i][1];
666                                                        // sort direction, true = asc, false = desc
667                                                        dir = order === 0;
668
669                                                        if (c.sortStable && a[col] === b[col] && l === 1) {
670                                                                return a[orgOrderCol] - b[orgOrderCol];
671                                                        }
672
673                                                        // fallback to natural sort since it is more robust
674                                                        num = /n/i.test(getCachedSortType(c.parsers, col));
675                                                        if (num && c.strings[col]) {
676                                                                // sort strings in numerical columns
677                                                                if (typeof (c.string[c.strings[col]]) === 'boolean') {
678                                                                        num = (dir ? 1 : -1) * (c.string[c.strings[col]] ? -1 : 1);
679                                                                } else {
680                                                                        num = (c.strings[col]) ? c.string[c.strings[col]] || 0 : 0;
681                                                                }
682                                                                // fall back to built-in numeric sort
683                                                                // var sort = $.tablesorter["sort" + s](table, a[c], b[c], c, colMax[c], dir);
684                                                                sort = c.numberSorter ? c.numberSorter(x[col], y[col], dir, colMax[col], table) :
685                                                                        ts[ 'sortNumeric' + (dir ? 'Asc' : 'Desc') ](a[col], b[col], num, colMax[col], col, table);
686                                                        } else {
687                                                                // set a & b depending on sort direction
688                                                                x = dir ? a : b;
689                                                                y = dir ? b : a;
690                                                                // text sort function
691                                                                if (typeof(cts) === 'function') {
692                                                                        // custom OVERALL text sorter
693                                                                        sort = cts(x[col], y[col], dir, col, table);
694                                                                } else if (typeof(cts) === 'object' && cts.hasOwnProperty(col)) {
695                                                                        // custom text sorter for a SPECIFIC COLUMN
696                                                                        sort = cts[col](x[col], y[col], dir, col, table);
697                                                                } else {
698                                                                        // fall back to natural sort
699                                                                        sort = ts[ 'sortNatural' + (dir ? 'Asc' : 'Desc') ](a[col], b[col], col, table, c);
700                                                                }
701                                                        }
702                                                        if (sort) { return sort; }
703                                                }
704                                                return a[orgOrderCol] - b[orgOrderCol];
705                                        });
706                                }
707                                if (c.debug) { benchmark("Sorting on " + sortList.toString() + " and dir " + order + " time", sortTime); }
708                        }
709
710                        function resortComplete($table, callback){
711                                var c = $table[0].config;
712                                if (c.pager && !c.pager.ajax) {
713                                        $table.trigger('updateComplete');
714                                }
715                                if (typeof callback === "function") {
716                                        callback($table[0]);
717                                }
718                        }
719
720                        function checkResort($table, flag, callback) {
721                                // don't try to resort if the table is still processing
722                                // this will catch spamming of the updateCell method
723                                if (flag !== false && !$table[0].isProcessing) {
724                                        $table.trigger("sorton", [$table[0].config.sortList, function(){
725                                                resortComplete($table, callback);
726                                        }]);
727                                } else {
728                                        resortComplete($table, callback);
729                                }
730                        }
731
732                        function bindEvents(table){
733                                var c = table.config,
734                                        $this = c.$table,
735                                        j, downTime;
736                                // apply event handling to headers
737                                c.$headers
738                                // http://stackoverflow.com/questions/5312849/jquery-find-self;
739                                .find(c.selectorSort).add( c.$headers.filter(c.selectorSort) )
740                                .unbind('mousedown.tablesorter mouseup.tablesorter sort.tablesorter keypress.tablesorter')
741                                .bind('mousedown.tablesorter mouseup.tablesorter sort.tablesorter keypress.tablesorter', function(e, external) {
742                                        // only recognize left clicks or enter
743                                        if ( ((e.which || e.button) !== 1 && !/sort|keypress/.test(e.type)) || (e.type === 'keypress' && e.which !== 13) ) {
744                                                return;
745                                        }
746                                        // ignore long clicks (prevents resizable widget from initializing a sort)
747                                        if (e.type === 'mouseup' && external !== true && (new Date().getTime() - downTime > 250)) { return; }
748                                        // set timer on mousedown
749                                        if (e.type === 'mousedown') {
750                                                downTime = new Date().getTime();
751                                                return e.target.tagName === "INPUT" ? '' : !c.cancelSelection;
752                                        }
753                                        if (c.delayInit && isEmptyObject(c.cache)) { buildCache(table); }
754                                        // jQuery v1.2.6 doesn't have closest()
755                                        var $cell = /TH|TD/.test(this.tagName) ? $(this) : $(this).parents('th, td').filter(':first'), cell = $cell[0];
756                                        if (!cell.sortDisabled) {
757                                                initSort(table, cell, e);
758                                        }
759                                });
760                                if (c.cancelSelection) {
761                                        // cancel selection
762                                        c.$headers
763                                                .attr('unselectable', 'on')
764                                                .bind('selectstart', false)
765                                                .css({
766                                                        'user-select': 'none',
767                                                        'MozUserSelect': 'none' // not needed for jQuery 1.8+
768                                                });
769                                }
770                                // apply easy methods that trigger bound events
771                                $this
772                                .unbind('sortReset update updateRows updateCell updateAll addRows sorton appendCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave '.split(' ').join('.tablesorter '))
773                                .bind("sortReset.tablesorter", function(e){
774                                        e.stopPropagation();
775                                        c.sortList = [];
776                                        setHeadersCss(table);
777                                        multisort(table);
778                                        appendToTable(table);
779                                })
780                                .bind("updateAll.tablesorter", function(e, resort, callback){
781                                        e.stopPropagation();
782                                        ts.refreshWidgets(table, true, true);
783                                        ts.restoreHeaders(table);
784                                        buildHeaders(table);
785                                        bindEvents(table);
786                                        commonUpdate(table, resort, callback);
787                                })
788                                .bind("update.tablesorter updateRows.tablesorter", function(e, resort, callback) {
789                                        e.stopPropagation();
790                                        // update sorting (if enabled/disabled)
791                                        updateHeader(table);
792                                        commonUpdate(table, resort, callback);
793                                })
794                                .bind("updateCell.tablesorter", function(e, cell, resort, callback) {
795                                        e.stopPropagation();
796                                        $this.find(c.selectorRemove).remove();
797                                        // get position from the dom
798                                        var l, row, icell,
799                                        $tb = $this.find('tbody'),
800                                        // update cache - format: function(s, table, cell, cellIndex)
801                                        // no closest in jQuery v1.2.6 - tbdy = $tb.index( $(cell).closest('tbody') ),$row = $(cell).closest('tr');
802                                        tbdy = $tb.index( $(cell).parents('tbody').filter(':first') ),
803                                        $row = $(cell).parents('tr').filter(':first');
804                                        cell = $(cell)[0]; // in case cell is a jQuery object
805                                        // tbody may not exist if update is initialized while tbody is removed for processing
806                                        if ($tb.length && tbdy >= 0) {
807                                                row = $tb.eq(tbdy).find('tr').index( $row );
808                                                icell = cell.cellIndex;
809                                                l = c.cache[tbdy].normalized[row].length - 1;
810                                                c.cache[tbdy].row[table.config.cache[tbdy].normalized[row][l]] = $row;
811                                                c.cache[tbdy].normalized[row][icell] = c.parsers[icell].format( getElementText(table, cell, icell), table, cell, icell );
812                                                checkResort($this, resort, callback);
813                                        }
814                                })
815                                .bind("addRows.tablesorter", function(e, $row, resort, callback) {
816                                        e.stopPropagation();
817                                        if (isEmptyObject(c.cache)) {
818                                                // empty table, do an update instead - fixes #450
819                                                updateHeader(table);
820                                                commonUpdate(table, resort, callback);
821                                        } else {
822                                                var i, rows = $row.filter('tr').length,
823                                                dat = [], l = $row[0].cells.length,
824                                                tbdy = $this.find('tbody').index( $row.parents('tbody').filter(':first') );
825                                                // fixes adding rows to an empty table - see issue #179
826                                                if (!c.parsers) {
827                                                        buildParserCache(table);
828                                                }
829                                                // add each row
830                                                for (i = 0; i < rows; i++) {
831                                                        // add each cell
832                                                        for (j = 0; j < l; j++) {
833                                                                dat[j] = c.parsers[j].format( getElementText(table, $row[i].cells[j], j), table, $row[i].cells[j], j );
834                                                        }
835                                                        // add the row index to the end
836                                                        dat.push(c.cache[tbdy].row.length);
837                                                        // update cache
838                                                        c.cache[tbdy].row.push([$row[i]]);
839                                                        c.cache[tbdy].normalized.push(dat);
840                                                        dat = [];
841                                                }
842                                                // resort using current settings
843                                                checkResort($this, resort, callback);
844                                        }
845                                })
846                                .bind("sorton.tablesorter", function(e, list, callback, init) {
847                                        var c = table.config;
848                                        e.stopPropagation();
849                                        $this.trigger("sortStart", this);
850                                        // update header count index
851                                        updateHeaderSortCount(table, list);
852                                        // set css for headers
853                                        setHeadersCss(table);
854                                        // fixes #346
855                                        if (c.delayInit && isEmptyObject(c.cache)) { buildCache(table); }
856                                        $this.trigger("sortBegin", this);
857                                        // sort the table and append it to the dom
858                                        multisort(table);
859                                        appendToTable(table, init);
860                                        if (typeof callback === "function") {
861                                                callback(table);
862                                        }
863                                })
864                                .bind("appendCache.tablesorter", function(e, callback, init) {
865                                        e.stopPropagation();
866                                        appendToTable(table, init);
867                                        if (typeof callback === "function") {
868                                                callback(table);
869                                        }
870                                })
871                                .bind("applyWidgetId.tablesorter", function(e, id) {
872                                        e.stopPropagation();
873                                        ts.getWidgetById(id).format(table, c, c.widgetOptions);
874                                })
875                                .bind("applyWidgets.tablesorter", function(e, init) {
876                                        e.stopPropagation();
877                                        // apply widgets
878                                        ts.applyWidget(table, init);
879                                })
880                                .bind("refreshWidgets.tablesorter", function(e, all, dontapply){
881                                        e.stopPropagation();
882                                        ts.refreshWidgets(table, all, dontapply);
883                                })
884                                .bind("destroy.tablesorter", function(e, c, cb){
885                                        e.stopPropagation();
886                                        ts.destroy(table, c, cb);
887                                });
888                        }
889
890                        /* public methods */
891                        ts.construct = function(settings) {
892                                return this.each(function() {
893                                        var table = this,
894                                                // merge & extend config options
895                                                c = $.extend(true, {}, ts.defaults, settings);
896                                        // create a table from data (build table widget)
897                                        if (!table.hasInitialized && ts.buildTable && this.tagName !== 'TABLE') {
898                                                // return the table (in case the original target is the table's container)
899                                                ts.buildTable(table, c);
900                                        }
901                                        ts.setup(table, c);
902                                });
903                        };
904
905                        ts.setup = function(table, c) {
906                                // if no thead or tbody, or tablesorter is already present, quit
907                                if (!table || !table.tHead || table.tBodies.length === 0 || table.hasInitialized === true) {
908                                        return c.debug ? log('stopping initialization! No table, thead, tbody or tablesorter has already been initialized') : '';
909                                }
910
911                                var k = '',
912                                        $this = $(table),
913                                        m = $.metadata;
914                                // initialization flag
915                                table.hasInitialized = false;
916                                // table is being processed flag
917                                table.isProcessing = true;
918                                // make sure to store the config object
919                                table.config = c;
920                                // save the settings where they read
921                                $.data(table, "tablesorter", c);
922                                if (c.debug) { $.data( table, 'startoveralltimer', new Date()); }
923
924                                // constants
925                                c.supportsTextContent = $('<span>x</span>')[0].textContent === 'x';
926                                // removing this in version 3 (only supports jQuery 1.7+)
927                                c.supportsDataObject = (function(version) {
928                                        version[0] = parseInt(version[0], 10);
929                                        return (version[0] > 1) || (version[0] === 1 && parseInt(version[1], 10) >= 4);
930                                })($.fn.jquery.split("."));
931                                // digit sort text location; keeping max+/- for backwards compatibility
932                                c.string = { 'max': 1, 'min': -1, 'max+': 1, 'max-': -1, 'zero': 0, 'none': 0, 'null': 0, 'top': true, 'bottom': false };
933                                // add table theme class only if there isn't already one there
934                                if (!/tablesorter\-/.test($this.attr('class'))) {
935                                        k = (c.theme !== '' ? ' tablesorter-' + c.theme : '');
936                                }
937                                c.$table = $this.addClass(ts.css.table + ' ' + c.tableClass + k);
938                                c.$tbodies = $this.children('tbody:not(.' + c.cssInfoBlock + ')');
939                                c.widgetInit = {}; // keep a list of initialized widgets
940                                // build headers
941                                buildHeaders(table);
942                                // fixate columns if the users supplies the fixedWidth option
943                                // do this after theme has been applied
944                                fixColumnWidth(table);
945                                // try to auto detect column type, and store in tables config
946                                buildParserCache(table);
947                                // build the cache for the tbody cells
948                                // delayInit will delay building the cache until the user starts a sort
949                                if (!c.delayInit) { buildCache(table); }
950                                // bind all header events and methods
951                                bindEvents(table);
952                                // get sort list from jQuery data or metadata
953                                // in jQuery < 1.4, an error occurs when calling $this.data()
954                                if (c.supportsDataObject && typeof $this.data().sortlist !== 'undefined') {
955                                        c.sortList = $this.data().sortlist;
956                                } else if (m && ($this.metadata() && $this.metadata().sortlist)) {
957                                        c.sortList = $this.metadata().sortlist;
958                                }
959                                // apply widget init code
960                                ts.applyWidget(table, true);
961                                // if user has supplied a sort list to constructor
962                                if (c.sortList.length > 0) {
963                                        $this.trigger("sorton", [c.sortList, {}, !c.initWidgets]);
964                                } else if (c.initWidgets) {
965                                        // apply widget format
966                                        ts.applyWidget(table);
967                                }
968
969                                // show processesing icon
970                                if (c.showProcessing) {
971                                        $this
972                                        .unbind('sortBegin.tablesorter sortEnd.tablesorter')
973                                        .bind('sortBegin.tablesorter sortEnd.tablesorter', function(e) {
974                                                ts.isProcessing(table, e.type === 'sortBegin');
975                                        });
976                                }
977
978                                // initialized
979                                table.hasInitialized = true;
980                                table.isProcessing = false;
981                                if (c.debug) {
982                                        ts.benchmark("Overall initialization time", $.data( table, 'startoveralltimer'));
983                                }
984                                $this.trigger('tablesorter-initialized', table);
985                                if (typeof c.initialized === 'function') { c.initialized(table); }
986                        };
987
988                        // *** Process table ***
989                        // add processing indicator
990                        ts.isProcessing = function(table, toggle, $ths) {
991                                table = $(table);
992                                var c = table[0].config,
993                                        // default to all headers
994                                        $h = $ths || table.find('.' + ts.css.header);
995                                if (toggle) {
996                                        if (c.sortList.length > 0) {
997                                                // get headers from the sortList
998                                                $h = $h.filter(function(){
999                                                        // get data-column from attr to keep  compatibility with jQuery 1.2.6
1000                                                        return this.sortDisabled ? false : ts.isValueInArray( parseFloat($(this).attr('data-column')), c.sortList);
1001                                                });
1002                                        }
1003                                        $h.addClass(ts.css.processing + ' ' + c.cssProcessing);
1004                                } else {
1005                                        $h.removeClass(ts.css.processing + ' ' + c.cssProcessing);
1006                                }
1007                        };
1008
1009                        // detach tbody but save the position
1010                        // don't use tbody because there are portions that look for a tbody index (updateCell)
1011                        ts.processTbody = function(table, $tb, getIt){
1012                                var holdr;
1013                                if (getIt) {
1014                                        table.isProcessing = true;
1015                                        $tb.before('<span class="tablesorter-savemyplace"/>');
1016                                        holdr = ($.fn.detach) ? $tb.detach() : $tb.remove();
1017                                        return holdr;
1018                                }
1019                                holdr = $(table).find('span.tablesorter-savemyplace');
1020                                $tb.insertAfter( holdr );
1021                                holdr.remove();
1022                                table.isProcessing = false;
1023                        };
1024
1025                        ts.clearTableBody = function(table) {
1026                                $(table)[0].config.$tbodies.empty();
1027                        };
1028
1029                        // restore headers
1030                        ts.restoreHeaders = function(table){
1031                                var c = table.config;
1032                                // don't use c.$headers here in case header cells were swapped
1033                                c.$table.find(c.selectorHeaders).each(function(i){
1034                                        // only restore header cells if it is wrapped
1035                                        // because this is also used by the updateAll method
1036                                        if ($(this).find('.tablesorter-header-inner').length){
1037                                                $(this).html( c.headerContent[i] );
1038                                        }
1039                                });
1040                        };
1041
1042                        ts.destroy = function(table, removeClasses, callback){
1043                                table = $(table)[0];
1044                                if (!table.hasInitialized) { return; }
1045                                // remove all widgets
1046                                ts.refreshWidgets(table, true, true);
1047                                var $t = $(table), c = table.config,
1048                                $h = $t.find('thead:first'),
1049                                $r = $h.find('tr.' + ts.css.headerRow).removeClass(ts.css.headerRow + ' ' + c.cssHeaderRow),
1050                                $f = $t.find('tfoot:first > tr').children('th, td');
1051                                // remove widget added rows, just in case
1052                                $h.find('tr').not($r).remove();
1053                                // disable tablesorter
1054                                $t
1055                                        .removeData('tablesorter')
1056                                        .unbind('sortReset update updateAll updateRows updateCell addRows sorton appendCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave keypress sortBegin sortEnd '.split(' ').join('.tablesorter '));
1057                                c.$headers.add($f)
1058                                        .removeClass( [ts.css.header, c.cssHeader, c.cssAsc, c.cssDesc, ts.css.sortAsc, ts.css.sortDesc].join(' ') )
1059                                        .removeAttr('data-column');
1060                                $r.find(c.selectorSort).unbind('mousedown.tablesorter mouseup.tablesorter keypress.tablesorter');
1061                                ts.restoreHeaders(table);
1062                                if (removeClasses !== false) {
1063                                        $t.removeClass(ts.css.table + ' ' + c.tableClass + ' tablesorter-' + c.theme);
1064                                }
1065                                // clear flag in case the plugin is initialized again
1066                                table.hasInitialized = false;
1067                                if (typeof callback === 'function') {
1068                                        callback(table);
1069                                }
1070                        };
1071
1072                        // *** sort functions ***
1073                        // regex used in natural sort
1074                        ts.regex = {
1075                                chunk : /(^([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)?$|^0x[0-9a-f]+$|\d+)/gi, // chunk/tokenize numbers & letters
1076                                hex: /^0x[0-9a-f]+$/i // hex
1077                        };
1078
1079                        // Natural sort - https://github.com/overset/javascript-natural-sort (date sorting removed)
1080                        ts.sortNatural = function(a, b) {
1081                                if (a === b) { return 0; }
1082                                var xN, xD, yN, yD, xF, yF, i, mx,
1083                                        r = ts.regex;
1084                                // first try and sort Hex codes
1085                                if (r.hex.test(b)) {
1086                                        xD = parseInt(a.match(r.hex), 16);
1087                                        yD = parseInt(b.match(r.hex), 16);
1088                                        if ( xD < yD ) { return -1; }
1089                                        if ( xD > yD ) { return 1; }
1090                                }
1091
1092                                // chunk/tokenize
1093                                xN = a.replace(r.chunk, '\\0$1\\0').replace(/\\0$/, '').replace(/^\\0/, '').split('\\0');
1094                                yN = b.replace(r.chunk, '\\0$1\\0').replace(/\\0$/, '').replace(/^\\0/, '').split('\\0');
1095                                mx = Math.max(xN.length, yN.length);
1096                                // natural sorting through split numeric strings and default strings
1097                                for (i = 0; i < mx; i++) {
1098                                        // find floats not starting with '0', string or 0 if not defined
1099                                        xF = isNaN(xN[i]) ? xN[i] || 0 : parseFloat(xN[i]) || 0;
1100                                        yF = isNaN(yN[i]) ? yN[i] || 0 : parseFloat(yN[i]) || 0;
1101                                        // handle numeric vs string comparison - number < string - (Kyle Adams)
1102                                        if (isNaN(xF) !== isNaN(yF)) { return (isNaN(xF)) ? 1 : -1; }
1103                                        // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2'
1104                                        if (typeof xF !== typeof yF) {
1105                                                xF += '';
1106                                                yF += '';
1107                                        }
1108                                        if (xF < yF) { return -1; }
1109                                        if (xF > yF) { return 1; }
1110                                }
1111                                return 0;
1112                        };
1113
1114                        ts.sortNaturalAsc = function(a, b, col, table, c) {
1115                                if (a === b) { return 0; }
1116                                var e = c.string[ (c.empties[col] || c.emptyTo ) ];
1117                                if (a === '' && e !== 0) { return typeof e === 'boolean' ? (e ? -1 : 1) : -e || -1; }
1118                                if (b === '' && e !== 0) { return typeof e === 'boolean' ? (e ? 1 : -1) : e || 1; }
1119                                return ts.sortNatural(a, b);
1120                        };
1121
1122                        ts.sortNaturalDesc = function(a, b, col, table, c) {
1123                                if (a === b) { return 0; }
1124                                var e = c.string[ (c.empties[col] || c.emptyTo ) ];
1125                                if (a === '' && e !== 0) { return typeof e === 'boolean' ? (e ? -1 : 1) : e || 1; }
1126                                if (b === '' && e !== 0) { return typeof e === 'boolean' ? (e ? 1 : -1) : -e || -1; }
1127                                return ts.sortNatural(b, a);
1128                        };
1129
1130                        // basic alphabetical sort
1131                        ts.sortText = function(a, b) {
1132                                return a > b ? 1 : (a < b ? -1 : 0);
1133                        };
1134
1135                        // return text string value by adding up ascii value
1136                        // so the text is somewhat sorted when using a digital sort
1137                        // this is NOT an alphanumeric sort
1138                        ts.getTextValue = function(a, num, mx) {
1139                                if (mx) {
1140                                        // make sure the text value is greater than the max numerical value (mx)
1141                                        var i, l = a ? a.length : 0, n = mx + num;
1142                                        for (i = 0; i < l; i++) {
1143                                                n += a.charCodeAt(i);
1144                                        }
1145                                        return num * n;
1146                                }
1147                                return 0;
1148                        };
1149
1150                        ts.sortNumericAsc = function(a, b, num, mx, col, table) {
1151                                if (a === b) { return 0; }
1152                                var c = table.config,
1153                                        e = c.string[ (c.empties[col] || c.emptyTo ) ];
1154                                if (a === '' && e !== 0) { return typeof e === 'boolean' ? (e ? -1 : 1) : -e || -1; }
1155                                if (b === '' && e !== 0) { return typeof e === 'boolean' ? (e ? 1 : -1) : e || 1; }
1156                                if (isNaN(a)) { a = ts.getTextValue(a, num, mx); }
1157                                if (isNaN(b)) { b = ts.getTextValue(b, num, mx); }
1158                                return a - b;
1159                        };
1160
1161                        ts.sortNumericDesc = function(a, b, num, mx, col, table) {
1162                                if (a === b) { return 0; }
1163                                var c = table.config,
1164                                        e = c.string[ (c.empties[col] || c.emptyTo ) ];
1165                                if (a === '' && e !== 0) { return typeof e === 'boolean' ? (e ? -1 : 1) : e || 1; }
1166                                if (b === '' && e !== 0) { return typeof e === 'boolean' ? (e ? 1 : -1) : -e || -1; }
1167                                if (isNaN(a)) { a = ts.getTextValue(a, num, mx); }
1168                                if (isNaN(b)) { b = ts.getTextValue(b, num, mx); }
1169                                return b - a;
1170                        };
1171
1172                        ts.sortNumeric = function(a, b) {
1173                                return a - b;
1174                        };
1175
1176                        // used when replacing accented characters during sorting
1177                        ts.characterEquivalents = {
1178                                "a" : "\u00e1\u00e0\u00e2\u00e3\u00e4\u0105\u00e5", // áàâãäąå
1179                                "A" : "\u00c1\u00c0\u00c2\u00c3\u00c4\u0104\u00c5", // ÁÀÂÃÄĄÅ
1180                                "c" : "\u00e7\u0107\u010d", // çćč
1181                                "C" : "\u00c7\u0106\u010c", // ÇĆČ
1182                                "e" : "\u00e9\u00e8\u00ea\u00eb\u011b\u0119", // éèêëěę
1183                                "E" : "\u00c9\u00c8\u00ca\u00cb\u011a\u0118", // ÉÈÊËĚĘ
1184                                "i" : "\u00ed\u00ec\u0130\u00ee\u00ef\u0131", // íìİîïı
1185                                "I" : "\u00cd\u00cc\u0130\u00ce\u00cf", // ÍÌİÎÏ
1186                                "o" : "\u00f3\u00f2\u00f4\u00f5\u00f6", // óòôõö
1187                                "O" : "\u00d3\u00d2\u00d4\u00d5\u00d6", // ÓÒÔÕÖ
1188                                "ss": "\u00df", // ß (s sharp)
1189                                "SS": "\u1e9e", // ẞ (Capital sharp s)
1190                                "u" : "\u00fa\u00f9\u00fb\u00fc\u016f", // úùûüů
1191                                "U" : "\u00da\u00d9\u00db\u00dc\u016e" // ÚÙÛÜŮ
1192                        };
1193                        ts.replaceAccents = function(s) {
1194                                var a, acc = '[', eq = ts.characterEquivalents;
1195                                if (!ts.characterRegex) {
1196                                        ts.characterRegexArray = {};
1197                                        for (a in eq) {
1198                                                if (typeof a === 'string') {
1199                                                        acc += eq[a];
1200                                                        ts.characterRegexArray[a] = new RegExp('[' + eq[a] + ']', 'g');
1201                                                }
1202                                        }
1203                                        ts.characterRegex = new RegExp(acc + ']');
1204                                }
1205                                if (ts.characterRegex.test(s)) {
1206                                        for (a in eq) {
1207                                                if (typeof a === 'string') {
1208                                                        s = s.replace( ts.characterRegexArray[a], a );
1209                                                }
1210                                        }
1211                                }
1212                                return s;
1213                        };
1214
1215                        // *** utilities ***
1216                        ts.isValueInArray = function(v, a) {
1217                                var i, l = a.length;
1218                                for (i = 0; i < l; i++) {
1219                                        if (a[i][0] === v) {
1220                                                return true;
1221                                        }
1222                                }
1223                                return false;
1224                        };
1225
1226                        ts.addParser = function(parser) {
1227                                var i, l = ts.parsers.length, a = true;
1228                                for (i = 0; i < l; i++) {
1229                                        if (ts.parsers[i].id.toLowerCase() === parser.id.toLowerCase()) {
1230                                                a = false;
1231                                        }
1232                                }
1233                                if (a) {
1234                                        ts.parsers.push(parser);
1235                                }
1236                        };
1237
1238                        ts.getParserById = function(name) {
1239                                var i, l = ts.parsers.length;
1240                                for (i = 0; i < l; i++) {
1241                                        if (ts.parsers[i].id.toLowerCase() === (name.toString()).toLowerCase()) {
1242                                                return ts.parsers[i];
1243                                        }
1244                                }
1245                                return false;
1246                        };
1247
1248                        ts.addWidget = function(widget) {
1249                                ts.widgets.push(widget);
1250                        };
1251
1252                        ts.getWidgetById = function(name) {
1253                                var i, w, l = ts.widgets.length;
1254                                for (i = 0; i < l; i++) {
1255                                        w = ts.widgets[i];
1256                                        if (w && w.hasOwnProperty('id') && w.id.toLowerCase() === name.toLowerCase()) {
1257                                                return w;
1258                                        }
1259                                }
1260                        };
1261
1262                        ts.applyWidget = function(table, init) {
1263                                table = $(table)[0]; // in case this is called externally
1264                                var c = table.config,
1265                                        wo = c.widgetOptions,
1266                                        widgets = [],
1267                                        time, w, wd;
1268                                if (c.debug) { time = new Date(); }
1269                                if (c.widgets.length) {
1270                                        // ensure unique widget ids
1271                                        c.widgets = $.grep(c.widgets, function(v, k){
1272                                                return $.inArray(v, c.widgets) === k;
1273                                        });
1274                                        // build widget array & add priority as needed
1275                                        $.each(c.widgets || [], function(i,n){
1276                                                wd = ts.getWidgetById(n);
1277                                                if (wd && wd.id) {
1278                                                        // set priority to 10 if not defined
1279                                                        if (!wd.priority) { wd.priority = 10; }
1280                                                        widgets[i] = wd;
1281                                                }
1282                                        });
1283                                        // sort widgets by priority
1284                                        widgets.sort(function(a, b){
1285                                                return a.priority < b.priority ? -1 : a.priority === b.priority ? 0 : 1;
1286                                        });
1287                                        // add/update selected widgets
1288                                        $.each(widgets, function(i,w){
1289                                                if (w) {
1290                                                        if (init || !(c.widgetInit[w.id])) {
1291                                                                if (w.hasOwnProperty('options')) {
1292                                                                        wo = table.config.widgetOptions = $.extend( true, {}, w.options, wo );
1293                                                                }
1294                                                                if (w.hasOwnProperty('init')) {
1295                                                                        w.init(table, w, c, wo);
1296                                                                }
1297                                                                c.widgetInit[w.id] = true;
1298                                                        }
1299                                                        if (!init && w.hasOwnProperty('format')) {
1300                                                                w.format(table, c, wo, false);
1301                                                        }
1302                                                }
1303                                        });
1304                                }
1305                                if (c.debug) {
1306                                        w = c.widgets.length;
1307                                        benchmark("Completed " + (init === true ? "initializing " : "applying ") + w + " widget" + (w !== 1 ? "s" : ""), time);
1308                                }
1309                        };
1310
1311                        ts.refreshWidgets = function(table, doAll, dontapply) {
1312                                table = $(table)[0]; // see issue #243
1313                                var i, c = table.config,
1314                                        cw = c.widgets,
1315                                        w = ts.widgets, l = w.length;
1316                                // remove previous widgets
1317                                for (i = 0; i < l; i++){
1318                                        if ( w[i] && w[i].id && (doAll || $.inArray( w[i].id, cw ) < 0) ) {
1319                                                if (c.debug) { log( 'Refeshing widgets: Removing ' + w[i].id  ); }
1320                                                // only remove widgets that have been initialized - fixes #442
1321                                                if (w[i].hasOwnProperty('remove') && c.widgetInit[w[i].id]) {
1322                                                        w[i].remove(table, c, c.widgetOptions);
1323                                                        c.widgetInit[w[i].id] = false;
1324                                                }
1325                                        }
1326                                }
1327                                if (dontapply !== true) {
1328                                        ts.applyWidget(table, doAll);
1329                                }
1330                        };
1331
1332                        // get sorter, string, empty, etc options for each column from
1333                        // jQuery data, metadata, header option or header class name ("sorter-false")
1334                        // priority = jQuery data > meta > headers option > header class name
1335                        ts.getData = function(h, ch, key) {
1336                                var val = '', $h = $(h), m, cl;
1337                                if (!$h.length) { return ''; }
1338                                m = $.metadata ? $h.metadata() : false;
1339                                cl = ' ' + ($h.attr('class') || '');
1340                                if (typeof $h.data(key) !== 'undefined' || typeof $h.data(key.toLowerCase()) !== 'undefined'){
1341                                        // "data-lockedOrder" is assigned to "lockedorder"; but "data-locked-order" is assigned to "lockedOrder"
1342                                        // "data-sort-initial-order" is assigned to "sortInitialOrder"
1343                                        val += $h.data(key) || $h.data(key.toLowerCase());
1344                                } else if (m && typeof m[key] !== 'undefined') {
1345                                        val += m[key];
1346                                } else if (ch && typeof ch[key] !== 'undefined') {
1347                                        val += ch[key];
1348                                } else if (cl !== ' ' && cl.match(' ' + key + '-')) {
1349                                        // include sorter class name "sorter-text", etc; now works with "sorter-my-custom-parser"
1350                                        val = cl.match( new RegExp('\\s' + key + '-([\\w-]+)') )[1] || '';
1351                                }
1352                                return $.trim(val);
1353                        };
1354
1355                        ts.formatFloat = function(s, table) {
1356                                if (typeof s !== 'string' || s === '') { return s; }
1357                                // allow using formatFloat without a table; defaults to US number format
1358                                var i,
1359                                        t = table && table.config ? table.config.usNumberFormat !== false :
1360                                                typeof table !== "undefined" ? table : true;
1361                                if (t) {
1362                                        // US Format - 1,234,567.89 -> 1234567.89
1363                                        s = s.replace(/,/g,'');
1364                                } else {
1365                                        // German Format = 1.234.567,89 -> 1234567.89
1366                                        // French Format = 1 234 567,89 -> 1234567.89
1367                                        s = s.replace(/[\s|\.]/g,'').replace(/,/g,'.');
1368                                }
1369                                if(/^\s*\([.\d]+\)/.test(s)) {
1370                                        // make (#) into a negative number -> (10) = -10
1371                                        s = s.replace(/^\s*\(([.\d]+)\)/, '-$1');
1372                                }
1373                                i = parseFloat(s);
1374                                // return the text instead of zero
1375                                return isNaN(i) ? $.trim(s) : i;
1376                        };
1377
1378                        ts.isDigit = function(s) {
1379                                // replace all unwanted chars and match
1380                                return isNaN(s) ? (/^[\-+(]?\d+[)]?$/).test(s.toString().replace(/[,.'"\s]/g, '')) : true;
1381                        };
1382
1383                }()
1384        });
1385
1386        // make shortcut
1387        var ts = $.tablesorter;
1388
1389        // extend plugin scope
1390        $.fn.extend({
1391                tablesorter: ts.construct
1392        });
1393
1394        // add default parsers
1395        ts.addParser({
1396                id: "text",
1397                is: function() {
1398                        return true;
1399                },
1400                format: function(s, table) {
1401                        var c = table.config;
1402                        if (s) {
1403                                s = $.trim( c.ignoreCase ? s.toLocaleLowerCase() : s );
1404                                s = c.sortLocaleCompare ? ts.replaceAccents(s) : s;
1405                        }
1406                        return s;
1407                },
1408                type: "text"
1409        });
1410
1411        ts.addParser({
1412                id: "digit",
1413                is: function(s) {
1414                        return ts.isDigit(s);
1415                },
1416                format: function(s, table) {
1417                        var n = ts.formatFloat((s || '').replace(/[^\w,. \-()]/g, ""), table);
1418                        return s && typeof n === 'number' ? n : s ? $.trim( s && table.config.ignoreCase ? s.toLocaleLowerCase() : s ) : s;
1419                },
1420                type: "numeric"
1421        });
1422
1423        ts.addParser({
1424                id: "currency",
1425                is: function(s) {
1426                        return (/^\(?\d+[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]|[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]\d+\)?$/).test((s || '').replace(/[,. ]/g,'')); // £$€¤¥¢
1427                },
1428                format: function(s, table) {
1429                        var n = ts.formatFloat((s || '').replace(/[^\w,. \-()]/g, ""), table);
1430                        return s && typeof n === 'number' ? n : s ? $.trim( s && table.config.ignoreCase ? s.toLocaleLowerCase() : s ) : s;
1431                },
1432                type: "numeric"
1433        });
1434
1435        ts.addParser({
1436                id: "ipAddress",
1437                is: function(s) {
1438                        return (/^\d{1,3}[\.]\d{1,3}[\.]\d{1,3}[\.]\d{1,3}$/).test(s);
1439                },
1440                format: function(s, table) {
1441                        var i, a = s ? s.split(".") : '',
1442                        r = "",
1443                        l = a.length;
1444                        for (i = 0; i < l; i++) {
1445                                r += ("00" + a[i]).slice(-3);
1446                        }
1447                        return s ? ts.formatFloat(r, table) : s;
1448                },
1449                type: "numeric"
1450        });
1451
1452        ts.addParser({
1453                id: "url",
1454                is: function(s) {
1455                        return (/^(https?|ftp|file):\/\//).test(s);
1456                },
1457                format: function(s) {
1458                        return s ? $.trim(s.replace(/(https?|ftp|file):\/\//, '')) : s;
1459                },
1460                type: "text"
1461        });
1462
1463        ts.addParser({
1464                id: "isoDate",
1465                is: function(s) {
1466                        return (/^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}/).test(s);
1467                },
1468                format: function(s, table) {
1469                        return s ? ts.formatFloat((s !== "") ? (new Date(s.replace(/-/g, "/")).getTime() || "") : "", table) : s;
1470                },
1471                type: "numeric"
1472        });
1473
1474        ts.addParser({
1475                id: "percent",
1476                is: function(s) {
1477                        return (/(\d\s*?%|%\s*?\d)/).test(s) && s.length < 15;
1478                },
1479                format: function(s, table) {
1480                        return s ? ts.formatFloat(s.replace(/%/g, ""), table) : s;
1481                },
1482                type: "numeric"
1483        });
1484
1485        ts.addParser({
1486                id: "usLongDate",
1487                is: function(s) {
1488                        // two digit years are not allowed cross-browser
1489                        // Jan 01, 2013 12:34:56 PM or 01 Jan 2013
1490                        return (/^[A-Z]{3,10}\.?\s+\d{1,2},?\s+(\d{4})(\s+\d{1,2}:\d{2}(:\d{2})?(\s+[AP]M)?)?$/i).test(s) || (/^\d{1,2}\s+[A-Z]{3,10}\s+\d{4}/i).test(s);
1491                },
1492                format: function(s, table) {
1493                        return s ? ts.formatFloat( (new Date(s.replace(/(\S)([AP]M)$/i, "$1 $2")).getTime() || ''), table) : s;
1494                },
1495                type: "numeric"
1496        });
1497
1498        ts.addParser({
1499                id: "shortDate", // "mmddyyyy", "ddmmyyyy" or "yyyymmdd"
1500                is: function(s) {
1501                        // testing for ##-##-#### or ####-##-##, so it's not perfect; time can be included
1502                        return (/(^\d{1,2}[\/\s]\d{1,2}[\/\s]\d{4})|(^\d{4}[\/\s]\d{1,2}[\/\s]\d{1,2})/).test((s || '').replace(/\s+/g," ").replace(/[\-.,]/g, "/"));
1503                },
1504                format: function(s, table, cell, cellIndex) {
1505                        if (s) {
1506                                var c = table.config, ci = c.headerList[cellIndex],
1507                                format = ci.dateFormat || ts.getData( ci, c.headers[cellIndex], 'dateFormat') || c.dateFormat;
1508                                s = s.replace(/\s+/g," ").replace(/[\-.,]/g, "/"); // escaped - because JSHint in Firefox was showing it as an error
1509                                if (format === "mmddyyyy") {
1510                                        s = s.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, "$3/$1/$2");
1511                                } else if (format === "ddmmyyyy") {
1512                                        s = s.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, "$3/$2/$1");
1513                                } else if (format === "yyyymmdd") {
1514                                        s = s.replace(/(\d{4})[\/\s](\d{1,2})[\/\s](\d{1,2})/, "$1/$2/$3");
1515                                }
1516                        }
1517                        return s ? ts.formatFloat( (new Date(s).getTime() || ''), table) : s;
1518                },
1519                type: "numeric"
1520        });
1521
1522        ts.addParser({
1523                id: "time",
1524                is: function(s) {
1525                        return (/^(([0-2]?\d:[0-5]\d)|([0-1]?\d:[0-5]\d\s?([AP]M)))$/i).test(s);
1526                },
1527                format: function(s, table) {
1528                        return s ? ts.formatFloat( (new Date("2000/01/01 " + s.replace(/(\S)([AP]M)$/i, "$1 $2")).getTime() || ""), table) : s;
1529                },
1530                type: "numeric"
1531        });
1532
1533        ts.addParser({
1534                id: "metadata",
1535                is: function() {
1536                        return false;
1537                },
1538                format: function(s, table, cell) {
1539                        var c = table.config,
1540                        p = (!c.parserMetadataName) ? 'sortValue' : c.parserMetadataName;
1541                        return $(cell).metadata()[p];
1542                },
1543                type: "numeric"
1544        });
1545
1546        // add default widgets
1547        ts.addWidget({
1548                id: "zebra",
1549                priority: 90,
1550                format: function(table, c, wo) {
1551                        var $tb, $tv, $tr, row, even, time, k, l,
1552                        child = new RegExp(c.cssChildRow, 'i'),
1553                        b = c.$tbodies;
1554                        if (c.debug) {
1555                                time = new Date();
1556                        }
1557                        for (k = 0; k < b.length; k++ ) {
1558                                // loop through the visible rows
1559                                $tb = b.eq(k);
1560                                l = $tb.children('tr').length;
1561                                if (l > 1) {
1562                                        row = 0;
1563                                        $tv = $tb.children('tr:visible').not(c.selectorRemove);
1564                                        // revered back to using jQuery each - strangely it's the fastest method
1565                                        /*jshint loopfunc:true */
1566                                        $tv.each(function(){
1567                                                $tr = $(this);
1568                                                // style children rows the same way the parent row was styled
1569                                                if (!child.test(this.className)) { row++; }
1570                                                even = (row % 2 === 0);
1571                                                $tr.removeClass(wo.zebra[even ? 1 : 0]).addClass(wo.zebra[even ? 0 : 1]);
1572                                        });
1573                                }
1574                        }
1575                        if (c.debug) {
1576                                ts.benchmark("Applying Zebra widget", time);
1577                        }
1578                },
1579                remove: function(table, c, wo){
1580                        var k, $tb,
1581                                b = c.$tbodies,
1582                                rmv = (wo.zebra || [ "even", "odd" ]).join(' ');
1583                        for (k = 0; k < b.length; k++ ){
1584                                $tb = $.tablesorter.processTbody(table, b.eq(k), true); // remove tbody
1585                                $tb.children().removeClass(rmv);
1586                                $.tablesorter.processTbody(table, $tb, false); // restore tbody
1587                        }
1588                }
1589        });
1590
1591})(jQuery);
Note: See TracBrowser for help on using the repository browser.